티스토리 뷰

생성자의 매개변수가 많을 때

 생성자는 선택적 매개변수가 많아질 때 설계가 곤란해진다. 클래스는 여러 개의 필드를 가질 수 있기 때문에, 초기화대상 필드의 수가 많아지면 (필자는 보통 4개 이상이 되면 많게 느껴지는 것 같다) 적절한 대응방안에 대해 고민하게 된다. 정적 팩토리 메서드의 경우도 마찬가지이다.

점층적 생성자 패턴 (Telescoping Constructor Pattern)

 생성자에 필요한 인자가 많아질 때, 개발자들은 일반적으로 '점층적 생성자 패턴' 을 사용한다. 단순히 생성자에 필요한 매개변수의 여러가지 조합으로 생성자의 입력 파라미터를 구성하는 구현방법이다.

public class Member {

    private Long memberId;
    private String name;
    private Integer age;
    private String basicAddress;
    private String detailAddress;
    private String mobilePhoneNumber;

    public Member(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public Member(Long memberId, String name, Integer age, String basicAddress, String detailAddress) {
        this.memberId = memberId;
        this.name = name;
        this.age = age;
        this.basicAddress = basicAddress;
        this.detailAddress = detailAddress;
    }

    public Member(Long memberId, String name, Integer age, String basicAddress, String detailAddress, String mobilePhoneNumber) {
        this.memberId = memberId;
        this.name = name;
        this.age = age;
        this.basicAddress = basicAddress;
        this.detailAddress = detailAddress;
        this.mobilePhoneNumber = mobilePhoneNumber;
    }
}

 위 코드와 같이 점층적으로 생성자의 인자를 늘려가며, 인스턴스를 생성할 수 있다. 이와 같은 패턴은 사용하는 것은 가능하지만 매개변수가 많아질 수록 클라이언트 코드의 가독성이 떨어진다는 단점이 있다.

 

 더욱 큰 문제는 단순히 가독성 저하의 문제가 아니다. 클라이언트가 실수로 매개변수의 인자를 세팅하지 않거나, 바꿔서 넣어주더라도 컴파일러는 알아챌 수 없기 때문에 런타임 오류를 일으킬 확률이 높다.

자바빈즈 패턴 (JavaBeans Pattern)

 이 패턴은 매개변수가 없는 생성자로 객체를 만든 후, setter 메서드들을 호출해 원하는 매개변수의 값을 설정하는 방식이다.

Member member = new Member();
member.setMemberId(1L);
member.setName("홍길동");
member.setAge(30);
member.setBasicAddress("서울특별시 종로구");
member.setDetailAddress("수표동 1번지");
member.setMobilePhoneNumber("01012345678");

 이 패턴은 단순히 생각해보아도 그리 좋지 못하다는 것을 금방 알 수가 있다. 인스턴스를 한 번 만드는데 여러 개의 메서드를 호출해야 한다. 또한 객체가 완전히 생성되는 시점까지는 필드 데이터의 일관성이 무너지는데, 문제는 이 일관성을 검증하는 프로세스를 별도로 관리해주어야 한다는 것이다.

 

 클린코드 관점에서도 이는 별로 좋지 못한 코딩 기법이다.

 

[객체지향 생활체조 원칙] 규칙 9. getter/setter/property를 쓰지 않는다

getter/setter/property를 쓰지 않는다  도메인 오브젝트로 설계한 Entity 또는 VO 클래스에는 getter/setter/property 사용을 지양해 상태노출을 최소화 하라는 지침이다. 숨은 의미  객체지향 프로그래밍의

limdingdong.tistory.com


빌더 패턴 (Builder Pattern)

 빌더 패턴은 점층적 생성자 패턴의 안정성과 자바 빈즈 패턴의 가독성 이라는 장점을 함께 갖는다. 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수 만으로 생성자(혹은 정적팩토리메서드)를 호출해 빌더 객체를 얻는다. 그런 다음 빌더 객체가 제공하는 일종의 setter 메서드를 통해 원하는 매개변수들을 설정해 준다. 마지막으로 매개변수가 없는 build 메서드를 호출해 클라이언트가 필요로 하는 인스턴스를 얻는다.

 

 빌더는 생성할 클래스 안에 정적 멤버 클래스로 만들어두는게 일반적인 설계이다.

public class Member {

    private Long memberId;
    private String name;
    private Integer age;
    private String basicAddress;
    private String detailAddress;
    private String mobilePhoneNumber;

    public static class Builder {
        // 필수 매개변수
        private Long memberId;
        private String name;
        private Integer age;

        // 선택 매개변수 - 기본값으로 초기화
        private String basicAddress = "";
        private String detailAddress = "";
        private String mobilePhoneNumber = "";

        public Builder (Long memberId, String name, Integer age) {
            this.memberId = memberId;
            this.name = name;
            this.age = age;
        }

        public Builder basicAddress(String basicAddress) {
            this.basicAddress = basicAddress;
            return this;
        }

        public Builder detailAddress(String detailAddress) {
            this.detailAddress = detailAddress;
            return this;
        }

        public Builder mobilePhoneNumber(String mobilePhoneNumber) {
            this.mobilePhoneNumber = mobilePhoneNumber;
            return this;
        }

        public Member build() {
            return new Member(this);
        }
    }

    private Member(Builder builder) {
        this.memberId = builder.memberId;
        this.name = builder.name;
        this.age = builder.age;
        this.basicAddress = builder.basicAddress;
        this.detailAddress = builder.detailAddress;
        this.mobilePhoneNumber = builder.mobilePhoneNumber;
    }
    
  }
Member member = new Member.Builder(1L, "홍길동", 30)
                            .basicAddress("서울특별시 종로구")
                            .detailAddress("수표동 1번지")
                            .mobilePhoneNumber("01012345678")
                            .build();

 상기 코드는 빌더를 정의하는 방법과 사용 예시를 예로 든 것이다. 빌더의 setter 메서드들은 빌더 자신을 호출하기 때문에 연쇄적인 호출이 가능하다. 이를 Fluent API 또는 Method Chaining 이라고 한다. 빌더 패턴은 기본적으로 가독성이 높다. 또한 빌더의 생성자와 메서드에서 각 파라미터의 유효성 검사 로직을 구현할 수 있기 때문에, 입력 매개변수의 검사가 용이하고 IllegalArgumentException 의 발생지점을 명확히 알 수 있다.


빌더패턴의 계층적 설계

 빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다. 각 계층의 클래스에 관련 빌더를 멤버로 정의할 수 있다.

public abstract class Account {

    private String accountId;
    private Member mainContractor;
    private String accountNumber;

    private BigDecimal contractAmount;
    private Integer contractTerm;

    abstract static class Builder<T extends Builder<T>> {
        private String accountId;
        private Member mainContractor;
        private String accountNumber;

        private BigDecimal contractAmount;
        private Integer contractTerm;

        public Builder (String accountId, Member mainContractor, String accountNumber) {
            this.accountId = accountId;
            this.mainContractor = mainContractor;
            this.accountNumber = accountNumber;
        }

        public Builder contractAmount(BigDecimal contractAmount) {
            this.contractAmount = contractAmount;
            return this;
        }

        public Builder contractTerm(Integer contractTerm) {
            this.contractTerm = contractTerm;
            return this;
        }

        abstract Account build();

        protected abstract T self();
    }

    Account(Builder<?> builder) {
        this.accountId = builder.accountId;
        this.mainContractor = builder.mainContractor;
        this.accountNumber = builder.accountNumber;
        this.contractAmount = builder.contractAmount;
        this.contractTerm = builder.contractTerm;
    }
}
public class AccountAutoLoan extends Account {

    private AutoLoanType autoLoanType;

    public static class Builder extends Account.Builder<Builder> {

        private AutoLoanType autoLoanType;

        public Builder(String accountId, Member mainContractor, String accountNumber) {
            super(accountId, mainContractor, accountNumber);
        }

        public Builder autoLoanType(AutoLoanType autoLoanType) {
            this.autoLoanType = autoLoanType;
            return self();
        }

        @Override
        public AccountAutoLoan build() {
            return new AccountAutoLoan(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }

    private AccountAutoLoan(Builder builder) {
        super(builder);
        this.autoLoanType = builder.autoLoanType;
    }
}
AccountAutoLoan account = new AccountAutoLoan.Builder("00001", member, "2022-0116-00001")
                        .autoLoanType(AutoLoanType.NEW_CAR)
                        .build();

 Account 의 Builder 칼래스는 재귀적 타입한정 을 이용하는 제네릭 타입으로 정의하였다. 이 클래스에는 abstract method 로 정의한 self() 메서드가 정의되어 있는데, 하위 클래스는 이를 활용하여 형변환 없이 Method Chaning 을 구현할 수 있게 된다.


빌더의 단점

 빌더 패턴도 몇몇 단점을 가진다. 인스턴스를 획득하려면 먼저 빌더부터 만들어주어야 한다. 또한 성능이 민감한 상황에서는 약간의 문제점을 갖는다.

 

 일반적으로 많이 사용하는 점층적 패턴에 비해 빌더 패턴은 코드 구성이 장황한 편이다. 적어도 생성자의 매개변수가 4개 이상이 될 경우 그 값어치를 한다. 그러나 API는 시간이 지날수록 매개변수가 많아지는 경향을 갖는다. 핵심 엔티티 클래스의 경우, 점층적 생성자와 정적팩토리메서드를 선구현하고 후에 빌더 패턴으로 전환하는 것 보다는 애초에 빌더패턴으로 시작하는 경우가 더 낫다.


마치며

 생성자나 정적 팩토리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는게 더 낫다고 한다. 지금까지는 실무에서 주로 팩토리 클래스를 통해 인스턴스 생성을 해 왔다. 팩토리 클래스 내부는 점층적 생성자들과 자바빈즈 패턴의 사용이 난무되어 있던 것 같다. 빌더를 활용한 리팩토링에 대해 진지하게 고민해보아야 겠다. 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday