kkokkio - 프로젝트/트러블슈팅

MySQL에서 중복 데이터를 관리하는 방법

파란배개 2025. 6. 16. 06:39

문제 개요

스케줄링 작업의 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 고려하기 시작함.

찾아보니 동시성 제어 관련 이슈와도 연관있는 것 같다.

최종 해결책 및 구현

테이블 속성의 업데이트를 할 필요가 없어서 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 오버헤드를 크게 줄여 전체적인 스케줄링 작업의 처리 속도를 개선함.