
안녕하세요, 오늘은 Type Converter 에 대해 정리해 보았습니다.
💡 문자를 숫자로, 숫자를 문자로 변환하는 등 Web Application을 만들다보면 Type을 변환해야 하는 경우가 많이 발생한다.문자열을 숫자로
@Slf4j
@RestController
public class TypeController {
@GetMapping("/param")
public void example(HttpServletRequest request) {
String stringExample = request.getParameter("example"); // 조회시 : String
Integer integerExample = Integer.valueOf(stringExample); // Integer로 Type 변환
log.info("intExample = {}", integerExample);
}
}

1. HTTP Request Paramter는 String으로 처리된다.
2. 다른 타입으로 변환해서 사용하고자 한다면 위와같이 변환하는 과정이 필요하다.
@Slf4j
@RestController
public class TypeController {
@GetMapping("/request-param")
public void example(@RequestParam Integer example) {
log.info("example = {}", example);
}
}

1. @RequestParam 을 사용하면 Integer Type으로 바인딩된다.
String) 10이다.@RequestParam을 사용하면 문자 10을 Integer Type의 숫자 10으로 변환된다.@ModelAttribute, @PathVariable 에서도 타입 변환을 확인할 수 있다.즉, Spring 내부적으로 중간에서 누군가가 Type을 자동으로 변환해주었다.
💡 개발자가 위의 1번 예시처럼 하나하나 모두 Type 변환을 해야한다면..? Spring이 제공하는 Type Converter를 사용하여 Type을 변환해주어서 편리하게 값을 사용할 수 있었다. ex) 문자 → 숫자, 숫자 → 문자, String → boolean 등등..Converter
하지만, 만약 개발자가 새로운 Type을 만들어서 변환하고 싶다면?
http://localhost:8080/example?name=wonuk&age=100
Person
private String name; = wonuk
private Integer age; = 1200

1. Spring은 Converter Interface를 통해 컨버터를 확장할 수 있도록 만들었다.
2. Converter Interface를 구현(implements)하여 컨버터로 등록하면 된다.
3. Converter는 모든 타입(T)에 적용할 수 있다.
즉, 변환하고자 하는 타입에 맞춰서 Type Converter를 구현하고 등록하면 된다.
💡 데이터 유형 간 변환을 담당하는 메커니즘, 주로 웹 요청 파라미터를 Java 객체로 변환하거나 그 반대로 변환할 때 사용된다. 커스텀 변환 로직도 정의 가능하다.Type Converter

org.springframework.core.convert.converter
코드예시 (String → Integer)
@Slf4j
public class StringToIntegerConverter implements Converter<String, Integer> {
@Override
public Integer convert(String source) {
log.info("source = {}", source);
// 예외처리는 생략
return Integer.valueOf(source);
}
}
@Slf4j
public class IntegerToStringConverter implements Converter<Integer, String> {
@Override
public String convert(Integer source) {
log.info("source = {}", source);
return String.valueOf(source);
}
}
"wonuk:1200"// Person 객체
@Getter
public class Person {
// 이름
private String name;
// 나이
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
// String -> Person 컨버터
public class StringToPersonConverter implements Converter<String, Person> {
// source = "wonuk:1200"
@Override
public Person convert(String source) {
String[] parts = source.split(":"); // : 를 구분자로 나누어 배열로 만든다.
String name = parts[0]; // 첫번째 배열은 이름이다. -> wonuk
int months = Integer.parseInt(parts[1]); // 두번째 배열은 개월수이다. -> 1200
// 1200 / 12 = 나이 구하는 로직
// 개월수 나누기 12로 나이를 구하는 로직, 현재는 12개월 단위만 고려한다.
int result = 100; // 100의 하드코딩이 아니라, 위 로직을 통해서 구해진값
int age = result; // 나이
return new Person(name, age); // Person으로 만들어 반환
}
}
// Person -> String 컨버터
public class PersonToStringConverter implements Converter<Person, String> {
@Override
public String convert(Person source) {
String name = source.getName(); // Person 이름
int months = source.getAge * 12; // 나이 * 12 = 개월수, 12개월 단위만 고려한다.
return name + ":" + months; // "wonuk:1200"
}
}
PersonToStringConverter converter = new PersonToStringConverter();
String source = "wonuk:1200";
converter.convert(source);



Converter Converter<Source, Type>ConverterFactoryGenericConverterConditionalGenericConverter 💡 Spring은 **Converter를 모아서 편리하게 관리하고 사용할 수 있게 해주는 기능을 제공한다.** 이것이 Conversion Service 이다.Conversion Service



ConverterRegistry
Converter를 추가하는 기능을 제공한다.
사용예시
Conversion Service가 Converter를 찾는다.public class ConversionServiceTest {
@Test
void defaultConversionService() {
DefaultConversionService dcs = new DefaultConversionService();
conversionService.addConverter(new StringToPersonConverter()); // 우리가 만든 컨버터
Person person = conversionService("wonuk:1200", Person.class);
assertThat(person).isEqualTo(new Person("wonuk", 100));
}
}
Class <S, T>
T convert(S source)
Converter를 사용할 때는 Type Converter를 몰라도된다.
Tpye Converter들은 Conversion Service 내부에서 숨겨진채 제공된다.
→ 반환 타입, 파라미터 타입, 제네릭 등으로 Conversion Service가 Converter를 찾는다.
즉, 사용자 입장에서는 ConversionService Interface에만 의존하면 된다.
등록과 사용을 구분하면 된다.

DefaultConversionService는 ConversionRegistry와 ConversionService 모두 구현했다.ConversionRegistry : 컨버터 등록ConversionService : 컨버터 사용Spring은 내부적으로 ConversionService를 사용해 Type을 변환한다.
@RequestParam과 같은 곳에서 해당 기능을 사용한다.
@Configuration // @Component + @Target(Type) + Runtime
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFomatters(FormatterRegistry registry) {
// registry.addConverter(new "${등록할 Converter}");
registry.addConverter(new StringToPersonConverter());
registry.addConverter(new PersonToStringConverter());
}
}
ConversionService를 제공한다.WebMvcConfigurer가 제공하는 addFomatters() 를 사용하여 Converter를 등록하면 된다.
StringToIntegerConverter와 같이 Spring이 기본적으로 제공하는 컨버터들이 존재하지만,
개발자가StringToIntegerConverter를 따로 커스텀하게 구현하여 컨버터로 추가하면
추가한 컨버터가 기본 제공된 컨버터보다 높은 우선순위를 가진다. 즉, 커스텀이 가능하다.
GET localhost:8080/test?person=wonuk:1200@Slf4j
@RestController
public class TypeController {
@GetMapping("/test")
public void example(@RequestParam Person person) {
log.info("person name = {}", person.getName());
log.info("person age = {}", person.getAge());
}
}
Postman

출력 결과

정상적으로 타입이 변환되어 출력되는것을 확인할 수 있다.
QueryString이 Person 객체 타입으로 변환되었다.
@RequestParam 뿐만이 아니라 @PathVariable, @ModelAttribute 모두 적용된다

1. @RequestParam 을 처리하는 RequestParamMethodArgumentResolver 에서
ConversionService 를 사용하여 타입을 변환한다.
→ RequestParamMethodArgumentResolver 코드 확인하러 가봅시다!
→ 내부적으로 굉장히 복잡하게 호출되기 떄문에 그냥 느낌만 이해하면 된다.
View Template에서도 이러한 Type Converter 기능을 제공한다.
💡 Converter는 입력과 출력 타입에 제한 없이 범용 타입 변환 기능을 제공한다. Formatter는 문자 변환에 특화된 기능이라고 생각하면 된다. 아래 내용을 통해 더 자세히 알아보자.Fomatter
“en”, “ko”“US”, “KR”“WIN”, “MAC”Printer, Parser를 상속받는다.
객체를 문자로 변환하고 문자를 객체로 변환하는 두가지 기능을 모두 가지고 있다.
Printer
Object를 String으로 변환한다.

Parser

숫자(10000)를 금액 형태(10,000)로 변환하는 Formatter 예시
@Slf4j
public class PriceFormatter implements Formatter<Number> {
@Override
public Number parse(String text, Locale locale) throws ParseException {
log.info("text = {}, locale={}", text, locale);
// 변환 로직
// 기본 제공되는 기능 숫자 중간에 (,) 쉼표를 넣어준다.
// Locale 정보를 활용하여 나라별로 다른 숫자 포맷으로 만들어준다.
NumberFormat format = NumberFormat.getInstance(locale);
return format.parse(text);
}
@Override
public String print(Number object, Locale locale) {
log.info("object = {}, locale = {}", object, locale);
return NumberFormat.getInstance(locale).format(object);
}
}
Integer, Long, Double 등등 다 Number class의 자식 클래스이다.

테스트
class PriceFormatterTest {
PriceFormatter formatter = new PriceFormatterTest();
@Test
void parse() throws ParseException {
Number result = formatter.parse("1,000", Locale.KOREA);
Assertions.assertThat(result).isEqualTo(1000L); // parse 결과는 Long이다.
}
@Test
void print() {
String result = formatter.print(1000, Locale.KOREA);
Assertions.assertThat(result).isEqualTo("1,000");
}
}


FormattingConversionService

DefaultFormattingConversionServiceFormattingConversionService + 통화, 숫자관련 Formatter를 추가한것

내부적으로 어댑터 패턴을 사용하여 Formatter가 Converter처럼 동작하도록 만들어준다.
Test
public class FormattingConversionServiceTest {
@Test
void formattingConversionService() {
DefaultFormattingConversionService conversionService =
new DefaultFormattingConversionService();
// 컨버터 등록
conversionService.addConverter(new StringToPersonConverter());
conversionService.addConverter(new PersonToStringConverter());
// 포맷터 등록
conversionService.addFormatter(new MyNumberFormatter());
// 컨버터도 사용가능
// 포맷터 사용
Assertions.assertThat(conversionService.convert(10000, String.class)).isEqualTo("10,000");
}
}
convert()를 사용하면 된다.Spring Boot는 DefaultFormattingConversionService를 상속한
WebConversionService 를 내부적으로 사용한다.


RequestDTO, ResponseDTO Fields 들에 아래 어노테이션들이 들어가면 자동으로 사용할 수 있다.
@NumberFormat
NumberFormatAnnotationFormatterFactory@DateTimeFormat
Jsr310DateTimeFormatAnnotationFormatterFactoryConverter, Formatter 모두 ConversionService를 통해서 일관성 있게 사용할 수 있다.
→ 어댑터 패턴 덕분
HttpMessageConverter에는 ConversionService가 적용되지 않는다.
즉, JSON 결과로 만들어지는 숫자, 날짜 등의 포맷을 변경하고 싶을 때는
Jackson 라이브러리가 제공하는 기능을 사용해야 한다.
Conversion Service는 대표적으로
@RequestParam, @ModelAttribute, @PathVariable 등에서 사용한다.
Converter, Fommatter를 직접 구현하고, 등록해서 사용할 수 있다.
오늘의 코드카타
class Solution {
public String solution(String new_id) {
String answer = new_id.toLowerCase();
answer = answer.replaceAll("[^a-z0-9-_.]", "");
while (answer.contains("..")) {
answer = answer.replace("..", ".");
}
if (answer.charAt(0) == '.') {
answer = answer.substring(1);
}
if (answer.length() > 0 && answer.charAt(answer.length() - 1) == '.') {
answer = answer.substring(0, answer.length() - 1);
}
if (answer.length() == 0) {
answer = "a";
}
if (answer.length() > 15) {
answer = answer.substring(0, 15);
if (answer.charAt(answer.length() - 1) == '.') {
answer = answer.substring(0, answer.length() - 1);
}
}
if (answer.length() == 1) {
answer += String.valueOf(answer.charAt(0)) + String.valueOf(answer.charAt(0));
} else if (answer.length() == 2) {
answer += String.valueOf(answer.charAt(1));
}
return answer;
}
}
public class Main {
public static void main(String[] args) {
Solution solution = new Solution();
System.out.println(solution.solution("...!@BaT#*..y.abcdefghijklm"));
System.out.println(solution.solution("z-+.^."));
System.out.println(solution.solution("=.="));
System.out.println(solution.solution("123_.def"));
System.out.println(solution.solution("abcdefghijklmn.p"));
}
}
신규 아이디 추천