본문 바로가기
[JAVA]/JPA

Entity 클래스에서 @Builder 제대로 알고 사용하자

by 황원용 2023. 5. 23.
728x90
💡 회사에서 본격적으로 ORM을 활용하여 API를 개발하기 시작하면서 전에는 아무렇지 않게 배운 대로 따라 치던 것들이 하나하나 왜 이렇게 되는지 의심이 생기고 궁금해지기 시작했다. 그중 데이터베이스의 테이블과 매핑되는 Entity 클래스에 대해 이야기해보려고 한다. 이 글에서 정리한 Entity에 대한 정보는 과거에 내가 적은 이 글에서 그대로 가져왔다.

 

 

Entity 클래스

  • 실제 데이터베이스 테이블과 매핑되는 핵심 클래스이다.
  • 데이터베이스의 테이블에 존재하는 칼럼들을 필드로 가지는 객체이다.
  • 데이터베이스의 테이블과 1:1 매핑이 되므로 테이블이 가지고 있지 않는 칼럼을 필드로 가져서는 안 된다.
  • Entity 클래스는 데이터베이스의 영속성을 목적으로 사용하는 객체이기 때문에 요청, 응답 등에 사용되어서는 안 된다.
    • DB로부터 조회된 Entity를 그대로 View로 넘기게 된다면 불필요한 정보가 노출되는 등 코드가 지저분해질 수 있기 때문이다.
  • setter 메서드의 사용을 지양해야 한다. 변경되지 않는 인스턴스에 대해서도 setter 메서드를 사용하면 접근이 가능하기 때문에 객체의 무결성, 일관성 등 변조되지 않음을 보장할 수 없게 된다. 따라서 생성자를 이용해 초기화하여 불변 객체로 사용하는 것이 좋다.
    • Builder를 사용하면 멤버 변수가 많아지더라도 어떤 값을 어떤 필드에 넣는지 코드를 통해 육안으로 확인 가능할 뿐만 아니라, 순서도 상관이 없고, 중간에 값을 하나만 바꾸는 작업이 불가능해 setter에 비해 안전하다.

 

위 내용을 고려하여 Entity 클래스를 만들어보자.

@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    private String username;
}
  • 아주 간단한 Member Entity이다.
  • @Entity를 붙이면 JPA에게 Entity 객체임을 명시하여 관리하도록 할 수 있다.
  • 자, 여기서 필요한 애너테이션을 하나 추가해 보자.

 

 

@Entity
@Getter // 추가
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    private String username;
}
  • @Getter 애너테이션을 붙였다.
  • 이로써 외부에서 Member Entity의 id나 username과 같은 속성 값을 가져올 수 있게 되었다.
  • @Getter를 이용하여 서비스 단에서 핵심 비즈니스 로직을 처리할 때나, Dto 객체와의 매핑 작업을 할 때 Member 엔티티를 사용될 수 있게 되었다.
  • @Setter 역시 비슷한 이유로 사용할 수 있으나, setter는 존재만으로 Entity 객체의 무결성과 일관성 등 안전성을 보장할 수 없게 된다. 어디서든 함부로 쓰일 수 있기 때문이다.
    • Entity 객체를 데이터베이스 테이블과의 매핑 작업 이외의 일에 사용하게 되면 Entity 객체가 맡는 역할이 많아져 의존성이 올라가고 객체 간 결합도가 올라가 객체지향 관점에서 좋지 못한 코드가 될 수 있다.
    • 따라서 Dto 객체와 매핑을 하여 역할을 분리하는 것이다.

 

엔티티 객체를 생성하고 값을 넣어주기 위해 Setter 역할을 하는 무언가가 반드시 필요하긴 하다.  어떻게 Setter를 쓰지 않고 해결할 수 있을까? 생성자 메서드 등 여러 방법이 있지만 여기서는 @Builder에 대해 알아보자!

 

@Builder 사용하기

  • 위의 코드에 @Builder를 추가해 인텔리제이에 옮겼더니 Member에 빨간 줄이 그어졌다.
  • 인텔리제이의 설명을 읽어보자.

  • @Builder 애너테이션을 추가하니 기본생성자를 반드시 가지고 있어야 한다는 에러가 발생했다.
  • 인텔리제이 말대로 기본 생성자를 추가해 보자.

 

 

  • 에러가 사라졌다.
  • 그런데, 여기서 짚고 넘어가야 할 부분이 있다. @NoArgsConstuctor를 저대로 두게 되면 어디선가 아무런 값도 가지지 않는 의미 없는 객체의 생성 가능성을 열어두게 된다. 따라서 NoArgsConstuctor의 접근 제어를 PROTECTED로 설정하여 사전에 무분별한 객체 생성의 가능성을 차단하는 것이 좋다.

 

 

  • 이제 테스트 코드를 작성하고 실행해 보자.

 

 

  • 어라..? builder 애너테이션이 적용이 되지 않는 것 같다.
  • 급한 대로 @Setter를 추가해서 테스트를 해보았다.

 

 

constructor Member in class jpabook.jpashop.Member cannot be applied to given types;

  • 이 오류 메시지는 Member 클래스에 Long과 String이라는 두 개의 인수를 예상하는 생성자를 호출하려고 시도하였으나 실패했음을 나타낸다.
    • @Builder에서 빌더 패턴을 사용하기 위해 모든 필드를 인자로 가지는 생성자를 요구한다.
  • 위에 builder 애너테이션이 적용 안 되는 데는 다 이유가 있었다.
  • 이 문제를 해결하기 위해서는 위에 언급한 대로 모든 필드를 인자로 가지는 생성자가 필요한데 당장 떠오르는 것은 @AllArgsConstructor이다. 바로 적용해 보자.

 

 

  • 문제없이 돌아가긴 하는데 여기서 주의할 점이 있다.
  • @AllArgsConstructor는 클래스에 존재하는 모든 필드에 대한 생성자를 자동으로 생성하는 애너테이션이다.

 

  • 언뜻 보기에는 편해 보이지만 @AllArgsConstructor와 @RequiredArgsConstructor는 인스턴스 멤버의 선언 순서에 영향을 받기 때문에 변수의 순서를 바꾸면 생성자의 입력값 순서도 바뀌어 미래에 발견하기 어려운 에러를 발생시킬 수 있다.(추가적인 작업에 따라 변수의 선언 순서는 바뀔 가능성이 있기 때문이다.)
    • 위의 예시를 보면 알겠지만 선언된 변수의 순서가 바뀔 때 어떠한 경고 메시지도 발생하지 않는다.(이것이 예시가 아니라 실제 상황이라 주문량과 취소량이 뒤바뀌었다면..?)
    • 그나마 갓텔리제이는 파라미터 값 옆에 필드명을 표시해주고 있다.
    • 하지만 실무처럼 복잡한 로직과 수많은 클래스가 잔뜩 있는 상황에서는 눈치채기가 힘들어 한 순간의 실수로 치명적인 결함을 발생시킬 수 있다! 
  • 따라서, 클래스 레벨에 @Builder와 @AllargsConstructor를 사용하는 것이 아니라, 생성자를 따로 만들고 생성자에 @Builder를 사용하는 것이 좋다.
  • 결론! @Builder를 사용하기 위해 @AllargsConstructor보다는 직접 생성자를 만들자.

 

 

  • 위의 문제를 모두 해결한 @Builder를 활용한 Member Entity이다.
  • 빌더 패턴을 활용하여 Entity 객체라는 본연의 역할에 집중하면서 서비스 단에서 비즈니스 로직을 수행할 때나, DTO 객체와 매핑할 때에도 Entity 객체의 안전성을 보장할 수 있게 되었다.

 

 

빌더 패턴 사용 시 고려 사항

  • 대부분의 기술이 그렇든 빌더 패턴 역시 만능은 아니다. 
    • 코드의 복잡성이 증가한다.
    • 생성자보다 성능이 떨어진다. 매번 메서드를 호출하여 빌더를 거쳐 인스턴스화하기 때문이다.(비용은 크지 않다.)
    • 클래스 필드의 개수가 4개 미만이거나, 필드의 변경 가능성이 없다면 차라리 생성자나 정적 팩토리 메서드를 이용하는 것이 더 좋을 수 있다고 한다.

 

 

 

참고

https://siahn95.tistory.com/170#주의사항%20및%20단점-1

https://inpa.tistory.com/entry/GOF-💠-빌더Builder-패턴-끝판왕-정리#3._지나친_빌더_남용은_금지

https://velog.io/@mooh2jj/올바른-엔티티-Builder-사용법

https://yuja-kong.tistory.com/99

728x90