Skip to content

Commit af2ba81

Browse files
authored
refactor: Siteuser 도메인 리팩터링 (#336)
* refactor: SiteUser 엔티티 - SiteUser 엔티티의 nickname 컬럼에 unique 제약 조건 추가 * fix: site_user 테이블의 nickname 컬럼에 unique 제약 조건 추가 * refactor: SiteUser 도메인 관련 - MyPageService에서 중복 닉네임 검증 로직 제거 - 컨트롤러에서 try-catch 문을 통해 예외 처리 * refactor: 회원 정보 변경 관련 로직 수정 - 변경된 컬럼만 업데이트하는 쿼리 사용 * refactor: sign up 시 닉네임 검증 로직 제거 - nickname 제약 조건의 이름 명시적으로 지정 * refactor: 도메인 로직 변경에 따른 테스트 코드 수정 - 게시글_좋아요_동시성_문제를_해결한다: 동일한 닉네임을 사용하지 않도록 변경 - 이메일이_같더라도_인증_유형이_다른_사용자는_정상_저장한다: 동일한 닉네임을 사용하지 않도록 변경 - 닉네임 무결성 테스트는 repository test로 분리 - 새로운_이미지로_성공적으로_업데이트한다: DB에서 사용자 조회 후 URL 비교 * refactor: 예외 처리를 컨트롤러가 아닌 서비스 계층에서 처리하도록 변경 * refactor: JPQL이 아닌 dirty check를 사용하도록 변경 외 - 중복 닉네임 검증보다 닉네임 수정 시간 검증이 선행되도록 변경 - 준영속 엔티티를 save()하는 과정에서 발생하는 N+1 문제 해결 - DB 레벨, 애플리케이션 레벨 검증 모두 사용하도록 - 위 변경사항에 따른 테스트 코드 수정 * refactor: 로그에만 예외 원문 포함하도록 변경 및 상태코드 수정 * refactor: 회원가입 시 닉네임 관련 애플리케이션 레벨 검증 추가 - CustomExceptionHandler에 DB 레벨에서 중복 닉네임 검증 추가에 따라 try-catch 문 삭제 * refactor: 미사용 코드 제거 (JPQL 관련) * test: 중복된_닉네임으로_사용자를_저장하면_예외_응답을_반환한다 테스트 수정 - save -> saveAndFlush * chore: 미사용 import문 제거 * chore: 함수명과 중괄호 사이 띄어쓰기 추가 (컨벤션 관련) * chore: 호출 순서에 따라 메서드 선언 재배치 * chore: 마이그레이션 파일 버전 수정 V13 -> V14
1 parent a14a510 commit af2ba81

11 files changed

Lines changed: 88 additions & 37 deletions

File tree

src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
55
import io.jsonwebtoken.JwtException;
66
import lombok.extern.slf4j.Slf4j;
7+
import org.springframework.dao.DataIntegrityViolationException;
78
import org.springframework.http.HttpStatus;
89
import org.springframework.http.ResponseEntity;
910
import org.springframework.web.bind.MethodArgumentNotValidException;
@@ -13,6 +14,7 @@
1314
import java.util.ArrayList;
1415
import java.util.List;
1516

17+
import static com.example.solidconnection.common.exception.ErrorCode.DATA_INTEGRITY_VIOLATION;
1618
import static com.example.solidconnection.common.exception.ErrorCode.INVALID_INPUT;
1719
import static com.example.solidconnection.common.exception.ErrorCode.JSON_PARSING_FAILED;
1820
import static com.example.solidconnection.common.exception.ErrorCode.JWT_EXCEPTION;
@@ -56,6 +58,15 @@ public ResponseEntity<ErrorResponse> handleValidationExceptions(MethodArgumentNo
5658
.body(errorResponse);
5759
}
5860

61+
@ExceptionHandler(DataIntegrityViolationException.class)
62+
public ResponseEntity<Object> handleDataIntegrityViolationException(DataIntegrityViolationException ex) {
63+
log.error("데이터 무결성 제약조건 위반 예외 발생 : {}", ex.getMessage());
64+
ErrorResponse errorResponse = new ErrorResponse(DATA_INTEGRITY_VIOLATION, "데이터 무결성 제약조건 위반 예외 발생");
65+
return ResponseEntity
66+
.status(DATA_INTEGRITY_VIOLATION.getCode())
67+
.body(errorResponse);
68+
}
69+
5970
@ExceptionHandler(JwtException.class)
6071
public ResponseEntity<Object> handleJwtException(JwtException ex) {
6172
String errorMessage = ex.getMessage();

src/main/java/com/example/solidconnection/common/exception/ErrorCode.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ public enum ErrorCode {
9696
USER_DO_NOT_HAVE_GPA(HttpStatus.BAD_REQUEST.value(), "해당 유저의 학점을 찾을 수 없음"),
9797
REJECTED_REASON_REQUIRED(HttpStatus.BAD_REQUEST.value(), "거절 사유가 필요합니다."),
9898

99+
// database
100+
DATA_INTEGRITY_VIOLATION(HttpStatus.CONFLICT.value(), "데이터베이스 무결성 제약조건 위반이 발생했습니다."),
101+
99102
// general
100103
JSON_PARSING_FAILED(HttpStatus.BAD_REQUEST.value(), "JSON 파싱을 할 수 없습니다."),
101104
JWT_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "JWT 토큰을 처리할 수 없습니다."),

src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.example.solidconnection.siteuser.controller;
22

3+
34
import com.example.solidconnection.common.resolver.AuthorizedUser;
45
import com.example.solidconnection.siteuser.domain.SiteUser;
56
import com.example.solidconnection.siteuser.dto.MyPageResponse;

src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
@UniqueConstraint(
3636
name = "uk_site_user_email_auth_type",
3737
columnNames = {"email", "auth_type"}
38+
),
39+
@UniqueConstraint(
40+
name = "uk_site_user_nickname",
41+
columnNames = {"nickname"}
3842
)
3943
})
4044
public class SiteUser {

src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.example.solidconnection.siteuser.repository;
22

3+
34
import com.example.solidconnection.siteuser.domain.AuthType;
45
import com.example.solidconnection.siteuser.domain.SiteUser;
56
import org.springframework.data.jpa.repository.JpaRepository;

src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import static com.example.solidconnection.common.exception.ErrorCode.CAN_NOT_CHANGE_NICKNAME_YET;
2323
import static com.example.solidconnection.common.exception.ErrorCode.NICKNAME_ALREADY_EXISTED;
24+
import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND;
2425

2526
@RequiredArgsConstructor
2627
@Service
@@ -47,27 +48,23 @@ public MyPageResponse getMyPageInfo(SiteUser siteUser) {
4748
* */
4849
@Transactional
4950
public void updateMyPageInfo(SiteUser siteUser, MultipartFile imageFile, String nickname) {
51+
SiteUser user = siteUserRepository.findById(siteUser.getId())
52+
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));
53+
5054
if (nickname != null) {
55+
validateNicknameNotChangedRecently(user.getNicknameModifiedAt());
5156
validateNicknameUnique(nickname);
52-
validateNicknameNotChangedRecently(siteUser.getNicknameModifiedAt());
53-
siteUser.setNickname(nickname);
54-
siteUser.setNicknameModifiedAt(LocalDateTime.now());
57+
user.setNickname(nickname);
58+
user.setNicknameModifiedAt(LocalDateTime.now());
5559
}
5660

5761
if (imageFile != null && !imageFile.isEmpty()) {
5862
UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, ImgType.PROFILE);
59-
if (!isDefaultProfileImage(siteUser.getProfileImageUrl())) {
60-
s3Service.deleteExProfile(siteUser);
63+
if (!isDefaultProfileImage(user.getProfileImageUrl())) {
64+
s3Service.deleteExProfile(user);
6165
}
6266
String profileImageUrl = uploadedFile.fileUrl();
63-
siteUser.setProfileImageUrl(profileImageUrl);
64-
}
65-
siteUserRepository.save(siteUser);
66-
}
67-
68-
private void validateNicknameUnique(String nickname) {
69-
if (siteUserRepository.existsByNickname(nickname)) {
70-
throw new CustomException(NICKNAME_ALREADY_EXISTED);
67+
user.setProfileImageUrl(profileImageUrl);
7168
}
7269
}
7370

@@ -82,6 +79,12 @@ private void validateNicknameNotChangedRecently(LocalDateTime lastModifiedAt) {
8279
}
8380
}
8481

82+
private void validateNicknameUnique(String nickname) {
83+
if (siteUserRepository.existsByNickname(nickname)) {
84+
throw new CustomException(NICKNAME_ALREADY_EXISTED);
85+
}
86+
}
87+
8588
private boolean isDefaultProfileImage(String profileImageUrl) {
8689
String prefix = "profile/";
8790
return profileImageUrl == null || !profileImageUrl.startsWith(prefix);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ALTER TABLE site_user
2+
ADD CONSTRAINT uk_site_user_nickname
3+
UNIQUE (nickname);

src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import java.util.concurrent.Executors;
2222
import java.util.concurrent.TimeUnit;
2323

24-
import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail;
24+
import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmailAndNickname;
2525
import static org.junit.jupiter.api.Assertions.assertEquals;
2626

2727
@TestContainerSpringBootTest
@@ -92,7 +92,8 @@ private Post createPost(Board board, SiteUser siteUser) {
9292

9393
for (int i = 0; i < THREAD_NUMS; i++) {
9494
String email = "email" + i;
95-
SiteUser tmpSiteUser = siteUserRepository.save(createSiteUserByEmail(email));
95+
String nickname = "nickname" + i;
96+
SiteUser tmpSiteUser = siteUserRepository.save(createSiteUserByEmailAndNickname(email, nickname));
9697
executorService.submit(() -> {
9798
try {
9899
postLikeService.likePost(tmpSiteUser, post.getId());

src/test/java/com/example/solidconnection/e2e/DynamicFixture.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66

77
public class DynamicFixture { // todo: test fixture 개선 작업 이후, 이 클래스의 사용이 대체되면 삭제 필요
88

9-
public static SiteUser createSiteUserByEmail(String email) {
9+
public static SiteUser createSiteUserByEmailAndNickname(String email, String nickname) {
1010
return new SiteUser(
1111
email,
12-
"nickname",
12+
nickname,
1313
"profileImage",
1414
PreparationStatus.CONSIDERING,
1515
Role.MENTEE

src/test/java/com/example/solidconnection/siteuser/repository/SiteUserRepositoryTest.java

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ class 이메일과_인증_유형이_동일한_사용자는_저장할_수_없다
2424
@Test
2525
void 이메일과_인증_유형이_동일한_사용자를_저장하면_예외_응답을_반환한다() {
2626
// given
27-
SiteUser user1 = createSiteUser("email", AuthType.KAKAO);
28-
SiteUser user2 = createSiteUser("email", AuthType.KAKAO);
27+
SiteUser user1 = createSiteUser("email", "nickname1", AuthType.KAKAO);
28+
SiteUser user2 = createSiteUser("email", "nickname2", AuthType.KAKAO);
2929
siteUserRepository.save(user1);
3030

3131
// when, then
@@ -36,8 +36,8 @@ class 이메일과_인증_유형이_동일한_사용자는_저장할_수_없다
3636
@Test
3737
void 이메일이_같더라도_인증_유형이_다른_사용자는_정상_저장한다() {
3838
// given
39-
SiteUser user1 = createSiteUser("email", AuthType.KAKAO);
40-
SiteUser user2 = createSiteUser("email", AuthType.APPLE);
39+
SiteUser user1 = createSiteUser("email", "nickname1", AuthType.KAKAO);
40+
SiteUser user2 = createSiteUser("email", "nickname2", AuthType.APPLE);
4141
siteUserRepository.save(user1);
4242

4343
// when, then
@@ -46,10 +46,42 @@ class 이메일과_인증_유형이_동일한_사용자는_저장할_수_없다
4646
}
4747
}
4848

49-
private SiteUser createSiteUser(String email, AuthType authType) {
49+
@Nested
50+
class 닉네임은_중복될_수_없다 {
51+
52+
@Test
53+
void 중복된_닉네임으로_사용자를_저장하면_예외_응답을_반환한다() {
54+
// given
55+
SiteUser user1 = createSiteUser("email1", "nickname", AuthType.KAKAO);
56+
SiteUser user2 = createSiteUser("email2", "nickname", AuthType.KAKAO);
57+
siteUserRepository.save(user1);
58+
59+
// when, then
60+
assertThatCode(() -> siteUserRepository.saveAndFlush(user2))
61+
.isInstanceOf(DataIntegrityViolationException.class);
62+
}
63+
64+
@Test
65+
void 중복된_닉네임으로_변경하면_예외_응답을_반환한다() {
66+
// given
67+
SiteUser user1 = createSiteUser("email1", "nickname1", AuthType.KAKAO);
68+
SiteUser user2 = createSiteUser("email2", "nickname2", AuthType.KAKAO);
69+
siteUserRepository.save(user1);
70+
siteUserRepository.save(user2);
71+
72+
// when
73+
user2.setNickname("nickname1");
74+
75+
// then
76+
assertThatCode(() -> siteUserRepository.saveAndFlush(user2))
77+
.isInstanceOf(DataIntegrityViolationException.class);
78+
}
79+
}
80+
81+
private SiteUser createSiteUser(String email, String nickname, AuthType authType) {
5082
return new SiteUser(
5183
email,
52-
"nickname",
84+
nickname,
5385
"profileImageUrl",
5486
PreparationStatus.CONSIDERING,
5587
Role.MENTEE,

0 commit comments

Comments
 (0)