fix: 시드 서비스 중복 카운트 및 트랜잭션 범위 버그 수정
같은 시트에 동일 기관이 두 번 들어오면 DB는 upsert로 한 행에 합치지만 집계는 둘 다 created로 세어 report가 실제 결과와 어긋났다. 처리 완료된 키를 그 자리에서 seen 집합에 반영해 두 번째 등장부터는 updated로 잡히게 했다. 또한 seed()가 엑셀 파싱까지 @Transactional 안에서 수행해 파싱 중에도 DB 커넥션을 점유하고 있었다. 파싱은 트랜잭션 밖으로 빼고, DB 조회+upsert 구간만 TransactionTemplate으로 감싸 커넥션 점유 구간을 최소화했다. (같은 빈 self-invocation으로 @Transactional을 추가하면 프록시를 우회해 트랜잭션이 걸리지 않으므로 이 방식을 택함.) 중복 기관 업로드 시나리오에 대한 테스트를 추가했다.
@9e342fa7f6447d5ac764d0eec499e61cb87ad0a4
--- src/main/java/kr/itn/itnhub/seed/SeedService.java
+++ src/main/java/kr/itn/itnhub/seed/SeedService.java
... | ... | @@ -3,7 +3,8 @@ |
| 3 | 3 |
import kr.itn.itnhub.org.Organization; |
| 4 | 4 |
import kr.itn.itnhub.org.OrganizationMapper; |
| 5 | 5 |
import org.springframework.stereotype.Service; |
| 6 |
-import org.springframework.transaction.annotation.Transactional; |
|
| 6 |
+import org.springframework.transaction.PlatformTransactionManager; |
|
| 7 |
+import org.springframework.transaction.support.TransactionTemplate; |
|
| 7 | 8 |
|
| 8 | 9 |
import java.io.InputStream; |
| 9 | 10 |
import java.util.HashSet; |
... | ... | @@ -15,29 +16,42 @@ |
| 15 | 16 |
|
| 16 | 17 |
private final SeedParser parser; |
| 17 | 18 |
private final OrganizationMapper mapper; |
| 19 |
+ private final TransactionTemplate transactionTemplate; |
|
| 18 | 20 |
|
| 19 |
- public SeedService(SeedParser parser, OrganizationMapper mapper) {
|
|
| 21 |
+ public SeedService(SeedParser parser, OrganizationMapper mapper, |
|
| 22 |
+ PlatformTransactionManager transactionManager) {
|
|
| 20 | 23 |
this.parser = parser; |
| 21 | 24 |
this.mapper = mapper; |
| 25 |
+ this.transactionTemplate = new TransactionTemplate(transactionManager); |
|
| 22 | 26 |
} |
| 23 | 27 |
|
| 24 |
- @Transactional |
|
| 28 |
+ /** |
|
| 29 |
+ * 엑셀 파싱은 DB와 무관하므로 트랜잭션 밖에서 수행한다. 커넥션을 잡아두는 구간은 |
|
| 30 |
+ * 기존 행 조회 + upsert 루프뿐이며, {@link TransactionTemplate}으로 그 구간만 감싼다.
|
|
| 31 |
+ * (같은 빈의 @Transactional 메서드를 self-invocation으로 호출하면 프록시를 우회해 |
|
| 32 |
+ * 트랜잭션이 아예 적용되지 않으므로 이 방식을 쓴다.) |
|
| 33 |
+ */ |
|
| 25 | 34 |
public SeedReport seed(InputStream xlsx) {
|
| 26 | 35 |
List<SeedRow> rows = parser.parse(xlsx); |
| 36 |
+ return transactionTemplate.execute(status -> seedRows(rows)); |
|
| 37 |
+ } |
|
| 27 | 38 |
|
| 28 |
- Set<String> before = new HashSet<>(); |
|
| 39 |
+ private SeedReport seedRows(List<SeedRow> rows) {
|
|
| 40 |
+ // DB에 이미 있는 키와, 이번 업로드에서 먼저 처리된 키를 모두 "이미 존재함"으로 취급한다. |
|
| 41 |
+ // 그래야 한 시트에 같은 기관이 두 번 들어와도 두 번째는 created가 아니라 updated로 집계된다. |
|
| 42 |
+ Set<String> seen = new HashSet<>(); |
|
| 29 | 43 |
for (Organization existing : mapper.findAll()) {
|
| 30 |
- before.add(key(existing.getOrgNo(), existing.getOrgName())); |
|
| 44 |
+ seen.add(key(existing.getOrgNo(), existing.getOrgName())); |
|
| 31 | 45 |
} |
| 32 | 46 |
|
| 33 | 47 |
int created = 0; |
| 34 | 48 |
int updated = 0; |
| 35 | 49 |
|
| 36 | 50 |
for (SeedRow row : rows) {
|
| 37 |
- if (before.contains(key(row.orgNo(), row.orgName()))) {
|
|
| 38 |
- updated++; |
|
| 39 |
- } else {
|
|
| 51 |
+ if (seen.add(key(row.orgNo(), row.orgName()))) {
|
|
| 40 | 52 |
created++; |
| 53 |
+ } else {
|
|
| 54 |
+ updated++; |
|
| 41 | 55 |
} |
| 42 | 56 |
mapper.upsertBySeed(toOrganization(row)); |
| 43 | 57 |
} |
--- src/test/java/kr/itn/itnhub/seed/SeedServiceTest.java
+++ src/test/java/kr/itn/itnhub/seed/SeedServiceTest.java
... | ... | @@ -102,6 +102,20 @@ |
| 102 | 102 |
} |
| 103 | 103 |
|
| 104 | 104 |
@Test |
| 105 |
+ void 같은_기관이_한_시트에_두번_들어있으면_한_행만_남고_두번째는_갱신으로_집계된다() throws Exception {
|
|
| 106 |
+ byte[] xlsx = workbook( |
|
| 107 |
+ new String[]{"001", "00", "국제방송교류재단", "팀", "송민지", "과장", "02-1", "a@b.kr"},
|
|
| 108 |
+ new String[]{"001", "00", "국제방송교류재단", "팀", "송민지", "과장", "02-1", "a@b.kr"});
|
|
| 109 |
+ |
|
| 110 |
+ SeedReport report = seedService.seed(new ByteArrayInputStream(xlsx)); |
|
| 111 |
+ |
|
| 112 |
+ assertThat(mapper.findAll()).hasSize(1); |
|
| 113 |
+ assertThat(report.total()).isEqualTo(2); |
|
| 114 |
+ assertThat(report.created()).isEqualTo(1); |
|
| 115 |
+ assertThat(report.updated()).isEqualTo(1); |
|
| 116 |
+ } |
|
| 117 |
+ |
|
| 118 |
+ @Test |
|
| 105 | 119 |
void 담당자정보가_없는_기관은_정보대기로_들어온다() throws Exception {
|
| 106 | 120 |
byte[] xlsx = workbook( |
| 107 | 121 |
new String[]{"008", "00", "경찰청", "데이터정책계", "박진우", "경위", "02-3", "e@f.kr"},
|
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?