che01 님의 블로그
TypeConverter — HTTP 요청 문자열을 객체로 편리하게 변환하기 본문
웹 개발하다 보면 항상 맞닥뜨리는 귀찮은 일이 하나 있다. HTTP로 넘어오는 모든 데이터는 문자열인데, 실제로는 숫자나 날짜, 객체로 써야 한다는 것. 매번 Integer.valueOf() 이런 식으로 변환하기엔 너무 번거롭다.
문제 상황
// HTTP 요청: GET /users?age=25&active=true
// 실제로 받는 건 이거
String age = request.getParameter("age"); // "25"
String active = request.getParameter("active"); // "true"
// 근데 쓸 때는 이래야 함
int userAge = Integer.valueOf(age);
boolean isActive = Boolean.valueOf(active);
Spring이 알아서 해주는 자동 변환
Spring은 @RequestParam, @PathVariable 같은 어노테이션 쓸 때 자동으로 타입 변환해준다.
@RestController
public class UserController {
@GetMapping("/users")
public String getUsers(@RequestParam Integer age, // "25" → 25
@RequestParam Boolean active, // "true" → true
@RequestParam LocalDate birthDate) { // "2023-01-15" → LocalDate
return String.format("나이: %d, 활성: %s, 생일: %s", age, active, birthDate);
}
@GetMapping("/users/{userId}")
public String getUser(@PathVariable Long userId) { // "123" → 123L
return "유저 ID: " + userId;
}
// 리스트도 됨
@GetMapping("/users/bulk")
public String getUsers(@RequestParam List<Long> userIds) { // "1,2,3" → [1L, 2L, 3L]
return "유저 IDs: " + userIds;
}
}
Enum도 된다:
public enum UserStatus {
ACTIVE, INACTIVE, PENDING
}
@GetMapping("/users/status")
public String getUsersByStatus(@RequestParam UserStatus status) {
// /users/status?status=ACTIVE → UserStatus.ACTIVE로 변환됨
return "상태: " + status;
}
커스텀 변환기 만들기
기본 제공되는 게 부족하면 직접 만들면 된다.
돈(Money) 객체 변환
@Getter
@AllArgsConstructor
public class Money {
private final BigDecimal amount;
private final String currency;
@Override
public String toString() {
return amount + " " + currency;
}
}
@Component
public class StringToMoneyConverter implements Converter<String, Money> {
@Override
public Money convert(String source) {
if (source == null || source.trim().isEmpty()) {
return null;
}
String[] parts = source.trim().split("\\s+");
if (parts.length != 2) {
throw new IllegalArgumentException("돈 형식이 잘못됨: " + source);
}
BigDecimal amount = new BigDecimal(parts[0]);
String currency = parts[1];
return new Money(amount, currency);
}
}
이제 컨트롤러에서 이렇게 쓸 수 있다:
@PostMapping("/payments")
public String processPayment(@RequestParam Money amount) {
// POST /payments?amount=100.50 USD
// "100.50 USD" 문자열이 Money 객체로 자동 변환
return "결제 처리: " + amount;
}
날짜 변환 (여러 형식 지원)
@Component
public class StringToLocalDateTimeConverter implements Converter<String, LocalDateTime> {
private static final List<DateTimeFormatter> FORMATTERS = Arrays.asList(
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"),
DateTimeFormatter.ofPattern("yyyy-MM-dd"),
DateTimeFormatter.ISO_LOCAL_DATE_TIME
);
@Override
public LocalDateTime convert(String source) {
if (source == null || source.trim().isEmpty()) {
return null;
}
source = source.trim();
for (DateTimeFormatter formatter : FORMATTERS) {
try {
if (formatter == DateTimeFormatter.ofPattern("yyyy-MM-dd")) {
return LocalDate.parse(source, formatter).atStartOfDay();
} else {
return LocalDateTime.parse(source, formatter);
}
} catch (DateTimeParseException ignored) {
// 다음 형식 시도
}
}
throw new IllegalArgumentException("날짜 파싱 실패: " + source);
}
}
사용:
@GetMapping("/events")
public String getEvents(@RequestParam LocalDateTime startDate,
@RequestParam LocalDateTime endDate) {
// 이런 형식들 다 됨:
// ?startDate=2023-12-25
// ?startDate=2023-12-25 14:30
// ?startDate=2023-12-25 14:30:45
return String.format("%s부터 %s까지 이벤트", startDate, endDate);
}
고급 변환기들
ConverterFactory - 비슷한 타입들 일괄 처리
@Component
public class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {
@Override
public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
return new StringToEnum<>(targetType);
}
private static class StringToEnum<T extends Enum> implements Converter<String, T> {
private final Class<T> enumType;
public StringToEnum(Class<T> enumType) {
this.enumType = enumType;
}
@Override
public T convert(String source) {
if (source == null || source.trim().isEmpty()) {
return null;
}
// 대소문자 구분 안함
return (T) Enum.valueOf(enumType, source.trim().toUpperCase());
}
}
}
GenericConverter - 복잡한 변환
@Component
public class CollectionToStringConverter implements GenericConverter {
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(Collection.class, String.class));
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) return null;
Collection<?> collection = (Collection<?>) source;
return collection.stream()
.map(String::valueOf)
.collect(Collectors.joining(", "));
}
}
변환기 등록하기
설정에서 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToMoneyConverter());
registry.addConverter(new StringToLocalDateTimeConverter());
registry.addConverterFactory(new StringToEnumConverterFactory());
}
}
@Component로 자동 등록
@Component // 이거 붙이면 자동으로 등록됨
public class StringToMoneyConverter implements Converter<String, Money> {
// ...
}
실무에서 주의할 점
에러 처리 제대로 하기
@Component
public class SafeStringToIntegerConverter implements Converter<String, Integer> {
@Override
public Integer convert(String source) {
if (source == null || source.trim().isEmpty()) {
return null;
}
try {
return Integer.valueOf(source.trim());
} catch (NumberFormatException e) {
throw new ConversionFailedException(
TypeDescriptor.valueOf(String.class),
TypeDescriptor.valueOf(Integer.class),
source, e);
}
}
}
성능 생각하기 (캐시 활용)
@Component
public class CachedStringToUserConverter implements Converter<String, User> {
private final UserService userService;
private final Cache<String, User> userCache;
public CachedStringToUserConverter(UserService userService) {
this.userService = userService;
this.userCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
}
@Override
public User convert(String userId) {
return userCache.get(userId, key -> userService.findById(Long.valueOf(key)));
}
}
테스트 작성
@ExtendWith(MockitoExtension.class)
class StringToMoneyConverterTest {
private StringToMoneyConverter converter;
@BeforeEach
void setUp() {
converter = new StringToMoneyConverter();
}
@Test
void 정상_변환_테스트() {
// given
String input = "100.50 USD";
// when
Money result = converter.convert(input);
// then
assertThat(result.getAmount()).isEqualByComparingTo("100.50");
assertThat(result.getCurrency()).isEqualTo("USD");
}
@Test
void 잘못된_형식_예외_테스트() {
// given
String input = "잘못된 돈";
// when & then
assertThatThrownBy(() -> converter.convert(input))
.isInstanceOf(IllegalArgumentException.class);
}
}
정리
Spring 타입 변환은 웹 개발할 때 정말 유용하다. 기본 제공되는 것들로 웬만한 건 다 해결되고, 특별한 요구사항 있으면 커스텀 변환기 만들어서 쓰면 된다.
핵심:
- @RequestParam, @PathVariable에서 자동 변환 활용
- 복잡한 객체는 커스텀 Converter 만들기
- 에러 처리랑 성능 고려하기
- 테스트 꼭 작성하기
다음에 "문자열을 객체로 어떻게 바꾸지?" 고민되면 Spring 타입 변환 써보자.
'Spring' 카테고리의 다른 글
| Spring Formatter란 ? (0) | 2025.06.06 |
|---|---|
| ConversionService란? (1) | 2025.06.05 |
| WebMvcConfigurer로 프레임워크 확장하기 (1) | 2025.06.05 |
| Spring MVC ArgumentResolver와 ReturnValueHandler (1) | 2025.06.05 |
| Spring @RequestBody와 HttpMessageConverter 동작 원리 (0) | 2025.06.05 |