메서드 오버로딩을 하다보면 여러 매개변수를 받는 생성자나 메서드를 여러 개 작성하다 보니 매개변수의 순서나 타입에 의한 혼동이 생길 수 있다.
많은 매개변수를 갖는 생성자를 사용하면 코드의 가독성도 떨어지고 잘못된 타입을 넣었을 때 오류를 발생할 수도 있다.
public class Car {
private String model;
private String color;
private int year;
public Car(String model) {
this.model = model;
}
public Car(String model, String color) {
this.model = model;
this.color = color;
}
public Car(String model, String color, int year) {
this.model = model;
this.color = color;
this.year = year;
}
}
위와 같은 경우 Car 객체를 생성할 때 필요한 매개변수를 정확히 맞추는 것이 쉽지 않고 나중에 객체의 속성이 추가되면 이 생성자들을 계속 오버로딩해야 할 수 있다.
또한 매개변수의 순서나 타입을 실수로 잘못 전달할 위험이 있다.
빌더 패턴을 사용하면 이런 메서드 오버로딩의 문제를 해결할 수 있다.
간단히 알아보고 실제로 사용해보자.
빌더 패턴을 사용하는 이유?
빌더 패턴을 통해 객체 생성 과정에서 복잡성 관리와 가독성 및 유지보수성을 늘릴 수 있다.
위에도 설명했듯이 빌더 패턴은 주로 객체의 속성이 많거나 일부 속성이 선택적인 경우에 유용하다.
객체의 생성을 단계별로 설정할 수 있도록 하므로 불변 객체를 만들거나 객체 생성 시 실수를 줄이는 데 효과적인 패턴이다.
빌더 패턴 사용 방법
빌더 패턴을 구현려면 주요 객체 클래스 안에 빌더 클래스를 정의하고 빌더 클래스를 통해 객체를 생성하는 방식으로 사용한다.
객체 생성자는 private으로 설정하여 외부에서 직접 생성이 불가능하게 하고 빌더 클래스를 통해서만 객체를 생성하도록 한다.
내부 클래스로 빌더 정의 -> 빌더 클래스의 각 메서드가 자신을 반환하도록 함 -> build() 메서드를 호출하여 객체 생성
이라고 간단하게 정리할 수 있다.
간단하게 Person 객체를 생성하는 예시를 들어보면,
public class Person {
private String name;
private int age;
private String address;
private String phone;
private Person(PersonBuilder builder) {
this.name = builder.name;
this.age = builder.age;
this.address = builder.address;
this.phone = builder.phone;
}
public static class PersonBuilder {
private String name;
private int age;
private String address;
private String phone;
public PersonBuilder name(String name) {
this.name = name;
return this;
}
public PersonBuilder age(int age) {
this.age = age;
return this;
}
public PersonBuilder address(String address) {
this.address = address;
return this;
}
public PersonBuilder phone(String phone) {
this.phone = phone;
return this;
}
public Person build() {
return new Person(this);
}
}
}
코드를 보면 생성자를 private으로 선언되어 직접 호출할 수 없고 빌더 객체를 통해서만 생성할 수 있게 하였다.
그리고 Person 객체를 생성하기 위해서 내부 클래스인 PersonBuilder를 생성하였다.
이 클래스를 통해 객체의 속성을 단계적으로 설정할 수 있는 메서드를 제공한다.
첫 번째 빌더 메서드를 보면 PersonBuilder 객체의 name 필드에 매개변수로 전달된 값을 설정한다.
메서드 체이닝이 가능하도록 this를 반환한다. 여기선 this는 현재 PersonBuilder 객체.
이런 식으로 나머지도 수행하고 빌더 패턴을 사용하여 Person 객체를 생성해주면 된다.
Person person = new Person.PersonBuilder()
.name("홍성민")
.age(25)
.address("서울시")
.phone("010-1234-5678")
.build();
new Person.PersonBuilder()을 통해 객체를 생성하고 이후 메서드가 호출되며 각 필드에 값이 설정된다.
마지막에 .build()를 써주면서 PersonBuilder 객체의 데이터를 기반으로 Person 객체가 생성된다.
빌더 패턴을 레고 블록 조립에 비유를 들어보자. 각 블록이 레고의 한 부분을 구성한다.
우린 블록을 하나씩 선택해서 필요한 형태의 물건을 만드는데, 마찬가지로 빌더 패턴에선 객체를 하나씩 설정하고 마지막에 완성된 객체를 반환하는 것이다.
스프링부트에서의 빌더 패턴 사용
스프링 부트 개발 공부를 하다보면 빌더 패턴은 주로 DTO나 JPA 엔티티 등을 생성할 때 사용된다.
복잡한 객체를 생성할 때 가독성과 유지보수성을 높이려면 필요한 패턴이다.
데이터 전송 객체인 DTO는 여러 필드를 가지기 때문에 객체 생성할 때 어떻게 하는 지 코드로 간단히 구현해보겠다.
public class ProductDTO {
private Long id;
private String name;
private double price;
public static class ProductDTOBuilder {
private Long id;
private String name;
private double price;
public ProductDTOBuilder id(Long id) {
this.id = id;
return this;
}
public ProductDTOBuilder name(String name) {
this.name = name;
return this;
}
public ProductDTOBuilder price(double price) {
this.price = price;
return this;
}
//private 생성자 제공하지 않은 관계로 필드 값 수동 복사
public ProductDTO build() {
ProductDTO productDTO = new ProductDTO();
productDTO.id = this.id;
productDTO.name = this.name;
productDTO.price = this.price;
return productDTO;
}
}
}
여기서 build() 메서드가 길어졌는데 DTO는 데이터 전송 외의 목적으로 강하게 연결되지 않도록 하기 위해서다.
이외에도 JPA에서 Specification 객체나 Criteria 객체를 생성할 때 빌더 패턴을 사용하면 복잡한 쿼리를 명확하게 작성할 수 있고
스프링의 @Configuration 클래스에서 빌더 패턴을 사용해서 복잡한 설정 객체를 설정할 수 있다.
Lombok을 이용한 빌더 패턴
Lombok 라이브러리를 통해 빌더 패턴을 더 간단하게 구현할 수도 있다.
Lombok의 @Builder 어노테이션을 사용하면 되는데,
import lombok.Builder;
@Builder
public class Product {
private Long id;
private String name;
private double price;
}
이렇게 @Builder 어노테이션을 하나만 추가하면 Lombok이 자동으로 빌더 클래스를 생성해준다.
코드 안에서 굳이 직접 작성할 필요 없이 간단하게 객체 생성이 가능한 것!
Product product = Product.builder()
.id(1L)
.name("Laptop")
.price(1200.00)
.build();
이렇게 바로 생성이 가능하다.
'Language > Java' 카테고리의 다른 글
[Java] Map을 왜 사용할까? (0) | 2025.03.17 |
---|---|
[Java] toString을 왜 쓸까? (0) | 2025.01.07 |
[Java] 예외처리 코드 연습 (0) | 2025.01.05 |
[Java] 다형성의 본질과 활용 (0) | 2024.12.31 |
[Java] String / StringBuffer / StringBuilder 차이점 (0) | 2024.10.04 |