Skip to content

Commit 7f26682

Browse files
pooreumjungclaude
andcommitted
refactor: ConstraintViolationException 파싱 로직 유틸로 통합
3곳에 중복된 예외 체인 탐색 로직을 ConstraintViolationUtils.isViolation()으로 추출. 버그 수정 시 한 곳만 고치면 되고, 제약 조건 이름 변경에도 누락 없이 대응 가능. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f4cb4ee commit 7f26682

4 files changed

Lines changed: 38 additions & 54 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.semosan.api.common.exception;
2+
3+
import org.hibernate.exception.ConstraintViolationException;
4+
5+
public final class ConstraintViolationUtils {
6+
7+
private ConstraintViolationUtils() {}
8+
9+
/**
10+
* 예외 체인 전체를 탐색해 특정 DB 제약 조건 이름이 원인인지 확인한다.
11+
* Hibernate ConstraintViolationException의 constraintName과 메시지 문자열을 모두 검사해
12+
* 드라이버마다 다른 예외 래핑 방식에 대응한다.
13+
*/
14+
public static boolean isViolation(Throwable exception, String constraintName) {
15+
Throwable current = exception;
16+
while (current != null) {
17+
if (current instanceof ConstraintViolationException constraintViolation
18+
&& constraintName.equals(constraintViolation.getConstraintName())) {
19+
return true;
20+
}
21+
String message = current.getMessage();
22+
if (message != null && message.contains(constraintName)) {
23+
return true;
24+
}
25+
current = current.getCause();
26+
}
27+
return false;
28+
}
29+
}

src/main/java/com/semosan/api/domain/hiking/service/HikingRecordService.java

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919
import com.semosan.api.domain.tracking.repository.TrackingPhotoRepository;
2020
import com.semosan.api.domain.tracking.repository.TrackingPointRepository;
2121
import com.semosan.api.domain.tracking.repository.projection.TrackingPathProjection;
22+
import com.semosan.api.common.exception.ConstraintViolationUtils;
2223
import com.semosan.api.domain.user.entity.User;
2324
import com.semosan.api.domain.user.service.UserReader;
2425
import lombok.RequiredArgsConstructor;
25-
import org.hibernate.exception.ConstraintViolationException;
2626
import org.springframework.dao.DataIntegrityViolationException;
2727
import org.springframework.data.domain.Page;
2828
import org.springframework.data.domain.Pageable;
@@ -147,26 +147,11 @@ public CourseDifficultyFeedbackResponse createCourseDifficultyFeedback(
147147
try {
148148
return CourseDifficultyFeedbackResponse.from(courseDifficultyFeedbackRepository.saveAndFlush(feedback));
149149
} catch (DataIntegrityViolationException e) {
150-
if (isDifficultyFeedbackHikingRecordUniqueViolation(e)) {
150+
// 동시 요청으로 유니크 제약 위반 시 이미 피드백이 존재하는 에러로 변환
151+
if (ConstraintViolationUtils.isViolation(e, DIFFICULTY_FEEDBACK_HIKING_RECORD_UNIQUE_CONSTRAINT)) {
151152
throw new GeneralException(ErrorStatus.COURSE_DIFFICULTY_FEEDBACK_ALREADY_EXISTS);
152153
}
153154
throw e;
154155
}
155156
}
156-
157-
private boolean isDifficultyFeedbackHikingRecordUniqueViolation(Throwable exception) {
158-
Throwable current = exception;
159-
while (current != null) {
160-
if (current instanceof ConstraintViolationException constraintViolation
161-
&& DIFFICULTY_FEEDBACK_HIKING_RECORD_UNIQUE_CONSTRAINT.equals(constraintViolation.getConstraintName())) {
162-
return true;
163-
}
164-
String message = current.getMessage();
165-
if (message != null && message.contains(DIFFICULTY_FEEDBACK_HIKING_RECORD_UNIQUE_CONSTRAINT)) {
166-
return true;
167-
}
168-
current = current.getCause();
169-
}
170-
return false;
171-
}
172157
}

src/main/java/com/semosan/api/domain/tracking/service/TrackingSessionService.java

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616
import com.semosan.api.domain.tracking.enums.TrackingSessionStatus;
1717
import com.semosan.api.domain.tracking.event.TrackingSessionTerminatedEvent;
1818
import com.semosan.api.domain.tracking.repository.TrackingSessionRepository;
19+
import com.semosan.api.common.exception.ConstraintViolationUtils;
1920
import com.semosan.api.domain.user.entity.User;
2021
import com.semosan.api.domain.user.service.UserReader;
2122
import lombok.RequiredArgsConstructor;
2223
import lombok.extern.slf4j.Slf4j;
23-
import org.hibernate.exception.ConstraintViolationException;
2424
import org.springframework.context.ApplicationEventPublisher;
2525
import org.springframework.dao.DataIntegrityViolationException;
2626
import org.springframework.stereotype.Service;
@@ -178,26 +178,11 @@ private TrackingSession saveSession(TrackingSession session) {
178178
try {
179179
return trackingSessionRepository.save(session);
180180
} catch (DataIntegrityViolationException e) {
181-
if (isActiveSessionUniqueViolation(e)) {
181+
// 동시 요청으로 유니크 제약 위반 시 이미 진행 중인 세션 존재 에러로 변환
182+
if (ConstraintViolationUtils.isViolation(e, ACTIVE_SESSION_UNIQUE_INDEX)) {
182183
throw new GeneralException(ErrorStatus.TRACKING_SESSION_ALREADY_IN_PROGRESS);
183184
}
184185
throw e;
185186
}
186187
}
187-
188-
private boolean isActiveSessionUniqueViolation(Throwable exception) {
189-
Throwable current = exception;
190-
while (current != null) {
191-
if (current instanceof ConstraintViolationException constraintViolation
192-
&& ACTIVE_SESSION_UNIQUE_INDEX.equals(constraintViolation.getConstraintName())) {
193-
return true;
194-
}
195-
String message = current.getMessage();
196-
if (message != null && message.contains(ACTIVE_SESSION_UNIQUE_INDEX)) {
197-
return true;
198-
}
199-
current = current.getCause();
200-
}
201-
return false;
202-
}
203188
}

src/main/java/com/semosan/api/domain/user/service/UserBlockService.java

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.semosan.api.domain.user.service;
22

3+
import com.semosan.api.common.exception.ConstraintViolationUtils;
34
import com.semosan.api.common.exception.GeneralException;
45
import com.semosan.api.common.status.ErrorStatus;
56
import com.semosan.api.domain.community.post.entity.FreePost;
@@ -9,7 +10,6 @@
910
import com.semosan.api.domain.user.repository.UserBlockRepository;
1011
import com.semosan.api.domain.user.repository.UserRepository;
1112
import lombok.RequiredArgsConstructor;
12-
import org.hibernate.exception.ConstraintViolationException;
1313
import org.springframework.dao.DataIntegrityViolationException;
1414
import org.springframework.stereotype.Service;
1515
import org.springframework.transaction.annotation.Transactional;
@@ -40,7 +40,8 @@ public void block(Long blockerId, Long blockedUserId) {
4040
try {
4141
userBlockRepository.saveAndFlush(UserBlock.create(blocker, blockedUser));
4242
} catch (DataIntegrityViolationException e) {
43-
if (!isUniqueViolation(e)) {
43+
// 동시 요청으로 유니크 제약 위반 시 멱등 처리 (이미 차단된 상태로 간주)
44+
if (!ConstraintViolationUtils.isViolation(e, BLOCK_UNIQUE_CONSTRAINT)) {
4445
throw e;
4546
}
4647
}
@@ -53,22 +54,6 @@ public void blockByPost(Long blockerId, Long postId) {
5354
block(blockerId, post.getAuthor().getId());
5455
}
5556

56-
private boolean isUniqueViolation(Throwable exception) {
57-
Throwable current = exception;
58-
while (current != null) {
59-
if (current instanceof ConstraintViolationException constraintViolation
60-
&& BLOCK_UNIQUE_CONSTRAINT.equals(constraintViolation.getConstraintName())) {
61-
return true;
62-
}
63-
String message = current.getMessage();
64-
if (message != null && message.contains(BLOCK_UNIQUE_CONSTRAINT)) {
65-
return true;
66-
}
67-
current = current.getCause();
68-
}
69-
return false;
70-
}
71-
7257
private User findActiveUserOrThrow(Long userId) {
7358
return userRepository.findByIdAndDeletedFalse(userId)
7459
.orElseThrow(() -> new GeneralException(ErrorStatus.USER_NOT_FOUND));

0 commit comments

Comments
 (0)