Skip to content

Commit 1bcd51a

Browse files
committed
fix: 제약 조건 이름 매칭 오탐 방지
1 parent 7f26682 commit 1bcd51a

3 files changed

Lines changed: 61 additions & 24 deletions

File tree

src/main/java/com/semosan/api/common/exception/ConstraintViolationUtils.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,25 @@
22

33
import org.hibernate.exception.ConstraintViolationException;
44

5+
import java.util.regex.Pattern;
6+
57
public final class ConstraintViolationUtils {
68

79
private ConstraintViolationUtils() {}
810

9-
/**
10-
* 예외 체인 전체를 탐색해 특정 DB 제약 조건 이름이 원인인지 확인한다.
11-
* Hibernate ConstraintViolationException의 constraintName과 메시지 문자열을 모두 검사해
12-
* 드라이버마다 다른 예외 래핑 방식에 대응한다.
13-
*/
1411
public static boolean isViolation(Throwable exception, String constraintName) {
12+
Pattern constraintNamePattern = Pattern.compile(
13+
"(?<![A-Za-z0-9_])" + Pattern.quote(constraintName) + "(?![A-Za-z0-9_])"
14+
);
1515
Throwable current = exception;
1616
while (current != null) {
1717
if (current instanceof ConstraintViolationException constraintViolation
1818
&& constraintName.equals(constraintViolation.getConstraintName())) {
1919
return true;
2020
}
2121
String message = current.getMessage();
22-
if (message != null && message.contains(constraintName)) {
22+
// 드라이버마다 다른 예외 래핑에 대응하되, 다른 제약명 일부와 겹치는 오탐은 막는다.
23+
if (message != null && constraintNamePattern.matcher(message).find()) {
2324
return true;
2425
}
2526
current = current.getCause();

src/main/java/com/semosan/api/domain/community/post/service/FreePostReportService.java

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.semosan.api.common.alert.DiscordAlertClient;
44
import com.semosan.api.common.alert.dto.DiscordEmbed;
55
import com.semosan.api.common.alert.dto.DiscordMessage;
6+
import com.semosan.api.common.exception.ConstraintViolationUtils;
67
import com.semosan.api.common.exception.GeneralException;
78
import com.semosan.api.common.status.ErrorStatus;
89
import com.semosan.api.domain.community.post.entity.FreePost;
@@ -13,7 +14,6 @@
1314
import com.semosan.api.domain.user.entity.User;
1415
import com.semosan.api.domain.user.repository.UserRepository;
1516
import lombok.RequiredArgsConstructor;
16-
import org.hibernate.exception.ConstraintViolationException;
1717
import org.springframework.dao.DataIntegrityViolationException;
1818
import org.springframework.stereotype.Service;
1919
import org.springframework.transaction.annotation.Transactional;
@@ -61,7 +61,7 @@ public void afterCommit() {
6161
});
6262
return saved;
6363
} catch (DataIntegrityViolationException e) {
64-
if (isUniqueViolation(e)) {
64+
if (ConstraintViolationUtils.isViolation(e, REPORT_UNIQUE_CONSTRAINT)) {
6565
throw new GeneralException(ErrorStatus.FREE_POST_REPORT_ALREADY_EXISTS);
6666
}
6767
throw e;
@@ -93,22 +93,6 @@ private DiscordMessage buildDiscordMessage(FreePostReport report) {
9393
);
9494
}
9595

96-
private boolean isUniqueViolation(Throwable exception) {
97-
Throwable current = exception;
98-
while (current != null) {
99-
if (current instanceof ConstraintViolationException constraintViolation
100-
&& REPORT_UNIQUE_CONSTRAINT.equals(constraintViolation.getConstraintName())) {
101-
return true;
102-
}
103-
String message = current.getMessage();
104-
if (message != null && message.contains(REPORT_UNIQUE_CONSTRAINT)) {
105-
return true;
106-
}
107-
current = current.getCause();
108-
}
109-
return false;
110-
}
111-
11296
private User findReporterOrThrow(Long reporterId) {
11397
return userRepository.findByIdAndDeletedFalse(reporterId)
11498
.orElseThrow(() -> new GeneralException(ErrorStatus.USER_NOT_FOUND));
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.semosan.api.common.exception;
2+
3+
import org.hibernate.exception.ConstraintViolationException;
4+
import org.junit.jupiter.api.Test;
5+
import org.springframework.dao.DataIntegrityViolationException;
6+
7+
import java.sql.SQLException;
8+
9+
import static org.assertj.core.api.Assertions.assertThat;
10+
11+
class ConstraintViolationUtilsTest {
12+
13+
private static final String CONSTRAINT_NAME = "uk_user_blocks_blocker_blocked";
14+
15+
@Test
16+
void isViolationReturnsTrueWhenHibernateConstraintNameMatches() {
17+
ConstraintViolationException cause = new ConstraintViolationException(
18+
"duplicate",
19+
new SQLException("unique violation"),
20+
CONSTRAINT_NAME
21+
);
22+
23+
boolean result = ConstraintViolationUtils.isViolation(
24+
new DataIntegrityViolationException("duplicate", cause),
25+
CONSTRAINT_NAME
26+
);
27+
28+
assertThat(result).isTrue();
29+
}
30+
31+
@Test
32+
void isViolationReturnsTrueWhenMessageContainsExactConstraintToken() {
33+
DataIntegrityViolationException exception = new DataIntegrityViolationException(
34+
"ERROR: duplicate key value violates unique constraint \"" + CONSTRAINT_NAME + "\""
35+
);
36+
37+
boolean result = ConstraintViolationUtils.isViolation(exception, CONSTRAINT_NAME);
38+
39+
assertThat(result).isTrue();
40+
}
41+
42+
@Test
43+
void isViolationReturnsFalseWhenMessageContainsOnlyConstraintNamePrefix() {
44+
DataIntegrityViolationException exception = new DataIntegrityViolationException(
45+
"ERROR: duplicate key value violates unique constraint \"" + CONSTRAINT_NAME + "_idx\""
46+
);
47+
48+
boolean result = ConstraintViolationUtils.isViolation(exception, CONSTRAINT_NAME);
49+
50+
assertThat(result).isFalse();
51+
}
52+
}

0 commit comments

Comments
 (0)