BindProject
[Backend] exception (CustomErrorCode 구현)
dding-shark
2025. 6. 17. 01:41
728x90
예외 모듈 설계와 공통 처리 전략
Bind 프로젝트는 서비스 구조를 퍼블릭/서비스 모듈로 분리하여 확장성과 재사용성을 확보하는 데 초점을 맞추고 있습니다.
이번 글에서는 퍼블릭 레이어 중 하나인 exception 모듈을 설계하게 된 이유와,
이를 어떻게 구현하고 테스트했는지를 설명합니다.
1. 도입 및 배경
초기에는 서비스마다 각기 다른 방식으로 예외 처리를 하다 보니 다음과 같은 문제가 발생했습니다:
- 클라이언트로 전달되는 에러 응답 형식이 제각각
- 예외 메시지에 일관성이 없음
- 도메인별 에러 코드를 추적하거나 관리하기 어려움
이를 해결하고자 예외 처리 방식을 통합한 공통 예외 처리 모듈을 설계하게 되었습니다.
2. 설계 목표 및 구조
BusinessException을 중심으로 한 공통 예외 처리 방식 정립ErrorCode를 enum 기반으로 도메인화하여 관리 가능하게 함- HTTP 상태 코드, 메시지, 에러 코드의 일관성 유지
- 서비스/도메인 모듈에서 재사용 가능한 경량 모듈
3. 설계 시 고려한 사항
- 퍼블릭 모듈로써 Spring에 과도하게 종속되지 않아야 함
- Kafka, Redis, Scheduler 등 비동기 시스템에서도 재사용 가능해야 함
- 도메인별로 에러 코드를 분리할 수 있도록 확장성을 확보해야 함
- API 응답에만 국한되지 않고 로깅, DLQ 처리 등 내부 사용도 고려
4. 모듈 구조
exception/
├── CustomErrorCode.java # 에러 코드 정의 인터페이스
├── GlobalErrorCode.java # 공통 에러 enum
├── BusinessException.java # 커스텀 런타임 예외
├── ExceptionResponse.java # 클라이언트에 반환할 DTO
└── GlobalExceptionHandler.java # 전역 예외 핸들러5. 핵심 구현 코드
CustomErrorCode.java
public interface CustomErrorCode {
String getCode();
String getMessage();
HttpStatus getStatus();
}
GlobalErrorCode.java
public enum GlobalErrorCode implements CustomErrorCode {
INVALID_INPUT("COMMON-001", "잘못된 요청입니다.", HttpStatus.BAD_REQUEST),
INTERNAL_ERROR("COMMON-999", "서버 내부 오류입니다.", HttpStatus.INTERNAL_SERVER_ERROR);
}
BusinessException.java
public class BusinessException extends RuntimeException {
private final CustomErrorCode errorCode;
public BusinessException(BindErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public BindErrorCode getErrorCode() {
return errorCode;
}
}
ExceptionResponse.java
public record ExceptionResponse(String code, String message, int status) {
public static ExceptionResponse from(BindErrorCode code) {
return new ExceptionResponse(code.getCode(), code.getMessage(), code.getStatus().value());
}
}
GlobalExceptionHandler.java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ExceptionResponse> handleBusiness(BusinessException ex) {
return ResponseEntity
.status(ex.getErrorCode().getStatus())
.body(ExceptionResponse.from(ex.getErrorCode()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ExceptionResponse> handleUnexpected(Exception ex) {
return ResponseEntity
.status(GlobalErrorCode.INTERNAL_ERROR.getStatus())
.body(ExceptionResponse.from(GlobalErrorCode.INTERNAL_ERROR));
}
}
6. 테스트 전략
단위 테스트
BusinessException이ErrorCode를 제대로 포함하는지 확인ExceptionResponse.from(...)가 정확히 변환되는지 검증
통합 테스트
@SpringBootTest(classes = GlobalExceptionHandlerTest.TestConfig.class)
@AutoConfigureMockMvc
class GlobalExceptionHandlerTest {
@Autowired private MockMvc mockMvc;
@Test
void shouldReturnCustomErrorResponse() throws Exception {
mockMvc.perform(get("/sample/error"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("COMMON-001"));
}
@TestConfiguration
@RestController
static class TestConfig {
@GetMapping("/sample/error")
public void errorEndpoint() {
throw new BusinessException(GlobalErrorCode.INVALID_INPUT);
}
}
}
퍼블릭 모듈에는
@SpringBootApplication이 없기 때문에@SpringBootTest(classes = ... )로 명시적인 컨텍스트를 제공해야 합니다.
7. 회고 및 확장 가능성
- 에러 코드 분리로 문제 추적성과 관리 효율성이 크게 개선됨
- API 응답 포맷이 표준화되어 프론트와의 계약도 훨씬 명확해졌음
- 추후 Kafka 이벤트 발행 실패에도 동일한
BusinessException기반으로 처리 가능 - 서비스별로
UserErrorCode,OrderErrorCode등을 정의하면 도메인 확장도 무리 없이 가능함
728x90