안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.
오늘은 SpringBoot의 REST API에서 자주 사용하거나 잘못 알고 있었던 어노테이션들을 정리하는 시간을 가지려고 합니다.
1. @RestController
이 어노테이션은 @Controller나 @Service와 같이 스테레오타입의 어노테이션으로 이 어노테이션이 지정된 클래스를 스프링 컴포넌트 검색으로 찾을 수 있습니다. 스테레오타입의 어노테이션이란 스프링 컨테이너가 스프링 관리 컴포넌트로 식별하게 해주는 어노테이션입니다. scan-auto-detection과 dependency injection을 사용하기 위해서 사용됩니다.
이 어노테이션을 사용하면 컨트롤러의 모든 HTTP 요청 처리 메서드에서 HTTP 응답 body 직접 쓰는 값을 반환한다는 것을 스프링에게 알려주는 역할을 수행합니다. 따라서, 반환 값이 HTML 형태의 뷰가 아닌 HTTP 응답으로 값을 전달할 수 있습니다. 만약, @Controller를 사용한다면 @ResponseBody 어노테이션을 함께 사용해야 합니다.
2. @RequestMapping
RequestMapping은 http 요청이 온 uri와 특정 메서드를 매핑시켜주는 역할을 수행합니다. @RequestMapping은 메서드단에서 사용하면 특정 메서드의 uri에 매핑되고 아래 코드와 같이 클래스 단에서 사용하면, 공통적인 uri를 묶어서 사용할 때 활용할 수 있습니다. @RequestMapping은 value, path, produces 등을 사용하는데, value는 path의 별칭이라고 합니다. (아직까지 value와 path로 인한 에러는 발생한 경험은 없지만 해당 어노테이션의 파라미터에 대한 차이점은 기회가 되면 정리하도록 하겠습니다.)
produces는 Accpet 헤더에 "application/json"이 포함된 요청만 처리하겠다는 것을 의미합니다.
@Slf4j
@RestController
@RequestMapping(path = "/api/v1/schools", produces = "application/json")
@RequiredArgsConstructor
public class SchoolController {
private final SchoolService schoolService;
@GetMapping("/search")
public ResponseEntity getSchoolsSearchRes(@RequestBody SchoolSearchReqDto reqDto) {
Page<SchoolSearchDto> schoolSearchDto = schoolService.getSchoolSearchDto(reqDto);
return new ResponseEntity(new RestPage<>(schoolSearchDto), HttpStatus.OK);
}
3. @CrossOrigin
아래 예시에는 사용되지 않았지만, 백엔드 팀과 프론트 팀이 협업하여 프로젝트를 진행한다고 하면, 프론트팀은 서버 api와 별도의 도메인(프로토콜과 호스트 및 포트로 구성)을 사용할 가능성이 있습니다. 이때, 동일 출처 정책(동일 출처 정책은 동일한 출처의 리소스에만 접근하도록 제한)으로 인해 에러가 발생할 수 있습니다. 이런 제약은 서버 응답에 CORS(Cross-Origin Resource Sharing) 헤더를 포함시켜 극복할 수 있습니다. CrossOrigin(origin="*") 어노테이션을 사용하면, 다른 도메인도 해당 REST API를 사용할 수 있게 허용합니다.
추가로, 해당 CORS를 허용하는 방법은 어노테이션 이외에 Config를 작성하여 Filter를 빈으로 등록하는 방법이 있습니다. 이는 4번과 연관 지어서 설명하겠습니다.
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
corsConfiguration.addAllowedOriginPattern("*");
corsConfiguration.addAllowedOriginPattern("http://localhost:3000");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
source.registerCorsConfiguration("/api/**", corsConfiguration);
return new CorsFilter(source);
}
}
4. @Configuration 및 @Bean
Configuration 어노테이션은 각 빈을 스프링 애플리케이션 컨텍스트에 제공하는 구성 클래스라는 것을 스프링에게 알려줍니다. 구성 클래스의 메서드는 @Bean 어노테이션으로 지정되어 있습니다. 이것은 각 메서드에 반환되는 객체가 애플리케이션 컨텍스트 빈으로 추가되어야 한다는 것을 의미합니다.
CorsConfig를 @Configuration으로, corsFilter를 @Bean으로 설정하면, 스프링 컨테이너가 corsFilter를 런타임 시점에 생성하여 스프링 빈으로 등록합니다.
따라서, 해당필터를 SecurityConfig에 filter로 등록한다면, CrossOrigin 어노테이션을 사용하지 않고 CORS error를 방지할 수 있습니다.
// SecurityConfig FilterChain
.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class);
5. @RequestBody
@RequestBody 어노테이션의 역할은 클라이언트가 보내는 HTTP 요청 본문(JSON, XML 등)을 JAVA 객체로 변환하는 역할을 수행합니다. HTTP 요청 본문 데이터는 Spring이 제공하는 HttpMessageConverter를 통해 타입에 맞는 객체로 변환됩니다. 이때, 기본생성자로 JSON 값을 JAVA 객체로 바인딩할 수 있습니다.
a. 어떻게 JSON을 JAVA 객체로 재구성할 수 있을까?
MappingJackson2HttpMessageConverter는 ObjectMapper를 통해 JSON 값을 java 객체로 역직렬화하는 것을 알 수 있습니다. 역직렬화란 생성자를 거치지 않고 리플렉션을 통해 객체를 재구성하는 메커니즘입니다. 직렬화가 가능한 클래스들은 기본 생성자가 필수입니다. 따라서, @RequestBody에 사용하려는 Dto 클래스는 기본 생성자가 없으면 바인딩에 실패합니다.
b. 구조 확인해 보기
디버깅 과정에서 해당 MappingJackson2HttpMessageConverter가 어떠한 방식으로 작동하여 json이 객체에 바인딩되는 것인지 파악해 보겠습니다. 먼저 IntelliJ에서 AbstractMessageConverterMethodArgumentResolver 클래스를 연 후, 중단점을 체크해 보았습니다.먼저 register uri를 가지는 컨트롤러와 해당 컨트롤러로 회원 가입 요청을 하겠습니다.
/**
*
* 회원 가입 요청
* BAD_REQUEST: 400 (요청한 파라미터의 타입 에러 혹은 바인딩 에러)
* CONFLICT: 409 (요청한 회원가입 이메일 이미 존재)
* OK: 200 (가입 완료)
*
* @param formRegisterUserDto
* @param bindingResult
* @return
*/
@PostMapping("/register")
public ResponseEntity formRegister(@Valid @RequestBody FormRegisterUserDto formRegisterUserDto,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
if (!userService.registerForm(formRegisterUserDto, passwordEncoder)) {
return new ResponseEntity<>(HttpStatus.CONFLICT);
}
return new ResponseEntity<>(HttpStatus.OK);
}
messageConverters는 request 요청한 dto가 컨버터 타입이 맞을 때까지 반복문이 실행됩니다.
만약 컨버터가 작동가능한 상태가 아니라면 GenericConverter는 null값이 됩니다.
MappingJackson2HttpMessageConverter 가 선택되면, 해당 메시지에 Body가 있고 그 Body가 파싱 되어 Dto에 값이 처리됩니다.
이후, contentType과 body를 설정한 후, body를 리턴하는 것을 확인할 수 있습니다.
따라서, 정리하면 @RequestBody는 클라이언트에게 Json으로 request 요청을 받게 되면, MappingJackson2HttpMessageConverter가 해당 컨트롤러에서 설정한 Dto로 객체를 바인딩해서 처리하는 것을 확인할 수 있습니다.
이외에도 다양한 어노테이션이 존재하는데, 다음 포스팅에 또 정리하는 시간을 가지도록 하겠습니다.
읽어주셔서 감사합니다~!
참조: https://frogand.tistory.com/163
'SpringBoot' 카테고리의 다른 글
[SpringBoot] 인터페이스 의존성 주입을 활용한 테스트 코드 작성하기 (0) | 2023.01.03 |
---|---|
[SpringBoot] 인터셉터(Interceptor) (0) | 2022.12.29 |
[SpringBoot] Json API 방식 XSS Filter 적용 후기 (0) | 2022.12.27 |
[SpringBoot] QueryDsl Redis 적용 (2) | 2022.12.26 |
[SpringBoot] @RequestHeader 사용 (0) | 2022.12.25 |