이팩티브 자바를 시작한지 4주가 지났다. 이제와서 올리게 되었다... 매주 스터디 이전에 발표도 꺼리게되고, 책도 펴기 힘들다. 하지만 잠깐의 귀찮음을 이겨내고 스터디에 참가해 다른 사람들의 발표를 들으며 부족했던 부분을 많이 배우고, 발표를 진행하며 애매했던 부분도 확실히 기억에 남는것같다. 매주 스터디 이후 github에 공유했던 마크다운을 업로드할 예정이다.
클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 pulic 생성자이다.
하지만 클래스의 인스턴스를 얻는 다른 방법이 있는데, 정적 팩터리 메서드 라고 하는 방법이 있다.
다음 코드는 boolean의 박싱클래스(Wrapper class)인 Boolean에서 발췌한 간단한 예다.
public final class Boolean implements java.io.Serializable,
Comparable<Boolean>
{
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
...
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
...
}
정적 팩터리 메서드는 GOF 디자인 패턴에서의 팩터리 메서드와 다르다
평소 Jpa를 사용할때면 Domain Entity에서 정적 팩토리 메서드를 사용하여 객체를 만들고, 데이터베이스에서 받아온 객체를 Dto로 변환하는 과정을 정적 펙토리 메서드로 사용하고 있었는데,
왜 좋은지, 왜 사용해야하는지는 생각해 본 적이 없는것 같다...
아래는 최근에 사용했던 정적 팩토리 메서드의 예시이다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long memberId;
...
@Builder
public Member (String email, String username, String password, Role role) {
this.email = email;
...
}
/*
* 이부분이 정적 팩토리 메서드
*/
public static Member createMember(Member member) {
return Member.builder()
.email(member.getEmail())
.username(member.getUsername())
.password(member.getPassword())
.role(member.getRole())
.build();
}
}
이 방식(정적 팩토리 메서드)은 장점과 단점이 모두 존재하는 방법이다.
public Member(String id, String pwd) {
this.id = id + "아이디";
this.pwd = pwd + "비밀번호";
}
public static Member createMemberWithKoreanTranslate(String id, String pwd) {
return Member.builder()
.id(id + "아이디")
.pwd(pwd + "비밀번호")
.build();
}
대충 이런 느낌이지 않을까 싶다.
하나의 시그니처로 하나의 생성자를 만들 수 있지만, 매개변수의 순서를 다르게 하거나 매개변수의 수를 다르게 하는 등의 방법으로 생성자 생성 제한을 피해 볼 수 있지만, 좋지 않은 방법이다.
사용하는 사람도 해당 생성자가 어떤 역할을 하는지 정확히 알기 힘들기 때문이다.
한 클래스에 시그니처가 같은 생성자가 여러 개 필요할 것 같으면 생성자 대신 정적 팩터리 메서드를 사용하고, 이름을 잘 짓도록 하자!
이 덕분에 불변 클래스는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다.
대표적으로 Boolean.valueOf(boolean)
메서드는 객체를 아예 생성하지 않는다.
생성비용이 매우 큰 객체가 자주 요청된다면 성능향상에 도움이 된다.
반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩터리 방식의 클래스는 인스턴스의 생명주기를 통제할 수 있다.
인스턴스를 통제하면 클래스를 싱글턴으로, 인스턴스화 불가로 만들수 있고, 불변값 클래스에서 동치인 인스턴스가 단 하나뿐임을 보장할 수 있다.
프로그래머는 명시한 인터페이스대로 동작하는 객체를 얻을 것임을 알기에 굳이 구현 클래스를 찾아보지 않아도 된다.
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
Enum<?>[] universe = getUniverse(elementType);
if (universe == null)
throw new ClassCastException(elementType + " not an enum");
if (universe.length <= 64) // 원소의 숫자가 64개 이하면 RegularEnumSet 반환 (EnumSet의 하위 타입)
return new RegularEnumSet<>(elementType, universe);
else // 원소의 숫자가 65개 이상이면 JumboEnumSet 반환 (EnumSet의 하위 타입)
return new JumboEnumSet<>(elementType, universe);
}
클라이언트는 두 클래스의 존재를 모르고, 다음 릴리즈때 삭제해도 아무 상관이 없다.
조건을 명시하지 않으면 기본 구현체를 반환하거나 지원하는 구현체들을 하나씩 돌아가며 반환하는데, 이 서비스 접근 API가 유연한 정적 팩터리
이다.
컬렉션 프레임워크의 유틸리티 구현 클래스들을 상속할 수 없다는 이야기이다.
상속보다 컴포지션(Item 18)을 하도록 유도하고 불변타입(Item 17)으로 만드려면 이 제약을 지켜야 한다는 점에서 오히려 장점으로 받아들일 수 있다.
Item18과 Item17의 내용이라 잘 모르겠습니다.
다만 불변타입의 경우, 변수에 final을 붙이고 만들고, Lombok Setter를 선언하지 않는 방식으로 만든다고 얼핏 알고있습니다.
생성자처럼 API 설명에 명확하게 드러나지 않으니 사용자는 정적 팩토리 메서드 방식 클래스를 인스턴스화 할 방법을 알아내야 한다.
그렇기 때문에 정적 팩터리 메서드에 잘 알려진 규약을 따라 짓는 식으로 문제를 완화시켜줘야 한다.
정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있기 때문에, 각 장단점을 이해하고 사용하는게 좋다. 하지만 public 생성자보다 정적 팩토리 메서드를 사용하는게 유리한 경우가 더 많으므로 자주 사용합시다!