[스프링/Spring] @Controller 그리고 @RestController

kindof

·

2021. 9. 17. 19:33

📖 문제

오늘은 프로젝트에서 어떤 아이템들을 분류하기 위한 카테고리를 생성하는 코드를 짜다가 마주한 문제에 대해 글을 작성해보려고 합니다.

 

사실 이 글을 작성한 지 한 달이 좀 넘어서 부분적인 내용을 수정하고 있는데 @Controller와 @RestController의 차이는 정말 당연한 개념이지만 반드시 알아야 하는 개념인 것 같습니다.

 

아래 코드를 보면서 이야기해보겠습니다.

@Controller
@RequiredArgsConstructor
public class CategoryApiController {

    private final CategoryService categoryService;

    // 등록 API
    @PostMapping("/api/category/add")
    public Integer save(@RequestBody CategorySaveRequestDto requestsDto){
        return categoryService.save(requestsDto);
    }
    ...
    ...
}
@RequiredArgsConstructor
@Service
public class CategoryService {

    private final CategoryDao categoryDao;

    // 등록
    @Transactional
    public Integer save(CategorySaveRequestDto requestDto){
        return categoryDao.save(requestDto.toEntity()).getId();
    }
    ...
    ...
    
}

위 코드는 프로젝트 내에서 필요한 아이템 카테고리에 대한 API를 관리하는 Controller와 Service 코드입니다.

 

컨트롤러에서는 save API 메서드를 통해 카테고리 생성에 관한 Dto를 Service에 전달하고, 해당 Service에서 JPA 레포지토리를 상속하는 DAO를 통해 저장하는 방식입니다. 

 

위와 같은 코드를 작성하고 나서 정상적으로 기능이 작동하는지 테스트 코드를 작성했습니다.

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CategoryApiControllerTest {
    @LocalServerPort
    private int port;

    @Autowired
    private CategoryDao categoryDao;

    @AfterEach
    public void tearDown() throws Exception{
        categoryDao.deleteAll();
    }

    @Autowired
    private WebApplicationContext context;

    private MockMvc mvc;

    @BeforeEach
    public void setup() {
        mvc = MockMvcBuilders
                .webAppContextSetup(context)
                .apply(springSecurity())
                .build();
    }

    @Test
    @WithMockUser(roles="USER")
    public void Category_생성() throws Exception{
        // 데이터 전송 객체 생성
        String name = "category1";

        CategorySaveRequestDto requestDto = CategorySaveRequestDto.builder().name(name).build();
        // PostMapping URL
        String url = "http://localhost:" + port + "/api/category/add";

        //when
        mvc.perform(post(url)
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .content(new ObjectMapper().writeValueAsString(requestDto)))
                .andExpect(status().isOk());

        List<Category> all = categoryDao.findAll();
        assertThat(all.get(0).getName()).isEqualTo(name);
    }
}

"category1"이라는 이름의 카테고리를 하나 만들고 requestDto에 담아서 컨트롤러에 보내는 로직이죠. 그리고 결과값으로는 categoryDao에 정상적으로 해당 카테고리가 생성되었는지 확인합니다.

 

그런데, 위 테스트 코드는 아래와 같은 에러를 던지면서 실패했습니다.

테스트 실패

분명 로직은 맞게 짠 것 같은데 위처럼 테스트는 실패했고, 이를 해결하기 위해 열심히 구글링을 해보던 중 @Controller와 @RestController의 차이가 원인이 된다는 것을 알게 되었습니다.

 

그리고 컨트롤러의 @Controller 어노테이션을 @RestController로 수정하니 테스트가 성공했죠.

테스트 성공

 

@Controller와 @RestController의 차이가 무엇이기에 작성했던 테스트 코드가 실패하고 성공했을까요?

 

 

🗒 @Controller와 @RestController

Spring MVC 컨트롤러인 @Controller는 주로 View를 반환하기 위해 사용합니다. ViewResolver 설정에 맞는 View를 찾아 화면에 렌더링해주는 것이죠.

 

하지만 컨트롤러가 항상 View를 반환하기 위해 사용되는 것은 아닌데요.

 

컨트롤러가 HTTP 메시지 바디에 직접 데이터를 입력해서 Rest API를 개발하려고 하면, HTTP Body에 JSON 형태의 데이터를 넘겨줘야 하는 경우도 있습니다.

 

그리고 이 때, Spring MVC 컨트롤러에서는 데이터를 받을 때 @RepuestBody를 통해 HTTP Request Body에서 내용을 객체로 전달받고, 응답을 하기 위해 @ResponseBody 어노테이션을 활용하여 객체를  Json 형태로 반환할 수 있게 하죠.

 

조금 더 구체적으로 이야기하자면, @ResponseBody를 사용하면 viewResolver 대신에 HttpMessageConverter가 동작하게 되는데요.

 

이 때 리턴하는 값이 스트링이라면 StringHttpMessageConverter가 동작하고, 객체를 반환한다면 MaappingJackson2HttpMessageConverter가 동작하게 되는 것입니다.

 


 

 

문제는 여기에 있었습니다. @Controller를 쓰면서 데이터를 반환하기 위해서는 @ResponseBody 어노테이션을 써야합니다. 그런데 위에서 작성한 코드에서는 요청을 보낼 때만 @RequestBody에 객체를 실어 보내고, 리턴할 때는 @ResponseBody가 없다는 것입니다.

 

따라서 우리가 리턴하는 값은 HttpResponse의 Body 데이터가 아닌, View 템플릿의 이름을 리턴하고 있는 꼴이 되죠.

 

반면, 스프링 4.0부터 추가된 @RestController는 @Controller + @ResponseBody가 결합된 어노테이션으로 위와 같은 문제를 정확히 해결해줍니다. 

@RestController

 

@RestController를 사용하면 @Controller + @ResponseBody 어노테이션을 붙이는 것과 동일하기 때문에 객체를 반환하면 객체 데이터는 JSON 형식의 HTTP 응답으로 작성된다는 것입니다.

 

따라서 @Controller를 @RestController로 치환해주면, @ResponseBody 기능도 자동으로 탑재되게 되고 우리가 전달하는 객체(생성된 카테고리의 Id)가 전달되는 것입니다.

 


이렇게 이번 글에서는 @Controller와 @RestController의 차이에 대해 공부해봤는데요.

 

정말 간단한 내용이지만 정확히 그 차이를 알아간다는 데 의의가 있는 것 같습니다.

 

감사합니다.