반응형

Global Exception Handling for Spring-Boot Application Using @ControllerAdvice


@ControllerAdvice
public class GlobalExceptionHandling {
    protected Logger logger;

   public GlobalExceptionHandling() {
        logger = LoggerFactory.getLogger(getClass());
   }

    @ResponseBody
    public ResponseEntity<?> handleUnauthenticationException(Exception e) {
        return errorResponse(e, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler({DataIntegrityViolationException.class, SQLIntegrityConstraintViolationException.class})
   @ResponseBody
    public ResponseEntity<?> handleConflictException(Exception e) {
        return errorResponse(e, HttpStatus.CONFLICT);
    }

    @ExceptionHandler({ SQLException.class, DataAccessException.class, RuntimeException.class })
    @ResponseBody
    public ResponseEntity<?> handleSQLException(Exception e) {
        return errorResponse(e, HttpStatus.INTERNAL_SERVER_ERROR);
    }

  @ExceptionHandler({ IOException.class, ParseException.class, ProcessingException.class, JsonParseException.class, JsonMappingException.class })
 @ResponseBody
  public ResponseEntity<?> handleParseException(Exception e) {
        return errorResponse(e, HttpStatus.BAD_REQUEST);
  }

   @ExceptionHandler({ InvalidKeyException.class, NoSuchAlgorithmException.class })
  @ResponseBody
      public ResponseEntity<?> handleHashException(Exception e) {
        return errorResponse(new Exception("Encrypt/Decrypt key is requested"), HttpStatus.LOCKED);
    }

    @ExceptionHandler({ Exception.class })
    @ResponseBody
    public ResponseEntity<?> handleAnyException(Exception e) {
        return errorResponse(e, HttpStatus.INTERNAL_SERVER_ERROR);
    }
   
    protected ResponseEntity<ExceptionMessage> errorResponse(Throwable throwable,
            HttpStatus status) {
        if (null != throwable) {
            return response(new ExceptionMessage(throwable), status);
        } else {
            return response(null, status);
        }
    }

    protected <T> ResponseEntity<T> response(T body, HttpStatus status) {
        return new ResponseEntity<T>(body, new HttpHeaders(), status);
    }
}

반응형

반응형

스프링 REST Docs

버전 1.0.1.RELEASE



스프링 MVC Test를 통해 자동생성된 코드와 손으로 쓰여진 문서를 합쳐서 REST 서비스를 문서화하기

소개 Introduction

스프링 REST Docs의 목표는 당신의 RESTful 서비스를 정확하고 읽기 편하게 문서화하는 것을 돕는 것이다.

높은 수준의 문서화를 하는 것은 어렵다. 이 작업에 잘 맞는 툴을 사용하는 데 있어서의 어려움을 편리하게 하는 하나의 방법의 끝으로 스프링 REST Docs는 Asciidoctor를 사용한다. 아스키닥터는 평문을 처리하여 당신의 필요에 맞는 스타일과 레이어를 적용한 HTML를 만들어준다.

스프링 REST Docs는 Spring MVC Test를 위해 쓰여진 테스트를 통해 만들어진 코드 조각들을 사용한다. 이 테스트 기반의 접근법test-driven approach은 당신의 서비스에 대한 문서화의 정확도를 보장해준다. 코드 조각이 올바르지 않다면 결과물 생성에 실패할 것이다.

하나의 RESTful서비스를 문서화하는 것은 주로 그 리소스들을 상세화하는 것이다. 각 리소스의 상세화에 두가지 주요 부분이 있는데 각각 그것이 소비하는 HTTP 요청Request의 디테일과 그 후 생성되는 HTTP 응답Response이다. 스프링 REST Docs는 당신이 이들 리소스와 HTTP요청과 응답 그리고 당신의 서비스의 구현체의 내부 디테일로 부터 문서화를 보호하는 등등의 작업할 수 있게 만들어준다. 이 분리된 방식은 당신으로 하여금 서비스의 구현체보다 당신의 서비스의 API를 문서화할수 있도록 도와준다. 또한 문서화의 재작업없이 구현체를 계속 추가/변경할 수 있다.

시작하기 Getting started

이 섹션은 스프링 REST Docs를 시작하는 법을 기술한다.

 샘플 어플리케이션 Sample applications

바로 돌려볼 수 있는 두가지 샘플 어플리케이션이 있다. 하나의 샘플은 Spring HATEOAS를, 다른 하나는 Spring Data REST를 사용하였다.  둘 다 스프링 REST Docs를 사용하여 구체적인 API가이드를 만든다. 하나씩 둘러보자.

각 샘플은  API가이드를 만들어내는 api-guide.adoc라는 이름의 파일을 하나 가지고 있으며 getting-started-guide.adoc 라는 이름의 파일은 초기에 따라할 수 있도록 시작하기 가이드를 만들어낸다.

생성된 코드조각은 src/test/java에서 확인할 수 있다. ApiDocumentation.java 는 API가이드를 위한 코드조각을 만든다.  GettingStartedDocumentation.java 는 시작하기 가이드를 위한 코드조각을 만든다.

빌드 설정 Build configuration

스프링 REST Docs를 사용하는 첫걸음은 당신의 프로젝트 빌드를 설정하는 것이다.

그래들 빌드 설정 Gradle build configuration

Spring HATEOAS 샘플build.gradle 파일을 포함하고 있는데 당신이 이를 레퍼런스로 사용할 수 있다. 이 설정의 주요 파트는 아래에 묘사해두었다:

plugins { 1
    id "org.asciidoctor.convert" version "1.5.2"
}

dependencies { 2
    testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc:1.0.1.RELEASE'
}

ext { 3
    snippetsDir = file('build/generated-snippets')
}

test { 4
    outputs.dir snippetsDir
}

asciidoctor { 5
    attributes 'snippets': snippetsDir 6
    inputs.dir snippetsDir 7
    dependsOn test 8
}
1아스키닥터 플러그인을 적용.
2testCompile설정에 spring-restdocs-mockmvc 의존성 추가.
3

코드조각이 생성되는 결과output 폴더를 프로퍼티에 설정.

4

하나의 결과물로서 코드조각 디렉토리를 추가하는 test task 를 설정.

5

asciidoctor task를 설정

6

당신의 문서에 생성된 코드조각을 포함할 때 사용될 속성값인 snippets 정의하기.

7

입력값input으로 snippets 디렉토리 설정하기.

8

test task에 의존하는 task를 만들어 문서가 생성되기전에 테스트를 돌릴 수 있게함.

문서 패키징하기 Packaging the documentation

아마도 당신은 프로젝트 jar파일안에 생성된 문서를 패키지하고 싶을 것이다. 예를 들면, 스프링 부트에 의해  정적인 컨텐트로서 제공하기  같이. asciidoctor task 에 의존하는 jar task 를 설정하고 jar의 정적인 디렉토리에 생성된 문서를 복사함으로서 이것이 가능하다:

jar {
    dependsOn asciidoctor
    from ("${asciidoctor.outputDir}/html5") {
        into 'static/docs'
    }
}

메이븐 빌드 설정 Maven build configuration

Spring Data REST 샘플은 pom.xml 파일을 포함하고 있어 레퍼런스로 활용이 가능할 것이다. 이 설정의 주요 파트는 아래에 묘사해두었다:

<dependency> 1
    <groupId>org.springframework.restdocs</groupId>
    <artifactId>spring-restdocs-mockmvc</artifactId>
    <version>1.0.1.RELEASE</version>
    <scope>test</scope>
</dependency>

<properties> 2
    <snippetsDirectory>${project.build.directory}/generated-snippets</snippetsDirectory>
</properties>

<build>
    <plugins>
        <plugin> 3
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <configuration>
                <includes>
                    <include>**/*Documentation.java</include>
                </includes>
            </configuration>
        </plugin>
        <plugin> 4
            <groupId>org.asciidoctor</groupId>
            <artifactId>asciidoctor-maven-plugin</artifactId>
            <version>1.5.2</version>
            <executions>
                <execution>
                    <id>generate-docs</id>
                    <phase>package</phase> 6
                    <goals>
                        <goal>process-asciidoc</goal>
                    </goals>
                    <configuration>
                        <backend>html</backend>
                        <doctype>book</doctype>
                        <attributes>
                            <snippets>${snippetsDirectory}</snippets> 5
                        </attributes>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>
1test scope 에 spring-restdocs-mockmvc 의존성 추가하기.
2

생성된 코드조각을 위한 결과 위치output location를 프로퍼티에 설정하기.

3

SureFire 플러그인을 추가하고 Documentation.java로 끝나는 이름을 가진 파일들을 포함하도록 설정하기.

4

Asciidoctor 플로그인 추가하기

5

당신의 문서에 생성된 코드조각을 포함할때 사용될 속성값인 snippets 정의하기.

6

프로젝트의 jar에 문서를 패키지하려면, prepare-package 구문을 사용해야한다.

문서 패키징하기 Packaging the documentation

아마도 당신을, 이를테면, 스프링 부트에 의해  정적인 컨텐트로서 제공하기와 같이 프로젝트 jar파일안에 생성된 문서를 패키지하고 싶을 것이다. 

먼저 이를 위해 위에서 언급한것처럼 prepare-package구문으로 실행하기 위해 아스키닥터 플러그인을 설정해야한다. 이제 메이븐의 리소스 플러그인을 설정하여 생성된 문서를 프로젝트의 jar의 어디에 포함시킬지 설정해보자:

<plugin> 1
    <groupId>org.asciidoctor</groupId>
    <artifactId>asciidoctor-maven-plugin</artifactId>
    <!-- … -->
</plugin>
<plugin> 2
    <artifactId>maven-resources-plugin</artifactId>
    <version>2.7</version>
    <executions>
        <execution>
            <id>copy-resources</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>copy-resources</goal>
            </goals>
            <configuration>
                <outputDirectory>
                    ${project.build.outputDirectory}/static/docs
                </outputDirectory>
                <resources>
                    <resource>
                        <directory>
                            ${project.build.directory}/generated-docs
                        </directory>
                    </resource>
                </resources>
            </configuration>
        </execution>
    </executions>
</plugin>
1

아스키닥터 플러그인을 쓰기위한 선언.

2

리소스 플러그인은 아스키닥터 플러그인 이후에 선언해주어야한다. 이들은 같은 구문 (prepare-package)을 가르키는데 리소스 플러그인이 아스키닥터 플러그인 이후에 실행되어야하기 때문이다.

 문서화 조각 생성하기 Generating documentation snippets

스프링 REST Docs는 당신이 문서화하려는 서비스를 Spring’s MVC Test framework를 통해 접근한다. 그 후 요청되 결과 응답을 위한 문서 코드조각을 생성한다.

Spring MVC 테스트 설정하기 Setting up Spring MVC test

문서 코드조각을 생성하는 첫걸음은 JUnit @Rule로 어토테이션된 public RestDocumentation 필드를 선언하는 것이다. RestDocumentation 룰은 생성된 코드조각이 쓰여져야하는 결과 디렉토리를 설정한다. 이 결과 디렉토리output directory는 당신이 build.gradle 또는 pom.xml파일에 설정해둔 코드조각 디렉토리snippets directory와 일치해야 한다.

메이븐에선 다음과 같다: (pom.xml은 보통 target/generated-snippets로 설정한다)

@Rule
public RestDocumentation restDocumentation = new RestDocumentation("target/generated-snippets");

그레들에선 다음과 같다: (build.gradl, 보통 build/generated-snippets를 사용한다)

@Rule
public RestDocumentation restDocumentation = new RestDocumentation("build/generated-snippets");

다음으로, MockMvc 인스턴스를 만드는 @Before 메소드를 제공한다: 

@Autowired
private WebApplicationContext context;

private MockMvc mockMvc;

@Before
public void setUp() {
    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
            .apply(documentationConfiguration(this.restDocumentation))
            .build();
}

MockMvc인스턴스는 RestDocumentationMockMvcConfigurer를 사용하여 설정한다. 이 클래스의 인스턴스는 org.springframework.restdocs.mockmvc.MockMvcRestDocumentation의  documentationConfiguration() 메소드로 부터 받아온다. RestDocumentationMockMvcConfigurera는 민감한 기본값들을 적용하며, 또한 설정을 커스터마이징하기위한 API를 제공해준다. 더 자세한 정보는 빌드설정 섹션을 참고하자.

RESTful 서비스 호출하기 Invoking the RESTful service

이제 MockMvc 인스턴스가 생성되었으니, RESTful서비스를 호출하고 요청과 응답을 문서화하는데 사용할 수 있다. 예를 들면:

this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) 1
    .andExpect(status().isOk()) 2
    .andDo(document("index")); 3
1

서비스의 루트 (/) 를 호출하고 응답이 application/json이어야 한다고 알려준다.

2

서비스가 만들어내는 원하는 응답을 결정한다.

3

결과 디렉토리로 설정된 위치에 index 라는 이름의 디렉토리에 코드조각을 만듦으로서 서비스로의 호출을 문서화한다. 이 코드조각들은 RestDocumentationResultHandler에 의해 만들어진다. 이 클래스의 인스턴스는 org.springframework.restdocs.mockmvc.MockMvcRestDocumentation의 정적인 document 메소드로 부터 얻어와 진다.

기본값으로, 3가지 코드조각이 만들어진다:

  • <output-directory>/index/curl-request.adoc

  • <output-directory>/index/http-request.adoc

  • <output-directory>/index/http-response.adoc

스프링 REST Docs에 의해 만들어지는 코드조각에 대한 더 자세한 정보는 아래의 당신의 API 문서화 하기를 참고하자.

 코드조각 사용하기 Using the snippets

생성된 코드조각들은 include macro를 사용하여 당신의 문서에 포함시킬 수 있다.  snippets속성은 빌드설정 섹션에서 구체적으로 알 수 있다. 예를 들면:

include::{snippets}/index/curl-request.adoc[]

당신의 API 문서화하기 Documenting your API

이 섹션에서 Spring REST Docs를 사용하여 당신의 API를 문서화하는 더 자세한 정보를 제공한다.

 하이퍼미디어 Hypermedia

Spring REST Docs Hypermedia-based API의 링크들의 문서화를 지언한다.

this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
    .andExpect(status().isOk())
    .andDo(document("index", links( 1
            linkWithRel("alpha").description("Link to the alpha resource"), 2
            linkWithRel("bravo").description("Link to the bravo resource")))); 3
1

응답의 링크를 설명하는 코드 조각을 만들어내도록 Spring REST docs를 설정하기. org.springframework.restdocs.hypermedia.HypermediaDocumentation의 정적인 links 메소드를 사용한다.

2

rel이 alpha인 링크를 예상함.org.springframework.restdocs.hypermedia.HypermediaDocumentation의 정적인 linkWithRel메소드를 사용한다.

3

rel이 bravo인 링크를 예상함.

결과는 리소스의 링크들을 설명하는 하나의 테이블이 들어있는 links.adoc라는 이름의 코드조각이다.

링크들을 문서화 할 때, 응답에 문서화되지않는 링크가 발견되면 테스트는 실패할 것이다. 유사하게 옵션으로 설정되지않은 하나의 문서화된 링크가 응답에 존재하지않으면 테스트 역시 실패될 것이다. 

문서화에 포함하고 싶지않은 어떤 링크가 있다면, 이를 무시하도록ignored 설정할 수 있다. 이는 위에서 설명한 테스트 실패를 피할 수 있도록 생성된 코드조각에 보여지지않을 것이다.

하이퍼미디어 링크 포멧 Hypermedia link formats

두개개의 링크 포멧이 기본값으로 사용된다:

  • Atom – 링크는 links라는 이름의 배열을 예상한다. 응답의 컨텐트 타입이 application/json과 호환될때 기본값으로 사용된다.

  • HAL – 링크는 _links.라는 이름의 맵을 예상한다. 응답의 컨텐트 타입이 application/hal+json과 호환될때 기본값으로 사용된다.

Atom또는 HAL-포멧을 사용중이지만 다른 컨텐트 타입을 가지고 있다면  links를 내장된 LinkExtractor 로 구현함으로서 지원가능하다. 예를 들면: 

.andDo(document("index", links(halLinks(), 1
        linkWithRel("alpha").description("Link to the alpha resource"),
        linkWithRel("bravo").description("Link to the bravo resource"))));
1

링크들이 HAL 포멧이라고 알려준다.org.springframework.restdocs.hypermedia.HypermediaDocumentation의 정적인  

halLinks 메소드를 사용한다.

당신의 API가 Atom 또는 HAL이외의 다른 포멧으로 링크를 나타내고 있다면, 응답에서 링크를 추출하기 위한 LinkExtractor인터페이스를  자체 구현하여 지원할 수 있다.

 요청과 응답의 페이로드 Request and response payloads

위에 언급된 이외의 추가적인 하이퍼미디어 특화된 지원을 위해, 요청과 응답 페이로드의 일반적인 문서화 지원 또한 제공된다. 예를 들어:

this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON))
    .andExpect(status().isOk())
    .andDo(document("index", responseFields( 1
            fieldWithPath("contact").description("The user's contact details"), 2
            fieldWithPath("contact.email").description("The user's email address")))); 3
1

응답 페이로드의 필드를 설명하는 코드조각을 만드는 Spring REST docs 설정. requestFields는 요청을 문서화하는데 사용된다. 둘다 org.springframework.restdocs.payload.PayloadDocumentation의 정적인 메소드이다.

2

contact경로의 필드를 예상한다. org.springframework.restdocs.payload.PayloadDocumentation의 정적인 fieldWithPath 메소드를 사용한다.

3

contact.email경로의 필드를 예상.

결과는 필드들이 설명된 테이블이 포함된 코드조각이다. 요청의 경우 코드조각은 request-fields.adoc라는 이름을 가진다. 응답의 경우 코드조각은response-fields.adoc라는 이름을 가진다.

필드를 문서화할 때, 페이로드안에 문서화되지않은 필드가 발견되면 테스트는 실패할 것이다. 이와 유사하게 문서화된 필드가 페이로드에서 발견되지않고 이 필드가 옵션으로 설정되지않았다면 테스트 역시 실패할 것이다. 계층적 구조 hierarchical structure를 가진 페이로드의 경우 하나의 필드를 문서화 하는 것으로 충분히 그의 모든 하위구조 또한 문서화 되는 것으로 간주될 수 있다.

문서화에 포함하고 싶지않은 어떤 링크가 있다면, 이를 무시하도록ignored 설정할 수 있다. 이는 위에서 설명한 테스트 실패를 피할 수 있도록 생성된 코드조각에 보여지지않을 것이다


기본값으로, Spring REST Docs 는 당신 문서화 하려는 페이로드가 JSON이라고 가정할 것이다. XML 페이로드를 문서화 하려면 요청 또는 응답의 컨텐트 타입이 application/xml와 호환되어야만 한다.

JSON 페이로드 JSON payloads

JSON 필드 경로 JSON field paths

JSON 필드 경로는 괄호bracket나 점dot 명명법을 사용한다. 점Dot 명명법은 예를들어 a.b와 같이 경로의 각 키를 구분하기위해 '.'을 사용한다. 괄호Bracket 명명법은  ['a']['b']와 같이 사각괄호square brackets와 작은따옴표single quotes에 각 키를 넣는다.

[]의 경우 배열을 나타내기 위해 사용된다. 점dot 명명법은 더 간결하지만 괄호bracket 명명법을 쓸 때 ['a.b'] 와 같이 하나의 키값에 점dot을 사용할 수 있다. 같은 경로에 a['b']와 같이 두개의 다른 명명법 사용이 가능하다.

이 JSON페이로드로:

{
    "a":{
        "b":[
            {
                "c":"one"
            },
            {
                "c":"two"
            },
            {
                "d":"three"
            }
        ],
        "e.dot" : "four"
    }
}

다음의 경로들이 표현되었다:

PathValue

a

b를 포함하고 있는 하나의 객체

a.b

3개의 객체를 포함하고 있는 하나의 배열

['a']['b']

3개의 객체를 포함하고 있는 하나의 배열

a['b']

3개의 객체를 포함하고 있는 하나의 배열

['a'].b

3개의 객체를 포함하고 있는 하나의 배열

a.b[]

3개의 객체를 포함하고 있는 하나의 배열

a.b[].c

스트링 one 과 two를 포함하고 있는 하나의 배열

a.b[].d

스트링 three

a['e.dot']

스트링 four

['a']['e.dot']

스트링 four

루트에 배열을 사용하는 응답 또한 문서화할 수 있다. 경로 []는 전체 배열로 참조될 것이다. 그 다음, 배열 전체에서 필드를 식별하기 위해 괄호bracket 또는 점dot 명명법을 사용할 수 있다. 예를 들면,  [].id 는 다음의 배열에서 찾을 수 있는 모든 객체의 id 필드와 상응된다:

[
    {
        "id":1
    },
    {
        "id":2
    }
]
JSON 필드 타입 JSON field types 

하나의 필드가 문서화될 때, Spring REST Docs는 페이로드를 조사함으로서 그 타입을 결정하려고 한다. 7개의 타입이 지원된다:

TypeDescription

array

필드의 각각 존재하는 값이 배열임

boolean

필드에 각각 존재하는 값이  boolean 임 (true 또는 false)

object

필드에 각각 존재하는 값이 객체임

number

필드에 각각 존재하는 값이 숫자임

null

필드에 각각 존재하는 값이 null임

string

필드에 각각 존재하는 값이 문자임

varies

 페이로드에서 서로 다은 다양한 타입을 가진 필드가 여러 번 있음

타입은 또한 FieldDescriptor type(Object) 메소드를 사용하여 명시적으로 설정할 수 있다. Object's toString 메소드로 결과가 문서안에 사용될 것이다. 보통 JsonFieldType에 열거된enumerated 값들중 하나가 사용될 것이다:

.andDo(document("index", responseFields(
        fieldWithPath("contact.email")
                .type(JsonFieldType.STRING) 1
                .optional()
                .description("The user's email address"))));

1

필드의 타입을 string으로 설정함 

XML 페이로드 XML payloads

XML field paths XML field paths
XML 필드 경로는 XPath를 사용하여 설명한다.  / 는 자식노드로 내려가는데 사용된다.
XML 필드 타입 XML field types 

XML페이로드를 문서화할 때, 반드시 FieldDescriptor의 type(Object) 메소드를 사용하여 필드의 타입을 제공해주어야한다. 지원되는 타입의 toString 메소드의 결과값이 문서화에 사용되어질 것이다.

 요청 파라미터들 Request parameters

요청 파라미터들은 requestParameters을 사용하여 문서화할 수 있다. 요청 파라메터들은 GET요청의 쿼리문query string안에 포함할 수 있다. 예를 들어:

this.mockMvc.perform(get("/users?page=2&per_page=100")) 1
    .andExpect(status().isOk())
    .andDo(document("users", requestParameters( 2
            parameterWithName("page").description("The page to retrieve"), 3
            parameterWithName("per_page").description("Entries per page") 4
    )));
1쿼리문에 page 와 per_page 두개의 파라메터를 가지는 GET 요청을 수행한다.
2

요청의 파라미터를 설명하는 코드조각을 만드는 Spring REST Docs 설정. org.springframework.restdocs.request.RequestDocumentation의 정적인 requestParameters 메소드를 사용한다. 

3

page 파라미터를 문서화 함. org.springframework.restdocs.request.RequestDocumentation의 정적인 parameterWithName 메소드를 사용함

4per_page 파라미터를 문서화 함.

요청 파라미터는 또한 POST 요청 바디body의 폼데이터form data를 포함시킬 수 있다.:

this.mockMvc.perform(post("/users").param("username", "Tester")) 1
    .andExpect(status().isCreated())
    .andDo(document("create-user", requestParameters(
            parameterWithName("username").description("The user's username")
    )));
1

단일 파라미터 username를 가지는 POST 요청을 수행함.

두 경우 모두에서 결과는 리소스에 의해 지원되는 파라미터를 설명하는 테이블을 포함하는 request-parameters.adoc이라는 이름의 코드조각이다. 

요청 파라미터를 문서화할 때,  요청에서 사용되지않는 파라미터가 문서화 되어있으면 테스트는 실패할 것이다. 이와 유사하게 문서화된 요청 파라미터가 요청에서 발견되지않으면 또한 실패할 것이다.

어느 요청 파라미터를 문서화하지 않으려면 무시하도록ignored 설정함으로서 위에 묘사된 실패들을 피하도록 생성된 코드조각에 보이는 것을 방지할 것이다.

 경로 파라미터들 Path parameters

요청의 경로 파라미터는 pathParameters를 사용하여 문서화할 수 있다. 예를 들어:

this.mockMvc.perform(get("/locations/{latitude}/{longitude}", 51.5072, 0.1275)) 1
    .andExpect(status().isOk())
    .andDo(document("locations", pathParameters( 2
            parameterWithName("latitude").description("The location's latitude"), 3
            parameterWithName("longitude").description("The location's longitude") 4
    )));
1

latitude 와 longitude 두 개의 파라미터를 가지는 GET 요청을 실행한다.

2

요청의 경로 파라미터를 설명하는 코드조각을 만드는 Spring REST Docs 설정. org.springframework.restdocs.request.RequestDocumentation의 정적인 pathParameters 메소드를 사용한다.

3Document the parameter named latitude라는 이름의 파라미터를 문서화 함. org.springframework.restdocs.request.RequestDocumentation의 정적인 parameterWithName 메소드를 사용한다.
4Document the parameter named longitude라는 이름의 파라미터를 문서화 함.

결과는 리소스에 의해 지원되는 경로 파라미터들을 설명하는 테이블을 가진 path-parameters.adoc 라는 이름의 코드조각이다.

경로 파라미터를 문서에서 이용할 수 있게 만드려면, 요청은 MockMvcRequestBuilders가 아니라 RestDocumentationRequestBuilders의 한 메소드를 사용하여 만들어야 한다. 

경로 파라미터를 문서화할 때, 요청에 사용되었으나 문서화 되지않은 경로가 있으면 테스트는 실패될 것이다. 이와 유사하게, 요청에는 없는데 문서화되어있는 경로 파라미터가 있다면 또한 실패할 것이다.

어떤 경로 파라미터를 문서화에 포함하고 싶지않다면, 무시ignored하도록 설정하여 위에 언급된 실패를 회피하도록 생성되는 코드조각에 보이지 않도록 방지할 수 있다.

HTTP 헤더 HTTP headers

요청이나 응답의 헤더는 각각 requestHeaders and responseHeaders 를 사용하여 문서화할 수 있다. 예를 들면:

this.mockMvc
        .perform(get("/people").header("Authorization", "Basic dXNlcjpzZWNyZXQ=")) 1
        .andExpect(status().isOk())
        .andDo(document("headers", requestHeaders( 2
                        headerWithName("Authorization").description(
                                "Basic auth credentials")), 3
                responseHeaders( 4
                        headerWithName("X-RateLimit-Limit").description(
                                "The total number of requests permitted per period"),
                        headerWithName("X-RateLimit-Remaining").description(
                                "Remaining requests permitted in current period"),
                        headerWithName("X-RateLimit-Reset").description(
                                "Time at which the rate limit period will reset"))));
1

기본 인증basic authentication에 사용되는 Authorization 헤더를 가지는 GET요청을 수행함 

2

요청의 헤더를 설명하는 코드조각을 만드는 Spring REST Docs 설정. org.springframework.restdocs.headers.HeaderDocumentation의 정적인requestHeaders메소드를 사용함.

3Authorization 헤더를 문서화함 org.springframework.restdocs.headers.HeaderDocumentation의 정적인headerWithName 메소드 사용.
4

응답의 헤더를 설명하는 코드조각을 만듦. org.springframework.restdocs.headers.HeaderDocumentation의 정적인 responseHeaders 메소드를 사용.

결과는 request-headers.adocresponse-headers.adoc라는 이름의 코드조각이다. 각각 헤더를 설명하는 테이블을 가지고 있다.

HTTP헤더를 문서화할 때, 문서화된 헤더를 요청이나 응답에서 찾을 수 없다면 테스트는 실패할 것이다.


제약사항 문서화하기 Documenting constraints

Spring REST Docs 는 문서의 제약사항을 도와주는 많은 클래스들을 제공한다. ConstraintDescriptions의 인스턴스는 클래스의 제약사항에 대한 설명에 접근하는데 쓴다. 예를 들어:

public void example() {
    ConstraintDescriptions userConstraints = new ConstraintDescriptions(UserInput.class); 1
    List<String> descriptions = userConstraints.descriptionsForProperty("name"); 2
}

static class UserInput {

    @NotNull
    @Size(min = 1)
    String name;

    @NotNull
    @Size(min = 8)
    String password;
}
1UserInput 클래스를 위한 ConstraintDescriptions 인스턴스를 만든다
2

Get이름 프로퍼티의 제약사항의 설명을 가져옴. 이 리스트는 두개의 설명을 가지고 있는데; 하나는 NotNull제약사항이고 다른 하나는 Size 제약사항이다.

 Spring HATEOAS 샘플에서 ApiDocumentation 클래스는 이 함수가 어떻게 동작하는 지 보여준다.

제약사항 찾기 Finding constraints

기본값으로 제약사항들은 Bean Validation을 사용하여 찾는다. 현재는 오직 프로퍼티 제약사항만 지원한다. 당신은 커스텀 ValidatorConstraintResolver 인스턴스로 ConstraintDescriptions를 만드는 데 사용되는 Validator를 커스터마이즈할 수 있다. 제약사항을 완전히 제어하려면, 사용하는 ConstraintResolver를 스스로 구현하면 된다.

제약사항 기술하기 Describing constraints

기본 설명값들descriptions은 Bean Validation 1.1의 제약사항에서 지원하는 전체 리스트는 다음과 같다:

  • AssertFalse

  • AssertTrue

  • DecimalMax

  • DecimalMin

  • Digits

  • Future

  • Max

  • Min

  • NotNull

  • Null

  • Past

  • Pattern

  • Size

기본 설명값들descriptions을 오버라이드 하거나 새로운 설명을 제공하려면 org.springframework.restdocs.constraints.ConstraintDescriptions. 의 기본이름을 가지는 리소스 번들을 만들어야한다. Spring HATEOAS-based 샘플은 이러한 리소스 번들 예제를 포함하고 있다.

리소스 번들의 각각의 키는 제약사항의 완전한 이름의 뒤에 .description을 더한 것이다. 예를 들어, 표준의 @NotNull 제약사항의 키는  javax.validation.constraints.NotNull.description이다.

프로퍼티 placeholder은 제약사항의 설명description에서 사용된 속성값을 참조한다. 예를 들어, @Min 제약사항의 기본 설명default descripton인 Must be at least ${value}는 제약사항의 value 속성을 참조한다.

제약사항 설명의 더 많은 제어를 위해서 커스텀 ResourceBundleConstraintDescriptionResolver를 가지고 ConstraintDescriptions를 만들어야 한다. 완전한 제어를 하러면, 커스텀 ConstraintDescriptionResolver구현으로 ConstraintDescriptions를 만들어야 한다. 

생성된 코드조각에서 제약사항 기술하기 Using constraint descriptions in generated snippets

일단 제약사항의 설명을 가지고 있다면, 이들을 마음놓고 사용할 수 있다. 하지만 당신은 생성된 코드조각을 선호할 것이다. 예를 들어, 제약사항 설명을 필드의 설명의 일부로서 포함시키길 원할 수 있다. 또는 요청 필드 코드조각의 추가 정보로서 제약사항을 포함할 수 있다.  Spring HATEOAS-based 샘플의 ApiDocumentation 클래스에서 후자의 접근법을 확인할 수 있다.


기본 코드조각들 Default snippets

당신이 MockMvc.perform을 호출하여 문서화 할 때 수많은 코드조각들이 자동으로 만들어진다:

SnippetDescription

curl-request.adoc

문서화를 위한  MockMvc 호출과 동일한 curl 명령어를 포함한다.

http-request.adoc

문서화를 위한 MockMvc 호출과 동일한 HTTP 요청을 포함한다.

http-response.adoc

리턴되는 HTTP응답을 포함한다.

당신은 어떤 코드조각을 기본값으로 만들지 설정할 수 있다. 자세한 정보는 설정 섹션을 보자.

 파라메터화된 결과 디렉토리 사용하기 Using parameterized output directories

document 에 의해 사용되는 결과 디렉토리는 파라미터화할 수 있다. 다음의 파라미터들이 지원된다:

ParameterDescription

{methodName}

테스트 메소드의 변경이 불가능한 이름

{method-name}

 kebab-case를 사용하여 포멧된, 테스트 메소드의 이름

{method_name}

 snake_case를 사용해 포멧된 테스트 메소드의 이름

{ClassName}

테스트 클래스의 변경이 불가능한 이름

{class-name}

kebab-case를 사용하여 포멧된 테스트 클래스의 이름

{class_name}

snake_case를 사용하여 포맷된 테스트 클래스의 이름

{step}

현재 테스트의  MockMvc.perform를 호출한 수

예를 들어,  테스트 클래스 GettingStartedDocumentation creatingANote 라는 이름의 테스트 메소드의 document("{class-name}/{method-name}") 는  getting-started-documentation/creating-a-note.라는 이름의 디렉토리에 코드조각을 만들것이다.

파라미터화된 결과 디렉토리는 특히 스프링 MVC 테스트의 alwaysDo기능과 같이 쓸 때 매우 유용하다. 이는 설정 메소드안에 문서화의 설정을 가능하게 해준다:

@Before
public void setUp() {
    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
            .apply(documentationConfiguration(this.restDocumentation))
            .alwaysDo(document("{method-name}/{step}/"))
            .build();
}

여기 설정된 값으로 MockMvc.perform 으로의 모든 호출은 더 많은 설정없이도 default snippets을 만들어낼 것이다. 이 함수가 어떻게 동작하는지 보려면 각각 샘플 어플리케이션에 있는 GettingStartedDocumentation 클래스를 보자.

 결과값 커스터마이징하기 Customizing the output

생성된 코드조작 커스터마이징하기 Customizing the generated snippets

Spring REST Docs는 Mustache 템플릿을 사용하여 생성된 코드조작을 만든다. 기본 템플릿은 스프링 REST Docs가 만들어내는 각각의 코드조각를 제공한다. 코드조각의 내용을 커스터마이즈하기위해 당신 자신만의 템플릿을 사용할 수 있다.

템플릿들은 org.springframework.restdocs.templates 패키지의 클래스 패스로부터 불러와지며 각각의 템플릿은 코드조각후에 이름이 지어지고 만들어낼 것이다. 예를 들어, curl-request.adoc코드조각을 위한 템플릿을 오버라이드 하려면 src/test/resources/org/springframework/restdocs/templates에 curl-request.snippet 이라는 이름의 템플릿을 만들어야한다.

추가정보 포함하기 Including extra information

생성된 코드조각을 포함할 때 추가 정보는 제공하는 두가지 방법이 있다:

  1. 하나 이상의 속성의 설명자descriptor에  attributes 메소드를 사용한다.

  2. curlRequesthttpRequesthttpResponse등등을 호출할 때 몇가지 속성값을 넘긴다. 이러한 속성값들은 전체 코드조각에 할당될 것이다.

추가적인 속성값을 템플릿이 처리되는 과정에서 이용할 수 있다. 커스텀 코드조각 템플릿들을 함께 엮어서 생성된 코드조각에 추가정보는 넣는것이 가능해진다.

위의 설명의 간단한 예로 요청 필드를 문서화할 때 제약사항 컬럼과 타이틀을 추가해보자. 이를 위한 첫번째 스텝은 당신이 문서화하려는 각각의 필드에  constraints 속성을 제공하고 title 속성을 제공하는 것이다:

A concrete example of the above is the addition of a constraints column and a title when documenting request fields. The first step is to provide a constraints attribute for each field that you are documenting and to provide a title attribute:

.andDo(document("create-user", requestFields(
        attributes(
                key("title").value("Fields for user creation")), 1
        fieldWithPath("name")
                .description("The user's name")
                .attributes(
                        key("constraints").value("Must not be null. Must not be empty")), 2
        fieldWithPath("email")
                .description("The user's email address")
                .attributes(
                        key("constraints").value("Must be a valid email address"))))); 3
1

요청 필드의 코드조각을 위한 title 속성을 설정

2name 필드를 위한 constraints 속성을 설정 
3

email 필드를 위한 constraints 속성을 설정

두번째 절차는 생성된 코드조각의 테이블의 필드 제약사항와 타이틀을 추가하기 위한 정보를 포함하는 request-fields.snippet 이름의 커스텀 템플릿을 제공해주는 것이다:

.{{title}} 1
|===
|Path|Type|Description|Constraints 2

{{#fields}}
|{{path}}
|{{type}}
|{{description}}
|{{constraints}} 3

{{/fields}}
|===
1

테이블에 title 추가하기

2

"Constraints" 이라는 이름의 새 컬럼 추가

3

테이블의 각 열에 설명자descriptor의 constraints 속성을 포함시키기

요청과 응답 커스터마이징하기 Customizing requests and responses

당신이 정확히 보낸 어떠한 요청과, 어떠한 받은 요청을 문서화 하고 싶지않는 상황이 있을 수 있다. Spring REST Docs는 많은 선행처리자들preprocessors을 제공하여 요청과 응답을 문서화하기 전 단계에서 변경할 수 있다.

선행처리Preprocessing는 OperationRequestPreprocessor와/또는OperationResponsePreprocessor를 가진 document을 호출함으로서 설정할 수 있다. 인스턴스들은 Preprocessors의 정적인 preprocessRequestpreprocessResponse를 사용함으로서 받아올 수 있다. 예를 들어:

this.mockMvc.perform(get("/"))
    .andExpect(status().isOk())
    .andDo(document("index",
            preprocessRequest(removeHeaders("Foo")), 1
            preprocessResponse(prettyPrint()))); 2
1Foo라는 이름의 헤더를 지우는 요청 선행처리자를 적용함.
2

응답의 내용을 이쁘게 출력pretty print하게 하는 응답 선행처리자를 적용함.

다른 방법으로, @Before 메소드의 선행처리자를 설정하고 파라미터화한 출력 디렉토리 지원을 사용함으로서, 모든 테스트에 똑같은 선행처리자를 적용할 수 있다.

@Before
public void setup() {
     this.document = document("{method-name}", 1
             preprocessRequest(removeHeaders("Foo")),
             preprocessResponse(prettyPrint()));
     this.mockMvc = MockMvcBuilders
             .webAppContextSetup(this.context)
             .alwaysDo(this.document) 2
             .build();
}
1

요청과 응답 선생처리를 위한 설정하는 RestDocumentationResultHandler 만들기.

2

문서화 결과 처리자result handler를 항상 호출하도록 설정하는 MockMvc 인스턴스 만들기

그 후 각각의 테스트에서 RestDocumentationResultHandler는 어떠한 특정 목적의 테스트를 위한 설정을 할 수 있다. 예를들어:

this.document.snippets( 1
        links(linkWithRel("self").description("Canonical self link")));
this.mockMvc.perform(get("/")) 2
    .andExpect(status().isOk());
1

테스트되는 리소스를 구체화한 링크를 문서화함. Document the links specific to the resource that is being tested

2

위에 사용된 alwaysDo의 사용때문에 perform 호출은 자동적으로 문서화 코드조각을 만들어낼 것이다.

위에 언급된 것을 포함한 다양한 내장 선행처리자들이 Preprocessors의 정적 메소드를 통해 이용할 수 있다. 더 자세한 내용은 아래를 보라.

 선행처리자들 Preprocessors

이쁘게 출력하기 Pretty printing

Preprocessors의 prettyPrint는 요청과 응답의 내용을 더 읽기 쉽게 만들어준다.

하이퍼미디어 기반의 API를 문서화하려는 중이라면, 클라이언트들이 하드코드된 URI를 사용하기보다는 링크를 사용하여 API를 찾아가기를 권할 것이다. 이를 위한 하나의 방법은 문서의 URI의 사용을 제한하는 것이다. Preprocessors의 maskLinks는 응답에 포함된  모든 href 링크들을  …​. 로 변경해준다. 이것은 당신이 원하는 다른 내용으로도 변경할 수 있다.

헤더 지우기 Removing headers

Preprocessors의 removeHeaders는 요청과 응답에 있는 특정한 헤더 이름을 지운다.

패턴 변경하기 Replacing patterns

Preprocessors의 replacePattern 은 요청과 응답의 내용을 바꾸는 일반적인 목적의 메카니즘을 제공한다. 정규식을 통해 나오는 모든 내욜이 변경될 것이다. 

자신만의 선행처리자 작성하기 Writing your own preprocessor

만일 내장 선행처리자들이 당신의 필요를 채울 수 없다면, OperationPreprocessor 인터페이스를 구현함으로서 자신만의 것을 만들수 있다. 그 후 당신의 커스텀 선행처리자를 아무 내장된 선행처리자와 똑같은 방식으로 사용하면 된다.

만일 요청이나 응답의 내용(body)만 변경하고자 하면, ContentModifier인터페이스를 구현하고 이를 내장 ContentModifyingOperationPreprocessor와 함께 사용하는 것을 고려해보자. 


설정 Configuration

 문서화된 URI들 Documented URIs

스프링 REST Docs의 URI 문서화를 위한 기본 설정값들은 다음과 같다:

SettingDefault

Scheme

http

Host

localhost

Port

8080

이 설정은 RestDocumentationMockMvcConfigurer에 의해 적용된다. 당신은 이 API를 당신이 필요로 하는 하나 또는 그 이상의 기본값들을 바꾸는 데 쓸 수 있다:

this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
        .apply(documentationConfiguration(this.restDocumentation).uris()
                .withScheme("https")
                .withHost("example.com")
                .withPort(443))
        .build();

요청의 컨텍스트 경로를 설정하려면, MockHttpServletRequestBuilder의 contextPath 메소드를 쓰자.

 코드조각 인코딩 Snippet encoding

아스키닥터가 사용하는 기본 인코딩은 UTF-8이다. Spring REST Docs 는 코드조각을 만드는데 똑같은 값을 사용한다. 만일 UTF-8이 아닌 다른 인코딩을 사용하려면 RestDocumentationMockMvcConfigure를 통해 설정하자:

this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
        .apply(documentationConfiguration(this.restDocumentation).snippets()
                .withEncoding("ISO-8859-1"))
        .build();

 기본 코드조각들 Default snippets

3가지 코드조각들이 기본적으로 만들어진다:

  • curl-request

  • http-request

  • http-response

이 기본 설정은 RestDocumentationMockMvcConfigurer에 의해 적용된다. 당신은 이 API를 설정을 바꾸는데 쓸 수 있다 . 예를 들어, 기본적으로 curl-request 코드조각만을 만드려면:

this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
        .apply(documentationConfiguration(this.restDocumentation).snippets()
                .withDefaults(curlRequest()))
        .build();

아스키닥터 작동하기 Working with Asciidoctor

이 섹션은 Spring REST Docs와 연관된 아스키닥터와 함께 동작하는 관점들에 대해 설명하였다.

 코드조작 포함하기 Including snippets

 include macro 는 당신의 문서에 생성된 코드조각을 포함사는데 사용된다. snippets 속성은 빌드 설정에서 구체적으로 다루었는데 코드조각의 출력 디렉토리를 참조하는데 사용된다. 예를 들어:

include::{snippets}/index/curl-request.adoc[]

태블릿 커스터마이징하기 Customizing tables

많은 코드조각들이 기본 설정에 의한 테이블을 포함하고 있다. 테이블의 모양은 코드조각이 포함될때 추가적인 설정을 제공하거나, 커스텀 코드조각 템플릿을 사용함으로서 커스터마이즈할 수 있다.

 컬럼 포멧팅 Formatting columns

아스키닥터는 테이블의 컬럼 포멧팅을 위한 많은 다양한 지원을 하고 있다. 예를 들어, 테이블 컬럼의 폭은 cols 속성을 사용하여 특정할 수 있다:

[cols=1,3] 1
include::{snippets}/index/links.adoc[]

1

두개의 컬럼으로 분리되는 테이블의 폭은 두번째 컬럼이 첫번째 컬럼보다 3배 더 크다.

타이틀 설정하기 Configuring the title

테이블의 타이틀은  .접두어를 사용한 줄에 의해 특정지을 수 있다:

.Links 1
include::{snippets}/index/links.adoc[]

1

테이블의 타이틀은 Links이다

더 읽을 거리 Further reading

테이블을 커스터마이징하는 더 자세한 정보는 아스키닥터 사용자 메뉴얼의 테이블 섹션을 참고하자.


반응형

반응형

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

반응형

스프링 세션으로 스케일 아웃하기 Scaling out with Spring Session

원문: http://www.jayway.com/2015/05/31/scaling-out-with-spring-session/

Stateless 아키텍쳐는 근 몇년간 타당한 이유로 꾸준히 인기가 올라왔다. 하지만 stateful 세션기반의 어플리케이션도 꾸준히 중요한 역할을 이어왔다. 예를 들어 더 나은 보안을 위해 CSRF 토큰이 이슈될때처럼. 낮은 부하의 단일 서버를 배포할 때, 세션관리는 당신이 합리적인 타임아웃 유효기간을 사용하고 세션에 많은 양의 데이터를 저장하지 않는 것처럼 매우 직관적이다. 문제는 스케일 아웃하기 더 힘들다는 것이다 각각의 요청은 아마 다른 서버에 있을 지도 모르는 그에 상응하는 세션과 함께 묶여져야하기 때문이다. 이를 극복하기 위해, 서버 공급자들은 서버간 다양한 종류의 세션 복제를 구현해왔다. 또다른 대안으로 로드밸런서를 세션과 붙여 같이 설정할 수 있다. 이 두가지 솔류션 다 잘 동작한다. 하지만 스프링 세션으로 스프링은 또 다른 옵션을 만들어냈다. 이 블로그는 세션을 스케일 아웃하기 위해 어떻게 스프링 세션을 레디스와 함께 설정하는 지 보여줄 거이다. 이 제안된 솔루션은 (스프링 기반이 아닌) 어떠한 서블릿에서도 작동할 것이다 - 이는 당신의 레가시 웹앱을 스케일 아웃해야할때 매우 적합하다.


예제 어플리케이션 Example Application

먼저 우리는 간단한 세션 기반의 HelloServlet 예제를 살펴보자:

HTTP GET이 호출될 때, 서블릿은 세션에 Mattias의 값을 가진 name 속성이 있으면  Hello Mattias! 를 응답하거나, 없다면 기본값인 Hello World!를 응답할 것이다.

HTTP POST이 호출될 때, 서블릿은 요청으로 부터 name 매개변수를 읽고, 새로운 세션을 만들고 (또는, 이미 세셨이 있다면 재사용하고), name의 값을 이와 상응하는 세션 속성에 저장할 것이다.

첫번째 요청은 세션이 없으므로 다음과 같이 다음과 같은 기본 응답을 받게 된다:

두번째 요청은 name 속성을 가진 POST 요청으로 세션이 생성된다:

덧붙이자면,  -i (또는 —-include) 옵션은 응답의 HTTP 헤더에 포함되는 cURL 플래그이며 -d (또는—data) 플래그는 요청의 매개변수로서 데이터를 제출하는데 사용된다. 응답에서 우리는 Set-Cookie 헤더의 값에 주목했다.

세번째 요청으로, GET 요청과 세션 상태의 유효화 검증이다:

어플리케이션의 서로 다른 인스턴스에서 같은 요청을 수행할 때 우리는 문제에 직면한다. 세션이 이미 존재함에도 불구하고 기본 응답이 되돌아왔다:

이제 스프링 세션을 써야할 때다!


스프링 세션 아키텍쳐 Spring Session architecture

스프링 세션의 컨셉은 매우 직관적이다:

  • 새 서블릿 필터를 만든다
  • 그 필터를 당신의 서블릿 필터체인filter chain에 추가한다
  • 그 필터를 레디스와 연결한다 (또는, Hazelcast, GemFire, Coherence가 지원하는 다른 MapSessionRepository 또는 당신에게  Map 레퍼런스를 제공해주는 다른 종류의 데이터 그리드data grid, 그러나 이들은 이 글의 범주에 벗어나므로 더이상 언급하지않는다)

의존성 추가하기 Adding dependencies

먼저, 우리는 몇가지 의존성을 추가해줘야한다. 메이븐에선 다음과 같이 POM에 추가해주면된다:

첫번째 의존성은 Redis 연결을 위해 필요하며 두번째는 스프링이 서블릿 필터를 만들기 위해 필요하다.


스프링 세션 설정 Spring Session Config

스프링 세션은 (내부적으로 Jedis client를 기반으로) 기본적으로 레디스 연결을 지원해준다. 아래의 XML 기반 설정의 예를 참고하거나  자바 기반 설정으로 대체할 수 있다.

마지막 두줄은 스프링에게  spring.redis.port 값을  application.properties 파일에서 찾으라고 알려주는것이다.  다음의 한줄과 같이: 


스프링 세션 설정 등록하기 Registering the Spring Session Config

(만인 당신의 어플리케이션이 이미 스프링을 사용하고 있지 않다면) 스프링 세션 설정을 불러오기위해  web.xml에 다음의 몇 줄을 추가해주어야 한다:


스프링 세션 서블릿 필터 추가하기 Adding the Spring Session servlet filter

어플리케이션에서 필요한 마지막 수정은 새로운 서블릿 필터를 추가하는 것이다. web.xml를 다시 열고 다음을 추가해준다:

single commit에서 바뀐 사항들을 모두 확인할 수 있다:


입 증 Verification

이제 어플리케이션을 모두 업데이트하였다. 원하는대로 동작하는지 입증해보자:

  • 어플리케이션을 다시 빌드한다
  • 레디스가 설치되었고 기본 포트인 6379에서 작동중인지 확인하라. 다른 포트에서 동작하고 있다면 spring.redis.port 설정을 업데이트 해주어야한다
  • 어플리케이션을 이를테면 8080 과 8081과 같이 두개의 인스턴스로 시작하라. 둘다 같은 레디스서버에 연결되어야한다.
  • 위에 언급된 이슈를 시도해보자

먼저 서버 인스턴스 한군데서 POST로 세션 상태를 만든다:

안은 인스턴스에서 GET 요청으로 세션 상태 검증하자:

다른 서버에서 같은 요청을 반복해보면 이제 같은 세션 데이터를 가지고 있는 것을 볼 수 있다:


고려 사항들 Considerations

  • 당신은 세션을 잃을지도 모른다는 걱정없이 원하는대로 서버를 시작하거나 멈출 수 있다. 이를테면 장애극복failover이나 자동 스케일링autoscaling은 세션의 관점에선 자동으로 처리된다.
  • 당신은 이제 더이상 sticky sessions 이나 복잡한 로드밸런서 설정이 필요없다. 부하를 나누려면 간단한 라운드-로빈Round-robin 전략이면 충분하다.
  • 운영의 관점에선 여전히 해줘야 할 것들이 있다. 누군가는 레디스 클러스터를 설정하고 관리해주어야 한다. (만일 AWS에서 동작한다몀 Amazon ElasticCache이 잘 맞을 것이다)
  • 스프링 부트를 사용중이라면 아마도 spring-session.xml설정이나  web.xml.을 추가하고 싶지않을 것이다.  자바 기반 설정을 위해  Spring Boot Guide를 확인하자.
  • 만일 당신이 RESTful API 를 개발중이다 쿠키를 싫어한다며? Spring Session Rest를 확인해보자.

주, 스프링 부트에서는 @EnableRedisHttpSession 어노테이션 한줄로 다 해결됨


반응형

반응형

타임리프3 5분 이주 가이즈 Thymeleaf 3 five-minute migration guide

원문: http://www.thymeleaf.org/doc/articles/thymeleaf3migration.html

당신은 Thymeleaf 2 사용자로서 새로운 Thymeleaf 3를 써보고 싶으시가요?

먼저, 좋은 소식이 있습니다. 당신이 구현해놓은 타임리프 템플릿들은 거의 100% 타임리프3와 호환될 것이므로, 당신은 단지 몇가지 설정만 바꿔주시면 됩니다.

Thymeleaf 3.0 BETA 버전은 안정적으모 2.1버전의 모든 기능을 지원합니다. 우리는 당신이 새 기능들과 성능 향상의 잇점을 취하도록 가능한한 빨리 타임리프3로 옮기기를 권장합니다.

현재 단계에서 타임리프 버전 2.1의 모든 다이얼렉트dialect들이 옮겨지진 않았다는게 약점이긴 하지만, 당신이 만약 타임리프 3에서 동작하지많은 외부 다이얼렉트를 사용중이라면, 타임리프 3와 호환되는 지 먼저 확인해보세요

템플릿 변경 Template changes

당신의 템플릿에 우리가 권장하는 유일한 변경사항은 아마도 당신이 사용하고있을 th:inline="text" 속성을 없애주는 것입니다. 왜냐하면 이들은 HTML이나 XML템플릿의 인라인 표현식을 출력하기 위해 더이상 필요하지 않기 때문입니다. 아 이건 그저 권장사항입니다 - 템플릿은 지우던 안지우던 어쨋거나 동작할 것입니다 - 하지만 당신이 이것을 지원줌으로서 성능향상이라는 혜택을 받을 수 있습니다.

더 자세한 정보는 밑의 향상된 인라인 메케니즘 섹셕을 참고사헤요

설정 변경 Configuration changes

이제 타임리프 사용자로서 가장 일반적인 선택인 thymeleaf-spring4 통합 패키지와 자바 설정을 사용한 타임리프 3의 설정의 예를 봅시다, 

먼저, 다음과 같이 타임리프3와 스프리4 통합 패키지를 얻기위한 메이븐 의존성을 업데이트합니다.

<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf</artifactId>
    <version>3.0.0.BETA01</version>
</dependency>
<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring4</artifactId>
    <version>3.0.0.BETA01</version>
</dependency>

두번째로, 스프링 설정:

@Configuration
@EnableWebMvc
@ComponentScan("com.thymeleafexamples")
public class ThymeleafConfig extends WebMvcConfigurerAdapter implements ApplicationContextAware {
 
    private ApplicationContext applicationContext;
 
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }
 
    @Bean
    public ViewResolver viewResolver() {
        ThymeleafViewResolver resolver = new ThymeleafViewResolver();
        resolver.setTemplateEngine(templateEngine());
        resolver.setCharacterEncoding("UTF-8");
        return resolver;
    }
 
    private TemplateEngine templateEngine() {
        SpringTemplateEngine engine = new SpringTemplateEngine();
        engine.setTemplateResolver(templateResolver());
        return engine;
    }
 
    private ITemplateResolver templateResolver() {
        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        resolver.setApplicationContext(applicationContext);
        resolver.setPrefix("/WEB-INF/templates/");
        resolver.setTemplateMode(TemplateMode.HTML);
        return resolver;
    }
 
}

타임리프2 설정과의 첫번째 차이점은 이제 스프링 어플리케이션을 위한 추천 템플릿이 SpringResourceTemplateResolver 라는 것입니다. 이는 설정 빈(configuration bean)이 ApplicationContextAware 인터페이스를 구현줘야만 하기 때문에 스프링 ApplicationContext에 참조(reference)가 필요합니다.

두번째 차이점은 템플릿 모드가 TemplateMode.HTMLT의 값을 가지는 것입니다. 템플릿 모드는 더이상 스트링이 아니며, 가능한 값들도 타임리프2와 약간 다릅니다. 잠시후 살펴볼 것입니다.

만일 추가적인 다이얼렉트를 추가해줘야한다면, 당신은 engine.addDialect(...) 메소드를 사용할 수 있습니다. 그러나 먼저 이것이 타임리프 3와 호환가능한 버전인지 확인해보아야 합니다.

Thymeleaf 3 + Spring 4 + Java config exampleThymeleaf 3 + Spring 4 + XML config example 그리고 Thymeleaf 3 + Servlet 3 example에서 간단한 "Hello World" 예제들의 소스코드를 찾아보거나 다운로드 할 수 있습니다.

또는 the Thymeleaf BETA 1 announcement.에서 추가적인 정보를 찾아볼 수 있습니다.

완전한 HTML5 마크업 지원 Full HTML5 markup support

타임리프 3.0은 더이상 XML 베이스가 아닙니다. 이 새 파싱 시스템에 감사하며, XML 유효한 HTML 코드를 더이상 만들 필요도 없습니다. (가독성이좋으므로 여전히 권장되기는 합니다). HTML 모드로, 타임리프는 클로징태그(closed tag), 따옴표 속성(quoted attribute)의 경우 이제 훨씬 더 관대해졌습니다.

따라서 이제 다음과 같이 더 완벽하게 처리능력이 향상된 (약간 가독성이 떨어지지만) 타임리프 템플릿이 되었습니다:

<div><p th:text=${mytext} ng-app>Whatever

새로운 파싱 시스템에 댛의 설명은 Full HTML5 support, new parsing infrastructure를 참고하세요.

템플릿 모드 Template modes

타임리프3는 이전 버전의 템플릿 모드의 묶음을 변경하였습니다. 새로운 템플릿 모드는 다음과 같습니다: 

  • HTML
  • XML
  • TEXT
  • JAVASCRIPT
  • CSS
  • RAW

2개의 마크업markup 템플릿 모드(HTML and XML), 3개의 텍스쳐textual 템플릿 모드 (TEXTJAVASCRIPT, CSS) 그리고 no-op 템플릿 모드 (RAW).

HTML 템플릿 모드는 HTML5, HTML 4 and XHTML을 포함하여 어떤 종류의 HTML 마크업 입력을 허용합니다. 제대로 구조화 했는지 확인하는 마크업 유효성검사를 수행하지않습니다. 템플릿 마크업 코드구조는 출력시 가장 큰 가능성을 가진 규모biggest possible extent를 존중할 것입니다.

다른 템플릿 모드에 대한 자세한 설명은 Thymeleaf 3.0 Template Mode set을 확인하세요.

또한 https://github.com/jmiguelsamper/thymeleaf3-template-modes-example에서 새 템플릿 모드에 대한 간단한 예제를 확인할 수 있습니다.


텍스쳐 템플릿 모드 Textual template modes

타임리프에 추가된 새로운 텍스쳐 템플릿 모드는 CSSJavascript 와 plain text를 출력할 수 있습니다. 당신의 CSS와 자바스크립트 파일들에 서버사이드 변수값을 사용하거나, 이메일 작성과 같은 plain text를 만들기 더 쉽게 되었습니다.

텍스쳐 모드에 대한 타임리프의 모든 기능을 활용하기 위해  새로운 신텍스syntax가 소개되었습니다 예를 들어, 당신은 이와 같이 iterate할 수 있습니다:

[# th:each="item : ${items}"]
    - [# th:utext="${item}" /]
[/]

이 새로운 신텍스에 대한 자세한 설명은 New syntax for textual template modes에서 확인하세요

향상된 인라인 메카니즘 Improved inlining mechanism

다음과 같이 추가적인 태그나 속성을 사용하지 않고 데이터를 손쉽게 출력할 수 있습니다:

<p>This product is called [[${product.name}]] and it's great!</p>

인라인inlining 이라 부르는 이 기능이 상당히 향상되어, 이제 타임리프3에서 훨씬 더 나은 지원이 가능하게 되었습니다. 자세한 정보는 Inlined output expressions를 확인하세요

현존의 인라인 메카니즘 또한 새로운 템플릿 보드에서 잘 작동합니다. 사실 th:inline="text" 속성은 더이상 필요하지 않습니다 왜냐하면 인라인은 이제 HTML 모드 그 자체이기 때문입니다. 자세한 정보는 Refactoring of the inlining mechanism에서 확인하세요

성능 향상 Performance improvements

타임리프 3.0의 주요한 성취는 이전 버전에서 항상 제기되었던 성능의 향상입니다.  2.1 버전까지의 XML기반의 템플릿엔진으로 수많은 훌륭한 강력한 기능들을 구현할 수 있었지만, 때때로 성능의 문제가 있었습니다. 타임리프 랜더링에 걸리는 시간이 많은 주요 프로젝트에서 무시되어 왔지만, 이 위험부담은 (수천개의 열을 가진 테이블을 다루는 고부하가 걸리는 웹사이트와 같은) 특별한 성격을 가지는 프로젝트에서 현저히 드러났습니다. 

타임리프 3 엔진은 성능에 집중하여 처음부터 다시 구현되었습니다. 타임리프3의 성능은 이전버젼보다 훨씬 더 향상되었으므로, 우리는 더욱 더 많은 프로젝트들의 다양한 요구사항을 맞출 수 있게 되었습니다. 그러나 타임리프의 성능은 단지 랜더링 시간뿐만 아니라 더 낮은 메모리 사용과 높은 병렬처리가 필요한 시나리오에 지연시간을 줄일 수 있도록 특별히 디자인되었습니다.

새로운 타임리프 3 아키텍쳐에 대한 기술적 논의는  New event-based template processing engine를 확인하세요

새로운 다이얼렉트 시스템 New Dialect system

타임리프3는 새로운 다이얼렉트 시스템을 특징지었습니다. 만일 이전 버전의 타임리프에서 이 타임리프 다이얼렉트를 개발하려면, 이것을 타임리프3 호환 가능하게 만들어줘야 할 것입니다.

새로운 다이얼렉트 인터페이스는 정말로 간단합니다...

public interface IDialect {
 
    public String getName();
 
}

…그러나 당신이 구현할 IDialecte의 구체적인 서브인터페이스에 의존하여 수많은 기능을 추가할 수 있습니다. 

새로운 다이얼렉트 시스템의 몇가지 기능을 정리해보자면:

  • 처리자processors 뿐만 아니라 선처리자pre-processors 와 후처리자post-processors가 있어 템플릿 컨텐트를 처리 전, 처리 후에 수정이 가능해졌다. 예를 들어, 캐시된 내용을 제공하기위해 선처리자를 사용하고, 출력물을 압축하고 최소화하기위해 후처리자를 사용할 수 있다.
  • 다이얼렉트 우선권Dialect precedence는 다이얼렉트를 가로지르는 프로세서들을 분류할 수 있는 새로운 개념입니다. 프로세서 Processor precedences 이제 다이얼렉트 우선권과 관련있는 걸로 간주되어 특정 다이얼렉트에 있는 모든 프로세서를 단지 다이얼렉트 우선권의 올바른 값을 설정하는 것만으로 다른 다이얼렉트로 부터의 어떤 프로세스 이전에 실행되도록 설정할 수 있습니다.
  • 객체 표현식 다이얼렉트Expression object dialects 는 새로운 객체 표현식 또는 유틸리티 객체 표현식을 제공하여 표준 다이얼렉트가 제공하는  #strings#numbers#dates와 같은 표현식을 템플릿의 어디에서나 사용할 수 있습니다. 
  • provide new expression objects or expression utility objects that can be used in expressions anywhere in templates, such as the #strings#numbers#dates, etc. provided by the Standard Dialect.

이들 기능에 대한 자세한 설명을 다음을 확인하세요:

Core API의 리펙토링 Refactoring of the core APIs

The core API는 아주 많이 리펙터되었습니다. 다음의 디테일을 확인하세요:

결 론 Final thoughts

Thymeleaf 3는 수많은 시간을 들여 공들여 작업한 지난 4년간의 타임리프 템플릿 엔진 프로젝트의 주요한 성과입니다. 훌륭한 새 기능들과 보이지 않는 곳에서의 수많은 성능 향상을 이루어냈습니다.

우리는 이 업데이트가 당신의 프로젝트에 꼭 필요한 요소들을 갖추도록 맞춰왔습니다. 그러니 이것을 시도해보는 것을 망설이지 마시고 피드백을 보내주세요!



반응형

'ReleaseNotes' 카테고리의 다른 글

스프링 부트 1.5 릴리즈 노트  (0) 2017.07.21
스프링 부트 1.4 릴리즈 노트  (0) 2016.08.06
Spring Boot 1.3.0 릴리즈 노트  (0) 2015.11.18
Spring Boot 1.2 릴리즈 노트  (0) 2015.11.18
Spring Boot 1.1 릴리즈노트  (0) 2015.11.18

반응형
스프링 부트 레퍼런스 11장 
첫번째 스프링 부트 어플리케이션 개발하기
Developing your first Spring Boot application

11. 첫번째 스프링 부트 어플리케이션 개발하기

스프링 부트의 핵심기능 일부를 사용해서 자바로 간단한 "Hello World!” 웹 어플리케이션을 개발해보자. 여기선 대부분의 IDE가 지원하는 메이븐을 사용할 것이다.

시작에 앞서 터미널을 열고 설치된 자바와 메이븐 버전을 확인해보자 

$ java -version
java version "1.7.0_51"
Java(TM) SE Runtime Environment (build 1.7.0_51-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.51-b03, mixed mode)
$ mvn -v
Apache Maven 3.2.3 (33f8c3e1027c3ddde99d3cdebad2656a31e8fdf4; 2014-08-11T13:58:10-07:00)
Maven home: /Users/user/tools/apache-maven-3.1.1
Java version: 1.7.0_51, vendor: Oracle Corporation
[Note]

이 예제는 자신의 폴더를 만들어서 진행해야하며 부가적 구성은 해당 폴더 안에서 진행한다고 가정함.

11.1 POM 생성하기

메이븐 pom.xml 파일을 먼저 만들어야한다. 이 pom.xml 파일은 당신의 프로젝트안에서 사용될 레시피다. 선호하는 에디터를 열고 다음을 추가하라:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>myproject</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.5.RELEASE</version>
    </parent>

    <!-- Additional lines to be added here... -->

</project>

이것으로 빌드는 정상 동작한다. mvn package 를 치고 확인할 수 있다. ( “jar will be empty - no content was marked for inclusion!'” 라는 경고메시지는 일단 무시).

[Note]

이 단계에서 IDE로 import할 수 있다. (대부분의 자바 IDE는 메이븐을 내장하고 있다) 그냥 간단히 아무 에디터를 써서 이 예제를 계속 해도 상관없음

11.2 classpath 의존성(dependencies) 추가

스프링 부트는 당신의 classpath에 jar파일들을 쉽게 추가하게 해서는 몇몇의 “Starter POM”들을 제공해주고 있다. 현 예제 어플리케이션은 POM의 parent 센션에서 이미 spring-boot-starter-parent 를 사용하고 있다.

이 spring-boot-starter-parent 는 유용한 메이븐의 기본사항들을 제공해주는 특별한 시작키트이며 또한 dependency-management 섹션을 제공하여 선별된 dependency들을 version태그없이 쓸 수 있다.

다른 “Starter POM”들은 특별한 타입의 어플리케이션을 개발할때 필요로 하는 dependency들을 맞춤제공해준다. 현재 우리는 웹 어플리케이션을 개발중이므로  spring-boot-starter-web dependency를 추가할 것이지만, 그전에 우리가 현재 가지고 있는 dependency들을 살펴보도록 하자

$ mvn dependency:tree

[INFO] com.example:myproject:jar:0.0.1-SNAPSHOT

이 mvn dependency:tree 명령어는 당신의 프로젝 dependency를 트리구조로 보여준다. 보면 알수있듯이 spring-boot-starter-parent 는 그 자체에 의존성을 가지고 있지않다. pom.xml 를 편집해서 spring-boot-starter-web 의존성을 parent 센션 밑에 아래와 같이 추가 해보자:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

mvn dependency:tree 를 다시 실행해보면 톰캣웹서버나 스프링 부트 자체를 포함하여 부수적인 dependency들이 추가된것을 볼 수 있다. 

11.3 코드작성하기

예제 어플리케이션을 완성하려면, 자바파일을 하나 만들어야 한다. 메이븐은 기본적으로  src/main/java 위치에 있는 소스를 컴파일하므로 해당 경로의 폴더를 만들고 다음의 파일을 추가 한다. src/main/java/Example.java:

import org.springframework.boot.*;
import org.springframework.boot.autoconfigure.*;
import org.springframework.stereotype.*;
import org.springframework.web.bind.annotation.*;

@RestController
@EnableAutoConfiguration
public class Example {

    @RequestMapping("/")
    String home() {
        return "Hello World!";
    }

    public static void main(String[] args) throws Exception {
        SpringApplication.run(Example.class, args);
    }

}

코드의 양은 보잘것없지만 많은 일이 진행되었다. 이 중요한 파트를 살펴보자.

11.3.1 @RestController 그리고 @RequestMapping annotations

Example 클래스의 첫번째 어노테이션은 @RestControllerstereotype annotation으로 잘알려진 이것은 코드를 읽을 사람들 또는 특별한 역할을 실행하는 클래스를 가진 스프링에 힌트를 제공한다. 우리의 경우 웹 @Controller 이므로, 스프링은 들어오는 Web request들을 처리할거라고 이해한다.

@RequestMapping 어노테이션은 "라우팅"정보를 제공하며 스프링에게 경로 "/"를 통해 들어오는 HTTP 요청이 home 메소드와 매칭되야한다고 알려준다.

@RestController 어노테이션은 스프링에게 호출자에게 되돌아가는 문자열을 랜더하라고 알려준다.

[Tip]

@RestController 와 @RequestMapping 어노테이션은 Spring MVC 어노테이션이다. (Spring Boot 특화된게 아님). 자세한 정보는 MVC section 참고

11.3.2 The @EnableAutoConfiguration annotation

두번째 클래스 레벨 어노테이션은 @EnableAutoConfiguration. 이 어노테이션은 스프링 부트에게 당신이 추가한 jar dependency들을 기준으로 어떻게 스프링을 설정하길 원하는지 "추측"하라고 알려준다.

spring-boot-starter-web 를 통해 추가된 톰캣과 스프링 MVC가 추가되었으므로,  자동설정 기능은 당신이 웹어플리케이션을 개발하고 있다고 가정하고 그에 따라 스프링을 설정한다.

11.3.3 The “main” method

우리의 어플리케이션의 마지막 파트는 main 메소드이다. 이것은 어플리케이션의 시작점을 위한 자바 명명법을 따르는 표준 메소드이다. 우리의 main 메소드는 run 에 의해 호출되는 스프링 부트의   SpringApplication 클래스를 실행한다.  SpringApplication 는 당신의 어플리케이션을 시동걸어, 스프링을 시작하고 이어 자동설정된 톰캣 웹서버를 동작시킨다. 우리는 기본 스프링 컴포넌트인 SpringApplication에 알리기 위해 Example.class 를 하나의  인자값으로  run  메소드에 넘겨야한다.  args 배열값 또한 command-line 인자값으로 외부적으로 넘길 수 있다.

11.4 예제 실행하기 Running the example

현재 단계에서  spring-boot-starter-parent POM을 사용한 이상 어플리케이션은 실행이 잘된다. 프로젝트의 메인 디렉토리에서 mvn spring-boot:run 를 치면 어플리케이션이 시작한다.

$ mvn spring-boot:run

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::  (v1.2.5.RELEASE)
....... . . .
....... . . . (log output here)
....... . . .
........ Started Example in 2.222 seconds (JVM running for 6.514)

브라우저를 열고 localhost:8080 를 치면 다음의 결과가 보인다:

Hello World!

 ctrl-c를 쳐서 프로그램을 우아하게 종료시키자

11.5 실행가능한 jar 만들기 Creating an executable jar

독립실행 가능한 jar파일을 만듦으로서 예제를 완성시켜보자. 실행가능한 jar (때로는 “fat jars”로 부르기도 함)는 당신의 컴파일된 클래스 뿐만 아니라 코드 실행에 관련된 모든 jar dependency들을 포함하여 압축한다.

실행가능한 jar를 만드려면 spring-boot-maven-plugin 를  pom.xml에 추가해야 한다. 다음의 라인을 dependencies 섹션 다음에 추가하자

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>
[Note]

spring-boot-starter-parent POM 은 repackage goal 을 설정하기 위해 <executions> 를 포함하고 있다. 부모 POM이 없다면 스스로 설정을 해야한다. 자세한 문서는 plugin documentation 참고.

 pom.xml 를 저장하고 콘솔에서 mvn package 를 친다.

$ mvn package

[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building myproject 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] .... ..
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ myproject ---
[INFO] Building jar: /Users/developer/example/spring-boot-example/target/myproject-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- spring-boot-maven-plugin:1.2.5.RELEASE:repackage (default) @ myproject ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

target 디렉토리에 myproject-0.0.1-SNAPSHOT.jar파일이 만들어졌다. 대략 10 Mb 정도의 사이즈이며 jar tvf명령어로 내부를 둘려볼 수 있다.

$ jar tvf target/myproject-0.0.1-SNAPSHOT.jar

target 디렉토리에는 myproject-0.0.1-SNAPSHOT.jar.original 라는 이름의 작은 파일도 있는데 이것은 스프링 부트에 의해 재패키지되기전의 메이븐 원본 jar파일이다.

이 어플리케이션을 실행하려면  java -jar 명령어를 쓰자:

$ java -jar target/myproject-0.0.1-SNAPSHOT.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::  (v1.2.5.RELEASE)
....... . . .
....... . . . (log output here)
....... . . .
........ Started Example in 2.536 seconds (JVM running for 2.864)

 ctrl-c로 우아하게 종료할 수 있다.

반응형

'Tutorials' 카테고리의 다른 글

스프링부트와 OAuth2 - (1/4)  (0) 2016.03.19
Scaling out with Spring Session  (0) 2015.12.15
Spring Security and AngularJS Part VII  (1) 2015.12.03
Spring Security and AngularJS Part VI  (0) 2015.12.03
Spring Security and AngularJS Part V  (0) 2015.12.03

반응형

스프링 시큐리티와 앵귤러JS Spring Security and AngularJS


Part I : 단일 페이지 보안 어플리케이션 (
A Secure Single Page Application)

Part II: 로그인페이지 (The Login Page)

Part III: 리소스 서버 (The Resource Server)

Part IV: API 게이트웨이 (The API Gateway)

Part V: OAuth2와 싱글사인온 (Single Sign On with OAuth2)

Part VI: 다중 UI 어플리케이션과 게이트웨이 (Multiple UI Applications and a Gateway)

Part VII: 모듈화한 AngularJS 어플리케이션  (Modular AngularJS Application)

Part VIII: Angular 어플리케이션 테스트 (Testing an Angular Application)


--------------------------------------------------------------------------------


모듈화한 앵귤러 JS 어플리케이션 Modular AngularJS Application

이 섹션에서 우리는 어떻게 스프링 시큐티리와 앵귤러JS로 단일 페이지 어플리케이션을 만드는지 계속 얘기해볼 것이다. 이제 우리는 어떻게하면 클라이언트 코드를 모듈화하는지, 어떻게 하면 앵귤러가 기본값으로 사용하지만 대부분의 사용자가 싫어하는 ("/#/login"같은) 파편화된 표기없이 "멋진" URL 경로를 사용할 수 있는지를 보여줄 것이다. 이 글은 시리즈의 일곱번째 섹션으로 첫번째 섹션부터 어플리케이션의 기초 구성단위를 처음부터 배워가도 된고 아니면 Github의 소스코드를 바로 가봐도 된다. 우리는 이 시리즈의 남은 섹션을 자바스크립트의 미진한 부분을 깔끔하게 정리 해볼 것이다. 동시에 어떻게 하면 스프링 시큐리티와 스프링 부트로 빌드된 백엔드 서버에 포근하게 맞출 수 있는지 보여줄 것이다.

어플리케이션 쪼개기 Breaking up the Application

이 시리즈에서 우리가 지금까지 동작했던 예제 어플리케이션 전체를 단하나의 자바스크립트 소스파일로 때우기에 충분히 사소했다. 더 큰 어플리케이션은 이와 같은 방식으로 가서는 안된다. 이렇게 하나로 시작했을 때조차,  예제에서 실제의 환경에서 동작하는 것처럼 만들어야 하므로 우리는 이들을 나눌것이다. 좋은 시작점은 두번째 섹션에서 만들었던 "단일"어플리케이션을 취해 그 소스코드의 구조를 들여다 보는 것이다. 여기 정적 컨탠트를 위한 디렉토리 리스트가 있다( 서버에 있는 "application.yml"은 제외했다):

static/
 js/
   hello.js
 home.html
 login.html
 index.html

여기에 몇가지 문제점이 있는데 하나는 명확하다: 모든 자바스크립트가 하나의 파일(hello.js)에 있다는 것이다. 또 하나는 감지하기 힘든데: 우리의 어플리케이션 내부의 뷰가 HTML이 "부분적"("login.html"과 "home.html")이지만 이들은 모두 평이한 구조안에 있으며 이들을 사용하는 컨트롤러 코드와 연관되어있지 않다.

자바스크립트를 더 자세히 들여다보자. 우리는 좀 더 관리하기 쉬운 조각들로 나누는 것을 앵귤러가 더 쉽게 만들어주고 있는 것을 볼 수 있다:

hello.js

angular.module('hello', [ 'ngRoute' ]).config(

  function($routeProvider, $httpProvider) {

    $routeProvider.when('/', {
      templateUrl : 'home.html',
      controller : 'home'
    }).when('/login', {
      templateUrl : 'login.html',
      controller : 'navigation'
    }).otherwise('/');

    ...

}).controller('navigation',
    function($rootScope, $scope, $http, $location, $route) {
      ...
}).controller('home', function($scope, $http) {
    ...
  })
});

여기에 약간의 "config" 와 두개의 컨트롤러("home" 과 "navigation")이 있고 컨트롤러는 이 일부분(각각 "html.html"과 "login.html")을 멋지게 맵핑해주고 있다. 이 일부들을 각각 쪼개보자:

static/
  js/
    home/
      home.js
      home.html
    navigation/
      navigation.js
      login.html
    hello.js
  index.html

컨트롤러 정의는 동작하는 데 필요한 HTML과 함께 각각의 모듈안으로 이동하였다. - 멋게 모듈화되었다. 만일 이미지나 커스텀 스타일시트가 필요하면, 우리는 이들과 똑같은 방식으로 만들어주면 된다.

same with those.

 모든 클라이언트 코드는 (index.html 를 제외하고) 단일 디렉토리 밑에 놓는다. 왜냐하면 "welcome" 페이지가 있어 "정적" 디렉토리로부터 자동적으로 불러지기 때문이다. 이것은 의도적으로서 모든 정적 리소스로의 단일 스프링 시큐리티 접근 규칙을 만들기 쉽게 하기 위해서다. 이들은 모두 보호받지 않는다. (스프링 부트 어플리케이션에서는 기본적으로  /js/** 를 보호하지 않기 때문이다), 하지만 다른 어플리케이션을 위해 다른 규칙이 필요하다면 다른 경로를 선택해주면 된다.

예를 들어 여기 home.js를 보자:

code,javascript
angular.module('home', []).controller('home', function($scope, $http) {
    $http.get('/user/').success(function(data) {
        $scope.user = data.name;
    });
});

그리고 여기 새로운 hello.js:

code,javascript
angular
    .module('hello', [ 'ngRoute', 'home', 'navigation' ])
    .config(

        function($routeProvider, $httpProvider) {

          $routeProvider.when('/', {
            templateUrl : 'js/home/home.html',
            controller : 'home'
          }).when('/login', {
            templateUrl : 'js/navigation/login.html',
            controller : 'navigation'
          }).otherwise('/');

          $httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

        });

어떻게 "hello" 모듈이 그들을 목록화함으로서 ngRoute와 함께 초기화 선언된 다른 두개를 신뢰하는지 알아두자. 이것을 동작하게 만드려면, 당신은 index.html안에 올바른 순서로 모듈정의를 불러주어야 한다:

...
<script src="js/angular-bootstrap.js" type="text/javascript"></script>
<script src="js/home/home.js" type="text/javascript"></script>
<script src="js/navigation/navigation.js" type="text/javascript"></script>
<script src="js/hello.js" type="text/javascript"></script>
...

이것은 실제 쓰이는 앵귤러 JS 의존성 관리 시스템이다. 다른 프레임워크들도 (단언컨대 더 우수한) 유사한 기능을 가지고 있다. 또한 더 큰 사이즈의 어플리케이션에서, 당신은 모든 자바스크립트를 함께 번들화 하는 빌드하는 절차를 밟아 브라우저가 더 효율적으로 불러올수 있게할 것이다. 그러나 이건 거의 취향의 차이이다.

"자연스러운" 라우트 사용하기 Using "Natural" Routes

앵귤러의 $routeProvider는 기본값으로 URL 경로안에 파편화된 위치탐지자를 가지고 동작한다. 예를 들어 "/login"경로로서 hello.js 안에 라우트되도록 명시한 로그인 페이지는 (브라우저 윈도우에서 당신이 보게되는) 실제 URL에서 "/#/login" 로 해석되어진다. 이는 루트패스 "/"를 통해 불러오는 index.html안의 자바스크립트에서 모든 라우트들을 이렇게 활성화한다. 파편화된 이름은 약간 사용자에 익숙하지않으며 때론 URL 경로가 앵귤러 라우트 선언과 같도록 "자연스럽게" 라우트되도록하는게 더 편리하다. 예를 들어 "/login"에 "/login"을 씀. 만일 당신이 오직 정적 리소스만 가지고 있다면 이렇게 할 수 없다. 왜냐하면 index.html 은 한방향으로만 불러지기 때문이다. 그러나 (프록시나 어떤 서버사이드 로직) 스택안에 어떤 활성화된 컴포넌트가 를 가지고 있다면, 모든 앵귤러 라우트로부터 index.html 를 불러옴으로서 처리할 수 있다.

이 시리즈에서 당신은 스프링 부트를 사용중이므로 물론, 당신은 서버-사이드 로직을 가지고 있다. 간단한 스프링 MVC 컨트롤러를 사용하여 당신은 어플리케이션의 라우트를 원래대로 사용할 수 있다. 이를 위해 당신이 해줘야 하는 일은 서버에 앵귤러를 열거해주는 것이다. 식별자규칙(naming convention)에 의해 이것을 하도록 선택하였다. 마침표를 포함하지 않은 (그리고 명시적으로 이미 맵핑되지않은) 모든 경로는 앵귤러 라우트로서 홈페이지에 보내져야(forward)한다:

@RequestMapping(value = "/{[path:[^\\.]*}")
public String redirect() {
  return "forward:/";
}

이 메소드는 스프링 어플리케이션의 어딘가의 (@RestController이 아니라) @Controller 에서 넣어줘야한다. 우리는 브라우저가 사용자가 실제 URL에서 보는 "실제" 라우트를 기억할 수 있도록 ("redirect가 아니라) "foward"를 사용했다. 비록, 우리가 이 어플리케이션에어 이 장점을 적용하진 않겠지만, 이는 또한 스프링 시큐리티의 인증을 위한 저장된 요청(saved request) 메카니즘이 막 바로 사용가능(out of the box)하도록 동작한다는 것을 의미한다.

 github의 예제코드에 있는 어플리케이션은 추가적인 라우트를 가지고 있어 당신은 더 완전한 기능을 확인할 수 있다. 그러므로 실사용이 가능한 어플리케이션일 것이다. ("/home"과 "/message"는 약간 다른 뷰를 가지는 다른 모듈들이다).

이 "자연스러운" 라우트를 가지는 어플리케이션을 완성시키기위해, 당신은 앵귤러에게 이 두가지 절차를 알려줘야한다. 첫번째는hello.js에서 $locationProvider안의 config함수에 "HTML5모드"설정을 추가해야한다:

angular.module('hello', [ 'ngRoute', 'home', 'navigation' ]).config(

  function($locationProvider, $routeProvider, $httpProvider) {

    $locationProvider.html5Mode(true);
    ...
});

index.html안의 HTML헤더안에 추가적인 <base/> 엘리먼트와 같이 묶어, 당신은 메뉴바의 링크를 파편화("#")를 없애기 위해 바꿔줘야한다:

<html>
<head>
<base href="/" />
...
</head>
<body ng-app="hello" ng-cloak class="ng-cloak">
    <div ng-controller="navigation" class="container">
        <ul class="nav nav-pills" role="tablist">
            <li><a href="/">home</a></li>
            <li><a href="/login">login</a></li>
            <li ng-show="authenticated"><a href="" ng-click="logout()">logout</a></li>
        </ul>
    </div>
...
</html>

앵귤러는<base/> 엘리먼트에 라우트를 정박하고 브라우저에 나타나는 URL들을 쓰기 위해 사용한다. 당신은 스프링 부트 어플리케이션을 동작하고 있으므로 기본 설정은 (8080포트상의) "/" 루트 경로로 부터 제공된다. 만일 같은 어플리케이션의 다른 루트 경로로부터 제공할 필요가 있다면, 그 경로를 서버-사이드 템플릿(많은 사람들이 단일 페이지 어플리케이션을 위해 정적리소스와 함께 쓰기를 원한다 그래서 그들은 정적 루트 경로와 함께 쓰인다)을 사용하여 HTML안으로 랜더해줘야 할 것이다.

인증 관련정보 추출하기 Extracting the Authentication Concerns

위에서 처럼 어플리케이션을 모듈화할 때, 당신은 코드가 그저 모듈들로 나누어져서 작동시킨 것뿐이라는 걸 알수 있을 것이다. 하지만, 사소하게 트집을 잡자면 우리는 여전히 $rootScope를 사용하여 컨트롤러간의 상태를 공유한 다는 점이다. 이것은 이런 작은 규모의 어플리케이션에서는 크게 잘못된 것은 아니다. 이렇게 함으로서 아주 괜찮은 수준의 프로토타입을 아주, 아주 빠르게 만들수 있게 된다. 그러니 이것에 대해 너무 슬퍼할 필요는 없다. 그러나 우리가 각각 분리된 모듈에서 인증관련 모든정보를 추출할 기회가 있다면, 앵귤러 용어에서 당신이 필요한 것을 "서비스"라고 정의한다. 그래서 ("auth"라 부르는) 새로운 모듈을 당신의 "home"과 "navigation" 모듈 다음으로 만들어보자:

static/
  js/
    auth/
      auth.js
    home/
      home.js
      home.html
    navigation/
      navigation.js
      login.html
    hello.js
  index.html

auth.js 코드를 작성하기 전, 우리는 다른 모듈의 수정을 예상할 수 있을 것이다. 먼저 navigation.js 에서 "navigation"모듈을 새로운 "auth"모듈에 의존성을 가지도록 만들어주어야한다. "auth"서비스를 컨트롤러에 주입하자 (물론 $rootScope 은 이제 더이상 필요없다):

angular.module('navigation', ['auth']).controller(
        'navigation',

        function($scope, auth) {

            $scope.credentials = {};

            $scope.authenticated = function() {
                return auth.authenticated;
            }

            $scope.login = function() {
                auth.authenticate($scope.credentials, function(authenticated) {
                    if (authenticated) {
                        console.log("Login succeeded")
                        $scope.error = false;
                    } else {
                        console.log("Login failed")
                        $scope.error = true;
                    }
                })
            };

            $scope.logout = function() {
              auth.clear();
            }

        });

이전의 컨트롤러와 많이 다르지않다 (여전히 사용자 액션, 로그인과 로그아웃을 위한 함수들과 로그인후 credential을 유지하고 있는 객체가 필요하다). 하지만 새 "auth" 서비스로 구현을 추상화하였다. "auth"서비스는 login()을 지원하기 위해 authenticate()함수가, logout()을 지원하기위해 clear() 함수를 필요로한다. 또한 authenticated 플래그가 있어 이전 컨트롤러의 $rootScope.authenticated 를 대체한다. 우리는 컨트롤러의 $scope에 붙여진 같은 이름으로 함수안에서 authenticated 플래그를 사용한다. 따라서 앵귤러가 이 값을 계속 확인하고 사용자가 로그인할때 UI를 업데이트한다.

"auth" 모듈을 재사용가능하게 만들길 원한다는 가정하에, 당신은 여기에 하드코드된 경로를 원하지 않을 것이다. 문제는 없지만, hello.js 모듈안의 경로를 초기화 하거나 설정해줘야할 것이다. 이것을 위해 run() 함수를 추가해보자:

angular
  .module('hello', [ 'ngRoute', 'auth', 'home', 'navigation' ])
  .config(
    ...
  }).run(function(auth) {

    auth.init('/', '/login', '/logout');

});

run() 함수는 "hello" 에 의존하는 어떠한 모듈에서 호출할 수 있다. 이 경우, auth 서비스를 주입하고 각각 홈페이지,로그인, 로그아웃 종단endpoint의 경로로 이것을 초기화해준다.

이제 당신은 index.html안에  다른 모듈뿐만 아니라 "auth" 모듈을 불러와야 한다.("auth"에 의존성이 있는 "login"모듈 전에):

...
<script src="js/auth/auth.js" type="text/javascript"></script>
...
<script src="js/hello.js" type="text/javascript"></script>
...

이렇게 하면 마침내 당신은 위에서 적어둔 세 함수 (authenticate()clear() 그리고 init())를 위한 코드를 작성할 수 있다. 여기 코드가 있다:

angular.module('auth', []).factory(
    'auth',

    function($http, $location) {

      var auth = {

        authenticated : false,

        loginPath : '/login',
        logoutPath : '/logout',
        homePath : '/',

        authenticate : function(credentials, callback) {

          var headers = credentials && credentials.username ? {
            authorization : "Basic "
                + btoa(credentials.username + ":"
                    + credentials.password)
          } : {};

          $http.get('user', {
            headers : headers
          }).success(function(data) {
            if (data.name) {
              auth.authenticated = true;
            } else {
              auth.authenticated = false;
            }
            $location.path(auth.homePath);
            callback && callback(auth.authenticated);
          }).error(function() {
            auth.authenticated = false;
            callback && callback(false);
          });

        },

        clear : function() { ... },

        init : function(homePath, loginPath, logoutPath) { ... }

      };

      return auth;

    });

 (예를 들어 "navigation" 컨트롤러엣 이미 주입된auth 서비스를 위해 "auth" 모듈은 팩토리를 생성한다. 이 팩토리는 단지 (auth) 객체를 돌려주는 함수이고, 이 객체는 3개의 함수를 가져야하고 플래그는 위에 우리가 예상한대로다. 위에서 우리가 "navigation" 컨트롤러안의 예전것과 대체로 같은 authenticate() 함수의 구현을 보여주었다. 이는 백앤드 리소스 "/user"를 호출하여 authenticated 플래그를 설정한다 그리고 그 플래그의 값에 따라 선택적인 콜백을 호출한다. 만일 성공적이면, $location서비스 (바로 아래서 이것을 다룬다)를 사용하여 사용자를 homePath 로 보낸다.

여기 당신이 "auth" 모듈에서 하드코드하는 것을 원하지 않는 다양한 경로들을 설정하는 init() 함수의 기본 뼈대 구현이 있다:

init : function(homePath, loginPath, logoutPath) {
  auth.homePath = homePath;
  auth.loginPath = loginPath;
  auth.logoutPath = logoutPath;
}

다음은 clear() 함수 구현이다 더 간단하다:

clear : function() {
  auth.authenticated = false;
  $location.path(auth.loginPath);
  $http.post(auth.logoutPath, {});
}

이는 authenticated 플래그를 설정해지하고 사용자를 로그인페이지로 되돌려보낸다. 그다음 HTTP POST를 로그아웃 경로로 보낸다. POST 호출은 우리가 여전히 "단일" 어플리케이션으로부터 CSRF 보호기능을 가지고 있기 때문에 성공할 것이다. 만일 403 메세지를 본다면 에러메세지와 서버로그를 확인하고 그다음 보내진 XSRF 쿠키를 확인하는 필터를 확인해보자

거의 마지막 수정은 사용자가 인증하지 않았을경우 "로그아웃" 링크를 숨기기위한 index.html이다:

<html>
...
<body ng-app="hello" ng-cloak class="ng-cloak">
  <div ng-controller="navigation" class="container">
    <ul class="nav nav-pills" role="tablist">
          ...
      <li ng-show="authenticated()"><a href="" ng-click="logout()">logout</a></li>
    </ul>
  </div>
...
</html>

"navigation" 컨트롤러는 "auth" 서비스를 얻기 위해서 그리고  $rootScope에 있지않은 플래그의 값을 찾기 위해서 당신은 그냥 authenticated플래그를 바꿔주고 authenticated()함수를 호출하면 된다.

로그인 페이지로 리다이렉션하기 Redirecting to the Login Page

이제껏 우리가 홈페이지를 구현했던 방법은, (그냥 로그인함으로서) 사용자가 인증받았을 때 보여주는 약간의 컨텐트를 가지고 있는 것이다. 어떤 어플리케이션들은 이 방식으로 동작하고 어떤것은 그렇지않다. 어떤 어플은 다른 사용자 경험을 제공해주어, 사용자가 인증받을 때까지 로그인페이지이외에 아무것도 볼수 없다. 우리도 어떻게하면 어플리케이션에 이 패턴을 가지도록 바꿔보자.

로그인페이지와 함께 모든 컨텐트를 숨기는 건 전통적으로 많은 것에 영향을 미치는 중요한 관심사다:당신은 UI 모듈에 위치한 로그인 페이지를 보여주기 위한 어떠한 로직도 원하지 않는다 ( 이것은 어디서나 중복적어서 코드를 더묵 만들기 힘들게 하고 유지보수가 더 어려워진다). 스프링 시큐리티는 서버에서 이런 많은 것 영향을 미치는 모든 관심사에 대한 것이다. Filters 과 AOP 인터셉터의 정점에서 빌드되었기 때문이다. 불행하게도 이것은 이 단일 페이지 어플리케이션에서는 크게 도움이 되지않는다. 하지만 앵귤러는 우리가 원하는 이러한 패턴의 구현을 쉽게 만들어줄 몇가지 기능을 가지고 있다. 이 기능의 도움을 받아 우리는 "라우트 수정route changes"를 위한 리스너를 설치할 수 있다. 따라서 매번 사용자가 새 라우트로 이동된다 (예를 들어 메뉴바 또는 어떠한 것을 클릭할때), 또는 페이지가 처음으로 로드될때, 당신은 라우스틀 조사하여 필요시 바꿀 수 있다.

리스너를 인스톨하려면, 우리는 auth.init()함수에 추가적인 약간의 코드를 써줘야한다.("hello" 모듈이 불러질때, 동작하도록 이미 준비되었으므로):

angular.module('auth', []).factory(
    'auth',

    function($rootScope, $http, $location) {

      var auth = {

        ...

        init : function(homePath, loginPath, logoutPath) {
          ...
          $rootScope.$on('$routeChangeStart', function() {
            enter();
          });
        }

      };

      return auth;

    });

새로운 enter()함수에 할당된 간단한 리스너를 등록했다. 이제 당신은 "auth" 모듈 팩토리 함수(팩토리 객체 자체에 접근할 수 있는)에 이것을 구현해줘야한다: 

enter = function() { if ($location.path() != auth.loginPath) { auth.path = $location.path(); if (!auth.authenticated) { $location.path(auth.loginPath); } } }

로직은 간단하다: 만일 경로가 로그인 페이지가 아닌 다른 어떠한 경로로 바뀌면 경로값을 기록한다. 그뒤 만일 사용자가 인증되있지않으면 로그인페이지로 간다. 우리가 경로값을 저장하는 이유는 그럼으로서 우리가 성공적인 인증후 이 값으로 돌아갈 수 있기 때문이다. (스프링 시큐리티는 서버쪽의 이 기능을 가지고 있으며 이것은 사용자들에게 아주 멋진 기능이다). 성공 핸들러에 약간의 코드를 추가함으로서 authenticate() 함수안에서 이것을 할 수 있다:

authenticate : function(credentials, callback) { ... $http.get('user', { headers : headers }).success(function(data) { ... $location.path(auth.path==auth.loginPath ? auth.homePath : auth.path); }).error(...); },

인증에 성공하면, 우리는그냥 홈페이지나 가장 최근에 선택했던 경로(로그인 페이지가 아니라면)로 이동하게 설정하면 된다.

마지막 수정할 것중 하나는 사용자 경험을 더욱 획일적으로 만드는 것이다: 우리는 어플리케이션이 처음 시작할때 홈페이지 대신 로그인 페이지를 보여주려고 한다. 당신은 이미 authenticate() 함수안에 (로그인페이지로 리다이렉트하는) 로직을 가지고 있으며, 따라서 당신이 필요한 것은 (사용자가 이미 쿠키를 가지고 있지 않으면 실패하게 되는) 텅빈 credential을 가지고 인증하기 위해 init() 함수에 다음과 같이 약간의 코드를 넣어주는게 전부다:

init : function(homePath, loginPath, logoutPath) {
  ...
  auth.authenticate({}, function(authenticated) {
    if (authenticated) {
      $location.path(auth.path);
    }
  });
  ...
}

auth.path 가 $location.path()을 가지고 초기화 했다면, 사용자가 브라우저에 명시적으로 라우트를 타입해주었을 때도 동작할 것이다 (예를 들어 홈페이지를 먼저 불러오기 싫을 때):

(IDE에서 main() 메소드를 통해 또는 커맨드라인에서 mvn spring-boot:run를 사용하여) 어플리케이션을 시작하고 http://localhost:8080를 방문하고 결과를 확인해보자:

기억하기:  쿠키와 HTTP Basic credential에 대한 브라우저 캐시를 삭제해야한다. 크롬에서 새 incognito 창을 여는게 최선의 방법이다.

결 론 Conclusion

이번 섹션에서 우리는 (이 튜토리얼의 두번째 섹션 의 어플리케이션을 가지고 시작하여) 어떻게 하면 앵귤러 어플리케이션을 모듈화하는지 살펴봤다. 어떻게 로그인 페이지로 리다이렉트하는지, 어떻게 사용자에 의해 쉽게 북마크하고 타입할 수 있게 "자연스럽게" 라우트를 사용하는지. 우리는 이 튜토리얼의 마지막 몇 섹션에서 클라이언트 코드에 좀 더 집중했었고 우리가 섹션III-VI. 에서 만든 분산 아키텍쳐를 잠시 파보았다. 이것이 여기에서 바꾼 것들이 다른 어플리케이션에서 적용할 수 없다는 의미가 아니다. (실제로 이것은 꽤나 사소한것이다) - 단신 우리가 클라이언트 쪽에서 하는 일을 배우는 도안 서버쪽 코드를 간소화했을 뿐이다. 서버사이드 몇몇 기능을 우리는 간단하게 사용했었고 의논하였다 (예를 들면 스프링 MVC에서 "자연스러운" 라우트를 가능하게 하기 위해 뷰를 "forward"했었다) 이렇게 우리는 앵귤러와 스프링을 함께 동작시키자는 주제를 계속 이어왔고 이곳 저곳에 작은 수정을 함으로서 매우 잘 이루어냈다.


반응형

반응형

스프링 시큐리티와 앵귤러JS Spring Security and AngularJS


Part I : 단일 페이지 보안 어플리케이션 (A Secure Single Page Application)

Part II: 로그인페이지 (The Login Page)

Part III: 리소스 서버 (The Resource Server)

Part IV: API 게이트웨이 (The API Gateway)

Part V: OAuth2와 싱글사인온 (Single Sign On with OAuth2)

Part VI: 다중 UI 어플리케이션과 게이트웨이 (Multiple UI Applications and a Gateway)

Part VII: 모듈화한 AngularJS 어플리케이션  (Modular AngularJS Application)

Part VIII: Angular 어플리케이션 테스트 (Testing an Angular Application)


--------------------------------------------------------------------------------


다중 UI 어플리케이션과 게이트웨이 Multiple UI Applications and a Gateway

이 섹션에서 우리는 어떻게 스프링 시큐티리와 앵귤러JS로 단일 페이지 어플리케이션을 만드는지 계속 얘기해볼 것이다. 이제 두번째 섹션과 네번째 섹션에서 만들었던 시스템의 기능을 섞기위해 어떻게 스프링 클라우드와 스프링 세션을 함께 사용하는지 보여줄 것이다. 그리고 서로 다른 책임을 가지는 3개의 단일 페이지 어플리케이션을 만들어볼것이다. (네번째 섹션과 같은) 게이트웨이를 만드는 목표는 API리소스를 사용하는 것뿐만 아니라 백엔드로부터 UI를 불러오는데 사용된다. 우리는 백엔드에 인증을 통과하기 위해 게이트웨이를 사용함으로서 두번째 섹션의 토큰 논쟁을 간소화할 수 있다. 그럼 다음 우리는 게이트웨이에서 인증과 식별을 제어하고 있는 동안 백엔드에서 로컬 접근 결정 입자를 어떻게 만드는가 보여주기위해 시스템을 확장할 것이다. 이것은 일반적으로 분산 시스템을 만드는 수많은 혜택을 가진 강력한 모델이다:

기억하기: 만일 당신이 이섹션의 샘플을 동작시키려면 브라우저의 쿠키와 HTTP Basic credential에 대한 캐시값을 지워주어야 한다. 크롬에선 새 incognito 창을 쓰는게 최선의 방법이다.

타겟 아키텍쳐 Target Architecture

우리가 이제부터 만드려는 시스템의 기본 그림이다:

Components of the System

이 시리즈의 다른 샘플 어플리케이션과 같이 UI(HTML과 자바스크립트)와 리소스 서버를 가지고 있다. 네번째 섹션의 샘플같이 게이트웨이를 가지고 있으나 여기에선 UI의 일부가 아닌 별도로 분리되었다. UI는 효율적으로 백엔드의 일부가 되었다. 이것은 우리에게 재설정과 재구현의 기능에 더 많은 선택을 가져다 준다. 또한 우리가 앞으로 볼수 있듯 더 많은 혜택을 가져다 줄것이다.

브라우저의 모든 요청이 게이트웨이로 간다. 이것은 백엔드의 아키텍쳐에 대해 알 필요가 없다(기본적로 백엔드에 대해 아무것도 모른다.) 브라우저가 이 게이웨이에서 하는 일중 하나는 인증이다. 예를 들어 두번째 섹션과 같이 사용자명과 패스워드를 보내고 쿠키를 돌려받는다. 이어지는 요청에서 자동적으로 쿠키가 들어있고 게이트웨이는 이것으로 백엔드를 통과한다. 쿠키를 통과시키기위해 클라이언트에서 어떠한 코드도 필요하지않다. 백엔드는 쿠키를 사용하여 인증을 하고 모든 컴포넌트가 해당 사용자에 대한 같은 정보를 공유하는 세션을 공유하기 때문이다. 게이트웨이에서 쿠키가 억세스 토큰으로 변환되었던 다섯번째 섹션과는 반대로, 엑세스 토큰은 모든 백엔드 컴포넌트에 의해 각각 독립적으로 디코드되어져야 한다.

네번째 섹션에서 처럼, 게이트웨이는 클라이언트와 서버간의 연동을 간소화한다. 이는 보안을 다루는 작고 잘 정의된 표면으로 표현된다. 예를 들면, 우리는 Cross Origin Resource Sharing를 걱정할 필요가 없다. 이는 아주 쉽게 잘못될 수 있기 때문에 우리에게 안도감을 줄 것이다.

완전한 프로젝트의 소스코드는 이곳 Github에서 받을 수 있다. 따라서 그냥 프로젝트를 클론하여 당신이 원하는 곳에서 직접 돌려볼 수 있다. 이 시스템의 마지막 상태에 ("double-admin"이라는) 추가적인 컴포넌트가 있지만 지금은 당장 무시하도록하자 

백엔드 만들기 Building the Backend

이 아키텍쳐에서 백앤드는 세번째 섹션에서 만들었던 이례적으로 실제 로그인페이지가 필요하지 않았던 스프링 세션 샘플과 매우 유사하다. 우리가 여기서 원하는 것을 얻는 가장 쉬운 방법은 아마도 첫번째 섹션의 "기초" 샘플에서 UI를 떼어내고, 세번째 섹션으로부터의 "리소스"서버를 복사하는 것이다.  "기본" UI를 얻기위해 몇가지 의존성을 추가해주면 된다.(세번째섹션에서 스프링 세션을 처음 사용했을 때와 같이):

pom.xml
<dependency>
  <groupId>org.springframework.session</groupId>
  <artifactId>spring-session</artifactId>
  <version>1.0.0.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-redis</artifactId>
</dependency>

그리고 메인 어플리케이션 클래스에 @EnableRedisHttpSession 어노테이션을 추가하자:

UiApplication.java
@SpringBootApplication
@EnableRedisHttpSession
public class UiApplication {

public static void main(String[] args) {
    SpringApplication.run(UiApplication.class, args);
  }

}

이제 UI가 있지만 "/resource" endpoint는 필요없다. 이것을 마치면, 당신이 매우 간단한 ("basic"샘플과 동일한) 앵귤러 어플리케이션을 가지게 되어 이것의 행위의 이유나 테스트를 간소화할 수 있다.

마지막으로 우리는 이 서버가 백엔드로서 동작하길 원하므로 application.properties에 기본값이 아닌 포트값을 설정할 것이다.

application.properties

server.port: 8081
security.sessions: NEVER

만일 위의 설정이application.properties의 전체 컨텐트라면 어플리케이션은 보호될것이고 (시작시 로그레벨 INFO로 콘솔에 표시되는) 랜덤 패스워드를 가지는 "user"라 불리는 사용자의 접근이 허용된다. "security.sessions" 설정은 스프링 시큐리티가 인증 토큰으로 쿠키를 받아들인다는 의미로 만일 이미 존재하지 않는다면 그들을 만들지 않을 것이다.

리소스 서버 The Resource Server

리소스 서버는 우리가 이미 만들어놓은 샘플들중하나로 쉽게 만들수 있다. 두번째 섹션의 "spring-session" 리소스 서버와 동일하다: 단지 "/resource" endpoint과 분산 세션 데이터를 얻기 위한 @EnableRedisHttpSession. 우리는 이 서버를 기본 포트값이 아닌 특정 값을 설정해주어 세션에서 인증을 찾아볼 때 쓸 수 있길 원한다. 이것을 위해서 application.properties에 다음과 같이 설정해주자:

application.properties
server.port: 9000
security.sessions: NEVER

 여기 github에 필요시 둘러볼 수 있는 완전한 샘플이 있다.

게이트웨이 The Gateway

(작동하는 가장 간단한) 게이트웨이의 초기 구현을 위해, 우리는 아무것도 없는 스프링 부트 껍데기 어플리케이션을 가지고 @EnableZuulProxy 어노테이션을 추가한다. 우리가 첫번째 섹션에서 봤듯이 이것을 위해서 여러가지 방법이 있지만 프로젝트의 뼈대를 만들기 위해 Spring Initializr를 이용하는게 하나의 방법이다. Spring Cloud Initializr를 사용하면 똑같지만 스프링 클라우드 어플리케이션을 위해서 전보다 더 쉬워진다. 첫번째 섹션과 똑같은 커맨드라인을 순서대로 사용해보자:

$ mkdir gateway && cd gateway
$ curl https://cloud-start.spring.io/starter.tgz -d style=web \
  -d style=security -d style=cloud-zuul -d name=gateway \
  -d style=redis | tar -xzvf -

그 다음으로 당신이 선호하는 IDE에서 프로젝트를 (보통의 메이븐 자바 프로젝트로서) import하자 또는 커맨드라인에서 "mvn"을 써서 파일로 동작시켜도 된다. 바로 시작해보고 싶으면 github에 있는 버전을 이용하자 몇가지 추가적인 기능이 있지만 지금은 아직 필요없다.

아무것도 없는 빈 초기 어플리케이션으로 시작하여 우리는 스프링 세션 의존성을 추가하고 (위의 UI에서 처럼) 거기에  @EnableRedisHttpSession 어노테이션을 더하자:

GatewayApplication.java

@SpringBootApplication
@EnableRedisHttpSession
@EnableZuulProxy
public class GatewayApplication {

public static void main(String[] args) {
    SpringApplication.run(GatewayApplication.class, args);
  }

}

게이트웨이는 이제 동작할 준비가 되었다, 하지만 아직 우리의 백엔드 서비스에 대해 알지 못한다. application.yml에 다음을 설정하자 (만일 당신이 위의 것을 curl로 이미 했다면 application.properties 에서 이름을 바꿔주자):

application.yml
zuul:
  routes:
    ui:
      url: http://localhost:8081
    resource:
      url: http://localhost:9000
security:
  user:
    password:
      password
  sessions: ALWAYS

프록시에는 2개의 라우트가 있다, 하나는 각각 UI와 리소스 서버를 위한 것으로 우리는 기본 패스워드와 세션 저장 전략session persistence strategy을 설정하였다. (스프링 시큐리티에 항상 인증을 위해 세션을 만들라고 알려줌으로서). 이 마지막 단계는 우리가 인증하기를 원하기 때문에 세션이 게이트웨이에서 관리되기 때문에 중요하다. 

서버 동작시키기 Up and Running

우리는 이제 3개의 포트에서 동작하는 3개의 컴포넌트를 갖췄다. 브라우저의 http://localhost:8080/ui/ 를 열면 당신은 이제 HTTP Basic challenge를 받아야 하며 "user/password"로 (게이트웨이에 당신의 credential로서) 인증할 수 있다. 그리고 일단 인증받으면, 리소스 서버에 프록시를 거친 백앤드 호출을 경유하여, UI에서 greeting을 볼 수 있게 된다.

당신이 개발자툴을 사용중이라면 (보통 크롬에서 F12로 열수 있고 파이어폭스는 플러그인을 설치해야한다) 브라우저와 백엔드간의 연동과 당신의 브라우저에서 확인할 수 있다.  여기 정리해두었다:

VerbPathStatusResponse

GET

/ui/

401

Browser prompts for authentication

GET

/ui/

200

index.html

GET

/ui/css/angular-bootstrap.css

200

Twitter bootstrap CSS

GET

/ui/js/angular-bootstrap.js

200

Bootstrap and Angular JS

GET

/ui/js/hello.js

200

Application logic

GET

/ui/user

200

authentication

GET

/resource/

200

JSON greeting

브라우저가 홈페이지를 단일연동으로서 불러온다고 취급하기 때문에 401을 볼 수 없을 것이다. 모든 요청은 프록시되었다. ( 관리를 위한 Actuator endpoint상에는 아직 게이트웨이에 컨텐트가 없다)

만세, 이제 동작한다!. 당신은 두개의 백엔드 서버를 가졌다. 하나는 독립적인 수용능력과 고립테스트가 가능한 UI인데 이들은 당신이 인증을 위해 설정해둔 제어를 한 보안이 설정된 게이트웨이와 함께 연결되었다. 만일 백엔드가 브라우저에서 접근가능하지 않다해도 상관없다 (사실 아마도 당신이 물리적인 보안을 더욱 제어할 수 있는 점에서 장점이 될 것이다)

로그인 폼 추가하기 Adding a Login Form

첫번째 섹션의 "기본" 샘플에서 처럼, 우리는 이제 예를 들면 두번째 섹션으로부터 복사함으로서, 게이트웨이에 로그인폼을 추가할 수 있다. 우리가 이것을 할때 또한 기본 네비게이션 엘리면트를 게이트웨이에 추가해줄 수 있다. 그래서 사용자가 프록시의 UI 백엔드 경로를 알 수 없게 한다. 게이트웨이안에 "단일"UI로부터의 정적인 에셋assets을 먼저 복사하고 (어딘가의 <body/>안에 위치한) 홈페이지안의 로그인 폼을 삽입하고 메세지 랜더링을 삭제하자:

index.html
<body ng-app="hello" ng-controller="navigation" ng-cloak
	class="ng-cloak">
  ...
  <div class="container" ng-show="!authenticated">
    <form role="form" ng-submit="login()">
      <div class="form-group">
        <label for="username">Username:</label> <input type="text"
          class="form-control" id="username" name="username"
          ng-model="credentials.username" />
      </div>
      <div class="form-group">
        <label for="password">Password:</label> <input type="password"
          class="form-control" id="password" name="password"
          ng-model="credentials.password" />
      </div>
      <button type="submit" class="btn btn-primary">Submit</button>
    </form>
  </div>
</body>

메세지 랜더링 대신, 우리는 더 멋진 커다란 네비게이션 버튼을 만들 것이다:

index.html
<div class="container" ng-show="authenticated">
  <a class="btn btn-primary" href="/ui/">Go To User Interface</a>
</div>

github의 샘플을 보면, "로그아웃" 버튼을 가진 작은 네비게이션 바 또한 볼 수 있을 것이다. 여기 로그인 폼의 스크린샷을 보자:

Login Page

로그인 폼을 지원하려면, 우리는 <form/>안에 선언한  login() 함수를 구현한 navigation" 컨트롤러를 가진 자바스크립트가 필요하다. 그리고 authenticated 플래그를 설정해서 홈페이지가 사용자가 인증을 받았는지 안받았는지에 따라 다르게 랜더할 것이다. 예를 들어:

gateway.js
angular.module('gateway', []).controller('navigation',
function($scope, $http) {

  ...

  authenticate();

  $scope.credentials = {};

$scope.login = function() {
    authenticate($scope.credentials, function() {
      if ($scope.authenticated) {
        console.log("Login succeeded")
        $scope.error = false;
        $scope.authenticated = true;
      } else {
        console.log("Login failed")
        $scope.error = true;
        $scope.authenticated = false;
      }
    })
  };

}

authenticate() 함수의 구현은 두번째 섹션과 유사하다:

gateway.js
var authenticate = function(credentials, callback) {

  var headers = credentials ? {
    authorization : "Basic "
        + btoa(credentials.username + ":"
            + credentials.password)
  } : {};

  $http.get('user', {
    headers : headers
  }).success(function(data) {
    if (data.name) {
      $scope.authenticated = true;
    } else {
      $scope.authenticated = false;
    }
    callback && callback();
  }).error(function() {
    $scope.authenticated = false;
    callback && callback();
  });

}

authenticated 플래그럴 저장하기 위해 $scope을 사용할 것이다. 이 간단한 어플리케이션은 오직 하나의 컨트롤러만 가지고 있기 때문이다.

이제 이 향상된 게이트웨이를 동작해보면 UI를 위해 URL을 기억하는 대신, 우리는 그냥 홈페이리를 부른 다음 다음의 링크를 통하면된다. 인증된 사용자를 위한 홈페이지를 보자:

Home Page

백엔드의 접근결정 단위 Granular Access Decisions in the Backend

이제 우리의 어플리케이션이 기능적으로 두번째 섹션과 네번째 섹션에 있는 것과 유사해졌지만, 추가적인 전담의 게이트웨이를 가지고 있다. 이 추가 레이어의 장점은 아직 명확하진 않지만 우리가 시스템을 확장할때 좀 더 강점을 가질 것이다. 우리가 서로 다른 백엔드 UI에 노출하기하기 위해 이 게이트웨이를 사용한다고 가정하보자, 메인UI에서 컨텐트를 "감시감독하기 위한" 사용자를 위해, 그리고 특별한 롤을 가진 사용자에만 이 기능의 접근을 제한하고자 원할때 우리는 프록시뒤에 "어드민" 어플리케이션을 추가 할것이다. 그리고 그 시스템은 다음과 같다:

Components of the System

application.yml에 다음과 같이 새 컴포넌트(어드민)와 게이트웨이에 새 라우트가 있다.

application.yml

zuul:
  routes:
    ui:
      url: http://localhost:8081
    admin:
      url: http://localhost:8082
    resource:
      url: http://localhost:9000

"USER" 롤을 가진 사용자들에 이용가능한 지금의 UI는 위의 다이아그램의 게이트웨이 박스(녹색글씨)에 표시되어있다. "ADMIN"롤의 사용자는 어드민 어플리케이션으로 가야한다. 이 "ADMIN"을 위한 접근 결정은WebSecurityConfigurerAdapter설정을 통해서 게이트웨이에서 적용하거나 어드민 어플리케이션 자체에 적용할 수 있다.(밑에서 어떻게 하는지 살펴볼것이다)

뿐만 아니라 어드민 어플리케이션에서 "읽기권한READER"과 "쓰기권한WRITER" 를 구분해줘야한다면, 우리는 주요 관리자가 사용자의 역할을 조정할 수 있게 해주어야 한다. 이것이 해당 역할이 백엔드서버가 어디있는지 알 수 있는지에 대한 접근결정단위(granular access decision)이다. 게이트웨이에서 우리는 단지 사용자 계정에 역할이 필요한지 그리고 이 정보가 이용가능한지를 확실히 해주기만 하면 된다. 그러나 게이트웨이는 이것을 어떻게 해셕하는지에 대해 알필요가 없다. 게이스웨이에서 우리는 샘플어플리케이션에 스스로 사용자계정을 갖추도록 만들어준다: 

SecurityConfiguration.class

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

  @Autowired
  public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
      .withUser("user").password("password").roles("USER")
    .and()
      .withUser("admin").password("admin").roles("USER", "ADMIN", "READER", "WRITER")
    .and()
      .withUser("audit").password("audit").roles("USER", "ADMIN", "READER");
  }

}

"어드민" 사용자는 3가지 새로운 롤을 가질수 있다 ("관리자ADMIN","읽기권한READER",'쓰기권한WRITER") 그리고 또한  "관리자ADMIN"권한을 가지지만 "쓰기권한WRITER"은 없는 청강audit사용자를 추가했다.

여담: 제품화단계의 시스템에서 사용자 계정 데이터는 백앤드 데이터베이스에서 관리(대게, 디렉토리 서비스)되지 스프링 설정에 하드코드되지 않는다. 그러한 데이터베이스 접속하는 예제 어플리케이션은 인터넷에서 쉽게 찾을 수 있다 예를 들면 스프링 시큐리티 예제.


접근 권한은 어드민 어플리케이션으로 옮겨졌다. (이 백엔드의 어디에서나 요구되는) "관리자admin"역할을 위해, 스프링 시큐리티에 다음과같이해보자:

SecurityConfiguration.java
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

@Override
  protected void configure(HttpSecurity http) throws Exception {
    http
    ...
      .authorizeRequests()
        .antMatchers("/index.html", "/login", "/").permitAll()
        .antMatchers("/admin/**").hasRole("ADMIN")
        .anyRequest().authenticated()
    ...
  }

}

"읽기권한reader"와 "쓰기권한writer"를 위해 어플리케이션 자체를 나누었다. 어플리케이션이 자바스크립트로 구현되었으므로 우리는 접근결정을 만들어줘야한다. 이것을 하는 하나의 방법은 이것을 내장한 뷰를 가진 홈페이지를 만드는 것이다:

index.html
<div class="container">
  <h1>Admin</h1>
  <div ng-show="authenticated" ng-include="template"></div>
  <div ng-show="!authenticated" ng-include="'unauthenticated.html'"></div>
</div>

앵귤러JS는 하나의 표현식으로 "ng-include"속성값을 검토하여 템플릿을 불러올것이다.

 더 복잡한 어플리케이션은 아마 자체로 모듈화한 메카니즘을 사용할 것이다. 예를 들어 이 시리즈의 거의 모든 다른 어플리케이션에 사용했던 $routeProvider 서비스와 같이.

 template 변수는 우리의 컨트롤러에서 초기화된다. 먼저 유틸리티 함수를 정의하자:

admin.js
var computeDefaultTemplate = function(user) {
  $scope.template = user && user.roles
      && user.roles.indexOf("ROLE_WRITER")>0 ? "write.html" : "read.html";
}

그 후 컨트롤러가 로드될때 유틸리티 함수를 사용하자:

admin.js
angular.module('admin', []).controller('home',

function($scope, $http) {

  $http.get('user').success(function(data) {
    if (data.name) {
      $scope.authenticated = true;
      $scope.user = data;
      computeDefaultTemplate(data);
    } else {
      $scope.authenticated = false;
    }
    $scope.error = null
  })
  ...

})

어플리케이션은 먼저 (이 시리즈에서 해왔던) 일반적인 "/user" endpoint를 살펴본다. 그다음 데이터를 추출하고 authenticated 플래그를 설정한다. 만일 사용자가 인증받았다면 사용자 데이터를 검토함으로서 템플릿을 산출한다.

백엔드에서 이 함수를 지원하기 위해, 우리는 endpoint가 필요하다. 예를 들면 메인 어플리케이션 클래스에서:

AdminApplication.java

@SpringBootApplication
@RestController
@EnableRedisHttpSession
public class AdminApplication {

  @RequestMapping("/user")
  public Map<String, Object> user(Principal user) {
    Map<String, Object> map = new LinkedHashMap<String, Object>();
    map.put("name", user.getName());
    map.put("roles", AuthorityUtils.authorityListToSet(((Authentication) user)
        .getAuthorities()));
    return map;
  }

  public static void main(String[] args) {
    SpringApplication.run(AdminApplication.class, args);
  }

}

"/user" endpoint에서 "ROLE_"접두사가 붙은 역할 이름을 받아옴으로서 우리는 인증의 종류를 구별할 수 있다.(이것은 스프링 시큐리티가 하는 일이다) 그러므로 "ROLE_" 접두사가 붙은 역할은 스프링 시큐리티 설정에서가 아니라 명확한 "역햘"의 메소드이름으로 작업에 초점을 맞추기위해 자바스크립트에서 필요하다.

왜 우리가 여기에 있지? Why are we Here?

이제 우리는 2개의 독립적인 UI와 백엔드 리소스 서버를 가진 작고 멋진 시스템을 갖추었다. 모든 것이 게이트웨이에서 똑같은 인증에 의해 보호되고있다. 게이트웨이가 마이크로 프록시(micro-proxy)로서 역할을 하고 있다는 사실은 백엔드 시큐리티에 대한 구현을 극도록 쉽게 만들어주었다. 그리고 그들은 자기들의 비지니스 관심사에만 집중할 수 있게 되었다. 스프링 세션의 사용은 또다시 수많은 번거로운 작업과 잠재적인 에러를 피할수 있게 해주었다.

백앤드가 선호하는 어떤 종류의 인증법을 독립적으로 가진다는 것(예를들어 당신이 물리적 주소와 로컬 credential의 셋트를 알고 있다면 UI로 직접 갈수 있다)은 매우 강력한 기능이다. 게이트웨이는 사용자를 인증하고 백엔드의 접근 규약을 만족시키는 사용자에 메타데이터를 할당할 수 있는한 관련이 없는 제약사항의 집합을 강제한다.  이는 백엔드 컴포넌트를 독립적으로 테스트하고 개발할수 있게 해주는 훌륭한 디자인이다. 만일 우리가 필요하다면 게이트웨이의 인증을 위해 외부 OAuth2 서버로 회귀할 수 있다 (다섯번째 섹션처럼 또는 완전히 다른 어떤한 것도 가능하다). 이경우도 백엔드는 손댈 필요가 없다.

이 아키텍쳐(인증을 제어하는 단일의 게이트웨이, 모든 컴포넌트에서 공유하는 세션 토) 의 보너스 기능은 "단일 로그아웃signgle logout"이다. 다섯번째 섹션에서 구현하기 복잡했던 기능으로 여기선 그냥 된다. 더 정확하게, 단일 로그아웃에 대한 사용자 경험에 대한 하나의 특별한 접근법이 우리의 완성된 시스템에 자동적으로 이용가능하다: 각각 개별의 UI가 같은 방식으로 "로그아웃"을 구현했다는 가정하에, 만일 사용자가 UI들중 아무 한군데에서( 게이트웨이, UI백엔드 또는 어드민 백엔드) 로그아웃을 하면, 그 사용자는 모든 곳에서 로그아웃이 된다(세션 무효화함으로서)

감사의 말: 이 시리즈를 만드는데 도움을 준 모든분께 감사드리고 싶다. 특히 각각의 섹션과 소스코드를 주의깊게 리뷰를 해주고, 내가 잘 알고 있다고 생각한 부분에서 조차 알지못했던 몇가지 트릭을 알게 해준 Rob Winch 와 Thorsten Spaeth.에게 감사하다. 첫번째 섹션이 발행된 이후 많은 수정을 하지 못했지만 다른 섹션들은 읽은 사람들의 통찰력과 답글들과 진화를 거듭했다. 그러므로 포럼에 참여하여 수고를 아끼지않아준 사람들과 이 섹션을 읽어준 사람들께도 감사하다.


반응형

+ Recent posts