본문 바로가기
Spring MVC

[MVC] 스프링 타입 컨버터(1)

by 개발 Blog 2025. 10. 26.

공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.

 

소개

웹 애플리케이션 개발을 하다 보면 문자를 숫자로, 숫자를 문자로 변환하는 등 타입 변환이 자주 필요하다. 스프링은 이런 변환 과정을 자동으로 처리해 주는 타입 컨버터 기능을 제공한다.

 

1. 기본 예시: 문자 → 숫자 변환

@GetMapping("/hello-v1")
public String helloV1(HttpServletRequest request) {
    String data = request.getParameter("data"); // 문자 타입
    Integer intValue = Integer.valueOf(data);   // 숫자 타입으로 변환
    return "ok";
}
  • 요청: http://localhost:8080/hello-v1?data=10
  • data는 문자열 "10"으로 들어온다.
  • 개발자가 직접 Integer.valueOf()를 사용해 변환해야 한다.

2. 스프링 제공 기능 활용

@GetMapping("/hello-v2")
public String helloV2(@RequestParam Integer data) {
    System.out.println("data = " + data);
    return "ok";
}
  • 요청: http://localhost:8080/hello-v2?data=10
  • 스프링이 자동으로 "10" → Integer 10 변환을 해준다.
  • 개발자는 타입 변환 과정을 신경 쓸 필요가 없다.

3. 타입 변환이 적용되는 주요 위치

  • @RequestParam: 쿼리 파라미터 → 자바 타입
  • @ModelAttribute: 요청 파라미터 → DTO 필드 값
  • @PathVariable: URL 경로 → 자바 타입
    예: /users/10 → "10"을 Integer 10으로 변환
  • @Value: YML, Properties 값 → 자바 타입
  • 스프링 빈 설정, 뷰 렌더링 시점

4. 스프링 타입 변환기의 역할

  • 문자열 "10" → 숫자 10
  • 숫자 1 → 문자열 "1"
  • 문자열 "true" → Boolean true
  • 필요시 개발자가 직접 만든 타입으로도 변환 가능

5. 컨버터 인터페이스

스프링은 타입 변환 확장을 위해 Converter 인터페이스를 제공한다.

 
public interface Converter<S, T> { T convert(S source); }
  • S: 변환할 소스 타입
  • T: 변환 결과 타입
  • 개발자가 직접 구현 후 스프링에 등록하면 커스텀 타입 변환 가능

  • "true" → Boolean 변환기
  • Boolean → "true" 변환기

6. 참고

  • 과거에는 PropertyEditor를 사용했지만, 동시성 문제와 불편함이 있었다.
  • 현재는 Converter를 통해 확장성과 안정성을 보장한다.

타입 컨버터 - Converter

1. Converter 인터페이스

스프링은 타입 변환을 위해 Converter<S, T> 인터페이스를 제공한다.

package org.springframework.core.convert.converter;
public interface Converter<S, T> {
    T convert(S source);
}

개발자가 필요한 경우 이를 구현하여 문자 → 숫자, 숫자 → 문자, 객체 변환 등을 처리할 수 있다.

 

2. 기본 타입 변환기 구현

String → Integer

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

 

Integer → String

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

 

3. 사용자 정의 타입 컨버터

IpPort 클래스

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

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

 

IpPort → String

@Slf4j
public class IpPortToStringConverter implements Converter<IpPort, String> {
    @Override
    public String convert(IpPort source) {
        log.info("convert source={}", source);
        return source.getIp() + ":" + source.getPort();
    }
}

 

4. 테스트 코드

@Test
void stringToInteger() {
    StringToIntegerConverter converter = new StringToIntegerConverter();
    Integer result = converter.convert("10");
    assertThat(result).isEqualTo(10);
}

@Test
void integerToString() {
    IntegerToStringConverter converter = new IntegerToStringConverter();
    String result = converter.convert(10);
    assertThat(result).isEqualTo("10");
}

@Test
void stringToIpPort() {
    StringToIpPortConverter converter = new StringToIpPortConverter();
    IpPort result = converter.convert("127.0.0.1:8080");
    assertThat(result).isEqualTo(new IpPort("127.0.0.1", 8080));
}

@Test
void ipPortToString() {
    IpPortToStringConverter converter = new IpPortToStringConverter();
    String result = converter.convert(new IpPort("127.0.0.1", 8080));
    assertThat(result).isEqualTo("127.0.0.1:8080");
}

 

5. 정리

  • Converter 인터페이스를 구현하면 원하는 타입 변환 로직을 손쉽게 추가할 수 있다.
  • 기본적으로 문자 ↔ 숫자, 문자 ↔ 불린, Enum 등 많은 컨버터는 스프링이 이미 제공한다.
  • 개발자는 필요한 경우 IpPort 같은 도메인 객체 변환기를 직접 구현할 수 있다.
  • 다만 이렇게 개별 컨버터를 직접 호출하면 일반적인 변환과 크게 다르지 않으므로, 이후에는 컨버터를 등록하고 관리하는 ConversionService 개념이 필요하다.

컨버전 서비스 - ConversionService

스프링에서 타입 변환은 매우 자주 발생한다. 예를 들어 @RequestParam이나 @ModelAttribute를 통해 문자열을 숫자나 객체로 바꿀 때마다 타입 변환이 일어난다. 하지만 매번 직접 Converter를 찾아서 사용하기는 불편하다. 이 문제를 해결하기 위해 스프링은 여러 컨버터를 묶어서 통합적으로 관리하는 컨버전 서비스(ConversionService) 를 제공한다.

 

ConversionService 인터페이스

ConversionService는 스프링 타입 변환의 핵심 인터페이스이다.
다음은 스프링 프레임워크에서 제공하는 ConversionService 인터페이스이다.

package org.springframework.core.convert;

import org.springframework.lang.Nullable;

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);
}
  • canConvert(): 변환이 가능한지 여부를 확인한다.
  • convert(): 실제 타입 변환을 수행한다.

ConversionService 사용 예시

DefaultConversionService는 ConversionService를 구현한 기본 클래스이며,
컨버터를 등록할 수 있는 기능도 제공한다.

package hello.typeconverter.converter;

import hello.typeconverter.type.IpPort;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.support.DefaultConversionService;

import static org.assertj.core.api.Assertions.*;

public class ConversionServiceTest {

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

        // 사용
        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);
        assertThat(ipPort).isEqualTo(new IpPort("127.0.0.1", 8080));

        String ipPortString = conversionService.convert(new IpPort("127.0.0.1", 8080), String.class);
        assertThat(ipPortString).isEqualTo("127.0.0.1:8080");
    }
}
  • DefaultConversionService는 컨버터를 등록하고, 필요할 때마다 convert() 메서드로 변환을 수행한다.

등록과 사용의 분리

컨버터를 등록하는 쪽에서는 StringToIntegerConverter, IpPortToStringConverter 등 어떤 변환을 등록하는지 명확히 알아야 한다.
하지만 사용하는 쪽에서는 타입만 지정하면 되며, 내부에서 어떤 컨버터가 동작하는지는 알 필요가 없다.

즉, 컨버터 등록과 사용을 분리할 수 있다. 컨버전 서비스는 내부적으로 모든 컨버터를 관리하며, 사용자는 오직 ConversionService 인터페이스만 의존하면 된다.

Integer value = conversionService.convert("10", Integer.class);

 

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

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

DefaultConversionService는 두 개의 인터페이스를 구현한다.

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

이렇게 인터페이스를 분리하면 다음과 같은 장점이 있다.

  1. 컨버터를 사용하는 클라이언트는 ConversionService만 의존한다.
  2. 컨버터 등록/관리와 사용의 관심사를 분리할 수 있다.
  3. 불필요한 메서드 의존이 사라져 유지보수가 편해진다.

스프링 내부에서의 사용

스프링은 내부적으로 ConversionService를 광범위하게 사용한다. 예를 들어 @RequestParam, @ModelAttribute, @Value 등에서 문자열을 다른 타입으로 변환할 때 모두 ConversionService를 이용한다. 컨버전 서비스를 직접 설정하고 빈으로 등록하면 스프링 전반의 타입 변환을 통합적으로 관리할 수 있다.

 

정리

  • ConversionService는 스프링의 통합 타입 변환 기능을 담당한다.
  • DefaultConversionService를 사용하면 컨버터를 등록하고 변환 기능을 사용할 수 있다.
  • 등록과 사용이 분리되어 의존성이 줄어든다.
  • ISP 원칙을 적용해 ConversionService(사용)와 ConverterRegistry(등록)가 분리되어 있다.
  • 스프링 내부의 다양한 기능(@RequestParam, @Value 등)에서도 자동으로 활용된다.

스프링에 Converter 적용하기

스프링은 내부적으로 ConversionService를 가지고 있으며, 애플리케이션에 커스텀 Converter를 등록하면 요청 파라미터, 바인딩, 설정 주입 등 다양한 지점에서 자동으로 타입 변환을 수행한다. WebMvcConfigurer가 제공하는 addFormatters(FormatterRegistry)를 통해 Converter를 등록하면 된다.

 

WebConfig에서 컨버터 등록

아래와 같이 WebMvcConfigurer를 구현하고 addFormatters에서 커스텀 컨버터를 등록한다. 이 시점에 스프링 내부 ConversionService에 컨버터가 합쳐져 전역으로 적용된다.

package hello.typeconverter;

import hello.typeconverter.converter.IntegerToStringConverter;
import hello.typeconverter.converter.IpPortToStringConverter;
import hello.typeconverter.converter.StringToIntegerConverter;
import hello.typeconverter.converter.StringToIpPortConverter;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@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());
    }
}

 

동작 확인: 기본 타입 변환

컨트롤러에서 @RequestParam Integer와 같이 래퍼 타입을 선언하면, 문자열 쿼리 파라미터가 정수로 변환된다.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

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

실행 예시
GET http://localhost:8080/hello-v2?data=10

 

로그 예시
StringToIntegerConverter : convert source=10
data = 10

 

스프링은 기본적으로 많은 내장 컨버터를 제공하므로 커스텀 등록 없이도 문자열→정수 변환이 동작한다. 다만 동일 경로를 커스텀 컨버터로 등록하면 커스텀 컨버터가 우선 적용된다.

 

동작 확인: 커스텀 타입 변환(IpPort)

문자열을 IpPort와 같은 도메인 타입으로 변환하도록 커스텀 컨버터를 등록했다면, @RequestParam에 객체 타입을 선언하는 것만으로 자동 변환이 일어난다.

import hello.typeconverter.type.IpPort;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/ip-port")
    public String ipPort(@RequestParam IpPort ipPort) {
        System.out.println("ipPort IP = " + ipPort.getIp());
        System.out.println("ipPort PORT = " + ipPort.getPort());
        return "ok";
    }
}

실행 예시
GET http://localhost:8080/ip-port?ipPort=127.0.0.1:8080

 

로그 예시
StringToIpPortConverter : convert source=127.0.0.1:8080
ipPort IP = 127.0.0.1
ipPort PORT = 8080

 

내부 처리 흐름 요약

요청 파라미터 바인딩은 RequestParamMethodArgumentResolver가 담당한다. 이 리졸버가 ConversionService를 통해 문자열을 대상 타입으로 변환한다. 내부적으로 여러 단계의 호출과 위임이 이루어지지만, 개발자는 컨버터만 올바르게 등록하면 된다. 동작을 더 자세히 보고 싶으면 커스텀 컨버터의 convert에 브레이크포인트를 걸어 호출 스택을 추적하면 된다

 

정리

WebMvcConfigurer의 addFormatters로 커스텀 컨버터를 전역 등록하면, 스프링의 ConversionService가 컨트롤러 파라미터 바인딩을 포함한 전 영역에서 자동으로 타입 변환을 수행한다.

 

뷰 템플릿에 컨버터 적용하기

이번에는 스프링 MVC의 뷰 템플릿(Thymeleaf)에 Converter를 적용하는 방법을 정리한다.
이전까지는 문자 → 객체 변환이었다면, 이번에는 객체 → 문자 변환을 다룬다.

 

1) ConverterController 구현

컨트롤러에서 Model에 데이터를 담아 뷰로 전달한다.

package hello.typeconverter.controller;

import hello.typeconverter.type.IpPort;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ConverterController {

    @GetMapping("/converter-view")
    public String converterView(Model model) {
        model.addAttribute("number", 10000);
        model.addAttribute("ipPort", new IpPort("127.0.0.1", 8080));
        return "converter-view";
    }
}
  • number: 정수형 데이터
  • ipPort: IpPort 객체
    → 뷰 템플릿에서 이 객체를 문자열 형태로 렌더링 할 때 컨버터가 동작한다.

2) 뷰 템플릿 (converter-view.html)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<ul>
    <li>${number}: <span th:text="${number}"></span></li>
    <li>${{number}}: <span th:text="${{number}}"></span></li>
    <li>${ipPort}: <span th:text="${ipPort}"></span></li>
    <li>${{ipPort}}: <span th:text="${{ipPort}}"></span></li>
</ul>
</body>
</html>
  • ${...} : 단순한 변수 표현식 (그대로 출력)
  • ${{...}} : 스프링의 ConversionService를 통해 타입 변환 후 출력

${{...}} 구문은 스프링이 제공하는 컨버전 서비스를 자동으로 활용한다.
따라서 IpPortToStringConverter, IntegerToStringConverter 같은 커스텀 컨버터가 자동으로 적용된다.

 

3) 실행 결과

실행 URL
http://localhost:8080/converter-view

 

출력 결과

${number}: 10000
${{number}}: 10000
${ipPort}: hello.typeconverter.type.IpPort@59cb0946
${{ipPort}}: 127.0.0.1:8080

 

로그 출력

IntegerToStringConverter : convert source=10000
IpPortToStringConverter : convert source=hello.typeconverter.type.IpPort@59cb0946
  • ${{number}}: Integer → String 변환
    하지만 숫자는 기본적으로 문자열로 변환되므로 큰 차이는 없다.
  • ${{ipPort}}: IpPort → String 변환
    IpPortToStringConverter가 실행되어 127.0.0.1:8080로 출력된다.

4) 폼에 Converter 적용하기

이번에는 입력 폼에서 Converter를 적용한다.

package hello.typeconverter.controller;

import hello.typeconverter.type.IpPort;
import lombok.Data;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class ConverterController {

    @GetMapping("/converter-view")
    public String converterView(Model model) {
        model.addAttribute("number", 10000);
        model.addAttribute("ipPort", new IpPort("127.0.0.1", 8080));
        return "converter-view";
    }

    @GetMapping("/converter/edit")
    public String converterForm(Model model) {
        IpPort ipPort = new IpPort("127.0.0.1", 8080);
        Form form = new Form(ipPort);
        model.addAttribute("form", form);
        return "converter-form";
    }

    @PostMapping("/converter/edit")
    public String converterEdit(@ModelAttribute Form form, Model model) {
        IpPort ipPort = form.getIpPort();
        model.addAttribute("ipPort", ipPort);
        return "converter-view";
    }

    @Data
    static class Form {
        private IpPort ipPort;

        public Form(IpPort ipPort) {
            this.ipPort = ipPort;
        }
    }
}
  • GET /converter/edit: IpPort 객체를 폼에 출력
  • POST /converter/edit: 폼 입력 데이터를 IpPort 객체로 변환하여 처리

5) 뷰 템플릿 (converter-form.html)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form th:object="${form}" th:method="post">
    th:field <input type="text" th:field="*{ipPort}"><br/>
    th:value <input type="text" th:value="*{ipPort}">(보여주기 용도)<br/>
    <input type="submit"/>
</form>
</body>
</html>

th:field는 id, name 생성뿐 아니라 ConversionService도 함께 적용된다.
따라서 IpPort 객체는 자동으로 문자열로 변환되어 폼에 표시된다.

 

6) 실행 흐름 요약

  • GET /converter/edit
    th:field가 자동으로 ConversionService를 적용 → IpPort → String 변환
  • POST /converter/edit
    @ModelAttribute가 ConversionService를 이용해 String → IpPort로 변환

즉, 하나의 컨버터를 등록해 두면 뷰 렌더링(출력)요청 바인딩(입력) 모두에서 자동으로 양방향 변환이 가능하다.

 

7) 정리

  • ${{...}} 문법을 사용하면 뷰에서 컨버전 서비스가 자동 적용된다.
  • th:field 또한 내부적으로 컨버전 서비스를 활용하여 입력/출력 모두 변환 처리한다.
  • 스프링의 ConversionService를 통한 타입 변환은 컨트롤러, 뷰, 폼 등 모든 계층에 일관되게 적용된다.

'Spring MVC' 카테고리의 다른 글

[MVC] 파일 업로드  (0) 2025.12.21
[MVC] 스프링 타입 컨버터(2)  (0) 2025.11.26
[MVC] API 예외 처리(2)  (1) 2025.09.22
[MVC] API 예외 처리(1)  (0) 2025.09.10
[MVC] 예외 처리와 오류 페이지  (0) 2025.09.06