안녕하세요 오랜만에 글을 쓰네요! 백엔드 개발자 최혜미입니다.
최근 eSIM 플랫폼 서비스를 운영하며 eSIM을 발급해주는 외부 연동사의 시스템 장애가 우리 서버의 마비로 이어지는 아찔한 경험을 했습니다.
단순히 "외부 업체가 느려져서"라고 치부하기엔, 내부 구조를 타고 흐르는 장애 전파 메커니즘이 생각보다 훨씬 치명적이었습니다.
이로 인해 약 2시간 동안 전체 서비스가 중단되는 뼈아픈 시간을 보내야만 했는데요...🫠
이번 글에서는 당시 발생했던 문제를 명확하게 정의해보고, 기존의 데이터 정합성은 유지하면서도 시스템 가용성을 개선했던 과정을 공유해보려 합니다.
가장 핵심적인 원인은 'DB 트랜잭션 내 외부 API 호출'이었습니다.
@Transactional에 의해 DB 커넥션(HikariCP)을 점유합니다.기존에 긴 트랜잭션을 유지했던 것은 데이터 정합성을 위한 의도적인 설계였습니다.
기존의 정합성 보장은 유지하되, 자원 점유 문제를 해결하기 위해 아키텍처를 전면 개편했습니다.
전체 프로세스를 물리적으로 분리된 3개의 짧은 트랜잭션으로 재설계하여 커넥션 점유 시간을 최소화했습니다.
issuing_started_at을 기록하여 '발급 중' 상태 선점 후 즉시 커넥션 반납.결과적으로 외부 API 응답을 기다리는 긴 시간 동안 우리 서버는 DB 커넥션을 전혀 점유하지 않게 되었습니다!
비관적 락 대신 컬럼 기반의 선점 방식을 도입하여 커넥션 없이도 동시성을 제어합니다.
UPDATE product
SET issuing_started_at = NOW()
WHERE id = :id
...
이 쿼리는 단일 트랜잭션 내에서 '문지기' 역할을 수행합니다. 거의 동시에 들어온 요청 중 단 하나만 업데이트 결과 1을 반환받고 나머지는 0을 받아 즉시 400 에러(중복 요청)를 반환하도록 처리했습니다.
동일 클래스 내 셀프 호출 시 @Transactional이 무시되는 AOP 프록시 문제를 해결하기 위해, DB 처리를 전담하는 EsimOrderProcessor를 별도로 분리했습니다. 이를 통해 각 단계의 트랜잭션이 물리적으로 완벽하게 시작되고 커밋됨을 보장했습니다.
흔히 경험이 중요하다고들 말하는데, 이번 사건을 겪으며 그 의미를 비로소 실감했습니다.
막상 개발을 시작하면 견고한 설계와 짧은 마감 기한 사이에서 균형을 잡기가 참 어렵지만, 외부 시스템의 불확실성을 우리 내부 리소스로 감내하려 해서는 안 된다는 귀중한 교훈을 얻었습니다.
이번에 깨달은 것은 정합성을 지키면서도 가용성을 잃지 않는 방법이었지만 앞으로 이러한 고민의 흔적들이 하나둘 모여 더욱 단단한 개발자로 성장할 수 있기를 바랍니다.