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

소개
웹 애플리케이션 개발을 하다 보면 문자를 숫자로, 숫자를 문자로 변환하는 등 타입 변환이 자주 필요하다. 스프링은 이런 변환 과정을 자동으로 처리해 주는 타입 컨버터 기능을 제공한다.
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 인터페이스를 제공한다.
- 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: 컨버터 등록에 초점
이렇게 인터페이스를 분리하면 다음과 같은 장점이 있다.
- 컨버터를 사용하는 클라이언트는 ConversionService만 의존한다.
- 컨버터 등록/관리와 사용의 관심사를 분리할 수 있다.
- 불필요한 메서드 의존이 사라져 유지보수가 편해진다.
스프링 내부에서의 사용
스프링은 내부적으로 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 |