본문 바로가기
JPA

[JPA] @Setter 대신 @Builder 사용해야하는 이유

by 개발LOG 2024. 2. 14.

@Setter로 생성자 이용 vs  @Builder

JUnit 테스트 할 때 @Setter를 사용하면 set문장이 많아져 코드가 길어진다. 이를 @Builder를 사용하면 한줄코드로 간편하다.  또한, 명확히 어떤 필드에 어떤 값을 채워야 할지를 알 수 있는 장점이 있다.

(@ Builder는 해당 클래스의 빌더 패턴 클래스를 생성해주고, 생성자 상단에 선언 시 생성자에 포함된 필드만 빌더에 포함한다.)

 

주의: @Builder를 사용하려면 기본생성자를 만들어주는

@NoArgsConstructor(access = AccessLevel.PROTECTED) 어노테이션도 함께 써야 한다. 

그 이유는

  1. Reflection을 통한 객체 생성: 빈 객체를 생성하고 필드 값을 설정하기 위해 Java Reflection을 사용할 때, 해당 클래스는 기본 생성자를 가져야 합니다. Lombok의 @Builder가 빈 객체를 생성할 때에도 기본 생성자를 사용하는데, 이때 @NoArgsConstructor가 필요합니다.
  2. JPA (Java Persistence API)에서의 사용: 많은 JPA 프레임워크에서는 엔티티 클래스에 기본 생성자를 요구합니다. @Builder를 사용하여 객체를 생성하는 경우, JPA가 엔티티를 올바르게 관리하기 위해서는 기본 생성자가 필요합니다.

따라서 @Builder를 사용할 때에는 해당 클래스에 기본 생성자를 제공하는 @NoArgsConstructor를 함께 사용하는 것이 권장됩니다. 이를 통해 코드의 안정성을 높일 수 있고, 다양한 상황에서 예상치 못한 문제를 방지할 수 있습니다.

 

코드예시:

엔티티 코드

package com.pnow.domain;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.util.List;
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED) //기본생성자
//@Setter
public class Category {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "category_id")
    private Long id;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private CategoryType categoryName;


    @OneToMany(mappedBy = "category", cascade = CascadeType.REMOVE)
    private List<Store> storeList;

    @Builder //JUnit 테스트용
    public Category(CategoryType categoryName) {
        this.categoryName = categoryName;
    }

}

 

JUnit repository 테스트에서 Builder() 사용 코드

package com.pnow.repository;

import com.pnow.domain.Category;
import com.pnow.domain.CategoryType;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;

@Slf4j
//@ExtendWith(SpringExtension.class) //JUnit5 버전
@SpringBootTest //자동으로 h2 데이터베이스 실행함
public class CategoryRepositoryTests {
    @Autowired
    private CategoryRepository categoryRepository;

    @AfterEach //JUnit5
    public void cleanup() {
        categoryRepository.deleteAll();
    }

    @Test
    public void 카테고리저장_불러오기() {
        //given
        for (CategoryType category: CategoryType.values()) {
            log.info("category : {}",category);
            categoryRepository.save(Category.builder()
            .categoryName(category)
            .build());
        }

        //when
        List<Category> categoryList = categoryRepository.findAll();

        //then
        Category category = categoryList.get(0);
        assertEquals(CategoryType.한식, category.getCategoryName());

    }


}

 

생성자나 빌더나 생성 시점에 값을 채워주는 역할은 똑같다. 다만, 생성자의 경우 지금 채워야 할 필드가 무엇인지 명확히 지정할 수 없다. 

예를 들어 다음과 같은 생성자가 있다면 개발자가 new Example(b, a) 처럼 a와 b의 위치를 변경해도 코드를 실행하기 전까지는 문제를 찾을 수가 없다.

public Example(String a, String b) {
	this.a=a;
	this.b=b;
}

 

하지만 빌더를 사용하게 되면 다음과 같이 어느 필드에 어떤 값을 채워야 할지 명확하게 인지할 수 있다.

Example.builder()
	.a(a)
	.b(b)
	.build();