스프링에서 이미지 업로드를 할때 Json 데이터를 Multipart로 같이 보내는 경우 Content-Type을 application/json으로 명시해줘주지 않으면 application/octet-stream not supported 예외가 발생한다.

 

multipart/form-data를 받을 경우 @RequestPart 애노테이션을 사용하는데 애노테이션의 주석을 보면 method argument가 String or MultipartFile이 아닌 경우 HttpMessageConverter에서 각 request part의 content-type을 체크한다고 나와있다.

 

@RequestPart 애노테이션이 붙은 파라미터를 처리하는 RequestPartMethodArgumentResolver는 AbstracMessageConverterMethodArgumentResolver를 extends 하고 있다. 그래서 resolveMultipartArgument()로 Multipart를 처리하고 Multipart가 아닌 경우에는 AbstracMessageConverterMethodArgumentResolver의 readWithMessageConverters()에서 messageConvert로 revolve 한다.

 

 

이때 Content-Type이 없는 경우 default로 application/octet-stream가 설정이 된다.

 

application/octet-stream는 보통 unknown binary file에 대해 default로 사용이 된다.

 

 

이제 등록된 MessageConverter를 돌면서 convert가 가능한지 체크를 한다.

 

 ByteArrayHttpMessageConverter가 application/octet-stream을 처리하지만 Controller에서는 byte[]가 아닌 json을 mapping하려고 하기 때문에 supports()에서 false를 반환한다.

 

그래서 json을 mapping해주기 위해 AbstractJackson2HttpMessageConverter를 extends해서 application/octet-stream 타입을 받아서 변환하도록 해줄 수도 있지만 request 전송시 content-type을 명시해주면 해결이 된다.

 

Spring Rest Docs에서 Error Code나 Enum의 문서를 보여주기 위해서는 Custom Snippet을 사용하는 것이 좋다.

기본 템플릿도 수정해서 사용할 수 있는데 일반적으로 optional, default, constraints 같은 column을 추가해서 사용한다. 

 

 

Error Code나 Enum을 나타내기 위한 템플릿은 column이 아예 달라서 새로 추가하는 것이 좋다. restdocs-core의 default snippet 템플릿이 있는 경로대로 테스트의 resources 하위에 test/resources/org/.. 디렉토리를 생성하고 템플릿을 추가하면 된다.

 

 

 

나는 error code를 문서화하기 위해 errorcde-response-fields.snippet을 생성했는데  xxx-response-fields처럼 네이밍을 지키면서 추가하면 된다. 그 다음 Custom Snippet을 적용하기 위해 AbstractFieldsSnippet를 상속받는 클래스를 구현한다.

 

 

추상 메서드인 getContentType(), getContent()를 구현해줘야 하는데 실수로 그냥 지나쳤다가 문서가 제대로 생성 되지 않았다.

getContent()로 body를 꺼내서 veriftContent()를 할 때 length가 0이면 SnippetException body is empty 예외가 발생한다. ㅠㅠ

 

 

그래서 아래와 같이 수정을 해주었다.

 

 

이제 custom snippet, CustomResponseFieldSnippet 클래스를 추가했으면 test 디렉토리 내부에 Enum 값들을 반환해 주는 Controller를 생성하고 해당 Controller에서 문서화하려는 Enum 값들을 반환해 준다

아래와 같이 Rest Docs용으로 생성한 Controller에 요청을 보내면 Enum 값들을 Map으로 반환해 줘서 key는 문서의 row로 사용하고 value는 column으로 사용한다. (column에서 보여줄 status, code, message 등을 DTO로 생성한다,)

 

그러면 아래와 같이 response가 생성이 되는데 key를 fieldWithPath()의 path로 지정하고 type, code, status, message는 attributes에 값을 넣어주면 column으로 보여줄 수 있다.

 

 

기존에는 responseFIelds(fieldWithPath()...)의 형태로 추가를 하면 response-fields.snippet을 찾아서 문서가 생성이 되었다.

 

하지만 이번에는 custom으로 생성을 하기 때문에 아까 생성해 놓은 CustomResponseFieldSnippet의 생성자를 사용해야 한다.

ignoreUndocumentedFields를 true로 줘서 문서화하지 않은 필드는 무시하도록 한다.

 

 

이제 Rest Docs 테스트 코드를 작성하면 되는데 나는 document()의 공통 부분을 restDocsTemplate() 메서드로 빼두었다.

 

createErrorCodeFieldDescriptors()에서 Map<String, ErrorCodeResponse>를 받아서 FieldDescriptor를 생성한다.

 

path에는 Map의 key가 들어가고

attributes에는 Map의 value인 ErrorCodeResponse의 type, code, status, message 필드들을 추가해 주면 된다.

 

 

이렇게 하면 Error Code를 테이블 형태로 보여줄 수 있다. ErrorCode를 하나의 Enum 클래스에서 관리한다면 자동으로 Error Code 동기화가 돼서 좋은 것 같다!

 

 

 

 

[참고]

 

RestDocs - Custom Error Code Enum 문서화

RestDocs - Custom Error Code Enum 문서화 Enum으로 Custom ErrorCode 관리시 Enum 문서 자동화 방법 SpringBoot 버전 2.7.8 restdocs 버전 2.0.7 개요 데브코스에서 프로젝트를 하다 고민이 생겼습니다. 프론트엔드와의

0soo.tistory.com

 

Spring Rest Docs 적용 | 우아한형제들 기술블로그

{{item.name}} 안녕하세요? 우아한형제들에서 정산시스템을 개발하고 있는 이호진입니다. 지금부터 정산시스템 API 문서를 wiki 에서 Spring Rest Docs 로 전환한 이야기를 해보려고 합니다. 1. 전환하는

techblog.woowahan.com

 

Spring REST Docs 적용 및 최적화 하기

Java, JPA, Spring을 주로 다루고 공유합니다.

backtony.github.io

 

 

스프링에서 외부 API 호출을 위해 RestTemplate, WebClient을 제공하는데 스프링 부트 3.2 버전부터 정식으로 RestClient가 추가되었다.

  • RestClient - synchronous client with a fluent API.
  • WebClient - non-blocking, reactive client with fluent API.
  • RestTemplate - synchronous client with template method API.
  • HTTP Interface - annotated interface with generated, dynamic proxy implementation.

WebClient, HttpInterface를 사용하면 직관적이고 깔끔하게 구현할 수 있지만 webflux 의존성을 추가해야 되는 단점이 있다. RestClient는 새로운 동기식 HTTP 클라이언트로 WebClient와 유사한 API로 사용할 수 있다. 이전에는 HttpInterface를 사용하려면  WebClient를 사용했어야 했는데 스프링 6.1부터 HttpExchangeAdapter의 구현체로 RestClientAdapter와 RestTemplateAdapter 둘 다 사용할 수 있게 되었다.

이번에 카카오맵 API를 사용하면서 RestClient, HttpInterface를 사용했는데 RestClient는 create() 혹은 builder를 통해 생성할 수 있고 builder를 사용하면 추가로 baseUrl, header, statusHandler 등 여러 설정을 할 수 있다.

 

 

build() 내부를 보면 설정이 없는 경우 여러 초기화 작업을 해준다. RestTemplate, RestClient는 내부적으로 HTTP 통신을 위해 Apache HttpComponents를 사용한다.

 

 

RestClient 설정시 ClientRequestFactory를 추가할 수 있는데 ClientHttpRequestFactory는 HttpClient, ConnectionManager 등의 설정을 할 수 있다.

RestClient.Builder에서 HttpComponents5 라이브러리를 직접 추가하지 않으면 SimpleClientHttpRequestFactory가 default로 들어간다. SimpleClientHttpRequestFactory를 그대로 사용하면 운영 환경에서 많은 문제가 생길 수 있어서 HttpComponentsClientHttpRequestFactory를 사용하는 것이 좋다.

(원래 HttpComponentsClientHttpRequestFactory가 default인데 라이브러리를 따로 추가하지 않은 경우 발생하는 문제이다.)

 

HttpComponentsClientHttpRequestFactory를 생성할 수가 없어서 주석을 보니 스프링 부트3 버전대에서 HttpComponents 5.1 이상 라이브러리를 추가로 필요로 한다고 나와있다.

 

HttpComponents 5.2.x 버전을 추가하고 다시 보면 default가 HttpComponentsClientHttpRequestFactory로 바뀌고 HttpComponentsClientHttpRequestFactory 또한 직접 생성해서 설정할 수도 있다.

 

하지만..! 스프링 부트 3.2 기준으로 HttpComponents 5.2.x 버전을 추가하면 HttpInterface Proxy 생성이 안 되는 문제가 있어서 최신 버전인 5.3으로 추가해야 한다

 

HttpComponentsClientHttpRequestFactory 내부에는 httpClient의 default ConnectionManager로 PoolingHttpClientConnectionManager가 주입이 되어 있다. PoolingHttpClientConnectionManager는 커넥션 풀을 사용하여 커넥션 관련 설정을 custom 할 수 있다.

 

 

HttpComponentsClientHttpRequestFactory는 상황에 따라 타임 아웃, 커넥션 풀 등의 설정을 통해 변경할 수 있다.

 

 

HttpInterface는 아래와 같이 인터페이스, 애노테이션 기반으로 정의를 해서 직관적으로 사용할 수 있다.

 

 

현재는 직접 해당 인터페이스의 프록시 구현체를 만들어서 Bean으로 등록해주어야 한다.

 

 

이렇게 설정한 외부 API 호출을 테스트하려면 @RestClientTest를 활용할 수 있다. 주의점은 RestTemplate, RestClient를 주입받는 Bean에서 파라미터로 RestTemplateBuilder or RestClient.Builder를 사용해야 한다. 그래서 위에 HttpInterface를 Bean으로 등록할 때 RestClient.Builder를 파라미터로 사용했다.

 

RestClientTest는 최소한의 Context로 테스트를 실행하기 때문에 테스트할 HttpInterface를 value에 추가해 주었다. MockRestServiceServer는 외부 API를 테스트하기 위한 Mock 서버이다

 

 

테스트는 간단하게 예상 Response 등을 설정해 주고 mockServer에 설정을 해준 뒤에 예상 경로로 요청이 가는지, 바인딩이 제대로 되는지 등의 테스트를 검증한다.

 

 

RestClient는 custom 메시지 컨버터를 추가할 수 있지만 default로 등록된 컨버터 중 MappingJackson2HttpMessageConverter가 json 응답을 객체에 바인딩해 준다.

 

 

[참고]

 

Spring Boot에서 외부 API 테스트하기

안녕하세요? 이번 시간엔 Spring Boot의 @RestClientTest 예제를 진행해보려고 합니다. 모든 코드는 Github에 있기 때문에 함께 보시면 더 이해하기 쉬우실 것 같습니다. 1. 문제 상황 예를 들어 외부 API를

jojoldu.tistory.com

 

RestTemplate 사용시 주의사항 - Incheol's TECH BLOG

ResponseEntity response = restTemplate.exchange(" ", HttpMethod.GET, requestEntity, String.class);

incheol-jung.gitbook.io

 

Apache HttpComponent 제대로 사용하기

Java 어플리케이션에서 일반적으로 http호출을 할때 Apache HttpClient를 사용하며 http client가 버전업을 하여 Apache HttpComponent로 변경된것은 아마 다들 알고있는 사항일것입니다. (아주 옛날에 바꼈으니

inyl.github.io

 

 

IntelliJ 디버깅을 할 때 JPA의 지연 로딩 옵션이 실행 되면서 쿼리가 나가는 경우가 있다. 아래 예시에서 장소(Spot)는 여러개의 방명록(Post)을 가지고 있고 @OneToMany 매핑이 되어 있다. 먼저 findAll()로 조회를 한 상태에서는 select 쿼리만 나간 상태이다.

 

 

이제 Spot 리스트에서 Spot의 내부를 열어보는순간 JPA의 BatchSize로 설정해놓은 size만큼 in 쿼리가 실행이 된다.

 

 

IntelliJ 디버깅에서 지연 로딩 쿼리가 발생하는 것으로 인해서 혼동이 올 수 있다. IntelliJ에서는 이를 옵션으로 설정할 수 있다.

Debugger 창의 variables 창 부분을 우클릭하면 Mute Renderers, Customize Data View 옵션이 있다. Mute Renderers에서 전체 체크를 하거나 Customize Data View에서 개별적으로 설정을 할 수 있다.

 

 

Mute Renderers를 체크하게 되면 디버거에서 모든 값들을 불러와서 보여주지 않고 제한적으로 보여준다. 이제 posts의 size()를 클릭하면 size()를 불러오면서 지연 로딩 쿼리가 나가게 된다.

 

 

위에 posts를 보면 인스턴스가 PersistentBag으로 되어 있는데 이는 JPA에서 Collection을 감싼 프록시 클래스다.

 

PersistentBag의 size()를 호출하면 내부적으로 실제 컬렉션이 초기화가 된다.

 

readSize() 내부를 보면 initialize()에서 initializeCollection()을 호출하고 EventListner로 초기화를 한다.

 

생각해보면 @OneToMany 관계인 Colleciton이 지연 로딩 상태에서 size() 또는 get()으로 List의 내부 요소에 대해 알려면 쿼리가 나가는 것이 당연하다. 

 

[참고]

https://intellij-support.jetbrains.com/hc/en-us/community/posts/4411482324114-How-could-intellij-changed-Hibernate-lazyload-rules-with-breakpoint-

https://kimcoder.tistory.com/503?category=964983

 

https://www.baeldung.com/java-debug-interface

 

+ Recent posts