JUINTINATION
빌더(Builder) 패턴 본문
빌더 패턴이란?
빌더 패턴은 객체를 생성할 때 생성자(Constructor)만 사용할 때 발생할 수 있는 문제를 개선하기 위해 고안되었으며, 생성기 패턴이라고도 한다. GOF 디자인 패턴 중 생성 패턴에 해당하며, 복잡한 객체를 생성하는 클래스와 표현하는 클래스를 분리하여 동일한 절차에서도 서로 다른 표현을 생성하는 방법을 제공한다.
생성자를 사용한 객체 생성
일반적인 개발에서 객체를 만드는 일반적인 방법은 new 예약어를 사용하여 클래스의 생성자를 호출하는 것이다. 리소스 풀(Resource pool)의 설정을 위한 ResourcePoolConfig 클래스를 구현할 때, 이 리소스 풀 설정 클래스에는 아래의 표와 같이 설정 가능한 멤버 변수가 있다.
멤버 변수 | 설명 | 함수 설정 | 기본값 |
name | 리소스 이름 | O | 없음 |
maxTotal | 전체 리소스 최대 크기 | X | 8 |
maxIdle | 유휴 리소스 최대 크기 | X | 8 |
minIdle | 유휴 리소스 최소 크기 | X | 0 |
이 리소스 풀 설정 클래스는 다음 코드와 같이 쉽게 구현할 수 있다. maxTotal, maxIdle, minIdle은 필수 변수가 아니므로 ResourcePoolConfig 클래스 객체를 생성할 때 생성자에 null값을 전달하여 기본값을 지정한다.
public class ResourcePoolConfig {
private static int DEFAULT_MAX_TOTAL = 8;
private static int DEFAULT_MAX_IDLE = 8;
private static int DEFAULT_MIN_IDLE = 0;
private String name;
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
public ResourcePoolConfig(String name, Integer maxTotal, Integer maxIdle, Integer minIdle) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("name should not be null");
} else {
this.name = name;
}
if (maxTotal != null) {
if (maxTotal <= 0) {
throw new IllegalArgumentException("maxTotal should be positive");
}
this.maxTotal = maxTotal;
}
if (maxIdle != null) {
if (maxIdle < 0) {
throw new IllegalArgumentException("maxIdle should not be negative");
}
this.maxIdle = maxIdle;
}
if (minIdle != null) {
if (minIdle < 0) {
throw new IllegalArgumentException("minIdle should not be negative");
}
this.minIdle = minIdle;
}
}
public String getName() {
return this.name;
}
public int getMaxTotal() {
return this.maxTotal;
}
public int getMaxIdle() {
return this.maxIdle;
}
public int getMinIdle() {
return this.minIdle;
}
}
ResourcePoolConfig 클래스에는 설정 가능한 항목이 4개뿐이기 때문에 생성자의 매개변수도 4개로 그리 많지 않다. 하지만 설정 가능한 항목의 개수가 8개, 10개 또는 그 이상으로 증가하는 경우, 생성자의 매개변수 목록이 매우 길어지기 때문에 코드의 가독서오가 사용 편의성이 나빠진다.
또한 생성자를 사용할 때 매개변수의 순서나 개수를 잘못 계산하면 잘못된 값을 전달하기 쉽기 때문에 숨겨진 버그가 발생할 수 있다. 이러한 상황을 보여주는 예제 코드는 다음과 같다.
ResourcePoolConfig config = new ResourcePoolConfig("dbconnectionpool", 16, null, 8, nu11, false, true, 10, 26, false, true);
setter 메서드를 사용한 멤버 변수 설정
설정 항목 중 name은 필수값이므로 생성자에 매개변수로 전달하여 강제로 설정하고, 나머지 항목들은 setter 메서드를 통해 사용자가 값을 바꾸도록 할 수 있다.
public class ResourcePoolConfig {
private static int DEFAULT_MAX_TOTAL = 8;
private static int DEFAULT_MAX_IDLE = 8;
private static int DEFAULT_MIN_IDLE = 0;
private String name;
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
public ResourcePoolConfig(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("name should not be null");
} else {
this.name = name;
}
}
public void setMaxTotal(int maxTotal) {
if (maxTotal <= 0) {
throw new IllegalArgumentException("maxTotal should be positive");
} else {
this.maxTotal = maxTotal;
}
}
public void setMaxIdle(int maxIdle) {
if (maxIdle < 0) {
throw new IllegalArgumentException("maxIdle should not be negative");
} else {
this.maxIdle = maxIdle;
}
}
public void setMinIdle(int minIdle) {
if (minIdle < 0) {
throw new IllegalArgumentException("minIdle should not be negative");
} else {
this.minIdle = minIdle;
}
}
public String getName() {
return this.name;
}
public int getMaxTotal() {
return this.maxTotal;
}
public int getMaxIdle() {
return this.maxIdle;
}
public int getMinIdle() {
return this.minIdle;
}
}
이와 같이 리팩터링된 ResourcePoolConfig 클래스는 다음 예제 코드와 같이 사용할 수 있다.
ResourcePoolConfig config = new ResourcePoolConfig("dbconnectionpool");
config.setMaxTotal(16);
config.setMaxIdle(8);
객체를 생성할 때 모든 값을 설정하기 위해 각 매개변수를 전부 지정할 필요가 없으며, 코드의 가독성과 사용 편의성이 훨씬 향상된다.
빌더 패턴을 이용한 매개변수 검증
빌더 패턴을 설명하는 글인데도 지금까지 빌더 패턴을 사용하지 않았다. 이는 빌더 패턴과 무관하게 생성자를 통해 필수 항목을 설정하고, setter 메서드를 통해 선택 항목을 설정하는 것으로도 요구 사항을 충족할 수 있었기 때문이다. 하지만 다음과 같은 세 가지 문제가 발생할 수 있다.
- 위의 예제와 달리 name과 같은 필수 항목이 매우 많다면 이 항목들은 모두 생성자의 매개변수로 지정해야 하는데 이러면 위에서 언급했던 생성자의 매개변수 목록이 매우 길어지는 문제가 다시 대두된다.
- 설정 항목 사이에 의존성이 있을 수 있다. 예를 들어 maxTotal, maxIdle, minIdle 중 하나가 설정되면, 나머지 두 개도 반드시 명시적으로 설정되어야 하거나 maxIdle, minIdle 값은 반드시 maxTotal 이하여야 할 수 있다. 이때 setter 메서드를 통해 항목을 임의로 설정하면 이러한 의존성이나 제약 조건을 만족하지 않는 값이 설정되어도 확인이 불가능하다.
- ResourcePoolConfig 클래스의 객체가 불변 객체여야 한다면 객체가 생성된 후에는 내부 속성값을 수정할 수 없어야 하지만, ResourcePoolConfig 클래스에서 setter 메서드가 노출된다.
public class ResourcePoolConfig {
private String name;
private int maxTotal;
private int maxIdle;
private int minIdle;
private ResourcePoolConfig(Builder builder) {
this.name = builder.name;
this.maxTotal = builder.maxTotal;
this.maxIdle = builder.maxIdle;
this.minIdle = builder.minIdle;
}
// Builder 클래스를 ResourcePoolConfig 클래스의 내부 클래스로 설계하면
// Builder 클래스를 독립적인 외부 클래스로 설계할 수 있음
public static class Builder {
private static int DEFAULT_MAX_TOTAL = 8;
private static int DEFAULT_MAX_IDLE = 8;
private static int DEFAULT_MIN_IDLE = 0;
private String name;
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
public ResourcePoolConfig build() {
// 필수 항목, 의존성, 제약 조건 등을 확인
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("...");
}
if (maxIdle > maxTotal) {
throw new IllegalArgumentException("...");
}
if (minIdle > maxTotal || minIdle > maxIdle) {
throw new IllegalArgumentException("...");
}
return new ResourcePoolConfig(this);
}
public Builder setName(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("name should not be null");
} else {
this.name = name;
}
return this;
}
public Builder setMaxTotal(int maxTotal) {
if (maxTotal <= 0) {
throw new IllegalArgumentException("maxTotal should be positive");
} else {
this.maxTotal = maxTotal;
}
return this;
}
public Builder setMaxIdle(int maxIdle) {
if (maxIdle < 0) {
throw new IllegalArgumentException("maxIdle should not be negative");
} else {
this.maxIdle = maxIdle;
}
return this;
}
public Builder setMinIdle(int minIdle) {
if (minIdle < 0) {
throw new IllegalArgumentException("minIdle should not be negative");
} else {
this.minIdle = minIdle;
}
return this;
}
}
public String getName() {
return this.name;
}
public int getMaxTotal() {
return this.maxTotal;
}
public int getMaxIdle() {
return this.maxIdle;
}
public int getMinIdle() {
return this.minIdle;
}
}
// minIdle > maxIdle이므로 IIlegalArgumentException 예외 발생
ResourcePoolConfig config = new ResourcePoolConfig.Builder()
.setName("dbconnectionpool")
.setMaxTotal(16)
.setMaxIdle(10)
.setMinIdle(12)
.build();
빌더 패턴을 사용하여 구현된 이 코드에서는 모든 유효성 검사 로직을 빌더 패턴의 Builder 클래스에 넣는다. 먼저 Builder 클래스의 객체를 생성하고 setter 메서드를 통해 Builder 클래스 객체의 속성값을 설정한 다음, 실제 객체를 생성하기 위해 build() 메서드를 사용하기 전에 집중적으로 확인하는 과정을 거친다.
또한 ResourcePoIConfig 클래스의 생성자의 접근 권한은 private이기 때문에, ResourcePooIConfig 클래스의 객체는 빌더만 만들 수 있다. 또한 ResourcePoolConfig 클래스는setter 메서드를 제공하지 않으므로, 이렇게 생성된 ResourcePoolConfig 클래스의 객체는 불변 객체다.
Lombok의 @Builder 어노테이션
Spring을 공부하다보면 Lombok을 자주 활용하게 될 것이다. Lombok이란 어노테이션 기반으로 코드 자동완성 기능을 제공하는 라이브러리로 Getter, Setter, ToString, Constructor(생성자) 등과 같은 반복되는 코드를 줄여 가독성을 높일 수 있다.
Lombok 어노테이션 중 @Builder 어노테이션으로 해당 클래스에 빌더 패턴을 사용할 수 있으며, @Getter 어노테이션으로 getter 메서드를 자동으로 생성되게 만들 수 있다.
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class ResourcePoolConfig {
private static int DEFAULT_MAX_TOTAL = 8;
private static int DEFAULT_MAX_IDLE = 8;
private static int DEFAULT_MIN_IDLE = 0;
private String name;
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
}
위의 코드를 delombok해보면 아래의 코드와 같다.
public class ResourcePoolConfig {
private static int DEFAULT_MAX_TOTAL = 8;
private static int DEFAULT_MAX_IDLE = 8;
private static int DEFAULT_MIN_IDLE = 0;
private String name;
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
ResourcePoolConfig(String name, int maxTotal, int maxIdle, int minIdle) {
this.name = name;
this.maxTotal = maxTotal;
this.maxIdle = maxIdle;
this.minIdle = minIdle;
}
public static ResourcePoolConfigBuilder builder() {
return new ResourcePoolConfigBuilder();
}
public String getName() {
return this.name;
}
public int getMaxTotal() {
return this.maxTotal;
}
public int getMaxIdle() {
return this.maxIdle;
}
public int getMinIdle() {
return this.minIdle;
}
public static class ResourcePoolConfigBuilder {
private String name;
private int maxTotal;
private int maxIdle;
private int minIdle;
ResourcePoolConfigBuilder() {
}
public ResourcePoolConfigBuilder name(String name) {
this.name = name;
return this;
}
public ResourcePoolConfigBuilder maxTotal(int maxTotal) {
this.maxTotal = maxTotal;
return this;
}
public ResourcePoolConfigBuilder maxIdle(int maxIdle) {
this.maxIdle = maxIdle;
return this;
}
public ResourcePoolConfigBuilder minIdle(int minIdle) {
this.minIdle = minIdle;
return this;
}
public ResourcePoolConfig build() {
return new ResourcePoolConfig(this.name, this.maxTotal, this.maxIdle, this.minIdle);
}
public String toString() {
return "ResourcePoolConfig.ResourcePoolConfigBuilder(name=" + this.name + ", maxTotal=" + this.maxTotal + ", maxIdle=" + this.maxIdle + ", minIdle=" + this.minIdle + ")";
}
}
}
앞서 정리한 Builder 패턴 코드와의 차이점을 살펴보자.
- ResourcePoIConfig 클래스의 생성자의 매개변수가 Builder 클래스가 아니다. 그렇기 때문에 기본값으로 정해둔 maxTotal, maxIdle, minIdle 변수들을 기본값으로 설정하기 위해서
.maxTotal(ResourcePoolConfig.DEFAULT_MAX_TOTAL)
과 같이 작성해야 한다. - 또한 ResourcePoIConfig 클래스의 생성자의 접근 권한은 private이 아니기 때문에 외부에서 생성자를 통해 ResourcePoIConfig 객체를 만들 수도 있다.
- ResourcePoolConfig 클래스 내부에 정적(static) 메서드인 builder()를 호출하여 ResourcePoolConfigBuilder 클래스를 가져온다. 위에서 사용한 예시처럼 다음과 같이 new 키워드를 사용하여 ResourcePoolConfig 클래스의 정적(static) 내부 클래스(static nested class)인 ResourcePoolConfigBuilder를 직접적으로 참조하지 않고 사용할 수 있다.
ResourcePoolConfig config = ResourcePoolConfig.builder()
.setName("dbconnectionpool")
.setMaxTotal(16)
.setMaxIdle(12)
.setMinIdle(10)
.build();
결론
최근 스프링을 공부하면서 자주 보게된 반가운 디자인패턴, Builder 패턴에 대해 알아보았다. @Builder 어노테이션은 어떤 객체를 생성할 때 쉽고 가독성 좋게 만드는 용도로만 적용했는데 이렇게 공부를 하면서 뜯어보니 재밌기도 했고 다른 어노테이션에 대한 궁금증도 커졌다.
위에서 정의한 build 메서드의 제약조건 등을 지키려면 오버라이딩만 적용하면 될 것 같은데 이와 같이 앞으로도 원하는 기능을 최대한 간결하고 가독성 좋게 작성해야겠다는 생각이 들었다.
'StudyNote' 카테고리의 다른 글
Swagger를 활용한 API Specification (2) | 2024.07.01 |
---|---|
비지터(Visitor) 패턴 (1) | 2024.06.26 |
스프링부트 + MySQL 프로젝트 도커라이징하기 (1) | 2024.03.11 |
Mac OS 자바 버전 여러 개 관리하기 (0) | 2024.02.14 |
Express.js를 사용한 스퍼트 프로젝트 (2) | 2024.02.08 |