-
생성자 매개변수가 많다면 빌더 사용을 고려해보자JAVA/Effective java 2020. 12. 28. 15:51
- static 팩토리 메서드, public 생성자 모두 매개변수가 많이 필요한 경우 불편해진다.
- 그 예가 NutritionFacts인데 해당 클래스를 한번 봐보자.
- 몇몇 필드등은 반드시 세팅되어야 되는 필드고 나머지 필드는 Optional 한 필드들도 있다.
- 기본 타입은 기본값으로 세팅이 될 것이고 참조 타입은 null로 기본 세팅이 될 것이다.
- 하지만 문제는 굉장히 코드가 길어지고 특히 자바에서는 이것이 뭘 뜻하는지 알 수가 없게 된다.
- 따라서 이런 생성자를 쓰게 되면 필요 없는 매개변수도 넘겨야 하는 경우가 발생하는데 보통 0 같은 기본값을 넘기게 된다.
- 동작하기는 하지만 읽기가 어렵고 작성하기도 역시 번거롭다.
그렇다면 매개변수를 받지 않는 생성자를 만드는 건 어떨까?
- 비어있는 생성자와 세터를 사용해서 필요한 필드만 설정할 수 있는 방법이다.
- 이로 인한 약간의 장점은 무엇을 세팅하는지 명확해진다는 것. 무엇에다 100을 집어넣는지 이제는 명확히 알 수 있게 된다.
- 하지만 단점은 코드가 장황해지고 최종적인 인스턴스를 까지 여러 번의 호출을 거치게 된다.
- 자바 빈이 중간에 사용되는 경우 안정적이지 않은 상태로 사용될 여지가 있다.
ex) estCalories까지 세팅하고 사용되는 경우
- 또한 게터와 세터로 인해 불변 클래스로 만들지 못한다는 단점이 있고
- 세터로 인해 바꿀 수 있는 필드들은 ThreadSafe 하지 않으므로 락킹이 필요해진다.
* 각 세터들이 호출될 때마다 freeze ( flag 등 )가 된 상태면 바꾸려는 시도가 유효하지 않으니 예외를 던지는 방법이 있다.
빌더를 사용해보기
- 생성자는 얼마든지 추가하거나 줄일 수가 있다.
- 이런 생성자의 신축성과 세터를 사용할 때의 가독성을 모두 취하는 방법이 빌더 패턴이다.
- 빌더 패턴은 만들려는 객체를 바로 만들지 않고 클라이언트는 빌더(생성자 등)에 필수 매개변수를 주면서 호출해 Builder 객체를 얻는다.
- 그 후 빌더 객체가 제공하는 세터와 비슷한 메서드를 사용해 부가적인 필드를 채워 넣고 최종적으로 build라는 메서드를 호출해서 만들려는 객체를 생성한다.
- 다음과 같이 비어있는 EnumSet으로 toppings를 만들고 add토핑이라는 메서드를 제공하는 빌더이다.
- build()를 호출하면 Pizza 타입의 인스턴스를 만들어야 하는데 실제 타입은 abstract이니 new 해서 만들 수 없고 Pizza의 하위 타입을 이 안에서 만들게 될 것이다.
- 그 후 Pizza라는 생성자에서 Builder를 받아서 Pizza가 가진 토핑을 빌더가 가진 토핑으로 세팅을 해준다.
- abstract Pizza build(); `Convariant 리턴 타입`을 위한 준비작업
- protected abstract T self(); // `self-type` 개념을 사용해서 메서드 체이닝이 가능케 한다. 계속해서 동일한 하위 인스턴스의 타입이 넘겨지도록 self를 구현한 것이다.
- 이제 NyPizza라는 구현체는 Pizza를 상속받고 이 구현체는 사이즈가 존재한다.
- 빌더를 만들 때 피자의 빌더를 상속해서 만드는데 build()를 호출하는 리턴 타입이 NyPizza 이기 때문에 빌드를 호출하는 클라이 언트가 타입 캐스팅을 할 필요가 없다.
- 이제 Builder가 가지고 있는 size를 NYPizza에 size 건네준다.
- 이때 이전에 super가 호출이 되니 토핑도 세팅이 되고 사이즈도 세팅이 되게 된다.
- Calzone에는 소스를 안에다 늘 거냐 말 거냐가 추가된다.
- 역시 마찬가지로 소스 인사이드라는 flag가 빌더가 가진 소스 인사이드로 세팅이 되도록 되어있다.
- 이 NyPizza 빌더는 아까 Pizza 빌더의 하위 클래스이고 addTopping을 쓸 수 있다.
- 이후 build()를 하면 Nypizza에 있는 빌드가 호출이 되므로 NyPizza를 리턴하게 되고 따라서 따로 타입 캐스팅이 필요 없다.
- 이 Calzone는 addTopping에서 인스턴스에 타입이 self가 리턴하는 객체의 타입이다.
- self()는 calzone에 빌더 자체를 리턴하게 돼있다. 따라서 calzone의 메서드를 사용할 수 있다.
- 즉 필수적인 파라미터는 Builder() 안에 넣고 부가적인 파라미터들은 메서드 체이닝을 통해 세팅 후 최종적으로 build()를 호출해주면 된다.
- 이때 추상 빌더는 재귀적인 타입 매개변수를 사용하고 self라는 메서드를 사용해 self-type 개념을 모방할 수 있다.
- 하위 클래스에서는 build 메소드의 리턴 타입으로 해당 하위 클래스 타입을 리턴하는 Covariant 리턴 타이핑을 사용하면 클라이언트 코드에서 타입 캐스팅을 할 필요가 없어진다.
- 빌더는 가변 인자 매개변수를 여러 개 사용할 수 있다는 소소한 장점도 있다.
- 생성자나 팩토리는 가변 인자를 맨 마지막 매개변수에 한 번밖에 못쓰기 때문이다.
- 또는 토핑 예제에서 본 것처럼 여러 메서드 호출을 통해 전달받은 매개변수를 모아 하나의 필드에 담는 것도 가능하다.
- 빌더는 꽤 유연해서 빌더 하나로 여러 객체를 생성할 수도 있고 매번 생성하는 객체를 조금씩 변화를 줄 수도 있다.
- 만드는 객체에 시리얼 번호를 증가하는 식으로 가능하다.
- 단점으로는 객체를 만들기 전에 먼저 빌더를 만들어야 하는데 성능에 민감한 상황에서는 그 점이 문제가 될 수도 있다.
- 그리고 생성자를 사용하는 것보다 코드가 더 장황하다.
- 따라서 빌더 패턴은 매개변수가 많거나(4개 이상?) 또는 앞으로 늘어날 가능성이 있는 경우에 사용하는 것이 좋다.
- 적은 코드를 살펴보면 클라이언트 코드는 깔끔할지 몰라도 작성하는 코드는 장황할 수 있다는 것을 알 수 있다.
- 성능면에서는 Builder 객체가 무거운 객체가 아니 이게 큰 영향은 없을 것이다.
- 성능 때문에 Build 패턴을 못쓰는 일은 없을 것이다. 중요한 점은 파라미터가 많을 시에 유용할 수 있다는 것이다.
롬복에 @Builder
- lombok에 @Builder에 대해서 들어본 적이 있을 것이다. @Builder를 붙이기만 해도 lombok이 빌더를 직접 만들어 주게 된다.
- self나 하위 클래스에서 구체적인 클래스를 넘겨주는지는 잘 모르겠지만 다음과 같이 간단한 수준은 매우 편리하게 구현이 가능한 것 같다.
참고 자료
- https://www.informit.com/store/effective-java-9780134685991
- https://github.com/keesun/study/tree/master/effective-java-3rd
'JAVA > Effective java' 카테고리의 다른 글
불필요한 객체를 만들지 말자 (0) 2021.01.01 리소스를 엮을 때는 의존성 주입을 선호하라 (0) 2020.12.31 private 생성자로 noninstantiability를 강제할 것 (0) 2020.12.30 private 생성자 또는 enum 타입을 사용해서 싱글톤으로 만들 것 (0) 2020.12.29 생성자 대신 static 팩토리 메소드 고려해보기 (0) 2020.02.27