Builder, Mapper로 Entity -> DTO 변환하기

Jieun Yang·2023년 11월 14일

Spring Boot

목록 보기
3/4

회원가입을 위한 정보를 입력 받은 request 데이터를 Builder 패턴을 이용하여 Entity로 변환한 후에, repository에 save가 되면 다시 mapper를 이용해 response 데이터로 변환하여 내보내려고 한다.


User

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {

    @Id
    @Column(name = "user_id")
    private String id;

    private String email;
    private String password;
    private String image;
    private String nickname;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "college_name")
    private College college;

    @Transient
    private String collegeName;

    @Enumerated(EnumType.STRING)
    private UserStatus status; //회원 인증 상태 
}

응답 데이터에 학교 ID가 아니라 학교 name을 반환해야 하기 때문에 collegeName도 추가하였다.


UserController

    @PostMapping("/users/new")
    public UserSignUpResponse createUser(@RequestBody @Valid UserSignUpRequest userSignUpRequest, BindingResult result) {

        return userService.signUp(userSignUpRequest);
    }

요청 데이터 받기

UserSignUpRequest

package com.coconut.ubo.dto;

import com.coconut.ubo.domain.College;
import com.coconut.ubo.domain.User;
import com.coconut.ubo.domain.UserStatus;
import lombok.*;
import lombok.extern.slf4j.Slf4j;


@Getter
@NoArgsConstructor
@Slf4j
public class UserSignUpRequest {

    private String email;
    private String password;
    private String nickname;
    private String college;
    private String image;

	...

}

사용자에게 이메일, 비밀번호, 닉네임, 학교명, 프로필 이미지를 입력 받는다.

UserReposiotry는 JpaRepository를 상속 받았다.


Mapper를 이용한 Entity -> DTO 변환

UserServiceImpl

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl implements UserService{
    
    private final UserMapper userMapper;
    private final UserRepository userRepository;
    private final CollegeRepository collegeRepository;

    /**
     * 회원가입
     */
    @Transactional
    @Override
    public UserSignUpResponse signUp(UserSignUpRequest request) {

        String userId =  validateDuplicateUser(request); //중복 회원 검증
        College college = validateCollegeName(request); //학교 유효성 검증

        User user = request.toEntity(userId, college);//DTO -> Entity(외래키 college 가져감)
        User savedUser = userRepository.save(user); //회원 저장
        
        ...
        
    }

중복 회원 검사 메서드와, 학교 유효성 검사 메서드를 통해 college 객체를 반환한다.

DTO에서 Entity로 변환하기 위한 메서드를 호출하고 객체를 저장해서 repository에 저장한다.


UserSignUpRequest

@Getter
@NoArgsConstructor
@Slf4j
public class UserSignUpRequest {

    ... 
    
    @Builder
    public UserSignUpRequest(String email, String password, String image, String nickname, String college) {
        this.email = email;
        this.password = password;
        this.image = image;
        this.nickname = nickname;
        this.college = college;
    }

    //User Entity로 변환
    public User toEntity(String userId, College college) {
        return User.builder()
                .id(userId)
                .email(email)
                .password(password)
                .image(image)
                .college(college)
                .nickname(nickname)
                .status(UserStatus.AUTH)
                .build();
    }
}

빌더 패턴을 이용해서 user 객체를 생성한다.
이메일에서 id를 추출한 userId와 college를 매개변수로 받아 넣어줬다.



클라이언트 응답 데이터 반환

요청 데이터는 setter 메서드가 아닌, builder 패턴으로 객체 생성을 했고... 응답 데이터는 요청 데이터의 필드들 그대로 다 보내주면 될 것 같은데? builder 패턴을 또 써서 코드를 길게 쓸 필요가 없어보였다.


builder 패턴으로 Entity -> DTO 변환

UserServiceImpl

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl implements UserService{
    
    ...
    
    /**
     * 회원가입
     */
    @Transactional
    @Override
    public UserSignUpResponse signUp(UserSignUpRequest request) {

        String userId =  validateDuplicateUser(request); //중복 회원 검증
        College college = validateCollegeName(request); //학교 유효성 검증

        User user = request.toEntity(userId, college);//DTO -> Entity(외래키 college 가져감)
        User savedUser = userRepository.save(user); //회원 저장
        
        //추가
        return UserSignUpResponse.toDTO(savedUser); //Builder 패턴으로 클라이언트 응답 데이터 반환 Entity -> DTO 변환
        
    }

UserSignUpResponse

package com.coconut.ubo.dto;

import com.coconut.ubo.domain.User;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class UserSignUpResponse {

    private String userId;
    private String email;
    private String password;
    private String nickname;
    private String collegeName;
    private String image;

    @Builder
    public UserSignUpResponse(String userId, String email, String password, String nickname, String collegeName, String image, String message) {
        this.userId = userId;
        this.email = email;
        this.password = password;
        this.nickname = nickname;
        this.collegeName = collegeName;
        this.image = image;
    }


    //Builder - 정적 팩토리 메서드
    public static UserSignUpResponse toDTO(User user) {
        return UserSignUpResponse.builder()
                .userId(user.getId())
                .email(user.getEmail())
                .password(user.getPassword())
                .image(user.getImage())
                .nickname(user.getNickname())
                .collegeName(user.getCollege() != null ? user.getCollege().getName() : null)
                .build();
    }

}

Request에서 했던 것처럼 생성자를 만들고, 메서드를 작성한다.


Mapper를 이용하여 Entity -> DTO 변환

Mapper를 이용하기 위해서는 MapStruct 의존성을 추가해야 한다

	//Mapper 라이브러리 MapStruct 추가
	implementation "org.projectlombok:lombok:1.18.30"
	implementation "org.mapstruct:mapstruct:1.5.5.Final"
	implementation 'org.projectlombok:lombok-mapstruct-binding:0.2.0'

	annotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.2.0"
	annotationProcessor "org.mapstruct:mapstruct-processor:1.5.5.Final"
	annotationProcessor "org.projectlombok:lombok:1.18.30"

주의할 점

LombokMapStruct을 같이 사용할 경우, 의존성 순서를 반드시 지켜야 한다.

또한 lombok-mapstruct 의존정 순서에 따라 AnnotationProcessor 컴파일 에러가 나기 때문에 lombok-mapstruct-binding을 추가해서 lombok, mapstruct가 잘 동작하도록 해준다.



UserMapper

@Mapper(componentModel = "spring")
public interface UserMapper {
    @Mapping(source = "id", target = "userId")
    @Mapping(source = "college.name", target = "collegeName")
    UserSignUpResponse userToUserSignUpResponse(User user);
}

UserMapper라는 이름의 인터페이스를 추가한다. 그러면 MapStruct이 알아서 UserMapperImpl을 생성한다.


UserSignUpResponse

@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PUBLIC)
public class UserSignUpResponse {

    private String userId;
    private String email;
    private String password;
    private String nickname;
    private String collegeName;
    private String image;

UserSignUpResponse 에 Builder 어노테이션을 추가한다.


UserServiceImpl

return userMapper.userToUserSignUpResponse(savedUser); //Mapper

return은 이렇게 바꾸면 됨


실행 오류


실행 오류가 난다.

인터넷을 찾아보니 Lombok이 먼저라고 하길래 순서에 맞춰서 했는데

java: no suitable constructor found for UserSignUpResponse(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String)
    constructor com.coconut.ubo.dto.UserSignUpResponse.UserSignUpResponse(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String) is not applicable
      (actual and formal argument lists differ in length)
    constructor com.coconut.ubo.dto.UserSignUpResponse.UserSignUpResponse() is not applicable
      (actual and formal argument lists differ in length)

이런 오류가 떴다.
흠... 뭐지? 왜 오류가 나는 거야 ㅠㅠ

내가 한 것들

  • Lombok과 MapStruct을 최신 버전으로 업데이트
  • 의존성 순서 지킴(Lombok 먼저)

@Getter
@Builder 
@NoArgsConstructor
public class UserSignUpResponse {

클래스 레벨에서 Builder 어노테이션은 오류가 나고,

@Getter
@NoArgsConstructor
public class UserSignUpResponse {

    private String userId;
    private String email;
    private String password;
    private String nickname;
    private String collegeName;
    private String image;

    @Builder
    public UserSignUpResponse(String userId, String email, String password, String nickname, String collegeName, String image, String message) {
        this.userId = userId;
        this.email = email;
        this.password = password;
        this.nickname = nickname;
        this.collegeName = collegeName;
        this.image = image;
    }

생성자 레벨에서 Builder 어노테이션을 추가해야만 했다.




문제 원인

모든 필드를 인자로 받는 생성자를 호출하려고 하지만, 해당 생성자를 찾을 수 없어서 생성자 호출 과정에서 오류가 발생했다.

생성자 어노테이션을 @NoArgsConstructor 만 사용하고 있었는데, 알고 보니 @Builder 를 클래스 레벨에 적용할 때에는 Lombok이 모든 필드를 인자로 받는 생성자를 생성하지 않는다는 것이다.

-> @AllArgsConstructor 어노테이션을 안 써서 생긴 문제.


@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserSignUpResponse {

    private String userId;
    private String email;
    private String password;
    private String nickname;
    private String collegeName;
    private String image;
}

@AllArgsConstructor 어노테이션 작성해주고, 아까 Request에서도 @AllArgsConstructor를 붙여준다.


실행 결과

잘 되더이다.



참고글
https://wise-develop.tistory.com/18
https://wedul.site/718

1개의 댓글

comment-user-thumbnail
2023년 11월 14일

큰 도움이 되었습니다, 감사합니다.

답글 달기