Baeldung의 이 글을 정리 및 추가 정보를 넣은 글입니다.

1. Overview

  • Spring에서 REST를 설정하는 법을 알아보도록 하자. 관련 Controller, HTTP response code 설정 방법, payload marshalling 관련 환경 설정, content negotiation 등도 어떻게 하는지 알아볼 것이다.

REST

  • 이 세 글에서 간단하게, 잘 정리하고 있다. 1., 2., 3.

  • (웹) 애플리케이션이 활용하는 리소스의 취득, 업데이트 등의 CRUD operation을 수행하는데 사용되는 API다. 이 때 HTTP protocol과 JSON/XML을 활용한다. 왜냐하면 애플리케이션이 활용되는 곳(모바일, 브라우저, 게임기(?) 등등)과 상관없이 통하는 API를 만들기 위해서다. 전자는 이들이 다 활용하는 통신 프로토콜, 후자는 객체에 대한 정보를 담는데 흔히 사용되는 양식.

  • 그러면서 확장성, 성능을 확보하고, 추상화의 이점을 최대한 가지려고 노력한다. 어떻게 이를 이루는지는 규약을 얘기하면서 설명.

  • 방식은 다음과 같다.

    1. URI를 통해 작업을 수행할 리소스를 명시
    2. HTTP 메서드를 통해 리소스에 수행할 작업의 종류를 명시
    3. HTTP request에 있는 pyload를 통해 리소스에 수행하는 작업에 필요한 정보들을 입력.
    4. 위를 기반으로 하는 HTTP 통신을 기반으로 해당 리소스에 해당 작업을 실제로 수행.
  • 이때 의무적으로 지켜야 하는 규약이 몇개 있다.

    • Uniform Interface : 위에서 언급한 URI를 기반으로 데이터를 조작한다는 인터페이스는 어떤 REST API든 지켜야 하는 부분이다.

      • 이유 - API 철학 때문에 그렇다. 이 API의 또다른 철학은 URI와 HTTP를 활용해 어떤 자원에, 어떤 동작을 하는지 request만으로 직접적으로 들어날 수 있도록 하는 것이 목표다. 그러면 밑의 서버 측에서 state 유지 불필요 등이 성립할 수 있고 외부서 동작의 의의 파악이 쉽기 때문이다. 이론적으론 말이다.
    • Client-Server Decoupling : Client/Server application이 독립적이어야 한다는 것이다. Client는 Server의 리소스 접근을 URI로 하는 것 외의 방식으로 수행해서는 안된다. 그리고 Server도 Client가 요청한 Data를 전달함으로서만 Client에 (간접적으로) 관여해야 하지 그 외의 방식으로 Client 애플리케이션의 동작에 관여해서는 안된다.

      • 이유 - decoupling, 그러니까 서로가 추상화된 영역만을 볼 수 있도록 하는 것, 그러니까 사실상 추상화가 가져다주는 이점은 보통 다음과 같다. 거의 모든 추상화가 여기에 해당되기에 기억하면 좋은데
        • 복잡도 하락 : 어떤 영역의 동작을 이해하는데 다른 영역의 동작 이해 필요가 없음. (Client가 Server을, Server이 Client를)
        • 재사용성 : 서로 분리된 영역 중 하나만 다른 애플리케이션에 활용하는 것이 가능.
        • 확장성 : 애플리케이션의 기능 증진을 위해 더 많은 자원을 투자해야 할 때 개별적으로 투자가 가능하고, 애플리케이션의 특정 기능을 추가하거나 확장할 때 전체가 아닌 일부분만 편집해도 가능.
        • 독립성 : 각 영역이 서로 다른 영역의 내부 동작에 영향을 받지 않음. 추상화된 영역만 동일하면 문제가 없음. 덕분에 개별적으로 발전이 가능하다.
        • 생산성 : 각 영역의 개발에 다른 영역의 개발이 연관되지 않음. 그래서 각 영역별로 독립적으로 발전이 가능해 생산성이 크게 늘어날 수 있다.
        • 보안 : 영역 중 하나가 보안에 문제가 생겨도 다른 영역에 문제를 일으킬 확률이 적음. 추상화로 인해 decoupling되어 서로 내부 동작에 영향을 줄 부분이 적기 때문.
      • 애플리케이션의 모듈화랑 비슷한 장점들을 가짐을 볼 수 있다. 사실 추상화가 되어야 모듈성이 부여되기 때문이기도 하다.
    • Stateless : Client가 요청을 보낼 때 Server이 해당 요청을 처리하는데 필요한 정보를 전부 포함해야 한다. Server도 이 API에서 '세션'을 운용해서는 안되고 따라서 세션 관련 정보를 보관해야 할 필요가 없다.

      • 규약 등장 이유 - state를 유지한다는 것 자체가 서버에 부하를 주는 요소인데, 이러면 서버의 확장성에 방해가 되기 때문이다. 즉 서버의 확장성을 위해 있는 규약이다.
    • Cacheable : 서버는 응답할 때 리소스의 캐싱이 허용되는지를 명시해야 한다. 리소스 캐싱이 가능할 경우, 클라이언트와 서버 측에서는 해당 리소스를 캐싱해 놓아야 한다.

      • 규약 등장 이유 - 이러면 캐싱이 되는 리소스에 한해 client는 요청을 안 보내고 리소스를 취득 하는 것도 가능해지고, 서버는 특정 요청에 대한 리소스 탐색 없이 바로 응답을 보내는 것이 가능해진다. 그러면 client 측은 성능 향상을, 서버 측은 확장성을 이루는 것이 가능해진다.
    • Layered System : Client가 요청을 보내고 이를 Server이 받을 때, 혹은 Server이 응답을 보내고 이를 Client가 받을 때 그 사이에 여러 layer이 관여 할 수 있음을 의식해야 한다. 그래서 Client랑 Server은 서로 직접적으로 연결되어 있다고 생각하지 말아야 한다. 그러면서 본인들 사이의 layer이 뭐가 있는지 알아내고 이에 따라 애플리케이션이 어떻게 행동할지 프로그래밍하려는 시도도 해서는 안된다.

      • 규약 등장 이유 - layer 하나 하나가 특정 기능을 추상화해서 다른 layer에 제공하는 시스템이라는 것을 인지하도록 하기 위해서다. 그러면 애초에 왜 이러한 계층 구조를 가지냐고 할 수 있는데... 결국 저렇게 계층을 나눈다는 것은 각 계층이 서로 다른 계층의 추상화된 영역만을 본다는 것을 의미한다. 그러면 앞에서 소개한 추상화가 가져다주는 장점 때문에 이런다는 것을 알 수 있다. 더 구체적으로 얘기하자면
        • 애플리케이션을 이해할 때 모든 층에서 이루어지는 일을 이해할 필요가 없다. 특히 client/server 개발자의 경우 관련 비즈니스 로직만 이해하면 된다. (복잡도 하락)
        • 나중에 특정 layer의 기능을 다른 곳에 쓸 수도 있다. (재사용성)
        • 각 layer이 다른 layer의 추상화된 영역만을 참고하지 내부 동작이랑 연관되어 있진 않기에 전체 애플리케이션이 아닌 특정 layer만 개별적으로 확장이 가능하다. (확장성)
        • 각 layer을 서로 다른 팀이 유지보수 및 테스팅 하는 것도 가능해 생산성이 높아지는 것을 기대할 수 있다. (독립성/생산성)
        • 특정 layer의 보안이 뚫려도 다른 layer에 영향이 가지 않는다. (보안)

Code On-demand 등의 선택적으로 지킬 수 있는 규약들도 있다.

  • 장점은

    • HTTP를 활용해야 하고 위의 규칙을 지킨다 외의 추가 규약이 없다. 그래서 설정할게 별로 없다.

    • HTTP를 활용하기만 하면 무엇으로 개발하든 도입이 가능하다. 언어나 실행 환경이 상관없다.

    • HTTP 메서드의 이름을 통해 리소스에 수행하려는 작업이 직접적으로 드러난다. 그리고 자원을 통해 관련 자원도 명시되어서 하려는 작업의 의의 파악이 쉽다.

    • 마찬가지로 저 규약만 지키면 개발자가 이 API를 기반으로 추가 기능을 마음대로 제공할 수 있는 자율성을 가진다.

  • 단점 및 신경써야 하는 부분은

    • '규약'이 있지 '표준'이 있는 것은 아니라서, 애플리케이션을 개발할 때 준수해야 하는 '표준'은 개발자들이 직접 정의해야 한다. 대표적인게 밑에 설명할 보안.

    • stateless 특징으로 인해 요청할 때마다 응답에 필요한 모든 내용물을 담아야 해서 HTTP 통신 사이에 전달하는 payload 크기가 비대해져 통신이 비효율적인 상황이 발생이 가능하다.

    • 보안 관련 규약 및 설정이 일절 제공되지 않는다. 정확히는 HTTPS에 온전히 의존하는 상황. 이 protocol을 활용해서 authentication/authorization 등을 구현하는 것은 온전히 개발자의 몫이기 때문에 개발자는 이와 관련해 항상 의식하고 있어야 한다.

    • 자율성이 보장된다는 것은 곧 애플리케이션마다 REST API를 가지고 어떤 기능을 제공하는지가 제각각일 수 있다는 것을 의미한다. 따라서 애플리케이션 개발자들이 제공하는 기능에 관한 문서화를 잘 해야 한다.

    • HTTP 활용을 안하는 애플리케이션에서는 사용이 불가능하다. 다만 웹의 경우 그럴 확률은 거의 없다고 봐야...

    • 하려는 일을 메서드랑 자원 이름을 통해 알 수 있는 것이 장점인데 장점인데 사실 이것만으로 나타내는데는 한계가 좀 있다.

2. Understanding REST in Spring

  • 그러면 Spring에서는 이를 어떻게 구축하는가? 2가지 방법이 있다.
    • MVC를 활용 (ModelAndView)
    • HTTP message converter을 활용

ModelAndView

  • 전자는 Spring MVC에서 제공하는 ModelAndView를 활용해서 REST API를 구현하는 것.

  • ModelAndView는 Spring MVC에서 활용되는 class 중 하나로 model과 view를 둘 다 합친 녀석이다. 꽤 오래전부터 존재했던 기능이지만 Spring 3.0부터 @ModelAttribute@ResponseBody가 등장하면서 사장된다. 그 이유는

    • model이 view를 결정하긴 하지만, 디자인 패턴 상에서 어쨌든 별개로 분리되어 있는 녀석이고, 이 때문에 코드 상에서 둘이 연관이 되어 있으면 model과 view의 desing pattern 상의 분리가 직관적으로 이루어지지 않게 된다. 안 그래도 이 디자인 패턴의 주요의의가 세 요소의 decoupling인데 이렇게 엮여있으면 해당 디자인 패턴을 사용하는 의의가 많이 없어진다. 새로운 기능인 @ModelAttribute을 쓰면 model 구성을 이 annotation을 통해 하고, view의 형성은 또 우리가 @ResponseBody와 함께 별도로 만든 코드, 혹은 thymeleaf를 통해 auto configure을 하면 되기 때문에 이 점이 많이 해결된다.

    • @ModelAttribute를 써보면 알겠지만 controller 내에서 model을 형성하는 method, 그리고 model을 기반으로 알맞는 view를 호출하거나 구성하는 method가 자연스럽게 분리가 된다. 이러면 코드 이해하기도 쉽고, model과 view의 코드상의 분리도 이루어지게 된다.

그래도 GPT 말로는 ModelAndView가 아직 활용되는 곳이 있다고는 하는데, 그게 정확한지는 모르겠어서 여기에 작성하지는 않는다.

  • 그래도 저건 MVC 자체에서의 문제점이지 REST에서는 이걸 활용할 수 있지 않을까? 라고 생각할 수 있다. 그러나 ModelAndView는 전시할 View를 반환하는데 특화되어 있지, HTTP response를 구상하는데 특화되어 있지는 않다. REST API는 View를 필요로 하는 것이 아니다. 어떤 HTTP request가 오면 그 request의 결과가 어떤지에 대한 response를 생성하는 능력과, 가끔 그 요청에서 요구하는 데이터를 JSON이나 XML 형태로 반환하는 능력이 필요하다. 이걸 ModelAndView라는 View를 반환하는 개념의 객체로 구현하는...것이 솔직히 되는지도 모르겠으나 글에서는 된다고 하고, 글에서도 언급되었듯이 그렇게 직관적이거나 간단하다고 생각하진 않는다.

  • Spring에서도 이 점을 인지해 REST API를 위한 요소인 HTTP message converter을 Spring 3.0 때 따로 만들었다. 이건 전자보다 상대적으로 가볍고, 환경 설정도 어렵지 않고 생긴 것도 직관적이기 때문에 현재는 이걸 활용해 REST API를 구현한다. 이 글에서도 후자의 방식만 소개한다.

3. The Java Configuration

@Configuration
@EnableWebMvc
public class WebConfig{
   //
}
  • 그러면 REST API 개발을 위해 우리가 사용해야 하는 configuration 관련 annotation은? 사실 @EnableWebMvc만 있어도 충분하다.

  • 이전에 배워서 알겠지만 이 annotation은 MVC design pattern을 사용할 때 기본 configuration을 사용하려는 경우 쓰이는 annotation이다. 앞에서 ModelAndView를 기반으로 REST를 만들지 않는다고 했고, MVC design pattern을 사용하는 것도 아니지만, 저 annotation을 포함해야 한다.

  • 그러면 대체 왜일까?

    • 저 annotation을 추가시 classpath에 POJO/JSON 사이 변환용 Jackson과 POJO/XML 사이 변환용 JAXB2라는 녀석이 존재하는지를 확인, 있는 경우 기본 JSON/XML converter을 저 둘을 기반으로 만든다는 것이다. 이게 유용한 이유는 보통 REST API에서의 리소스가 주로 가지는 양식이 저 둘이기 때문이다.

      • 여기서 유의해야하는 점은 Jackson이나 JAXB2가 gradle/maven 상으로 정의되어 있어서 classpath에 등록되어야 저 행동이 보장된다는 것이다.
    • 애초에 Spring은 따로 REST API를 위한 configuration annotation을 만들지를 않았다. 왜냐하면 MVC에서 요구하는 기능이랑 겹치는게 많기 때문. 예를 들면 request가 오면 이를 controller의 어느 method에 mapping을 해야 하는지, request에 있는 내용물을 어떤 data로 mapping을 해야 한다든지, exception 발생시 어떻게 handling을 해야 하는지.

    • 안 그래도 위에 언급한 것처럼 둘이 겹치는게 많은데, 둘을 위한 configuration을 별도로 만들면 개발자가 알아야 하는 것이 늘어나서 피곤해질 수 있다.

이와 관해서 더 세부적인 설정을 하고 싶은 경우 위 글에서 언급했듯 WebMvcConfigurationSupport를 직접 활용해야 한다.

다시 강조하지만 MVC pattern을 사용하는 것은 아니다. REST API layer이랑 MVC design pattern에서 요구하는 기능이, 특히 보면 controller 부분이 겹치는게 많아서 그런 것이다.

3.1 Using Spring Boot

  • Spring Boot를 활용하는 경우에는 @SpringBootApplication annotation이 달려있고 spring-webmvc library가 classpath에 등록, 즉 gradle/maven상에 이미 dependency로 추가되어 있는 경우 autoconfiguration에 의해 @EnableWebMvc도 자동으로 추가된다. 이 경우 @EnableWebMvc를 우리가 수동으로 집어넣을 필요가 없다. (또 덕분에 애플리케이션에서 필요로 하는 converter들도 자동으로 만들어진다.)

  • 몇가지 MVC 관련 기능을 더 추가하고 싶다면? 앞의 경우도 그렇고 이 경우에도 그렇고 @Configuration이 달려있는 class에다가 WebMvcConfigurer 인터페이스를 구현하면 된다.

  • WebMvcConfigurationSupport를 직접 활용해서 설정을 할 경우 앞의 경우든 이 경우든 WebMvcRegistrations라는 인터페이스를 활용하도록 하자. 이것을 통해 RequestMappingHandlerMapping, RequestMapingHandlerAdapter, ExceptionHandlerExceptionResolver의 구현체를 직접 우리가 만들어서 기본거 대신 사용하도록 제공해주는 것이 가능하다...고는 하는데 구체적인 활용을 여기서 알아보진 않겠다. 만약 할거면 저 인터페이스를 implement 한 다음에 각 method에서 return하는 것을 우리가 직접 만든 구현체로 하면 된다.

  • 마지막으로 이건 Spring Boot에만 해당되는 내용인데, 그냥 Spring Boot에서 제공하는 MVC configuration말고 우리가 만든 MVC configuration을 기반으로 동작하게 하고 싶으면 직접 @EnableWebMvc annotation이 달린 class를 만들어서 정의하면 된다. 그러면 그걸 대신 쓴다.

4. Testing the Spring Context

  • Spring의 경우 밑과 같이 하면 된다. 다만 이 글이 테스트 관련 글은 아니어서 자세한 설명은 생략.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration( 
  classes = {WebConfig.class, PersistenceConfig.class},
  loader = AnnotationConfigContextLoader.class)
public class SpringContextIntegrationTest {

   @Test
   public void contextLoads(){
      // When
   }
}

4.1 Using Spring Boot

  • Spring Boot의 경우 @SpringBootTest로 간단히 전체 context를 형성하고, @AutoConfigureMockMvc를 통해 간단히 MockMvc를 만들어서 애플리케이션에 request를 보내는 테스트를 밑처럼 간단히 만드는 것이 가능하다.
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class FooControllerAppIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void whenTestApp_thenEmptyResponse() throws Exception {
        this.mockMvc.perform(get("/foos")
            .andExpect(status().isOk())
            .andExpect(...);
    }

}
  • 전체 context를 형성할 필요가 없으면 @WebMvcTest를 활용해도 된다고 한다.
@RunWith(SpringRunner.class)
@WebMvcTest(FooController.class)
public class FooControllerWebLayerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private IFooService service;

    @Test()
    public void whenTestMvcController_thenRetrieveExpectedResult() throws Exception {
        // ...

        this.mockMvc.perform(get("/foos")
            .andExpect(...);
    }
}
  • 그런데 이 글이 테스트 관련 글은 아니어서, 자세한 설명은 생략하겠다.

5. The Controller

  • MVC design을 REST가 따르진 않지만, 그래도 REST API를 사용하는 server 측 application에서는 client측 request에 따라 어떤 작업을 할지 결정하는 controller이 필요하다. View랑 Model이 없는 MVC 이 때 핵심으로 작용하는 annotation이 바로 @RestController이다. 밑은 /foos로 오는 REST API를 따르는 HTTP request가 왔을 때 이를 어떻게 처리할지를 정의하는 controller을 만든 코드다.
@RestController
@RequestMapping("/foos")
class FooController {

    @Autowired
    private IFooService service;

    @GetMapping
    public List<Foo> findAll() {
        return service.findAll();
    }

    @GetMapping(value = "/{id}")
    public Foo findById(@PathVariable("id") Long id) {
        return RestPreconditions.checkFound(service.findById(id));
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Long create(@RequestBody Foo resource) {
        Preconditions.checkNotNull(resource);
        return service.create(resource);
    }

    @PutMapping(value = "/{id}")
    @ResponseStatus(HttpStatus.OK)
    public void update(@PathVariable( "id" ) Long id, @RequestBody Foo resource) {
        Preconditions.checkNotNull(resource);
        RestPreconditions.checkNotNull(service.getById(resource.getId()));
        service.update(resource);
    }

    @DeleteMapping(value = "/{id}")
    @ResponseStatus(HttpStatus.OK)
    public void delete(@PathVariable("id") Long id) {
        service.deleteById(id);
    }

}

@RestController

  • javadoc

  • 이 annotation의 역할은 간단하다. @Controller + @ResponseBody.

  • 이 annotation이 적절히 처리되려면 handlerMapping-HandlerAdapter 쌍이 제대로 지정되어 있어야 한다. 기본 설정을 사용한다면 RequestMappingHandlerMappingRequestMappingHandlerAdapter 쌍이 미리 지정되어 있기 때문에 신경 쓸 필요는 없지만 참고하자.

  • 구체적 용도는 이 글 참고

코드 해석

  • REST API의 layered-architecture을 잘 따르는 것을 볼 수 있다. controller은 request가 어디서 처리될지만을 유도하며, 실제 request가 어떻게 처리되는지에 대한 내용은 IFooService라는 service가 담고 있다. 해당 service의 instance는 IoC container에 의해 inject된다. (@Autowired) 이 service는 private인데, controller이 해당 내부 동작을 이해할 필요가 없기 때문**.

  • RestPreconditions에서 데이터를 찾는데 성공했는지, 못했는지를 판별하는 함수를 따로 정의하고 있다. 이 함수는 원하는 리소스를 찾지 못하면 exception을 던지는데, 이게 REST API랑 어떻게 상호작용하는지는 곧 알아보겠다.

public class RestPreconditions {
    public static <T> T checkFound(T resource) {
        if (resource == null) {
            throw new MyResourceNotFoundException();
        }
        return resource;
    }
}
  • MVC랑 유사하게 @RequestMapping를 활용해서 REST API에서 활용하는 HTTP method들을 어디서 처리할지를 지정한다. @PathVariable을 활용해서 URL에 있는 내용물을 활용하는 것도 가능하다.

HttpMessageconverter

  • Resource marshalling, 그러니까 HTTP message 상에 넣을 데이터로의 변환은 앞의 configuration으로 인해 설정된 converter이 자동으로 해준다.

  • 이 때 누굴 뭘로 marshalling할지 표기하는데 @ResponseBody@RequestBody annotation이 사용된다. 이 두 annotation은 다음 글에서 자세히 다뤄볼 것이다.

  • converter 관련 class는 데이터 타입별로 여러개가 있지만 이들의 모체는 바로 HttpMessageConverter이다.

  • 앞에서 converter들이 @EnableMVC를 활용하면 classpath에 있는 라이브러리를 기반으로 자동으로 추가된다고 했다. Jackson이나 JAX2B의 경우 이 때 추가되는 converter이 MappingJackson2HttpMessageConverter/Jaxb2RootElementHttpMessageconverter이다.

  • 구체적인 동작에 관한 좋은 글이 하나 있으니 참고.

ContentNegotiationStrategy

  • 이때 marshalling에서 중요한 부분은, 어떤 자료구조로 변환할 것이냐인데 (content negotiation), Spring에서 보통 이는 request HTTP의 Accept header을 Spring이 파악, 이에 기반해 적절한 converter을 선택함으로서 이루어진다.

  • 당연히 구체적으로 설정하는 방법은 없냐고 할 수 있고 존재도 한다. 그 class 이름은 바로 ContentNegotiationStrategy.

  • 단순히 어떤 자료구조를 반환할지를 지정하는데만 사용되지 않는다. 정확히는, request에 대한 response를 제작하는데 어떤 규칙을 지킬 것인지를 전부 지정하는데... 이를 설정하는데 사용되는 것이 ContentNegotiationConfigurer이며, 이걸로 configure을 하는 method는 configureContentNegotiation으로, WebMvcConfigurerAdapter을 extend하는 configuration class에서 오버라이딩이 가능하다.

  • ContentNegotiationConfigurer javadoc

Content Negotiation은 REST API에서처럼 HTTP message의 payload에 넣을 data-format을 뭘로 할지 정할 때, 즉 HttpMessageConverter에서도 사용되지만, 다른 두 가지 경우에서도 사용이 가능하다.

  • Request Mapping : HTTP request가 왔는데, 거기서 요구하는 format에 따라 다른 controller method가 처리해야해서 무슨 method를 선택할지 결정할 때.
  • View Resolution : request의 요청에 대해 적절한 view를 찾을 때.

좀 더 자세한 내용은 이 글을 참고. 옛날 글이라 일부 내용은 좀 구식이라는 점 유의. 예를 들어 favorPathExtension은 현재는 deprecated된 method다.

@ResponseStatus

  • javadoc

  • Request에 따라 적절한 HTTP status code를 반환해야 하는 경우가 있다. 보통은 200(OK)이겠지만, 상황에 따라 다를 수 있다! 이 경우 위 annotation을 사용한다.

  • 단 조심해야 하는 것이, Exception을 throw하는 경우에, 혹은 이 annotation의 reason을 설정하는 경우에 사용하면 HttpServletResponse.sendError이 사용된다는 것을 유의하자. 이게 유의해야하는 것이, 호출되면 Servlet Container 측에서 내부적으로 해당 response가 완성되었다고 가정하기에 그 이상으로 편집되어서는 안되기 때문이다. ...라고 documentation에 나와있지만 사실 개인적으로 이후에 어떻게 편집하도록 프로그래밍하는 것이 일반적으로 가능한지는 잘 모르겠다. 그래도 참고는 해두자.

  • class 차원에서 해당 annotation을 사용하는 것도 가능하다. 이 경우 모든 Exceptionhandler이랑 @RequestMapping에게 부여된다. 이 경우 하위에서 오버라이딩 하는것도 가능하다는 점 유의.

6. Mapping the HTTP Response Codes

  • REST API에서 특히 중요한 부분 중 하나로 각 request에 대해 어떤 response를 반환해야 하는가가 있다. 여러 상황에 대해 알아보고, 이를 Spring에서 개발하는 경우 어떻게 처리하는지 알아보도록 하자.

6.1 Unmapped Requests

  • 만약 Client 요청과 관련된, 즉 map된 controller method를 찾지 못하면 405 (METHOD NOT ALLOWED)를 반환해야 한다. 그러면서 가능한 메서드들을 나타내기 위해 Allow라는 HTTP header과 함께 가능한 method들을 반환하는 것이 관습이다.

  • 이는 Spring에서 알아서 설정해준다. 그래서 별로 신경 쓸 필요는 없다. (야호~)

6.2 Valid Mapped Requests

  • 만약 map된 controller method가 있다면 Spring은 기본적으로 반환할 HTTP status code를 200 (OK)로 설정한다.

  • 그래서 GET을 handle하는 method는 @ResponseStatus annotation이 없다.

6.3 Client Error

  • client에서 일으킬만한 오류가 있다면 관련 exception을 먼저 우리가 정의하고, @ResponseStatus header을 붙여가지고 어떤 response를 전달할지를 정의해놓아야 한다.
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class BadRequestException extends RuntimeException {
   //
}
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
   //
}
  • 여기서 유의해야하는 것은 반환하는 exception이 REST API에서 활용하는 exception, 그러니까 자체적으로 만든 exception이어야 한다는 것이지 타 layer에서 발생하는 exception을 그대로 반환해서는 안된다. 예를 들어 DAO/DAL과 같이 데이터와 관련된 exception이 데이터 layer에서 발생한 경우 해당 exception을 그대로 반환하지 말고, 해당 exception에 관해 REST API에서 지정한 exception으로 치환을 하고 그걸 반환해야 한다는 것이다.

  • 또 이 exception은 따로 catch를 할 필요가 없다. 그러지 않아도 자동으로 처리가 된다. 물론 저렇게 @ResponseStatus annotation이 되어 있는 경우에 한해서 말이다.

6.4 Using @ExceptionHandler

  • 6.3의 방법 말고도 오류 관련 status code를 exception이랑 연동시키는 방법이 있는데 @ExceptionHandler이라는 annotation을 controller class 내부에서 사용하는 것이다.

  • 다만 해당 controller에서 발생하는 exception들에 대해서만 처리가 가능해가지고 각 controller마다 exception들에 대해 예외 처리 정의를 따로 해야한다는 문제점이 있다. 다른 말로는... 만일 controller별로 각 exception마다 이에 대응할 코드가 다르다면 이 방식을 쓰는게 좋다.

  • @ExceptionHandler은 굳이 REST API에서뿐만 아니라, controller을 포함하는 handler에서 발생하는 오류를 처리하는데 사용될 수 있는 annotation이라는 점 유의. 관련 javadoc

  • value field에 처리할 exception을 명시해야 한다.

7. Additional Maven Dependencies

  • 앞에서 말했던, JSON이나 XML 변환을 위한 converter이 자동으로 등록되도록 Jackson 혹은 JAXB2를 dependency에 추가하라는 내용이다. 그래서 딱히 자세히 언급은 안하겠다.
<dependencies>
   <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.9.8</version>
   </dependency>
   <dependency>
      <groupId>javax.xml.bind</groupId>
      <artifactId>jaxb-api</artifactId>
      <version>2.3.1</version>
      <scope>runtime</scope>
   </dependency>
</dependencies>

7.1 Using Spring Boot

  • 다만 Spring boot의 경우 JSON을 REST API에서 활용하는 data로 정할 경우, web-starter을 사용하면 Jackson이 자동으로 포함되어 있기 때문에 따로 dependency로 추가할 필요가 없다는 점 유의
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • XML, 즉 JAX2B를 사용할거면 따로 추가해야하긴 한다.
profile
안 흔하고 싶은 개발자. 관심 분야 : 임베디드/컴퓨터 시스템 및 아키텍처/웹/AI

0개의 댓글