반응형

Exception Handling in Spring MVC

Spring MVC는 예외처리를 위한 몇가지 훌륭한 접근법을 제공해주지만 Spring MVC를 가르칠때 학생들이 종종 헷갈려하거나 불편해한다는 것을 알았다.

이 글에서 이를 위해 사용가능한 다양한 옵션을 보여줄 것이다. 우리의 목표는 가능한한 컨트롤러 메소드에서 명시적으로 예외처리를 하지 않는 것이다. 이들의 횡단관심사cross-cutting concern는 전용코드에서 별도로 처리하는 더 나은 방식을 제공해준다.

3가지 옵션이 있다: 예외별, 컨트롤러별, 전역별 per exception, per controller or globally.

이 글에서 다루어진 예제 어플리케이션은 다음의 주소에서  받아볼 수 있다

http://github.com/paulc4/mvc-exceptions.

아래의 Sample Application 섹션에 자세한 설명을 해두었다..

알림: 데모 어플리케이션은 2014년 10월에 새롭게 다시 업데이트되어 스프링 부트 1.1.8에서 사용가능하며, 사용하거나 이해하기 더 쉽게 바뀌었다.

HTTP 상태코드 사용하기 Using HTTP Status Codes

보통 웹요청 처리시 발생하는 미처리 예외unhandled exception는 서버가 HTTP 500 응답을 리턴한다. 그러나 당신이 작성한 커스텀 예외를 (HTTP명세서에 의해 정의된 모든 HTTP 상태코드를 지원하는) @ResponseStatus 어노테이션과 함께 사용할 수 있다.  어노테이션된 예외annotated exception가 컨트롤러 메소드에서 발생할 때, 그리고 다른 곳에서 처리되지 않을때, 이는 자동적으로 특정한 상태코드를 가지고 리턴되는 적절한 HTTP응답을 발생할 것이다.

예를 들면, 여기 Order가 빠진 경우의 예외exception를 보자:

    @ResponseStatus(value=HttpStatus.NOT_FOUND, reason="No such Order")  // 404
    public class OrderNotFoundException extends RuntimeException {
        // ...
    }

그리고 그것을 사용하고 있는 컨트롤러:

    @RequestMapping(value="/orders/{id}", method=GET)
    public String showOrder(@PathVariable("id") long id, Model model) {
        Order order = orderRepository.findOrderById(id);
        if (order == null) throw new OrderNotFoundException(id);
        model.addAttribute(order);
        return "orderDetail";
    }

만일 이 메소드에 유효하지않은 Order ID가 들어오면 익숙한 HTTP 404 응답이 리턴되어질 것이다.

컨트롤러 기반 예외처리 Controller Based Exception Handling

@ExceptionHandler 사용하기 Using @ExceptionHandler

당신은 부가적인 (@ExceptionHandler) 메소드를 어느 컨트롤러에나 추가하여 같은 컨트롤러의 요청처리request handling (@RequestMapping) 메소드에 의해 발생하는 예외처리들을 구체화할 수 있다. 이러한 메소드들은 다음의 일들을 할 수 있다:

  1. @ResponseStatus 어노테이션 없이 예외를 처리한다 (보통 당신이 작성하지않은 선정의된 예외들)
  2. 사용자를 특정한 에러페이지로 리다이렉트한다
  3. 완전히 컨스텀 에러 응답을 만든다

아래의 컨트롤러는 이 3가지 옵션을 보여준다:

@Controller
public class ExceptionHandlingController {

  // @RequestHandler methods
  ...
  
  // Exception handling methods
  
  // Convert a predefined exception to an HTTP Status code
  @ResponseStatus(value=HttpStatus.CONFLICT, reason="Data integrity violation")  // 409
  @ExceptionHandler(DataIntegrityViolationException.class)
  public void conflict() {
    // Nothing to do
  }
  
  // Specify the name of a specific view that will be used to display the error:
  @ExceptionHandler({SQLException.class,DataAccessException.class})
  public String databaseError() {
    // Nothing to do.  Returns the logical view name of an error page, passed to
    // the view-resolver(s) in usual way.
    // Note that the exception is _not_ available to this view (it is not added to
    // the model) but see "Extending ExceptionHandlerExceptionResolver" below.
    return "databaseError";
  }

  // Total control - setup a model and return the view name yourself. Or consider
  // subclassing ExceptionHandlerExceptionResolver (see below).
  @ExceptionHandler(Exception.class)
  public ModelAndView handleError(HttpServletRequest req, Exception exception) {
    logger.error("Request: " + req.getRequestURL() + " raised " + exception);

    ModelAndView mav = new ModelAndView();
    mav.addObject("exception", exception);
    mav.addObject("url", req.getRequestURL());
    mav.setViewName("error");
    return mav;
  }
}

이 메소드들중 아무거나, 당신이 추가적인 처리를 위해 고를 수 있다. 가장 일반적인 예제는 예외를 로그하는 것이다.

처리Handler 메소드는 유연한 특징을 가지고 있어 당신이 HttpServletRequestHttpServletResponseHttpSession 그리고/또는 Principl. 와 같은 명확히 서블릿 관련 객체들을 패스할 수 있다. 중요한 알림:  Model 은 @ExceptionHandler 메소드의 파라메터가 될 수 없다. 대신, 위의 handleError()에 의해 보여진.ModelAndView 를 사용하여 메소드안에 하나의 모델을 설정할 수 있다.

예외와 뷰 Exceptions and Views

예외를 모델에 추가할 때 조심해야할 점은, 당신의 사용자들은 구체적인 자바 예외나 stack-trace가 포함된 웹페이지를 보고 싶지않아 한다는 것이다. 하지만 페이지 소스안에 코멘트로서 구체적인 예외 상태를 넣어 당신을 서포트하는 사람들을 도와주려는 것은 유용할 수 있다. 만일 JSP를 사용한다면 당신은 아래와 같이 예외 메세지나 (숨겨진 <div> 를 사용하여) stack-trace를 출력하는 등등을 할 수 있을 것이다.

    <h1>Error Page</h1>
    <p>Application has encountered an error. Please contact support on ...</p>
    
    <!--
    Failed URL: ${url}
    Exception:  ${exception.message}
        <c:forEach items="${exception.stackTrace}" var="ste">    ${ste} 
    </c:forEach>
    -->

타임리프Thymeleaf에서 이와 같은 일을 하려면 support.html를 보자
예제 어플리케이션에선 다음과 같은 결과를 볼 수 있다.

Example of an error page with a hidden exception for support

전역 예외 처리 Global Exception Handling

@ControllerAdvice 클래스 사용하기 Using @ControllerAdvice Classes

컨트롤러 어드바이스는 당신에게 똑같은 예외처리 기술을 사용하지만, 개별 컨트롤러가 아니라 전체 어플리케이션에 적용할 수 있게 만들어 준다. 이들을 어노테이션 기반 인터셉터annotation driven interceptor로 이해하면 될 것이다.

@ControllerAdvice 어노테이션을 가지는 클래스는 컨트롤러 어드바이스controller-advice가 되며 3가지 타입을 메소드를 지원할 수 있다:

  • @ExceptionHandler으로 어노테이션된 예외처리 메소드
  • @ModelAttribute으로 어노테이션된 (추가적인 데이터를 모델에 추가하기 위한) 모델 향상Model enhancement 메소드. [Note] 이들 속성들은  예외처리 뷰에서 사용할 수 없다.
  • @InitBinder로 어노테이션된 (폼처리를 설정하는데 사용되는) 바인더 초기화Binder initialization 메소드 

우리는 여기서 예외처리만 다룰것이므로 @ControllerAdvice 메소드에 대한 자세한 사항은 온라인 메뉴얼을 보자.

위에서 본 어느 예외처리도 컨트롤러-어드바이스 클래스에서 정의할 수 있다 - 그러나 이제 이들은 이제 모든 컨트롤러에서 발생하는 예외에 적용될 것이다. 아래 간단한 예제를 보자:

@ControllerAdvice
class GlobalControllerExceptionHandler {
    @ResponseStatus(HttpStatus.CONFLICT)  // 409
    @ExceptionHandler(DataIntegrityViolationException.class)
    public void handleConflict() {
        // Nothing to do
    }
}

어떠한 예외에 처리되는 기본 처리자가 필요하면, 다음과 같이 약간만 손보면 된다. 어노테이션된 예제는 프레임워크에 의해 처리된다는 것을 명심하자:

@ControllerAdvice
class GlobalDefaultExceptionHandler {
    public static final String DEFAULT_ERROR_VIEW = "error";

    @ExceptionHandler(value = Exception.class)
    public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
        // If the exception is annotated with @ResponseStatus rethrow it and let
        // the framework handle it - like the OrderNotFoundException example
        // at the start of this post.
        // AnnotationUtils is a Spring Framework utility class.
        if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null)
            throw e;

        // Otherwise setup and send the user to a default error-view.
        ModelAndView mav = new ModelAndView();
        mav.addObject("exception", e);
        mav.addObject("url", req.getRequestURL());
        mav.setViewName(DEFAULT_ERROR_VIEW);
        return mav;
    }
}

더 자세히 들어가보기 Going Deeper

HandlerExceptionResolver

HandlerExceptionResolver를 구현한 DispatcherServlet의 application context에서 선언된 모든 스프링빈은 MVC 시스템에서 올라오는 모든 예외를 처리하고 인터셉트하는데 사용되어지며 컨트롤러에 의해 처리되지않는다. 인터페이스틑 아래와 같다:

public interface HandlerExceptionResolver {
    ModelAndView resolveException(HttpServletRequest request, 
            HttpServletResponse response, Object handler, Exception ex);
}

handler 는 예외가 발생한 컨트롤러를 참조한다. (@Controller 인스턴스들은 스프링 MVC가 지원하는 핸들러의 하나의 타입일 뿐이라는 것을 기억하자. 예를 들면, HttpInvokerExporter 와 WebFlow Executor 또한 핸들러의 타입들이다.

이 뒷단에서 MVC는 세가지 resolver를 기본으로 생성한다. 이 3가지 리졸버들은 위에 논의돈 행동들을 구현한 것이다:

  • ExceptionHandlerExceptionResolver는 핸들러(컨트롤러)와 컨트롤러-어드바이스들상의 적절한 @ExceptionHandler 메소드를 위한 uncaught exception에 맞닿아 매치된다.
  • matches uncaught exceptions against for
    suitable @ExceptionHandler methods on both the handler (controller) and on any controller-advices.
  • ResponseStatusExceptionResolver는 (섹션1에서 설명한) @ResponseStatus 에 의해 어노테이션 된 uncaught exception들을 찾는다.
  • DefaultHandlerExceptionResolver 는 표준 스프링 예외를 변환하고 그들을 HTTP상태코드로 변환한다. (스프링MVC에서 내부적으 동작하는 부분에 대해서 언급하지는 않겠다)

이들은 서로 순서에 따라 연쇄작동하고 처리한다. (내부적으로 스프링은 이를 담당하는 빈들 생성하는데 - HandlerExceptionResolverComposite이 이를 담당한다)

resolveException 의 메소드 시그니쳐는 Model을 포함하지않는다는 것을 상기하자. 아래에 그 이유가 있다


필요시 자신의 커스텀 예외처리 시스템을 설정하기 위해, 커스텀

`HandlerExceptionResolver`를 구현할 수 있다. Handlers 는 보통 스프링의 `Ordered`인터페이스를 구현하여 당신이 실행하는 핸들러의 순서를 정의 할수 있다.


###SimpleMappingExceptionResolver

Spring has long provided a simple but convenient implementation of `HandlerExceptionResolver`
that you may well find being used in your appication already - the `SimpleMappingExceptionResolver`.
It provides options to:

  * Map exception class names to view names - just specify the classname, no package needed.
  * Specify a default (fallback) error page for any exception not handled anywhere else
  * Log a message (this is not enabled by default).
  * Set the name of the `exception` attribute to add to the Model so it can be used inside a View
(such as a JSP). By default this attribute is named ```exception```.  Set to ```null``` to disable.  Remember
that views returned from `@ExceptionHandler` methods _do not_ have access to the exception but views
defined to `SimpleMappingExceptionResolver` _do_.

Here is a typical configuration using XML:
<bean id="simpleMappingExceptionResolver"
      class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="exceptionMappings">
        <map>
            <entry key="DatabaseException" value="databaseError"/>
            <entry key="InvalidCreditCardException" value="creditCardError"/>
        </map>
    </property>
    <!-- See note below on how this interacts with Spring Boot -->
    <property name="defaultErrorView" value="error"/>
    <property name="exceptionAttribute" value="ex"/>

    <!-- Name of logger to use to log exceptions. Unset by default, so logging disabled -->
    <property name="warnLogCategory" value="example.MvcLogger"/>
</bean>

Or using Java Configuration:

@Configuration
@EnableWebMvc // Optionally setup Spring MVC defaults if you aren’t doing so elsewhere
public class MvcConfiguration extends WebMvcConfigurerAdapter {
@Bean(name=“simpleMappingExceptionResolver”)
public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() {
SimpleMappingExceptionResolver r =
new SimpleMappingExceptionResolver();

    Properties mappings = new Properties();
    mappings.setProperty("DatabaseException", "databaseError");
    mappings.setProperty("InvalidCreditCardException", "creditCardError");

    r.setExceptionMappings(mappings);  // None by default
    r.setDefaultErrorView("error");    // No default
    r.setExceptionAttribute("ex");     // Default is "exception"
    r.setWarnLogCategory("example.MvcLogger");     // No default
    return r;
}
...

}


The _defaultErrorView_ property is especially useful as it ensures any uncaught exception generates a suitable application defined error page. (The default for most application servers is to display a Java stack-trace - something your users should _never_ see). ###Extending SimpleMappingExceptionResolver It is quite common to extend `SimpleMappingExceptionResolver` for several reasons: * Use the constructor to set properties directly - for example to enable exception logging and set the logger to use * Override the default log message by overriding `buildLogMessage`. The default implementation always returns this fixed text:<ul style="margin-left: 2em"><i>Handler execution resulted in exception</i></ul> * To make additional information available to the error view by overriding `doResolveException` For example:

public class MyMappingExceptionResolver extends SimpleMappingExceptionResolver {
public MyMappingExceptionResolver() {
// Enable logging by providing the name of the logger to use
setWarnLogCategory(MyMappingExceptionResolver.class.getName());
}

@Override
public String buildLogMessage(Exception e, HttpServletRequest req) {
    return "MVC exception: " + e.getLocalizedMessage();
}

@Override
protected ModelAndView doResolveException(HttpServletRequest request,
        HttpServletResponse response, Object handler, Exception exception) {
    // Call super method to get the ModelAndView
    ModelAndView mav = super.doResolveException(request, response, handler, exception);

    // Make the full URL available to the view - note ModelAndView uses addObject()
    // but Model uses addAttribute(). They work the same. 
    mav.addObject("url", request.getRequestURL());
    return mav;
}

}


This code is in the demo application as <a href="https://github.com/paulc4/mvc-exceptions/blob/master/src/main/java/demo1/web/ExampleSimpleMappingExceptionResolver.java">ExampleSimpleMappingExceptionResolver</a> ###Extending ExceptionHandlerExceptionResolver It is also possible to extend `ExceptionHandlerExceptionResolver` and override its `doResolveHandlerMethodException` method in the same way. It has almost the same signature (it just takes the new `HandlerMethod` instead of a `Handler`). To make sure it gets used, also set the inherited order property (for example in the constructor of your new class) to a value less than `MAX_INT` so it runs _before_ the default ExceptionHandlerExceptionResolver instance (it is easier to create your own handler instance than try to modify/replace the one created by Spring). See <a href="http://github.com/paulc4/mvc-exceptions/blob/master/src/main/java/demo1/web/ExampleExceptionHandlerExceptionResolver.java">ExampleExceptionHandlerExceptionResolver</a> in the demo app for more. ###Errors and REST RESTful GET requests may also generate exceptions and we have already seen how we can return standard HTTP Error response codes. However, what if you want to return information about the error? This is very easy to do. Firstly define an error class:

public class ErrorInfo {
public final String url;
public final String ex;

public ErrorInfo(String url, Exception ex) {
    this.url = url;
    this.ex = ex.getLocalizedMessage();
}

}


Now we can return an instance from a handler as the ```@ResponseBody``` like this:

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MyBadDataException.class)
@ResponseBody ErrorInfo handleBadRequest(HttpServletRequest req, Exception ex) {
return new ErrorInfo(req.getRequestURL(), ex);
}
```

언제 무엇을 써야하나? What to Use When?

보통 스프링은 당신에게 선택할 수 있게 제공하는 것을 선호한다. 그래서 당신이 해야하는게 뭘까? 여기 몇가지 중요한 룰이 있다. 하지만 XML설정이나 어노테이션을 선호한다면 그 역시 상관없다.

  • 당신이 작성한 예외들에 @ResponseStatus를 추가하는 것을 고려하라.
  • @ControllerAdvice 클래스에 @ExceptionHandler 메소드를 구현하거나 SimpleMappingExceptionResolver의 인스턴스를 사용하는 모든 종류의 예외들에 대해 아마 당신의 어플리케이션 설정에 이미  SimpleMappingExceptionResolver 를 이미 사용하고 있다면, 여기에 새로운 예외클래스를 추가하는게 @ControllerAdvice를 구현하는 것보다 더 쉬울 것이다.
  • 컨트롤러 특정 예외 처리를 하려면 당신의 컨트롤러에 @ExceptionHandler 메소드를 추가하자.
  • Warning: 같은 어플리케이션에 이들 옵션을 너무 많이 혼용하여 사용하지 않아야한다. 같은 예외같 하나 이상의 방식으로 처리되어질 수 있고, 이경우 원치않는 행동을 얻을 수 있다. 컨트롤러에서 @ExceptionHandler 메소드는 항상 @ControllerAdvice 인스턴스의 메소드들 전에 선택되어진다. 무슨 컨트롤러 어드바이스가 먼저 처리되는지 정의하지 않는다.

예제 어플리케이션 Sample Application

예제 어플리케이션은 github에서 받을 수 있다.
스프링 부트와 타임리프를 사용하는 간단한 웹어플리케이션이다.

이 어플리케이션은 2014년 10월에 더 이해하기 쉽게 개정되었다. 그 기반은 동일하다. 스프링부트 1.1.8과 스프링 4.1을 사용하지만 스프링 3.x에서 또한 동작가능하다.

이 데모는 클라우드 파운드리의 http://mvc-exceptions-v2.cfapps.io/에서 동작하고 있다.

데모에 대해 About the Demo

어플리케이션은 서로 다른 예외처리 기술을 쓰는 5개의 데모페이지를 가지고 있다:

  1. 그 자신의 예외처리를 위한 @ExceptionHandler 메소드를 가진 하나의 컨트롤러 
  2. 글로벌 컨트롤러 어드바이저에 의해 처리되는 예외를 뿌리는 하나의 컨트롤러
  3. SimpleMappingExceptionResolver 를 사용하여 예외처리
  4. 3번과 동일하지만 비교를 위해 SimpleMappingExceptionResolver를 disabled 함
  5. 어떻게 스프링 부트가 에러페이지를 만드는지 보여줌

홈 웹페이지는 index.html 이며:

  • 각 데모페이지로의 링크
  • 스프링부트에 관심있는 사람을 위해 스프링 부트 종단의 링크

각각 데모페이지는 몇개의 링크를 가지고 있으며 모두 예외를 발생시킨다. 당신 브라우저 백버튼을 사용하여 각 데모페이지로 되돌아 올수 있다.

이 데모를 내장 톰켓 컨테이너에서 자바 어플리케이션으로 실행할 수 있게 만든 스프링 부트에 감사한다. 이 어플리케이션을 싱핼하려면 다음중 하나의 명령어를 사용하면 된다:

  • mvn exec:java
  • mvn spring-boot:run

기본 홈페이지 URL은 http://localhost:8080.

스프링 부트와 에러처리 Spring Boot and Error Handling

스프링 부트 는 스프링 프로젝트를 최소한의 설정으로 돌릴 수 있게 만들어준다. 스프링부트는 클래스 패스의 키 클래스과 패키지들을 찾아 자동으로 민감한 기본값들을 생성한다. 예를 들면 당신이 서블릿 환경을 사용중이라면 스프링 MVC를 가장 일반적으로 많이 사용하는 뷰-리졸버view-resolvers, 핸들러 매핑handler mappings 등등을 설정해준다. 만일 JSP나 타임리프 파일이 있으면 그에 맞는 해당 뷰 기술을 자동으로 설정한다.

스프링 MVC는 기본적으로 제공하는 에러페이지가 없다. 기본 에러페이지를 설정하는 가장 흔한 방법은 언제나 SimpleMappingExceptionResolver 를 가지는 것이다. (스프링 버전1 이후로) 그러나 스프링 부트는 또한 에러 처리fallback error-handling 페이지를 제공하고 있다.

시작시 스프링 부트는 /error를 위한 매핑을 찾는다. 명명법에 의해 /error 로 끝나는 URL은 같은 이름의 논리적 뷰와 매핑된다: error. 데모 어플리케이션에서, 이 뷰는 타임리프 템플릿의  error.html로 매핑된다. (만일 JSP를 사용하고 있다면 
InternalResourceViewResolver가 설정되면서 error.jsp로 매핑될 것이다)

/error 와 매핑되는 뷰가 없다면, 스프링 부트는 “Whitelabel Error Page” 라 불리는 그 자신의 에러페이지를 정의한다 (HTTP상태 정보와 uncaught exception로 부터의 메세지와 같은 어떠한 에러 디테일 가지는 최소한의 페이지). 만일 error.html 템플릿을 이를테면, error2.html로 이름을 바꾸고 재시작하면 이것이 사용되어지는 것을 확인할 수 있다.

defaultErrorView()로 불리는 @Bean 메소드를 자바 설정으로 정의함으로서, 당신은 자신만의 에러 View인스턴스를 리턴할 수 있다. (더 자세한 정보는 스프링 부트의ErrorMvcAutoConfiguration 클래스를 보자)

기본 에러 뷰를 설정하기 위해 이미  SimpleMappingExceptionResolver를 사용중이라면? 간단히 defaultErrorView에 스프링부트에서 사용하는 같은 뷰: error를 정의해주면 된다. 또는 application.properties파일에 error.whitelabel.enabled를 false로 설정하여 스프링 부트의 기본 에러페이지를 disabled하면 된다. 이경우 당신의 컨테이너의 기본 에러페이지가 사용될 것이다.

Main의 생성자에서 스프링 부트 프로퍼티를 설정하는 예제링크

이 데모에서 SimpleMappingExceptionResolver의 defaultErrorView 프로퍼티는 의도적으로 error이 아니라 defaultErrorPage로 설정되어 당신은 핸들러가 에러페이지를 생성할때나 스프링 부트가 응답할때 볼 수 있을 것이다. 보통은 둘다 error로 설정되어 있다.

또한 이 데모 어플리케이션에서 stack-trce를 HTML소스에 숨겨둔 서포트 준비가 된support-ready 에러페이지를 만드는 법을 확인할 수 있다. (코멘트로서). 이상적으로 이러한 정보는 로그로 부터 얻어야 하지만 실제 삶은 언제나 이상적이지 않는다. 어쨋든 이 페이지에서 보려주려고 하는 것은 어떻게 존재하는 에러처리 메소드인 handleError 가 그 자신에 추가적인 정보를 제공하기 위해  ModelAndView를 만드는 가 이다. 다음의 사항도 확인해보자:

ExceptionHandlingController.handleError() on github
GlobalControllerExceptionHandler.handleError() on github


반응형

반응형

Spring REST API에 Swagger 2 설정하기

Setting Up Swagger 2 with a Spring REST API

(원문소스: http://www.baeldung.com/swagger-2-documentation-for-spring-rest-api)


1. 개요 Overview

REST API를 만들 때 문서화를 잘하는 것은 중요하다.

더우기 API를 변경할 때마다 레퍼런스 문서에 똑같이 명시해주어야한다. 이것을 수작업으로 반영하는 것은 매우 지루한 일이므로, 이것을 자동화하는 것은 필수다.

이 튜토리얼에서 우리는 스프링 REST 웹서비스를 위한 스웨거 2 Swagger 2 for a Spring REST web service를 들여다 볼것이다. 이 문서에서 스웨거 2 명세서의 구현체인 Springfox를 사용할 것이다.

스웨거에 익숙하지않은 사용자라면 먼저 이 글을 읽기전에 공식웹페이지를 방문해보기를 권한다.

2. 타겟 프로젝트 Target Project

REST서비스 생성은 이 문서의 범주가 아니므로 사용자는 이미 간단히 적용할만한 프로젝트를 가지고 있어야 한다. 아직 없다면 다음의 링크가 좋은 출발점이 될 것이다:

3. 메이븐 의존성 추가 Adding the Maven Dependency

위에서 언급한 것처럼 우리는 스웨거 명세서의 스프링 폭스 구현체를 사용할 것이다. 메이븐 프로젝트의 pom.xml파일에 다음의 의존성을 추가하자.

1
2
3
4
5
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.2.2</version>
</dependency>

4. 프로젝트에 스웨거 2 통합하기 Integrating Swagger 2 into the Project

4.1. 자바 설정 Java Configuration

스웨거의 설정은 주로 Docket 빈 설정에 중심을 둔다:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@EnableSwagger2
public class SwaggerConfig {                                   
    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2) 
          .select()                                 
          .apis(RequestHandlerSelectors.any())             
          .paths(PathSelectors.any())                         
          .build();                                          
    }
}

스웨거 2는 @EnableSwagger2 어노테이션 설정으로 사용가능해진다.

Docket 빈을 정의한 후, 그 select() 메소드는 ApiSelectorBuilder 인스턴스를 리턴하는데 이는 스웨거에 의해 노출되는 끝단endpoint 을 제어하는 하나의 방법을 제공해준다.

RequestHandlers 의 selection을 위한 서술부는 RequestHandlerSelectors 와 PathSelectors 의 도움을 받아 설정할 수 있다. 둘 다 any() 를 쓰면 사용자의 전체 API가 스웨거를 통해 문서화 되어질 것이다.

이 설정을 통해 기존의 스프링 부트 프로젝트에 스웨거 2를 통합할 수 있다. 다른 종류의 스프링 프로젝트에서는 몇가지 추가적인 작업을 해주어야한다.

4.2. 스프링 부트 없이 설정하기 Configuration Without Spring Boot

스프링 부트를 사용하지 않는다면 사용자는 리소스 핸들러의 자동설정 혜택을 받을 수 없다. 스웨거 UI는 리소스의 세트를 추가하는 데 이는 사용자가 @EnableWebMvc 어노테이션을 가지는, WebMvcConfigurerAdapter 를 확장한 하나의 클래스와 일부로서 설정해주어야 한다.

1
2
3
4
5
6
7
8
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("swagger-ui.html")
      .addResourceLocations("classpath:/META-INF/resources/");
 
    registry.addResourceHandler("/webjars/**")
      .addResourceLocations("classpath:/META-INF/resources/webjars/");
}

4.3. 검증 Verification

스프링폭스가 동작하는지 검증하려면 다음의 URL을 브라우저로 접속해보자:

http://localhost:8080/your-app-root/v2/api-docs

가독성이 매우 낮은 key-value페어를 가진 수많은 JSON응답을 볼 수 있는데, 다행히 스웨거는 이를 위해 스웨거 UI를 제공해준다.

5. 스웨거 UI Swagger UI

스웨거 UI는 스웨거가 생성하는 API문서를 사용자 대화방식으로 더 쉽게 만들어주는 내장 솔루션이다.

5.1. 스프링폭스의 스웨거 UI 사용하기 Enabling Springfox’s Swagger UI

스웨거 UI를 사용하려면, 하나의 추가적인 메이븐 의존성 필요하다.:

1
2
3
4
5
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.2.2</version>
</dependency>

이제 사용자는 브라우저에서 다음의 링크에 접속하여 테스트할 수 있다 - http://localhost:8080/your-app-root/swagger-ui.html

다음과 같은 페이지가 보여질 것이다:

Screenshot_1

5.2. 스웨거 문서 둘러보기 Exploring Swagger Documentation

이제 사용자의 어플리케이션에 정의된 모든 컨트롤러의 리스트가 보여질 것이다. 그들중 하나를 클릭하면 유효한 HTTP 메소드 (DELETEGETHEADOPTIONSPATCH,POSTPUT)를 리스트해준다. 

각 메소드를 확장해보면 응답상태response status, 컨탠트타입content-type 그리고 파라메터 리스트와 같은 추가적인 유용한 데이터를 제공해준다. 또한 UI를 통해 각각의 메소드를 테스트할 수있다.

사용자의 코드와 동기화 되어 있는 스웨거의 능력은 매우 중요하다. 이를 증명하기 위해, 새 컨트롤러를 어플리케이션에 추가해보자:

1
2
3
4
5
6
7
8
@RestController
public class CustomController {
 
    @RequestMapping(value = "/custom", method = RequestMethod.POST)
    public String custom() {
        return "custom";
    }
}

이제 스웨거 문서를 새로고침해보면, 컨트롤러 리스트에서 custom-controller 를 볼 수 있을 것이다. 위에서 만든것처럼 POST 메소드 하나만 보여질 것이다.

6. 고급 설정 Advanced Configuration

사용자 어플리케이션의 Docket 빈에서 사용자가 API 문서를 생성하는데 더 많은 설정이 가능하다.

6.1. 스웨거의 응답코드에 API 필터링하기 Filtering API for Swagger’s Response

사용자 API 전체를 문서화하는 것은 보통 바람직한 방법은 아니다. 사용자는 Docket 클래스의 apis() 와paths() 에 파라메터를 넣어줌으로서 스웨거의 응답코드를 제한할 수 있다.

위에서 봤듯이 RequestHandlerSelectors 는 any or none 서술을 허용하지만  base package, class annotation, 그리고 method annotations에 기초하여 API 를 필터링할 수도 있다.

PathSelectors 는 사용자 어플리케이션의 요청경로request paths를 스캔하는 서술문들 통해 추가적인 필터링을 제공해주는데 any()none(), regex(), 또는 ant() 를 사용할 수 있다.

아래의 예제에서 우리는 ant() 서술어를 사용하여 스웨거가 특정 패키지, 특정 경로에 있는 컨트롤러만 포함하도록 하였다.

1
2
3
4
5
6
7
8
@Bean
public Docket api() {               
    return new Docket(DocumentationType.SWAGGER_2)         
      .select()                                      
      .apis(RequestHandlerSelectors.basePackage("org.baeldung.web.controller"))
      .paths(PathSelectors.ant("/foos/*"))                    
      .build();
}

6.2. 커스텀 정보 Custom Information

스웨거는 또한 응답코드를 사용자가  “Api Documentation”, “Created by Contact Email”, “Apache 2.0” 와 같은 정보를 커스터마이즈 할 수 있는 몇가지 기본값을 제공해준다.

이 값들을 수정하려면 apiInfo(ApiInfo apiInfo) 메소드를 사용하면 된다. ApiInfo 클래스는 API에 대한 커스텀 정보를 포함하고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Bean
public Docket api() {               
    return new Docket(DocumentationType.SWAGGER_2)         
      .select()
      .apis(RequestHandlerSelectors.basePackage("com.example.controller"))
      .paths(PathSelectors.ant("/foos/*"))
      .build()
      .apiInfo(apiInfo());
}
 
private ApiInfo apiInfo() {
    ApiInfo apiInfo = new ApiInfo(
      "My REST API",
      "Some custom description of API.",
      "API TOS",
      "Terms of service",
      "myeaddress@company.com",
      "License of API",
      "API license URL");
    return apiInfo;
}

6.3. 커스텀 메소드 응답 메세지 Custom Methods Response Messages

스웨거는 Docket의 globalResponseMessage() 메소드를 통해 HTTP 메소드의 전역적 오버라이딩 응답메세지 globally overriding response messages of HTTP methods 를 허용한다. 먼저 스웨거가 기본 응답메시지를 쓰지않도록 설정해주어야한다.

모든 GET 메소드에 대한 500 와 403 응답 메시지를 오버라이드하려면, Docket 의 초기화 블럭에 약간의 코드를 추가해주어야한다. (초기화 코드는 여기서 제외하였다):

1
2
3
4
5
6
7
8
9
10
11
.useDefaultResponseMessages(false)                                  
.globalResponseMessage(RequestMethod.GET,                    
  newArrayList(new ResponseMessageBuilder()  
    .code(500)
    .message("500 message")
    .responseModel(new ModelRef("Error"))
    .build(),
    new ResponseMessageBuilder()
      .code(403)
      .message("Forbidden!")
      .build()));

Screenshot_2

7. 결론 Conclusion

이 튜토리얼에서 우리는 스프링 REST API의 문서를 생성해주는 스웨거2를 설정하였다. 또한 스웨거의 산출물을 시각화하고 커스터마이즈하는 방법들을 알아보았다.

이 튜토리얼의 전체 구현은 다음의 github 프로젝트에서 확인할 수 있다 – 이클리스 기반이며 임포트해서 쉽게 돌려볼 수 있다.


반응형

반응형

스웨거 2.0으로 스프링 부트 어플리케이션 API 문서화하기

Usage of Swagger 2.0 in Spring Boot Applications to document APIs

(원문링크: http://heidloff.net/article/usage-of-swagger-2-0-in-spring-boot-applications-to-document-apis/)

IBM VP인 엔젤 디아즈(Angel Diaz)는 SearchCloudComputing의 인터뷰에서 "스웨거Swagger는 대부분의 개발자가 [REST] API를 그려내는 방식"이라고 인용하였다. 2.0버젼에서 확장성과 같은 많은 중요한 기능이 추가되었고 큰 커뮤니티와 많은 개발자들이 이제 이것을 사용하고 있다. 추가적으로 Open API Initiative의 일부로서 리눅스 재단하에 스웨거의 명세서가 열린 정부 모델로서 만들어지고 있다.

내가 스웨거에 대해 가장 좋아하는 부분은 (자바) 소스코드내에서 어노테이션을 통해 직접 API를 문서화할 수 있다는 것이다. 이를 통해 문서와 실제 API는 항상 동기화되어있다.

IBM은 몇몇의 제품과 서비스의 API 문서화를 위해 내부적으로 스웨거를 사용한다. 더나아가 IBM Bluemix의 API관리서비스가 당신의 API를 손쉽게 관리하기 위해 스웨거 2.0 정의를 가지고 온다.

자바 스프링 프레임워크는 엔터프라이즈 커뮤니티안에 수많은 견인장치들을 가지고 있다. 스프링 부트 어플리케이션에서 스웨거를 사용하려고 나는 스프링폭스Springfox를 썼는데 이는 스웨거 코어의 일부는 아니지만 스프링안에서 아주 나이스하게 통합되어있으며 코어 스웨거 어노테이션을 지원한다.

아래는 스웨거 어노테이션을 사용하여 스프링 예제인 Building a RESTful Web Service를 확장한 것이다.

먼저 당신은 스프링폭스와 스웨거 라이브러리 의존성을 정의해야한다. 아래의 경우 메이븐을 사용하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?xml version="1.0" encoding="UTF-8"?>
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>org.springframework</groupId>
    <artifactId>gs-rest-service</artifactId>
    <version>0.1.0</version>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.3.0.RELEASE</version>
    </parent>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.2.2</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.2.2</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
 
    <properties>
        <java.version>1.8</java.version>
    </properties>
     
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
 
    <repositories>
        <repository>
            <id>spring-releases</id>
            <url>https://repo.spring.io/libs-release</url>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-releases</id>
            <url>https://repo.spring.io/libs-release</url>
        </pluginRepository>
    </pluginRepositories>
</project>

그 다음 스프링 부트 어플리케이션에 스웨거를 enable하였다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package hello;
 
import static springfox.documentation.builders.PathSelectors.regex;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import com.google.common.base.Predicate;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
 
@SpringBootApplication
@EnableSwagger2
@ComponentScan("hello")
public class Application {
 
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
     
    @Bean
    public Docket newsApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("greetings")
                .apiInfo(apiInfo())
                .select()
                .paths(regex("/greeting.*"))
                .build();
    }
     
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("Spring REST Sample with Swagger")
                .description("Spring REST Sample with Swagger")
                .termsOfServiceUrl("http://www-03.ibm.com/software/sla/sladb.nsf/sla/bm?Open")
                .contact("Niklas Heidloff")
                .license("Apache License Version 2.0")
                .licenseUrl("https://github.com/IBM-Bluemix/news-aggregator/blob/master/LICENSE")
                .version("2.0")
                .build();
    }
}

컨트롤러에서는 API 문서화를 위해 스웨거 어노테이션을 사용하였다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package hello;
 
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
 
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import io.swagger.annotations.ResponseHeader;
 
@RestController
public class GreetingController {
 
    private static final String template = "Hello, %s!";
    private final AtomicLong counter = new AtomicLong();
 
    @ApiOperation(value = "getGreeting", nickname = "getGreeting")
    @RequestMapping(method = RequestMethod.GET, path="/greeting", produces = "application/json")
    @ApiImplicitParams({
        @ApiImplicitParam(name = "name", value = "User's name", required = false, dataType = "string", paramType = "query", defaultValue="Niklas")
      })
    @ApiResponses(value = {
            @ApiResponse(code = 200, message = "Success", response = Greeting.class),
            @ApiResponse(code = 401, message = "Unauthorized"),
            @ApiResponse(code = 403, message = "Forbidden"),
            @ApiResponse(code = 404, message = "Not Found"),
            @ApiResponse(code = 500, message = "Failure")})
    public Greeting greeting(@RequestParam(value="name", defaultValue="World") String name) {
        return new Greeting(counter.incrementAndGet(),
                String.format(template, name));
    }
}

이 예제 리소스에서 파라메터들을 문서화하였다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package hello;
 
import com.fasterxml.jackson.annotation.JsonProperty;
 
import io.swagger.annotations.ApiModelProperty;
 
public class Greeting {
 
    private final long id;
    private final String content;
 
    public Greeting(long id, String content) {
        this.id = id;
        this.content = content;
    }
 
    public long getId() {
        return id;
    }
 
    @JsonProperty(required = true)
    @ApiModelProperty(notes = "The name of the user", required = true)
    public String getContent() {
        return content;
    }
}

이제 API explorer에서 결과물을 보자:

swaggerspring


반응형

'API Documentation' 카테고리의 다른 글

Spring REST Docs v1.0.1 레퍼런스  (1) 2016.03.05
Spring REST API에 Swagger 2 설정하기  (1) 2016.03.05

+ Recent posts