spring_2기[본캠프]/과제

[과제] Spring 플러스 프로젝트 Day 9

minwoo95 2026. 3. 20. 22:07

https://github.com/TheOne-team-1/TheOne-Bottle-Shop

 

GitHub - TheOne-team-1/TheOne-Bottle-Shop: 주류 커머스 플랫폼 백엔드 구현하기

주류 커머스 플랫폼 백엔드 구현하기. Contribute to TheOne-team-1/TheOne-Bottle-Shop development by creating an account on GitHub.

github.com

 

1. 개요
Spring Boot 기반의 주류 판매 플랫폼인 'TheOne-Bottle-Shop' 프로젝트에서 배송지 관리 API의 단위 테스트를 수행하던 중, 예외 처리 핸들러와 테스트 기대값 사이의 불일치로 인한 테스트 실패를 해결하고, 전역 예외 처리(@RestControllerAdvice) 체계를 고도화함.

2. 문제 상황
MemberAddressControllerTest 수행 중 다음과 같은 두 가지 주요 결함이 발견됨.

상황 A: 권한 부족 시 잘못된 상태 코드 반환
기대 결과: 타인의 배송지 삭제 시도 시 403 Forbidden 반환.

실제 결과: 400 Bad Request 반환으로 인한 AssertionError 발생.

상황 B: 존재하지 않는 리소스 조회 시 서버 에러 발생
기대 결과: 존재하지 않는 ID로 조회 시 404 Not Found 반환.

실제 결과: 핸들러 부재로 인해 최상위 예외인 Exception.class가 낚아채어 500 Internal Server Error 발생.

3. 원인 분석
3.1 HTTP Status Code 설계 미흡
기존 GlobalExceptionHandler 내의 AccessDeniedException 핸들러가 ResponseEntity.status(HttpStatus.BAD_REQUEST)로 고정되어 있었음. 이는 클라이언트의 요청 형식 오류(400)와 권한 제한(403)을 구분하지 못하는 설계적 결함임.

3.2 JPA 예외 계층 구조 이해 부족
서비스 레이어에서 던지는 jakarta.persistence.EntityNotFoundException이 RuntimeException을 상속받고 있으나, 전역 핸들러에서 이를 구체적으로 처리하지 않아 스프링의 기본 에러 처리 프로세스에 의해 500 에러로 격상됨.

4. 해결 과정
4.1 구체적 예외 핸들러 구현
GlobalExceptionHandler에 구체적인 예외 타입을 지정하고, 의미론적으로 정확한 HTTP 상태 코드를 매핑함.

// 1. 권한 거부 예외 처리 (403)
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<BaseResponse<Void>> handleAccessDeniedException(AccessDeniedException e) {
    log.error("AccessDeniedException 발생: {}", e.getMessage());
    return ResponseEntity
            .status(HttpStatus.FORBIDDEN) // 정확한 403 상태 코드 명시
            .body(BaseResponse.fail(HttpStatus.FORBIDDEN.name(), "권한이 없습니다."));
}

// 2. JPA 엔티티 미존재 예외 처리 (404)
@ExceptionHandler(jakarta.persistence.EntityNotFoundException.class)
public ResponseEntity<BaseResponse<Void>> handleEntityNotFoundException(jakarta.persistence.EntityNotFoundException e) {
    log.error("EntityNotFoundException 발생: {}", e.getMessage());
    return ResponseEntity
            .status(HttpStatus.NOT_FOUND) // 404 상태 코드 명시
            .body(BaseResponse.fail(HttpStatus.NOT_FOUND.name(), e.getMessage()));
}
4.2 테스트 코드 검증
수정 후 MockMvc를 이용한 테스트 재실행 결과, 모든 검증(Assertion)이 통과됨을 확인함.

status().isForbidden() -> PASSED

status().isNotFound() -> PASSED

5. 인사이트 및 향후 과제
5.1 테스트 커버리지 분석 (Coverage: 44%)
원인: @WebMvcTest 기반의 슬라이스 테스트는 서비스 레이어를 Mocking 하므로, 실제 비즈니스 로직(Service, Repository)의 코드 라인을 통과하지 않음.

대책: 로직의 복잡도가 높은 Service 레이어에 대해 JUnit5와 Mockito를 활용한 **단위 테스트(Unit Test)**를 보강하여 커버리지를 80% 이상으로 끌어올릴 계획임.

5.2 보안적 관점에서의 예외 처리
에러 메시지에 내부 스택 트레이스나 구체적인 쿼리 정보를 노출하지 않도록 BaseResponse 규격을 엄격히 준수함. 이는 정보 노출(Information Exposure) 취약점을 예방하는 중요한 설계 원칙임을 재확인함.

6. 결론
예외 처리는 단순히 에러를 막는 것이 아니라, API의 사용자(클라이언트)와 소통하는 방식입니다. 오늘 트러블슈팅을 통해 HTTP 상태 코드의 의미론적 중요성을 깊이 이해하게 되었으며, 견고한 에러 핸들링이 시스템의 안정성과 보안에 직결됨을 배웠습니다.