공부 기록

[프로젝트] builder와 BeanUtils.copyProperties() 본문

프로그래밍/프로젝트

[프로젝트] builder와 BeanUtils.copyProperties()

I'm_ 2024. 1. 30. 15:21

✅ 들어가며

첫 번째 Pull Request에서 받은 코드리뷰를 통해 고민해 오던 Entity에 Setter를 없애는 방법에 대한 힌트를 얻게 되었다.

나는 프로젝트에서 Entity↔DTO 변환을 위해 BeanUtils.copyProperties()를 사용하였다. 해당 방법을 사용하면서 Entity에 Setter를 생성해야 했어서 이전에 공부한 도메인 중심 개발과 상충되는 부분이 생겨 고민을 했었는데, 송아쌤과 스터디메이트의 코드를 보고 builder에 대해 공부해 보고 적용해보고자 한다.

 

✅ Builder

생성자에 @Builder를 붙여주면 빌더 패턴 코드가 빌드된다.

  • 각 인자가 어떤 의미인지 알기 쉽다.
  • Setter가 없으므로 불변 객체를 만들 수 있다.
  • 한 번에 객체를 생성하므로 객체 일관성이 깨지지 않는다.
  • build() 함수가 잘못된 값이 입력되었는지 검증하게 할 수도 있다.

 

🖥️ 변경 전 코드

@Entity
@Getter //lombok 어노테이션 : 클래스 내 모든 필드의 Getter 메서드 자동 생성
@Setter
public class Food {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private long foodId;

    @Column(nullable = false, length = 50)
    private String foodName;

    @Column(nullable = false)
    private int price;

    @Column(nullable = false)
    private String foodDescription;

}
//단일 음식 조회
    public FoodDto findFood(long id){
        Food food = foodRepository.findById(id).get();
        FoodDto foodDto = new FoodDto();
        BeanUtils.copyProperties(food, foodDto);
        return foodDto;
    }

 

🖥️  변경 후 코드

@Entity
@Getter //lombok 어노테이션 : 클래스 내 모든 필드의 Getter 메서드 자동 생성
@NoArgsConstructor
public class Food {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private long foodId;

    @Column(nullable = false, length = 50)
    private String foodName;

    @Column(nullable = false)
    private int price;

    @Column(nullable = false)
    private String foodDescription;

    @Builder
    Food (long foodId, String foodName, int price, String foodDescription){
        this.foodId = foodId;
        this.foodName = foodName;
        this.price = price;
        this.foodDescription = foodDescription;
    }
}
public FoodDto findFood(long id){
    Food food = foodRepository.findById(id).get();
    FoodDto foodDto = FoodDto.builder()
            .foodId(food.getFoodId())
            .foodName(food.getFoodName())
            .price(food.getPrice())
            .foodDescription(food.getFoodDescription())
            .build();
    return foodDto;
}

 

인자가 있는 생성자에 @Builder를 붙이고 나서 테스트를 해보았을 때, Entity→DTO가 있는 GET, DTO→Entity가 있는 POST 모두에서 에러가 발생하였다.

코드 리뷰를 참고해서 기본생성자를 추가하니 테스트를 모두 통과해서 기본생성자가 필요한 이유에 대해서 좀 더 공부해보았다.

✅ 기본생성자 (@NoArgsConstructor)

📌 기본 생성자를 강제하는 경우

  • @RequestBody를 객체(DTO)로 바인딩하는 과정에서 기본 생성자가 존재하지 않는다면 정상적으로 바인딩되지 않는다. (GET메서드에서 에러가 발생한 이유)
    이는 스프링의 @RequestBody 바인딩 방식이 기본 생성자를 통해 객체를 생성한 후 Java Reflection을 이용해 필드 값을 집어넣어 주는 방식이기 때문이다. Reflection은 클래스의 이름만 알면 생성자, 필드, 메서드와 같은 클래스의 모든 정보에 접근이 가능하지만 생성자의 매개변수 정보는 가져올 수 없다. 

  • JPA가 DB에서 데이터를 조회한 후 객체를 생성할 때도 Reflection을 사용하므로 JPA에서는 기본 생성자를 반드시 생성해야 한다. (POST메서드에서 에러가 발생한 이유)
❗Reflection이란?
- 구체적인 클래스 타입을 알지 못해도 그 클래스의 메서드, 타입, 변수들을 접근할 수 있도록 해주는 자바 API
- 컴파일 시간이 아닌 실행 시간에 동적으로 특정 클래스의 정보를 추출해 낼 수 있는 프로그램 기법

 


📚 참고 자료