← 목록으로
📅 2026.06.01
레거시 커스텀 Exception 혼재로 인한 AOP 예외 처리 및 공통 응답 구조 개선
TroubleshootingJavaSpringAOPExceptionLoggingB2B

오래된 B2B 엔터프라이즈 시스템에서는 비즈니스 요건이 추가됨에 따라 개발자마다 고유의 커스텀 예외(Custom Exception)를 양산하는 경향이 있습니다.
이로 인해 예외 처리 체계가 파편화되고, 불명확한 에러 로그와 정제되지 않은 예외 전파로 인해 실제 고객 장애 대응(CS) 리소스가 과도하게 낭비되는 문제를 해결한 프로세스를 정리했습니다.


1. AS-IS: 파편화된 커스텀 예외 체계 분석

기존 시스템 내에 존재하는 커스텀 예외들을 리스트업하고 구조적 한계를 분석하는 사전 조사를 진행했습니다.

Checked vs Unchecked Exception 혼재

  • 소스 전반에 Exception, IOException 등 Checked Exception을 무분별하게 상속받아 시그니처 레벨(throws)에 강제되는 코드와, 상단 프레젠테이션 레이어에서 예외를 먹어버리는(Swallow) 코드가 엉켜 예외 전파 경로를 추적하기 어려웠습니다.

불분명한 상속 개념과 GlobalException 남용

  • 시스템 내의 약 100여 개 커스텀 예외들이 계층 관계 없이 중구난방으로 정의되어 있었으나, 다행히 최종적으로는 최상위 예외인 GlobalException을 거쳐 RuntimeException을 상속(Unchecked Exception)받도록 느슨한 상속 구조는 갖추어져 있었습니다.
  • RuntimeException을 사용함으로써 불필요한 컴파일 타임 에러나 명시적 예외 선언에서는 벗어나 있었고, 예외 발생 시 호출자에게 에러 정보를 전달하기 위한 후속 처리 메서드들 자체는 잘 정의된 상태였습니다.

2. TO-BE: 개선 방향 및 아키텍처 설계

예외 관리 전략을 직관적이고 표준화된 AOP 예외 제어 모델로 일원화하기 위해 4가지 아키텍처적 개선 단계를 적용했습니다.

2.1 Sysout 로그의 log4j 파사드(Facade) 전환 및 스택 트레이스(Stack Trace) 간소화

기존 소스코드 곳곳에 하드코딩되어 있던 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();
    }
}

2.2 Global Exception Handler 기반 API 공통 Response 설계

모든 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()));
    }
}

2.3 Exception Page Resolver 및 Ajax Filter 도입

B2B 서비스의 특성상 화면 기반 요청과 비동기 Ajax 요청이 공존하므로, 이에 맞춘 지능형 예외 분기 전략을 수립했습니다.

Tomcat web.xml 설정 및 ExceptionPageResolver 연동

  • 예외 상태 코드(예: 404, 500, 403 등)에 따라 Tomcat 컨테이너 수준에서 지정된 에러 안내 페이지(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) 구현

  • 일반적인 페이지 이동 요청 시에는 에러 페이지로 렌더링되게 하되, 비동기 Ajax 요청의 경우 렌더링된 에러 HTML이 응답되어 프론트엔드가 파손되는 것을 방지하고자 Custom Filter를 도입했습니다.
  • 이 필터는 HTTP 요청 헤더(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;
            }
        }
    }
}

3. 개선 결과 및 기대 성과

직관적인 오류 페이지 제공 및 사용자 신뢰 구축

  • 모든 예외 스택이 그대로 노출되던 화면 대신, 사전에 정의된 깔끔한 예외 렌더링 페이지(Exception Page)가 작동함으로써 엔드유저는 에러 코드를 확인하여 담당 관리자에게 신속하게 전달 및 문의할 수 있게 되었습니다.

웹 로그 기반 모니터링 가속화

  • 인프라 관리자는 웹서버 및 WAS 로그를 수집하여 DB 에러(예: Connection Timeout, Deadlock)인지, 혹은 일반적인 비즈니스 룰 예외인지 로그 등급과 정형화된 Error Format을 통해 1초 만에 확인하고 대응할 수 있게 되었습니다.

아쉬운 점과 향후 가이드라인

  • log4j로 정제된 에러 로그를 출력하도록 개선하였으나, 장기적으로는 미사용 예외 클래스들의 일괄 정리 및 비즈니스 성격에 맞는 예외 카테고리 간소화가 지속적으로 진행되어야 합니다.
  • 직관적이고 정형화된 Exception Message Handler 패키지를 사내 공통 프레임워크로 표준화하고 공홈(공식 레포지토리)을 통해 버전업 배포함으로써, 향후 타 부서 B2B 신규 프로젝트 생성 시에도 CS 대응 리소스를 제로에 수용하도록 최적화할 계획입니다.