회원가입을 위한 정보를 입력 받은 request 데이터를 Builder 패턴을 이용하여 Entity로 변환한 후에, repository에 save가 되면 다시 mapper를 이용해 response 데이터로 변환하여 내보내려고 한다.
@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도 추가하였다.
@PostMapping("/users/new")
public UserSignUpResponse createUser(@RequestBody @Valid UserSignUpRequest userSignUpRequest, BindingResult result) {
return userService.signUp(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를 상속 받았다.
@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에 저장한다.
@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 패턴을 또 써서 코드를 길게 쓸 필요가 없어보였다.
@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 변환
}
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를 이용하기 위해서는 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"
Lombok과 MapStruct을 같이 사용할 경우, 의존성 순서를 반드시 지켜야 한다.
또한 lombok-mapstruct 의존정 순서에 따라 AnnotationProcessor 컴파일 에러가 나기 때문에 lombok-mapstruct-binding을 추가해서 lombok, mapstruct가 잘 동작하도록 해준다.
@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(source = "id", target = "userId")
@Mapping(source = "college.name", target = "collegeName")
UserSignUpResponse userToUserSignUpResponse(User user);
}
UserMapper라는 이름의 인터페이스를 추가한다. 그러면 MapStruct이 알아서 UserMapperImpl을 생성한다.
@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 어노테이션을 추가한다.
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)
이런 오류가 떴다.
흠... 뭐지? 왜 오류가 나는 거야 ㅠㅠ
내가 한 것들
@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
큰 도움이 되었습니다, 감사합니다.