오래된 B2B 엔터프라이즈 시스템에서는 비즈니스 요건이 추가됨에 따라 개발자마다 고유의 커스텀 예외(Custom Exception)를 양산하는 경향이 있습니다.
이로 인해 예외 처리 체계가 파편화되고, 불명확한 에러 로그와 정제되지 않은 예외 전파로 인해 실제 고객 장애 대응(CS) 리소스가 과도하게 낭비되는 문제를 해결한 프로세스를 정리했습니다.
기존 시스템 내에 존재하는 커스텀 예외들을 리스트업하고 구조적 한계를 분석하는 사전 조사를 진행했습니다.
Checked vs Unchecked Exception 혼재
Exception, IOException 등 Checked Exception을 무분별하게 상속받아 시그니처 레벨(throws)에 강제되는 코드와, 상단 프레젠테이션 레이어에서 예외를 먹어버리는(Swallow) 코드가 엉켜 예외 전파 경로를 추적하기 어려웠습니다.불분명한 상속 개념과 GlobalException 남용
GlobalException을 거쳐 RuntimeException을 상속(Unchecked Exception)받도록 느슨한 상속 구조는 갖추어져 있었습니다.RuntimeException을 사용함으로써 불필요한 컴파일 타임 에러나 명시적 예외 선언에서는 벗어나 있었고, 예외 발생 시 호출자에게 에러 정보를 전달하기 위한 후속 처리 메서드들 자체는 잘 정의된 상태였습니다.예외 관리 전략을 직관적이고 표준화된 AOP 예외 제어 모델로 일원화하기 위해 4가지 아키텍처적 개선 단계를 적용했습니다.
기존 소스코드 곳곳에 하드코딩되어 있던 System.out.println()이나 e.printStackTrace()를 걷어내고 프레임워크 표준 로깅 시스템과 연동되도록 개선했습니다.
// 개선 전: 무분별한 System.out 및 stack trace 직접 출력
try {
businessService.execute(data);
} catch (GlobalException e) {
System.out.println("에러 발생: " + e.getMessage());
e.printStackTrace();
}
// 개선 후: AOP 및 Facade 기반 log4j 구조화 로깅 적용
public class LoggingFacade {
private static final Logger logger = Logger.getLogger(LoggingFacade.class);
public static void logError(String contextMessage, Throwable throwable) {
// 불필요한 전체 Stack Trace 대신, 핵심 원인(Cause) 메세지만 정제하여 출력
String simplifiedStack = getSimplifiedStackTrace(throwable);
logger.error(String.format("[%s] Error Message: %s | Stack Message: %s",
contextMessage, throwable.getMessage(), simplifiedStack));
}
private static String getSimplifiedStackTrace(Throwable t) {
if (t == null) return "No Stack Trace";
StackTraceElement[] elements = t.getStackTrace();
if (elements.length > 0) {
// 가장 핵심적인 최상단 호출 스택 정보만 직관적으로 요약
StackTraceElement topFrame = elements[0];
return String.format("%s.%s(Line:%d)",
topFrame.getClassName(), topFrame.getMethodName(), topFrame.getLineNumber());
}
return t.getMessage();
}
}모든 API 응답을 통일된 공통 포맷으로 응답(JSON)하며, 비즈니스 예외와 HTTP 상태 코드(또는 returnCode)의 1:1 매핑을 자동으로 수행하는 전역 처리기를 구축했습니다.
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = Logger.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(GlobalException.class)
public ResponseEntity<ErrorResponse> handleGlobalException(GlobalException ex) {
// 예외와 매핑된 고유 비즈니스 에러 코드 및 HTTP Status 확인
ErrorCode errorCode = ex.getErrorCode();
// 2.2 개선에 따라 stack trace를 간소화하여 로그 출력
LoggingFacade.logError("API Exception Handler Blocked", ex);
ErrorResponse response = new ErrorResponse(
errorCode.getReturnCode(),
ex.getMessage() != null ? ex.getMessage() : errorCode.getDefaultMessage()
);
return new ResponseEntity<>(response, HttpStatus.valueOf(errorCode.getHttpStatus()));
}
}B2B 서비스의 특성상 화면 기반 요청과 비동기 Ajax 요청이 공존하므로, 이에 맞춘 지능형 예외 분기 전략을 수립했습니다.
Tomcat web.xml 설정 및 ExceptionPageResolver 연동
<!-- Tomcat web.xml 에러 페이지 매핑 설정 -->
<error-page>
<error-code>404</error-code>
<location>/WEB-INF/views/error/404.jsp</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/WEB-INF/views/error/500.jsp</location>
</error-page>
<error-page>
<exception-type>com.enterprise.exception.GlobalException</exception-type>
<location>/WEB-INF/views/error/globalError.jsp</location>
</error-page>Ajax 비동기 통신을 위한 에러 필터(Filter) 구현
X-Requested-With: XMLHttpRequest)를 실시간 분석하여, 에러 페이지 리졸빙 대상에서 제외하고 JSON 에러 메시지만 순수하게 호출자에게 전달하도록 강제합니다.public class AjaxErrorFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
try {
chain.doFilter(request, response);
} catch (Exception ex) {
if ("XMLHttpRequest".equals(httpRequest.getHeader("X-Requested-With"))) {
// Ajax 요청인 경우 리다이렉트/포워드를 하지 않고, JSON 형식으로 에러 전송
httpResponse.setContentType("application/json;charset=UTF-8");
httpResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
String jsonResponse = String.format("{\"success\":false,\"message\":\"%s\"}", ex.getMessage());
httpResponse.getWriter().write(jsonResponse);
} else {
// 일반 브라우저 요청인 경우 컨테이너 예외 처리기로 다시 throw
throw ex;
}
}
}
}직관적인 오류 페이지 제공 및 사용자 신뢰 구축
웹 로그 기반 모니터링 가속화
아쉬운 점과 향후 가이드라인