[Spring] 쿼리 파라미터를 객체로 받아보자

neo-the-cow·2023년 11월 17일
1

Spring

목록 보기
2/2
post-thumbnail

0. 들어가기 전에

  • 스프링/부트 환경에서 쿼리 파라미터를 객체로 바인딩합니다.
  • 실습 환경과 목표는 다음과 같습니다.
  • 공부하며 기록하는탓에 오개념이 있을 수 있습니다. 댓글, 이메일로 알려주시면 더 공부하고 잘 반영하겠습니다.

🛠️ 실습 환경

Lang: Java 17
Framework: Spring Boot 3.1.5

📝 실습 목표

쿼리 파라미터의 개수가 많아질 때 하나의 불변 객체로 바인딩 하기

0.1 왜?

  • 컨트롤러를 작성하다보면 보통 GET요청에 대해 다음과 같은 코드를 작성하게 됩니다.
@GetMapping("/uri")
public ...리턴값 getFoo() {
	...
	return ... 리턴값
}
  • 이 때 일부 파라미터를 클라이언트로 받아오려면 어떻게 할까요?
@GetMapping("/uri")
public ...리턴값 getFoo(
		@RequestParam(name = "val1") String val1,
    	@RequestParam(name = "val2") String val2
) {
	...
	return ... 리턴값
}
  • 이렇게 파라미터가 3개 미만이라면 크게 가독성을 해치지도 않으면서 명시적으로 잘 사용할 수 있을지도 모릅니다.
  • 하지만 다음과 같은 요청을 처리하려면 그만큼 많은 @RequestParam애노테이션과 매개변수로 코드 가독성이 저해될겁니다.
https://domain.com/resource?val1={val1}&val2={val2}&val3={val3}&val4={val4}...
@GetMapping("/uri")
public ...리턴값 getFoo(
		@RequestParam(name = "val1") String val1,
    	@RequestParam(name = "val2") String val2,
        @RequestParam(name = "val3") String val3,
    	@RequestParam(name = "val4") String val4,
        ...
) {
	...
	return ... 리턴값
}

0.2 그럼 대안은?

0.2.1 @RequestBody

  • 작성일(2023.11.17) 기준으로 위키피디아의 내용에 따르면 다음과 같이 GET요청에도 본문을 포함 할 수 있다고 합니다.

  • 스프링에서 또한 GET요청 파라미터에도 @RequestBody애노테이션을 붙여 요청 본문을 받을 수 있습
    니다.
@GetMapping("/uri")
public ...리턴값 getFoo(
		@RequestBody Foo foo
        ...
) {
	...
	return ... 리턴값
}
  • 다만 이렇게 받은 Foo 객체는 쿼리 파라미터로 받아온 파라미터를 바인딩한게 아니라 말 그대로 요청 본문의 내용(json or sth...)을 Foo 타입으로 역직렬화 한 것입니다.
  • 포스팅에서 목표한 "쿼리 파라미터들을 바인딩하자"에는 적합하지 않은 솔루션이 될 수 있습니다.
  • 추가로 맥락상 GET요청에 필요한 내용을 본문에 담는건 상황에 따라 적합하지 않을 수 있습니다.

    💬 HTTP GET 메소드와 body
    💬 Get 요청 body에 관해서..

0.2.2 Map<String, String>

  • 스프링에서는 쿼리 파라미터 목록을 Map타입으로 바인딩 할 수 있습니다.
@GetMapping("/uri")
public ...리턴값 getFoo(
		@RequestBody Map<String, String> params
        ...
) {
	...
    params.get("paramName");
    ...
	return ... 리턴값
}
  • 하지만 각 파라미터 이름을 직접 입력해줘야하고 null값과 원치 않는 파라미터입력에 대한 보호 로직을 추가해야 한다는 단점이 있을 수 있습니다.

0.2.3 @ModelAndAttribute

  • 대안으로 @ModelAndAttribute 애노테이션을 사용할 수 있습니다.
@GetMapping("/uri")
public ...리턴값 getFoo(
		@ModelAndAttribute Foo foo
        ...
) {
	...
	return ... 리턴값
}

---

@Getter
@Setter
class Foo {
	Bar bar1
    Bar bar2
    ...
}
  • 하지만 @ModelAndAttribute애노테이션을 이용해 객체로 바인딩 할 땐 바인딩 할 객체에 각 파라미터에 대한 필드가 있어야 하고 그 필드들에 대한 Setter가 모두 존재해야한다는 단점이 있습니다.
  • 이는 보통 컨트롤러 레이어에서 받는 Dto역할을 하는 객체의 필드값이 변경될 여지가 있어 불변성을 보장하지 않아 서두에 설정했던 목표를 위반하게 됩니다.

1. 파라미터 바인딩 하기

  • 위에서 살펴봤던 방법들의 단점을 보완하고 조금더 명시적으로 바인딩을 처리하기 위해 커스텀 애노테이션을 만들고 파라미터 값을 모아 하나의 객체로 바인딩 하는 로직을 구성합니다.

1.1 커스텀 애노테이션과 메서드 인자 리졸버

  • 먼저 실습에 앞서 쿼리파라미터로 넘어온 페이징 관련 변수들을 Pageable 객체로 바인딩 해주는 PageableHandlerMethodArgumentResolver를 살펴보겠습니다.

  • PageableHandlerMethodArgumentResolverPageableArgumentResolver 인터페이스를 구현하고 PageableArgumentResolver 인터페이스는 HandlerMethodArgumentResolver를 상속받고 있는걸 확인할 수 있습니다

  • 제가 이해한 HandlerMethodArgumentResolver의 주석 내용은 다음과 같습니다.
의역)
요청 파라미터를 인자(타입)로 변환하는 전략인터페이스입니다.

boolean suppertsParameter -> 파라미터가 리졸버를 지원하는지를 참/거짓으로 반환합니다.

Object resolveArgument -> 파라미터를 변환해 리턴합니다.
  • 다시 PageableHandlerMethodArgumentResolver인터페이스로 돌아와서 구현체 로직을 살펴보면 다음과 같습니다.

  • PageableHandlerMethodArgumentResolverSupport에 정의된 방식대로 Pageable객체를 만들어 반환합니다.
  • 이렇게 별다른 애노테이션 없이도 파라미터 조건만 만족하면 Pageable객체로 바인딩 해줍니다.
  • 하지만 이번 포스팅에서는 애노테이션을 달아주어 리졸브 과정을 거치는것을 명시적으로 표현해주는 방식으로 실습해보겠습니다.

1.2 구현해보기

  • 채팅 서비스에서 채팅방 검색 기능을 구현한다고 가정해보겠습니다.
  • 우선 채팅방 검색을 하려면 공개여부, 검색타입(제목, 방장아이디 등), 키워드, 페이지번호, 페이지사이즈, 정렬기준, 정렬방향 총 7가지의 파라미터가 필요합니다.
  • 앞서보았던 Pageable객체로의 바인딩을 사용해 페이지번호, 페이지사이즈, 정렬기준, 정렬방향 이 4개의 파라미터를 묶어주면 공개여부, 검색범위(제목, 방장아이디 등), 키워드가 남게됩니다.
  • 남은 3개의 파라미터들을 RoomSearchParam라는 객체로 바인딩하겠습니다.

1.2.1 바인딩 할 객체 선언하기

public record RoomSearchParam(
		boolean isPublic,
        String type,
        String value
) {}

💬 record?

자바 14부터 추가된 데이터 클래스입니다.
final 클래스라 상속할 수 없으며 모든 필드는 private final 로 선언됩니다.
각 필드의 이름으로 정의된 Getter메서드와 모든 필드를 초기화하는 생성자가 기본으로 생성됩니다.

1.2.2 커스텀 애노테이션 만들기

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RoomSearchRequest {
}
  • 스프링 애노테이션을 만들어줍니다.
  • @Target(ElementType.PARAMETER): 메서드 파라미터로 선언한 객체에만 지정할 수 있는 애노테이션이라고 선언합니다.
  • @Retention(RetentionPolicy.RUNTIME): 애노테이션 정보를 런타임시까지 유지합니다.

1.2.3 리졸버 설정하기

@Component
public class RoomSearchParamHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {

    private final List<String> types = List.of("title", "host");

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType().equals(Request.RoomSearchParam.class) &&
                parameter.hasParameterAnnotation(RoomSearchRequest.class);
    }

    @Override
    public Object resolveArgument(
            MethodParameter parameter,
            ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest,
            WebDataBinderFactory binderFactory
    ) throws Exception {
        boolean isPublic = Boolean.parseBoolean(webRequest.getParameter("public"));
        String type = webRequest.getParameter("type");
        String value = webRequest.getParameter("value");
        validateParam(type, value);
        return new Request.RoomSearchParam(isPublic, type, value);
    }

    private void validateParam(String type, String value) {
        if ((type == null && value != null) || (type != null && !types.contains(type))) {
            throw new IncompatibleParametersException();//TODO: 상세 예외 처리
        }
    }

}
  • 앞서 살펴봤던 PageableHandlerMethodArgumentResolver와는 다르게 supportsParameter() 메서드에서 위에서 생성한 @RoomSearchRequest 애노테이션이 붙어있는지 까지 검사합니다.
  • 만약 애노테이션이 없다면 리졸브과정을 거치지 않게됩니다.

1.2.4 리졸버 등록하기

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new RoomSearchParamHandlerMethodArgumentResolver());
    }

    ...

}

2. 맺음말

커스텀 애노테이션을 생성하고 메서드 인자를 바인딩하는 방법을 알아보았습니다.
혹시나 오류가 발생한다거나 결과가 올바르지 않다면 중간에 오탈자는 없었는지, 빠진건 없는지 한번 다시 확인해 주시고 이해가 안되는 부분이 있다면 댓글로 남겨주시면 확인하는 대로 답변 달겠습니다.

profile
Hi. I'm Neo

0개의 댓글