본 게시물은 트래블록스 프로젝트를 진행하며 발생한 문제 및 개선 사항을 정리한 글입니다.
https://hyomee2.tistory.com/133
2. 알림 조회 성능 개선 - 반정규화
본 게시물은 트래블록스 프로젝트를 진행하며 발생한 문제 및 개선 사항을 정리한 글입니다. https://hyomee2.tistory.com/132 1. 알림 조회 성능 개선 - 인덱스본 게시물은 트래블록스 프로젝트를 진행하
hyomee2.tistory.com
앞선 글에서는 반정규화를 이용하여 병목이었던 COUNT 집계 쿼리를 제거하고 알림 목록 조회 API의 응답 시간을 개선했다.
하지만 반정규화를 적용하며 또 다른 문제를 마주했다.
바로 동시성 문제다!
문제 상황
예를 들어 현재 특정 사용자의 notification_count(알림 개수)가 10개라고 해보자.
이때 동시에 두 알림이 생성된다면 아래와 같은 상황이 발생할 수 있다.
Thread A: 10 조회 -> Thread A: 11 저장
Thread B: 10조회 -> Thread B: 11 저장
실제로는 알림이 2개 생성되었으니 최종적으로 12가 되어야 하지만,
마지막에 저장한 값이 덮어쓰여져 11이 된다.
이러한 문제를 Lost Update라고 한다.
해결 방안 검토
1. Optimistic Lock
충돌이 자주 발생하지 않음을 가정하고 충돌을 감지하는 방식이다.
엔티티의 버전을 관리하여, 수정 시 조회했던 버전과 현재 버전이 일치하는지 확인하고,
만약 다른 트랜잭션이 먼저 데이터를 수정했다면 OptimisticLockException을 발생시킨다.
장점
- Lock을 잡지 않고 있기 때문에 다른 트랜잭션을 대기시키지 않아 높은 처리량을 기대할 수 있다.
- 충돌이 자주 발생하지 않는 환경에서 우수한 성능을 갖는다.
단점
- 충돌 발생 시 OptimisticLockException이 발생하므로 재시도 로직을 구현해야 한다.
- 동일한 데이터에 대한 수정이 빈번할수록 충돌과 재시도가 반복되어 오히려 성능이 저하될 수 있다.
notification_count는 여러 알림이 동시에 생성될 경우 동일한 행을 빈번하게 갱신해야 한다.
따라서 optimistic lock을 적용하면 충돌이 자주 발생하고, 예외 처리와 재시도 로직도 반복될 가능성이 높기 때문에 적합하지 않다고 판단했다.
2. Pessimistic Lock
동시에 같은 데이터를 수정하는 일이 빈번하게 발생할 것이라고 가정하는 동시성 제어 방식이다.
데이터를 조회할 때 쓰기 락을 획득하여 다른 트랜잭션이 해당 데이터를 수정하지 못하도록 한다.
장점
- 데이터를 수정하기 전에 행을 잠그므로 Lost Update를 방지할 수 있다.
- 충돌이 발생하더라도 예외나 재시도 없이 순차적으로 처리된다.
- 충돌 가능성이 높은 환경에서도 데이터 정합성을 안정적으로 보장할 수 있다.
단점
- 충돌이 발생할 경우 다른 트랜잭션들은 락이 해제될 때까지 대기해야 하므로 응답시간, 처리량이 감소할 수 있다.
비관적 락은 데이터 정합성이 중요한 경우에 적합하다고 판단했다.
현재 notification_count를 단순히 1 증가시키는 연산에서 비관적 락을 적용하면
동일 회원에 대한 다른 수정 작업도 락이 해제될 때까지 대기해야 한다.
이는 응답 시간 증가와 처리량 감소로 이어질 수 있어 현재 상황에 적합하지 않다고 판단했다.
3. synchronized
Java의 synchronized를 사용하면 단일 서버 환경에서 스레드 간 동시 접근을 제어할 수 있다.
하지만 synchronized는 JVM 내부에서만 동작하며,
Java 객체에 대한 접근을 제어할 뿐 데이터베이스 자체의 상태를 관리하는 방식은 아니다.
notification_count 값은 결국은 DB에 저장되는 데이터이기 때문에,
애플리케이션 레벨에서 Lock을 관리하는 것보다는 DB 레벨에서 원자성을 보장하는 것이 더 적절하다고 판단했다.
4. DB Atomic UPDATE
현재값을 조회한 후 애플리케이션에서 계산하는 것이 아니라,
데이터베이스가 제공하는 원자적 UPDATE 연산을 이용하여 값을 직접 증가, 감소시키는 방식이다.
예를 들어 아래와 같이 하나의 UPDATE문에서 현재값을 기준으로 연산을 수행한다.
UPDATE members
SET notification_count = notification_count + 1
WHERE member_id = :memberId;
데이터베이스는 UPDATE를 수행하는 동안 해당 행에 대한 쓰기 lock을 획득하여 연산의 원자성을 보장하므로,
동시에 여러 요청이 들어와도 연산이 유실되지 않는다.
장점
- 데이터베이스가 원자성을 방지하므로 Lost Update를 방지할 수 있다.
- SELECT -> 수정 -> UPDATE 과정없이 UPDATE 한번으로 처리되어 불필요한 조회를 제거할 수 있다.
- 별도의 lock 관리나 재시도 로직이 필요없이 구현이 단순하다.
- 분산 환경에서도 동일한 방식으로 사용할 수 있다.
단점
- 단순한 증감 연산에는 적합하지만 여러 행을 함께 수정하거나 복잡한 비즈니스 로직에 적용은 어렵다.
따라서 결론적으로는 DB Atomic UPDATE를 통하여 Lost Update 문제를 해결했다.
* 위의 네가지 방식에 대해서 간단하게 작성했지만, 다음 글에서 자세히 다뤄볼 예정이다.
테스트
JUnit을 기반으로 동시성 테스트를 진행했다.
코드는 아래에서 확인할 수 있다.
travlocks-server/src/test/java/org/umc/travlocksserver/domain/notification/service/NotificationCommandServiceTest.java at develo
트래블록스 백엔드 레포지토리입니다. Contribute to Travlocks/travlocks-server development by creating an account on GitHub.
github.com
테스트 환경
알림 생성 시 개수와 관련한 동시성 문제를 검증하기 위해 JUnit 기반 동시성 테스트를 작성했다.
ExecutorService를 사용하여 100개의 스레드가 동시에 알림을 생성하는 것을 가정했다.
테스트 결과
DB Atomic Update 적용 전
100개의 동시 요청을 보낸 결과, 예상과 같이 모든 요청이 반영되지 않았으며, 평균적으로 약 12건만 정상적으로 반영되었다.
동일한 데이터를 여러 스레드가 동시에 수정하며 데이터 정합성이 보장되지 않은 것을 확인할 수 있었다.

DB Atomic Update 적용 후
100개의 동시 요청을 보낸 결과, DB Atomic Update 적용 전과 달리, 모든 요청이 정상적으로 처리되어 기대한 값과 정확히 일치했다.

| 동시 요청수 | 성공수 | 성공률 | |
| DB Atomic Update 적용 전 | 100개 | 12개 | 12% |
| DB Atomic Update 적용 후 | 100개 | 100개 | 100% |
마무리
알림 조회 API의 성능을 개선하던 중 찾은 COUNT을 해결하고 나니 또 다른 동시성 문제가 있었다.
동시성 문제를 해결할 때 흔히 "Lock"이 떠오르곤 하는데,
여러 동시성 문제 해결 방식을 찾고 고민해보며 이번 경우는 DB 레벨에서 원자성을 보장하는 것이 적절하다고 판단하고 적용할 수 있었다.
이를 통해 현재 상황에 맞는 적절한 방식을 선택하는 것을 연습하고, 그 중요성을 깨달을 수 있었다.
또한 단순히 어떤 방식을 적용했다는 것에서 끝내지 않고,
JUnit으로 여러 스레드를 통해 동시 요청을 보내는 테스트 코드를 작성해보고,
실제 수치적으로 적용 전과 후의 변화를 검증해볼 수 있어서 동시성 제어의 중요성을 깨달았다.
'projects > travlocks' 카테고리의 다른 글
| 2. 알림 조회 성능 개선 - 반정규화 (0) | 2026.07.03 |
|---|---|
| 1. 알림 조회 성능 개선 - 인덱스 (0) | 2026.07.03 |
| [트러블슈팅] Redis 캐시 조회 시 Invalid UTF-32 character 발생 문제 해결 (0) | 2026.01.29 |