티스토리 뷰
1. 현재 상황 분석
최근 프로젝트를 테스트하던 중 동화 생성 기능에서 문제가 발견되었습니다.
동화 생성 도중 문제가 발생해 데이터베이스에서는 롤백에 되었지만, S3에 업로드된 이미지(표지 및 페이지 이미지)는 그대로 남아 불필요한 데이터가 쌓이는 불일치 문제가 발생했습니다.
이번 글에서는 S3와 DB의 데이터 불일치에 관한 문제에 대해 해결해보도록 하겠습니다.
2. 코드 분석

처음 기능을 구현할 때 이미지 생성 - S3 업로드 - 트랜잭션 커밋 순으로 롤백될 확률을 줄이도록 개발을 했으나,
동화 생성에 관련된 다른 코드들이 추가되면서 트랜잭션의 크기가 점차 커졌고, S3에 업로드되는 파일도 늘어나게 되었습니다.
이렇게 트랜잭션의 크기가 커질수록 롤백될 경우의 수도 많아지고, 관리되지 못한 S3 객체들이 쌓이게 된다는 점을 발견했습니다. 특히, S3의 비용은 시간이 지남에 따라 증가할 수 있기 때문에, 지속적으로 프로젝트를 운영하게 되면 불필요한 비용이 발생할 수 있다는 문제도 고려해야 했습니다.
3. 해결방법 - @TransactionalEventListener
@TransactionalEventListener는 트랜잭션의 상태를 감지하여 이벤트를 실행하는 어노테이션입니다.
트랜잭션 롤백을 감지하고, 그에 따라 업로드된 S3 객체에 대한 추가 처리를 자동화할 수 있다고 생각했습니다.
3-1. 트랜잭션 상태
트잭션의 상태를 감지하는 단계는 총 4가지로 나뉩니다.
1. BEFORE_COMMIT
2. AFTER_COMMIT
3. AFTER_ROLLBACK
4. AFTER_COMPLETION
이 중에서 트랜잭션 롤백 시점을 감지할 수 있는 상태는 AFTER_ROLLBACK입니다. 이 상태를 활용해 롤백 발생 시 불필요한 파일 삭제 작업을 자동으로 처리해보도록 하겠습니다.
3-2. @TransactionalEventListener 적용
4. 성능 최적화
동화를 생성할 때, 하나의 파일을 S3에 업로드하는 작업이라면 해당 로직은 큰 문제가 없을 가능성이 있습니다.
그러나 동화 생성 과정에서 각 페이지가 약 10개 정도 생성되어 S3에 업로드됩니다.
이 경우, 각 페이지 업로드 후 문제가 발생하면 10개의 이벤트가 발생하며, 이를 삭제하기 위해 S3에 10번 접근해야 합니다.
이 과정은 성능 저하를 유발할 가능성이 크다고 판단을 했습니다.
물론 이 부분을 @Async를 활용해 비동기로 처리하면 응답 속도는 개선될 수 있지만,
S3 접근 횟수 자체는 줄어들지 않으므로 성능 문제가 완전히 해결되지는 않습니다.
4-1. 해결방법에 대한 고민
삭제 작업은 반드시 즉시 처리해야 하는 작업은 아닙니다. 따라서 이벤트 발생 시 바로 S3로 삭제 요청을 보내는 대신, 삭제해야 할 파일 정보를 DB에 저장하는 방식을 고려해볼 수 있습니다.
이후 특정 시점에 스케줄러를 통해 DB에 저장된 삭제 대상 파일 정보를 조회한 뒤, 한 번에 삭제 요청을 보내는 방법을 활용할 수 있습니다.
해결 방안 요약
1. 비동기 처리: 응답 속도 개선을 위해 비동기 처리 적용
2. 삭제 정보 DB 저장: 삭제 요청을 즉각적으로 처리하지 않고 정보를 DB에 기록
3. 스케줄링 활용: 스케줄러를 통해 한 번에 삭제 요청을 처리
4-2. 해결방법 적용 - 비동기 처리
S3 파일 삭제 관련 작업은 사용자의 즉각적인 응답성과는 무관하므로, 별도의 스레드에서 처리되도록 @Async를 활용했습니다.
롤백 이벤트는 일반적으로 자주 발생하지 않는 작업이므로, 경량 작업 처리가 가능한 SimpleAsyncTaskExecutor를 활용하여 복잡한 스레드 풀 설정 없이도 효율적으로 처리하도록 설정하였습니다.
4-3. 해결방법 적용 - 삭제 정보 DB 저장
트랜잭션 롤백 이벤트 발생 시, S3 삭제 요청을 즉시 처리하는 대신, 삭제 예정 파일의 정보를 저장할 수 있도록 S3DeleteFile 테이블을 설계하고 적용하였습니다.
롤백 이벤트로 인해 책과 관련된 표지 파일(1개)과 페이지 파일(10개) 총 11개의 삭제 대상 정보가 S3DeleteFile 테이블에 저장된 것을 확인할 수 있습니다.
4-4. 해결방법 적용 - 스케줄링 활용
매일 자정, 스케줄링을 통해 S3DeleteFile 테이블에 저장된 삭제 예정 파일 정보를 조회하고, 해당 파일들을 S3에서 삭제하도록 구현하였습니다.
S3의 DeleteObjects API는 한 번에 최대 1000개까지의 객체 삭제 요청만 허용됩니다.
이를 고려하여 삭제 대상 파일 리스트를 ChunkSize(1000) 단위로 나누어 처리하도록 구현하였습니다.
현재 서비스에서는 롤백 이벤트는 드물게 발생하며, 한 번 발생할 경우에도 삭제해야 할 파일 수는 약 11개 내외로 예상됩니다. 따라서 현실적으로 ChunkSize(1000)을 초과하는 요청이 발생할 가능성은 낮습니다.
하지만, 시스템이 성장하거나 사용자가 많아질 경우 삭제 대상 파일의 수가 증가할 가능성을 고려했습니다. 이를 대비하기 위해 ChunkSize 로직을 추가하여, 초과 요청으로 인한 예외 상황을 사전에 방지하고 안정성을 확보하고자 했습니다. 이 로직은 처리 대상 파일이 적은 경우에도 성능에 영향을 거의 미치지 않기 때문에 적용을 해도 큰 무리가 없다는 판단을 했습니다.
이번 글에서는 S3와 DB 간의 데이터 불일치 문제를 해결하기 위한 방법을 다뤄보았습니다.
다음 글에서는 앞서 언급한 첫 번째 문제인 동시성 문제를 해결하기 위한 방법을 소개하도록 하겠습니다.
'Troubleshooting' 카테고리의 다른 글
[인기글 불러오기] #2 인덱스 최적화 (0) | 2025.04.07 |
---|---|
[인기글 불러오기] #1 쿼리 구조 개선 (0) | 2025.04.07 |
[이메일 발송 기능] #3 비동기 성능 최적화 (0) | 2025.04.04 |
[이메일 발송 기능] #2 별도 스레드를 활용한 비동기 호출 방식 (0) | 2025.01.10 |
[이메일 발송 기능] #1 동기 호출 방식 (0) | 2025.01.05 |
- @Entity
- 메일
- 1차 캐시
- onetoone
- 스키마 자동 생성
- @GeneratedValue
- 연관관계
- @Cacheable
- mappedBy
- 조인 전략
- 준영속
- @joincolumn
- @ManyToOne
- @Table
- @OneToMany
- 비동기
- @MappedSuperclass
- 변경감지
- Redis
- 즉시 로딩
- @Id
- 최적화
- 인메모리 db
- JPA
- 비영속
- @TransactionalEventListener
- N + 1
- 영속성 컨텍스트
- 엔티티 매니저
- 단일 테이블 전략