2022 05 19
2022-05-19¶
Effective Java Item 1. 생성자 대신 정적 팩토리 메서드를 고려하라¶
- 개요
- 클라이언트는 그동안 public 생성자를 통해 인스턴스를 획득하곤 했음
- 정적 팩토리 메서드를 클래스가 제공해서, 클래스의 인스턴스를 반환하는 정적 메서드를 만들어보자
- 정적 팩토리 메서드 != 팩토리 메서드 패턴
- 디자인 패턴이랑 정적 팩터리 메서드 노상관
- 생성자 대신 정적 팩토리 메서드 제공하는 것은 장단점이 있음
- 장점 1. 이름이 생긴다
- 코드의 가독성을 높여줌
- 각 생성자는 별도의 시그니처를 가지는데, 이 제약에서 벗어나고자 눈가림하듯 순서만 바꾸면 쓰레기 코드
- 여러 시그니처를 갖는 생성자가 필요하다면 정적 팩터리 메서드 써보자
- 장점 2. 생성자와 달리 매번 새로운 객체를 만들지 않아도 된다
- 생성자는 리턴값을 가질 수 없음. 따라서 발생하는 몇가지 불편함이 있음
- 불변 객체라면 그거 쓰면되고,
- 미리 만들어둔것 캐싱해두고 쓰면 될 듯!
- 객체를 만드는 비용이 크다면 성능 크게 개선 가능
- 어떤 시점에 어떤 객체가 얼마나 존재하는지를 정밀하게 제어할 수 있다는 것이 장점 => 생성자에겐 없던 객체 통제 능력 (instance-controlled class)
- Singleton 만들기 쉬워짐
- 객체 생성 불가능하게 만들수도 있음
- 불변 객체 제공도 가능
- 장점 3. 하위 자료형을 반환할 수 있다
- 반환되는 객체의 클래스를 훨씬 유연하게 결정 가능
- 기존 생성자가 클래스의 관리자로써 못하던 일을 가능케 해줌
- 구현의 세부사항을 감출 수 있는 좋은 코드 작성 가능
- interface-based-framework에서 좋음
- Java 8 이전에는 인터페이스에 정적 메서드 X
- JCF에서 45개의 utility implementation of interfaces가 있었는데 공통적으로 다음을 제공
- unmodifiable
- synchronized
- JCF에서 45개의 utility implementation of interfaces가 있었는데 공통적으로 다음을 제공
- 반환되는 객체의 클래스를 훨씬 유연하게 결정 가능
- 장점 4. input 파라미터에 따라 다른 인스턴스를 반환할 수 있다
- ex. EnumSet
- long => RegularEnumSet
- long array => JumboEnumSet
- 이렇게 하면 추후에 EnumSet을 상속받은 구현체가 생겼을 때 손쉽게 붙일 수 있겠지?
- 유지/보수/확장에 유리한 코드 작성 가능
- ex. EnumSet
- 장점 5. 반환하고자 하는 클래스가 런타임에 생겨도 괜찮음
- 정적 팩터리 메서드가 반환하는 객체의 클래스는 정적 팩터리 메서드가 정의된 클래스의 코드가 작성된 순간에 없어도 OK
- 서비스 제공자 프레임워크 (ex. JDBC)
- 다양한 서비스 제공자들이 하나의 서비스를 구성하는 시스템
- 클라이언트는 실제 구현된 서비스를 이용
- 핵심 컴포넌트
- 서비스 인터페이스 (Connection)
- 서비스 제공자가 구현
- 제공자 등록 API (DriverManager.registerDriver)
- 구현체를 시스템에 등록하여 클라이언트가 쓸 수 있도록
- 서비스 접근 API (DriverManager.getConnection)
- 클라이언트에게 실제 서비스 구현체 제공
- 서비스 제공자 인터페이스 (Driver)
- 서비스 제공자가 구현하며, 서비스 구현체의 객체를 생성하기 위함
- 구현체는 클래스 이름으로 등록되며, 리플렉션을 통해 만들어짐
- 서비스 인터페이스 (Connection)
- 서비스 접근은 결국 유연한 정적 팩토리 메서드
// 서비스 인터페이스 public interface Service {} // 서비스 제공자 인터페이스 public interface Provider { Service newService(); } // 서비스 등록과 접근에 사용되는 객체 생성 불가능 클래스 public class Services { private Services() { } // 서비스 이름과 서비스 간 대응관계 보관 public static final String DEFAULT_PROVIDER_NAME = "<def>"; private static final Map<String, Provider> providers = new ConcurrentHashMap<String, Provider>(); // 제공자 등록 API public static void registerDefaultProvider(Provider p) { registerProvider(DEFAULT_PROVIDER_NAME, p); } public static void registerProvider(String name, Provider p) { providers.put(name, p); } // 서비스 접근 API public static Service newInstance() { return newInstance(DEFAULT_PROVIDER_NAME); } public static Service newInstance(String name) { Provider p = providers.get(name); if (Objects.isNull(p)) { throw new IllegalArgumentException(); } return p.newService(); } }
- 단점 1. 정적 팩터리 메서드만 있는 클래스라면, public/protected로 선언된 생성자가 없기에 하위 클래스 못 만듦
- 오히려 좋은 부분 : 상속보다 컴포지션 유도
-
단점 2. 다른 정팩메와 완전한 구분이 어려움
- 따라서 정팩메 이름 지을 때 잘 지어라
- valueOf: 주어진 값과 같은 값을 갖는 객체 반환, 형변환 메서드
- of: valueOf의 축약
- getInstance: 싱글턴이라면, 항상 같은 객체 반환
- newInstance: getInstance와 같지만, 호출될 때 마다 다른 인스턴스
- getType: getInstance와 같지만, Type 형을 반환해야하는 경우
- newType: newInstance와 같지만, Type 형을 반환해야하는 경우
-
내 생각
- 결국 Java의 생성자가 충분히 강력하지 않아서 발생하는 문제를 해결하고자 정팩메 사용
- 생성자는 클래스의 생명주기를 충분히 관리하고, 완전한 중앙 통제형 집권자여야함.
- 어떤 시기에 어떤 객체가 있고, 몇 개가 있고 등등...
- 생성자는 클래스의 생명주기를 충분히 관리하고, 완전한 중앙 통제형 집권자여야함.
- 난 정팩메를 언제 많이 쓰지? 나는 정팩메를 똑바로 쓰고 있는가?
- 그냥 생성자마냥 쓰는거 아닌가? 빌더 패턴 마냥 쓰는거 아닌가? 그냥 필드 추출해서 DTO로 감쌀때만 쓰지 않는가?
- Enum에서 조건에 맞는 인스턴스를 반환해주는 경우
- Enum에 등록된 반환 객체도 singleton
- singleton를 반환하는 정적 팩터리 메서드가 좋긴하겠다...!
public static FilterStrategyFactory of(String mainnet, String category, String ventureCapital) { return Arrays.stream(values()) .filter(strategy -> strategy.isMainnetEmpty == mainnet.isEmpty()) .filter(strategy -> strategy.isCategoryEmpty == category.isEmpty()) .filter(strategy -> strategy.isVentureCaptialEmpty == ventureCapital.isEmpty()) .findFirst() .orElseThrow(() -> new IllegalStateException("발생할 수 없는 로직입니다.")); }
- 도메인 받아서 필드 추출해서 쓱 DTO 만들때도 많이 씀
- https://github.com/CryptoHows/cryptohows-web-backend/blob/master/src/main/java/xyz/cryptohows/backend/project/ui/dto/CoinResponse.java
- 새로운 객체를 그냥 만드는 것에 대한 책임을 넘겨주는 느낌
- 사실 꼭 정팩메일 필요가 없어보이긴 함
- 그냥 무지성으로 쓰고 있긴했음
- 다만 좋은 것은, of/toList 등으로 이름을 붙여줄 수 있다는 점! (생성자였으면 이 만큼 직관적이진 않았을 수도)
@Getter public class CoinResponse { private final Long id; private final String coinSymbol; private final String coinInformation; public CoinResponse(Long id, String coinSymbol, String coinInformation) { this.id = id; this.coinSymbol = coinSymbol; this.coinInformation = coinInformation; } public static CoinResponse of(Coin coin) { return new CoinResponse( coin.getId(), coin.getCoinSymbol(), coin.getCoinInformation() ); } public static List<CoinResponse> toList(List<Coin> coins) { return coins.stream() .map(CoinResponse::of) .collect(Collectors.toList()); } }
- 결국 Java의 생성자가 충분히 강력하지 않아서 발생하는 문제를 해결하고자 정팩메 사용
- Question
- 정적 팩터리 메서드 vs 빌더 패턴
- 싱글턴의 단점?