본문 바로가기

Language & Framework/Java

이펙티브 자바 (2) 생성자에 매개 변수가 많으면 빌더를 고려하라 (+ 자바에서 freeze 구현하기)

 

이펙티브 자바의 두 번째 아이템, 빌더 패턴이다.

 

사실 내 블로그에서 이미 빌더 패턴에 대한 포스팅을 작성한 적이 있기 때문에 이번에는 거르고 넘어갈까 했는데, 내가 워낙 좋아하는 빌더 패턴이라 다시 한 번 작성하게 되었다.

 

우선, 멤버에 Null이 존재하면 안되고 여러 매개인수가 필요한 상황에 빌더 패턴이 아닌 다른 방법으로 객체를 생성하는 경우를 살펴보자.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class ConstructorChainingBurger {
    private final int bread;
    private final int patty;
    private final int onion;
    private final int tomato;
    private final int lettuce;
    private final int source;
 
    public ConstructorChainingBurger(int bread) {
        this(bread,0);
    }
 
    public ConstructorChainingBurger(int bread, int patty) {
        this(bread,patty,0);
    }
 
    public ConstructorChainingBurger(int bread, int patty, int onion) {
        this(bread,patty,onion,0);
    }
 
    public ConstructorChainingBurger(int bread, int patty, int onion, int tomato) {
        this(bread,patty,onion,tomato,0);
    }
 
    public ConstructorChainingBurger(int bread, int patty, int onion, int tomato, int lettuce) {
        this(bread,patty,onion,tomato,lettuce, 0);
    }
 
    public ConstructorChainingBurger(int bread, int patty, int onion, int tomato, int lettuce, int source) {
        this.bread = bread;
        this.patty = patty;
        this.onion = onion;
        this.tomato = tomato;
        this.lettuce = lettuce;
        this.source = source;
    }
}
cs

 

책에서 첫 번째로 소개하고 있는 방법은 Constructor Chaining을 이용한 점층적 생성자 패턴이다.

사실 나도 해당 방법으로 생성자를 만들어본 경우는 없는데, 인수가 이렇게 많지 않을 때에는 나름 나쁘지 않은 방법인 것 같아서 적용할 수 있는 조건이 된다면 한 번 적용해보고 싶다.

 

아무튼 생성자가 한 두개일 때는 괜찮겠지만, 필요한 인자가 많으면 많을수록 생성자도 끝없이 늘어날 것이고 각 생성자의 의도를 파악하기 힘들어진다.

사실 누가 설명하지 않아도 크게 문제가 있다는 것을 눈치챌 수 있을 것이다.

 

다음으로 방법은 자바 빈즈 패턴을 사용하는 것이다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class JavaBeansBurger {
    private int bread;
    private int patty;
    private int onion;
    private int tomato;
    private int lettuce;
    private int source;
 
    public void setBread(int bread) {
        this.bread = bread;
    }
 
    public void setPatty(int patty) {
        this.patty = patty;
    }
 
    public void setOnion(int onion) {
        this.onion = onion;
    }
 
    public void setTomato(int tomato) {
        this.tomato = tomato;
    }
 
    public void setLettuce(int lettuce) {
        this.lettuce = lettuce;
    }
 
    public void setSource(int source) {
        this.source = source;
    }
}
cs

 

 

 

위의 코드보다 훨씬 간결하지만 setter를 활용하는 방법은 현재는 거의 사용되지 않는다.

 

1. 불변성을 보장할 수 없다.

: Setter로 객체의 필드를 세팅하려면 우선 NoArgsConstructor가 필수이므로 final 키워드를 사용할 수 없고 따라서 불변을 강제할 수 없으므로 안정성이 떨어진다.

=> 단, JS의 freeze를 구현하여 강제로 불변을 유지시킬 수 있다. 글 제일 마지막에서 부록(?)으로 다루겠다. 책에서 다루는 내용이긴 한데 현실적으로 쓸모는 없다.

 

2. 객체를 완성하지 않고 사용하게 될 수 있다.

: 만약 어떤 클래스에서 최소한으로 세팅되어야하는 필드들이 있을 때, 이것을 개발자에게 알려줄 수 있는 방법은 오직 주석(혹은 Javadoc) 뿐이다. 컴파일 시점에 이를 강제하거나 빌더처럼 한 눈에 들어오지 않는다.

 

3. setter의 의도를 알기 어렵다.

: 사실 2번과 같은 말이라고 봐도 무방하다. 점층적 생성자 패턴과 마찬가지로 너무 다양한 setter가 존재할 때 사용자는 이것을 얼만큼 세팅해줘야 하는지, 반드시 필요한 게 무엇인지 직관적으로 알 수가 없다.

 

그리고 두 번째 말하지만 "구글에서 말하길, 쓰지 말래요". 설득 완료!

농담이니까 진지하게 받아들이지는 말자.

 

그래서 이런 문제들을 해결하기 위해 등장한 것이 "Builder Pattern"이다.

그런데 이 글에서는 오리지널 빌더 패턴에 대해 다루지는 않을 것이다. 왜냐면 이미 기존에 글을 작성했으니까.

 

https://7357.tistory.com/206?category=1077082 

 

디자인 패턴 (4) Builder Pattern : 빌더 패턴 _ 어노테이션부터 직접 구현까지

Builder Pattern 빌더 패턴은 아마도 개발 환경에서 (프레임워크가 기본 제공해주는 것 외에)가장 많이 쓰이는 패턴 중 하나가 아닐까 싶다. LOMBOK 환경에서는 어노테이션 하나만 붙이면 직접 구현할

7357.tistory.com

 

 

도구가 있는데 안 쓰는 것도 멍청한 일이라고 했던가, 우리에게는 사랑스러운 Lombok과 롬복이 제공해주는 @Builder가 있다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Builder
public class LombokBurger {
    private final int bread;
    private final int patty;
    private final int onion;
    private final int tomato;
    private final int lettuce;
    private final int source;
 
    public static void main(String[] args) {
        LombokBurger burger = LombokBurger.builder()
                .bread(2)
                .lettuce(2)
                .onion(1)
                .patty(1)
                .source(1)
                .tomato(1)
                .build();
    }
}
cs

 

 

정말 좋은 세상이다. 어노테이션 하나만 붙이면 디자인 패턴을 사용할 수 있다니..

빌더의 장점은 무엇일까?

 

1. 생성해놓은 인스턴스가 가진 특성을 누가 봐도 쉽게 알 수 있다.

: 설명이 필요 없을 것 같다. 위 코드를 보고 바로 맥도날드 노래 부르기 SSAP 가능이다.

 

2. 매개 변수가 많을 때, 일일히 필요한 매개변수가 무엇인지 확인하지 않아도 된다.

: 이 부분은 애매한데, IntelliJ라는 우리의 소울 메이트가 해당 기능을 제공해주고 있기 때문이다.

그럼에도 나는 빌더 패턴을 더 선호하는데, 하나씩 찔끔찔끔 보여주는 게 답답하기 떄문이다.

그리고 오랜 시간 작업하다보면 메모리 누수 때문인지 컴퓨터가 점점 느려지며 인텔리제이가 일을 제대로 안하는데, 그런 상황에 인자를 보여주지 않으면 속이 끓어오른다.

 

3. 불변을 보장한다.

: 위 코드만 보고 이해할 수 있는 내용은 아니니 궁금하면 내가 작성한 빌더 패턴 글을 읽어보거나, 추가적으로 검색해보자.

 

4. 계층적으로 설계된 코드에서 편리하게 사용할 수 있다.

: 코드로 설명하면 빠른데, 작성해보니 너무너무너무너무너무 길어진다. 이걸로만 글 하나를 쓸 수 있을 것 같다. 따라서 생략하였으니 궁금하면 아래 링크에서 쉽게 이해할 수 있다. 다만 나는 이게 큰 의미가 있는 것인지는 잘 모르겠다.. 일단 저자는 이것을 장점이라고 했다. 내가 이것의 필요성을 이해하기엔 아직 수준이 너무 낮을지도.

 

 

https://medium.com/codex/multiple-level-inheritance-for-builder-pattern-in-java-7809c1d3fa7b

 

Multiple Level Inheritance for Builder Pattern in Java

Builder is one of creational design patterns which builds a complex object from a simple one using a step-by-step approach.

medium.com

 

 

 

 

지금까지 빌더 패턴의 장점에서만 이야기했으니 단점을 꼽을 차례다.

 

1. 코드가 장황해진다.

: 이는 Lombok 하나로 해결되는 문제기는 하지만 완벽하지는 않다. 이유는 아래에서 다룬다.

 

2. 아무튼간에, 생성 비용이 존재한다.

: 무의미하다면 무의미한 단점이지만, 모든 클래스를 굳이 빌더로 생성하는 것은 쥐꼬리만큼이라도 낭비가 될 수 있다는 것이다.

 

3. 매개 변수가 많은 것이 아니라면 굳이 장황하게 코드를 늘려가며 빌더 패턴을 택할 이유가 없다.

: 그러나 저자가 이야기하길, 대부분의 API는 시간이 갈수록 비대해지고 그 때가 되어 빌더 패턴을 적용하면 기존의 레거시 코드들 때문에 이미 만들어뒀던 생성자를 처리하기 힘드니 애초에 빌더를 적용할 것을 권장하고 있다.

 

 

아래는 롬복을 사용했을 때 생기는 치명적인 단점이다.

구글 검색해보면 알겠지만 이 문제 때문에 빌더 패턴은 호불호가 많이 갈린다. 

 

빌더 어노테이션으로 구현되는 빌더는 아주 기본적인 형태의 빌더 패턴을 따른다.

즉, 어떤 값이 반드시 필요한지 사용자에게 강제할 수 없다.

 

예를 들어 @Builder를 사용할 경우

 

1
2
3
4
5
6
7
8
    @Builder
    public User(String email, String password, String picture) {
        this.email = email;
        this.password = password;
        this.picture = picture;
        this.userStatus = UserStatus.ACTIVE;
        this.pwdModified = LocalDateTime.now();
    }
cs

 

해당 값 중 어떤 값을 사용자가 입력하지 않는다면, 해당 값은 그냥 Null이나 해당 자료형의 기본값을 가진 상태로 인스턴스가 생성되는 것이다.

특정 값을 반드시 등록해야만 객체를 생성할 수 있게 하고 싶어도 마땅히 방법이 없다. User.builder.build라고 입력해도 객체는 생성된다.

 

그나마 가능성 있는 건 this.email이 null일 경우 unckeked Exception 날려서 개발하는 사람 괴롭히기?

아마 그렇게 해도 대다수의 개발자는 그냥 throw해버리고 내부를 들여다볼 생각조차 안해볼테니 의미 없을 것이다.

 

따라서 팀원들 중 누군가가 폭탄일 경우 (혹은 내가 폭탄인 경우) 롬복의 빌더를 통해 생성된 객체는 문제를 일으킬 가능성이 있다.

외부에서부터 입력되는 값은 validation으로 거른다지만 내부의 실수는 어떻게 걸러내겠는가?

사람 자체를 회사에서 걸러내지 않는 한 불가능한 일이다.

혹은 검증 검증 검증 또 검증하는 방법도 있겠다..

 

나도 기존에는 빌더에 장점만 있다고 생각했으나 최근 팀프로젝트를 겪으며 생각이 조금 변했다.

팀에 이상한 사람이 있었다는 게 아니라, 그런 가능성을 생각하게 됐다는 말이다.

 

아무튼 빌더 사용 시에는 이런 점들을 고려해야할 것이다.

그렇다고 빌더를 일일히 구현해서 사용하자니, IDE의 기능이 있는데 그렇게까지 해가면서 빌더를..?

생각해볼만한 문제인 것 같다.

 

* 만들면서 배우는 클린 아키텍처의 저자인 톰 홈버그는 빌더가 오히려 생성자보다 실수를 유발하기 쉬우며, IDE의 강력한 기능이 있기 때문에 생성자만으로 충분하다고 이야기하고 있다.

 

 

 

 

 

 

빌더에 대한 글은 이만 접고, 마지막으로 JS의 freeze() 메서드를 구현하여 Java Beans Pattern을 따르면서도 불변을 유지하는 방법이다.

 

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze

 

Object.freeze() - JavaScript | MDN

Object.freeze() 메서드는 객체를 동결합니다. 동결된 객체는 더 이상 변경될 수 없습니다. 즉, 동결된 객체는 새로운 속성을 추가하거나 존재하는 속성을 제거하는 것을 방지하며 존재하는 속성의

developer.mozilla.org

 

자바스크립트에서 freeze()를 사용하면 그 시점부터 해당 Object 내부에 속성을 추가하거나 변경할 수 없다.

JS에 처음 입문하던 당시에 드림코딩의 엘리님께서 이걸 사용하는 걸 보고 이렇게까지 하는 이유가 뭐야.. 했는데 이제 나도 불변이 아니면 마음이 불편해지는 사람이 되어버렸다.

 

아무튼 이를 구현하는 방법은 간단하다. 그냥 freeze()메서드를 만들면 된다.

freeze 메서드를 사용하면 다른 멤버에 더 이상 아무런 수정도 가할 수 없게 만들면 되겠네요.

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class JavaBeansFrozen {
    private String name;
    private int age;
    private boolean isFrozen;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        if ( !isFrozen ) {
            this.name = name;
        }
    }
 
    public void setAge(int age) {
        if ( !isFrozen ) {
            this.age = age;
        }
    }
 
    public void freeze() {
        if ( isFrozen ) {
            isFrozen = true;
        }
    }
 
    public void unFreeze() {
 
        isFrozen = false;
    }
}
cs

 

이런 식으로 그냥 해당 메서드 실행 이후부터 객체가 불변이 되도록 구현하면 된다.

하지만 자바에는 이것 외에도 불변 객체를 만들 수 있는 방법이 많고, setter를 이용해서 인스턴스를 세팅하는 방법이 오직 mutable하다는 이유 하나는 아니기 때문에 별다른 의미는 없다. 그냥 책에서 다루니 언급했을 뿐..

 

오늘은 여기서 끝이다.