문제 개요
스케줄링 작업의 DB에 저장하는 과정에서 동일한 post_id와 keyword_id 조합을 가진 데이터(fingerPrint)가 중복되어 삽입되는 문제가 발생.
오류 메시지 및 원인 분석
작동시 해당 오류 메세지가 발생함을 확인.
2025-05-01T17:14:01.687+09:00 ERROR 16586 --- [kkokkio] [cTaskExecutor-1] o.h.engine.jdbc.spi.SqlExceptionHelper : Duplicate entry '46-55bdedb2a00d7c1ad7567d9647cc24318a470d72d1514e9177a8fc7827357' for key 'keyword_source.uq_ps_keyword_fingerprint'
2025-05-01T17:14:01.691+09:00 TRACE 16586 --- [kkokkio] [cTaskExecutor-1] o.s.t.i.TransactionInterceptor : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.saveAll] after exception: org.hibernate.exception.ConstraintViolationException: could not execute statement [Duplicate entry '46-55bdedb2a00d7c1ad7567d9647cc24318a470d72d1514e9177a8fc7827357' for key 'keyword_source.uq_ps_keyword_fingerprint'] [/* insert for site.kkokkio.domain.source.entity.KeywordSource */insert into keyword_source (created_at,keyword_id,fingerprint,updated_at) values (?,?,?,?)]
2025-05-01T17:14:01.693+09:00 TRACE 16586 --- [kkokkio] [cTaskExecutor-1] o.s.t.i.TransactionInterceptor : Completing transaction for [site.kkokkio.domain.source.service.SourceService.searchNews] after exception: org.springframework.dao.DataIntegrityViolationException: could not execute statement [Duplicate entry '46-55bdedb2a00d7c1ad7567d9647cc24318a470d72d1514e9177a8fc7827357' for key 'keyword_source.uq_ps_keyword_fingerprint'] [/* insert for site.kkokkio.domain.source.entity.KeywordSource */insert into keyword_source (created_at,keyword_id,fingerprint,updated_at) values (?,?,?,?)]; SQL [/* insert for site.kkokkio.domain.source.entity.KeywordSource */insert into keyword_source (created_at,keyword_id,fingerprint,updated_at) values (?,?,?,?)]; constraint [keyword_source.uq_ps_keyword_fingerprint]
해결 및 고민 과정
먼저 DB에서 find를 한 후 save를 하려고 했는데 스케줄링 작업이여서 효율적이지 못하다고 판단.
- 매번 데이터를 조회하는 오버헤드가 크고, 위에서 언급한 레이스 컨디션으로 인해 여전히 중복 데이터가 발생할 가능성 존재.
MySQL의 자체 쿼리인 **** ON DUPLICATE KEY UPDATE 또는 INSERT IGNORE, REPLACE INTO 고려하기 시작함.
찾아보니 동시성 제어 관련 이슈와도 연관있는 것 같다.
- https://code-killer.tistory.com/183
- https://nakhonest.tistory.com/entry/동시성-문제데드락-해결기-X-락인데-왜-공유가-가능하지
- https://joojimin.tistory.com/71
최종 해결책 및 구현
테이블 속성의 업데이트를 할 필요가 없어서 INSERT IGNORE로 진행함.
INSERT Ignore 를 여러 record에 한번에 적용하려면 Custom Repository 구현 필요.
https://github.com/orgs/prgrms-web-devcourse-final-project/projects/116/views/6?pane=issue&itemId=108803712&issue=prgrms-web-devcourse-final-project|WEB4_5_GAEPPADAK_BE|82
결과 및 성능 분석
코드 구현 결과
@Repository
@RequiredArgsConstructor
public class PostKeywordRepositoryCustomImpl implements PostKeywordRepositoryCustom {
private final EntityManager em;
@Transactional
@Override
public void insertIgnoreAll(List<PostKeyword> mappings) {
if (mappings.isEmpty()) return;
// SQL Injection 방지를 위해 VALUES 절의 개수를 동적으로 생성
StringBuilder sql = new StringBuilder("""
INSERT IGNORE INTO post_keyword
(post_id, keyword_id, created_at, updated_at)
VALUES
""");
for (int i = 0; i < mappings.size(); i++) {
sql.append("(?, ?, NOW(), NOW())"); // 파라미터 Placeholder (?) 사용
if (i < mappings.size() - 1) sql.append(", ");
}
Query query = em.createNativeQuery(sql.toString());
int index = 1; // 파라미터 인덱스 시작
for (PostKeyword pk : mappings) {
query.setParameter(index++, pk.getPost().getId());
query.setParameter(index++, pk.getKeyword().getId());
}
query.executeUpdate();
}
}
- DB 중복 저장 오류 해결 -> MySQL의 INSERT IGNORE 처리를 위해 Custom Repository 설정함.
- setParameter를 통해 파라미터를 바인딩하여 SQL Injection 위험을 방지하고 성능을 향상
- 여러 건의 데이터를 한 번의 쿼리로 처리함으로써 I/O 오버헤드를 크게 줄여 전체적인 스케줄링 작업의 처리 속도를 개선함.
'kkokkio - 프로젝트 > 트러블슈팅' 카테고리의 다른 글
JwtAuthenticationFilter에서 요청할 때마다 Member DB 조회하는 문제 (0) | 2025.06.16 |
---|---|
[Spring 영속성] don't flush the Session after an exception occurs 오류 - 트랜잭션 안에서 JPA 엔티티 재사용 시 문제 (0) | 2025.06.16 |
Prometheus + Grafana 모니터링 도입기 (0) | 2025.06.16 |
JWT는 강제로 만료할 수 없는데 우리는 어떻게 블랙리스트를 관리하는가 (0) | 2025.06.16 |
accessToken의 만료 여부를 어떻게 확인 할 수 있을까 (0) | 2025.06.16 |