[Spring] 올바른 Lombok 사용법/Dto,toEntity() - @Builder

hyewon jeong·2023년 1월 26일
5

Spring

목록 보기
30/55
post-thumbnail

💡 올바른 Lombok 사용법 - @Builder

Entity 클래스를 작성할 때 어노테이션을 많이 씁니다.

아래와 같이 많이 쓰게 되는데요.

@Getter
@Setter // 문제 1. 객체가 무분별하게 변경될 가능성 있음
@NoArgsConstructor // 문제 2. 기본 생성자의 접근 제어자가 불명확함
@Builder
@AllArgsConstructor // 문제3. 객체 내부의 인스턴스멤버들을 모두 가지고 있는 생성자를 생성 (매우 위험)
@Entity
public class Member

이 어노테이션에서 문제 3가지가 보입니다. 이를 개선하기 위해 해결방법을 정리해봅니다.

해결 1. @Setter를 사용하지 않기

Setter는 그 의도가 분명하지 않고 객체를 언제든지 변경할 수 있는 상태가 되어서 객체의 안전성이 보장받기 힘듭니다. 특히 엔티티에서는 @Setter를 사용 시 해당 변경 가능성이 어디서 누구에 의해 발생했는지 추적하기가 힘들어진다.

때문에 값 변경이 필요한 경우 의미 있는 메서드를 생성하여 이를 사용하는 것이 좋습니다.

해결법 : 의미있는 메서드로 생성하자.

해결 2.❤️@NoArgsConstructor(access = AccessLevel.PROTECTED)로 변경

기본 생성자(NoArgsConstructor)의 접근 제어를 PROCTECTED 로 설정하면 아무런 값도 갖지 않는 의미 없는 객체의 생성 무분별하게 생성하는 것을 막을 수 있다.

예 
@NoArgsConstructor (access = AccessLevel.PROTECTED)
class User{
...}
post entity
User user = new User(); -> 빈값생성자로 컴파일에러남 
//@NoArgsConstructor(access = AccessLevel.PROTECTED) 

Member member = new Member(); //컴파일 에러 발생

이때, '의미있는 객체' 생성을 위해서 @Builder을 사용할 수 있습니다.

@Builder를 사용하는 방법은 총 2가지인데,
1) 클래스에 @Builder를 붙이기
2) 생성자에 @Builder를 붙이기

❤️ 번외 - NoargsConstructor에 access = AccessLevel.PROTECTED은 왜 붙이는거지?

오픈소스를 보다보면, NoargsConstructor에 접근 제어자를 걸어둔 경우가 있다. 접근 제어자가 추가된 이유는 다음과 같다.

JPA Proxy 때문에 어쩔 수 없이 매개변수가 없는 기본 생성자를 만들었는데, 이걸 어디선가 잘못사용하면 불완전한 객체가 만들어질 수 있다.

그래서 매개변수가 없는 기본 생성자를 다른데에서 함부로 가져다 못쓰도록 막고 싶은 경우가 있다.

그러면 기본 생성자를 private으로 만들어야 할 것이다.
그렇다고 private으로 되어있으면 JPA가 프록시를 만들 때 접근하지 못해 객체를 생성하질 못하게 된다.
따라서, 스펙에서는 기본 생성자 접근을 protected로 열어두길 권장하고 있다.

❤️ @NoArgsContructor를 사용할 때 주의점

예 )
dto 멤버변수에 final 을 붙이는 이유 :
다른 팀원이 실수로 변경할 수 있기 때문에 미리 문제예방 ! 하기 위함

  1. 필드들이 final로 생성되어 있는 경우에는 필드를 초기화 할 수 없기 때문에 생성자를 만들 수 없고 에러가 발생하게 됩니다.

이 때는 @NoArgsConstructor(force = true) 옵션을 이용해서 final 필드를 0, false, null 등으로 초기화를 강제로 시켜서 생성자를 만들 수 있습니다.

해결 3. @AllArgsConstructor는 쓰지 않기

만약 해결 2의 방법1(클래스에 @Builder를 붙이기)을 사용, 클래스 레벨에서 @Builder와 @NoArgsConstructor를 함께 쓰면 오류가 발생합니다.

이를 해결하기 위해서는 모든 필드를 가지는 생성자를 만들어주어야 하는데 @AllArgsConstructor도 같이 써주게 됩니다.

이유는 @Builder를 조금 더 살펴보면 이해할 수 있다. @Builder는 생성자 유무에 따라 다음과 같이 동작한다.

  • 생성자가 없는 경우 : 모든 멤버 변수를 파라미터로 받는 기본 생성자 생성
  • 생성자가 있을 경우 : 따로 생성자를 생성하지 않음
    생성자가 있는 경우 @Builder
    즉, 기본 생성자가 존재하기 때문에 @Builder에서는 생성자를 별도로 생성하지 않는데, 이 기본 생성자에는 접근 제한 속성이 부여되어있어 문제가 발생하는 것이다. 이 두 개의 어노테이션을 부여한 클래스를 컴파일 해보면 문제가 발생하는 위치를 쉽게 확인할 수 있다.
public class User {
	private String name;
    private String gender;
    private String phone;
    private int age;

    // @NoArgsConstructor(access = AccessLevel.PROTECTED)로 생성된 생성자
    protected User() {}

    public static User.UserBuilder builder() {
        return new User.UserBuilder();
    }

    public static class UserBuilder {
        private String name;
        private String gender;
        private String phone;
        private int age;

        UserBuilder() {
        }

        public User.UserBuilder name(String name) {
            this.name = name;
            return this;
        }
        
        public User.UserBuilder gender(String gender) {
            this.gender = gender;
            return this;
        }
        
        public User.UserBuilder phone(String phone) {
            this.phone = phone;
            return this;
        }
        
        public User.UserBuilder age(int age) {
            this.age = age;
            return this;
        }

        public User build() {
            /// 일치하는 생성자가 없어 문제 발생
            return new User(this.name, this.gender, this.phone, this.age);
        }
    }
}

결국 문제는 일치하는 생성자가 없어 발생하는 문제이니 생성자를 만들어주면 되는게 아닌가?! 이 말을 다른 말로 하자면, 모든 필드를 파라미터로 가지는 @AllArgsConstructor 하나만 추가해주면 해결할 수 있다는 의미다.

하지만 @AllArgsConstructor 는 위험합니다.
@AllArgsConstructor는 선언된 필드의 순서대로 생성자를 생성해준다. 그리고 이를 이용하여 순서대로 파라미터를 지정하여 객체를 만든다고 가정했을 때, 제 3자가 선언된 필드 사이에 임의의 필드를 넣는다면 객체를 만드는 코드에서도 수정이 일어나야하며 협업하는 부분에서 문제가 발생한다.

그래서
해결 2의 방법2(생성자에 @Builder를 붙이기)를 사용해서
@AllArgsConstructor를 쓰는 일이 없도록 합니다.

💡 DTO , Entity

Repository에서 DTO를 반환해주면 안될까?

결론부터 말하자면, 이 방법은 지양해야 한다.

Repository는 Aggregate의 영속성과 Repository 자체만을 고려해야한다. 따라서 Repository의 책임은 Presentation Layer와 Aggregate의 상태 공유가 아니라, Aggregate의 상태를 영속하는 것에 있기 때문이다.

그렇다면 어떤 방법이 맞는 것인가?

명확하게 어떤 방법이 맞다고 결론 짓기 어려운 문제 같다.
개인적으로 Service Layer에서 DTO의 변환을 처리해야 한다고 생각한다.

Dto

: 계층간 데이터 교환을 위해 사용하는 객체를 말합니다.

  • Spring boot와 JPA를 사용하다 보면, Enitity 클래스의 중요성과 민감성에 대해서 잘알고 있을 것입니다. Entity 클래스는 데이터베이스와 맞닿는 핵심 클래스이며, Entity 클래스를 기준으로 테이블이 생성되고 스키마가 변경됩니다.

  • 그렇기 때문에 다양한 계층에서 Entity를 직접적으로 사용하게 된다면 원치 않게 Entity의 속성을 변경시킬 위험이 존재하며, Entitiy의 모든 속성이 불필요하게 외부에 노출될 가능성이 있습니다.

  • 그렇기 때문에 우리는 DTO를 사용합니다.
    Entity 클래스에서 필요한 데이터만 선택적으로 DTO에 담아서 생성해 사용함으로써, Entitiy 클래스를 감추며 보호할 수 있습니다.

때문에 DTO의 사용은 당연히 사용돼야하는 패턴입니다!

Entity

: Server,DB단 접근
DB 저장할 때는 Dto ->Entity 로 해줘야 하기 때문임

DAO(Repository)

: 서버와 데이터베이스 사이 연결고리역할

Dto -> Entity 변환

-@Buider 으로 toEntity() 설계

💡 Refactoring



참고

profile
개발자꿈나무

0개의 댓글