Commit d8a8775
* 유저 활동 카운터(createdCourse 등)의 Lost Update 방지
RunnectUser.createdCourse/createdRecord/createdScrap/createdPublicCourse가
순수 in-memory ++ 로만 구현돼 있어, 동시 요청이 같은 유저 row를 커밋 전에
읽으면 한쪽 증가분이 조용히 유실될 수 있었다(로컬 Postgres 대상 재현
테스트로 실증, 다음 커밋 참고).
RunnectUser에 @Version 낙관적 락을 추가하고, 카운터 증가 + 스탬프 지급을
메인 트랜잭션(코스/기록/스크랩 생성)에서 분리된 REQUIRES_NEW 트랜잭션
(UserStampService.recordActivityAndAwardStamp)으로 격리했다 — 낙관적 락
충돌이 코스/기록/스크랩 생성 자체까지 롤백시키지 않게 하기 위함이다.
충돌 시에는 OptimisticLockRetrier가 지터를 두고 재시도한다.
* 유저 카운터 Lost Update 재현/수정 검증 테스트 추가
로컬 Postgres에 대해 실제 트랜잭션 두 개가 커밋 전 상태를 서로 보지
못하는 상황을 재현해, @Version 도입 전에는 조용한 데이터 손상
(assertion 실패)으로, 도입 후에는 명시적 충돌 감지
(ObjectOptimisticLockingFailureException)로 바뀌었음을 검증한다.
두 번째 테스트는 실제 프로덕션 경로(OptimisticLockRetrier +
recordActivityAndAwardStamp)를 스레드 10개로 동시 호출해, 재시도로
충돌이 모두 해소되고 최종 카운트가 정확히 맞는지(호출부에는 예외가
전파되지 않는지) 검증한다.
* 스탬프/카운터 갱신이 메인 저장 롤백과 무관하게 커밋되던 문제 수정
CodeRabbit 리뷰 지적: recordActivityAndAwardStamp()가 REQUIRES_NEW라 호출
즉시 별도 트랜잭션으로 독립 커밋되는데, 이 호출이 메인 엔티티 저장보다
먼저(Scrap) 또는 저장 직후지만 같은 트랜잭션 커밋 전에(Course/Record) 실행되고
있었다. 메인 저장이 실패해 트랜잭션이 롤백돼도 이미 커밋된 스탬프/카운터는
되돌아가지 않는다.
실제로 재현: 같은 유저가 같은 코스를 동시에 스크랩하면 유니크 제약으로 한쪽만
저장에 성공하는데(#252), 수정 전에는 실패한 쪽도 스탬프/카운터가 반영돼
createdScrap이 실제 스크랩 개수(1)보다 많은 2로 기록됨을 테스트로 확인.
OptimisticLockRetrier에 runAfterCommit()을 추가해, 메인 트랜잭션이 실제로
커밋된 뒤에만(TransactionSynchronization.afterCommit) 재시도 로직이 실행되게
했다 — 이 프로젝트에 이미 있던 랭킹(Redis) 갱신의 afterCommit 패턴과 동일한
구조. 재시도가 모두 소진되는 극단적인 경우엔 메인 작업 자체는 이미 성공했으므로
예외를 던지지 않고 로그로만 남긴다.
* 스크랩 동시성 테스트가 로컬 시드 데이터에 우연히 의존하던 문제 수정
이 테스트는 PublicCourseRepository를 목으로 대체하면서도 (user_id,
public_course_id) FK 제약을 만족하려면 실제 course/public_course row가
DB에 있어야 한다는 걸 놓치고, 로컬 개발 DB에 우연히 남아있던 더미 데이터
(id=1)를 그대로 가정하고 작성했었다.
PR #251 CI에서 재현: CI는 매번 새로 뜨는 빈 DB라 그 row가 아예 없어서,
두 스레드 모두 진짜 유니크 제약 위반이 아니라 FK 제약 위반으로 실패했다.
그런데 둘 다 같은 DataIntegrityViolationException → ConflictException
경로를 타기 때문에 겉보기엔 "정상 통과"처럼 보였다 — 실제로는 검증하려던
레이스와 무관한 이유로 우연히 같은 예외 타입이 나온 것뿐이었다. 진단 로그로
capturedException/스레드별 결과/실제 저장된 scrap 개수/public_course 실존
여부를 직접 찍어보고서야 확인했다.
이제 @beforeeach에서 필요한 course/public_course row를 테스트가 직접
INSERT(네이티브 SQL)해서 어떤 환경에서 실행해도 동일하게 동작하도록 했다.
(로컬 Postgres 컨테이너에 postgis 패키지가 아예 설치돼 있지 않았던 것도
같이 확인해 postgresql-15-postgis-3를 설치했다 — 기존에 문서화된 "PostGIS
공유 라이브러리 결함"의 실제 원인이었다.)
* CI Postgres 이미지를 PostGIS 포함 이미지로 교체
Course.path가 PostGIS geometry 컬럼인데 CI는 순정 postgres:15를 써서
course 테이블 생성 자체가 매번 실패하고 있었다. Hibernate가 ddl-auto
실패를 조용히 로그만 남기고 넘어가는 바람에 지금까지 드러나지 않았을
뿐, CI에 course 테이블이 존재한 적이 없었다 — 실제 Course/PublicCourse
row가 필요 없던 테스트만 우연히 계속 통과해온 것이었다.
ScrapConcurrencyTest에 실제 course/public_course row를 직접 만들도록
고치는 과정에서 발견했다. prod/staging과 동일한 postgis/postgis 이미지로
바꿔 CI가 실제 스키마로 검증하도록 한다.
---------
Co-authored-by: 나미 <dnska6657@gmail.com>
1 parent ff0a1ab commit d8a8775
13 files changed
Lines changed: 428 additions & 33 deletions
File tree
- .github/workflows
- src
- main/java/org/runnect/server
- common/module/concurrency
- course/service
- record/service
- scrap/service
- user
- entity
- service
- test/java/org/runnect/server
- course/service
- record/service
- scrap
- service
- user
- service
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
17 | 22 | | |
18 | 23 | | |
19 | | - | |
| 24 | + | |
20 | 25 | | |
21 | 26 | | |
22 | 27 | | |
| |||
Lines changed: 78 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
Lines changed: 5 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
12 | 13 | | |
13 | 14 | | |
14 | 15 | | |
| |||
39 | 40 | | |
40 | 41 | | |
41 | 42 | | |
| 43 | + | |
42 | 44 | | |
43 | 45 | | |
44 | 46 | | |
| |||
65 | 67 | | |
66 | 68 | | |
67 | 69 | | |
68 | | - | |
69 | | - | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
70 | 73 | | |
71 | 74 | | |
72 | 75 | | |
| |||
Lines changed: 5 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
12 | 13 | | |
13 | 14 | | |
14 | 15 | | |
| |||
52 | 53 | | |
53 | 54 | | |
54 | 55 | | |
| 56 | + | |
55 | 57 | | |
56 | 58 | | |
57 | 59 | | |
| |||
91 | 93 | | |
92 | 94 | | |
93 | 95 | | |
94 | | - | |
95 | | - | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
96 | 99 | | |
97 | 100 | | |
98 | 101 | | |
| |||
Lines changed: 6 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| 9 | + | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
| |||
29 | 30 | | |
30 | 31 | | |
31 | 32 | | |
| 33 | + | |
32 | 34 | | |
33 | 35 | | |
34 | 36 | | |
| |||
46 | 48 | | |
47 | 49 | | |
48 | 50 | | |
49 | | - | |
50 | | - | |
51 | | - | |
52 | 51 | | |
53 | 52 | | |
54 | 53 | | |
| |||
60 | 59 | | |
61 | 60 | | |
62 | 61 | | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
63 | 66 | | |
64 | 67 | | |
65 | 68 | | |
| |||
Lines changed: 9 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| 15 | + | |
15 | 16 | | |
16 | 17 | | |
17 | 18 | | |
| |||
67 | 68 | | |
68 | 69 | | |
69 | 70 | | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
70 | 79 | | |
71 | 80 | | |
72 | 81 | | |
| |||
Lines changed: 40 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
| |||
20 | 21 | | |
21 | 22 | | |
22 | 23 | | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
23 | 63 | | |
24 | 64 | | |
25 | 65 | | |
| |||
Lines changed: 11 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
| |||
24 | 25 | | |
25 | 26 | | |
26 | 27 | | |
| 28 | + | |
27 | 29 | | |
28 | 30 | | |
29 | 31 | | |
| |||
55 | 57 | | |
56 | 58 | | |
57 | 59 | | |
| 60 | + | |
| 61 | + | |
58 | 62 | | |
59 | 63 | | |
60 | 64 | | |
61 | 65 | | |
62 | 66 | | |
63 | 67 | | |
64 | | - | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
65 | 74 | | |
66 | 75 | | |
67 | 76 | | |
| |||
130 | 139 | | |
131 | 140 | | |
132 | 141 | | |
133 | | - | |
134 | | - | |
| 142 | + | |
135 | 143 | | |
136 | 144 | | |
137 | 145 | | |
| |||
Lines changed: 11 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
| 27 | + | |
27 | 28 | | |
28 | 29 | | |
29 | 30 | | |
| |||
68 | 69 | | |
69 | 70 | | |
70 | 71 | | |
| 72 | + | |
| 73 | + | |
71 | 74 | | |
72 | 75 | | |
73 | 76 | | |
74 | 77 | | |
75 | 78 | | |
76 | 79 | | |
77 | | - | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
78 | 87 | | |
79 | 88 | | |
80 | 89 | | |
| |||
163 | 172 | | |
164 | 173 | | |
165 | 174 | | |
166 | | - | |
167 | | - | |
| 175 | + | |
168 | 176 | | |
169 | 177 | | |
170 | 178 | | |
| |||
0 commit comments