Skip to content

Commit f8858b7

Browse files
authored
Merge pull request #179 from SEMOSAN/refactor/#174-like-conflict-handler
[Refactor] 좋아요 동시 요청 충돌 처리 공통화
2 parents bffc810 + 394a55e commit f8858b7

4 files changed

Lines changed: 82 additions & 15 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.semosan.api.common.util;
2+
3+
import org.springframework.dao.DataIntegrityViolationException;
4+
5+
public final class LikeConflictHandler {
6+
7+
private LikeConflictHandler() {}
8+
9+
public static boolean handleConcurrentCreate(Runnable createAction, Runnable conflictLogAction) {
10+
try {
11+
createAction.run();
12+
return true;
13+
} catch (DataIntegrityViolationException e) {
14+
conflictLogAction.run();
15+
return true;
16+
}
17+
}
18+
}

src/main/java/com/semosan/api/domain/community/like/service/PostLikeService.java

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

33
import com.semosan.api.common.exception.GeneralException;
44
import com.semosan.api.common.status.ErrorStatus;
5+
import com.semosan.api.common.util.LikeConflictHandler;
56
import com.semosan.api.domain.community.like.dto.PostLikeToggleResponse;
67
import com.semosan.api.domain.community.like.entity.PostLike;
78
import com.semosan.api.domain.community.like.repository.PostLikeRepository;
@@ -42,14 +43,10 @@ private boolean toggle(Long postId, Long userId) {
4243
return false;
4344
}
4445

45-
try {
46+
return LikeConflictHandler.handleConcurrentCreate(() -> {
4647
postLikeRepository.save(PostLike.create(post, user));
4748
communityNotificationService.sendPostLikeNotification(post, user);
48-
return true;
49-
} catch (DataIntegrityViolationException e) {
50-
log.warn("PostLike 동시 요청 감지: postId={}, userId={}", postId, userId);
51-
return true;
52-
}
49+
}, () -> log.warn("PostLike 동시 요청 감지: postId={}, userId={}", postId, userId));
5350
}
5451

5552
public long count(Long postId) {

src/main/java/com/semosan/api/domain/mountain/service/CourseLikeService.java

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

33
import com.semosan.api.common.exception.GeneralException;
44
import com.semosan.api.common.status.ErrorStatus;
5+
import com.semosan.api.common.util.LikeConflictHandler;
56
import com.semosan.api.domain.mountain.dto.response.CourseLikeToggleResponse;
67
import com.semosan.api.domain.mountain.entity.Course;
78
import com.semosan.api.domain.mountain.entity.CourseLike;
@@ -39,17 +40,14 @@ private boolean toggle(Long userId, Long courseId) {
3940
courseLikeRepository.delete(existing);
4041
return false;
4142
})
42-
.orElseGet(() -> createCourseLike(user, course, userId, courseId));
43+
.orElseGet(() -> createCourseLike(user, course));
4344
}
4445

45-
private boolean createCourseLike(User user, Course course, Long userId, Long courseId) {
46-
try {
47-
courseLikeRepository.save(CourseLike.create(user, course));
48-
return true;
49-
} catch (DataIntegrityViolationException e) {
50-
log.warn("CourseLike 동시 요청 감지: courseId={}, userId={}", courseId, userId);
51-
return true;
52-
}
46+
private boolean createCourseLike(User user, Course course) {
47+
return LikeConflictHandler.handleConcurrentCreate(
48+
() -> courseLikeRepository.save(CourseLike.create(user, course)),
49+
() -> log.warn("CourseLike 동시 요청 감지: courseId={}, userId={}", course.getId(), user.getId())
50+
);
5351
}
5452

5553
private Course findCourseById(Long courseId) {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.semosan.api.common.util;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.springframework.dao.DataIntegrityViolationException;
5+
6+
import java.util.concurrent.atomic.AtomicBoolean;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
10+
11+
class LikeConflictHandlerTest {
12+
13+
@Test
14+
void handleConcurrentCreateReturnsTrueWhenCreateActionSucceeds() {
15+
AtomicBoolean createCalled = new AtomicBoolean(false);
16+
AtomicBoolean logCalled = new AtomicBoolean(false);
17+
18+
boolean result = LikeConflictHandler.handleConcurrentCreate(
19+
() -> createCalled.set(true),
20+
() -> logCalled.set(true)
21+
);
22+
23+
assertThat(result).isTrue();
24+
assertThat(createCalled).isTrue();
25+
assertThat(logCalled).isFalse();
26+
}
27+
28+
@Test
29+
void handleConcurrentCreateReturnsTrueAndRunsLogActionWhenDataIntegrityViolationOccurs() {
30+
AtomicBoolean logCalled = new AtomicBoolean(false);
31+
32+
boolean result = LikeConflictHandler.handleConcurrentCreate(
33+
() -> {
34+
throw new DataIntegrityViolationException("duplicate");
35+
},
36+
() -> logCalled.set(true)
37+
);
38+
39+
assertThat(result).isTrue();
40+
assertThat(logCalled).isTrue();
41+
}
42+
43+
@Test
44+
void handleConcurrentCreateRethrowsUnexpectedException() {
45+
RuntimeException exception = new IllegalStateException("unexpected");
46+
47+
assertThatThrownBy(() -> LikeConflictHandler.handleConcurrentCreate(
48+
() -> {
49+
throw exception;
50+
},
51+
() -> {}
52+
)).isSameAs(exception);
53+
}
54+
}

0 commit comments

Comments
 (0)