![@RequestParam MultiValueMap의 내부 List 타입 불일치 문제 해결](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctG4I2%2FbtsAc3NMSYG%2F8ClnsDxBlkrzb1fwnr7S3k%2Fimg.png)
@RequestParam MultiValueMap의 내부 List 타입 불일치 문제 해결
사이드 프로젝트의 주문 서비스의 주문요청 처리 기능을 개발하며 발생한 이슈 해결과정입니다.
주문 서비스의 주문요청 처리 프로세스는 다른 서비스에서 여러가지 정보들을 조회하여 검증하는 작업을 거치게 됩니다.
여태까지 해왔던 모놀리틱한 구조의 프로젝트에서는 주문 서비스에서 회원의 정보 조회도 가능하였으나, 각각의 서비스가 철저히 분리된 마이크로 서비스 아키텍처를 적용하였기에 서비스간 통신을 하기 위해서는 해당 서비스에 API 호출을 하는 방식을 이용해야 했습니다. Spring 프로젝트에서 API 호출을 하는 방법으로는 RestTemplate
와 OpenFeign
를 사용하는 방법이 있는데 저는 비교적 쉽게 사용할 수 있는 OpenFeign
을 사용하였습니다.
이제 사용할 기술은 정해졌고, 상품 서비스에 주문된 상품목록들의 가격정보를 조회하기 위하여 주문된 상품 옵션 ID의 리스트를 쿼리파라미터로 보내주면, 상품들의 가격 정보를 반환해주는 API를 호출하기로 결정하였습니다.
같은 Key를 가지는 여러 ID 목록을 쿼리파라미터로 보내야 했기에 데이터를 저장하면 덮어씌워지는 HashMap
자료구조는 사용할 수 없어서, Key의 Value값을 List의 형태로 저장할 수 있는 MultiValueMap
자료구조를 사용하기로 하고, @SpringQueryMap
어노테이션을 사용하여 쿼리파라미터를 전달하였습니다.
아래는 FeignClient
를 사용하여 상품 서비스에 API 호출을 하는 코드입니다.
주문 서비스의 ProductServiceClient
@FeignClient(name = "product-service", url = "http://127.0.0.1:8087/api/v1/products")
public interface ProductServiceClient {
@GetMapping("/prices")
ApiResponse<List<ProductPriceResponse>> getOrderPriceInfo(@SpringQueryMap MultiValueMap<String, Long> params);
}
그리고, 해당 요청을 처리하기위한 상품 서비스의 컨트롤러 부분을 다음과 같이 작성하였습니다.
상품 서비스의 ProductController
@GetMapping("/prices")
public ApiResponse<List<ProductPriceResponse>> getOrderProductsInfo(@RequestParam MultiValueMap<String, Long> productOptionIdMap) {
List<Long> productOptionIds = productOptionIdMap.get("productOptionId");
List<ProductPriceResponse> response = productService.getOrderProductsInfo(productOptionIds);
return ApiResponse.ok(response);
}
쿼리파라미터로 받은 MultiValueMap에서 상품 옵션 ID 값 리스트를 꺼내어 ProductService에 넘겨주는 모습입니다.
상품 시스템 - ProductService
// 상품 옵션 ID 리스트를 받아서 상품 가격 정보 조회
public List<ProductPriceResponse> getOrderProductsInfo(List<Long> productOptionIds) {
List<ProductOption> productOptions = productOptionRepository.findAllByIdIn(productOptionIds);
return productOptions.stream()
.map(po -> ProductPriceResponse.from(po))
.collect(Collectors.toList());
}
위는 컨트롤러에서 받은 상품 옵션 ID의 리스트를 받아서 상품 가격 정보들을 데이터베이스에서 In절로 조회화여 반환해주는 코드입니다.
위와 같이 코드를 작성하고, 상품 서비스와 주문 서비스 둘다 정상적으로 애플리케이션이 실행되어지는 것 까지 확인한 다음, 해당 API가 의도한 대로 정상적으로 동작을 하는지 확인하기 위하여 Postman으로 테스트 주문 요청을 보냈고, 이때까지 저는 몰랐습니다.
MultiValueMap의 저주가 시작되었음을 . . .
쿼리를 실행할 때 타입이 안맞다고 하는데, 분명히 서비스에서 List<Long>
타입을 Repository로 넘겨주었기에 무언가 이상했습니다.
눈으로 직접 값을 확인하기 위하여 포스트맨으로 해당 API에 상품 옵션 ID 목록을 1, 2, 3, 13로 지정하여 요청을 보내고, Service 레이어에서 출력해봤습니다.
의도한대로 값을 잘 받아온모습입니다. 그런데 이 값을 바로 Repository로 넘기게 되면 동작을 안하게 되니, [1, 2, 3, 13]을 직접 리스트로 만들어서 레포지토리에 전달해보았습니다.
변경 코드
실행 결과
컨트롤러에서 받아온 파라미터 대신, 직접 만든 List를 Repository에 인자로 인터페이스니까 인자로 받아온 productsOptionIds
구현 클래스는 다른가? 싶어서 전부 프린트해보았습니다.
수정 코드
실행 결과
List.of()
를 통하여 새롭게 만든 ids는 ImmutableCollections라는 구현체였고, 인자로 받아온 productOptionIds
는 ArrayList가 구현체 임을 확인할 수 있었습니다.
이 부분이 문제가 아닌것은 인지하고 있었으나, 혹시나 싶어 ArrayList로 만들어서 다시 실행해보았습니다.
실행 코드
결과
예상대로 이 부분이 문제가 아닌것을 확인할 수 있었고, 이때부터는 멘붕이 시작되었습니다.
멘붕이 느껴 지는 삽질코드들..
위와 같이 무한 삽질 코드를 작성하다 불현듯 컨트롤러에서 받아온 MultiValueMap
이 의심스러워졌고 컨트롤러의 코드부분을 다시 살펴보았습니다.
상품 서비스의 ProductController
@GetMapping("/prices")
public ApiResponse<List<ProductPriceResponse>> getOrderProductsInfo(@RequestParam MultiValueMap<String, Long> productOptionIdMap) {
List<Long> productOptionIds = productOptionIdMap.get("productOptionId");
List<ProductPriceResponse> response = productService.getOrderProductsInfo(productOptionIds);
return ApiResponse.ok(response);
}
흐름을 잘 보면, MultiValueMap에서 get(”key”)를 하여 얻은 결과를 서비스에 넘겨주고 있습니다.
MultiValueMap에서 get()을 결과값은 얕은복사가 되어 반환이 되는가 싶어서 구글링을 해보니 get()을 호출한다면 깊은복사를 하여 return해주어 MultiValueMap에 들어있는 값에는 영향이 없다고 합니다.
❗ 물론 얕은복사를 하여 service를 호출한다고 해도 매개변수, 지역변수는 새롭게 메모리가 할당되기에 상관은 없을것입니다.
깊은복사와 얕은복사의 문제도 아닌데 왜 이럴까 싶어서 다시 한번 값 확인을 해보려고 이번엔 Controller에서 출력을 해보았습니다.
실행
결과
갑자기 ClassCastException이 발생하여 뭔가 이상하다싶어서 열심히 구글링과 고민을 하여 다음과 같은 결론을 얻게 되었습니다.
💡 MultiValueMap 자료구조로 바인딩 될 때, Controller에서는 MultiValueMap<String, Long> 이라고 제네릭 타입을 명시하였으나 제네릭이 String이 아니라면 Object로 한번 감싸서 값을 넣어주기 때문에, Repository에서는 List의 값을 조작하였을 때 타입이 맞지 않는 오류가 발생한다.
위에서 도출한 결론을 토대로 Controller에서 MultiValueMap의 제네릭을 <String, String>으로 고치고, 상품의 ID 목록을 꺼낼 때 한번 Long.parseLong()을 통하여 변환해준 뒤 다음과 같이 정상적으로 실행되는 것을 확인할 수 있게되었습니다.
최종 코드
실행 결과
'트러블 슈팅' 카테고리의 다른 글
EC2 메모리 부족 빌드 실패 해결 (Gradle build daemon disappered unexpectedly) (0) | 2024.08.07 |
---|---|
(Spring) MySQL 격리수준에 따른 문제 해결 (2) | 2024.03.28 |
개발을 하며 만났던 문제들과 해결 과정, 공부한 내용 등을 기록합니다.
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!