보이지 않는 트랜잭션은 글로 드러낼 때 비로소 위험해진다
Hatched by min dulle
Aug 30, 2026
8 min read
0 views
94%
시스템을 망가뜨리는 것은 대개 한 줄의 코드가 아니다
성능 장애의 원인이 데이터베이스에 수천만 건의 데이터가 쌓여서가 아니라, 코드 리뷰에서 아무도 문제 삼지 않은 트랜잭션 하나일 수 있다면 어떨까? 더 이상한 사실도 있다. 그 트랜잭션은 눈에 띄는 복잡한 로직이 아니라, 안전해 보이는 @Transactional 하나와 내부 호출 몇 번으로 만들어질 수 있다.
우리는 시스템의 위험을 대체로 크기로 판단한다. 데이터가 많을수록 위험하고, 요청량이 클수록 위험하며, 쿼리가 복잡할수록 위험하다고 생각한다. 그러나 실제 시스템에서는 크기가 아니라 숨겨진 구조적 연결이 장애의 규모를 결정하는 경우가 많다. 한 개의 긴 트랜잭션이 전역 자료구조와 락 경합을 자극하고, 그 결과 전체 서비스의 처리량이 급격히 떨어질 수 있다.
여기서 중요한 질문이 생긴다.
어떻게 작은 코드 조각이 시스템 전체의 비용으로 변하는가? 그리고 우리는 왜 그 변환을 사전에 알아차리지 못하는가?
이 질문은 데이터베이스 설계만의 문제가 아니다. 사고가 머릿속에만 머물 때 생기는 문제와도 정확히 연결된다. 머릿속에서 대충 이해한 구조는 실제로는 존재하지 않는 것과 비슷하다. 글로 쓰고, 경계를 표시하고, 호출 관계를 펼쳐 놓을 때에야 비로소 숨은 전제가 드러난다.
이 글의 핵심 주장은 간단하다. 글쓰기는 생각을 표현하는 도구가 아니라, 보이지 않는 시스템 비용을 발견하는 관측 장치다. 좋은 설계와 좋은 코드는 모두 같은 과정을 거친다. 암묵적인 관계를 바깥으로 꺼내고, 국소적인 편의가 전역적인 결과로 바뀌는 경로를 보여 주는 과정이다.
편리한 추상화가 숨기는 것들
트랜잭션은 본래 훌륭한 추상화다. 여러 데이터 변경을 하나의 작업처럼 묶고, 중간 상태를 외부에 노출하지 않으며, 실패했을 때 되돌릴 수 있게 한다. 개발자는 복잡한 데이터 일관성 문제를 매번 처음부터 다루지 않아도 된다.
문제는 추상화가 비용을 없애는 것이 아니라 비용의 위치를 바꾼다는 데 있다. 애플리케이션 코드에서는 단순한 메서드 호출처럼 보이지만, 데이터베이스 내부에서는 락, 가시성 정보, 트랜잭션 ID, 저장 구조, 캐시, 정리 작업이 함께 움직인다. 특히 트랜잭션 안에서 또 다른 트랜잭션처럼 보이는 호출이 반복되면, 개발자가 읽는 코드의 깊이와 데이터베이스가 관리해야 하는 상태의 깊이가 달라진다.
예를 들어 다음과 같은 구조를 생각해 보자.
주문 처리 트랜잭션
상품 재고 확인
회원 상태 갱신
쿠폰 사용 기록
알림 발송 기록
각 메서드에 일관성을 보장한다는 이유로 트랜잭션 어노테이션을 붙이면, 코드는 안전해 보인다. 그러나 실제 실행 경계가 어떻게 합쳐지는지, 내부적으로 중첩된 트랜잭션이나 세이브포인트가 생기는지, 장시간 유지되는 락이 어디에서 만들어지는지는 코드 표면만으로 쉽게 드러나지 않는다.
PostgreSQL의 MultiXact와 같은 내부 구조는 이 문제를 선명하게 보여 준다. 여러 트랜잭션이 하나의 행에 관여하는 상황을 관리하기 위해 전역 자료구조와 디스크 기반 저장 영역, 캐시, 락이 함께 동원된다. 평소에는 이 비용이 드러나지 않을 수 있다. 하지만 특정 접근 패턴과 긴 트랜잭션이 만나면, 작은 애플리케이션 변화가 전역 경합으로 증폭된다.
여기서 중요한 것은 특정 내부 구조의 세부 사항을 암기하는 일이 아니다. 더 일반적인 원리를 이해하는 일이다.
국소적인 선언은 전역적인 상태를 만든다.
메서드 하나에 붙은 선언은 그 메서드 안에만 머물지 않는다. 호출 그래프를 따라 트랜잭션 경계를 바꾸고, 데이터베이스의 메타데이터 접근을 늘리며, 다른 요청이 기다려야 하는 시간을 바꾼다. 개발자가 보는 단위는 메서드지만, 시스템이 지불하는 단위는 공유 자원이다.
글쓰기는 시스템의 숨은 상태를 펼쳐 놓는다
이 지점에서 글쓰기가 중요해진다. 생각이 복잡해서 글을 쓰는 것이 아니다. 글로 쓰기 전까지는 생각이 실제로 충분히 복잡해지지 않았기 때문에 글이 필요하다.
머릿속에서는 다음과 같은 문장이 쉽게 성립한다.
“이 메서드는 데이터를 안전하게 처리한다.”
하지만 글로 옮기려면 질문이 생긴다.
- 안전하다는 것은 원자성을 의미하는가, 격리 수준을 의미하는가?
- 트랜잭션은 어디에서 시작되고 어디에서 끝나는가?
- 내부 호출은 같은 트랜잭션에 참여하는가, 새로운 경계를 만드는가?
- 가장 오래 걸리는 작업은 무엇인가?
- 이 트랜잭션이 기다리는 자원은 다른 요청과 공유되는가?
- 데이터베이스 내부에서 이 호출은 어떤 상태와 자료구조를 움직이는가?
이 질문들은 단순한 문서화 항목이 아니다. 시스템의 인과 관계를 외부화하는 절차다. 코드를 읽을 때는 호출이 순서대로 보이지만, 글을 쓸 때는 시간과 범위와 의존성이 함께 보인다. 머릿속에서는 “재고를 확인하고 쿠폰을 처리한다”로 끝나던 흐름이, 문서에서는 “재고 행을 잠근 뒤 외부 서비스 응답을 기다리고, 그동안 다른 요청이 같은 자원을 기다린다”로 바뀐다.
이 차이는 결정적이다. 전자는 기능 설명이고, 후자는 장애 설명이다.
글쓰기는 또한 경계 표시 장치다. 모든 시스템 문제는 경계가 흐려질 때 커진다. 데이터베이스 트랜잭션과 비즈니스 작업의 경계, 빠른 조회와 느린 외부 호출의 경계, 애플리케이션 객체와 데이터베이스 상태의 경계가 섞이면, 개발자는 한 작업의 비용을 다른 작업의 비용으로 착각한다.
다음과 같은 표만 작성해도 많은 문제가 드러난다.
| 단계 | 애플리케이션 의도 | 실제 공유 자원 | 유지 시간 | 실패 시 영향 |
|---|---|---|---|---|
| 재고 잠금 | 중복 판매 방지 | 재고 행과 락 | 주문 처리 전체 | 대기 요청 증가 |
| 회원 상태 갱신 | 상태 일관성 보장 | 회원 행과 트랜잭션 메타데이터 | 중첩 호출 종료까지 | 경합 확대 |
| 외부 결제 호출 | 결제 결과 반영 | 데이터베이스 트랜잭션 | 네트워크 지연만큼 | 긴 트랜잭션 |
표의 목적은 예쁘게 정리하는 것이 아니다. 의도와 실제 비용 사이의 간극을 발견하는 것이다. “안전하게 처리한다”는 의도가 데이터베이스에서는 “공유 자원을 오랫동안 붙잡는다”로 구현될 수 있다.
장애 조사는 글쓰기의 극한 형태다
운영 장애를 조사할 때 좋은 팀은 로그 몇 줄을 보고 결론을 내리지 않는다. 가설을 세우고, 관측 지점을 정하고, 대조군을 만들고, 실제 실행으로 확인한다. 이것은 과학적 방법이면서 동시에 글쓰기의 방법이기도 하다.
가령 특정 코드 경로에 세이브포인트가 반복해서 만들어진다는 의심이 있다고 하자. 먼저 다음과 같이 가설을 문장으로 쓴다.
긴 트랜잭션 안에서 중첩된 트랜잭션 경계가 반복되면, 테이블 데이터의 크기와 무관하게 전역 트랜잭션 관리 구조의 경합이 증가하고 처리량이 감소할 수 있다.
이 문장은 단순한 추측보다 훨씬 유용하다. 무엇을 비교해야 하는지 알려 주기 때문이다. 중첩 경계를 만드는 경로와 그렇지 않은 경로를 나누고, 같은 데이터와 같은 요청량으로 실행하며, 처리량뿐 아니라 대기 시간과 내부 캐시 접근, 락 경합을 관찰해야 한다.
실험 결과가 나오면 처음의 설명을 다시 써야 한다. 데이터베이스 크기를 크게 늘렸는데도 결과가 크게 달라지지 않는다면, 문제의 핵심은 데이터 양이 아니라 접근 패턴일 가능성이 높다. 반대로 단 하나의 긴 트랜잭션만으로도 영향이 커진다면, 평균적인 요청의 성능을 측정하는 것만으로는 충분하지 않다는 뜻이다.
이 과정에서 글은 세 가지 역할을 한다.
첫째, 가설의 경계를 제한한다. “트랜잭션이 느리다”는 가설은 너무 넓다. “긴 트랜잭션이 특정 전역 관리 구조에 반복적으로 접근한다”는 가설은 검증할 수 있다.
둘째, 관찰 대상을 바꾼다. 애플리케이션의 느린 메서드만 보던 사람이 데이터베이스의 내부 통계와 캐시, 대기 이벤트를 함께 보게 된다.
셋째, 우연한 해결과 구조적 해결을 구분한다. 트래픽이 줄어 잠시 정상화된 것은 해결이 아니다. 불필요한 중첩 경계를 제거하고, 트랜잭션의 시작과 종료를 다시 설계하는 것이 해결이다.
좋은 장애 보고서는 과거를 설명하는 문서가 아니다. 다음 장애가 발생하기 전에 팀의 사고 방식을 바꾸는 실행 가능한 모델이다.
트랜잭션을 코드가 아니라 자원 계약으로 보라
트랜잭션을 메서드의 장식처럼 다루면, 어노테이션은 계속 늘어난다. 어떤 메서드가 데이터를 수정하니 붙이고, 다른 메서드도 안전해야 하니 붙이고, 호출되는 하위 메서드에도 습관적으로 붙인다. 이때 트랜잭션은 설계의 결과가 아니라 불안을 달래는 표시가 된다.
더 나은 관점은 트랜잭션을 자원 계약으로 보는 것이다. 트랜잭션을 선언한다는 것은 다음에 동의하는 행위다.
“이 작업은 특정 데이터의 일관성을 위해 공유 자원을 일정 시간 동안 점유하겠습니다. 그 비용을 감당할 수 있고, 실패했을 때 이 범위 전체를 되돌리는 것이 맞습니다.”
이 계약을 검토하려면 각 트랜잭션에 네 가지 질문을 붙여야 한다.
1. 원자성이 정말 필요한 범위인가
하나의 비즈니스 요청 안에 있다고 해서 모든 단계가 반드시 하나의 데이터베이스 트랜잭션이어야 하는 것은 아니다. 결제 요청, 이메일 발송, 검색 색인 갱신처럼 서로 다른 시스템을 거치는 작업은 오히려 하나의 긴 트랜잭션으로 묶을수록 위험해질 수 있다.
2. 점유하는 자원은 무엇인가
행 락만 생각해서는 부족하다. 트랜잭션 관리 정보, 인덱스, 전역 캐시, 디스크 기반 메타데이터도 경쟁 자원이 될 수 있다. 눈에 보이는 테이블과 보이지 않는 관리 구조를 모두 목록에 넣어야 한다.
3. 가장 느린 단계가 경계 안에 있는가
네트워크 호출, 파일 접근, 사용자 입력 대기 같은 작업이 트랜잭션 안에 들어가면, 데이터베이스는 그 작업이 끝날 때까지 기다린다. 평균 지연 시간이 짧아도 꼬리 지연이 발생하는 순간 락의 생존 시간이 길어질 수 있다.
4. 이 경계는 코드 리뷰에서 설명 가능한가
“안전하니까 붙였습니다”는 설명이 아니다. 어떤 불변식을 보호하는지, 어느 범위까지 원자성이 필요한지, 예상 점유 시간은 얼마인지 설명할 수 있어야 한다. 설명할 수 없다면 설계가 아직 머릿속에만 있는 것이다.
이 관점은 트랜잭션을 무조건 줄이라는 뜻이 아니다. 필요한 트랜잭션은 더 명확하게 보호하고, 필요하지 않은 트랜잭션은 제거하라는 뜻이다. 안전성을 높이는 가장 좋은 방법이 선언을 추가하는 것이 아니라, 보호해야 할 불변식을 정확히 쓰는 것일 때가 많다.
바로 적용할 수 있는 운영 원칙
Key Takeaways
-
트랜잭션 경계를 문장으로 먼저 써라. 어떤 불변식을 보호하는지, 시작점과 종료점이 어디인지, 실패 시 무엇을 되돌리는지 코드보다 먼저 적는다.
-
호출 그래프와 자원 점유 시간을 함께 그려라. 메서드 호출 관계만 그리지 말고, 각 단계에서 어떤 행, 락, 캐시, 외부 시스템을 얼마나 오래 사용하는지 표시한다.
-
중첩 트랜잭션을 기본값으로 만들지 마라. 하위 메서드마다 일관성을 위해 트랜잭션을 추가하기 전에 상위 경계와 어떻게 결합되는지 확인한다. 중첩이 꼭 필요한 이유를 코드 리뷰에서 설명하게 한다.
-
평균 성능보다 최악의 생존 시간을 측정하라. 하나의 긴 트랜잭션이 전체 경합을 키울 수 있으므로, 최대 실행 시간, 락 대기 시간, 긴 트랜잭션 목록, 관련 내부 캐시 지표를 함께 모니터링한다.
-
장애 조사의 첫 단계로 가설을 글로 작성하라. “느려졌다”가 아니라 어떤 접근 패턴이 어떤 공유 구조를 자극하며 어떤 지표에 나타날지 적는다. 그 문장이 실험 설계가 되게 만든다.
생각을 쓴다는 것은 시스템을 실행해 보는 일이다
글쓰기는 흔히 개인의 생산성 기법으로 소개된다. 생각을 정리하고, 기억을 남기고, 다른 사람과 소통하기 위해 글을 쓴다고 말한다. 그러나 복잡한 기술 시스템에서 글쓰기는 그보다 훨씬 적극적인 행위다. 글은 실행되지 않는 코드처럼 작동한다. 문장의 흐름을 따라가며 입력과 상태와 결과를 점검하게 만들고, 생략된 전제를 오류로 드러낸다.
머릿속의 설계는 낙관적인 경향이 있다. 모든 호출은 빠르게 끝나고, 모든 실패는 예상한 위치에서 발생하며, 모든 트랜잭션은 자신이 감싼 데이터만 관리한다고 가정한다. 글로 쓰는 순간 이 가정들이 문장 사이에서 충돌한다. 외부 호출이 트랜잭션 안에 있다는 사실, 하위 메서드가 경계를 추가한다는 사실, 전역 구조가 병목이 될 수 있다는 사실이 더 이상 배경으로 숨지 못한다.
생각을 명확하게 만드는 글은 설명서가 아니다. 시스템이 무엇을 숨기고 있는지 찾아내는 테스트다.
결국 중요한 능력은 트랜잭션 어노테이션의 문법을 아는 것이 아니다. 편리한 추상화가 어디에서 전역 비용으로 바뀌는지 알아차리는 능력이다. 그리고 그 경로를 다른 사람이 검토할 수 있는 언어로 바꾸는 능력이다.
다음번에 코드에 안전을 더하기 위해 선언 하나를 추가하려 할 때, 잠시 멈춰 보자. 먼저 그 선언이 보호하는 불변식과 새로 만드는 공유 상태를 한 문단으로 써 보자. 문단을 끝까지 쓰지 못한다면, 아직 코드를 수정할 때가 아니다.
시스템 장애는 종종 실행 중인 코드에서 시작되지만, 더 자주 아직 쓰이지 않은 생각의 빈틈에서 시작된다.
Sources
Hatch New Ideas with Glasp AI 🐣
Glasp AI allows you to hatch new ideas based on your curated content. Let's curate and create with Glasp AI :)
Start Hatching 🐣