외주 SI 프로젝트에서 서버를 올린 날, 모든 등록 기능이 저장되지 않았습니다. 화면은 "저장되었습니다"를 띄웠고, 오류는 한 건도 없었습니다.
증상
- 등록 버튼을 누르면 성공 메시지가 뜹니다.
- 목록을 새로고침하면 그 건이 없습니다.
- 서버 로그에는
1 rows affected가 찍혀 있습니다. - 예외도, 오류 코드도 없습니다.
개발 환경에서는 멀쩡했습니다. 서버에 올린 것만 이랬습니다.
먼저 확인한 것
저장이 안 될 때 가장 흔한 원인부터 지웠습니다.
키 충돌이 아닙니다
같은 키를 두 번 넣으면 무결성 위반이 납니다.
ORA-00001: unique constraint violated
이게 한 번도 안 났습니다. 그런데 새로 만드는 키 값이 계속 1이었습니다.
여기가 결정적이었습니다. 채번은 "지금 있는 가장 큰 값 + 1" 방식이었는데, 매번 1이 나온다는 건 매번 테이블이 비어 있다는 뜻입니다. 앞 건이 남아 있으면 2가 나와야 합니다.
넣은 건 맞는데, 남지 않았습니다.
권한도, 락도 아닙니다
권한 문제면 오류가 납니다. 락이면 응답이 늦습니다. 둘 다 아니었습니다. 빠르게 성공하고 조용히 사라졌습니다.
어디서 사라지나
드라이버가 돌려주는 1 은 "한 행을 넣었다"는 뜻입니다. "한 행이 남았다"가 아닙니다. 둘 사이에 커밋이 있습니다.
커밋이 없으면 커넥션이 풀로 돌아갈 때 되돌아갑니다. 이때도 아무 소리가 안 납니다.
원인
두 가지가 겹쳤습니다.
1. 트랜잭션이 걸려 있지 않았습니다
서비스 계층에 트랜잭션 선언이 빠진 경로가 있었습니다. 그러면 아무도 커밋을 부르지 않습니다.
2. 커넥션 풀이 자동 커밋을 꺼두고 있었습니다
datasource:
hikari:
auto-commit: false
이 설정 자체는 맞는 설정입니다. 트랜잭션을 제대로 쓰는 코드에서는 이게 정석입니다.
문제는 1번과 만났을 때입니다. 자동 커밋이 켜져 있었다면 트랜잭션 선언이 빠져도 어찌어찌 저장은 됐을 겁니다. 꺼져 있으니 조용히 전부 사라졌습니다.
즉 이 설정은 원인이 아니라 증폭기였습니다. 개발 환경에서 안 걸린 것도 설정이 달라서였습니다.
고친 뒤 넣은 안전장치
등록 직후 다시 읽어 본다
Long id = repository.insert(row);
// 트랜잭션이 끝난 뒤 새 세션으로 다시 읽는다.
assertNotNull(repository.findById(id));
넣었다는 응답을 믿지 않고, 남았는지를 확인합니다. 테스트가 이걸 안 하고 있었기 때문에 통과했습니다.
채번 값을 로그로 본다
새 키가 계속 같은 값이면 앞 건이 안 남고 있다는 신호입니다. 저장 실패보다 훨씬 먼저 보입니다. 이걸 일찍 봤으면 반나절 아꼈습니다.
환경 사이 설정 차이를 목록으로 만든다
개발에서 되고 서버에서 안 되면 코드가 아니라 설정입니다. 그런데 설정 파일은 여러 곳에 흩어져 있어서 눈으로 비교가 안 됩니다. 실제로 다른 값만 뽑아 표로 만들었습니다.
가져갈 것
1 rows affected는 저장됐다는 뜻이 아닙니다. 넣었다는 뜻입니다.- 키가 계속 같은 값이면 앞 건이 안 남고 있는 겁니다. 아주 좋은 조기 신호입니다.
- 맞는 설정이 다른 결함과 만나면 피해를 키웁니다. 설정만 되돌리면 진짜 원인은 남습니다.
- 등록 테스트는 넣고 다시 읽는 데까지가 한 벌입니다.
- 환경이 다르면 설정 차이부터 보세요.
가장 무서웠던 건 아무도 오류를 못 본다는 점이었습니다. 사용자는 저장했다고 믿고 창을 닫습니다. 발견이 늦으면 그만큼의 입력이 통째로 사라집니다.
만든 것들
'Coding, Testing, Challenge' 카테고리의 다른 글
| 화면이 멈춘 게 아니라, 세 가지가 각각 달랐습니다 (0) | 2026.09.17 |
|---|---|
| 4초짜리 쿼리가 1,200초가 됐습니다 (0) | 2026.09.15 |
| 타임아웃을 30초로 걸었는데 410초를 모두 돌았다. (0) | 2026.09.11 |
| 토큰을 숨기는 명령어로 토큰을 유출했습니다 (0) | 2026.09.08 |
| 테스트는 전부 초록불인데, 그 컬럼은 없었습니다 (0) | 2026.09.07 |