1. 개요
이 글은 수강신청 또는 예약신청과 같은 선착순 기반 신청 시스템에서 발생하는 대량 요청 처리에 대한 최적화 경험을 DB트랜잭션 중심으로 정리한 내용입니다. 해당 시스템은 신청 시작 직후 짧은 시간 동안 수만 건의 요청이 집중되는 특징을 가지고 있습니다. 사용자는 조회, 신청, 취소를 반복하며 최대 학점, 신청 가능 인원, 예약 한도 등의 여러 조건을 만족해야 합니다. 따라서 데이터 정합성을 유지하기 위한 데이터베이스 트랜잭션 처리가 매우 중요합니다.
실제 운영 환경에서는 약 2만 명의 사용자가 동시에 서비스를 이용하며, 5분에서 10분 사이에 약 8만 건에서 10만 건 수준의 신청 및 취소 요청이 발생하였습니다. 이러한 환경에서 신청 정보 생성과 신청자 수 집계 데이터 갱신이 핵심적인 트랜잭션 처리 대상이었습니다.
2. 초기 구현과 성능 문제
프로젝트 개발 완료 후 Apache JMeter를 활용하여 운영 환경과 유사한 조건으로 부하 테스트를 수행하였습니다. 초기 구현에서는 신청 API와 취소 API 각각 모두 API시작 시점에서 종료 시점까지 전체 범위에 트랜잭션이 적용되었습니다.
그러나 신청 가능 여부를 판단하기 위한 사전 조건이 많았고, 레거시 시스템에서 이관된 SQL의 성능 문제도 존재하였습니다. 이로 인해 API 처리 시간이 길어졌으며 트랜잭션 유지 시간도 증가하였습니다. 결과적으로 데이터베이스와의 연결이 장시간 유지되면서 커넥션 풀이 부족해졌습니다.
대량 요청이 발생하는 상황에서는 DBCP에서 커넥션을 획득하기 위한 대기 시간이 길어졌고, 일부 요청은 시간 초과로 실패하였습니다. 정상 응답 비율이 기대 수준에 미치지 못하는 문제가 발생하였습니다. 최종 목표는 모든 요청이 정상 응답을 돌려받는 것입니다.
3. 성능 개선 과정
첫 번째 개선 단계에서는 DB 인덱스를 추가하고 SQL 튜닝을 진행하였습니다. 이를 통해 조회 속도는 향상되었으며 API 처리 시간도 감소하였습니다. 하지만 여전히 커넥션 풀 부족 현상은 해결되지 않았습니다.
두 번째 단계에서는 MSA 환경에서 서비스 Pod 수를 증가시켜 스케일아웃을 수행하였습니다. 최종적으로 약 30개의 Pod를 운영하였으며 처리량은 증가하였습니다. 그러나 병목이 데이터베이스 커넥션에 존재하였기 때문에 요청 실패 문제는 완전히 해결되지 않았습니다.
세 번째 단계에서는 HikariCP의 커넥션 수를 기본값 10개에서 100개까지 확대하였습니다. 그 결과 WAS에서 동시에 처리하는 요청 수가 증가하였지만 메모리 사용량이 급격히 늘어나면서 Out Of Memory 오류가 발생하였습니다.
네 번째 단계에서는 Tomcat Heap 메모리를 256MB에서 4GB로 증설하였습니다. OOM 문제는 해소되었으나 커넥션 부족 현상은 계속 발생하였습니다. 다만 전체 실패율은 일부 감소하였습니다.
4. WAS와 DB 자원 균형 조정
문제의 본질은 단순히 처리 능력 부족이 아니라 데이터베이스 커넥션 사용 방식에 있다는 점이 확인되었습니다. DB커넥션 요청부분이 병목구간이 되면 k8s가 해당 POD를 서비스불가 상태로 전환하여 병목구간이 해소가 되도록 해야 되는데 Tomcat WAS 가 지속적으로 요청을 받을 수 있어서 요청실패가 발생하고 있었습니다.
이에 따라 Tomcat WAS의 요청을 동시 처리하는 리퀘스트 스레드 수를 기본값 200개에서 50개로 축소하였으며, DBCP의 커넥션 수도 100개에서 50개로 재조정하였습니다.
이러한 변경을 통해 DB커넥션 부족 현상은 상당 부분 완화되었습니다. 그러나 동시에 처리 가능한 요청 수가 제한되면서 또 다른 성능 제약이 발생하였습니다.
추가적으로 통합 데이터베이스 환경의 제약도 존재하였습니다. 여러 시스템이 하나의 물리 데이터베이스를 공유하고 있었기 때문에 사용할 수 있는 최대 DB커넥션 수가 제한적이었습니다. 따라서 단순히 커넥션 수를 증가시키는 방식으로는 문제를 해결할 수 없었습니다.
5. 요청 제어와 트랜잭션 최적
운영 안정성을 확보하기 위하여 기존 레거시 시스템에서 사용하던 요청 대기 솔루션(넷퍼넬)을 재도입하였습니다. 이를 통해 순간적인 트래픽 폭주를 제어할 수 있었으며, 운영 중 발생하던 대량 실패 문제는 사라졌습니다.
이후에는 트랜잭션 범위를 최소화하는 작업을 진행하였습니다. 신청 가능 여부를 판단하는 다양한 조건 중 실시간 변경이 필요하지 않고, 실시간 변경이 되지 않을 데이터의 조회 작업을 트랜잭션 범위 밖으로 이동시켰습니다. 이를 통해 DB 커넥션의 전체 점유 시간이 감소하였고 처리 가능한 요청 수가 증가하였습니다.
6. 데드락 문제와 해결
트랜잭션 최적화 이후에는 새로운 문제가 발생하였습니다. 신청자 수 집계 정보를 갱신하는 과정에서 select for update 쿼리 즉 비관적 락을 사용하고 있었는데, 동일 레코드에 대한 대량 요청이 집중되면서 데드락 현상이 발생하였습니다.
특히 하나의 트랜잭션이 추가적인 DB 커넥션을 요구하는 상황에서 커넥션 풀이 부족하면 교착 상태가 발생할 수 있었습니다. 이를 해결하기 위하여 요청당 필요한 최대 커넥션 수를 분석하고 적절한 커넥션 풀 크기를 산정하였습니다.
DB Pool Size = T × (C - 1) + 1
여기서 T는 WAS의 최대 스레드 수이고, C는 하나의 요청이 필요로 하는 최대 데이터베이스 커넥션 수입니다. 해당 공식을 기반으로 설정을 조정한 이후 데드락 현상은 사라졌습니다.
7. 신청 인원 초과 문제 대응
대량 요청 환경에서는 신청 제한 수를 초과하여 신청이 완료되는 현상도 발생하였습니다. 이를 해결하기 위하여 신청 완료 직후 시점을 기준으로 다시 한 번 검증을 수행하였습니다. 선착순으로 제한 수를 초과했는지 확인하는 방법이라 시점이 지나도 무관하게 확인할 수 있었습니다.
검증 결과 제한 인원을 초과한 경우에는 해당 트랜잭션을 롤백하거나 신청을 취소하는 방식으로 정합성을 유지하였습니다. 이를 통해 최종 데이터의 정확성을 확보할 수 있었습니다.
8. 비관적 락에서 낙관적 락으로 전환
성능 분석 결과 비관적 락(Pessimistic Lock)은 대량 요청 환경에서 주요 병목 요소로 작용하고 있었습니다. 이에 따라 비관적 락 기반 구조를 낙관적 락(Optimistic Lock) 기반 구조로 변경하였습니다.
낙관적 락을 적용하기 위해 수정 시각 속성을 버전 정보로 활용하였습니다. 엔티티의 수정 시간이 변경될 때마다 버전이 증가하도록 구성하였으며, 충돌이 발생하면 OptimisticLockException 예외가 발생하도록 하였습니다.
또한 Spring Retry 기능을 활용하여 충돌 발생 시 예외발생을 조건으로 자동 재시도를 수행하도록 하였습니다. 재시도 간격은 0.5초 미만으로 설정하였고 최대 10회까지 재시도하도록 구현하였습니다.
이러한 구조 변경 이후 데이터베이스 커넥션 사용 효율이 크게 향상되었습니다. 데드락 문제는 사실상 사라졌으며 커넥션 부족 현상도 해소되었습니다. 최종적으로 WAS 스레드 수 50개, DB커넥션 수 20개 수준에서도 안정적인 운영이 가능하였습니다.
9. 결론
본 사례는 단순한 서버 증설이나 커넥션 증가만으로는 대량 요청 문제를 해결할 수 없음을 보여줍니다. 실제 병목 지점을 정확하게 분석하고, 트랜잭션 범위를 최소화하며, 적절한 락 전략을 선택하는 것이 중요합니다.
특히 비관적 락을 낙관적 락으로 전환하고 재시도 전략을 도입한 것은 가장 효과적인 개선 방안 중 하나였습니다. 또한 데이터베이스 커넥션 풀, WAS 스레드 수, 트랜잭션 범위 간의 균형을 맞추는 것이 대량 요청 처리 시스템의 안정성과 성능을 확보하는 핵심 요소임을 확인할 수 있었습니다.
이러한 경험으로 수강신청, 예약 시스템, 이벤트 응모, 선착순 판매와 같은 다양한 대량 트래픽 시스템 설계에 적용할 수 있는 중요한 요소들을 알 수 있었습니다.
Daniel(K)