BindProject

전역 에러 핸들러로 try-catch를 걷어내기

dding-shark 2025. 7. 23. 19:02
728x90

문제의 시작: MVP 이후 리팩토링의 시작 

기능을 하나씩 추가하며 API 엔드포인트를 늘려갈 때였다. AuthController등 컨트롤러들은여러 기능을 담고 있었고, 테스트 할때 문제 인식을 명확하게 하고자 TRY-CATCH 문으로 각 에러들을 핸들러 했다, 이제 테스트도 충분히 했고, 여러 유즈케이스의 대응이 되는거같아서 지저분한 코드 보다는 읽기 쉬운코드로 리팩토링 해보려고 한다.
처음 마주했던 의 모습:AuthController.java

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth/v1")
public class AuthController {

    private final AuthService authService;

    @PostMapping("/signup")
    public ResponseEntity<BaseResponse<?>> signup(@RequestBody SignUpRequest req) {
        try {
            authService.registerUser(req);
        } catch (AuthException e) {
            // 여기!
            return ResponseEntity.badRequest().body(BaseResponse.fail(e.getErrorCode()));
        }
        return ResponseEntity.ok(BaseResponse.success());
    }

    @PostMapping("/login")
    public ResponseEntity<BaseResponse<LoginResponse>> login(@RequestBody LoginRequest req) {
        try {
            LoginResponse loginResponse = authService.login(req);
            return ResponseEntity.ok(BaseResponse.success(loginResponse));
        } catch (AuthException e) {
            // 그리고 여기도!
            return ResponseEntity.badRequest().body(BaseResponse.fail(e.getErrorCode()));
        }
    }
    // ... 다른 메서드들도 마찬가지였다.
}

모든 메서드가 try로 비즈니스 로직을 감싸고, catchAuthException을 잡아 동일한 형태의 실패 응답을 반환했다. 코드는 정상적으로 동작은 하지만, 이제 슬슬 기능 이외의 여러문제가 신경쓰이기 시작하니, 리팩토링을 할때가 된거같다.

  • "반복적인 코드는 개발자를 화나게 해요.": 새로운 API를 추가할 때마다 이 try-catch 구문을 복사해서 붙여넣고 있는 나를 발견했다.
  • "컨트롤러의 진짜 역할이 눈에 안보인다 ": 컨트롤러는 요청을 받아 서비스에 넘겨주는 역할에 충실해야 하지 않을까? 예외를 잡아서 를 만드는 일까지 떠맡는 건 과연 옳은 걸까? 

이 지저분한 코드를 그대로 둘 수는 없었다. 해결책이 필요했다.

해결의 실마리: @RestControllerAdvice와의 만남

나는 이 문제를 해결하기 위해 Spring이 제공하는 AOP(관점 지향 프로그래밍) 개념을 떠올렸고, 곧 @RestControllerAdvice라는 보석 같은 어노테이션을 발견했다. 이 어노테이션을 사용하면 프로젝트 전역에서 발생하는 예외를 한 곳에서 처리할 수 있었다. 마치 모든 예외를 감시하고 처리하는 지휘자를 두는 것과 같았다.
나는 다음과 같은 계획을 세웠다.

  1. 이때를 위해 모든 커스텀 예외의 공통 조상을 만들었다. () CustomBaseException
  2. 예외 처리만을 전담하는 핸들러를 만든다.GlobalExceptionHandler
  3. 컨트롤러에서 try-catch를 모두 걷어낸다!

변화의 과정: 코드를 정화하다

1단계: 예외의 체계화
먼저 AuthException, 등 산발적으로 존재하던 예외들을 한 뿌리에서 관리하기 위해 이라는 추상 클래스를 만들고, 모든 커스텀 예외가 이 `CustomBaseException`클래스를 상속하도록 구조를 만들었었다.

 

2단계: 전역 예외 처리기 구현
그리고 프로젝트의 모든 예외를 처리할 를 만들었다. 

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    // CustomBaseException을 상속한 모든 예외는 여기서 처리된다!
    @ExceptionHandler(CustomBaseException.class)
    public ResponseEntity<ExceptionResponse> handleCustomBaseException(CustomBaseException ex) {
        // ... 예외에 맞는 응답을 생성하는 로직 ...
        return ResponseEntity
                .status(ex.getErrorCode().getStatus())
                .body(ExceptionResponse.from(ex.getErrorCode()));
    }
}

@ExceptionHandler(CustomBaseException.class) 이 한 줄이 핵심이었다. 이제 서비스 레이어 어디에서든 AuthException이나 이 던져지면, Spring이 알아서 이 핸들러로 보내줄 것이었다.
3단계: 마침내, 정화의 시간
가장 기대했던 마지막 단계. 나는 확신을 갖고 AuthController로 돌아가 지저분했던 try-catch 블록들을 삭제하기 시작했다.
새롭게 태어난 AuthController.java

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth/v1")
public class AuthController {
    private final AuthService authService;

    @PostMapping("/signup")
    public ResponseEntity<BaseResponse<?>> signup(@RequestBody SignUpRequest req) {
        authService.registerUser(req); // 예외가 터지면? 알아서 처리될 것이다!
        return ResponseEntity.ok(BaseResponse.success());
    }

    @PostMapping("/login")
    public ResponseEntity<BaseResponse<LoginResponse>> login(@RequestBody LoginRequest req) {
        LoginResponse loginResponse = authService.login(req); // 그냥 비즈니스 로직에만 집중
        return ResponseEntity.ok(BaseResponse.success(loginResponse));
    }
}

코드가 놀랍도록 깔끔해졌다. 컨트롤러는 이제 정말 '컨트롤러'의 역할에만 충실하게 되었다. 더 이상 예외 처리를 걱정하지 않아도 됐다.

회고를 마치며

이 리팩토링 과정을 통해 나는 단순히 코드 몇 줄을 줄인 것 이상의 것을 얻었다.

  • 명확해진 책임: 컨트롤러는 요청과 응답을, 서비스는 비즈니스 로직을, 핸들러는 예외 처리를 담당하게 되었다. 각자의 역할이 명확해지니 코드 전체를 이해하기가 훨씬 쉬워졌다.
  • 높아진 유지보수성: 이제 예외 응답 정책이 바뀌어도 단 한 곳만 수정하면 된다. GlobalExceptionHandler
  • 개발의 즐거움: 무엇보다 깨끗하고 일관성 있는 코드를 작성하게 되면서 개발 과정 자체가 더 즐거워졌다.

작은 불편함에서 시작된 고민이었지만, 그 끝에는 프로젝트의 건강함을 되찾는 값진 경험이 있었다. 앞으로도 나는 내 코드에 계속 질문을 던지며 더 나은 구조를 찾아 나서는 여정을 멈추지 않을 것이다.

728x90