본문 바로가기
우아한테크코스/레벨 2 - Spring

[Spring] @Controller와 @RestController 차이, 그리고 ResponseEntity

by shyun00 2024. 4. 17.

드디어 스프링 관련된 미션이 시작되었다.

예전에 공부를 할 때에는 주어진 자료를 따라 치기에 집중했던 것 같아서,

이번에는 구체적인 원리나 차이점 등을 좀 더 살펴보면서 학습하고자 한다.

 

그 중 이번에 가장 먼저 다룰 내용은 @Controller@RestController이다.

컨트롤러란, 클라이언트의 요청을 직접적으로 전달받는 엔드포인트(Endpoint)이다.

 

공식 문서에서 Spring MVC 컨트롤러에 대한 내용은 아래와 같이 설명하고 있다.

Spring MVC provides an annotation-based programming model where @Controller and @RestController components use annotations to express request mappings, request input, exception handling, and more. Annotated controllers have flexible method signatures and do not have to extend base classes nor implement specific interfaces.

 

@Controller, @RestController를 붙여주기만 하면 해당 클래스는 컨트롤러 클래스가 된다.

그렇다면 이 둘의 차이는 어떤 것인지 한번 살펴보자.

 

@Controller

먼저 @Controller 의 코드이다. 주로 View를 응답할 때 사용한다.

(그래서 Thymeleaf와 같은 ViewResolver를 사용하지 않는다면, @Controller를 사용할 일은 많지 않은 것 같다.)

...
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";
}

 

@Controller를 활용한 아래 예시 코드를 살펴보자.
/hello 경로로 Get요청이 오면, model에 "message"라는 이름에 해당하는 값으로 "Hello World!"라는 값을 넣어 index.html에 전달한다.


최종적으로 사용자는 model의 데이터가 들어간 view를 응답으로 받게 된다.

View의 이름을 String 타입으로 리턴해주면 해당 View가 응답으로 전달되는 것이다.

@Controller
public class HelloController {

    @GetMapping("/hello")
    public String handle(Model model) {
        model.addAttribute("message", "Hello World!");
        return "index";
    }
}

 

하지만 사용자 요청에 따라 View가 아니라 Json 데이터를 응답해주어야하는 경우가 있을 수도 있다.

(CSR 방식의 애플리케이션에서는 view를 바로 전달해주기보다 필요한 데이터만 전달해주는 경우가 많다.)

 

이 때는 해당 메서드에 @ResponseBody를 붙여주면 된다.

@Controller
public class ReservationController {

    @ResponseBody
    @PostMapping("/reservations")
    public Reservation createReservation(@RequestBody ReservationCreateDto dto) {
        Reservation reservation = new Reservation(index.getAndIncrement(), dto.getName(), dto.getDate(), dto.getTime());
        reservations.add(reservation);
        return reservation;
    }
}

 

@ResponseBody를 붙이면 Reservation 객체에 대한 내용이 HttpMessageConverter에 의해 Json 형태로 변경되어 아래와 같이 전달된다. (HttpMessageConverter는 요청이나 응답의 Content-Type 헤더를 기준으로 적절한 변환기를 사용해 데이터를 변환하므로, 일반적으로 Json 타입을 리턴하나 요청에 따라 XML 등 다른 타입을 리턴할수도 있다.)

[
  {
    "id": 2,
    "name": "asdfasg",
    "date": "2024-04-23",
    "time": "13:57"
  }
]

 

그렇다면, view를 전달하지 않고 Json 데이터를 전달하는 백엔드 애플리케이션은 모든 컨트롤러 메서드에 @ResponseBody를 붙여주어야 하는걸까?

아니다. 이 때 사용되는 것이 @RestController이다.

 

@RestController

@RestController를 이해하기 가장 쉬운 방법은 해당 코드를 살펴보는 것이다.

위에서 봤던 @Controller와의 차이점이 보이는가?

@Controller와 @ResponseBody가 붙어있는 것을 확인할 수 있다.

...
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
    @AliasFor(
        annotation = Controller.class
    )
    String value() default "";
}

즉, @Controller에 @ResponseBody가 추가된 것으로 Json 형태의 데이터를 반환하기 위해 사용된다.

@RestController의 모든 메서드에 @ResponseBody를 붙이는 것과 같은 역할을 한다.

 

따라서 위에서 예약을 등록했던 코드를, 아래와 같이 수정할 수 있다.(@ResponseBody를 삭제했다.)

@RestController
public class ReservationController {

    @PostMapping("/reservations")
    public Reservation createReservation(@RequestBody ReservationCreateDto dto) {
        Reservation reservation = new Reservation(index.getAndIncrement(), dto.getName(), dto.getDate(), dto.getTime());
        reservations.add(reservation);
        return reservation;
    }
}

 

그런데 이 때, 이렇게 전달된 Reservation 객체의 데이터에 원하는 HttpStatus를 설정할 수 없다.

흔히 우리가 HTTP 요청을 했을 때 받는 201 Created, 304 Not Modified 등을 설정할 수 없다는 뜻이다.

이렇게 되면 요청이 올바르게 완료된 것인지, 잘못된 것인지 확인하기가 어렵다.

 

만약 현재 상태에서 HttpStatus를 지정해주고자 한다면, @ResponseStatus(HttpStatus.OK) 와 같은 애너테이션을 추가해야한다.애너테이션으로 직접 HttpStatus를 매번 적용하기 번거롭지 않은가. 이 때 사용할 수 있는 것이 ResponseEntity이다.

 

ResponseEntity

ResponseEntity의 코드는 아래와 같다.

공식 문서에서도 ResponseEntity에 대해 다음과 같이 설명하고 있다. ResponseEntity is like @ResponseBody but with status and headers.

말 그대로, @ResponseBody와 유사하지만 상태와 헤더를 가진 객체를 뜻한다.

(참고: ResponseEntity 타입으로 리턴할 경우 HttpMessageConverter에 의해 Json 형태로 변환되므로, @Controller를 사용해도 Json 형태로 응답한다.)

public class ResponseEntity<T> extends HttpEntity<T> {
    private final HttpStatusCode status;

    public ResponseEntity(HttpStatusCode status) {
        this((Object)null, (MultiValueMap)null, status);
    }

    public ResponseEntity(@Nullable T body, HttpStatusCode status) {
        this(body, (MultiValueMap)null, status);
    }
    ...
}

 

단순히 객체를 리턴하지 않고, ResponseEntity를 통해 상태를 같이 전달하면 아래와 같이 코드를 수정할 수 있다.

 

이렇게 되면 reservation에 대한 내용을 Body로 갖는 HTTP 응답이 전달된다.

(ResponseEntity를 생성하는 방법은 생성자, build 등 여러가지가 있다. 필요에 따라 적절한 방법을 사용하자!)

@RestController
public class ReservationController {

    @PostMapping("/reservations")
    public ResponseEntity<Reservation> createReservation(@RequestBody ReservationCreateDto dto) {
        Reservation reservation = new Reservation(index.getAndIncrement(), dto.getName(), dto.getDate(), dto.getTime());
        reservations.add(reservation);
        return new ResponseEntity<>(reservation, HttpStatus.CREATED);
    }
}

 

여기까지 @Controller와 @RestController의 차이점과 각 사용 용도에 대해 알아보았다.

백엔드 / 프론트엔드를 분리하고 CSR 방식의 애플리케이션을 개발한다면 @RestController를 주로 사용하게 될 것 같다.

두가지의 특징에 대해 이해하고 상황에 맞는 적절한 방법을 사용할 수 있도록 해야겠다. :)

 

참고자료 (Spring 공식 문서)

@Annotated Controllers

@RequestMapping

@RequestBody

@ResponseBody

ResponseEntity