부트캠프를 다닐 때 이펙티브 자바 책을 받았다. 그 중 초기 부분에는 정적 팩토리 메서드와 빌더 패턴에 대해서 설명한다.
(사실 부트캠프 초기에는 이펙티브 자바가 너무 어려웠어서 이해하지 못했다... 이제는 조금씩 읽히는게 그만큼 실력이 늘었나 뿌듯하다.)
Car car = new Car(); 라는 단순한 생성자 사용을 지양하고 정적 팩토리 메서드나 빌터 패턴을 사용하는 데에는 불변 객체의 캡슐화 라는 의미가 가장 크다고 생각한다.
하지만 두 패턴 중 어떤게 더 나은 방법인지, 어떨때 써야하는지에 대한 개인적인 방향성은 항상 잡지 못했었다.
두 방식을 다시 공부해보며 개인적인 선택 기준을 정립하고자 이 글을 적게 되었다.
1. 정적 팩토리 메서드 (static factory method)
정적 팩토리 메서드란 개발자가 구성한 static method를 통해 간접적으로 생성자를 호출하여 객체를 생성하는 디자인 패턴이다.
먼저, 예시를 살펴보자. 해당 코드는 토이 프로젝트 내 DTO 클래스 일부이다.
앞서 설명한 것처럼 정적 팩토리 메서드인 of() 메서드 내에서 생성자를 호출하여 간접적으로 객체를 생성하는 패턴임을 알 수 있다.
그렇다면 대체 왜 정적 팩토리 메서드를 권장하는 것일까?
1-1. 이름을 가질 수 있다.
객체는 생성 목적에 따라 생성자를 오버로딩하여 구분하여 사용했다. new 키워드를 통해 객체를 생성할 경우, 개발자는 생성자의 내부 구조를 알고 있어야 객체를 올바르게 생성할 수 있다는 번거로움이 있었다. 하지만 정적 팩토리 메서드는 이름을 가질 수 있다.
만약 member 객체를 생성함에 있어 member의 name은 필수적으로 필요하지만, teamName의 경우 기본적으로 A팀이고, 그 외에 추가할 수 있다고 가정하자. 이 경우 name은 필수속성이고, teamName은 선택속성이 된다.
new 키워드의 경우 생성자를 오버로딩하여 객체를 생성할 수 있지만, 생성자의 기본 속성(클래스 명과 같음)의 이유로 인하여 반환될 객체의 특성을 제대로 표현할 수 없다.
class Member {
private String name; //필수속성
private String teamName; //선택속성
// private 생성자
private Member(String name, String teamName) {
this.name = name;
this.teamName = teamName;
}
// 정적 팩토리 메서드 (매개변수 하나는 from 네이밍)
public static Member teamAFrom(String name) {
return new Member(name, "A");
}
// 정적 팩토리 메서드 (매개변수 여러개는 of 네이밍)
public static Member Of(String name, String teamName) {
return new Member(brand, teamName);
}
}
그러나 위 예시처럼 정적 팩토리 메서드를 사용하여 생성될 객체에 대한 설명을 메서드 명에서 충분히 설명한다면, 코드의 가독성을 높여주고 협업을 함에 있어 개발자들이 일일히 내부 코드를 뜯어보지 않더라도 의미를 파악할 수 있다는 편의성을 제공할 수 있다.
1-2. 하위 자료형 객체를 반환할 수 있다.
클래스의 다형성의 특징을 응용한 정적 팩토리 메서드의 특징이다. 이는 생성자 역할을 하는 정적 팩토리 메서드가 반환값을 가지고 있기 때문에 가능한 것이다.
interface User {
public static Member getMember() {
return new Member();
}
public static Admin getAdmin() {
return new Admin();
}
}
1-3. 인자에 따라 다른 객체를 반환하도록 분기할 수 있다.
1-2.의 확장 예제로 볼 수 있다. 메서드이니 매개변수를 받을 수 있기 때문에 분기를 통해 다른 객체를 반환하도록 할 수 있다.
interface User {
public static User getUser(String role) {
if (role.equals("MEMBER") {
return new Member();
}
if (role.equals("ADMIN") {
return new Admin();
}
}
}
1-4. 객체 생성을 캡슐화 할 수 있다.
생성자를 사용하는 경우 외부에 내부 구현을 드러내야 하는데, 정적 팩토리 메서드는 구현부를 외부로부터 숨길 수 있어 캡슐화 및 정보은닉을 할 수 있다는 특징이 있다.
웹 어플리케이션을 개발하는 과정에서 각 계층간 데이터 전송을 할 경우 DTO 클래스를 사용한다. DTO와 Entity간에는 자유롭게 형 변환이 가능해야 하는데, 정적 팩토리 메서드를 사용하면 내부 구현을 모르더라도 쉽게 변환할 수 있다.
만약 new 키워드를 사용할 경우 외부에서 생성자 내부 구현을 모두 드러내야 할 것이다.
public record MemberResponse(
Long id,
String name
) {
public static MemberResponse of(Member member) {
return new MemberResponse(
member.getId(),
member.getName()
);
}
}
//Member Entity를 MemberResponse(DTO로 변환)
MemberResponse memberResponse = MemberResponse.of(member);
//new키워드 사용
MemberResponse memberResponse = new MemberResponse(member.getId(), member.getName());
2. 빌더 패턴 (Builder pattern)
복합 객체의 생성 과정과 표현 방법을 분리하여 동일한 생성 절차에서 서로 다른 표현 결과를 만들 수 있게 하는 디자인 패턴이다
앞서 설명한 것처럼 new 키워드를 사용한 생성자 사용 기법이나 정적 팩토리 메서드 기법은 객체에 전달해야 할 매개변수 중 선택적 매개변수가 많을 경우 유연성이 떨어진다. 그리고 이러한 단점을 빌더 패턴으로 보완할 수 있다.
먼저 예시를 살펴보자.
@Builder
public class Member {
private String name; //필수속성
private String teamName; //필수속성
private int age; //선택속성
private String address; //선택속성
}
//Builder pattern 호출
Member member = Member.builder()
.name("날아")
.teamName("A")
.age(11)
.address("서울")
.build();
빌더패턴은 개발자가 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자나 정적 팩토리 메서드를 호출하여 빌더 객체를 얻은 뒤, 빌더 객체가 제공하는 일종의 setter 메서드를 통해 선택적 매개변수들을 설정하고 마지막으로 build 메서드를 호출하여 객체를 얻게 된다.
이러한 빌더패턴은 보통 클래스 내부에 정적 멤버 클래스로 정의하는게 일반적이다. 그러나 이를 직접 구현하는 것은 생성자를 오버로딩 하는 것보다 번거로울 수 있다. 직접 구현한 예를 살펴보자.
public class Member {
private String name; //필수속성
private String teamName; //필수속성
private int age; //선택속성
private String address; //선택속성
public static class Builder{
private final String name;
private final String teamName;
private final int age;
private String address;
public Builder(String name, String teamName) {
// 여기서 입력값의 유효성 검사 가능
this.name = name;
this.teamName = teamName;
}
public Builder age(int val) {
// 여기서도 입력값의 유효성 검사 가능
this.age = val;
return this;
}
public Builder address(String val) {
this.protein = val;
return this;
}
public Member build() {
return new Member(this);
}
}
private Member(Builder builder) {
// 최종적으로 builder 내부의 값들을 복사하여 불변식을 검사할 수 있다.
name = builder.name;
teamName = builder.teamName;
age = builder.age;
address = builder.address;
}
}
이를 지원하기 위해 @Builder 어노테이션을 사용한다. @Builder 어노테이션은 클래스 위에 붙여주기만 하면 자동으로 해당 클래스 내부에 Builder 클래스를 만들고 각 매개변수의 setter를 만들어주며 모든 매개변수를 받아 객체를 생성해주는 생성자 또한 자동으로 생성해준다. (선택속성을 매개변수로 넣지 않을 경우 각 자료형의 기본형이 값으로 설정된다. ex) int 형 -> 0)
빌더 패턴을 사용하면 더이상 생성자를 오버로딩하거나 정적 팩토리 메서드를 다양하게 만들지 않아도 되며, 데이터의 순서에 상관 없이 객체를 만들어내 생성자 인자 순서를 파악할 필요가 없다.
3. 정리
해당 부분들을 정리하면서 개인적인 법칙(?)을 정하자면 다음과 같다.
- 객체는 생성 의도를 드러낼 수 있는 정적 팩토리 메서드를 주로 사용하도록 한다.
- 만약 객체 생성에 필요한 인자가 많고 변화가 잦다면 빌더 패턴을 고려한다.
- 객체는 최대한 @setter를 지양한다. (만약 필요하다면 객체 클래스 내에 대체 메서드를 만든다.)
(이렇게 정리하고보니 이펙티브 자바와 똑같은 것 같다..)
참고
https://tecoble.techcourse.co.kr/post/2020-05-26-static-factory-method/
정적 팩토리 메서드(Static Factory Method)는 왜 사용할까?
…
tecoble.techcourse.co.kr
https://pamyferret.tistory.com/67
빌더 패턴(Builder pattern)을 써야하는 이유, @Builder
빌더 패턴(Builder pattern)이란? 객체를 정의하고 그 객체를 생성할 때 보통 생성자를 통해 생성하는 것을 생각한다. Bag bag = new Bag("name", 1000, "memo"); 하지만 생성자를 통해 객체를 생성하는데 몇 가
pamyferret.tistory.com
'JAVA' 카테고리의 다른 글
자료구조 Collection - List, Set, Map (0) | 2023.06.02 |
---|---|
[JAVA] 멀티쓰레드 & 동기화/비동기화 & Sync/Async (0) | 2023.02.06 |
[JAVA] Thread 란 (0) | 2023.02.06 |
[JAVA] GC 동작 알고리즘 (0) | 2023.02.05 |
[JAVA] Call by Value & Call by Reference (0) | 2023.02.05 |