목표
Spring기반의 익명 게시판 서버만들기 과제를 진행했다. 과제를 제출하고 멘토님께서 코드리뷰를 해주셨는데 그 중 DTO에 @AllArgsConstructor보다는 생성자 레벨의 @Builder를 이용하는것을 권장한다는 내용이었다. 이번 글을 통해 Builder가 무엇인지, 왜 @AllArgsConstructor가 아닌 @Builder의 사용을 권장하는지 그 이유에 대해 알아보고자 한다.
Builder Pattern
누구나 한 번쯤은 객체를 생성하기위해 생성자에 많은 매개변수를 입력하다가 데이터를 잘못 전달했다거나, 객체 필드의 수정이 이루어질때마다 생성자를 변경해 기존 코드를 수정했다거나, 객체 생성을 위해 필요하지 않은 데이터를 억지로 집어넣는 등 객체 생성자와 관련하여 겪은 다양한 경험이 있을 것이다.
Builder Pattern은 이러한 문제점을 해결하기 위해 고안된 객체 생성 패턴이다. 생성자에 발이 묶여 모든 필드를 억지로 우겨넣는 것이 아니라, Builder를 이용하면 객체를 단계별로 생성할 수 있다. 아래 그림을 보면 로봇이 벨트를 지나며 단계별로 제작되는것을 볼 수 있는데 Builder도 이와 같은 개념이다.
생성자 레벨의 @Builder를 이용한 DTO 생성
이번 글은 Builder Pattern에 대한 내용을 정리하기 위한 글이 아니니 바로 본론으로 들어가보자. Builder를 사용하려면 직접 Builder 클래스를 구현하는 방법도 있지만, Spring에서는 Lombok에서 제공하는 @Builder를 사용해 손쉽게 객체의 생성자 레벨에 Builder Pattern을 적용할 수 있다. 아래는 극단적인 예시이긴 하지만 모든 필드를 String으로 가지는 ResponseDTO와 클라이언트에 응답하기 위해 DTO를 사용하는 코드이다.
@AllArgsConstructor
public class UserResponseDto {
private final String email;
private final String username;
private final String nickname;
private final String address;
}
//생성자를 이용한 DTO 생성
return new UserResponseDto(
user.getEmail(),
user.getUsername(),
user.getNickname(),
user.getAddress()
);
위와 같이 작성하면 물론 정상적으로 동작하겠지만, 실수로 DTO를 생성자의 순서를 바꿔 전달한다고 하더라도 개발 단계에서 이를 인지하기는 쉽지 않을 것이다. 아래 생성자 레벨에 @Builder를 적용한 코드를 보자
public class UserResponseDto {
private final String email;
private final String username;
private final String nickname;
private final String address;
@Builder
public UserResponseDto(String email, String password, String username, String nickname, String address) {
this.email = email;
this.username = username;
this.nickname = nickname;
this.address = address;
}
}
//Builder를 이용한 응답
return UserResponseDto.Builder()
.email(potato.getEmail())
.username(potato.getUsername())
.nickname(potato.getNickname())
.address(potato.getAddress())
.build()
전반적인 코드의 복잡성은 증가했지만, DTO를 생성하는 부분에 Builder를 이용해 가독성 높여 직관적으로 어떤 데이터에 어떤 값이 설정되는지 쉽게 파악할 수 있다. 잘못된 값이 입력될 가능성이 줄어 안전한 응답이 가능해진 것이다. DTO에 Builder를 사용하는것은 오버스펙이라는 말도 있지만, 복잡한 DTO를 생성해야할때에는 Builder를 사용하는 것이 마냥 나쁘지는 않아보인다.
클래스 레벨의 @Builder를 이용한 DTO 생성
클래스 레벨에 Builder Pattern을 이용하면 Open-Closed, 확장에 열려있고 수정에 닫혀있어야하는 원칙을 지키며 코드의 유연성을 높일 수 있다. 아래 그림을 보면 House라는 기본 외형을 기반으로 Garage, SwimmingPool 등 추가적인 구조물을 가지고 있는 집들이 있다.
개념적인 코드이지만, DTO를 이용해서 House가 어떤 구조물을 가지고 있는지 반환하는 코드이다. House에 Garage와 Garden이 동시에 있는 집을 지어 반환하고 싶다고 할때, 기본 생성자를 이용하면 아래와 같이 null값 또는 더미 데이터를 억지로 넣어 반환하거나 Garage, Garden 필드만 가지고 있는 생성자를 새로 만들어줘야한다. 하지만, 우리는 이런식으로 null값을 직접 대입해가며 코드를 더럽히고 싶지 않다. 또한, 새로운 구조물이 생겨 DTO에 추가해야할 경우 작성해놓은 코드를 모두 수정해야할 것이다.
@AllArgsConstructor
public class HouseResponse {
private House house;
private Garage garage;
private Garden garden;
private FancyStatues fancyStatues;
private SwimmingPool swimmingPool;
}
//기본 생성자를 이용한 응답
return new HouseResponse(house, garage, garden, null, null);
아래와 같이 클래스 레벨에 @Builder를 이용하면 위 문제점이 깨끗히 사라진다. Builder를 이용해 동적으로 객체를 생성하면 null값이나 더미 데이터를 직접적으로 넣어줄 필요가 없고, DTO에 필드를 추가한다고 해도 생성자를 명시적으로 사용하지 않기 때문에 기존 코드에는 영향을 미치지 않는다. 앞으로는 코드를 작성할때 복잡한 생성자에 값을 직접 넣어줘야할 필요가 생기면 Builder를 활용해 데이터를 안전하게 다루는 노력을 하도록 해야겠다.
@Builder
@AllArgsConstructor
public class HouseResponse {
private House house;
private Garage garage;
private Garden garden;
private FancyStatues fancyStatues;
private SwimmingPool swimmingPool;
}
//Builder를 이용한 응답
return HouseResponse.builder()
.house(house)
.garage(garage)
.garden(garden)
.build();
*Pattern에 대한 개념이 아직 부족해 틀린 내용이 많을 수 있습니다. 발견하신다면 댓글로 알려주시면 감사하겠습니다😊