2024.09.19.목.TIL Type Converter

김기남·2024년 9월 19일
post-thumbnail

안녕하세요, 오늘은 Type Converter 에 대해 정리해 보았습니다.

문자열을 숫자로

💡 문자를 숫자로, 숫자를 문자로 변환하는 등 Web Application을 만들다보면 Type을 변환해야 하는 경우가 많이 발생한다.
  • 코드예시 (HttpServletRequest)
@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. 다른 타입으로 변환해서 사용하고자 한다면 위와같이 변환하는 과정이 필요하다.

  • 코드예시 (@RequestParam)
@Slf4j
@RestController
public class TypeController {

	@GetMapping("/request-param")
	public void example(@RequestParam Integer example) {
		log.info("example = {}", example);
	}

}


1. @RequestParam 을 사용하면 Integer Type으로 바인딩된다.

정리

  1. HTTP Request Parameter로 전달하는 10 값은 실제로는 문자열(String) 10이다.
  2. @RequestParam을 사용하면 문자 10을 Integer Type의 숫자 10으로 변환된다.
  3. @ModelAttribute, @PathVariable 에서도 타입 변환을 확인할 수 있다.

즉, Spring 내부적으로 중간에서 누군가가 Type을 자동으로 변환해주었다.

Converter

💡 개발자가 위의 1번 예시처럼 하나하나 모두 Type 변환을 해야한다면..? Spring이 제공하는 Type Converter를 사용하여 Type을 변환해주어서 편리하게 값을 사용할 수 있었다. ex) 문자 → 숫자, 숫자 → 문자, String → boolean 등등..

하지만, 만약 개발자가 새로운 Type을 만들어서 변환하고 싶다면?

http://localhost:8080/example?name=wonuk&age=100

Person

private String name; = wonuk

private Integer age; = 1200

Spring Converter Interface

  • Spring 이 제공하는 Converter 인터페이스


1. Spring은 Converter Interface를 통해 컨버터를 확장할 수 있도록 만들었다.
2. Converter Interface를 구현(implements)하여 컨버터로 등록하면 된다.
3. Converter는 모든 타입(T)에 적용할 수 있다.

즉, 변환하고자 하는 타입에 맞춰서 Type Converter를 구현하고 등록하면 된다.

Type Converter

💡 데이터 유형 간 변환을 담당하는 메커니즘, 주로 웹 요청 파라미터를 Java 객체로 변환하거나 그 반대로 변환할 때 사용된다. 커스텀 변환 로직도 정의 가능하다.
  • 참고 : Spring Converter와 같은 이름을 가진 Interface가 많으니 주의해야 한다.

  • org.springframework.core.convert.converter

  • 코드예시 (String → Integer)

    • <S, T> 에서 S는 변환할 Source T는 변환할 Type으로 설정하면 된다.
@Slf4j
public class StringToIntegerConverter implements Converter<String, Integer> {
	
	@Override
	public Integer convert(String source) {
		log.info("source = {}", source);
		// 예외처리는 생략 
		
		return Integer.valueOf(source);
	}
}
  1. 파라미터로 들어온 source가 Interger로 변환된다.
  • 코드예시 (Integer → String)
@Slf4j
public class IntegerToStringConverter implements Converter<Integer, String> {
	
	@Override
	public String convert(Integer source) {
		log.info("source = {}", source);

		return String.valueOf(source);
	}
}
  1. 파라미터로 들어온 source가 String으로 변환된다.
  • 코드예시 (String → Person)
    • 요청 예시 "wonuk:1200"
    • localhost:8080/example?value=wonuk:1200
    • RequestBody
    • “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"
	}
	
}

정리

  • Converter Interface는 아주 단순하다.
  • 하지만 이 Converter를 직접 생성하여 사용하면 컨트롤러에서 변환하는 방식과 큰 차이가 없다.
PersonToStringConverter converter = new PersonToStringConverter();
String source = "wonuk:1200";
converter.convert(source);
  • Converter를 편리하게 등록하고 사용할 수 있도록 만들어주는 기능이 필요하다.
  • Spring은 문자, 숫자, Boolean, Enum등 자주 사용되는 타입에 대한 컨버터를 기본제공이 된다.



Spring의 다양한 Type Converter

  1. Converter
    1. 기본적인 변환을 담당하는 인터페이스
    2. 하나의 소스 타입을 대상 타입으로 변환할 수 있다. → Converter<Source, Type>
  2. ConverterFactory
    1. 클래스 계층 구조가 복잡한 경우 사용
    2. 여러 하위 클래스를 가지는 클래스 계층 구조에서 특정 하위 클래스를 변환
  3. GenericConverter
    1. 정교한 변환을 위한 인터페이스
    2. 대상 필드의 어노테이션 정보를 활용 가능
  4. ConditionalGenericConverter
    1. 특정 조건을 만족하는 경우에만 변환을 수행하는데 사용
    2. 예를 들어, 특정 어노테이션이 존재할 때만 변환을 수행하는 등의 조건부 변환에 활용

Conversion Service

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

ConversionService Interface

  • ConversionService 또한 인터페이스로 구성되어 있다.

  1. Convert가 가능한가? 확인하는 기능을 제공한다.
  2. Convert 즉, 변환 기능을 제공한다.

DefaultConversionService

  • ConversionService를 구현한 구현체



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에만 의존하면 된다.
등록과 사용을 구분하면 된다.

인터페이스 분리 원칙 ISP(Interface Segregation Principal)

  • 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다.

  • DefaultConversionServiceConversionRegistryConversionService 모두 구현했다.
  • ConversionRegistry : 컨버터 등록
  • ConversionService : 컨버터 사용
  • 인터페이스를 분리하면 컨버터를 사용하는 Client는 필요한 메서드만 알면된다.
  • ConversionRegistry 가 변경되어도 ConversionService와 연관이 없다.
  • 위 예시가 ISP의 좋은 예
  • Spring은 내부적으로 위와같이 등록, 사용이 분리된 인터페이스들이 아주 많다.

Spring은 내부적으로 ConversionService를 사용해 Type을 변환한다.
@RequestParam 과 같은 곳에서 해당 기능을 사용한다.

Spring Converter 적용

  • 코드예시
@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());
	}
}
  • Spring은 내부적으로 ConversionService를 제공한다.
  • WebMvcConfigurer가 제공하는 addFomatters() 를 사용하여 Converter를 등록하면 된다.
  • 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 모두 적용된다

동작흐름

  • Argument Resolver


1. @RequestParam 을 처리하는 RequestParamMethodArgumentResolver 에서
ConversionService 를 사용하여 타입을 변환한다.

RequestParamMethodArgumentResolver 코드 확인하러 가봅시다!
→ 내부적으로 굉장히 복잡하게 호출되기 떄문에 그냥 느낌만 이해하면 된다.

Thymeleaf

View Template에서도 이러한 Type Converter 기능을 제공한다.

Fomatter

💡 Converter는 입력과 출력 타입에 제한 없이 범용 타입 변환 기능을 제공한다. Formatter는 문자 변환에 특화된 기능이라고 생각하면 된다. 아래 내용을 통해 더 자세히 알아보자.
  • 객체를 특정한 포맷에 맞춰서 문자로 출력하는 기능에 특화된것이 Fomatter이다.
    • Converter보다 조금 더 세부적인 기능이라고 생각하면 된다.

Locale

  • 지역 및 언어 정보를 나타내는 객체.
    • 언어코드 ex) “en”, “ko”
    • 국가코드 ex) “US”, “KR”
    • Variant ex) “WIN”, “MAC”
  • 특정 지역 및 언어에 대한 정보를 제공하여 국제화 및 지역화 기능을 지원한다.
  • 주로 다국어 작업에서 사용된다.
  • 국제화 →
    Web Application은 하나인데 Locale 정보에 따라서 한글을 보여줄지 영문을 보여줄지 선택할 수 있다.

Formatter Interface

  • Printer, Parser를 상속받는다.

  • 객체를 문자로 변환하고 문자를 객체로 변환하는 두가지 기능을 모두 가지고 있다.

  • Printer

    • Object를 String으로 변환한다.

  • Parser

    • String을 Object로 변환한다.

숫자(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);
  }
	
}
  • Number
    • 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");
		}
}

AnnotationFormatterFactory

  • Field의 Type을 활용할 수 있다.
  • Annotation 정보를 활용할 수 있다.

ConversionService

  • Formatter를 지원하는 ConversionService를 사용하면 Formatter를 추가할 수 있다.
    • FormattingConversionService

  • DefaultFormattingConversionService
    • FormattingConversionService + 통화, 숫자관련 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");

   }
}

  • ConversionService가 제공하는 convert()를 사용하면 된다.

참고

Spring Boot는 DefaultFormattingConversionService를 상속한
WebConversionService 를 내부적으로 사용한다.

Spring이 제공하는 Formatter


💡 Spring은 Java에서 기본적으로 제공하는 타입들에 대해 기본적으로 Formatter들을 제공한다. 하지만 기본 제공 Formatter는 기본 형식이 정해져있어서 필드마다 다른 형식으로 지정하기 어렵다. Spring은 어노테이션 기반으로 원하는 형식으로 Formatter를 사용할 수 있도록 기능을 제공한다.

RequestDTO, ResponseDTO Fields 들에 아래 어노테이션들이 들어가면 자동으로 사용할 수 있다.

  1. @NumberFormat

    • 숫자 관련 지정 Formatter 사용
    • NumberFormatAnnotationFormatterFactory
  2. @DateTimeFormat

    • 날짜 관련 지정 Formatter 사용
    • Jsr310DateTimeFormatAnnotationFormatterFactory

    정리

  3. Converter, Formatter 모두 ConversionService를 통해서 일관성 있게 사용할 수 있다.
    → 어댑터 패턴 덕분

  4. HttpMessageConverter에는 ConversionService가 적용되지 않는다.

    1. HttpMessageConverter의 역할은
    2. RequestBody Message를 객체로 변환하거나
    3. 객체를 ResponseBody에 입력하는 것
    4. JSON을 객체로 변환하는데는 Jackson 라이브러리를 사용한다.
  5. 즉, JSON 결과로 만들어지는 숫자, 날짜 등의 포맷을 변경하고 싶을 때는
    Jackson 라이브러리가 제공하는 기능을 사용해야 한다.

  6. 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"));
    }
}

신규 아이디 추천

profile
새로운 시작~!

0개의 댓글