본문 바로가기

Coding, Testing, Challenge

저장은 성공했다고 나오는데, 데이터가 없습니다

저장은 성공했는데 데이터가 없습니다

외주 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저장됐다는 뜻이 아닙니다. 넣었다는 뜻입니다.
  • 키가 계속 같은 값이면 앞 건이 안 남고 있는 겁니다. 아주 좋은 조기 신호입니다.
  • 맞는 설정이 다른 결함과 만나면 피해를 키웁니다. 설정만 되돌리면 진짜 원인은 남습니다.
  • 등록 테스트는 넣고 다시 읽는 데까지가 한 벌입니다.
  • 환경이 다르면 설정 차이부터 보세요.

가장 무서웠던 건 아무도 오류를 못 본다는 점이었습니다. 사용자는 저장했다고 믿고 창을 닫습니다. 발견이 늦으면 그만큼의 입력이 통째로 사라집니다.


만든 것들