본문 바로가기
Back/Spring Boot

컨트롤러에서 Enum 타입 @RequestParam @PathVariable로 매핑하기

by 은z 2024. 4. 15.

상황

컨트롤러 단에서 Get Mapping 시, Enum 타입의 코드 값을 넘겨주고 받도록 코드를 구현했다.

그런데 해당 api 호출 시에 에러를 뱉었다.

 

 

/api/enum-test?enumType=T 로 호출 시 에러 발생 (enum 타입의 코드 값으로 호출하면 에러 발생)

/api/enum-test?enumType=TITLE 로 호출 시 정상 동작

 


도입

알고보니 @RequestParam @PathVariable를 통해 파라미터를 넘길 때 정수형 혹은 문자열 타입으로만 제한하여 사용할 수 있다고 한다. 따라서 Enum의 코드 타입을 변환시켜줄 수 있도록 별도의 구현이 필요하다.

 

일단 내가 작성한 코드를 살펴보자.

 

 

✏️EnumType.java

@Getter
@AllArgsConstructor
public enum EnumType implements Serializable, CommonType {

    @JsonProperty("T")
    TITLE("제목", "T"),
    @JsonProperty("N")
    NAME("작성자", "N");

    private String desc;
    private String code;

    private static final Map<String, EnumType> CODE_MAP = Stream.of(values())
            .collect(Collectors.toMap(EnumType::getCode, Function.identity()));

    @JsonCreator
    public static EnumType convertCodeToName(String value) {
        return Optional.ofNullable(CODE_MAP.get(value))
                .orElseThrow(() -> new IllegalArgumentException("invalid value"));
    }
}

 

 

✏️ CommonType 인터페이스

public interface CommonType {

    String getCode();
    String getDesc();
}

 

 

✏️ Controller.java (문제가 된 코드)

view 단에서 /api/enum-test?enumType=T 로 호출에러 발생 (enum 타입의 코드 값으로 호출하면 에러 발생)

/api/enum-test?enumType=TITLE 로 호출 시 정상 동작

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class TestApiController {

    @GetMapping("/enum-test")
    public ResponseEntity findBoardPostList(@RequestParam EnumType enumType) {
        log.debug("test : {}", enumType);
        return ResponseEntity.ok().build();
    }
}

 

여기서 의문이 생길 수 있다. 

enum의 코드값을 넘겨주면 에러가 발생하는데, 왜 full name 으로 호출하면 정상적으로 동작하는 걸까?

바로 Spring 에서 기본적으로 제공해주는 StringToEnumConverterFactory 덕분에 Enum의 name값으로는 매핑이 가능한 것이다.

org.springframework.core.convert.support 하위에 존재함.

 

 

 

 

나는 Enum의 이름이 아니라 프론트단에서 별도로 정의한 code 값으로 넘겨주고 싶다! ( /api/enum-test?enumType=T )

어떻게 해야할까?

 


 

 

적용 (Code 값으로 주고 받기)

✔️첫번째 방법

일단 별도의 Converter를 구현하지 않고는 컨트롤러 단에서 넘겨받는 객체가 enum의 코드값이면 안된다는 것은 알았다.

그럼 기존처럼 파라미터를 String 타입으로 받아서 해결하면 되지 않을까?

 

 

✏️ Controller.java

@GetMapping("/enum-test")
public ResponseEntity findBoardPostList(@RequestParam String enumType) {
    log.debug("test : {}", EnumType.convertCodeToName(enumType).getCode()); // Enum 객체로 바꾸고, 해당 코드를 얻는다.
    return ResponseEntity.ok().build();
}

📌참고로 @RequsetParam 어노테이션은 생략 가능하다. (생략 시 스프링 MVC는 내부에서 required=false를 적용)

 

위 코드처럼 수정했더니, /api/enum-test?enumType=T 호출 시 아래와 같이 로그에 정상적으로 찍힌다.

test : T

 

 

파라미터로 전달받은 String을 Enum 객체로 바꾸는 작업 (EnumType.convertCodeToName(enumType)) 을 진행했다.

그리고 getCode() 메소드를 이용하여 별도로 정의한 code 값으로 반환했다.

 

순서를 정리해보면

"Enum의 code 값을 프론트단에서 넘겨주기 -> Controller에서 String 타입으로 받기 -> Enum 객체로 바꾸기 -> Name을 code값으로 바꾸기

 

물론 이렇게 작업을 진행해서 service 단으로 넘겨 원하는 로직을 태워도 된다.

하지만 매번 이렇게 String 을 Enum 객체로 변경하는 작업은 매우 귀찮고 비효율적이다.

뭔가 전역적으로 변환하여 사용할 수 있도록 하는 방법은 없을까?

 

 

 

 

✔️두번째 방법

찾아보니 방법이 있다!

 

먼저 CommonType Interface 구현한 Enum을 대상으로 String을 Enum 객체로 변환할 수 있는 ConverterFactory를 만들자.
CommonType 인터페이스는 별도로 코드값과 설명을 작성하기 위해 만들어 뒀다. ( EnumType 클래스에 implements 하여 사용함 )

 

✏️ CodeToEnumConverterFactory.java

import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import java.util.Arrays;


public class CodeToEnumConverterFactory implements ConverterFactory<String, Enum<? extends CommonType>> {
    @Override
    public <T extends Enum<? extends CommonType>> Converter<String, T> getConverter(Class<T> targetType) {
        return new StringToEnumsConverter<>(targetType);
    }

    private static final class StringToEnumsConverter<T extends Enum<? extends CommonType>> implements Converter<String, T> {

        private final Class<T> enumType;
        private final boolean constantEnum;

        public StringToEnumsConverter(Class<T> enumType) {
            this.enumType = enumType;
            this.constantEnum = Arrays.stream(enumType.getInterfaces()).anyMatch(i -> i == CommonType.class);
        }

        @Override
        public T convert(String source) {
            if (source.isEmpty()) {
                return null;
            }

            T[] constants = enumType.getEnumConstants();
            for (T c : constants) {
                if (constantEnum) {
                    if (((CommonType) c).getCode().equals(source.trim())) {
                        return c;
                    }
                } else {
                    if (c.name().equals(source.trim())) {
                        return c;
                    }
                }
            }
            return null;
        }
    }
}

 

✏️[24.08.01 추가] 위 코드는 Null Pointer 역참조에 취약한 부분이 있어서 보안 강화를 위해 추가했다. 수정한 코드는 아래에 링크를 걸어둔다.

2024.08.01 - [Back/Java] - [JAVA][시큐어코딩] Null Pointer 역참조 취약에 대응하기

 

[JAVA][시큐어코딩] Null Pointer 역참조 취약에 대응하기

상황 프로젝트 마무리 단계에서 소스코드 보안약점 진단 실행 후, 수정이 필요한 사항이 생겼다.Null Pointer 역참조에 대한 개념과 수정 방안에 대해 정리해보겠다.   ✔️Null Pointer 역참조 ???널

zoetechlog.tistory.com

 

 

 

 

그리고 만든 CodeToEnumConverterFactory 를 Spring에 등록하면 끝.

@Configuration
public class WebConfig implements WebMvcConfigurer {

    /**
     * CodeToEnumConverterFactory.class 등록
     */
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverterFactory(new CodeToEnumConverterFactory());
    }
    
}

 

 

 

 

RequestParam의 데이터 타입이 EnumType 인 것에 주목하자.

@GetMapping("/enum-test")
public ResponseEntity findBoardPostList(@RequestParam EnumType enumType) {
    log.debug("test1 : {}", enumType);
    log.debug("test2 : {}", enumType.getCode());
    return ResponseEntity.ok().build();
}

/api/enum-test?enumType=T  이렇게 호출하면??

결과는 콘솔창에 아래와 같이 찍힌다!

test1 : TEST
test2 : T

 

 

댓글