본문 바로가기

Spring

[Spring] DI와 예외처리

Q. DI(Dependency Injection)에 대한 설명과 해당 기술의 장점에 대해 설명해주세요.

 

DI(Dependency Injection)의존성 주입으로 객체를 직접 생성하는 것이 아니라 외부에서 생성한 후 주입시켜주는 방식입니다. 모듈 간 결합도는 낮아지고 유연성이 높아집니다.

 

의존관계 주입 방법에는 생성자를 이용하는 방법, Setter를 이용하는 방법, 필드 주입 방법이 있습니다.

 

 

생성자 주입은 생성자 호출 시점에 1회 호출되는 것이 보장됩니다. 따라서 주입받은 객체가 변하지 않거나, 반드시 객체의 주입이 필요한 경우 사용할 수 있습니다.

 

Setter 주입은 주입받는 객체가 변경될 가능성이 있는 경우 사용합니다.

 

다만, 의존관계 주입의 변경이 필요한 상황은 거의 없기 때문에 생성자 주입을 사용하여 불필요한 수정의 가능성을 열어두지 않고 불변성을 보장하는 것이 좋습니다.

 

DI의 장점

  • 의존성이 줄어듭니다. 주입받는 대상이 변하더라도 해당 객체의 구현 자체를 수정할 일이 없거나 줄어들게 됩니다. 
  • 재사용성이 높은 코드가 됩니다. 클래스 내부에서만 사용되던 객체를 별도로 구분하여 구현하면 다른 클래스에서 재사용할 수 있습니다.
  • 테스트하기 좋은 코드가 됩니다. 주입받는 객체의 테스트를 기존 클래스의 테스트와 분리하여 진행할 수 있습니다.

        (내부에서 직접 생성하는 객체에 대해서, mocking을 할 방법이 없습니다. 따라서 그만큼 단위테스트를 하기가 까다로워집니다.)

  • 가독성이 높아집니다. 기능들을 별도로 분리하게 되어 가독성이 높아집니다.

 


 

 

Q. Spring MVC에서의 예외 처리 기법에 대해서 설명해주세요.

 

Spring은 아래와 같은 도구들로 ExceptionResolver를 동작시켜 에러를 처리할 수 있습니다.

  1. ResponseStatus
  2. ResponseStatusException
  3. ExceptionHandler
  4. ControllerAdvice, RestControllerAdvice

[ @ResponseStatus ]

@ResponseStatus는 에러 HTTP 상태를 변경하도록 도와주는 어노테이션으로 다음과 같은 경우들에 적용할 수 있습니다.

  • Exception 클래스 자체
  • 메소드에 @ExceptionHandler와 함께
  • 클래스에 @RestControllerAdvice와 함께

하지만 이는 BasicErrorController에 의한 응답입니다. 즉, @ResponseStatus를 처리하는 ResponseStatusExceptionResolver는 WAS(Web Application Server)까지 예외를 전달시키며, 복잡한 WAS의 에러 요청 전달이 진행되는 것입니다. @ResponseStatus는 다음과 같은 한계점들을 가지고 있습니다.

  • 에러 응답의 내용(Payload)를 수정할 수 없음(DefaultErrorAttributes를 수정하면 가능하긴 함)
  • 예와 클래스와 강하게 결합되어 같은 예외는 같은 상태와 에러 메세지를 반환함
  • 별도의 응답 상태가 필요하다면 예외 클래스를 추가해야 됨
  • WAS까지 예외가 전달되고, WAS의 에러 요청 전달이 진행됨
  • 외부에서 정의한 Exception 클래스에는 @ResponseStatus를 붙여줄 수 없음

프로퍼티 설정이나 에러 응답 커스터마이징을 통해 일부 문제를 해결할 수 있고, 메세지 소스를 사용해 다국어 처리도 할 수 있습니다. 하지만 개발자가 원하는대로 에러를 처리하는 것이 어렵기 때문에, 이러한 문제를 해결하기 위해서는 다른 방법을 사용해야 합니다.

[ ResponseStatusException ]

외부 라이브러리에서 정의한 코드는 우리가 수정할 수 없으므로 @ResponseStatus를 붙여줄 수 없습니다. Spring5에는 @ResponseStatus의 프로그래밍적 대안으로써 손쉽게 에러를 반환할 수 있는 ResponseStatusException가 추가되었습니다. ResponseStatusException는 HttpStatus와 함께 선택적으로 reason과 cause를 추가할 수 있고, 언체크 예외을 상속받고 있어 명시적으로 에러를 처리해주지 않아도 됩니다.

 

@ResponseStatus와 동일하게 예외가 발생하면 ResponseStatusExceptionResolver가 에러를 처리합니다. ResponseStatusException를 사용하면 다음과 같은 이점을 누릴 수 있습니다.

  • 기본적인 예외 처리를 빠르게 적용할 수 있으므로 손쉽게 프로토타이핑할 수 있음
  • HttpStatus를 직접 설정하여 예외 클래스와의 결합도를 낮출 수 있음
  • 불필요하게 많은 별도의 예외 클래스를 만들지 않아도 됨
  • 프로그래밍 방식으로 예외를 직접 생성하므로 예외를 더욱 잘 제어할 수 있음

하지만 그럼에도 불구하고 ResponseStatusException는 다음과 같은 한계점들을 가지고 있습니다. 이러한 이유로 API 에러 처리를 위해서는 @ExceptionHandler를 사용하는 방식이 더 많이 사용됩니다.

  • 직접 예외 처리를 프로그래밍하므로 일관된 예외 처리가 어려움
  • 예외 처리 코드가 중복될 수 있음
  • Spring 내부의 예외를 처리하는 것이 어려움
  • 예외가 WAS까지 전달되고, WAS의 에러 요청 전달이 진행됨

[ @ExceptionHandler ]

@ExceptionHandler는 유연하게 에러를 처리할 수 있는 방법을 제공하는 기능입니다. @ExceptionHandler는 어노테이션을 추가함으로써 에러를 손쉽게 처리할 수 있습니다.

  • 컨트롤러의 메소드
  • @ControllerAdvice나 @RestControllerAdvice가 있는 클래스의 메소드

컨트롤러의 메소드에 @ExceptionHandler를 추가함으로써 에러를 처리할 수 있습니다. @ExceptionHandler에 의해 발생한 예외는 ExceptionHandlerExceptionResolver에 의해 처리가 됩니다.

 

@ExceptionHandler는 Exception 클래스들을 속성으로 받아 처리할 예외를 지정할 수 있습니다. 만약 ExceptionHandler 어노테이션에 예외 클래스를 지정하지 않는다면, 파라미터에 설정된 에러 클래스를 처리하게 됩니다. 또한 @ResponseStatus와도 결합가능한데,  만약 ResponseEntity에서도 status를 지정하고 @ResponseStatus도 있다면 ResponseEntity가 우선순위를 갖습니다.

ExceptionHandler는 @ResponseStatus와 달리 에러 응답(payload)을 자유롭게 다룰 수 있다는 점에서 유연합니다. 예를 들어 응답을 다음과 같이 정의해서 내려준다면 좋을 것입니다.

  • code: 어떠한 종류의 에러가 발생하는지에 대한 에러 코드
  • message: 왜 에러가 발생했는지에 대한 설명
  • erros: 어느 값이 잘못되어 @Valid에 의한 검증이 실패한 것인지를 위한 에러 목록

여기서 code로 E001, E002 등과 같이 내부적으로 정의한 값을 사용하는 것보다 BAD_REQUEST와 같은 Http 표준 상태와과 같이 가독성 좋은 값을 사용하는 것이 클라이언트의 입장에서도 대응하기 좋고, 유지보수하는 입장에서도 좋습니다.

 

Spring은 예외가 발생하면 가장 구체적인 예외 핸들러를 먼저 찾고, 없으면 부모 예외의 핸들러를 찾습니다. 예를 들어 NullPointerException이 발생했다면, 위에서는 NullPointerException 처리기가 없으므로 Exception에 대한 처리기가 찾아집니다.

@ExceptionHandler를 사용 시에 주의할 점은 @ExceptionHandler에 등록된 예외 클래스와 파라미터로 받는 예와 클래스가 동일해야 한다는 것입니다. 만약 값이 다르다면 스프링은 컴파일 시점에 에러를 내지 않다가 런타임 시점에 에러를 발생시킵니다.

 

ExceptionHandler의 파라미터로 HttpServletRequest나 WebRequest 등을 얻을 수 있으며 반환 타입으로는 ResponseEntity, String, void 등 자유롭게 활용할 수 있습니다. 

@ExceptionHandler는 컨트롤러에 구현하므로 특정 컨트롤러에서만 발생하는 예외만 처리됩니다. 하지만 컨트롤러에 에러 처리 코드가 섞이며, 에러 처리 코드가 중복될 가능성이 높습니다. 그래서 스프링은 전역적으로 예외를 처리할 수 있는 좋은 기술을 제공해줍니다.

[ @ControllerAdvice와 @RestControllerAdvice ]

Spring은 전역적으로 @ExceptionHandler를 적용할 수 있는 @ControllerAdvice와 @RestControllerAdvice 어노테이션을 각각 Spring3.2, Spring4.3부터 제공하고 있습니다. 두 개의 차이는 @Controller와 RestController와 같이 @ResponseBody가 붙어 있어 응답을 Json으로 내려준다는 점에서 다릅니다. 

 

ControllerAdvice는 여러 컨트롤러에 대해 전역적으로 ExceptionHandler를 적용해줍니다. 위에서 보이듯 ControllerAdvice 어노테이션에는 @Component 어노테이션이 있어서 ControllerAdvice가 선언된 클래스는 스프링 빈으로 등록됩니다. 그러므로 우리는 다음과 같이 전역적으로 에러를 핸들링하는 클래스를 만들어 어노테이션을 붙여주면 에러 처리를 위임할 수 있습니다.

 

ControllerAdvice는 전역적으로 적용되는데, 만약 특정 클래스에만 제한적으로 적용하고 싶다면 @RestControllerAdvice의 basePackages 등을 설정함으로써 제한할 수 있습니다.

스프링 예외에는 대표적으로 잘못된 URI를 호출하여 발생하는 NoHandlerFoundException 등이 있습니다. Spring은 스프링 예외를 미리 처리해둔 ResponseEntityExceptionHandler를 추상 클래스로 제공하고 있습니다.

ResponseEntityExceptionHandler에는 스프링 예외에 대한 ExceptionHandler가 모두 구현되어 있으므로 ControllerAdvice 클래스가 이를 상속받게 하면 됩니다.

만약 이 추상 클래스를 상속받지 않는다면 스프링 예외들은 DefaultHandlerExceptionResolver가 처리하게 되는데, 그러면 예외 처리기가 달라지므로 클라이언트가 일관되지 못한 에러 응답을 받지 못하므로 ResponseEntityExceptionHandler를 상속시키는 것이 좋습니다. 또한 이는 기본적으로 에러 메세지를 반환하지 않으므로, 스프링 예외에 대한 에러 응답을 보내려면 아래 메소드를 오버라이딩 해야 합니다.

 

ControllerAdvice를 이용함으로써 다음과 같은 이점을 누릴 수 있습니다.

  • 하나의 클래스로 모든 컨트롤러에 대해 전역적으로 예외 처리가 가능함
  • 직접 정의한 에러 응답을 일관성있게 클라이언트에게 내려줄 수 있음
  • 별도의 try-catch문이 없어 코드의 가독성이 높아짐

 

이러한 이유로 API에 의한 예외 처리를 할 때에는 ControllerAdvice를 이용하면 됩니다. 하지만 ControllerAdvice를 사용할 때에는 항상 다음의 내용들을 주의해야 합니다. 여러 ControllerAdvice가 있을 때 @Order 어노테이션으로 순서를 지정하지 않는다면 Spring은 ControllerAdvice를 임의의 순서로 처리할 수 있으므로 일관된 예외 응답을 위해서는 이러한 점에 주의해야 합니다.

  • 한 프로젝트당 하나의 ControllerAdvice만 관리하는 것이 좋음
  • 만약 여러 ControllerAdvice가 필요하다면 basePackages나 annotations 등을 지정해야 함
  • 직접 구현한 Exception 클래스들은 한 공간에서 관리함

 


 

Spring은 에러 처리라는 공통 관심사(cross-cutting concerns)를 메인 로직으로부터 분리하는 다양한 예외 처리 방식을 고안하였고, 예외 처리 전략을 추상화한 HandlerExceptionResolver 인터페이스를 만들었습니다.

 

예외가 던져지면 디스패처 서블릿까지 전달되는데, 적합한 예외 처리를 위해 HandlerExceptionResolver 구현체들을 빈으로 등록해서 관리합니다. 그리고 적용 가능한 구현체를 찾아 예외 처리를 하는데, 우선순위대로 아래의 4가지 구현체들이 빈으로 등록되어 있습니다.

  • DefaultErrorAttributes: 에러 속성을 저장하며 직접 예외를 처리하지는 않음
  • ExceptionHandlerExceptionResolver: 에러 응답을 위한 Controller나 ControllerAdvice에 있는 ExceptionHandler를 처리함
  • ResponseStatusExceptionResolver: Http 상태 코드를 지정하는 @ResponseStatus 또는 ResponseStatusException를 처리함
  • DefaultHandlerExceptionResolver:  스프링 내부의 기본 예외들을 처리함

 

[ Spring의 예외 처리 흐름 ]

 

  1. ExceptionHandlerExceptionResolver가 동작함
    1. 예외가 발생한 컨트롤러 안에 적합한 @ExceptionHandler가 있는지 검사함
    2. 컨트롤러의 @ExceptionHandler에서 처리가능하다면 처리하고, 그렇지 않으면 ControllerAdvice로 넘어감
    3. ControllerAdvice안에 적합한 @ExceptionHandler가 있는지 검사하고 없으면 다음 처리기로 넘어감
  2. ResponseStatusExceptionResolver가 동작함
    1. @ResponseStatus가 있는지 또는 ResponseStatusException인지 검사함
    2. 맞으면 ServletResponse의 sendError()로 예외를 서블릿까지 전달되고, 서블릿이 BasicErrorController로 요청을 전달함
  3. DefaultHandlerExceptionResolver가 동작함
    1. Spring의 내부 예외인지 검사하여 맞으면 에러를 처리하고 아니면 넘어감
  4. 적합한 ExceptionResolver가 없으므로 예외가 서블릿까지 전달되고, 서블릿은 SpringBoot가 진행한 자동 설정에 맞게 BasicErrorController로 요청을 다시 전달함

 

 

 

참고

https://mangkyu.tistory.com/204

'Spring' 카테고리의 다른 글

[Spring] 트랜잭션 & TCP와 UDP  (0) 2023.07.26
[Spring] 의존성 주입(DI)  (0) 2023.07.25
[Spring] Rest Client & @SpringBootTest와 @WebMvcTest  (0) 2023.07.25
[Spring] JPA & 제네릭  (0) 2023.07.21
[Spring] @Transactional 어노테이션  (0) 2023.04.18