Restful 하게 Enum Parameter 처리

이프·2025년 7월 1일

back-end

목록 보기
9/16

목적

"ENUM Parameter를 RESTFUL 하게 처리하자"


@Schema(description = "상품 목록 조회 요청")
public record PointProductSearchCondition(
	@Schema(description = "페이지 (nullable)", example = "1")
	Integer page,
	@Schema(description = "페이지 당 항목 수 (nullable)", example = "10", defaultValue = "10")
	Integer size,
	@Schema(description = "상품코드 및 상품명 (nullable)", example = "텀블러")
	String keyword,
	@Schema(description = "상품 판매 상태 (nullable)")
	SellingStatus status
) {
}
	@GetMapping
	public ApiTemplate<List<PointProductSearchResponse>> findPointProducts(
		@ParameterObject @ModelAttribute PointProductSearchCondition condition
	) {
		List<PointProductSearchResponse> result = pointProductQueryRepository.findTop10PointProducts(condition);
		return ApiTemplate.ok(POINT_PRODUCTS_SEARCH_SUCCESS, result);
	}

해당 api를 처리하는 중 status를 처리하려고 하니까 뭔가 거슬리는게 발생했다.

swagger에서 status가 Enum 그대로 대문자로 표현되는 것이다.
이게 무슨 문제가 있을까..?
사실 문제는 없다. 다만, restful api와는 거리가 멀어진다.

curl -X 'GET' \
  'http://localhost:8080/api/point-products?status=EXCHANGEABLE' \
  -H 'accept: */*'

이런식으로 status가 대문자로 전달된다.

"여기서 RESTFUL한 API란?"

restful하려면 url이 lower kebab case여야만한다.
즉, point-products와 같이 소문자이며 구분은 하이픈(-)으로 해야한다.

그럼 이제 restful api를 처리하도록 코드를 수정해보자.


Converter를 활용하자

앞서, 다른 도메인에서 작성한 코드를 가져와서 확인해보자.

@Getter
@AllArgsConstructor
public enum Purpose {

	CHALLENGE("challenge"),
	CHALLENGE_AUTH("challenge_auth"),
	INFO("info"),
	PRODUCT("product");

	private final String value;
}

@Component
public class StringToPurposeConverter implements Converter<String, Purpose> {
	@Override
	public Purpose convert(String source) {
		for (Purpose purpose : Purpose.values()) {
			if (purpose.getValue().equals(source)) {
				return purpose;
			}
		}
		throw new IllegalArgumentException("Cannot convert '" + source + "' to Purpose");
	}
}

spring core에서는 Converter를 제공한다.
1. webMvc 설정에서 이 Converter를 활용할 수 있다.
2. 지금처럼 Component scan 되도록 할 수 있다.

이 방식의 문제점은 무엇일까?
1. 단일 값 임에도 불구하고 매번 Enum에 value를 추출할 수 있도록 처리해야한다.
2. 혹은 toString을 재정의한다.
3. 매번 Enum에 대한 Converter를 만들어야한다.

그럼, API에 사용되는 Enum이 100개라면 같은 로직의 Converter가 100개가 생성되는 불편함이 있었다.

그래서 공통 모듈로 개선해보기로 했다.


ConverterFactory 활용

Spring core는 기본적으로 IOC, DI 등을 제공하니 웬만하면 Factory가 존재한다.

package org.springframework.core.convert.converter;

public interface ConverterFactory<S, R> {
    <T extends R> Converter<S, T> getConverter(Class<T> targetType);
}

이 팩토리를 활용해보자! 우리가 원하는 목적은 Enum을 처리하는 것이므로 EnumConverter를 만들도록 처리하면 된다.

Enum ConverterFactory

@SuppressWarnings({"unchecked", "rawtypes"})
public class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {

	@Override
	public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
		return source -> {
			try {
				return convertUpperCase(targetType, source.trim());
			} catch (NullPointerException e) {
				return null;
			}
		};
	}

	private <T extends Enum> T convertUpperCase(Class<T> targetType, String source) {
		try {
			return (T)Enum.valueOf(targetType, source.toUpperCase());
		} catch (IllegalArgumentException e) {
			return convertFromKebabCase(targetType, source);
		}
	}

	private <T extends Enum> T convertFromKebabCase(Class<T> targetType, String source) {
		try {
			return (T)Enum.valueOf(targetType, source.replace("-", "_").toUpperCase());
		} catch (IllegalArgumentException e) {
			throw new IllegalArgumentException("변환할 수 없는 요청: " + source);
		}
	}
}

기본적으로 converter interface는 functional interface로 nullable이다. 그래서 ide에서 노란 문구가 너무 많이뜨니까 warning을 막아주자.

동작 방식은 아래와 같다.
1. uppercase로 처리한다.
2. kebabcase를 snake upper case로 처리한다.
이 과정에서 Enum.valueOf는 IAE, NPE를 반환하므로 적당히 처리해준다.

Swagger도 수정하기

우리가 restful한 api를 처리하는지 알려주기 위해서 문서화도 해줘야한다.

	@Schema(type = "string",
		description = "상품 판매 상태 (nullable)",
		allowableValues = {"exchangeable", "sold-out"})
	SellingStatus status

scheme에서 type을 string으로 하고 value를 소문자로 강제로 덮어씌운다.

public enum SellingStatus {
	@JsonProperty("exchangeable")
	EXCHANGEABLE,
	@JsonProperty("sold-out")
	SOLD_OUT
}

혹은 enum에서 이렇게 처리해도 된다.

@Getter
@AllArgsConstructor
public enum Purpose {

	CHALLENGE("challenge"),
	CHALLENGE_AUTH("challenge-auth"),
	INFO("info"),
	PRODUCT("product");

	private final String value;

	@Override
	public String toString() {
		return getValue();
	}
}

to String을 override 해도 된다.

JsonProperty는 queryParam이 아닌 requestBody를 처리하는 것이므로 편한대로 설정하면 될 것 같다. 나는 enum을 최대한 더럽히기 싫어서 JsonProperty만 피했다.

결과

결과적으로 모든 API에서 enum에 대한 queryParam을 소문자로 처리할 수 있게됐다.

이건 실수로 sold_out이라고 적었다. swagger 명세의 단점이 보이는 대표적인 예시

목적에 맞게 잘 해결됐다.

기존 코드 또한 잘 적용 됐다.

전체 아키텍처

마치며

이번 포스팅에서는 RESTful API의 관점에서 Enum 파라미터를 어떻게 우아하게 처리할 수 있는지 살펴보았다.

핵심 개선사항

Before: 각 Enum마다 개별 Converter 작성

  • 중복 코드 발생
  • 유지보수 비용 증가
  • 일관성 없는 처리 방식

After: ConverterFactory를 활용한 공통 처리

  • 모든 Enum에 대해 일관된 변환 로직
  • kebab-case 지원으로 RESTful API 준수
  • 확장 가능한 구조

이런 작은 개선이 실제 개발에서는 큰 차이를 만든다.
API 사용자 입장에서는 status=exchangeable이 status=EXCHANGEABLE보다 훨씬 자연스럽고, 일관된 네이밍 컨벤션을 유지할 수 있다.

특히 프론트엔드 개발자와의 협업에서 "이 파라미터는 대문자로, 저 파라미터는 소문자로" 같은 혼란을 방지할 수 있어 팀 전체의 생산성 향상에 도움이 된다.

profile
if (이런 시나리오는 어떨까?) then(테스트로 검증하고 해결) else(다음 시나리오 고민)

0개의 댓글