1. 개요

이 예제에서는 Problem Spring 웹 라이브러리 를 사용하여 application/problem+json 응답 을 생성하는 방법 을 탐색할 것 입니다. 이 라이브러리는 오류 처리와 관련된 반복 작업을 피하는 데 도움이 됩니다.

문제 Spring Web을 Spring Boot 애플리케이션에 통합함으로써  프로젝트 내에서 예외를 처리하고 그에 따라 응답을 생성하는 방식을 단순화 할 수 있습니다 .

2. 문제 라이브러리

문제 는 Java 기반 Rest API가 소비자에게 오류를 표현하는 방식을 표준화하기 위한 작은 라이브러리입니다.

문제  는 알리고자 하는 오류의 추상화입니다 . 여기에는 오류에 대한 편리한 정보가 포함되어 있습니다. 문제  응답 의 기본 표현을 살펴보겠습니다  .

{
  "title": "Not Found",
  "status": 404
}

이 경우 상태 코드와 제목만으로도 오류를 설명하기에 충분합니다. 그러나 자세한 설명을 추가할 수도 있습니다.

{
  "title": "Service Unavailable",
  "status": 503,
  "detail": "Database not reachable"
}

필요에 따라 사용자 지정 문제 개체를 만들 수도 있습니다 .

Problem.builder()
  .withType(URI.create("https://example.org/out-of-stock"))
  .withTitle("Out of Stock")
  .withStatus(BAD_REQUEST)
  .withDetail("Item B00027Y5QG is no longer available")
  .with("product", "B00027Y5QG")
  .build();

이 예제에서는 Spring Boot 프로젝트의 문제 라이브러리 구현에 중점을 둘 것입니다.

3. 문제 스프링 웹 설정

Maven 기반 프로젝트이므로 pom.xml 에 problem-spring-web 의존성을 추가해 보겠습니다 .

<dependency>
    <groupId>org.zalando</groupId>
    <artifactId>problem-spring-web</artifactId>
    <version>0.23.0</version>
</dependency>
<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.4.0</version> 
</dependency>
<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.4.0</version>  
</dependency>

또한 spring-boot-starter-web 및  spring-boot-starter-security 의존성이 필요합니다. spring Security는 problem-spring-web 버전 0.23.0부터 필요합니다 .

4. 기본 구성

첫 번째 단계로 화이트 라벨 오류 페이지를 비활성화해야 사용자 정의 오류 표현을 대신 볼 수 있습니다.

@EnableAutoConfiguration(exclude = ErrorMvcAutoConfiguration.class)

이제 ObjectMapper에 필요한 구성 요소 중 일부를 등록해 보겠습니다 .

@Bean
public ObjectMapper objectMapper() {
    return new ObjectMapper().registerModules(
      new ProblemModule(),
      new ConstraintViolationProblemModule());
}

그런 다음 application.properties 파일 에 다음 속성을 추가해야 합니다.

spring.resources.add-mappings=false
spring.mvc.throw-exception-if-no-handler-found=true
spring.http.encoding.force=true

마지막으로 ProblemHandling 인터페이스를 구현해야 합니다.

@ControllerAdvice
public class ExceptionHandler implements ProblemHandling {}

5. 고급 구성

기본 구성 외에도 Security 관련 문제를 처리하도록 프로젝트를 구성할 수도 있습니다. 첫 번째 단계는 Spring Security와 라이브러리 통합을 활성화하는 구성 클래스를 만드는 것입니다.

@Configuration
@EnableWebSecurity
@Import(SecurityProblemSupport.class)
public class SecurityConfiguration {

    @Autowired
    private SecurityProblemSupport problemSupport;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // Other security-related configuration
        http.exceptionHandling()
          .authenticationEntryPoint(problemSupport)
          .accessDeniedHandler(problemSupport);
        return http.build();
    }
}

마지막으로 Security 관련 예외에 대한 예외 처리기를 만들어야 합니다.

@ControllerAdvice
public class SecurityExceptionHandler implements SecurityAdviceTrait {}

6. REST 컨트롤러

애플리케이션을 구성하고 나면 RESTful 컨트롤러를 만들 준비가 된 것입니다.

@RestController
@RequestMapping("/tasks")
public class ProblemDemoController {

    private static final Map<Long, Task> MY_TASKS;

    static {
        MY_TASKS = new HashMap<>();
        MY_TASKS.put(1L, new Task(1L, "My first task"));
        MY_TASKS.put(2L, new Task(2L, "My second task"));
    }

    @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
    public List<Task> getTasks() {
        return new ArrayList<>(MY_TASKS.values());
    }

    @GetMapping(value = "/{id}",
      produces = MediaType.APPLICATION_JSON_VALUE)
    public Task getTasks(@PathVariable("id") Long taskId) {
        if (MY_TASKS.containsKey(taskId)) {
            return MY_TASKS.get(taskId);
        } else {
            throw new TaskNotFoundProblem(taskId);
        }
    }

    @PutMapping("/{id}")
    public void updateTask(@PathVariable("id") Long id) {
        throw new UnsupportedOperationException();
    }

    @DeleteMapping("/{id}")
    public void deleteTask(@PathVariable("id") Long id) {
        throw new AccessDeniedException("You can't delete this task");
    }

}

이 컨트롤러에서는 일부 예외를 의도적으로 발생시킵니다. 이러한 예외는 자동으로 문제  개체 로 변환되어  오류 세부 정보가 포함된 application/problem+json 응답을 생성합니다.

이제 내장된 어드바이스 특성과 사용자 지정 문제 구현을 만드는 방법에 대해 이야기하겠습니다.

7. 내장 조언 특성

어드바이스 특성은 예외를 포착하고 적절한 문제 객체를 반환하는 작은 예외 처리기입니다.

일반적인 예외에 대한 기본 제공 어드바이스 특성이 있습니다. 따라서 간단히 예외를 던짐으로써 사용할 수 있습니다.

throw new UnsupportedOperationException();

그 결과 다음과 같은 응답을 받게 됩니다.

{
    "title": "Not Implemented",
    "status": 501
}

Spring Security와의 통합도 구성했으므로 Security 관련 예외를 발생시킬 수 있습니다.

throw new AccessDeniedException("You can't delete this task");

그리고 적절한 응답을 얻습니다.

{
    "title": "Forbidden",
    "status": 403,
    "detail": "You can't delete this task"
}

8. Custom형 문제 만들기

Problem 의 사용자 지정 구현을 만드는 것이 가능합니다 . AbstractThrowableProblem 클래스 를 확장하기만 하면 됩니다 .

public class TaskNotFoundProblem extends AbstractThrowableProblem {

    private static final URI TYPE
      = URI.create("https://example.org/not-found");

    public TaskNotFoundProblem(Long taskId) {
        super(
          TYPE,
          "Not found",
          Status.NOT_FOUND,
          String.format("Task '%s' not found", taskId));
    }

}

그리고 다음과 같이 사용자 정의 문제를 던질 수 있습니다.

if (MY_TASKS.containsKey(taskId)) {
    return MY_TASKS.get(taskId);
} else {
    throw new TaskNotFoundProblem(taskId);
}

TaskNotFoundProblem  문제 를 던진 결과 다음과 같은 결과가 나타납니다.

{
    "type": "https://example.org/not-found",
    "title": "Not found",
    "status": 404,
    "detail": "Task '3' not found"
}

9. 스택 트레이스 다루기

응답 내에 스택 추적을 포함하려면 이에 따라 ProblemModule 을 구성해야 합니다.

ObjectMapper mapper = new ObjectMapper()
  .registerModule(new ProblemModule().withStackTraces());

일련의 원인은 기본적으로 비활성화되어 있지만 동작을 재정의하여 쉽게 활성화할 수 있습니다.

@ControllerAdvice
class ExceptionHandling implements ProblemHandling {

    @Override
    public boolean isCausalChainsEnabled() {
        return true;
    }

}

두 기능을 모두 활성화하면 다음과 유사한 응답을 받게 됩니다.

{
  "title": "Internal Server Error",
  "status": 500,
  "detail": "Illegal State",
  "stacktrace": [
    "org.example.ExampleRestController
      .newIllegalState(ExampleRestController.java:96)",
    "org.example.ExampleRestController
      .nestedThrowable(ExampleRestController.java:91)"
  ],
  "cause": {
    "title": "Internal Server Error",
    "status": 500,
    "detail": "Illegal Argument",
    "stacktrace": [
      "org.example.ExampleRestController
        .newIllegalArgument(ExampleRestController.java:100)",
      "org.example.ExampleRestController
        .nestedThrowable(ExampleRestController.java:88)"
    ],
    "cause": {
      // ....
    }
  }
}

10. 결론

이 기사에서는 Application/problem+json 응답 을 사용하여 오류 세부 정보가 포함된 응답을 생성하기 위해 Problem Spring 웹 라이브러리를 사용하는 방법을 탐색했습니다   . 또한 Spring Boot 애플리케이션에서 라이브러리를 구성하고 문제  개체 의 사용자 지정 구현을 만드는 방법도 배웠습니다 .

이 사용방법(예제)의 구현은 GitHub 프로젝트 에서 찾을 수 있습니다.  이것은 Maven 기반 프로젝트이므로 그대로 가져오고 실행하기 쉬워야 합니다.

Generic footer banner