ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 엘레강트 오브젝트 - 주 생성자 재활용
    책책책 책을 읽읍시다/프로그래밍 2023. 8. 1. 00:07

     응집도가 높고 견고한 클래스에는 적은 수의 메서드와 상대적으로 더 많은 수의 생성자가 존재한다. 생성자의 개수가 더 많을수록 클래스는 더 개선되고, 사용자 입장에서 클래스를 더 편하게 사용할 수 있다. 아래와 같이 Cash란 클래스가 있고 다양한 방법으로 Cash 인스턴스를 생성하고 싶은 경우 다음과 같이 여러 종류의 생성자를 활용한다.

    public class Cash {
        private int dollars;
    
        public Cash(int dollars) {
            this.dollars = dollars;
        }
    
        public String usd() {
            return String.format("$ %d", this.dollars);
        }
    }
    new Cash(30);
    new Cash("$29.95");
    new Cash(29.95d);
    new Cash(29.95f);
    new Cash(29.95, "USD");

     위 문장들은 모두 동일한 객체를 생성한다. 여기에서 동일하다는 말은 생성된 객체들이 동일하게 행동한다는 뜻이다. 생성자가 많아질수록 클라이언트가 클래스를 더 유연하게 사용할 수 있게 된다. 하지만 메서드가 많아질수록 클래스를 사용하기는 더 어려워진다. 메서드가 많아지면 클래스의 초점이 흐려지고, 단일 책임 원칙(Single Responsibility Principle)을 위반하기 떄문이다.

     Cash 클래스 사용자는 텍스트로 표현된 숫자를 다룰 경우에도 별도의 변환이나 파싱을 할 필요가 없기 때문에 추가적인 유연성을 얻게 된다. Cash 클래스가 이런 작업들을 대신해서 처리해주고 있다. 텍스트 형식의 값을 사용해야 하면 여기 텍스트를 처리하는 생성자가 있다. doulbe 타입의 수를 전달해야 하면 double 타입의 인자를 받는 또 다른 생성자가 있다. 이런 유연성 덕분에 코드를 더 적게 작성해도 되고 중복 코드 역시 줄어든다. 반대로 퍼블릭 메서드를 많이 제공하는 방ㅎ식은 유연성을 저하시키기 때문에 좋지 않은 생각이다.

     

     생성자의 주된 작업은 제공된 인자를 사용해서 캡슐화하고 있는 프로퍼티를 초기화하는 일이다. 이런 초기화 로직을 단 하나의 생성자에만 위치시키고 '주(primary)' 생성자라고 부르기를 권장하며, 다음 예제처럼 '부(secondary)' 생성자라고 부르는 다른 생성자들이 이 주 생성자를 호출하도록 만들기 바란다.

    public class Cash {
        private int dollars;
        
        public Cash(float dollars) {
            this((int) dollars);
        }
    
        public Cash(String dollars) {
            this(Cash.parse(dollars));
        }
    
        public Cash(int dollars) {
            this.dollars = dollars;
        }
    }

     이 예제에서는 주 생성자가 모든 부 생성자 뒤에 위치되어있는데, 주된 이유는 유지보수성 때문이다. 반년 전에 만들어 두었던 클래스에 정의된 10개의 생성자들을 하나하나 읽어가면서 주 생성자가 어떤 것인지 찾고 싶지는 않다. 마지막에 정의된 생성자로 곧장 스크롤을 내렸을 때 그곳에 항상 주 생성자가 있다면 유지보수하기에 더 편하다.

    '하나의 주 생성자와 다수의 부 생성자(one primary, many secondary)' 원칙의 핵심은 중복 코드를 방지하고 설계를 더 간결하게 만들기 때문에 유지보수성이 향상된다는 점이다. 이 원칙을 따르지 않는 코드와 비교해보자.

    public class Cash {
        private int dollars;
    
        public Cash(float dollars) { // 틀렸다!
            this.dollars = (int) dollars;
        }
    
        public Cash(String dollars) { // 틀렸다!
            this.dollars = Cash.parse(dollars);
        }
    
        public Cash(int dollars) {
            this.dollars = dollars;
        }
    }

     여기에서 dollars의 값이 항상 양슈여야 한다고 가정하겠다. 이를 보장하기 위해서는 서로 다른 세 곳의 생성자 안에 일일이 유효성 검사 로직을 작성해야 합니다. 이에 반해 하나의 주 생성자와 두 개의 부 생성자를 구현한 첫 번째 예제에서는 유효성 검사 로직을 한 곳에만 추가하면 된다.

     

     불행하게도 모든 '객체지향' 언어들이 메서드 오버로딩(method overloading)을 지원하는 것은 아니다. 메서드 오버로딩을 지원하지 않는 대표적인 언어로는 Ruby와 PHP가 있다. 메서드 오버로딩은 객체지향 프로그래밍에서 반드시 제공되어야 하는 아주 중요한 기능이다. 메서드 오버로딩을 사용하면 메서드를 비즈니스 언어와 유사하게 만들 수 있기 때문에 코드의 가독성을 극적으로 향상시킬 수 있다. 예를 들어, content(File)와 contentInCharset(File, Charset)이라는 두 개의 이름 대신 content(File)와 content(File, Charset)이라는 이름을 사용하면 코드가 훨씬 더 간결해진다.

     

    회고


     메서드 오버로딩은 점층적 생성자 패턴(null을 파라미터로 넘기기 때문에 저자는 싫어할듯)으로도 활용할 수 있고, 다른 public 메서드에 적용하면 짧은 길이의 메서드명 + 파라미터 조합으로 풍부한 표현을 가능하게 해준다. 후자의 이점이 강력해서 개인적으로도 좋아하는 기능이다.

     String을 예로 들면 valueOf라는 한개의 오퍼레이션 이름으로 여러 개의 파라미터를 받아 String 타입을 출력해준다.

    String.valueOf

     만약 valueOfInteger, valueOfDouble과 같았으면 어땠을까? 메서드명이 다소 장황해지고, 클라이언트에서 valueOf안에 들어갈 타입이 바뀐다면 호출하는 메서드도 같이 바꿔 주어야할 것이다. float 타입의 변수 x가 있는데, 숫자 범위가 좀 커져서 double 타입으로 바뀌어야 한다고 치자. 오버로딩 버전이라면 String s = valueOf(x); 코드를 그대로 두면되지만, 파라미터 타입별로 따로 정의했다면 valueOfFloat(x)를 valueOfDouble(x)로 수정해주어야 한다. 누가봐도 오버로딩된 방식이 편하다.

     

     Spring Data JPA 환경에서 JpaRepository와 CustomRepository에서의 메서드 명명방식이 충돌날 때가 있다. 아래와 같이 Member라는 엔티티와 이에 대한 영속화를 담당하는 MemberRepository가 있다고 가정해보.

    public class Member {
    
        @Id @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        @Column(name = "username")
        private String username;
        @Column(name = "age")
        private int age;
    
        public Member(String username, int age) {
            this.username = username;
            this.age = age;
        }
    }
    
    public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
        List<Member> findByUsername(String username);
    }

     findByUsername은 Spring Data JPA의 쿼리메서드 규약에 따라 작성한 메서드명이다. 저렇게 CRUD 예약어인 find와 WHERE 조건을 명시하는 By 및 엔티티 속성명을 순서대로 조합해야 SELECT member_id, username, age FROM member WHERE username = ? 이라는 쿼리가 작성되니 쿼리메서드 기능을 사용하려면 무조건 따라야 한다. 

     쿼리메서드 외 JPQL이나 QueryDSL로 직접 커스텀한 쿼리를 작성하고자 한다면, MemberRepositoryCustom 처럼 별도 인터페이스와 구현체를 만들어 사용한다. 이때 CustomRepository에 쿼리를 대표하는 메서드명은 어떻게 작성하는게 좋을까?

     Spring Data JPA 쿼리메서드 규약에 맞추거나 메서드 오버로딩하는 방식이 있는데, 나는 후자를 선호한다. 이유는 다음 전자에 해당하는 방식으로 작성된 코드를 보면 된다.

    public interface MemberRepositoryCustom {
        List<Member> findByAge(int age);
        
        List<Member> findByUsernameAndAge(String username, int age);
        
        //List<Member> findByUsernameAndAgeAnd...
    }

     우선 장점은 메서드명만 보고 어떤 쿼리가 실행될지 알 수 있다는 점이다. Between, In, GraterThan 등의 예약어도 붙을 수 있어서 어떤 조건으로 실행될지 한눈에? 파악 가능하다.

     단점으로는 첫째, 메서드명이 계속해서 길어질 수 있다. 맨 아래 주석처리 된 코드처럼 조건이 여러개 붙을 경우 다 적어줘야 하는데, 이는 JpaRepository에서 쿼리메서드를 사용하지 않고 CustomRepository를 사용하는 이유이기도 하다. 좀 더 잘 살아보자고 클래스 하나 더 만들었는데 똑같은 실수를 반복하는 것이다. 둘째, 공개된 메서드인데 구현 그 자체를 노출하기도 한다. findByUsernameAndAge라고 클라이언트에 노출하였으나 안에서는 나이 범위를 두도록 수정할 수도 있다. 그럼 메서드명도 findByUsernameAndAgeGraterThan과 같은 식으로 바꾸어 주어야한다. 내부 구현의 변화가 의존관계를 맺는 곳까지 전파된다.

     

     이번엔 메서드 오버로딩 방식으로 작성된 코드를 보자.

    public interface MemberRepositoryCustom {
        List<Member> findBy(int age);
        
        List<Member> findBy(String username, int age);
        
        List<Member> findBy(String username, int age, int teamId);
    }

     개인적으로 메서드 시그니처가 매우 조화롭게 이어졌다고 생각한다. 반환 타입은 List<Member>이고, 메서드명은 무엇으로 찾는다라는 뜻의 findBy이며, 무엇에 해당하는 파라미터가 뒤에 적혀져있어 결과를 예상하기에 충분하다. 메서드명이 장황해지지 않고 findBy(int age)의 쿼리 조건을 equals로 하든 in으로 하든 API 계약만 위반하지 않는다면 자유롭게 바꿀수도 있다.

     

     메서드 오버로딩은 책에서 추천한바와 같이 객체 사용 유연성을 더해주므로 비슷한 동작, 동일한 결과를 만들어야 하는 메서드가 여러 개 필요한 상황에서 적극 활용하자.

     원래 내용은 부 생성자는 주 생성자에 의존하자인데, 메서드 오버로딩 내용으로 논점이 확대되었다.

    댓글

Designed by Tistory.