이지은님의 블로그
250221 - Java Spring 심화: Spring MVC의 데이터 변환과 처리 메커니즘(ArgumentResolver, ReturnValueHandler, Converter, Formatter) 본문
250221 - Java Spring 심화: Spring MVC의 데이터 변환과 처리 메커니즘(ArgumentResolver, ReturnValueHandler, Converter, Formatter)
queenriwon3 2025. 2. 22. 02:12▷ 오늘 배운 것
오늘부터 Java Spring 심화 강의를 듣고 배우고 정리한 것을 블로그로 작성해보도록 하겠다.
<< 목차 >>
1. HttpMessageConverter
1) HttpMessageConverter
2) HttpMessageConverter 내부구조
3) 우선 순위
4) HttpMessageConverter 동작 예시
2. ArgumentResolver & ReturnValueHandler
1) RequestMappingHandlerAdapter
2) ArgumentResolver
3) ReturnValueHandler
4) 다시보는 HttpMessageConverter
5) 대표적인 ArgumentResolver, ReturnValueHandler
6) 정리
7) WebMvcConfigurer에서 적용
3. TypeConverter
1) 문자열을 숫자로 convert
2) Converter 인터페이스
3) ConversionService 인터페이스
4) DefaultConversionService
5) WebMvcConfigurer에서 적용
4. Formatter
1) Formatter
2) Formatter Interface
3) FormattingConversionService
4) DefaultFormattingConversionService
1. HttpMessageConverter
1) HttpMessageConverter
클라이언트가 요청하면 @ResponseBody가 있는 경우 HttpMessageConverter가 동작된다. HTTP 응답 메세지 Body에 데이터를 직접 입력 후 반환한다.
HttpMessageConverter가 적용되는 경우
- HTTP 요청 : @RequestBody, HttpEntity<>, RequestEntity<>
- HTTP 응답 : @ResponseBody, HttpEntity<>, ResponseEntity<>
@RestController = @Controller + @ResponseBody
2) HttpMessageConverter 내부구조
1️⃣ canRead(), canWrite()로 class MediaType 지원여부를 체크
2️⃣ read(), write() 메서드로 HttpMessage를 읽고 쓴다.
3) 우선 순위
1️⃣ ByteArrayHttpMessageConverter(byte[] 처리)
: */* -> application/octet-stream
2️⃣ StringHttpMessageConverter(String 처리)
: */* -> text/plain
3️⃣ MappingJackson2HttpMessageConverter(Json 처리)
: application/json -> application/json
4) HttpMessageConverter 동작 예시
HTTP 요청(Request) | HTTP 응답(Response) | ||
1️⃣ Application/json 형태로 요청 받으면 컨트롤러에서 @RequestBody, HttpEntity<>로 바인딩 | 1️⃣ 컨트롤러에서 @Response, HttpEntity<>로 응답 반환 | ||
2️⃣ MessageConverter가 canRead()에서드로 읽기가 가능한지 true/false로 반환 - 클래스 확인(byte[], String, Object) - Media Type 지원 여부 확인 |
2️⃣ MessageConverter가 canWrite() 메서드로 쓰기가 가능한지 true/false로 반환 - 클래스 확인(byte[], String, Object) - 요청헤더 Accept의 Media Type 지원 여부 확인 |
||
3️⃣ read() 메서드를 호출하여 Object 생성 | 3️⃣ write() 메서드를 호출하여 HTTP Response Message Body 데이터 입력 |
* 우선순위에 따라 동작
2. ArgumentResolver & ReturnValueHandler
1) RequestMappingHandlerAdapter
: @RequestMapping을 처리하는 HandlerAdapter의 구현체
(@RequestMapping의 종류: @PostMapping, @GetMapping, @PutMapping, @PatchMapping, @DeleteMapping)
2) ArgumentResolver
: 파라미터를 바인딩 시켜준다. 메서드의 파라미터를 자동으로 바인딩
ArgumentResolver의 종류
1️⃣ RequestBodyArgumentResolver(@RequestBody)
2️⃣ RequestHeaderArgumentResolver(@RequestHeader)
HandlerMethodArgumentResolver 인터페이스
1️⃣ supportsParameter(): 컨트롤러가 필요로하는 메서드의 파라미터를 지원하는지 여부 검사(boolean 리턴)
2️⃣ resolveArgument(): Object로 만들어줌, 만들어진 객체가 Controller 호출시 메서드의 파라미터로 연결됨
3) ReturnValueHandler
: 컨트롤러 메서드가 반환하는 값을 처리하여 HTTP응답에 맞게 변환
ModelAndView, @ResponseBody, HttpEntity<>
ArgumentResolver의 종류
1️⃣ ModelAndViewMethodReturnValueHandler(ModelAndView 객체 반환)
2️⃣ HttpEntityMethodProcessor(HttpEntity 객체 반환)
HandlerMethodReturnValueHandler 인터페이스
4) 다시보는 HttpMessageConverter
요청시에는 Argument Resolver가 사용하고, 응답시에는 ReturnValueHandler가 사용한다.
5) 대표적인 ArgumentResolver, ReturnValueHandler
1️⃣ RequestResponseBodyMethodProcessor와 2️⃣ HttpEntityMethodProcessor는
모두 HandlerMethodArgumentResolver와 HandlerMethodReturnValueHandler을 상속하기 때문에 요청과 응답에대해 모두 처리할 수 있다.
1️⃣ RequestResponseBodyMethodProcessor
: @RequestBody, @ResponseBody 두가지 모두 처리
2️⃣ HttpEntityMethodProcessor
: HttpEntity<> 처리
- HttpEntityMethodProcessor.supportParameter(): 객체 여부 확인
- HttpEntityMethodProcessor.resolveArgument(): 실제 Object로 만들어주는 메서드
6) 정리
ArgumentResolver와 HttpMessageConverter는 다르다.
HTTP 요청(Request) | HTTP 응답(Response) |
1️⃣ @RequestBody, HttpEntity를 처리하는 ArgumentResolver 존재 | 1️⃣ @ResponseBody, HttpEntity를 처리하는 ReturnValueHandler 존재 |
2️⃣ ArgumentResolver들이 HttpMessageConverter를 호출하여 필요한 Object로 변환한다. |
2️⃣ ReturnValueHandler 들이 HttpMessageConverter를 호출하여 필요한 응답을 입력한다. |
7) WebMvcConfigurer에서 적용
: Spring MVC의 설정을 사용자 정의할 수 있도록 제공되는 인터페이스로 implements하여 설정을 확장하거나 커스터마이징할 수 있음.
@Conponent가 포함된 @Configuration을 통해서 Spring Bean을 등록
- HandlerMethodArgumentResolver
- HandlerMethodReturnValueHandler
- HttpMessageConverter
모두 인터페이스로 구현되어 있다.
따라서
HandlerMethodArgumentResolver | ➡️ | addArgumentResolvers() |
HandlerMethodReturnValueHandler | ➡️ | addReturnValueHandlers() |
HttpMessageConverter | ➡️ | extendMessageConverters() |
필요한 메서드를 오버라이딩하여 사용하면 된다.
3. TypeConverter
: Spring에서 객체의 타입을 서로 변환하는 데 사용되는 인터페이스로,
Spring의 데이터 바인딩 과정에서 문자열을 특정 객체로 변환하거나 하나의 객체 타입을 다른 타입으로 변환할 때 사용한다.
1) 문자열을 숫자로 convert
1️⃣ HttpServletRequest - 직접 검증 및 변환
2️⃣ @RequestParam - 추가적인 변환 작업없이 바인딩
3️⃣ @ModelAttribute, @PathVariable에서도 타입 변환 가능
2) Converter 인터페이스
public class StringToIntegerConverter implements Converter<String, Integer> {
// Integer → String 변환
@Override
public Integer convert(String source) {
return Integer.valueOf(source);
}
}
Converter<S, T>인터페이스를 사용하여 S는 변환할 Source T는 변환할 Type으로 설정
3) ConversionService 인터페이스
: Converter를 모아서 편리하게 관리하고 사용할 수 있게 해주는 기능을 제공
1️⃣ boolean canConvert: Convert 가능 여부를 확인하는 기능
2️⃣ convert(): 변환하는 기능
4) DefaultConversionService
: ConversionService를 구현한 구현체
ConvertRegistry에 다양한 Converter를 등록한다.
import static org.assertj.core.api.Assertions.*;
public class ConversionServiceTest {
@Test
void defaultConversionService() {
// given
DefaultConversionService dcs = new DefaultConversionService();
dcs.addConverter(new StringToPersonConverter());
Person wonuk = new Person("wonuk", 100);
// when
Person stringToPerson = dcs.convert("wonuk:1200", Person.class);
// then
assertThat(stringToPerson.getName()).isEqualTo(wonuk.getName());
assertThat(stringToPerson.getAge()).isEqualTo(wonuk.getAge());
}
}
컨버터는 ConversionService 내부에서 숨겨진채 제공된다.
즉, 클라이언트는 ConversionService 인터페이스만 의존하면 된다.
(컨버터 등록과 사용의 분리, 인터페이스 분리 원칙(ISP))
DefaultConversionService에는
1️⃣ ConversionRegistry로 컨버터를 등록할 수 있으며
2️⃣ ConversionService로 컨버터를 사용할 수 있다. (@RequestParam , @PathVariable, @ModelAttribute)
—> 인터페이스를 분리하면 컨버터를 사용하는 클라이언트는 필요한 메서드만 알면된다.(인터페이스 분리 원칙, ISP)
5) WebMvcConfigurer에서 적용
WebMvcConfigurer에는 ConversionService가 인터페이스로 구현되어 있다.
따라서
ConversionService | ➡️ | addFomatters() |
필요한 메서드를 오버라이딩하여 사용하면 된다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFomatters(FormatterRegistry registry) {
// registry.addConverter(new "${등록할 Converter}");
registry.addConverter(new StringToPersonConverter());
registry.addConverter(new PersonToStringConverter());
}
}
* Spring이 기본적으로 제공하는 컨버터들이 존재하지만, 개발자가 커스텀하여 추가한 컨버터가 기본 제공된 컨버터보다 높은 우선순위를 가진다.
4. Formatter
1) Formatter
: 주로 사용자 지정 포맷을 적용해 데이터 변환을 처리할 때 사용.
Formatter는 문자열을 객체로 변환하거나 객체를 문자열로 변환하는 과정에서 포맷팅을 세밀하게 제어할 수 있다.
Lacale: 지역 및 언어정보를 나타내는 객체 (국제화 및 지역화)
2) Formatter Interface
1️⃣ Printer: Object -> String 변환
2️⃣ Parser: String -> Object 변환
public class PriceFormatter implements Formatter<Number> {
@Override
public Number parse(String text, Locale locale) throws ParseException {
NumberFormat format = NumberFormat.getInstance(locale);
return format.parse(text); // // "10,000" -> 10000L
}
@Override
public String print(Number object, Locale locale) {
return NumberFormat.getInstance(locale).format(object); // 10000L -> "10,000"
}
}
테스트 코드를 사용한 확인
class PriceFormatterTest {
PriceFormatter formatter = new PriceFormatter();
@Test
void parse() throws ParseException {
// given, when
Number result = formatter.parse("1,000", Locale.KOREA);
// then
// parse 결과는 Long
Assertions.assertThat(result).isEqualTo(1000L);
}
@Test
void print() {
// given, when
String result = formatter.print(1000, Locale.KOREA);
// then
Assertions.assertThat(result).isEqualTo("1,000");
}
}
3) FormattingConversionService
: ConversionService와 Formatter를 결합한 구현체,
타입 변환과 포맷팅이 필요한 모든 작업을 한 곳에서 수행할 수 있도록 설계되어 있어서 다양한 타입의 변환과 포맷팅 적용 가능
4) DefaultFormattingConversionService
: FormattingConversionService의 구현체
addConverter()로 컨버터와 포매터 모두 등록할 수 있다.
ConversionService가 제공하는 convert()를 사용한다.
public class FormattingConversionServiceTest {
@Test
void formattingConversionService() {
// given
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
// Converter 등록
conversionService.addConverter(new StringToPersonConverter());
conversionService.addConverter(new PersonToStringConverter());
// Formatter 등록
conversionService.addFormatter(new PriceFormatter());
// when
String result = conversionService.convert(10000, String.class);
// then
Assertions.assertThat(result).isEqualTo("10,000");
}
}
5) WebConversionService
: SpringBoot는 기본적으로 DefaultFormattingConversionService를 상속받은 WebConversionService를 사용한다.
6) Spring이 사용하는 formatter
1️⃣ @NumberFormat - 숫자 관련지정 Formatter사용
2️⃣ @DateTimeFormat - 날짜 관련 지정 Formatter 사용
3️⃣ json형태로 요청받을 경우 @JsonFormat 나 커스텀 Deserializer를 사용
// 커스텀 Deserializer사용
@Data
public class JsonFormatDto {
@JsonDeserialize(using = CurrencyDeserializer.class)
private BigDecimal price;
@JsonDeserialize(using = LocalDateDeserializer.class)
private LocalDate orderDate;
}
public class CurrencyDeserializer extends JsonDeserializer<BigDecimal> {
@Override
public BigDecimal deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
String value = p.getText().replaceAll("[^\\d.]", ""); // 숫자와 소수점만 남기기
return new BigDecimal(value);
}
}
public class LocalDateDeserializer extends JsonDeserializer<LocalDate> {
@Override
public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
return LocalDate.parse(p.getText(), DateTimeFormatter.ofPattern("dd-MM-yyyy"));
}
}
// @JsonFormat 사용
@Data
public class JsonFormatDtoV2 {
@JsonDeserialize(using = CurrencyDeserializer.class)
private BigDecimal price;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy")
private LocalDate orderDate;
}