[SPRING]@Builder를 야무지게 사용해보자

wannabeing·2025년 4월 9일
3

SPRING

목록 보기
5/12
post-thumbnail

@Builder는 객체를 생성할 때 빌더패턴 적용을 도와주는 어노테이션이다.

❓빌더패턴: 복잡한 객체의 생성 과정을 단순화하기 위한 디자인 패턴


✅ 장단점

가독성 👍

// 가독성이 떨어짐 및 순서 잘지켜야 함..
User user = new ("", "", "", "", "" ... );

// 가독성 좋아짐, 순서 안지켜도 상관없음
User user = User.builder().
				.hobby(hobby)
				.id(id)
                .email(email)
                .password(password)
				.build();

객체를 유연하게 생성할 수 있다!

// 현대자동차 G70 외부 하얀색, 내부 베이지색, 풀옵션
Car g70 = Car.builder().
			.brand(hyundai)
            .outColor(white)
            .innerColor(beige)
            .option(full)
            .build();
// 현대자동차 Gv80 깡통 (외/내부 검은색, 옵션X)
Car gv80 = Car.builder().
			.brand(hyundai)
            .build();

생성자가 여러개면, 더욱더 가독성과 유연성이 증가한다고 생각한다.
위와 같은 이유로 검색 도메인에서 많이 사용된다고 한다!


매개변수 디폴트 값을 간접지원한다!

// ❌ name 입력 안하면 name 기본값으로 홍길동 지정
public User (String email, String password, String name = "홍길동") {
	this.emali = email;
    this.password = password;
    this.name = name;
}

자바에서는 매개변수에 디폴트 값을 입력을 지원안한다.
빌더를 이용하면 간접적으로 매개변수 디폴트 값을 설정할 수 있다!

@Builder
class User {
    private String email;
    private String password;
    private String name = "홍길동"; // 디폴트 값 역할

    ...
}

// ✅ 실제 사용
User user = User.builder()
			.email(email)
            .password(password)
            .build(); // (default) name: 홍길동

User user = User.builder()
			.email(email)
            .password(password)
            .name("wannabeing")
            .build(); // name: wannabeing
	

속성에 대한 변경 가능성이 최소화 된다!

가능하다면 클래스의 모든 속성에 final 키워드를 사용하는 것이 좋다고 생각한다.

첫번째 이유는 객체가 불변성을 띄지 않으면 멀티쓰레드 환경에서 예외처리 하기에 어려워진다고 생각하기 때문이다. 또한 동시성 문제도 해결된다.

두번째로는 불변성을 갖고 있으면 객체의 캡슐화를 지키기 위한 좋은 방법이 될 수 있다고 생각한다.

❗️ 근데 클래스를 작성하다보면 모든 속성에 final을 붙이면
생성자가 여러개가 생길 수 있고, 그러면 가독성이 나빠지고, 코드가 길어지며
생성자 오버로딩이 많이 발생하게 된다.

하지만 @Builder를 사용하게 된다면?
생성자 오버로딩이 줄어들게 되고, 가독성이 좋아지며
코드가 기존보다 짧아진다는 장점이 있다.


생성자보다는 성능이 떨어진다!

매번 빌더를 호출하여 생성자를 통해 객체를 생성하기 때문이다.
성능을 중시하는 프로젝트에선 고려할 수도 있는 부분이다.


클래스 필드 개수가 애매하다면..?

5개가 넘어가지 않고, 필드의 변경 가능성이 적은 프로젝트라면
"정적 팩토리 메서드 패턴"를 사용하는게 좋다고 한다.


컴파일러가 에러를 잡지 못한다!

User user = User.builder().name("홍길동").build();  
// email, password 빠짐 → NullPointerException 예외 발생

빌더는 컴파일 타임에 필수값 누락을 잡아주지 못한다고 한다.
→ 필드 설정이 빠져도 문제 없이 빌드가 됨
→ 실행 중 NullPointerException 발생


❗️ 주의사항

DTO에 사용할 때는 주의하자!

@Getter
@RequiredArgsConstructor
public class FeedRequestDto {
	@NotBlank(message = "내용을 입력해주세요.")
	@Size(max = 500, message = "내용은 500자 이내로 입력해주세요.")
	private final String contents;

	@NotBlank(message = "이미지를 첨부해주세요.")
	private final String image;

	@NotBlank(message = "비밀번호를 입력해주세요.")
	@Size(max = 20, message = "비밀번호는 20자를 넘을 수 없습니다.")
	private final String password;
}

DTO(Data-Transfer-Object): 데이터를 전송하는데에 사용하는 객체이다.
따라서 DTO는 신뢰 해야하는 객체이므로, @Builder 어노테이션으로 유연하게 생성하지 못하게 막아야 한다고 생각한다.
또한 불변성을 띄어야 되므로 final 키워드를 항상 붙이고, @RequiredArgsConstructor를 사용하거나 MapStruct 라이브러리, 정적 팩토리 메서드 패턴을 사용하는 방향이 더 유용하다고 생각한다.

RequestDTO의 경우, 사용자의 입력데이터를 역직렬화할 때 사용하기 때문에
보통의 경우 객체를 생성하는 일이 없다!
따라서 RequestDTO에 붙이게 된다면 주의해서 붙이자!


클래스 단에 붙이는걸 주의하자!

@Entity
@Table(name = "feed")
@Getter
@NoArgsConstructor
@Builder // ✅ 클래스 단에 @Builder 어노테이션 작성
public class Feed extends BaseEntity{

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@Column(nullable = false, columnDefinition = "longtext")
	private String contents;

	@Column(nullable = false, columnDefinition = "text")
	private String image;
}

클래스 단에 붙이면 모든 필드가 빌더의 대상이 된다!!
Feed 클래스의 id값은 외부에서 설정하면 안되는 값이다.

따라서 클래스 단에 붙이는게 아닌, 생성자 단에 붙이는 방식이 권장된다.
아래와 같이 수정할 수 있다.

@Entity
@Table(name = "feed")
@Getter
@NoArgsConstructor
public class Feed extends BaseEntity{

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@Column(nullable = false, columnDefinition = "longtext")
	private String contents;

	@Column(nullable = false, columnDefinition = "text")
	private String image;
    
    // ✅ 빌더 생성자 
    @Builder
    public Feed (String contents, String image) {
    	this.contents = contents;
        this.image = image;
    }
}

클래스단에 붙일 수도 있다!

@Getter
@Builder
@JsonPropertyOrder({"feeds", "pages"})
public class FeedPageResponseDto {
	private final List<FeedResponseDto> feeds;
	private final Page<FeedResponseDto> pages;
}
  1. 모든 객체 생성에 통일화를 위해 빌더를 사용한다.
  2. 모든 필드가 final (필드가 많은 것도 이유가 됨)
  3. 또 다른 객체를 받는 생성자가 필요 없음.
  4. 생성자가 @RequiredArgsConstructor 하나만 필요함

위의 조건을 충족하기에 클래스 단에 @Builder를 사용했다.


@SuperBuilder !?

@Builder 클래스를 상속받은 경우에
상속받은 필드에서 부모 클래스의 필드를 빌더로 초기화할 수 없다.
그 때, @SuperBuilder를 사용한다.


@SuperBuilder는 클래스단에서만!

클래스단에서만 사용가능하다. 생성자단에서 사용할 수 없다..!


아래와 같이 사용해봤다.

// ✅ 부모 @SuperBuilder
@Getter
@SuperBuilder
public class FeedResponseDto {
	private final Long id;

	private final String contents;

	private final String image;

	@Builder.Default
	private Long likes = 0L;

	@Builder.Default
	private Long commentCount = 0L;

	@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
	private final LocalDateTime createdAt;

	@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
	private final LocalDateTime updatedAt;
}
// ✅ 자식 @SuperBuilder
@Getter
@SuperBuilder
public class FeedDetailResponseDto extends FeedResponseDto {
	private final UserProfileResponseDto user;
	private final List<CommentResponseDto> comments;
}

이제 우리는 FeedDetailResponseDto를 생성할 때, 한번에 부모 필드까지 초기화할 수 있다!!

FeedDetailResponseDto dto = FeedDetailResponseDto.builder()
			.id(feed.getId())
			.contents(feed.getContents())
			.image(feed.getImage())
			.likes(feed.getLikes())
            .commentCount(feedLikeRepository.countByFeedId(feed.getId()))
			.createdAt(feed.getCreatedAt())
			.updatedAt(feed.getUpdatedAt())
			.user(myProfile)
			.comments(comments)
			.build();

✅ @Builder에 대한 내 생각

1, 가독성이 좋아진다.
→ 필드가 많고 타입이 비슷할 때 빌더를 사용하면,
객체 생성할 때 확실히 가독성이 좋아진다!

2. 필드 값의 기본값 설정을 간접적으로 지원한다.
→ 필드 설정을 빼먹을 수도 있지만, 꼭 필요한 값만 자유롭게 설정할 수 있다는 점에서 유연하다는 장점이 있다고 생각한다.

3. DTO와 잘 맞는다. (궁합이 좋다)?
→ DTO 자체가 필드가 많고, 비슷한 타입 필드가 많다고 한다.
따라서 DTO와 잘맞는다고 볼 수 있다고 생각한다.

4. 유지보수에 유연하다.
→ 필드가 추가되더라도 수정하는데 유연하다는 느낌을 받았다.

5. 생성자가 많을수록 @Builder의 장점이 극대화된다.
→ 생성자가 많다는 건 생성할 때 고려할 필드가 많다는 뜻이다.
그 때, 빌더를 쓰면 유연하게 객체를 만들 수 있다.

6.엔티티 클래스에는 되도록 사용하지 않는 게 좋다.
→ 엔티티 클래스에는 민감한 정보가 많고, JPA와 충돌이 날 수 있어서 조심해야 한다고 한다. 생성자를 직접 만드는 게 더 좋은 방법이라 생각한다.

7. 성능에 큰 영향을 끼치지 않는다.
→ 빌더를 통해 생성자를 호출하기 때문에 성능에 영향을 끼친다고 생각이 들었지만, 튜터님들께 여쭤봤을 때, 그 영향이 미미한 수준이고 성능저하를 일으킬 정도는 아니라고 한다.


⭐️ 따라서

프로젝트 내에서 빌더패턴을 사용하게 된다면
객체를 상황에 알맞게 유연하게 생성할 수 있다고 한다.

반면에 객체를 받아 새로운 객체를 만드는 생성자가 있거나
필드 개수가 적은 프로젝트라면 고려해봐야된다고 생각한다!

알맞게 사용하자!


출처

Builder 패턴
@Builder 어노테이션
내배캠 튜터님들

profile
wannabe---ing

1개의 댓글

comment-user-thumbnail
2025년 4월 24일

엄청난 글을 보고 말았습니다..

답글 달기