실무에서 시스템을 운영하다 보면, 데이터의 변경 이력을 엄격하게 추적하기 위해 원본 테이블과 1:1로 매칭되는 이력 테이블(_hst)을 두는 경우가 매우 흔하다.
문제를 겪었던 프로젝트는 원본 테이블에 CUD(Create, Update, Delete)가 발생하면 데이터베이스 트리거(Trigger)가 백그라운드에서 돌아가고, 시퀀스(Sequence)의 NEXTVAL을 호출해 이력 테이블의 PK(이력 일련번호)를 채우며 INSERT 되는 아키텍처를 띈다.
비즈니스 로직에 개입하지 않고 DB 단에서 조용하고 확실하게 이력을 남길 수 있어 자주 쓰이는 패턴이다. 그런데, 기존 시스템(AS-IS)에서 새로운 시스템(TO-BE)으로 대규모 데이터를 이관하는 과정에서 이 뻔한 구조가 거대한 폭탄으로 돌아오는 현상을 겪었다.
문제의 발단: 이관 서버의 시퀀스 초기화
사건의 발단은 데이터 이관 작업 후 발생했다. 기존 운영 DB에 쌓여있던 수백만 건의 원본 데이터와 이력(_hst) 데이터를 무사히 TO-BE DB로 마이그레이션하는 데 성공했다.
문제는 '시퀀스 객체의 상태'였다. 테이블의 데이터는 있는 그대로 잘 퍼왔지만, TO-BE 환경에 시퀀스를 새로 생성(또는 초기화)하면서 시작값(START WITH)을 이관된 데이터의 MAX(PK) 값으로 맞춰주지 않고 기본값인 1로 시작해 버린 것이다.
기괴한 현상: 시간이 거꾸로 흐르는 이력 조회
시스템을 오픈하고 사용자들이 데이터를 수정하기 시작하자, 이력 테이블의 조회가 기괴하게 꼬이기 시작했다. 분명 방금 수정한 최신 데이터인데, 이력 조회 화면에서는 보이지 않거나 맨 마지막 페이지 구석에 처박혀 있는 현상이 발생했다.
원인을 파악하기 위해 로그를 까보니, 문제는 이력 테이블을 조회할 때 정렬하는 기준에 있었다.
보통 특정 데이터의 변경 이력을 화면에 뿌려줄 때, 최신순으로 보여주기 위해 시간 데이터 대신 인덱스가 타기 쉬운 이력 테이블의 PK(시퀀스 값)를 기준으로 내림차순 정렬(ORDER BY HST_SEQ DESC)을 한다.
하지만 시퀀스가 초기화된 상태에서 트리거가 동작하자 다음과 같은 타임라인 역전 현상이 벌어졌다.
문제 예시
- AS-IS 데이터 이관 직후 상태:
- 기존 회원 '홍길동'의 이력 데이터 중 가장 마지막 시퀀스 번호: 100,500
- 이관된 이력 테이블의 MAX(HST_SEQ): 100,500
- TO-BE DB의 시퀀스 현재 값: 1 (오류의 시작)
- TO-BE 시스템 오픈 후 CUD 발생:
- 사용자가 '홍길동'의 전화번호를 수정 (Update)
- 트리거 동작 → _hst 테이블에 INSERT 발생
- 이때 TO-BE의 시퀀스가 1부터 시작하므로, 방금 수정한 최신 이력의 PK로 1이 채번됨.
이력 조회 화면 쿼리 실행
SELECT * FROM MEMBER_HST
WHERE USER_ID = '홍길동'
ORDER BY HST_SEQ DESC;
조회 결과: 정렬 기준이 HST_SEQ DESC이므로, 과거 시스템에서 이관된 번호인 100,500번 데이터가 화면 맨 위에(가장 최신인 것처럼) 노출된다. 정작 방금 수정한 진짜 최신 데이터는 번호가 1이기 때문에 과거 이력의 맨 밑바닥으로 가라앉아 버렸다.
해결
만약 이력 테이블의 PK에 Unique Constraint(고유 제약조건)라도 걸려있었다면 PK 무결성 제약조건 위배(Duplicate Key) 에러가 나면서 CUD 자체가 롤백되어 바로 눈치챘을 것이다. 하지만 이력 테이블 특성상 제약조건을 느슨하게 풀어두는 경우가 많아 에러 없이 조용히 데이터가 꼬여가고 있었다.
결국 사태를 파악한 직후, 다음 두 가지 조치를 긴급하게 수행했다.
- 시퀀스 동기화: TO-BE DB의 이력 시퀀스를 삭제하고, 이관된 이력 데이터의 MAX(HST_SEQ) + 1 값으로 START WITH를 지정해 시퀀스를 재성성
- 꼬인 데이터 보정: 이미 1번부터 채번되어 잘못 들어가 버린 최신 이력 데이터들의 PK를 찾아내, 정상적인 시퀀스 대역으로 수동 UPDATE 처리를 진행
데이터 이관 시 테이블의 데이터를 옮기는 것에만 혈안이 되어, 그 데이터를 지탱하는 '시퀀스의 현재 값'을 동기화하는 것을 누락하면 시스템 전체의 정합성이 얼마나 우스워질 수 있는지 뼈저리게 느꼈다.
느낀점
특히 금융이나 공공 도메인처럼 데이터의 이력(Audit) 증명이 절대적으로 중요한 환경에서, 타임라인이 꼬이는 것은 단순한 버그 이상의 치명적인 결함이 될 수 있다고 생각했고,,,,
사실, 프로시저나 트리거는 사용하지 않는 것이 좋을 것 같다고 생각이 들었다. 디버깅도 쉽지 않고, 관리할 항목이 추가되고, 만약 api가 호출돼서 CUD 로직을 탈 때 트리거 오류가 나면 실행 자체가 되지 않는다. 필요하다면 사용해야겠지만 굳이굳이? 사용할 필요는 없는 것 같음.
'실무' 카테고리의 다른 글
| [DB] 대용량 페이징 조회 성능 개선 | 민민의 하드디스크 - 티스토리 (0) | 2026.01.04 |
|---|---|
| [Spring] @Transactional과 Exception 처리 | 민민의 하드디스크 - 티스토리 (2) | 2025.07.05 |
| [FE] jQuery 반복문($.each) 사용 시 주의점 | 민민의 하드디스크 - 티스토리 (0) | 2025.05.21 |
| [Spring] 스프링배치 흐름 및 @Schedueld 어노테이션 사용 시 유의사항 | 민민의 하드디스크 - 티스토리 (0) | 2025.05.21 |
| [DB] CONNECT BY와 WITH RECURSIVE 계층형 데이터 조회 | 민민의 하드디스크 - 티스토리 (1) | 2025.03.23 |