반응형

스프링 부트 1.5 릴리즈 노트

원문: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-1.5-Release-Notes

스프링 부트 1.4에서 업그레이드된 것

스프링부트 1.4이후 디프리케이트된것들

스프링부트 1.4에서 디프리케이트된 클래스, 메소드 그리고 프로퍼티들이 이번 릴리즈에서 제거되었다. 업그레이드전 디프리케이트된 메소드를 호출하는지 확인하자. 특히 HornetQ와 벨로시티 Velocity서포트가 삭제되었다.


이름이 바뀐 스타터들

스프링부트 1.4에서 이름이 바뀐 다음의 스타터들이 이번에 삭제되었다. 만일 "해결할수 없는 의존성 Unresolved dependency" 에러가 뜬다면 사용중인 스타터 이름이 올바른지 확인해보자:

  • spring-boot-starter-ws → spring-boot-starter-web-services

  • spring-boot-starter-redis → spring-boot-starter-data-redis

@ConfigurationProperties validation

JSR-303 규약 어노테이션을 사용하는 @ConfigurationProperties 클래스를 가지고 있다면 이제 그것들에 추가적으로 @Validated 어노테이션을 해줘야한다. 이미 있는 유효성은 여전히 잘 동작할것이지만 로그에 warning이 뜬것이다. 머지않아 @Validated 이 없는 클래스는 사용할 수 없을 것이다. 

스프링 세션 저장 Spring Session store

이제껏 스프링 세션과 레디스를 특별한 설정없이 사용해왔다면, 레디스는 자동으로 세션을 저장하는데 사용되었다. 이제는 저장 타입을 명시해주어야한다. 레디스와 스프링 세션을 사용하는 사용자들은 다음의 설정을 추가 해주어야한다:

spring.session.store-type=redis

액츄에이터 보안 Actuator security

Actuator "sensitive" endpoints are now secure by default (even if don’t have a dependency on "Spring Security"). If your existing Spring Boot 1.4 application makes use of Spring Security (and doesn’t have any custom security configuration) things should work as before. If your existing Spring Boot 1.4 application has custom security configuration and you wish to have open access to your sensitive endpoints, you will need to explicitly configure that in your security configuration. If you’re upgrading a Spring Boot 1.4 application that doesn’t have dependency on Spring Security and you wish to retain open access to your sensitive endpoints you’ll need to set management.security.enabled to false. See the updated reference documentation for more details.

The default role required to access the endpoints has also changed from ADMIN to ACTUATOR. This is to prevent accidental exposure of endpoints if you happen to use the ADMIN role for other purposes. If you want to restore Spring Boot 1.4 behavior set the management.security.roles property to ADMIN.

인메모리 매트릭스 리파지토리 InMemoryMetricRepository

InMemoryMetricRepository는 더이상 직접적으로 MultiMetricRepository를 구현하지않는다.  정식 InMemoryMetricRepository에 의해 뒷받침되어 MultiMetricRepository 인터페이스를 만족시키는 새로운 InMemoryMultiMetricRepository 빈이 이제 등록되었다. 대부분의 사용자가 MetricRepository 또는 MultiMetricRepository 인터페이스를 사용하고 있으므로 이 변화는 직접 보여지지않는다.  

spring.jpa.database

The spring.jpa.database 는 spring.datasource.url 프로퍼티를 통한 일반적인 데이터 베이스를 자동으로 찾아준다. 만일 spring.jpa.database를 수동으로 정의하며 사용해왔고 일반적인 데이터베이스를 사용하고 있다면 이 프로퍼티들을 다 없애버려도 될것이다.

하나 이상의 Dialect 를 가진 몇몇의 데이터베이스 (이를테면 Microsoft SQL Server 는 3개를 가지고있다) 그리고 당신이 사용중인 데이터베이스의 버젼이 설정한 것과 맞지 않는다면, 이전에 잘 작동한 설정을 가지고 하이버네이트의 Dialect자동 찾기에 맡기기 원한다면 spring.jpa.database=default 로 설정하자. 대안으로  spring.jpa.database-platform 프로퍼티를 사용하여 당신의 dialect를 설정할 수 있다.

@IntegrationComponentScan

스프링 Integration의 @IntegrationComponentScan 어노테이션은 이제 자동 설정된다. 권장되는 프로젝트 구조를 따른다면 당신은 이것을 지워줘야한다.

ApplicationStartedEvent

현재 당신의 코드에서 ApplicationStartedEvent를 위한 리스너 listener가 있다면,  ApplicationStartingEvent를 쓰도록 코드를 리팩토링 해주어야한다. 우리는 이 클래스가 무엇을 하는지 더 정확하게 알수 있게 이름을 바꾸었다.

Spring Integration Starter

spring-boot-starter-integration POM 은 더이상 spring-integration-jmx를 포함되지 않는다. 만일 당신이 Spring Integration JMX support가 필요하다면 spring-integration-jmx 의존성을 스스로 추가해줘어야한다.

Devtools excluded by default

메이븐과 그레들 플러그인 둘다 이제 기본적으로 "fat" jars에 spring-boot-devtools jar를 패키징 하지않는다. 당신이 devtools 원격 서포트를 사용중이라면, 이제 당신의 build.gradle 또는 pom.xml 파일에 excludeDevtools 프로퍼티를 명시적으로 설정해주어야한다.

Gradle 1.x

스프링 부트 그래들 플러그인은 이제 더이상 그래들 1.x와 그래들 2.x 초기버전과 호환되지 않는다. 그래들 2.9 이상 버전인지 확인하자.

Remote CRaSH shell

불행히도 remote SSH support을 제공하고저 스프링부트에서 사용하는 CRaSH project 는 더이상 유지가 되지않고 있으므로 우리는 remote actuator SSH support을 디프리케이트 하기로 결정하였고 스프링 부트 2.0에서 완전히 제거될 예정이다.

OAuth 2 Resource Filter

OAuth2 리소스 필터의 기본 순서가 3에서 SecurityProperties.ACCESS_OVERRIDE_ORDER - 1 로 변경되었다. 이 지점은 the actuator endpoints 바로 뒤이고 basic authentication filter chain 전이다. 이 기본값은 security.oauth2.resource.filter-order = 3 으로 설정하면 예전값으로 되돌릴 수 있다.

JSP servlet

JSP 서블릿응ㄴ 이제 더이상 기본값으로 개발모드가 아니다. 개발 모드는 DevTools을 쓸때 자동적으로 활성화 된다.  server.jsp-servlet.init-parameters.development=true.설정을 통해 명시적으로 활성화 할 수도 있다. 

Ignored paths and @EnableWebSecurity

스프링 부트 1.4 또는 이전 버전에서 Actuator는 항상 어떤 Ignored paths를 설정하여 @EnableWebSecurity의 사용을 무시해왔다. 이것은 1.5에서 바로 잡았으며  @EnableWebSecurity 를 사용하면 당신에게 완전한 제어가 가능했던 웹 보안의 자동 설정이 꺼질 것이다.

새로운 사항 및 주목할만한 점

Tip
Check the configuration changelog for a complete overview of the changes in configuration.

Third-party library upgrades

서드파티 라이브러리의 버전이 최신 버젼으로 업그레이드 되었다. 이 업데이트는 pring Data Ingalls, Jetty 9.4, JooQ 3.9, AssertJ 2.6.0, Hikari 2.5 그리고 Neo4J 2.1 이며 몇몇 메이븐 플러그인도 업그레이드 되었다.

Loggers endpoint

새로운 액츄에이터 loggers endpoint는 동작중인 어플리케이션의 로깅 레벨을 바꿀수 있게 해준다. 이는 JMX와 MVC endpoint에도 사용할수있다. 예를 들면 MVC endpoint의 로깅레벨을 바꾸려면 다음의 JSON를 /loggers/com.yourcorp.application에  post로 이슈해주면 된다:

{
  "configuredLevel": "DEBUG"
}

JMX endpoint를 사용하는 로거를 업데이트하려면  setLogLevel 를 사용하면 되는데 자세한 사항은 the updated documentation에서 확인하자.

Apache Kafka support

스프링 부트 1.5는 spring-kafka프로젝트를 통해 아파치 Kafka 자동설정을 지원한다. 손쉽게 Kafka를 사용하려면  spring-kafka 의존성과 `spring.kafka.* 어플리케이션 프로퍼티를 적절하게 설정해주면 된다.

Kafka로 부터 메세지를 받으려면 어노테이션 메소드를 쓰는게 제일 간단하다: 

@Component
public class MyBean {

    @KafkaListener(topics = "someTopic")
    public void processMessage(String content) {
        // ...
    }

}

Cloud Foundry actuator extensions

Spring Boot’s actuator module now includes additional support that is activated when you deploy to a compatible Cloud Foundry instance. The /cloudfoundryapplication path provides an alternative secured route to all NamedMvcEndpoint beans.

Cloud Foundry management UIs can make use of the endpoint to display additional actuator information. For example, Pivotal Cloud Foundry shows health information next to the application status:

apps manager

You can read more about the Cloud Foundry endpoint in the reference documentation and for and example of the kinds of things it can be used for you can read this blog post about PCF 1.9.

LDAP support

스프링 부트는 이제 어떤 표준 규약을 따르는 LDAP 서버를 자동설정해준다. 뿐만 아니라 Unbounded를 사용하여 내장 인메모리 LDAP 서버를 지원한다.

자세한 사항은 the documentation를 확인하자.

AuditEvents Endpoint Support

A new AuditEventsJmxEndpoint bean now allows you to query previously recorded AuditEvents over JMX. The MBean exposes AuditEventRepository find methods via getDataoperations. Audits are automatically recoded for authentication and authorization event and you can record your own events using the AuditEventRepository. That information is also exposed by a new /auditevents MVC endpoint.

Transaction manager properties

이제  spring.transaction.* 프로퍼티를 사용하여 PlatformTransactionManager를 자동설정해주는 다양한 aspects를 설정하는것이 가능해졌다. 현재는 "default-timeout" 과  rollback-on-commit-failure 프로퍼티가 지원된다.

JmxEndpoint interface

새로운 JmxEndpoint 인터페이스가 소개되어 당신이 액츄에이터 actuator endpoint를 JMX를 통해서만 노출되도록 개발할 수 있다. 이 인터페이스는 이미 MVC only endpoint를 제공해주는  MvcEndpoint 인터페이스와 매우 유사하다.

Vendor specific flyway migrations

You can now define flyway migration that are specific to a database vendor. To use vendor specific migrations, set the flyway.locations property as follows:

flyway.locations=db/migration/{vendor}

See this how-to for more details.

Deprecation level

It is now possible to define a deprecation level for a property in the metadata. The level can either be warning (default) or error.

Here is an example of manual metadata for a property that got moved in Spring Boot 2:

{
  "name": "server.context-parameters",
  "type": "java.util.Map<java.lang.String,java.lang.String>",
  "description": "ServletContext parameters.",
  "deprecation": {
    "level": "error",
    "replacement": "server.servlet.context-parameters"
  }
}

So far, when a deprecated property is not bound anymore, we remove the metadata altogether. This new feature allows to flag a property (server.context-parameters here) as an error. Newer versions of your favorite IDE should use that to offer assistance.

테스팅 업데이트 Testing updates

이제 @Test…​a로 시작하는 어노테이션에 의해 임포트import되던 자동설정을 제외할 수 있게 되었다. 모든 현존하는  @Test…​어노테이션들은 이제 excludeAutoConfiguration 속성값을 가지게 되었다. 또 다른 방법으로 당신의 테스트코드에 직접적으로  @ImportAutoConfiguration(exclude=…​)를 추가해줘도 된다.

스프링 부트 1.5에서 새로운 @JdbcTest 어노테이션이 소개되어 직접 JDBC과 상호연동되는 테스트를 할 수 있다.

Custom fat jar layouts

스프링 부트 메이븐과 그래들 플러그인은 이제 far jar 레이아웃을 커스터마이즈할 수 있게 지원한다. 이 기능은 스프링부트의 영역 밖에서 개발된  다음과 같은 실험적인 레이아웃을 가능하게 해준다. 더 자세한 업데이트된 문서를 참고하자

JmsTemplate customizations

이제  spring.jms.template.* 네임스페이스의 추가된 키값을 사용하여 자동설정된  JmsTemplate을 커스터마이즈 할수 있게 되었다.

Miscellaneous

  • Mockito 2.x 은 이제  @MockBean 으로 사용할 수 있다.(Mockito 1.9 와 호환가능함)

  • 내장 런쳐 스크린트는 이제 a force-stop을 지원한다. 

  • 카산드라를 위한 새로운 health check가 추가되었다.

  • 카산드라 사용자 정의 타입들이 이제 해결되었다. (스프링 데이터의 SimpleUserTypeResolver를 통해)

  • The skip 프로퍼티는 이제  스프링 부트 메이븐 플러그인  run 에서, 'stop' 그리고 'repackage` goals 에서 사용할 수 있다.

  • 복수개의 main m메소드 클래스가 있다면, 메이븐과 그래들 플러그인은 이제 자동적으로  @SpringBootApplication 어노테이션이 있는것을 실행할 것이다.

Deprecations in Spring Boot 1.5

  • TomcatEmbeddedServletContainerFactory.setTldSkip has been deprecated in favor of setTldSkipPatterns

  • ApplicationStartedEvent has been replaced by ApplicationStartingEvent

  • Several constants in LoggingApplicationListener have been replaced by versions in LogFile

  • Caching with Guava has been deprecated since Guava support will be dropped in Spring Framework 5. Upgrade to Caffeine

  • CRaSH support has been deprecated since it’s no longer actively maintained

  • Several protected methods in EndpointMBeanExporter have been deprecated following the introduction of JmxEndpoint

  • SearchStrategy.PARENTS has been replaced with SearchStrategy.ANCESTORS.

  • Apache DBCP support has been deprecated in favor of DBCP 2

  • The server.undertow.buffers-per-region property has been deprecated because it is not used (see UNDERTOW-587)

  • @AutoConfigureTestDatabase has been moved from org.springframework.boot.test.autoconfigure.orm.jpa to org.springframework.boot.test.autoconfigure.jdbc

프로퍼티 이름변경 Property Renames

  • server.max-http-post-size 프로퍼티는 특정기술 형태값으로 바뀌었다. (예를 들면, server.tomcat.max-http-post-size)

  • spring.data.neo4j.session.scope 프로퍼티는 제거되었다.


반응형

반응형

스프링 부트와 OAuth2

Spring Boot And OAuth2

(원문소스: https://spring.io/guides/tutorials/spring-boot-oauth2/)

Authorization 서버 돌리기 Hosting an Authorization Server

이 섹션에서 우리는 우리가 만들 앱을 여전히 페이스북과 Github을 사용하여 인증이 가능하지만 스스로 억세스 토큰을 만들 수 있는 완전무결fully-fledged한 OAuth2 인가서버Authorization Server로 만들도록 수정할 것이다. 이들 토큰은 이후 보호된secure 백엔드 리소스에서 사용되거나 똑같은 방식으로 보호되고 있는 다른 어플리케이션과의 SSO를 하는데 쓸 수 있다.

인증 설정을 정리하기 Tidying up the Authentication Configuration

인가서버Authorization server 기능을 시작하기 전에, 우리는 두개의 외부 제공자를 위한 설정코드를 깔끔하게 정리해보자. ssoFilter() 메소드안의 몇몇 코드가 중복되어있으므로 이를 공유 메소드로 가져올 것이다:

SocialApplication.java
private Filter ssoFilter() {
  CompositeFilter filter = new CompositeFilter();
  List<Filter> filters = new ArrayList<>();
  filters.add(ssoFilter(facebook(), "/login/facebook"));
  filters.add(ssoFilter(github(), "/login/github"));
  filter.setFilters(filters);
  return filter;
}

예전 메소드의 중복된 코드를 가지는 새로운 편리한 메소드이다:

SocialApplication.java
private Filter ssoFilter(ClientResources client, String path) {
  OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter(
      path);
  OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(client.getClient(),
      oauth2ClientContext);
  facebookFilter.setRestTemplate(facebookTemplate);
  facebookFilter.setTokenServices(new UserInfoTokenServices(
      client.getResource().getUserInfoUri(), client.getClient().getClientId()));
  return facebookFilter;
}

앱의 최종 버전에서 별도의 @Beans으로 선언된OAuth2ProtectedResourceDetails 그리고 ResourceServerProperties를 합친  새로운 wrapper 객체 ClientResources를 사용하였다:

SocialApplication.java
class ClientResources {

  private OAuth2ProtectedResourceDetails client = new AuthorizationCodeResourceDetails();
  private ResourceServerProperties resource = new ResourceServerProperties();

  public OAuth2ProtectedResourceDetails getClient() {
    return client;
  }

  public ResourceServerProperties getResource() {
    return resource;
  }
}

이 Wrapper와 전에 사용한 같은 YAML 설정을 함께 사용할 수 있지만, 각각 제공자를 위한 단일 메소드를 가지게 되었다:

SocialApplication.java
@Bean
@ConfigurationProperties("github")
ClientResources github() {
  return new ClientResources();
}

@Bean
@ConfigurationProperties("facebook")
ClientResources facebook() {
  return new ClientResources();
}

인가서버 돌리기 Enabling the Authorization Server

우리가 우리의 앱에 OAuth2 인가서버를 돌리려는데, 최소한 몇몇 기본기능(하나의 클라이언트와 억세스토큰을 만드는 능력)으로 시작하려고 수많은 삽질을 할 필요없다, 하나의 인가서버Authorization Server는 종단의 묶음이상이하도 아니다. 이들은 스프링 MVC 핸들러로서 스프링 OAuth2에서 구현되었다. 우리는 이미 보호된secure 어플리케이션을 가지고 있기때문에, 실제로 @EnableAuthorizationServer 어노테이션을 추가해주기만 하면 된다:

SocialApplication.java
@SpringBootApplication
@RestController
@EnableOAuth2Client
@EnableAuthorizationServer
public class SocialApplication extends WebSecurityConfigurerAdapter {

   ...

}

이 곳에 새 어노테이션을 추가함으로서, 스프링 부트는 우리가 지원하고 싶은 OAuth2 클라이언트에 약간의 디테일만 제공해주면, 그에 필요한 종단을 설치하고 그들에 시큐리티 설정을 해줄 것이다:

application.yml
security:
  oauth2:
    client:
      client-id: acme
      client-secret: acmesecret
      scope: read,write
      auto-approve-scopes: '.*'

이 클라이언트는 우리가 외부제공자로서 필요한 facebook.client* and github.client* 와 동일하다. 외부제공자를 우리 앱에서 사용하기 위해 우리는 클라이언트 id 와 secret을 받아서 등록해 주어야 한다. 우리의 경우, 이미 스스로 똑같은 기능을 제공하고 있으기 때문에 작동확인을 위한 (최소한 하나의) 클라이언트가 필요하다.

우리는 모든 스쿠프와 정규식 매칭되는 auto-approve-scopes설정을 하였다. 이는 우리가 이 앱을 실제 시스템에 돌릴때는 필요하지않지만,  스프링 OAuth2가 우리의 사용자가 억세스 토큰을 요구할 때 설정된게 없다면 사용자에게 팝업을 띄울 대체자나 whitelabel 승인페이지 없이 빠르게 무언가를 동작할 수 있게 해준다. 토큰을 승인하는 명시적 승인절차를 추가하려면 UI를 제공하여 (/oauth/confirm_access위치의) whitelabel 버전을 대체해주어야 한다.

억세스토큰 받는 법 How to Get an Access Token

억세스 토큰은 이제 우리의 새 인가서버에서 사용이 가능하다. 토큰을 얻는 가장 간단한 방법은 "acme" 클라이언트로서 하나를 긁어오는 것이다. 앱을 실행하고 다음과 같이 curl을 보내면 이것을 확인할 수 있다:

$ curl acme:acmesecret@localhost:8080/oauth/token -d grant_type=client_credentials
{"access_token":"370592fd-b9f8-452d-816a-4fd5c6b4b8a6","token_type":"bearer","expires_in":43199,"scope":"read write"}

클라이언트 credential 토큰은 어떤한 환경하에서(이를테면 토큰 종단이 동작하는지 테스트하는 등) 유용하다. 하지만, 우리 서버의 모든 기능을 장점을 취하려면 사용자들이 토큰을 만들수 있게 해줘야한다. 우리 앱에 보통의 사용자 행동을 통해 토큰을 받으려면, 사용자를 인증할 수 있어야 한다. 앱이 시작할 때 로그를 주의깊게 살펴보면, 아마 기본 스프링부트 사용자 (Spring Boot User Guide 참고)를 위한 임의 패스워드에 대한 로그를 볼 수 있을 것이다. 당신은 이 패스워드를 "사용자" ID와 같이 사용하여 사용자 방식으로 토큰을 받을 수 있다:

$ curl acme:acmesecret@localhost:8080/oauth/token -d grant_type=password -d username=user -d password=...
{"access_token":"aa49e025-c4fe-4892-86af-15af2e6b72a2","token_type":"bearer","refresh_token":"97a9f978-7aad-4af7-9329-78ff2ce9962d","expires_in":43199,"scope":"read write"}

"…​"이 있는 곳을 실제 패스워드로 바꿔줘야한다. 이것을 "패스워드 승인password grant"라고 부르는데 억세스 토큰을 위해 사용자아이디와 패스워드를 교환하는 것이다. 패스워드 승인은 당신이 credential을 저장하거나 유효한지 확인할수 있는 로컬 사용자 데이터베이스를 가지고 있는 네이티브 또는 모바일 어플리케이션에 적합하다. 웹앱 또는 어떤 "소셜"로그인을 가진 앱에서는 "인가코드 승인authorization code grant"이 필요하다. 이는 리다이렉트를 처리하기 위해 그리고 외부제공자로부터 사용자 인터페이스를 랜더할수 있는 브라우저가 필요하다는 의미이다.

클라이언트 어플리케이션 만들기 Creating a Client Application

우리 인가서버를 위한 클라이언트 어플리케이션은 그 자체로 하나의 웹어플리케이션이다. 스프링 부트로 손쉽게 만들수 있는데 다음 예제를 보자:

ClientApplication.java
@EnableAutoConfiguration
@Configuration
@EnableOAuth2Sso
@RestController
public class ClientApplication {

	@RequestMapping("/")
	public String home(Principal user) {
		return "Hello " + user.getName();
	}

	public static void main(String[] args) {
		new SpringApplicationBuilder(ClientApplication.class)
				.properties("spring.config.name=client").run(args);
	}

}

클라이언트를 위한 요소들은 (단지 사용자의 이름을 출력하는) 하나의 홈페이지와 (spring.config.name=client를 통한) 설정파일을 위한 명시적 이름이다. 우리가 이 앱을 돌리면 우리가 다음과 같이 제공한 설정파일을 찾을 것이다:

client.yml
server:
  port: 9999
security:
  oauth2:
    client:
      client-id: acme
      client-secret: acmesecret
      access-token-uri: http://localhost:8080/oauth/token
      user-authorization-uri: http://localhost:8080/oauth/authorize
    resource:
      user-info-uri: http://localhost:8080/me

이 설정은 우리가 메인 앱에서 사용한 값들과 거의 비슷한 것 같지만, 페이스북이나 Github 대신에 "acme" 클라이언트를 가진다. 이 앱은 메인 앱과의 충돌을 피하기 위해 9999포트를 통해 동작되며 우리가 아직 구현하지 않은 사용자 정보 종단인 "/me"를 참조한다.

사용자 정보 종단 보호하기 Protecting the User Info Endpoint

우리가 페이스북과 Github을 사용한 것과 같이 새 인가서버에서 싱글사인온을 사용하려면, 생성된 억세스 토큰에 의해 보호되는 /user 종단을 만들어줘야한다. 지금까지 우리는 /user 종단을 가지고 있었고 사용자가 인증할 때 만들어진 쿠키에 의해 보호되고 있었다. 이를 추가적으로 로컬에서 승인된 억세스 토큰으로 보호하려면, 현존의 종단을 재사용하여 새로운 경로로 alias를 만들어주면 된다:

SocialApplication.java
@RequestMapping({ "/user", "/me" })
public Map<String, String> user(Principal principal) {
  Map<String, String> map = new LinkedHashMap<>();
  map.put("name", principal.getName());
  return map;
}

 우리는 브라우저에 노출하고 싶지 않는 정보를 숨기기 위해, 또한 두개의 외부 인증 제공자사이의 종단의 행동을 구별하기 위해 Principal을 Map 으로 변환하였다. 원칙적으로 우리는 여기에 제공자의 구제적인 고유식별자 또는 만일 이용가능하다면 이메일과 같더 자세한 정보를 추가 할 수 있다.

이제 우리의 앱에 의해 만들어진 억세스 토큰에 보호되는 "/me" 경로는 (인가서버일 뿐아니라) 하나의 리소스 서버이다. 우리는 새 설정클래스를 만들었다. (메인앱의 n 내부 클래스로서, 하지만 이는 또한 별도의 독립실행가능한standalone 클래스로 나눌 수 있다.):

SocialApplication.java
@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration
    extends ResourceServerConfigurerAdapter {
  @Override
  public void configure(HttpSecurity http) throws Exception {
    http
      .antMatcher("/me")
      .authorizeRequests().anyRequest().authenticated();
  }
}

추가적으로 우리는 메인 어플리케이션의 시큐티리를 위해 @Order를 명시해줘야한다:

SocialApplication.java
@SpringBootApplication
...
@Order(6)
public class SocialApplication extends WebSecurityConfigurerAdapter {
  ...
}

@EnableResourceServer 어노테이션은 기본값으로 @Order(3)를 가지는 시큐리티 필터를 만든다. 메인 어플리케이션 시큐리티를 @Order(6)로 옮김으로서 우리는 "/me"이 우선순위를 가지도록 보장해줄 수 있다.

OAuth2 클라이언트 테스트하기 Testing the OAuth2 Client

두개의 앱을 모두 동작시키고 브라우저오 127.0.0.1:9999를 방문하여 새 기능을 테스트해보자. 클라이언트 앱을 로컬 인가서버로 리다이렉트될 것이고 이후 사용자에게 페이스북이나 Github으로 인증을 선택하도록 할 것이다.  일단 테스트 클라이언트로 제어가 완전히 되돌아오면, 로컬 억세스 토큰은 승인되고 인증이 완료된다. (브라우저에서 "Hello" 메시지를 볼 수 있을 것이다). 만인 이미 Github이나 페이스북으로 인증이 되어있다면 이 원격 인증을 아마 알아차리지 못했을 것이다.

 테스트 클라이언트 앱에서 "localhost"를 사용하지 말자. 이는 메인 앱으로부터 쿠키를 빼앗아오기 때문에 인증이 엉망이 되어버린다. 만일 127.0.0.1이 "localhost"와 매핑되어 있지 않으면, 당신은 당신의 OS에서 이를 설정해주어야한다. (이를테면 "/etc/hosts") 또는 만일 있다면 다른 로컬 주소를 사용하자.

비인증 사용자를 위한 에러페이지 추가하기 Adding an Error Page for Unauthenticated Users

이 섹션에서 우리는 이전에 만든 Github 인증으로 전환하는 logout 앱을 수정하여 인증받지 못한 사용자를 위한 약간의 처리를 추가해볼 것이다. 동시에 우리는 특정 Github 조직에 속해있는 사용자들만 인증하도록 허용하는 규칙을 포함하도록 인증로직을 확장해 볼 것이다. "조직organization"은 Github만의 특정 개념이지만 유사한 규칙을 이를테면 구글같은 당신이 특정 도메인으로부터의 사용자만을 인증하도록 다른 제공자에게 적용할 수 있다

Github으로 전환하기 Switching to Github

logout 예제는 OAuth2 제공자로서 페이스북을 사용했다. 우리는 이를 로컬 설정을 수정함으로서 손쉽게 Github으로 전환할 수 있다:

application.yml
security:
  oauth2:
    client:
      clientId: bd1c0a783ccdd1c9b9e4
      clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
      accessTokenUri: https://github.com/login/oauth/access_token
      userAuthorizationUri: https://github.com/login/oauth/authorize
      clientAuthenticationScheme: form
    resource:
      userInfoUri: https://api.github.com/user

클라이언트에서 인증실패 감지하기 Detecting an Authentication Failure in the Client

클라이언트에서 우리는 인증하지 못한 사용자들에게 약간의 피드백을 제공해주려고 한다. 이를 제공해주려고 우리는 정보 메시지를 가진 div를 추가하였다:

index.html
<div class="container text-danger" ng-show="home.error">
There was an error (bad credentials).
</div>

이 텍스트는 컨트롤러에서 "error'플래그가 설정되었을때만 보여진다. 이를 위해 다음과 같이 약간의 코드가 필요하다:

index.html
angular
  .module("app", [])
  .controller("home", function($http, $location) {
    var self = this;
    self.logout = function() {
      if ($location.absUrl().indexOf("error=true") >= 0) {
        self.authenticated = false;
        self.error = true;
      }
      ...
    };
  });

"home" 컨트롤러는 브라우저가 로드될때 그 위치를 체크한다. 만일 URL에 "error=true"가 있다면 플래그가 설정된다.

에러페이지 추가하기 Adding an Error Page

클라이언트에서 플래그설정을 지원하기 위해 인증에러를 잡아내서 쿼리 파라메터안에 설정된 이 플래그를 가진 홈페이지로 리다이렉트할 수 있어야한다. 따라서 우리는 다음과 같은 보통의 @Controller내에 하나의 종단이 필요하다:

SocialApplication.java
@RequestMapping("/unauthenticated")
public String unauthenticated() {
  return "redirect:/?error=true";
}

이 예제 앱에서 우리는 메인 어플리케이션 클래스에 이것을 넣어주었다. 이제 이는 @RestController가 아니라 @Controller로 바꿔줌으로서 이제 리다이렉트를 처리할 수 있다. 우리가 마지막으로 해주어야할 것은 비인증 응답(UNAUTHORIZED로 알려진 HTTP 401)을 "/unauthenticated" 종단으로매핑해주는 것이다. 그냥 다음과 같이 추가해주면 된다:

ServletCustomizer.java
@Configuration
public class ServletCustomizer {
  @Bean
  public EmbeddedServletContainerCustomizer customizer() {
    return container -> {
      container.addErrorPages(
          new ErrorPage(HttpStatus.UNAUTHORIZED, "/unauthenticated"));
    };
  }
}

(이 예제에선, 단지 간결함을 위해 이는 메인 클래스 안에 내장된 클래스nested class로서 추가된다)

서버에서 401 만들기 Generating a 401 in the Server

401 응답은 사용자가 Github으로 로그인 안하거나 못할 경우 이미 스프링 시큐리티로 부터 보내지고 있다. 따라서 앱은 사용자가 인증에 실패할 경우 이미 동작하고 있는 것이다. (예를 들어, 토큰승인을 거절함으로서)

여기에 양념을 조금 더 쳐서, 우리는 인증 규칙을 확장하여 올바른 "조직"에 속해있지않은 사용자의 인증을 거부할 것이다. 사용자에 대해 더 많은 정보는 Github API를 사용하면 쉽다. 우리는 단지 인증 처리의 올바를 부분에 이것을 플러그해주기만 하면 된다. 운좋게도 이러한 간단한 사용예를 위해 스프링 부트는 손쉬운 확장점을 제공해주고 있다: 만일 우리가 타입 AuthoritiesExtractor @Bean을 선언해주면, 이는 인증된 사용자의 인가Authorities (보통은 "역할roles")를 구성하는데 사용될 것이다. 우리는 사용자가 올바른 조직에 속해있는지 확인하는데 사용할 수 있으면, 아닐 경우 예외가 발생한다:

SocialApplication.java
@Bean
public AuthoritiesExtractor authoritiesExtractor(OAuth2RestOperations template) {
  return map -> {
    String url = (String) map.get("organizations_url");
    @SuppressWarnings("unchecked")
    List<Map<String, Object>> orgs = template.getForObject(url, List.class);
    if (orgs.stream()
        .anyMatch(org -> "spring-projects".equals(org.get("login")))) {
      return AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER");
    }
    throw new BadCredentialsException("Not in Spring Projects origanization");
  };
}

우리가 이 메소드안에 OAuth2RestOperations을 autowired했다는걸 기억하자, 우리는 이를 인증받은 사용자의 행동을 위한 Github API에 접근하는데 사용할 수 있다. 우리는 이렇게 하여 (스프링 오픈소스 프로젝트를 저장하는데 사용되는 조직인) "spring-projects"와 매치되는 조직들중 하나를 찾는다. 당신이 스프링 엔지니어링 팀의 일원이 아니라면 성공적으로 인증을 받기 위해 당신 자신의 값으로 대체하면 된다. 매치되는 값이 없다면 BadCredentialsException 예외가 발생하여 스프링 시큐리티가 이를 받아서 401 응답을 낸다.

 명맥하게 위의 코드는 어떤것은 Github에, 어떤것은 다른 OAuth2제공자에게 적용하도록 다른 인증 규칙로 일반화할 수 있다. 당신이 필요한건 제공자의 API에 대한 약간의 지식과 OAuth2RestOperations이 전부다.

결 론 Conclusion

우리는 스프링 부트와 스프링 시큐리티를 사용하여 최소한의 노력으로 수많은 스타일을 가진 앱을 만드는 법을 보아왔다. 모든 예제를 통해 작동하는 주요 테마는 외부 OAuth2 제공자를 사용하는 "소셜"로그인이다. 마지막 예제조차 "내부적으로" 그러한 서비스를 제공하는데 사용된다. 왜냐하면 이는 외부 제공자들이 가지고 있는 동일한 기본기능을 가지기 때문이다. 모든 예제 앱들은 더욱 구체적인 사용예를 위해 손쉽게 확장하고 재설정이 가능한데 보통은 하나의 설정파일을 수정하는것 이상도 이하도 아니다. 만일 당신이 페이스북이나 Github (또는 유사한) 제공자를 통해 등록하기하고 자신의 호스트 주소에 클라이언트 credentials을 얻으려면 자신만의 서버 버전을 사용해야한다는 것을 기억하자 또한 이들 credential을 소스컨트롤에 올리는 일이 없도록 주의하자!


반응형

반응형

스프링 부트와 OAuth2

Spring Boot And OAuth2

(원문소스: https://spring.io/guides/tutorials/spring-boot-oauth2/)

OAuth2 클라이언트의 수동설정 Manual Configuration of OAuth2 Client

이 섹션에서 우리는 @EnableOAuth2Sso 어노테이션의 '마법'으로 이미 만들어본  logout 앱을 모든 설정을 명시적으로 직접 설정하도록 수정해볼 것이다.

클라이언트와 인증 Clients and Authentication

 @EnableOAuth2Sso에는 OAuth2 클라이언트와 인증의 2가지 기능이 있다. 클라이언트는 재사용이 가능하고, 또한 당신의 인가서버Authorization Server (우리의 경우 페이스북)가 제공하는(우리의 경우 Graph API) OAuth2 리소스들과 상호작동하는데 사용할 수 있다. 이 인증 기능은 당신의 앱을 스프링시큐리티와 맞도록 맞춰준다. 일단 페이스북과 소통하는게 끝나면, 당신의 앱은 정확히 다른 보호된 스프링 앱secure Spring app과 똑같이 동작할 것이다.

클라이언트 

클라이언트 부분은 스프링 시큐리티 OAuth2에 의해 제공되며 @EnableOAuth2Client라는 다른 어노테이션을 쓴다. 따라서 이 수정의 첫 걸음은 @EnableOAuth2Sso를 삭제하고 더 낮은 레벨의 어노테이션으로 변경하는 것이다:

SocialApplication
@SpringBootApplication
@EnableOAuthClient
@RestController
public class SocialApplication extends WebSecurityConfigurerAdapter {
  ...
}

일단 이렇게 하면 우리는 우리를 위해 만들어진 유용한 몇가지 기능을 가지게 된다. 먼저 OAuth2ClientContext를 주입할 수 있어, 우리의 시큐리티 설정에 추가할 인증 필터를 만드는데 쓸 수 있다:

SocialApplication
@SpringBootApplication
@EnableOAuthClient
@RestController
public class SocialApplication extends WebSecurityConfigurerAdapter {

  @Autowired
  OAuth2ClientContext oauth2ClientContext;

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.antMatcher("/**")
      ...
      .addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);
  }

  ...

}

이 필터는 우리가 OAuth2ClientContext를 사용하는 새 메소드에서 생성된다:

SocialApplication
private Filter ssoFilter() {
  OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/facebook");
  OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), oauth2ClientContext);
  facebookFilter.setRestTemplate(facebookTemplate);
  facebookFilter.setTokenServices(new UserInfoTokenServices(facebookResource().getUserInfoUri(), facebook().getClientId()));
  return facebookFilter;
}

이 필터 역시 페이스북에 등록된 클라이언트 설정에 대해 알아야한다:

SocialApplication
  @Bean
  @ConfigurationProperties("facebook.client")
  OAuth2ProtectedResourceDetails facebook() {
    return new AuthorizationCodeResourceDetails();
  }

그리고 인증을 완료하려면 페이스북의 사용자 정보 종단user info endpoint이 어딘지 알아야 한다:

SocialApplication
  @Bean
  @ConfigurationProperties("facebook.resource")
  ResourceServerProperties facebookResource() {
    return new ResourceServerProperties();
  }

이 두개의 "정적인" 데이터 객체(facebook()과 facebookResource())에 우리가  @ConfigurationProperties. 로 설정된 @Bean 을 사용했다는 것을 알아두자. 이것은 우리가 application.yml를, 설정의 접두어를 security.oauth2대신 facebook로 사용하는 약간 새로운 포멧으로 전환할 수 있다는 것을 의미한다:

application.yml
facebook:
  client:
    clientId: 233668646673605
    clientSecret: 33b17e044ee6a4fa383f46ec6e28ea1d
    accessTokenUri: https://graph.facebook.com/oauth/access_token
    userAuthorizationUri: https://www.facebook.com/dialog/oauth
    tokenName: oauth_token
    authenticationScheme: query
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://graph.facebook.com/me

리다이렉트 처리하기 Handling the Redirects

마지막 수정은 우리의 앱에서 페이스북으로 리다이렉트를 명시적으로 지원하게 만들어주는 것이다. 이는 서블릿 Filter를 가진 Spring OAuth2에서 처리되어진다. 이 필터는 어플리케이션 컨텍스트application context에서 이미 사용가능한데 우리가 @EnableOAuthClient선언을 해두었기 때문이다. 우리가 이 필터를 엮어서 사용하려면 그냥 스프링부트 어플리케이션의 올바른 순서right order안에서 호출해 주기만 하면 된다. 이것을 위해 우리는 FilterRegistrationBean이 필요하다:

SocialApplication.java
@Bean
public FilterRegistrationBean oauth2ClientFilterRegistration(
    OAuth2ClientContextFilter filter) {
  FilterRegistrationBean registration = new FilterRegistrationBean();
  registration.setFilter(filter);
  registration.setOrder(-100);
  return registration;
}

우리는 이미 사용가능할 필터를 Autowire해두었고, 이 필터를 메인 스프링 시큐리티 필터가 불러지기 전에 호출되도록 충분히 낮은 순서로 등록하였다. 이 방법으로 우리는 인증요청의 예외exception발생을 통해 리다이렉트를 처리할 수 있다.

이 단계까지의 수정을 통해 앱은 동작이 가능하며, 실행시 지난 섹션에서 만드 logout예제와 동일하게 동작한다. 설정을 단계별로 쪼개고 몇시적으로 우리에게 가르쳐줌으로서 스프링 부트가 해주는 마법과 같은 자동화는 더이상 없다. (이는 단지 설정의 뼈대일 뿐이다). 그리고 이는 막바로 사용가능하게 자동으로 제공되었던 기능을 확장하기 위한, 우리 자신의 의사과 비지니스 요구사항을 추가하기 위한 준비가 된다.

Github으로 로그인하기 Login with Github

이 섹션에서 우리가 이미 페이스북 링크를 통해 로그인이 가능했던 앱을 Github 인증을 추가하여 사용자가 선택할 수 있는 링크를 추가하도록 기존의 app을 수정할 것이다. 

Github 링크 추가하기 Adding the Github Link

클라이언트에서의 수정은 매우 사소하다. 단지 또다른 링크만 추가해주면 된다:

index.html
<div class="container" ng-show="!home.authenticated">
  <div>
    With Facebook: <a href="/login/facebook">click here</a>
  </div>
  <div>
    With Github: <a href="/login/github">click here</a>
  </div>
</div>

원칙적으로, 일단 우리가 인증 제공자를 추가하려면 "/user" 종단으로부터 되돌아오는 데이터에 대해 더 신중해져야 한다. Github과 페이스북 둘다 사용자 정보안에 "name"필드가 똑같이 있다. 따라서 우리의 종단에 실제로 수정을 해줄 필요가 없다.

Github 인증필터 추가하기 Adding the Github Authentication Filter

서버단의 주요 수정은 우리의 새 링크로 부터 오는 "/login/github" 요청을 처리하는 부가적인 시큐리티 필터를 추가하는 것이다. 이미 우리는 ssoFilter() 메소드에서 만들어진 페이스북을 위한 커스텀 인증 필터를 가지고 있으므로, 인증 경로를 하나이상 처리할 수 있도록 그냥 기존의 것을 Composite필터를 써서 바꿔주기만 하면 된다

SocialApplication.java
private Filter ssoFilter() {

  CompositeFilter filter = new CompositeFilter();
  List<Filter> filters = new ArrayList<>();

  OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/facebook");
  OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), oauth2ClientContext);
  facebookFilter.setRestTemplate(facebookTemplate);
  facebookFilter.setTokenServices(new UserInfoTokenServices(facebookResource().getUserInfoUri(), facebook().getClientId()));
  filters.add(facebookFilter);

  OAuth2ClientAuthenticationProcessingFilter githubFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/github");
  OAuth2RestTemplate githubTemplate = new OAuth2RestTemplate(github(), oauth2ClientContext);
  githubFilter.setRestTemplate(githubTemplate);
  githubFilter.setTokenServices(new UserInfoTokenServices(githubResource().getUserInfoUri(), github().getClientId()));
  filters.add(githubFilter);

  filter.setFilters(filters);
  return filter;

}

우리의 예전 ssoFilter()의 코드는 하나는 페이스북, 다른 하나는 Github용으로 중복된다. 이 두개의 필터는 하나의 컴포지트composite로 합쳐진다.

facebook()과 facebookResource() 메소드 역시 유사하게 github()과 githubResource()로 추가해줘야한다:

SocialApplication.java
@Bean
@ConfigurationProperties("github.client")
OAuth2ProtectedResourceDetails github() {
	return new AuthorizationCodeResourceDetails();
}

@Bean
@ConfigurationProperties("github.resource")
ResourceServerProperties githubResource() {
	return new ResourceServerProperties();
}

그리고 이에 상응하는 설정들 역시 추가해준다:

application.yml
github:
  client:
    clientId: bd1c0a783ccdd1c9b9e4
    clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
    accessTokenUri: https://github.com/login/oauth/access_token
    userAuthorizationUri: https://github.com/login/oauth/authorize
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://api.github.com/user

여기의 클라이언트 디테일들은 Github에 등록된 정보여야 하며 (페이스북과 동일하게) localhost:8080주소를 가르켜야한다.

앱은 이제 준비되었고 사용자가 페이스북 또는 Github으로 인증을 선택하도록 동작할 것이다.

로컬 사용자 데이터베이스를 추가하기 How to Add a Local User Database

많은 어플리케이션은 인증을 외부 제공자에의해 위임할 경우에도, 그들 사용자의 데이터를 로컬에서 가지고 있어야 한다. 여기에 그 코드를 보여주진 않을 것이지만 이를 쉽게 해주는 두가지 단계가 있다:

  1. 당신의 데이터베이스를 위한 백엔드를 선택한 후, 외부 인증으로부터 일부 또는 전체를 불러올 수 오는데 사용하는 커스텀 User 객체를 위한 (스프링 데이터 등을 사용하여) 리파지토리repository를 설정하라

  2. 당신의 /user 종단에서 리파지토리 검사에 의해 로그인된 각각 고유한 사용자를 위한  User 객체를 규정하자. 만일 현재의 Principal의 식별자를 가진 사용자가 이미 있다면, 업데이트하거나, 그렇지않다면 생성할 수 있다.

힌트: User 객체에 필드 하나를 추가하여 외부 제공자의 고유 식별자를 링크해주자 (사용자의 이름같은 게 아닌 무언가 고유한 것)


반응형

반응형

스프링 부트와 OAuth2

Spring Boot And OAuth2

(원문소스: https://spring.io/guides/tutorials/spring-boot-oauth2/)

기본페이지 추가하기 Add a Welcome Page

이 섹션에서 우리는 '페이스북에 로그인하기'라는 명시적 링크를 추가함으로서 방금 만든 simple앱을 수정할 것이다.  즉시 리다이렉트되는 대신, 새 링크는 홈페이지에 보여져 사용자가 로그인을 할지 안할지 선택할 수 있게 된다. 사용자가 링크를 클릭했을 때만 인증된 컨탠트가 보여질 것이다.

홈페이지의 조건적 컨탠트 Conditional Content in Home Page

어떠한 컨탠트를 사용자가 인증했을 때 또는 우리가 서버측 랜더링(이를테면 Freemaker또는 Thymeleaf)을 사용하지 않을지 조건적으로 랜더하거나 또는 자바스크립트를 조금 써서 우리가 그냥 브라우저에게 그것하라고 물어보게 할 수 있다. 이것을 위해 우리는 AngularJS를 쓸 것이다. 만약 당신이 선호하는 다른 프레임워크가 있다면 클라이언트 코드를 옮기는게 그다지 어렵지 않을 것이다.

앵귤러JS를 쓰기위해 우리는 앵귤러 앱 컨테이너로서 <body>에 마크해 주었다.

index.html
<body ng-app="app" ng-controller="home as home">
...
</body>

Body안의 <div> 엘리먼트들은 그것이 보여지는 부분들 제어하는 모델에 묶여bound질 수 있다.

index.html
<div class="container" ng-show="!home.authenticated">
	Login with: <a href="/login">Facebook</a>
</div>
<div class="container" ng-show="home.authenticated">
	Logged in as: <span ng-bind="home.user"></span>
</div>

이 HTML은 authenticated 플래그와 인증받은 사용자를 위한 user객체를 가진 "home" 컨트롤러를 필요로 한다. 여기 이들 기능의 간단한 구현체를 보자 (<body>)의 끝 줄에 이들을 넣자):

index.html
<script type="text/javascript" src="/webjars/angularjs/angular.min.js"></script>
<script type="text/javascript">
  angular.module("app", []).controller("home", function($http) {
    var self = this;
    $http.get("/user").success(function(data) {
      self.user = data.userAuthentication.details.name;
      self.authenticated = true;
    }).error(function() {
      self.user = "N/A";
      self.authenticated = false;
    });
  });
</script>

서버측 수정 Server Side Changes

이를 동작시키려면, 서버쪽에 약간 수정을 해줘야한다. "home" 컨트롤러는 현재 인증받은 사용자정보를 받아오는  "/user"라는 종단endpoint이 필요하다. 메인 클래스에서 다음과 같이 아주 쉽게할 수 있다:

SocialApplication
@SpringBootApplication
@EnableOAuth2Sso
@RestController
public class SocialApplication {

  @RequestMapping("/user")
  public Principal user(Principal principal) {
    return principal;
  }

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

}

우리가 핸들러 메소드에 주입한 @RestController 와 @RequestMapping 그리고 java.util.Principal 등을 알아두자

위와 같이 /user 종단에 Principal 전체를 리턴하는 것은 매우 안좋은 발상이다. (브라우저 클라이언트에 노출해서는 안되는 정보를 포함하고 있기 때문이다.) 재빠른 동작을 위해 이렇게 했지만 이 가이드의 후반에 이 종단의 정보를 숨기도록 바꿀 것이다.

이 앱은 이제 사용자가 우리가 제공한 링크를 클릭함으로서 전처럼 인증할 수 있게 잘 동작할 것이다. 이 링크를 보이도록 만드려면 WebSecurityConfigurer를 추가하여 홈페이지의 보안을 꺼주어야 한다:

SocialApplication
@SpringBootApplication
@EnableOAuth2Sso
@RestController
public class SocialApplication extends WebSecurityConfigurerAdapter {

  ...

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      .antMatcher("/**")
      .authorizeRequests()
        .antMatchers("/", "/login**", "/webjars/**")
        .permitAll()
      .anyRequest()
        .authenticated();
  }

}

스프링 부트는 @EnableOAuth2Sso 어노테이션을 가진 클래스의 WebSecurityConfigurer에 특별한 의미를 부여한다: OAuth2 authentication processor를 가진 시큐리티 필터체인security filter chain을 쓸 수 있도록 설정한다. 홈페이지를 보이도록 만드는 데 필요한건 홈페이지와 관련된 정적인 리소스를 authorizeRequests()에 명시하는 것이다. (우리는 인증을 다루는데 필요한 로그인 종단의 접근 또한 포함할 것이다.). 모든 이외의 요청들(예를 들어, /user종단) 인증이 필요하다.

이 단계의 수정을 거쳐 어플리케이션은 완성되었다. 서버를 돌리고 홈페이지를 방문하면 "Login with Facebook"이라는 멋지게 스타일된 HTML링크를 볼 수 있을 것이다. 이 링크는 당신을 페이스북으로 직접 가게해주는게 아니라, 인증을 처리하는 로컬 경로로 데려다준다.(그리고 페이스북으로 리다이렉트를 보낸다). 일단 인증을 받았다면, 로컬앱으로 다시 리다이렉트 되어, 이제 당신의 이름을 보여줄 것이다. ( 당신이 페이스북에 데이터접근을 허락한다고 설정했을 경우)

로그아웃 버튼 추가하기 Add a Logout Button

이 섹션에서 우리는 사용자가 앱을 로그아웃하도록 버튼을 추가함으로서 click 앱을 수정할것이다. 이는 간단한 기능같이 보일지 모르지만 사실 구현에 매우 신중해야한다. 정확히 어떻게 이렇게 하는지 약간의 시간을 들여 곱씹어볼 가치가 있다. 사실 대부분의 수정은 읽기 전용 리소스에서 읽고 쓰기가 가능한 리소스로 (로그아웃을 한다는 것은 상태변경을 해주어야한다) 앱을 변환하는 작업을 해주는 것이다. 단지 정적인 컨텐트가 아닌 어떤 실제 어플리케이션에서 같은 변경을 해주어야한다.

클라이언트측 변경 Client Side Changes

클라이언트에서 우리는 그냥 로그아웃 버튼과 자바스크립트로 서버에 인증을 취소할지 물어보는 호출만 제공해주기만 하면 된다. 먼저, UI의 "인증받은authenticated" 섹션에서 버튼을 추가하자: 

index.html
<div class="container" ng-show="home.authenticated">
  Logged in as: <span ng-bind="home.user"></span>
  <div>
    <button ng-click="home.logout()" class="btn btn-primary">Logout</button>
  </div>
</div>

그다음 logout() 함수를 제공하여 자바스크립트를 참조한다:

index.html
angular
  .module("app", [])
  .controller("home", function($http, $location) {
    var self = this;
    self.logout = function() {
      $http.post('/logout', {}).success(function() {
        self.authenticated = false;
        $location.path("/");
      }).error(function(data) {
        console.log("Logout failed")
        self.authenticated = false;
      });
    };
  });

logout() 함수는 /logout 로  POST 호출을 한뒤 authenticated 플래그를 초기화한다. 이제 우리는 종단의 구현을 위해 서버측으로 옮겨가보자.

로그아웃 종단 추가하기 Adding a Logout Endpoint

스프링 시큐리티는 우리가 하려고 하는 일(세션을 정리하고 쿠키를 무효화invalidate함)을 위한 /logout 종단 지원을 내장하고 있다. 이 종단을 설정하려면, 간단히 우리의 WebSecurityConfigurer에 있는 configure method 메소드를 다음과 같이 확장하면 된다:

SocialApplication.java
@Override
protected void configure(HttpSecurity http) throws Exception {
  http.antMatcher("/**")
    ... // existing code here
    .and().logout().logoutSuccessUrl("/").permitAll();
}

/logout종단은 우리에게 이 요청을 POST로 보낼것을 요구한다. 그리고 Cross Site Request Forgery (씨서프sea surf라 발음하는 CSRF)로 부터 사용자를 보호하기 위해 이 요청안에 하나의 토큰을 넣으라고 요구한다. 이 토큰의 값은 현재 세션으로 링크되어있는데 이는 무엇을 보호하는지 제공해주는 일을 한다. 이제 우리의 자바스크립트 앱에 이 데이터를 넣는 방법을 알아야한다.

앵귤러JS 또한 (그들이 XSRF라 부르는) CSRF 지원을 내장하고 있다. 하지만 스프링 시큐리티의 통상적인 방법과 약간 다르게 구현되어있다. 앵귤러가 서버에게 원하는 것은 "XSRF-TOKEN"이라 부르는 쿠키를 보내주는 것이다. 이것이 들어있으면 앵귤러는 "X-XSRF-TOKEN"이라는 이름의 헤더를 되돌려준다. 스프링 시큐리티에서 이것을 알려주기 위해, 우리는 쿠키를 만드는 필터를 하나 추가해주어야한다, 또한 현재의 CSRF 필터에 이 헤더이름을 알려주어야한다. WebSecurityConfigurer에서:

SocialApplication.java
@Override
protected void configure(HttpSecurity http) throws Exception {
  http.antMatcher("/**")
    ... // existing code here
    .and().csrf().csrfTokenRepository(csrfTokenRepository())
    .and().addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
}

csrfHeaderFilter()는 다음의 커스텀 필터이다:

SocialApplication.java
private Filter csrfHeaderFilter() {
  return new OncePerRequestFilter() {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
        FilterChain filterChain) throws ServletException, IOException {
      CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
      if (csrf != null) {
        Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
        String token = csrf.getToken();
        if (cookie == null || token != null && !token.equals(cookie.getValue())) {
          cookie = new Cookie("XSRF-TOKEN", token);
          cookie.setPath("/");
          response.addCookie(cookie);
        }
      }
      filterChain.doFilter(request, response);
    }
  };
}

그리고 csrfTokenRepository()는 여기 정의되어있다.:

SocialApplication.java
private CsrfTokenRepository csrfTokenRepository() {
  HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
  repository.setHeaderName("X-XSRF-TOKEN");
  return repository;
}

출동준비완료! Ready To Roll!

여기까지의 수정으로, 우리는 서버를 동작하고 새로운 로그아웃 버튼을 시도할 준비를 마쳤다. 앱을 시작하고 새 브라우저 창에 홈페이지를 열어서 "login"링크를 클릭하면 당신을 페이스북으로 데려다 줄 것이다. (이미 로그인 되어있다면 아마 리다이렉트를 알아차리지 못할 것이다.). 현재 세션을 종료하기위해 "Logout"버튼을 클릭하면 앱은 비인증 상태로 되돌아 온다. 궁금하면 브라우저가 로컬서버에서 받아온 요청안에 새 쿠키와 헤더를 확인할 수 있을 것이다.

이제 브라우저 클라이언트에서 로그아웃 종단이 동작하는 것을 기억하자, 이제 모든 HTTP요청들((POST,PUT,DELETE,등등)에서 또한 잘 동작할 것이다. 이 앱은 이제 약간은 더 실전적 기능을 가진 좋은 플랫폼이 되었다.


반응형

반응형

스프링 부트와 OAuth2

Spring Boot And OAuth2

(원문소스: https://spring.io/guides/tutorials/spring-boot-oauth2/)


이 가이드에서 OAuth2와 Spring Boot를 사용해 어떻게 하면 "소셜 로그인"의 다양한 일을 하는 예제를 만들 수 있는 지 보여줄 것이다. 간단한 하나의 싱글사인온 제공자에 연결해보는 걸로 시작하여, Facebook 이나 Github과 같은 authentication 제공자와 함께 OAuth2 Authorization Server를 스스로 돌리는 일을 해볼것이다. 예제들은 백엔드로 모두 스프링 부트와 스프링 OAuth를 사용한 단일 페이지 앱이 될 것이며 프론트엔드로 모두 AngularJS 를 사용하였다. 하지만 자바스크립트단의 코드나 서버의 랜더링사용은 최소화 하였다.

각각 새로운 기능을 추가해놓은 몇가지 예제들이다:

  • simple: 매우 기본적인 정적 앱으로 그냥 하나의 홈페이지와 스프링부트의 @EnableOAuth2Sso를 통해 무작위 로그인을 한다 (홈페이지를 방문하면 자동으로 페이스북으로 리다이렉트 될 것이다)

  • click: 사용자가 로그인을 클릭할 수 있는 명시적 링크를 추가하였다.

  • logout: 인증받은 사용자를 위한 로그아웃 링크를 추가하였다.

  • manual @EnableOAuth2Sso 어노테이션을 사용하지않고 수동으로 설정하여 똑같이 동작시켜본다.

  • gitub: 두번째 로그인 제공자로서 Github을 추가하여 사용자는 홈페이지에서 어느것을 사용할 지 고를 수 있다.

  • auth-server스스로 토큰을 발행할 수 있지만 여전히 인증을 위해 외부 OAuth2 제공자를 사용할 수 있는 완전무결한fully-fledged OAuth2  Authorization 서버를 돌린다. 

  • custom-error Github API에 기반한 커스텀 인증과, 비인증 사용자를 위한 에러 메세지를 추가하였다.

이들은 각각 IDE에서 불러올 수 있으며, 앱을 시작할 수 있는 SocialApplication 메인 클래스가 있다. 이들 모두 http://localhost:8080 로 홈페이지에 접근할 수 있다.( 로그인을 하고 그 내용을 보려면 최소한 페이스북 계정이 필요하다). 또한, gmvn spring-boot:run를 사용하여 커맨드라인상에서 앱을 돌리거나, mvn package 를 통해 jar파일을 빌드하고 java -jar target/*.jar 로도 돌릴 수 있다. 프로젝트 상위레벨에 있는 wrapper를 사용하면 메이븐을 설치하지않다도 된다. 예를 들면:

$ cd simple
$ ../mvnw package
$ java -jar target/*.jar
 모든 앱은 localhost:8080

에서 동작한다. 페이스북과 Github에 이 주소로 OAuth2 클라이언트를 등록했기 때문이다. 다른 호스트나 포트로 돌리려면 스스로 각각 앱을 등록하고 config 파일에 해당 인증정보들을 넣어줘야한다. 기본설정값을 사용하면 localhost로 돌린다고 당신의 페이스북이나 Github 인증정보가 누출되거나 위험해지지않는다. 하지만 당신이 인터넷에 무엇을 노출할지 조심해야한다. 공개 소스 컨트롤에 당신의 앱을 등록하지 않아야한다.

페이스북으로 싱글 사인온 하기 Single Sign On With Facebook

이 섹션에서, 우리는 인증을 위해 페이스북을 사용하는 최소한의 어플리케이션을 만들것이다. 이는 스프링 부트의 자동설정을 사용하면 매우 쉽다. 

새 프로젝트 만들기

먼저 스프링 부트 어플리케이션을 만들어야 한다. 이를 위한 수많은 방법이 있지만, 가장 쉬운 것은 http://start.spring.io 를 방문하여 빈 프로젝트를 만드는 것이다. (시작점으로서 "Web" 의존성을 골라주자). 커맨드라인으로 아래와 같이하면 똑같이 만들 수 있다:

$ mkdir ui && cd ui
$ curl https://start.spring.io/starter.tgz -d style=web -d name=simple | tar -xzvf -

이제 당신이 선호하는 IDE에서 이 프로젝트를 불러올 수 있다. (기본적으로 보통의 메이븐 자바프로젝트다) 또는 파일 작업을 하고 그냥 커맨드라인에서 "mvn"을 사용해도 된다.

홈페이지 추가하기

새 프로젝트의 "src/main/resources/static" 폴더에 index.html파일을 만들자. 몇몇의 스타일시트와 자바스크립트를 링크해줘야하며 그 결과는 다음과 같다:

index.html
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <title>Demo</title>
    <meta name="description" content=""/>
    <meta name="viewport" content="width=device-width"/>
    <base href="/"/>
    <link rel="stylesheet" type="text/css" href="/webjars/bootstrap/css/bootstrap.min.css"/>
    <script type="text/javascript" src="/webjars/jquery/jquery.min.js"></script>
    <script type="text/javascript" src="/webjars/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
	<h1>Demo</h1>
	<div class="container"></div>
</body>
</html>

이 소스의 무엇도 OAuth2 로그인 기능과 무관하지만 우리는 홈페이지에 약간의 기본적인 기능을 시작할 수 있을 뿐아니라 멋지게 보이는 UI 를 원한다.

앱을 시작하고 홈페이지를 로드하면, 스타일 시트가 로드되지 않았다는 걸 알 수 있을 것이다. 이제 그들을 아래와 같이 의존성을 추가해줌으로서 해결할 수 있다:

pom.xml
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>angularjs</artifactId>
	<version>1.4.3</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>jquery</artifactId>
	<version>2.1.1</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>bootstrap</artifactId>
	<version>3.2.0</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>webjars-locator</artifactId>
</dependency>

우리는 트위터 부트스트랩과 (우리가 지금 당장 써야하는) jQuery, 거기에 나중에 쓸 AngularJS를 추가했다. 다른 의존성은 webjars "locator"로 webjars 사이트에 의해 라이브러리로서 제공받는 것이고 이것은 스프링에 의해 정확한 버전을 알 필요없이(그렇기 때문에 index.html에서 버전없는 /webjar/all}링크를 사용했다webjars의 정적인 어셋을 위치locate하는데 사용된다. 이 webjar locator를 활성화하려면 어플리케이션의 application.properties에서 Spring MVC 리소스 체인을 활성화시켜줘야한다:

application.properties
spring.resources.chain.enabled: true

이 수정을 통해 이제 우리는 이뻐보이는 홈페이지를 가지게 되었다.

어플리케이션 보호하기 Securing the Application

어플리케이션을 안전하게 만드려면, 스프링 시큐리티 의존성을 추가해주면 된다. 그러면 기본적으로 HTTP Basic으로 보호될 것이다. 따라서 우리가 (페이스북으로 위임하는) "소셜" 로그인을 만드려고 하므로, Spring Security OAuth2 의존성 또한 추가해줘야한다:

pom.xml
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.security.oauth</groupId>
	<artifactId>spring-security-oauth2</artifactId>
</dependency>

페이스북으로의 링크를 만드면, 메인클래스에서 @EnableOAuth2Sso 어노테이션을 해줘야한다:

SocialApplication.java
@SpringBootApplication
@EnableOAuth2Sso
public class SocialApplication {

  ...

}

그리고 약간의 설정이 필요하다 (더 나은 식별성을 위해 application.properties을 YAML 으로 변환하였다):

application.yml
security:
  oauth2:
    client:
      clientId: 233668646673605
      clientSecret: 33b17e044ee6a4fa383f46ec6e28ea1d
      accessTokenUri: https://graph.facebook.com/oauth/access_token
      userAuthorizationUri: https://www.facebook.com/dialog/oauth
      tokenName: oauth_token
      authenticationScheme: query
      clientAuthenticationScheme: form
    resource:
      userInfoUri: https://graph.facebook.com/me
...

이 설정은 페이스북의 개발자사이트에 등록된 클라이언트 앱을 참조한다. 당신은 당신의 앱에서 리다이렉트할 곳을 등록해줘야한다. (우리의 경우 Home page). 이것은 "localhost:8080"으로 등록되었는데, 이 주소로 돌아가는 앱에서만 작동한다.

이 수정으로 앱을 다시 돌리고 http://localhost:8080의 홈페이지를 방문해보자. 홈페이지 대신, 당신은 페이스북 로그인으로 리다이렉트될 것이다. 로그인하려고 인가authorization를 수락하면, 당신의 로컬 앱으로 다시 리다이렉트되어 돌아와지고 홈페이지가 보여질 것이다. 만일 페이스북 로그인이 되어있는 상태면, 브라우저를 완전히 새롭게 열어 쿠키도 없고 캐시된 데이터가 없더라고 이 로컬앱에서 재인증할 필요없다. (이것이 싱글사이온의 의미이다)

당신이 이 섹션의 예제 어플리케이션을 돌리는 중이라면, 브라우저의 쿠키와 HTTP Basic credentials의 캐시를 지웠는지 먼저 확인하자. 크롬의 incognito window를 사용하는 것이 가장 좋은 방법이다.

이 예제의 접근을 승인하는 것grant access은 안전하다. 로컬에서만 동작하고 있는 이 앱에서만 토큰을 사용할 수 있고 범위scope도 제약적이기 때문이다. 당신이 이와 같은 절차를 거치는 앱을 로그인할 때, 무엇을 승인해야하는지 반드시 살펴봐야한다: 당신이 불편해할 만큼의 권한을 요구할 수도 있기때문이다. (예를 들면, 당신의 개인정보를 수정할 수 있는 권한을 요구할 수 있다.)

방금 뭔일 있었나? What Just Happened?

당신이 방금 만든 이 앱은, OAuth2 용어로, 클라이언트 어플리케이션이다. 그리고 이 앱은 페이스북(인가서버Authorization Server)로부터 억세스토큰access token을 받기위해 authorization code grant를 사용했다. 그 후 당신의 로그인ID와 이름을 포함한 (당신이 허용해준) 몇가지 개인 정보를 페이스북에 요청하기 위해 억세스토큰을 사용했다. 이 단계까지 페이스북은 당신이 보낸 토큰을 디코딩하고 사용자의 정보에 접근하는 앱의 권한을 체크하는 등등의 리소스 서버로서 역할을 하고 있다. 이 단계가 성공적으로 완료되면, 당신의 앱은 사용자 정보를 스프링 시큐리티 컨텍스트Spring security context안에 넣음으로서 당신을 인증할 것이다.

(크롬의 F12누르면 보이는) 브라우저 도구를 열고 네트워크 트래픽을 따라가보면, 페이스북으로 리다이렉트되어 가고오고 하는것을 확인할 수 있으며 최종적으로 새 Set-Cookie 헤더를 가지고 홈페이지로 되돌아와진다. 이 쿠키(기본값으로JSESSIONID)는 스프링(또는 어느 서블릿 기반의) 어플리케이션에서 당신의 인증 상세정보를 위한 하나의 토큰이다.

이제 우리는, 어떠한 정보를 보기위해 사용자가 페이스북과 같은 외부 제공자로 인증해야하는 측면에서, 안전한 어플리케이션을 가지게 되었다. 우리는 웹사이트에 매번 이를 사용하려는게 아니라 기본 식별, 그리고 당신의 사이트에 서로 다른 사용자간에 컨텐트를 구별하기 위한 목적으로 이는 오늘날 이러한 종류의 인증이 왜 인기가 많은가를 설명하는 훌륭한 출발점이 될 것이다. 다음 섹션에서 우리는 어플리케이션에 약간의 기본 기능들을 추가하고, 사용자가 처음 페이스북으로 리다이렉트 할 때 언제 그리고 무엇을 하는지 조금더 구체적으로 알아볼 것이다.


반응형

반응형
스프링부트 : REST어플리케이션에서 예외처리하기

(원문소스: http://www.ekiras.com/2016/02/how-to-do-exception-handling-in-springboot-rest-application.html)


기억할 점

스프링 부트 어플리케이션에서 예외처리하는 방법은 3가지다:

  1. 전역 처리 Global Level using -  @ControllerAdvice
  2. 컨트롤러단에서 처리 Controller Level using@ExceptionHandler 
  3. 메소드단위 처리 Method Level usingtry/catch  

REST 어플리케이션에서 예외처리하기 

예외처리를 테스트하기위에 3가지 예외를 만들것이다:

  • 모든 커스텀 예외의 부모가 될 BaseException.
  • Base Exception을 확장한 CustomException1.
  • Base Exception을 확장한 CustomException2.

다음의 메소드들을 만든다
  • ex1() throws BaseException
  • ex2() throws CustomException1
  • ex3() throws CustomException2
  • ex4() throws NullPointerException 
  • ex5() throws NumberFormatException

이제 위에 언급한 방법을을 사용하여 이들 예외를 어떻게 처리하는지 둘러보자

방법 1 :  @ControllerAdvice를 사용한 전역 처리Global Exception Handling

클래스에 @ControllerAdvice and @RestController. 를 어노테이션을 추가하면 REST 응답을 리턴하게 될 것이다.

  • @ControllerAdvice는 당신의 스프링 어플리케이션에게 이 클래스가 당신의 어플리케이션의 예외처리를 맡을 거라고 알려주게 된다.
  • @RestController 는 이 클래스를 컨트롤러로 만들어주고 이 클래스가 응답을 렌더할 수 있게 해준다.
  • @ExceptionHandler 어노테이션을 사용하여 예외를 처리할 클래스를 정의한다. ( 기본클래스는 모두 상속되거나 확장된 클래스를 처리할 것이다) 
  • 당신은 @ResponseStatus 어노테이션을 사용하여 예외의 응답 상태를 설정할 수 있다.

HomeController.class

  1. package com.ekiras.controller;  
  2.   
  3. import com.ekiras.exception.BaseException;  
  4. import com.ekiras.exception.CustomException1;  
  5. import com.ekiras.exception.CustomException2;  
  6. import org.springframework.web.bind.annotation.ExceptionHandler;  
  7. import org.springframework.web.bind.annotation.RequestMapping;  
  8. import org.springframework.web.bind.annotation.RestController;  
  9.   
  10. /** 
  11.  * @author ekansh 
  12.  * @since 19/2/16 
  13.  */  
  14. @RestController  
  15. @RequestMapping({"","/"})  
  16. public class HomeController {  
  17.   
  18.     @RequestMapping("/ex1")  
  19.     public String ex1(){  
  20.         // 전역 처리자 메소드 handleBaseException에 잡힐 것이다.  
  21.         throw new BaseException("Base Exception");  
  22.     }  
  23.   
  24.     @RequestMapping("/ex2")  
  25.     public String ex2(){  
  26.         //전역 처리자 메소드 handleBaseException에 잡힐 것이다.    
  27.         throw new CustomException1();  
  28.     }  
  29.   
  30.     @RequestMapping("/ex3")  
  31.     public String ex3(){  
  32.         // 전역 처리자 메소드 handleBaseException에 잡힐 것이다.    
  33.         throw new CustomException2();  
  34.     }  
  35.   
  36.     @RequestMapping("/ex4")  
  37.     public String ex4(){  
  38.         //전역 처리자 메소드 handleBaseException에 잡힐 것이다.    
  39.         throw new NullPointerException("null pointer exception");  
  40.     }  
  41.   
  42.     @RequestMapping("/ex5")  
  43.     public String ex5(){  
  44.         // 컨트롤러 예외 처리자 메소드 nfeHandler에 잡힐 것이다.
  45.         throw new NumberFormatException("number format exception");  
  46.     }  
  47.   
  48.     /** 
  49.      * 이 컨트롤러 내에서 발생하는 모든 Number Format 예외를 처리한다     *  
  50.      * */  
  51.     @ExceptionHandler(value = NumberFormatException.class)  
  52.     public String nfeHandler(NumberFormatException e){  
  53.         return e.getMessage();  
  54.     }  
  55.   
  56. }  

GlobalExceptionHandler.class

  1. package com.ekiras.handler.exception;  
  2.   
  3. import com.ekiras.exception.BaseException;  
  4. import org.springframework.http.HttpStatus;  
  5. import org.springframework.web.bind.annotation.ControllerAdvice;  
  6. import org.springframework.web.bind.annotation.ExceptionHandler;  
  7. import org.springframework.web.bind.annotation.ResponseStatus;  
  8. import org.springframework.web.bind.annotation.RestController;  
  9.   
  10. /** 
  11.  * @author ekansh 
  12.  * @since 19/2/16 
  13.  */  
  14. @ControllerAdvice  
  15. @RestController  
  16. public class GlobalExceptionHandler {  
  17.   
  18.     @ResponseStatus(HttpStatus.BAD_REQUEST)  
  19.     @ExceptionHandler(value = BaseException.class)  
  20.     public String handleBaseException(BaseException e){  
  21.         return e.getMessage();  
  22.     }  
  23.   
  24.     @ExceptionHandler(value = Exception.class)  
  25.     public String handleException(Exception e){return e.getMessage();}  
  26.   
  27.   
  28. }  
  • handleBaseException(BaseException e) ::  BaseException, CustomException1 과 CustomException2 클래스들의 예외를 잡는다.
  • handleException(Exception e) ::  Exception클래스의 자식들의 모든 예외를 처리한다.

알림 :: 만일 BaseException 또는 그의 자식 예외가 발생하면 handleException() 메소드가 아닌 handleBaseException()이 이 예외를 잡을 것이다. 

방법 2 : @ExceptionHandler를 사용한 컨트롤러단 예외처리


  1. @ExceptionHandler(value = NumberFormatException.class)  
  2.  public String nfeHandler(NumberFormatException e){  
  3.      return e.getMessage();  
  4.  }  

HomeController.java 에 있는 위의 코드는 이 컨트롤러내에서 발생하는 모든 NumberFormatException를잡을 것이다. 다른 컨트롤러에서 발생하는 NumberFormatException는 처리하지않는다.







반응형

반응형

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 프로젝트에서 확인할 수 있다 – 이클리스 기반이며 임포트해서 쉽게 돌려볼 수 있다.


반응형

+ Recent posts