[스프링] 타입 컨버터

2025. 10. 9. 16:07·스프링

웹 개발을 처음 시작했을 때, 컨트롤러에서 파라미터를 받을 때마다 이렇게 했다고 한다.

@GetMapping("/hello-v1")
public String helloV1(HttpServletRequest request) {
    String data = request.getParameter("data");
    int intValue = Integer.parseInt(data); // 직접 변환
    System.out.println("intValue = " + intValue);
    return "ok";
}

지금 보면 별거 없어 보이지만,

이 방식은 프로젝트가 커질수록 끝없는 반복 지옥이 된다.

문자열을 숫자로 바꾸고, 날짜로 바꾸고, 객체로 바꾸고…

컨트롤러마다 같은 코드가 계속 복붙된다.

 

그뿐만 아니라, 변환 실패 시 예외 처리도 직접 해야 한다.
입력값 "abc" 하나 잘못 들어오면 NumberFormatException이 터지고,
사용자에게 친절한 메시지 하나 보여주는 것도 쉽지 않다.

이건 비즈니스 로직과 변환 로직이 섞인 코드다.
요약하자면,

“컨트롤러는 데이터 검증과 비즈니스 처리에 집중해야 하는데,
변환 때문에 엉뚱한 일을 너무 많이 하고 있었다.”

 

그래서 스프링은 이 문제를 해결하기 위해
타입 변환(Converter) 시스템을 만들어냈다.

이제부터 그 발전 과정을 단계별로 따라가 본다.

 

1.수작업 파싱에서 Converter로

스프링은 타입 변환을 표준화하기 위해 Converter<S, T> 인터페이스를 제공한다.
이 인터페이스는 “이 타입을 저 타입으로 바꾼다”는 명확한 역할만 가진다.

예를 들어 "127.0.0.1:8080" 같은 문자열을 IpPort 객체로 바꾸려면 이렇게 만든다.

@Slf4j
public class StringToIpPortConverter implements Converter<String, IpPort> {
    @Override
    public IpPort convert(String source) {
        String[] split = source.split(":");
        String ip = split[0];
        int port = Integer.parseInt(split[1]);
        return new IpPort(ip, port);
    }
}

이제 변환 규칙이 코드 한 곳에 고정된다.
컨트롤러에서는 더 이상 파싱 코드를 볼 일이 없다.

 

예를 들어 이렇게 쓸 수 있다.

public class ConverterTest {
    public static void main(String[] args) {
        StringToIpPortConverter converter = new StringToIpPortConverter();
        IpPort ipPort = converter.convert("127.0.0.1:8080");
        System.out.println("ip = " + ipPort.getIp());
        System.out.println("port = " + ipPort.getPort());
    }
}

잘 작동하긴 하지만,
문제는 이걸 매번 직접 선언하고 불러 써야 한다는 점이다.
프로젝트 안에서 String → Integer, String → Boolean, String → IpPort 같은 변환이 여러 개 생기면
컨버터 인스턴스가 여기저기 흩어진다.

 

게다가 “어떤 컨버터를 써야 하지?” “이 타입은 변환 가능한가?”
이런 관리 포인트가 늘어나기 시작한다.

 

그래서 스프링은 컨버터들을 한곳에 모아두고 통합 관리하는 시스템을 만들었다.
그게 바로 다음에 나올 ConversionService다.

 

 

2.컨버터를 모아 쓰는 ConversionService

컨버터가 하나둘 늘어나기 시작하면 관리가 귀찮아진다.
프로젝트 안에서 String → Integer, String → Boolean, String → IpPort…
이런 변환이 계속 생기면 컨버터 인스턴스가 여기저기 흩어진다.

 

그래서 스프링은 컨버전 서비스(ConversionService) 라는 통합 변환 관리자를 만들었다.
컨버터를 한 번 등록해두면, 필요한 곳에서 자동으로 찾아서 쓴다.

DefaultConversionService service = new DefaultConversionService();
service.addConverter(new StringToIpPortConverter());
IpPort result = service.convert("127.0.0.1:8080", IpPort.class);

이제 변환은 convert() 한 줄로 끝난다.
내부에서는 ConversionService가 등록된 컨버터 중에서
입력 타입(String)과 출력 타입(IpPort)이 맞는 걸 자동으로 찾아 실행한다.

 

그리고 이 컨버전 서비스는 단순한 유틸이 아니다.
스프링 MVC 내부에서 아주 깊숙이 사용된다.

 

@RequestParam, @ModelAttribute, @PathVariable 같은 어노테이션이
요청 데이터를 컨트롤러 파라미터로 바인딩할 때,
바로 이 ConversionService를 거쳐간다.

 

즉, 이런 코드가 돌아가는 이유가 바로 이거다.

@GetMapping("/ip-port")
public String ipPort(@RequestParam IpPort ipPort) {
    log.info("ipPort = {}", ipPort);
    return "ok";
}

요청 URL이 /ip-port?ipPort=127.0.0.1:8080이라면,
@RequestParam이 내부적으로 ConversionService를 호출해서
String → IpPort 변환을 수행한다.

 

결과적으로 컨트롤러는 순수한 도메인 타입만 선언하면 된다.
나머지 변환은 전부 스프링이 알아서 처리한다.

 

3.스프링 MVC 전역에 등록하기

이제 변환기를 애플리케이션 전체에 적용하려면 설정이 필요하다.
스프링은 WebMvcConfigurer의 addFormatters()를 통해 전역 등록을 지원한다.

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToIpPortConverter());
        registry.addConverter(new IpPortToStringConverter());
    }
}

이제 이 프로젝트 안의 모든 컨트롤러와 템플릿이 자동으로 변환을 적용받는다.

 

4.뷰와 폼에서도 자동 변환

타임리프(Thymeleaf)는 ConversionService를 활용해 뷰에서 변환을 처리한다.
${{...}} 문법을 쓰면 등록된 컨버터를 통해 자동 변환이 일어난다.

<li>${ipPort}: <span th:text="${ipPort}"></span></li>
<li>${{ipPort}}: <span th:text="${{ipPort}}"></span></li>

${ipPort}는 단순히 toString()을 호출하지만,
${{ipPort}}는 컨버전 서비스를 통해 IpPort → String으로 변환해 출력한다.

입력 폼(th:field="*{ipPort}")에서도 반대로 String → IpPort 변환이 자동으로 이뤄진다.

 

 

5.문자 ↔ 객체 변환에 특화된 Formatter

Converter로도 문자열을 숫자로 바꿀 수 있지만,
Formatter는 “사람이 읽는 형식”을 다루는 데 특화되어 있다.
특히 숫자, 날짜, 통화처럼 표시 형식이 중요한 데이터에 자주 쓰인다.

 

예를 들어보자.
쇼핑몰 주문 화면에 가격을 출력해야 하는데,
DB에는 단순히 int price = 1000;으로 저장돼 있다.
이걸 그대로 보여주면 1000이라서 눈에 안 들어온다.
우리가 원하는 건 "1,000" 같은 형식이다.

 

반대로 사용자가 "1,000"을 입력했을 때,

서버에서는 그걸 int 1000으로 정확하게 바꿔야 한다.
이럴 때 딱 맞는 게 Formatter다.

@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
        return NumberFormat.getInstance(locale).parse(text);
    }

    // 서버 내부 숫자 → 화면 출력용 문자열로 변환
    @Override
    public String print(Number object, Locale locale) {
        log.info("object = {}, locale = {}", object, locale);
        // 1000 -> "1,000"
        return NumberFormat.getInstance(locale).format(object);
    }
}

이 Formatter는 단순히 숫자를 바꾸는 게 아니라,
로케일(Locale)에 맞춰 자동으로 구분 기호(,)나 소수점(.)을 처리해준다.
예를 들어 한국 로케일에서는 1,000, 미국 로케일에서는 1,000.00 식으로 표현이 달라진다.

 

이 Formatter를 컨버전 서비스에 등록하면
String ↔ Number 변환이 전역적으로 자동 적용된다.

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addFormatter(new MyNumberFormatter());
    }
}

이제 컨트롤러에서 이런 코드가 가능해진다.

@GetMapping("/formatter-test")
public String formatterTest(@RequestParam Number price) {
    return "입력한 가격: " + price;
}

요청 URL이 /formatter-test?price=1,000이라면
스프링이 "1,000"을 자동으로 숫자 1000으로 변환해 준다.

 

즉, Formatter는 사람이 읽는 데이터를 다룰 때 진가를 발휘한다.

  • 화면엔 "1,000"처럼 보기 좋게 출력하고
  • 입력값 "1,000"은 자동으로 숫자로 되돌리고
  • Locale에 따라 통화, 날짜 형식도 자연스럽게 처리한다

그래서 돈, 날짜, 전화번호, 우편번호처럼
“보이는 값은 문자열이지만 내부에서는 숫자나 객체여야 하는 데이터”에 꼭 필요하다.

 

 

6. 스프링이 지원하는— 애노테이션 기반 포맷터

지금까지 Converter, Formatter, 그리고 ConversionService까지
일일이 등록하면서 타입 변환의 원리를 직접 다뤄봤다.

하지만 현재에서 이런 코드를 매번 등록할 일은 거의 없다.
왜냐하면, 스프링이 우리가 위에서 구현했던 과정을 이미 내부적으로 지원하기 때문이다.

 

예를 들어, 아까 우리가 MyNumberFormatter를 직접 만들어서
“1000 → 1,000” 변환을 직접 처리했지만,
이제 그걸 애노테이션 한 줄로 대체할 수 있다.

@Data
static class Form {

    // 숫자 포맷 지정 — "1000" ↔ "1,000"
    @NumberFormat(pattern = "###,###")
    private Integer number;

    // 날짜 포맷 지정 — "2025-10-09 15:30:00" ↔ LocalDateTime
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime localDateTime;
}

이 두 애노테이션이 바로 스프링이 기본 제공하는 포맷터 설정 방식이다.
이걸 붙이는 순간 스프링은 내부적으로 다음과 같은 일을 자동으로 해준다.

  1. @NumberFormat을 보고 숫자용 포맷터를 등록한다.
  2. @DateTimeFormat을 보고 날짜/시간 포맷터를 등록한다.
  3. ConversionService를 통해 요청 파라미터, 모델 바인딩, 템플릿 렌더링 단계에서 자동 변환을 수행한다.

즉,
우리가 직접 만들었던 MyNumberFormatter, DateFormatter 같은 것들은
지금은 스프링이 알아서 등록하고 실행해주는 구조로 발전해온 거다.

 

입력 "1,000"이 들어오면 자동으로 Integer 1000으로 변환되고,
출력 시엔 다시 "1,000" 형태로 보여준다.
날짜(LocalDateTime)도 마찬가지로 2025-10-09T15:30 같은 객체가
"2025-10-09 15:30:00" 문자열로 자연스럽게 변환된다.

 

 

마무리하며

예전엔 Integer.parseInt() 하나로 하루를 보내던 시절이 있었다한다.
요청 올 때마다 파라미터 꺼내서 형변환하고,
잘못 들어오면 NumberFormatException 터지고,
그걸 또 try-catch로 감싸서 로그 찍고…
그게 당연하다고 생각했다.

하지만 이제는 컨트롤러에 타입만 선언해두면 된다.
스프링이 알아서 변환해주고, 포맷까지 맞춰준다.

Converter → ConversionService → Formatter → 애노테이션
이게 스프링 타입 변환이 자라온 과정이다.

  • 반복되는 변환 로직은 컨버터로 묶고
  • 여러 타입은 컨버전 서비스로 관리하고
  • 숫자나 날짜 같은 포맷은 포매터로 다듬고
  • 실무에서는 대부분 애노테이션 한 줄로 끝낸다

사실 요즘 프로젝트에서는 애노테이션만 써도 충분하다.
그런데 굳이 직접 컨버터나 포매터를 만들어봤던 이유는 하나다.내부가 어떻게 돌아가는지 알아야, 스프링이 지원하지 않는 형식도 직접 다룰 수 있기 때문이다.

 

결국 스프링이 해주는 건 우리가 직접 만들었던 로직의 자동화 버전이다.
그래서 원리만 이해하고 있으면,
특수한 데이터 포맷이 생겨도, 새로운 도메인 타입이 추가돼도
겁먹지 않고 직접 등록해서 쓸 수 있다.

 

타입 변환은 단순히 “문자 → 객체”를 바꾸는 게 아니다.
데이터를 얼마나 깔끔하게 흘려보낼 수 있느냐의 문제다.
그걸 한 번 이해해두면, 스프링의 데이터 처리 구조가 훨씬 명확하게 보인다.

 

 

감사합니다.

'스프링' 카테고리의 다른 글

[스프링] Swagger세팅과 사용  (0) 2025.11.27
[스프링] 파일 업로드  (0) 2025.10.09
[스프링] API 예외 처리  (0) 2025.10.05
[스프링] 예외 처리와 오류 페이지  (0) 2025.10.01
[스프링] 로그인 처리 3 - 스프링 인터셉터(Interceptor)  (0) 2025.09.29
'스프링' 카테고리의 다른 글
  • [스프링] Swagger세팅과 사용
  • [스프링] 파일 업로드
  • [스프링] API 예외 처리
  • [스프링] 예외 처리와 오류 페이지
0kingki_
0kingki_
자바 + 스프링 웹 개발
  • 0kingki_
    0kingki_
    0kingki_
  • 전체
    오늘
    어제
    • 분류 전체보기 (134)
      • 코딩 테스트 (54)
      • 자바 (21)
      • 스프링 (27)
      • 타임리프 (16)
      • 스프링 데이터 JPA (8)
      • 최적화 (2)
      • QueryDSL (4)
      • AWS (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    다형성
    객체지향
    예외 처리
    컬렉션
    dfs
    thymeleaf
    쿼리
    불변객체
    재귀
    코딩테스트
    코딩 테스트
    타임리프
    백준
    spring
    QueryDSL
    스프링
    SOLID
    Java
    BFS
    JPA
    mvc
    예외처리
    자바
    LocalDateTime
    스프링 데이터 JPA
    SpringDataJpa
    최적화
    스프링 컨테이너
    fetch join
    쿼리dsl
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
0kingki_
[스프링] 타입 컨버터
상단으로

티스토리툴바