회원 가입시에 email, username, birthday, nickname, phone 등 여러가지 데이터를 받는다.
@ToString
@Builder
@Setter @Getter
public class JoinUserRequest {
@Email
@NotBlank @NotNull
private String email; //이메일
@Pattern(regexp="(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}")//숫자 적어도하나, 영문 적어도 하나, 특수문자 적어도 하나, 공백 제거, 8~20자리
@NotBlank @NotNull
private String password; //패스워드
@NotBlank @NotNull
private String username; //본명 (진짜 본인 이름)
@NotBlank @NotNull
private String sex; //성별 -> 남/여
@Pattern(regexp = "[0-9]{10,11}", message = "10~11자리의 숫자만 입력가능합니다")
@NotBlank @NotNull
private String phone; //전화번호 ('-'없이 ex:01075528034)
@Pattern(regexp = "[0-9]{6}", message = "7자리의 숫자만 입력가능합니다")
@NotBlank @NotNull
private String birthday; //생년월일 7자리 (ex: 970223)
@Size(min=1,max=7, message="3자이상 7자미만으로 작성해야 합니다.")
@NotBlank @NotNull
private String nickname; //사용할 닉네임
}
근데 우리는 일단은 email과 nickname은 다른 User와 중복되지 않도록 했다.
하여 Join에 대한 요청이 컨트롤러로 넘어올 때, DB에 접근하여
1. 이미 존재하는 email인지
2. 이미 존재하는 nickname인지 확인해줘야 한다.
아래는 컨트롤러이다.
// TODO 한 줄 넣기
@PostMapping("/api/join")
public Header<AfterJoinUserResponse> join(@RequestBody @Valid JoinUserRequest user, BindingResult bindingResult){
if (bindingResult.hasErrors()) return responseError();
if(userService.hasUserEmailOf(user.getEmail())) return Header.ERROR("이미 존재하는 email입니다."); // 이미 존재하는 email 이므로 재요청
if(userService.hasUserNicknameOf(user.getNickname())) return Header.ERROR("이미 존재하는 닉네임입니다."); // 이미 존재하는 email 이므로 재요청
return userService.join(user);
}
문제점 1
그런데 위의 컨트롤러를 보면 3개의 if문이 존재한다.
첫번째 if : JoinUserRequest에 대한 유효성 검사. 유효성 검사 탈락이면 Error반환.
두번째 if : hasUserEmailOf 이미 존재하는 email인지 UserService -> UserRepository -> DB에 접근하여 검사. 이미 존재하면 Error반환.
세번째 if : hasUserNicknameOf 이미 존재하는 nickname인지 UserService -> UserRepository -> DB에 접근하여 검사. 이미 존재하면 Error반환
문제가 2가지 있는데,
1. 컨트롤러에서 서비스 로직을 너무 환히 보여주는 느낌 + 3개의 if문
2. email, nickname을 확인하기 위해 두번의 select를 날려야 한다는 점.
해결 1
하여 맨 처음에 생각해낸 아이디어는hasEmailAndNicknameOf 메서드를 만들어 if문을 하나 줄이고, 좀 더 무거워지겠지만 1개의 select를 날리는 것이다.
UserController의 join
@PostMapping("/api/join")
public Header<AfterJoinUserResponse> join(@RequestBody @Valid JoinUserRequest user, BindingResult bindingResult){
if (bindingResult.hasErrors()) return responseError();
if(userService.hasEmailAndNicknameOf(user.getEmail(), user.getNickname())) return Header.ERROR("이미 존재하는 email 혹은 닉네임입니다."); // 이미 존재하는 email 이므로 재요청
return userService.join(user);
}
UserService의 hasEmailAndNicknameOf메서드
public boolean hasEmailAndNicknameOf(String email, String nickname) {
List<User> result = userRepository.hasEmailAndNicknameOf(email, nickname);
return !result.isEmpty();
}
UserRepository의 hasEmailAndNicknameOf메서드 (querydsl사용)
@Override
public List<User> hasEmailAndNicknameOf(String email, String nickname) {
return queryFactory.selectFrom(user)
.where(user.email.eq(email).or(user.nickname.eq(nickname)))
.fetch();
}
위의 Repository의 hasEmailAndNicknameOf 메서드는 인자로 들어온 email, nickname이 존재하는 user를 모두 찾아서 List<User>타입으로 반환해준다.
하여 Service에서 List<User>에 아무 데이터가 없다면 중복이 없음을 알 수 있다.
--> 결론적으로 DB에 2번 select하는 것을 1번으로 줄이고, controller의 if문도 한 번 줄일 수 있게 되었다.
해결 2
하지만 controller에서 hasEmailAndNicknameOf의 호출을 숨길 수 있다. UserService의 join에서 hasEmailAndNicknameOf를 내부적으로 호출하는 것이다.
아래는 수정된 UserService의 join메서드이다.
@Transactional
public Header<AfterJoinUserResponse> join(JoinUserRequest joinUser) {
// -- 여기서 hasEmailAndNicknameOf 호출 --
if(hasEmailAndNicknameOf(joinUser.getEmail(), joinUser.getNickname()))
return Header.ERROR("이미 존재하는 email 혹은 닉네임입니다.");
User user = User.makeUser(
joinUser.getEmail(),
passwordEncoder.encode(joinUser.getPassword()),
joinUser.getUsername(),
joinUser.getSex(),
joinUser.getPhone(),
joinUser.getBirthday(),
joinUser.getNickname());
User saveUser = userRepository.save(user);
AfterJoinUserResponse afterJoinUser = AfterJoinUserResponse
.builder()
.email(saveUser.getEmail())
.username(saveUser.getUsername())
.nickname(saveUser.getNickname()).build();
return Header.OK(afterJoinUser);
}
이렇게 join 안에서 hasEmailAndNickname을 호출하면 Controller는 아래와 같이 아주 심플해진다.
@PostMapping("/api/join")
public Header<AfterJoinUserResponse> join(@RequestBody @Valid JoinUserRequest user, BindingResult bindingResult){
if (bindingResult.hasErrors()) return responseError();
return userService.join(user);
}
문제점
해결
그런데 또 다른 문제가 있다. 지금은 합쳐서 email혹은 nickname이 이미 존재한다고 하고 있다. 하지만 사용자는 email이 이미 존재하는지? nickname이 이미 존재하는지? 알고 싶을 것이다.
하여 이를 위해서 아래와 같이 Service에 메서드를 수정하고 추가하였다.
@Transactional
public Header<AfterJoinUserResponse> join(JoinUserRequest joinUser) {
// -- 수정된 부분 --
List<User> sameUsers = hasEmailAndNicknameOf(joinUser.getEmail(), joinUser.getNickname());
if(!sameUsers.isEmpty()) return responseAlreadyExistIdError(sameUsers, joinUser);
// ---------------
User user = User.makeUser(
joinUser.getEmail(),
passwordEncoder.encode(joinUser.getPassword()),
joinUser.getUsername(),
joinUser.getSex(),
joinUser.getPhone(),
joinUser.getBirthday(),
joinUser.getNickname());
User saveUser = userRepository.save(user);
AfterJoinUserResponse afterJoinUser = AfterJoinUserResponse
.builder()
.email(saveUser.getEmail())
.username(saveUser.getUsername())
.nickname(saveUser.getNickname()).build();
return Header.OK(afterJoinUser);
}
private Header<AfterJoinUserResponse> responseAlreadyExistIdError(List<User> sameUsers, JoinUserRequest joinUser) {
for (User sameUser : sameUsers) {
if(sameUser.getEmail().equals(joinUser.getEmail())) return Header.ERROR("이미 존재하는 email입니다.");
if(sameUser.getNickname().equals(joinUser.getNickname())) return Header.ERROR("이미 존재하는 닉네임입니다.");
}
return Header.ERROR();
}
public List<User> hasEmailAndNicknameOf(String email, String nickname) {
return userRepository.hasEmailAndNicknameOf(email, nickname);
}
하여 위와 같이 수정하였다. hasEmailAndNicknameOf 메서드는 boolean이 아니라 List<User>를 반환하도록 한다. 그리고 이를 호출한 Join메서드 안에서 hasEmailAndNicknameOf에서 반환된 List가 비어있지 않다면 responseAlreadyExistIdError를 호출한다.
responseAlreadyExistIdError는 내부에서 email이 이미 존재하는지, nickname이 존재하는지 알아내어 적절한 ERROR를 반환해준다.
이제 이를 테스트 해보자. 아래는 User테이블에 존재하는 데이터이다.
먼저 이미 존재하는 닉네임인 '강민순'으로 회원가입을 시도해보면 아래와 같이 "이미 존재하는 닉네임입니다"라는 응답을 받는다.
2. 이번에는 이미 존재하는 email인 'akflffk12@naver.com'으로 회원가입을 시도해보면 아래와 같이 '이미 존재하는 email입니다'라는 응답을 받는다.
'memo > 기록' 카테고리의 다른 글
jpa 성능 최적화하기2 (update VS delete) (0) | 2023.03.04 |
---|---|
jpa 성능 최적화하기1 (하나의 엔티티에 두개의 toMany 연관관계) (0) | 2023.03.03 |
AWS LAMBDA with Node.js (0) | 2023.02.22 |
Querydsl join시 해당하는 ID를 찾을 수 없습니다. (0) | 2022.03.26 |