타임아웃·경계값 입력 시 발생하던 null 처리 버그 수정
목차
세션 관련 비즈니스 로직에서 null 체크 누락과 잘못된 기본값 처리가 복합적으로 얽혀서 발생하는 버그를 잡은 날이었다. 수정 자체는 단순했는데, 거기까지 오는 과정이 생각보다 길었다.
왜 이런 버그가 간헐적으로 보이는가
처음 이슈를 접했을 때 "간헐적 버그"로 분류하고 싶은 충동이 있었다. 정상 케이스에서 재현이 안 되니까. 근데 간헐적이라는 말은 대부분 "재현 조건을 아직 모른다"는 뜻이지, 진짜 랜덤하게 터지는 경우는 거의 없다.
이번 케이스도 그랬다. 조건을 좁혀보니 명확했다.
재현 조건:
1. 특정 값이 null 또는 빈 값인 경우
2. 경계값 입력 시 (0, 음수, 최대값 등)
3. 타임아웃 상황에서의 상태 불일치
세 조건 중 하나만 발생해도 터지는 게 아니라, 코드 실행 경로에 따라 다르게 튀었다. null 단독으로는 기본값으로 대체돼서 그냥 넘어가고, 타임아웃 단독으로는 예외 처리로 잡히는데, 경계값이 들어온 상태에서 타임아웃이 겹치면 기본값 처리 로직이 그 상태를 그대로 흘려보내는 구조였다.
null 체크 누락과 잘못된 기본값 처리가 따로 놀 때는 각자 다른 방어막에 걸렸는데, 동시에 트리거되면 두 방어막이 서로에게 책임을 미루는 상황이 됐다. 어느 한 쪽만 고쳐서는 해결이 안 된 이유다.
cookies.txt JSESSIONID 값 갱신 — 수정 내용
수정 범위는 파일 1개, 주요 영역은 비즈니스 로직이었다.
| 항목 | 내용 |
|---|---|
| 수정 파일 | 1개 |
| 주요 영역 | 비즈니스 로직 |
| 발생 조건 | 경계값/null 입력 + 타임아웃 복합 |
| 수정 방식 | 방어 코드 + null-safe 처리 |
구체적으로 세 가지를 했다.
첫째, null-safe 처리 로직을 추가했다. 값이 들어오는 진입점에서 null과 빈 값을 명시적으로 걸러내고, 이후 로직이 non-null을 보장받고 동작하도록 구조를 바꿨다. 기존엔 "어딘가에서 처리되겠지"라는 암묵적 가정이 코드에 박혀 있었다.
// 기존 — null이 그대로 하위 로직으로 내려감
String sessionId = getSessionId(cookies);
process(sessionId); // 내부에서 NPE 가능성
// 수정 후 — 진입점에서 명시적 보호
String sessionId = getSessionId(cookies);
if (sessionId == null || sessionId.isBlank()) {
log.warn("sessionId is null or blank — aborting process");
return;
}
process(sessionId);
둘째, 경계값 케이스를 명시적으로 처리했다. 0, 음수, 최대값 같은 엣지 케이스는 "정상 범위 외"로 판단하는 분기를 별도로 뒀다. 기존엔 이 값들이 정상 흐름으로 흘러들어가서 의도치 않은 상태를 만들고 있었다.
셋째, 관련 로그를 보강했다. 어떤 값이 들어왔고, 어떤 분기를 탔는지 남기도록 했다. 이번에 디버깅하면서 로그가 없어서 시간을 많이 잡아먹었기 때문에, 다음에 비슷한 이슈가 생기면 바로 추적할 수 있게 했다.
수정 후에는 동일 조건으로 재테스트를 돌렸고, 추가로 유사 패턴이 있는 다른 코드도 함께 훑었다. 같은 실수가 여러 군데 퍼져 있으면 한 곳 고쳐봐야 의미 없으니까. 다행히 큰 추가 수정 없이 몇 군데 방어 코드만 더 붙이는 선에서 마무리됐다.
디버깅 과정에서 실제로 효과 있었던 것
처음엔 원인을 잘못 짚었다. 세션 관련 코드니까 JSESSIONID 갱신 부분에 문제가 있을 거라고 가정하고 cookies.txt 쪽을 한참 뒤졌는데, 거기가 아니었다. 그 가정을 버리고 데이터 흐름을 위에서부터 다시 따라가면서 찾아냈다.
이번에 효과가 있었던 순서는 이렇다.
- 로그부터 — 스택 트레이스와 실제 입출력 값을 먼저 본다. 코드 보기 전에 데이터가 어떻게 움직였는지 확인.
- 재현 조건 좁히기 — 어떤 입력에서 발생하는지 확정짓는다. "간헐적"이라는 딱지를 일찍 붙이면 잘못된 방향으로 간다.
- 이진 탐색 — 문제가 있는 코드 범위를 절반씩 줄여나간다. 의심 구간을 넓게 잡은 채로 코드를 읽는 건 비효율적이다.
- 가정을 검증 — "아마 이럴 것이다"가 아니라 실제로 찍어보거나 조건을 직접 만들어서 확인한다.
이 순서 자체가 새로운 건 아닌데, 압박을 받거나 "이쪽일 것 같다"는 강한 직감이 생기면 2번과 4번을 건너뛰고 싶어진다. 이번에도 초반에 그랬다가 시간을 날렸다.
null 처리 버그는 코드베이스에서 자주 보이는 유형이고, 특히 타임아웃처럼 비결정적으로 보이는 외부 요인과 겹치면 원인 파악이 늦어지기 쉽다. 방어 코드를 진입점에 가능한 한 가깝게 두는 게 이런 복합 케이스를 줄이는 가장 현실적인 방법이다. 완벽한 타입 시스템이나 컴파일 타임 보장이 없는 환경에서는 더 그렇다.
댓글 0
첫 댓글 달아줘.