개요
컨트롤러에서 @RequestParam 어노테이션이나 @ModelAttribute 어노테이션을 붙이면 String 자료형이 다른 제네릭 타입으로 자동 형 변환되는 것을 확인할 수 있습니다.
이는 스프링에서 기본으로 제공해주는 TypeConverter 덕분인데 이번 게시글에서는 TypeConverter에 대해 알아보겠습니다.
1. 스프링 타입 컨버터
- HTTP 요청 파라미터는 모두 문자열로 처리
- 따라서, 요청 파라미터를 다른 제너릭 타입으로 변환하기 위해서는 변환하는 과정을 거쳐야 함
- 개요에서도 언급했듯이 @RequestParam, @ModelAttribute, @PathVariable 같은 어노테이션이 해당 과정을 대신해주기도 함
- 그 외 TypeConverter 적용 예시
- @Value 어노테이션을 통해 yml 정보 읽어올 때 (로컬, 개발, 운용 서버 설정이 각각 다르므로 yml 파일을 나누고 빌드할 때 각 서버의 맞는 값을 불러올 때 @Value 사용)
- View 렌더링 할 때
1.1 컨버터 인터페이스
- 스프링은 객체지향 원칙을 철저하게 지키므로 컨버터 인터페이스 또한 확장 가능하도록 제공
- 커스텀 인터페이스는 모든 타입에 적용할 수 있으며 S 타입에서 T 타입으로 변환하는 컨버터와 T 타입에서 S 타입으로 변환하는 컨버터 모두 구현 가능
- 입출력 타입에 제한이 없는 범용적인 타입 변환 기능을 제공
- 커스텀하게 컨버터를 구현하려면 해당 인터페이스를 구현해서 WebConfig에 등록해주면 됨
- WebConfig에 한번 등록한 이후에는 별도로 호출하지 않아도 자동으로 형 변환 지원
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@FunctionalInterface | |
public interface Converter<S, T> { | |
@Nullable | |
T convert(S source); | |
} |
1.2 간단한 예시
- 생년월일을 저장하는 LocalDate 객체를 String 타입으로 변환하는 converter와
- String 타입을 생년월일을 나타내는 LocalDate 객체로 변환하는 converter를 구현
StringToBirthdayConverter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Slf4j | |
public class StringToBirthdayConverter implements Converter<String, LocalDate> { | |
@Override | |
public LocalDate convert(String source) { | |
log.info("birthday: {}", source); | |
String[] numbers = source.split("\\."); | |
int year = Integer.parseInt(numbers[0]); | |
int month = Integer.parseInt(numbers[1]); | |
int day = Integer.parseInt(numbers[2]); | |
return LocalDate.of(year, month, day); | |
} | |
} |
BirthdayToStringConverter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Slf4j | |
public class BirthdayToStringConverter implements Converter<LocalDate, String> { | |
@Override | |
public String convert(LocalDate source) { | |
log.info("{}, {}, {}", source.getYear(), source.getMonthValue(), source.getDayOfMonth()); | |
StringBuilder birthday = new StringBuilder(); | |
birthday.append(source.getYear()) | |
.append(".") | |
.append(String.format("%02d", source.getMonthValue())) | |
.append(".") | |
.append(String.format("%02d", source.getDayOfMonth())); | |
return birthday.toString(); | |
} | |
} |
WebConfig
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Configuration | |
public class WebConfig implements WebMvcConfigurer { | |
@Override | |
public void addFormatters(FormatterRegistry registry) { | |
registry.addConverter(new BirthdayToStringConverter()); | |
registry.addConverter(new StringToBirthdayConverter()); | |
} | |
} |
간단한 테스트 코드
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class ConverterTest { | |
@Test | |
void birthdayToString() { | |
BirthdayToStringConverter converter = new BirthdayToStringConverter(); | |
String birthday = converter.convert(LocalDate.of(2021, 8, 11)); | |
assertThat(birthday).isEqualTo("2021.08.11"); | |
} | |
@Test | |
void stringToBirthday() { | |
StringToBirthdayConverter converter = new StringToBirthdayConverter(); | |
LocalDate birthday = converter.convert("2021.08.11"); | |
assertThat(birthday).isEqualTo(LocalDate.of(2021, 8, 11)); | |
} | |
} |
1.3 ConversionService
- 앞선 테스트 코드처럼 구현한 TypeConverter를 매번 직접 찾아서 형 변환에 사용하는 것은 불편
- 따라서, 스프링은 개별 컨버터들을 모아 두고 그것들을 묶어서 편리하게 사용할 수 있도록 ConverionService를 제공
- ConversionService 인터페이스는 변환이 가능한지 유무 그리고 변환하는 메서드를 제공
- @RequestParam 어노테이션에서 자동으로 형 변환이 진행되는 것은 결국 @RequestParam을 처리하는 ArgumentResolver에서 ConversionService를 사용해서 타입을 변환
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface ConversionService { | |
boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType); | |
boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType); | |
@Nullable | |
<T> T convert(@Nullable Object source, Class<T> targetType); | |
@Nullable | |
Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType); | |
} |
1.4 DefaultConversionService
- DefaultConversionService는 ConversionService를 구현하면서 converter를 등록하는 기능도 제공
- 객체지향 SOLID 법칙(https://jaimemin.tistory.com/1751) 중 인터페이스 분리 원칙에 따라 DefaultConversionService는 컨버터 사용에 초점을 둔 ConversionService와 컨버터 등록에 초점을 둔 ConverterRegistry를 구현
- 등록과 사용을 분리함에 따라 컨버터를 등록할 때는 구체적인 타입 컨버터를 명확하게 알아야 하지만 사용하는 입장에서는 TypeConverter에 대해 몰라도 됨 (훌륭한 객체지향 설계)
- 앞서 WebConfig에서 FormatterRegistry에 컨버터들을 등록했는데 FormatterRegistry를 타고 올라가면 결국 ConverionService를 구현하는 것을 확인할 수 있음
1.5 용도에 따른 다양한 컨버터
- Converter: 기본 타입 컨버터
- ConverterFactory: 전체 클래스 계층 구조가 필요할 때 사용하는 컨버터
- GenericConverter: 정교한 구현, 대상 필드의 어노테이션 정보 사용 가능한 컨버터
- ConditionalGenericConverter: 특정 조건이 참인 경우에만 실행하는 컨버터
- 보다 자세한 내용은 https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#core-convert 참고
2. 포맷터
- WebConfig 코드를 보면 FormatterRegistry에 컨버터를 등록하는 것을 확인할 수 있음
- 이는 곧 컨버터뿐만 아니라 포맷터 또한 WebConfig에 등록할 수 있다는 뜻
- Converter는 입출력 타입에 제한이 없는 범용적인 타입 변환 기능을 제공
- Formatter는 문자에 특화되며 Locale을 사용
- 10000 -> 10,000으로 변환한다던지
- LocalDateTime 객체 값인 2021-08-11 T 21:33:30를 2021.08.11 21:33:30으로 변환하는 것이 포맷터의 기능
- 정리를 하자면, 객체를 특정한 포맷에 맞추어 문자로 출력하거나 또는 그 반대의 역할을 하는 것에 특화된 것이 포맷터
2.1 포맷터 인터페이스
- Formatter는 객체를 문자로 혹은 문자를 객체로 변경하는 두 가지 기능을 모두 수행
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface Formatter<T> extends Printer<T>, Parser<T> { | |
} | |
@FunctionalInterface | |
public interface Printer<T> { | |
String print(T object, Locale locale); | |
} | |
@FunctionalInterface | |
public interface Parser<T> { | |
T parse(String text, Locale locale) throws ParseException; | |
} |
* print 메서드: 객체를 문자로 변경
* parse 메서드: 문자를 객체로 변경
2.2 스프링에서 기본으로 제공하는 포맷터
- 앞서 언급한 예시 또한 기본으로 제공함 (NumberFormatter, DateTimeFormatter 등등)
- 스프링에서 아래 사진처럼 다양한 포맷터를 지원
- 포맷터는 기본 형식이 지정되어있기 때문에 객체의 각 필드마다 다른 형식으로 포맷을 지정하기 어렵기 때문에 스프링은 이런 문제를 해결해줄 어노테이션 기반 형식을 지원
- @NumberFormat: 숫자 관련 형식 지정 포맷터
- @DateTimeFormat: 날짜 관련 형식 지정 포맷터
@Data
public class Example {
@NumberFormat(pattern = "###,###,###")
private Integer digit;
@DateTimeFormat(pattern = "yyyy.MM.dd HH:mm:ss")
private LocalDateTime localDateTime;
}

2.3 FormattingConversionService
- ConversionService에는 converter만 등록 가능하고 formatter는 등록 불가능
- 하지만, 포맷터는 객체에서 문자 혹은 문자에서 객체로 변환시켜주는 컨버터의 일종이므로 formatter를 지원하는 ConversionService를 통해 등록해주면 됨
- FormattingConversionService를 통해 formatter를 등록해주면 되고 DefaultFormattingConversionService는 FormattingConversionService에 기본적인 통화, 숫자 관련 몇 가지 기본 formatter를 추가 제공
- 위에서 언급한 ConversionService는 내부에서 어댑터 패턴을 적용해 formatter가 converter처럼 동작하도록 지원
- 상속 관계를 정리하자면 아래와 같음
- FormattingConversionService는 ConversionService 관련 기능을 상속 받음
- 스프링 부트는 WebConversionService를 내부에서 사용하는데 이는 DefaultFormattingConversionService를 상속 받음
- 따라서, 앞서 converter 예시처럼 WebConfig에서 converter와 함께 formatter를 등록해주면 됨
- 주의할 점은 converter가 formatter보다 우선순위가 높으므로 비슷한 기능을 하는 converter가 있을 경우 formatter가 정상적으로 동작하지 않음
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Configuration | |
public class WebConfig implements WebMvcConfigurer { | |
@Override | |
public void addFormatters(FormatterRegistry registry) { | |
registry.addConverter(new BirthdayToStringConverter()); | |
registry.addConverter(new StringToBirthdayConverter()); | |
// 컨버터가 포맷터보다 우선위가 높음 | |
registry.addFormatter(new CustomFormatter()); | |
} | |
} |
비고
- HttpMessageConverter의 경우 ConversionService 적용 안됨
- 예를 들자면, Jackson 라이브러리를 통해 HTTP의 메시지 바디 내용을 객체로 변환하거나 객체를 HTTP 메시지 바디에 입력할 때 변환 과정은 해당 라이브러리에 의존적
- 커스텀 ConversionService를 구현해서 적용한다고 해도 영향이 없음
- 포맷은 전적으로 Jackson 라이브러리에 의존적
- 앞서 언급했듯이 ConversionService는 @RequestParam, @ModelAttribute, @PathVariable와 같은 어노테이션 혹은 View Template 등에서 사용 가능
출처
인프런 스프링 MVC 2편 (김영한 강사님)
반응형
'Spring' 카테고리의 다른 글
[SpringBoot] ThreadLocal 간단 정리 (0) | 2021.11.13 |
---|---|
[SpringBoot] 파일 업로드 및 다운로드 (2) | 2021.08.14 |
[SpringBoot] API 예외처리 (1) | 2021.08.08 |
[SpringBoot] HTML 예외처리와 예외 페이지 (0) | 2021.08.03 |
[SpringBoot] Filter, Interceptor 개념 정리 및 로그인 처리 (4) | 2021.08.01 |