개발 slecs

인증 오류가 500으로 터지던 문제를 401로 바로잡다

목차

인증 컨텍스트에서 requireUserId()가 던지는 예외가 500으로 터지고 있었다. 클라이언트 입장에서는 "서버 오류"로 보였지만, 실제 원인은 인증 정보가 없는 요청이었다.


배경: 500이 나오면 안 되는 상황이었다

requireUserId()는 이름 그대로 "인증된 유저 ID가 반드시 있어야 한다"는 전제를 강제하는 메서드다. 토큰이 없거나, 만료됐거나, 파싱이 실패한 경우 — 이 중 어떤 케이스든 결과는 같다. 해당 요청은 인증이 안 된 것이고, 클라이언트에게는 401 Unauthorized를 돌려줘야 한다.

근데 실제로는 500이 떨어지고 있었다. 이유는 단순했다. AuthContext.java 내부에서 userId가 null이거나 컨텍스트 자체가 비어 있을 때, 그냥 NullPointerException이나 일반 RuntimeException이 그대로 올라갔던 것이다. Spring의 기본 예외 처리기는 이걸 잡아서 500으로 내려보낸다. HTTP 상태 코드 매핑이 누락된 전형적인 케이스였음.


문제의 구조

이런 류의 버그가 생기는 경로는 보통 이렇다.

상태 기대 응답 실제 응답 (수정 전)
토큰 없음 401 500
토큰 만료 401 (필터에서 처리)
userId null (컨텍스트 누락) 401 500
userId 정상 200 200

필터 레벨에서 처리되는 케이스는 이미 401이 잘 내려가고 있었다. 문제는 필터를 통과했더라도 AuthContext에서 userId를 꺼낼 때 값이 없는 경우였다. 필터와 컨텍스트 사이 어딘가 갭이 생긴 것인데, 이 갭을 requireUserId()가 명시적으로 방어하지 않고 있었던 거다.

// 수정 전 (암묵적으로 예외가 전파되는 형태)
public Long requireUserId() {
    return Optional.ofNullable(currentUserId)
        .orElseThrow(() -> new RuntimeException("userId not found"));
}

// 수정 후 (401에 매핑되는 예외 사용)
public Long requireUserId() {
    return Optional.ofNullable(currentUserId)
        .orElseThrow(() -> new UnauthorizedException("인증 정보가 없습니다."));
}

UnauthorizedException@ResponseStatus(HttpStatus.UNAUTHORIZED)를 달고 있거나, 글로벌 예외 핸들러에서 401로 매핑되는 타입이면 된다. 핵심은 "의도를 담은 예외 타입"을 쓰는 것.


왜 이런 게 늦게 발견되는가

솔직히 말하면 로컬이나 개발 환경에서 이 케이스를 재현하려면 의도적으로 토큰을 빼고 요청을 날려봐야 한다. 정상 플로우만 테스트하면 절대 안 걸린다. 그리고 500이 떨어진다 해도 기능 자체가 "안 된다"는 결과는 동일하니까, QA에서 놓치기 쉽다.

팀 차원에서 이런 걸 잡으려면:

  • 인증 실패 케이스를 포함한 단위 테스트가 AuthContext 레벨에 있어야 함
  • API 레벨 통합 테스트에서 "인증 없이 보호된 엔드포인트를 때렸을 때 401이 오는지" 검증이 있어야 함
  • 모니터링에서 5xx 스파이크를 볼 때 "이게 진짜 서버 오류인지, 인증 오류가 잘못 내려온 건지" 구분할 수 있어야 함

코드리뷰 때도 requireXxx() 패턴의 메서드가 어떤 예외를 던지는지는 짚고 넘어가는 편이 좋다. 이름이 "require"이면 실패 시 반드시 예외가 발생한다는 계약인데, 그 예외가 적절한 HTTP 상태로 이어지는지까지 봐야 한다.


회고

AuthContext.java 한 파일, 메서드 몇 줄 수정이지만 클라이언트 경험 관점에서는 꽤 의미 있는 변경이다. 500은 "우리가 실수했다"는 신호고, 401은 "당신이 인증을 안 했다"는 신호다. 이 둘을 혼동하면 클라이언트 개발자는 괜히 서버 오류로 오인하고 리포트를 올린다. 디버깅 시간이 낭비된다.

작은 수정일수록 "왜 이게 이렇게 됐지?"를 한 번 더 파고드는 게 습관이 됐다. 이 케이스도 단순 매핑 누락이지만, 테스트 커버리지 갭을 드러내는 지표이기도 했다.

다음


🛒 이 글과 어울리는 추천 상품

*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.

댓글 0

첫 댓글 달아줘.