스프링 mvc - 타입 컨버터

meluu_·2024년 2월 1일
0

스프링

목록 보기
26/27
post-thumbnail

🌿 시작하기 앞서


스프링 부트 3.2.1 버전을 기준으로 작성됨

클라이언트가 값을 보내면 JSON or String 등으로 값이 넘어가는데

@RequestParam 이나 @ModelAttribute , @PathVariable 을 사용할 때 자동으로 타입을 변환해주는 걸로 알고 있다. 그렇다면 어디서 어떻게 변환되어 파라미터로 들어오는 것일까?

결과적으로 말하면 스프링이 중간에 타입을 변환해준다.



🌱 스프링 타입 컨버터


스프링의 타입 변환 적용 예

  • 스프링 MVC 요청 파라미터
  • @RequestParam , @ModelAttribute , @PathVariable
  • @Value 등으로 YML 정보 읽기
  • XML에 넣은 스프링 빈 정보를 변환
  • 뷰를 렌더링 할 때
package org.springframework.core.convert.converter;

// S : 변환할 타입, T : 변환할 타입
public interface Converter<S, T> {
	T convert(S source);
}

스프링은 확장 가능한 컨버터 인터페이스 제공
스프링에 추가적인 타입 변환이 필요하면 이 컨버터를 구현해서 등록하면 된다.

❗ 주의
해당 인터페이스를 사용해야함
org.springframework.core.convert.converter.Converter


✔️ Converter


문자 -> 숫자 컨버터 구현

@Slf4j
public class StringToIntegerConverter implements Converter<String, Integer> {
	
    @Override
	public Integer convert(String source) {
		log.info("convert source={}", source);
		return Integer.valueOf(source);
	}
}

사용자 정의 타입 컨버터


IP, Port를 가지는 Ip 객체

@Getter
@EqualsAndHashCode
public class IpPort {
    private String ip;
    private int port;

    public IpPort(String ip, int port) {
        this.ip = ip;
        this.port = port;
    }
}

Ip/Port 객체 <-> String


	// Iport -> String
    @Override
    public String convert(IpPort source) {
        log.info("convert source={}", source);
        // IpPort 객체 -> "127.0.0.1:8080"
        return source.getIp() + ":" + source.getPort();
    }
    
    // String -> IPort
    @Override
    public IpPort convert(String source) {
        log.info("convert source={}", source);
        // "127.0.0.1:8080"
        String[] split = source.split(":");
        String ip = split[0];
        int port = Integer.parseInt(split[1]);

        return new IpPort(ip, port);
    }

스프링이 제공하는 다양한 방식의 타입 컨버터
Converter : 기본 타입 컨버터
ConverterFactory : 전체 클래스 계층 구조가 필요할 때
GenericConverter : 정교한 구현, 대상 필드의 애노테이션 정보 사용 가능
ConditionalGenericConverter : 특정 조건이 참인 경우에만 실행


✔️ 컨버전 서비스 - ConversionService


타입 컨버터를 직접 찾아서 타입 변환에 사용하는 것은 매우 불편

스프링은 개별 컨버터를 모아두고 그것들을 묶어서 편리하게 사용할 수 있는 기능 제공

ConversionService 인터페이스 (컨버터 사용)

package org.springframework.core.convert;

public interface ConversionService {
	boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);
	boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
	
    <T> T convert(@Nullable Object source, Class<T> targetType);
	Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
}

ConverterRegistry 인터페이스 (등록/관리)

package org.springframework.core.convert.converter;

public interface ConverterRegistry {
    // 컨버터 등록
	void addConverter(Converter<?, ?> converter);

// 생략.. 자세한건 찾아보자
}

사용

public class ConversionServiceTest {

    @Test
    void conversionService() {
        // 등록
        DefaultConversionService conversionService = new DefaultConversionService();
        conversionService.addConverter(new StringToIntegerConverter());
        conversionService.addConverter(new IntegerToStringConverter());
        conversionService.addConverter(new IpPortToStringConverter());
        conversionService.addConverter(new StringToIpPortConverter());

        // 사용 
        assertThat(conversionService.convert("10", Integer.class)).isEqualTo(10);
        assertThat(conversionService.convert(10, String.class)).isEqualTo("10");
        
        IpPort ipPort = conversionService.convert("127.0.0.1:8080", IpPort.class);

        String ipPortString = conversionService.convert(new IpPort("127.0.0.1", 8080), String.class);


    }
}

DefaultConversionService

DefaultConversionService 는 ISP원칙을 지켰다

  • ConversionService : 컨버터 사용에 초점
  • ConverterRegistry : 컨버터 등록에 초점

개발 입장에서는 등록 및 관리하는 ConverterRegistry 만 의존

클라이언트 입장에서는 컨버터를 사용하는 ConversionService 만 의존

컨버터를 사용하는 클라이언트는 꼭 필요한 메서드만 알게된다. -> ISP (인터페이스 분리 원칙)

스프링은 내부에서 CovnersionService를 사용해서 타입 변환함 -> @RequestParam


✔️ 스프링에 Converter 적용하기


웹 애플리케이션에 Converter 적용

등록

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToIntegerConverter());
        registry.addConverter(new IntegerToStringConverter());
        registry.addConverter(new StringToIpPortConverter());
        registry.addConverter(new IpPortToStringConverter());
    }
}

WebMvcConfigurer 가 제공하는 addFormatters() 를 사용해서 추가하고 싶은 컨버터를 등록
-> 스프링 내부에서 사용하는 ConversionService 에 컨버터를 추가

사용

public String helloV2(@RequestParam Integer data) {
	System.out.println("data = " + data);
	return "ok";
}

실행시 로그에는
StringToIntegerConverter : convert source=10 //우리가 만든 컨버터에서 출력한 로그 
data = 10 이 출력된다. 

처리과정

@RequestParam@RequestParam 을 처리하는 ArgumentResolver
RequestParamMethodArgumentResolver 에서 ConversionService 를 사용해서 타입을 변환


✔️ 뷰 템플릿에서 컨버터 적용


뷰 템플릿에 경우 핵심 정리해놓았다.

model에 객체나 각종 데이터를 등록
${{...}} 를 사용하면 컨버전 서비스가 적용된다.

form의 경우

th:field 를 사용하는데 여기에도 컨버전 서비스가 적용됨


✔️ 포맷터 - Formatter


문자에 특화(객체 문자, 문자 객체) + 현지화(Locale)

실상황에서 대부분 문자 <-> 다른 타입 이런 경우가 많기에
문자 즉 String에 특화된 컨버터인 포맷터를 사용해보자

  • 예시
    • 1000 <-> "1,000" 형식이 있는 숫자 단위
    • 날짜 객체 -> 문자 ("2024-02-01 15:30:24) 와 같이 출력 / 반대 상황

Locale

날짜 숫자의 표현 방법은 Locale현지화 정보 사용

Formatter 인터페이스

// T 타입 객체를 문자로 변환
public interface Printer<T> {
	String print(T object, Locale locale);
}

// String -> Locale을 반영하여 T 타입으로 변환
public interface Parser<T> {
	T parse(String text, Locale locale) throws ParseException;
}

public interface Formatter<T> extends Printer<T>, Parser<T> {
}

1000 -> "1,000" 구현

@Slf4j
public class MyNumberFormatter implements Formatter<Number> {
    @Override
    public Number parse(String text, Locale locale) throws ParseException {
        log.info("text={} locale={}", text, locale);

        //"1,000" -> 1000
        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);
    }
}

숫자 중간에 쉼표를 적용 -> NumberFormat 객체 사용
Locale 정보를 활용해서 나라별로 다른 숫자 포맷을 생성

포맷터 인터페이스의 구현체들


✔️ 포맷터를 지원하는 컨버전 서비스


FormattingConversionService 는 포맷터를 지원하는 컨버전 서비스이다.
DefaultFormattingConversionServiceFormattingConversionService에 기본적인 통화, 숫자 관련 몇가지 기본 포맷터를 추가 제공

DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();

//컨버터 등록
conversionService.addConverter(new StringToIpPortConverter());

//포맷터 등록
conversionService.addFormatter(new MyNumberFormatter());

웹 어플리케이션에 적용


@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {

        //String <-> Integer 는 우선순위를 위해 삭제 
        registry.addConverter(new StringToIpPortConverter());
        registry.addConverter(new IpPortToStringConverter());

        // 추가
        registry.addFormatter(new MyNumberFormatter());
    }
}

추가로 스프링부트는 DefaultFormattingConversionService를 상속받은 WebConversionService를 내부에서 사용한다.


✔️ 스프링 제공 기본 포맷터


포맷터는 기본 형식이 지정되어있기에 객체의 각 필드마다 다른 형식 포맷 지정은 어려움

스프링은 이를 해결하기 위해 애노테이션 기반 형식 지정 포맷터를 두 가지 제공

  • @NumberFormat : 숫자 관련 형식 지정 포맷터 사용 , NumberFormatAnnotationFormatterFactory
  • @DateTimeFormat : 날짜 관련 형식 지정 포맷터 사용,
    Jsr310DateTimeFormatAnnotationFormatterFactory
@Controller
public class FormatterController {

    @GetMapping("/formatter/edit")
    public String fromatterForm(Model model) {
        Form form = new Form();
        form.setNumber(10000);
        form.setLocalDateTime(LocalDateTime.now());
        model.addAttribute("form", form);
        return "formatter-form";
    }


    @PostMapping("/formatter/edit")
    public String formatterEdit(@ModelAttribute Form form) {
        return "formatter-view";
    }

    @Data
    static class Form {

        @NumberFormat(pattern = "###,###")
        private Integer number;

        @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        private LocalDateTime localDateTime;

    }

}

form으로 데이터를 보내면

결과

스프링 공식 Formatting 문서


주의


메시지 컨버터 (HttpMessageConverter) 에는 컨버전 서비스가 적용되지 않는다.
객체 -> Json으로 변환할 때 메시지 컨버터는 Jackson 같은 라이브러리를 사용한다. 따라서 해당 라이브러리가 제공하는 설정을 통해 포맷을 지정해야 한다.

컨버전 서비스는 @RequestParam, @ModelAttribute, @PathVariable, 뷰 템플릿 등에서 사용 가능



🔖 학습내용 출처

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술

profile
열심히 살자

0개의 댓글