최근에 프로젝트를 진행하면서 DTO 클래스 혹은 Entity 클래스에서 Lombok라이브러리의 @Setter @Getter @Builder @Data @ToString @AllArgsConstructo @NoArgsConstructor 등의 어노테이션을 사용할 때 해당 어노테이션의 대략적인 역할만을 알고 난발하며 사용했다.
엊그제 유튜브에서 백엔드 개발자들이 성능개선을 위해 엄청나게 노력하고 있다는 얘기를 보고 나름 기본적이라고 생각해온 것들이 정확히 어떤 구조를 가지고 있는지, 장점, 단점, 성능에 미치는 영향에 대해 공부할 필요가 있다고 느껴 조금 알아보고자 한다.
빌더 패턴(Builder Pattern)
기존 패턴과 빌더 패턴의 차이
점층적 생성자 패턴(Telescoping Constructor Pattern)
정의)
필수 매개변수와 선택 매개변수를 0개부터 모두 받는 형태로 다양한 매개변수를 입력받아 인스턴스를 생성할 때 사용하던 생성자를 오버로딩하는 형식이다
문제점)
클래스 인스턴스 필드들이 많을 수록 생성자에 들어갈 인자의 수가 늘어가 어떤 인자가 어떤 필드인지 헷갈릴 수 있으며 파악이 어려워진다. 또한 타입이 다양할 수록 생성자 메서드 수가 늘어나 가독성과 유지보수 측면에서 좋지 않다.
자바 빈 패턴(Java Beans pattern)
정의)
점층적 생성자 패턴의 단점을 보완하기 위해 Setter 메서드를 사용한 자바 빈(Bean) 패턴이 고안 되었다. 생성자엔 매개변수가 없으며 객체 생성 후 Setter 메서드를 이용해 클래스 필드의 초기값을 설정하는 형식이다.
문제점)
기존 생성자 오버로딩에 배해 가독성이 증가하고 선택 매개변수에 해당되는 Setter 메서드를 호출해 유연하게 객체생성이 가능하지만 객체 생성 시점에 모든 값들을 주입하지 않아 일관성과 불변성의 문제가 발생한다.
빌더 패턴(Builder Pattern)
정의)
별도의 Builder 클래스를 만들어 메서드를 통해 step-by-step으로 값을 입력 받은 후 최종적으로 build() 메서드로 하나의 인스턴스를 생성해 리턴하는 형식이다.
이점)
생성자 오버로딩 열거 하지 않아도 되며 데이터 순서에 상관없이 객체를 만들어 생성자 인자 순서를 파악할 필요가 없으며 잘못된 값을 넣는 실수를 하지 않게 된다.
쉽게 말해 위 2개의 패턴의 장점만을 가져왔다고 볼 수 있다.
빌더 패턴 구조
class Student {
private int id
private String name = "홍길동";
private String grade = "freshman";
private String phoneNumber = "010-1234-1234";
public Student(int id, String name, String grade String phoneNumber) {
this.id = id;
this.name = name;
this.grade = grade;
this.phoneNumber = phoneNumber;
}
@Override
public String toString() {
return "Student { " +
"id='" + id + '\'' +
", name=" + name +
", grade=" + grade +
", phoneNumber=" + phoneNumber +
" }";
}
빌더 클래스 구현
Builder 클래스 생성 후 필드 멤버 구성을 만들 Student 클래스 멤버 구성과 똑같이 구성한다.
class StudentBuilder {
private int id;
private String name;
private Stirng grade;
private Stirng phoneNumber;
각 멤버에 대한 Setter 메서드를 구현한다. 이 때 가독성과 기존 Setter와의 차별성을 위해 메서드 이름에서 set은 제한다.
class StudentBuilder {
private int id;
private String name;
private String grade;
private String phoneNumber;
public StudentBuilder id(int id) {
this.id = id;
return this;
}
public StudentBuilder name(String name) {
this.name = name;
return this;
}
public StudentBuilder grade(String grade) {
this.grade = grade;
return this;
}
public StudentBuilder phoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
return this;
}
}
Student 객체를 만들어주는 build 메서드를 구성한다. 빌더 클래스의 필드들을 Student 생성자의 인자에 넣어 멤버 구성이 완료된 Student 인스턴스가 생성된다.
class StudentBuilder {
private int id;
private String name;
private Stirng grade;
private Stirng phoneNumber;
public StudentBuilder id(ind id) { ... }
public StudentBuilder name(String name) { ... }
public StudentBuilder grade(Stirng grade) { ... }
public StudentBuilder phoneNumber(String phoneNumber) { ... }
public Student build() {
return new Student(id, name, grade, phoneNumber);
}
}
빌더 클래스 실행
아래와 같이 구성한 빌더 객체를 실행하면
public static void main(Stirng[] args) {
Student student = new StudentBuilder()
.id(201840011)
.name("홍길동")
.grade("Freshman")
.phoneNumber("010-9876-9876")
.build();
System.out.println(student);
아래와 같은 결과가 나온다.
Student { id='201840011', name=홍길동, grade=Freshman }
빌더패턴 @NoArgsConstructor와 @AllArgsConstructor의 문제
@NoArgsConstructor와 @Builder를 함께 사용하면 오류가 발생한다.
이 오류는 직관적인 해결방안은 @AllArgsConstructor를 사용하는 것이지만 이는 위험성이 있다.
@AllArgsConstructor는 클래스에 존재하는 모든 필드에 대한 생성자를 자동으로 생성하게 되는데 선언된 필드의 순서대로 생성자를 생성하기 때문에 인스턴스 객체 생성시 변수의 순서를 바꾸면 생성자의 입력 값 순서도 바뀌게 되어 오류를 발생시킬 수 있다.
그래서 @AllArgsConstructor를 사용하지 않고 @NoArgsConstructo와 @Builder를 클래스에 놓지 않고 생성자에 놓는 것을 권장한다.