From 32beb10a2460dc4decf56bfed0fe8858f4f9caab Mon Sep 17 00:00:00 2001 From: limju Date: Wed, 13 Sep 2023 17:53:49 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Jackson=20(ObjectMapper)=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/config/application.yml | 7 -- .../src/main/resources/config/application.yml | 7 -- mens-core/README.md | 4 +- mens-core/pom.xml | 2 + .../xit/biz/ens/model/cmm/CmmEnsFileDTO.java | 4 +- .../biz/ens/model/kakao/KkopayDocAttrDTO.java | 10 +- .../biz/ens/model/kakao/KkopayDocBulkDTO.java | 4 +- .../xit/biz/ens/model/kakao/KkopayDocDTO.java | 4 +- .../kr/xit/biz/ens/model/nice/NiceCiDTO.java | 36 +++--- .../config/support/CustomJacksonConfig.java | 75 +++++++++++ .../config/support/WebClientConfig.java | 118 +++++++++++------- .../core/spring/util/ApiWebClientUtil.java | 51 +++++++- .../kr/xit/core/spring/util/SpringUtils.java | 5 + .../xit/core/support/utils/ConvertHelper.java | 3 +- .../kr/xit/core/support/utils/JsonUtils.java | 52 ++++---- 15 files changed, 266 insertions(+), 116 deletions(-) create mode 100644 mens-core/src/main/java/kr/xit/core/spring/config/support/CustomJacksonConfig.java diff --git a/mens-api/src/main/resources/config/application.yml b/mens-api/src/main/resources/config/application.yml index 7072751..9e1bead 100644 --- a/mens-api/src/main/resources/config/application.yml +++ b/mens-api/src/main/resources/config/application.yml @@ -77,13 +77,6 @@ spring: data-source-properties: rewriteBatchedStatements: true - # @JsonInclude(JsonInclude.Include.NON_EMPTY) 설정 - jackson: - default-property-inclusion=non_null: non_null - - - - logging: level: root: error diff --git a/mens-batch/src/main/resources/config/application.yml b/mens-batch/src/main/resources/config/application.yml index 30b9ef6..5646ebe 100644 --- a/mens-batch/src/main/resources/config/application.yml +++ b/mens-batch/src/main/resources/config/application.yml @@ -77,13 +77,6 @@ spring: data-source-properties: rewriteBatchedStatements: true - # @JsonInclude(JsonInclude.Include.NON_EMPTY) 설정 - jackson: - default-property-inclusion=non_null: non_null - - - - logging: level: root: error diff --git a/mens-core/README.md b/mens-core/README.md index c69030b..ec25fe4 100644 --- a/mens-core/README.md +++ b/mens-core/README.md @@ -142,9 +142,9 @@ json <-> java class 변환 적용 */ ``` ## Json 데이타 null 필드 제외 -@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonInclude(JsonInclude.Include.NON_NULL) ```text - @JsonInclude(JsonInclude.Include.NON_EMPTY) + @JsonInclude(JsonInclude.Include.NON_NULL) @JsonNaming(PropertyNamingStrategies.UpperSnakeCaseStrategy.class) public static class RequestDataHeader { diff --git a/mens-core/pom.xml b/mens-core/pom.xml index a6a475d..fa3f123 100644 --- a/mens-core/pom.xml +++ b/mens-core/pom.xml @@ -290,6 +290,7 @@ 2.9.0 + com.fasterxml.jackson.dataformat jackson-dataformat-xml @@ -302,6 +303,7 @@ com.fasterxml.jackson.datatype jackson-datatype-jsr310 + commons-configuration diff --git a/mens-core/src/main/java/kr/xit/biz/ens/model/cmm/CmmEnsFileDTO.java b/mens-core/src/main/java/kr/xit/biz/ens/model/cmm/CmmEnsFileDTO.java index c1b0cc3..8e8656f 100644 --- a/mens-core/src/main/java/kr/xit/biz/ens/model/cmm/CmmEnsFileDTO.java +++ b/mens-core/src/main/java/kr/xit/biz/ens/model/cmm/CmmEnsFileDTO.java @@ -33,7 +33,7 @@ public class CmmEnsFileDTO { @NoArgsConstructor @AllArgsConstructor @SuperBuilder - @JsonInclude(JsonInclude.Include.NON_EMPTY) + @JsonInclude(JsonInclude.Include.NON_NULL) public static class FmcExcelUpload { //----------------------------------------------------------------------------------- @@ -95,7 +95,7 @@ public class CmmEnsFileDTO { @NoArgsConstructor @AllArgsConstructor @SuperBuilder - @JsonInclude(JsonInclude.Include.NON_EMPTY) + @JsonInclude(JsonInclude.Include.NON_NULL) public static class FmcExcel { //----------------------------------------------------------------------------------- diff --git a/mens-core/src/main/java/kr/xit/biz/ens/model/kakao/KkopayDocAttrDTO.java b/mens-core/src/main/java/kr/xit/biz/ens/model/kakao/KkopayDocAttrDTO.java index 33532c6..0cb4f61 100644 --- a/mens-core/src/main/java/kr/xit/biz/ens/model/kakao/KkopayDocAttrDTO.java +++ b/mens-core/src/main/java/kr/xit/biz/ens/model/kakao/KkopayDocAttrDTO.java @@ -36,7 +36,7 @@ public class KkopayDocAttrDTO { @NoArgsConstructor @AllArgsConstructor @SuperBuilder - @JsonInclude(JsonInclude.Include.NON_EMPTY) + @JsonInclude(JsonInclude.Include.NON_NULL) public static class Send implements IApiResponse { /** * 발송할 문서의 제목 : 필수 @@ -91,7 +91,7 @@ public class KkopayDocAttrDTO { @NoArgsConstructor @AllArgsConstructor @SuperBuilder - @JsonInclude(JsonInclude.Include.NON_EMPTY) + @JsonInclude(JsonInclude.Include.NON_NULL) public static class Receiver { /** * 받는이 CI @@ -137,7 +137,7 @@ public class KkopayDocAttrDTO { @Data @SuperBuilder @NoArgsConstructor - @JsonInclude(JsonInclude.Include.NON_EMPTY) + @JsonInclude(JsonInclude.Include.NON_NULL) public static class Property { /** * 본인인증 후 사용자에게 보여줄 웹페이지 주소 : 필수 @@ -185,7 +185,7 @@ public class KkopayDocAttrDTO { @SuperBuilder @NoArgsConstructor @AllArgsConstructor - @JsonInclude(JsonInclude.Include.NON_EMPTY) + @JsonInclude(JsonInclude.Include.NON_NULL) public static class DocumentBinderUuid implements IApiResponse { /** * 카카오페이 문서식별번호(max:40) - 필수 @@ -200,7 +200,7 @@ public class KkopayDocAttrDTO { @SuperBuilder @NoArgsConstructor @AllArgsConstructor - @JsonInclude(JsonInclude.Include.NON_EMPTY) + @JsonInclude(JsonInclude.Include.NON_NULL) public static class DocStatus implements IApiResponse { /** *
diff --git a/mens-core/src/main/java/kr/xit/biz/ens/model/kakao/KkopayDocBulkDTO.java b/mens-core/src/main/java/kr/xit/biz/ens/model/kakao/KkopayDocBulkDTO.java
index 33979f1..80271b4 100644
--- a/mens-core/src/main/java/kr/xit/biz/ens/model/kakao/KkopayDocBulkDTO.java
+++ b/mens-core/src/main/java/kr/xit/biz/ens/model/kakao/KkopayDocBulkDTO.java
@@ -85,7 +85,7 @@ public class KkopayDocBulkDTO extends KkopayDocAttrDTO {
     @SuperBuilder
     @NoArgsConstructor
     @AllArgsConstructor
-    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    @JsonInclude(JsonInclude.Include.NON_NULL)
     public static class BulkSendRes {
         /**
          * 문서 아이디(외부)(max=40) - 필수
@@ -148,7 +148,7 @@ public class KkopayDocBulkDTO extends KkopayDocAttrDTO {
     @SuperBuilder
     @NoArgsConstructor
     @AllArgsConstructor
-    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    @JsonInclude(JsonInclude.Include.NON_NULL)
     public static class BulkStatus {
         /**
          * 카카오페이 문서식별 번호(max:40) - 필수
diff --git a/mens-core/src/main/java/kr/xit/biz/ens/model/kakao/KkopayDocDTO.java b/mens-core/src/main/java/kr/xit/biz/ens/model/kakao/KkopayDocDTO.java
index 0181e0a..1c6eaed 100644
--- a/mens-core/src/main/java/kr/xit/biz/ens/model/kakao/KkopayDocDTO.java
+++ b/mens-core/src/main/java/kr/xit/biz/ens/model/kakao/KkopayDocDTO.java
@@ -106,7 +106,7 @@ public class KkopayDocDTO extends KkopayDocAttrDTO {
     @SuperBuilder
     @NoArgsConstructor
     @AllArgsConstructor
-    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    @JsonInclude(JsonInclude.Include.NON_NULL)
     public static class ValidTokenResponse implements IApiResponse {
         /**
          * 토큰상태값(성공시 USED) : 필수
@@ -192,7 +192,7 @@ public class KkopayDocDTO extends KkopayDocAttrDTO {
     @NoArgsConstructor
     @AllArgsConstructor
     @ToString
-    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    @JsonInclude(JsonInclude.Include.NON_NULL)
     @EqualsAndHashCode(callSuper = true)
     public static class OneTimeToken extends ValidTokenRequest {
         /**
diff --git a/mens-core/src/main/java/kr/xit/biz/ens/model/nice/NiceCiDTO.java b/mens-core/src/main/java/kr/xit/biz/ens/model/nice/NiceCiDTO.java
index b207433..103f1cd 100644
--- a/mens-core/src/main/java/kr/xit/biz/ens/model/nice/NiceCiDTO.java
+++ b/mens-core/src/main/java/kr/xit/biz/ens/model/nice/NiceCiDTO.java
@@ -2,9 +2,7 @@ package kr.xit.biz.ens.model.nice;
 
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonInclude.Include;
-import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.databind.PropertyNamingStrategies;
-import com.fasterxml.jackson.databind.PropertyNamingStrategy;
 import com.fasterxml.jackson.databind.annotation.JsonNaming;
 import io.swagger.v3.oas.annotations.media.Schema;
 import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
@@ -12,9 +10,6 @@ import java.io.Serializable;
 import java.util.HashMap;
 import java.util.Map;
 import javax.validation.Valid;
-import javax.validation.constraints.Max;
-import javax.validation.constraints.Min;
-import javax.validation.constraints.NotBlank;
 import javax.validation.constraints.Size;
 import kr.xit.biz.common.AuditFields;
 import kr.xit.core.model.IApiResponse;
@@ -25,7 +20,6 @@ import lombok.Builder.Default;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 import lombok.experimental.SuperBuilder;
-import org.hibernate.validator.constraints.Length;
 
 /**
  * 
@@ -58,7 +52,7 @@ public class NiceCiDTO {
     @NoArgsConstructor
     @AllArgsConstructor
     @Builder
-    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    @JsonInclude(JsonInclude.Include.NON_NULL)
     @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
     public static class TokenRequest {
         /**
@@ -90,7 +84,7 @@ public class NiceCiDTO {
     @NoArgsConstructor
     @AllArgsConstructor
     @SuperBuilder
-    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    @JsonInclude(JsonInclude.Include.NON_NULL)
     public static class TokenResponse implements IApiResponse {
         @Schema(requiredMode = RequiredMode.REQUIRED)
         @Valid
@@ -111,7 +105,7 @@ public class NiceCiDTO {
     @NoArgsConstructor
     @AllArgsConstructor
     @SuperBuilder
-    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    @JsonInclude(JsonInclude.Include.NON_NULL)
     public static class TokenRevokeResponse implements IApiResponse {
 
         @Schema(requiredMode = RequiredMode.REQUIRED)
@@ -134,7 +128,7 @@ public class NiceCiDTO {
     @NoArgsConstructor
     @AllArgsConstructor
     @SuperBuilder
-    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    @JsonInclude(JsonInclude.Include.NON_NULL)
     @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
     public static class TokenResDataBody {
         //-----------------------------------------------------------------------
@@ -210,7 +204,7 @@ public class NiceCiDTO {
     @NoArgsConstructor
     @AllArgsConstructor
     @SuperBuilder
-    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    @JsonInclude(JsonInclude.Include.NON_NULL)
     public static class PublickeyRequest {
         @Schema(requiredMode = RequiredMode.REQUIRED)
         @Valid
@@ -231,8 +225,8 @@ public class NiceCiDTO {
     @NoArgsConstructor
     @AllArgsConstructor
     @SuperBuilder
-    @JsonInclude(JsonInclude.Include.NON_EMPTY)
-    public static class PublickeyResponse implements IApiResponse {
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    public static class PublickeyResponse { //implements IApiResponse {
         @Schema(requiredMode = RequiredMode.REQUIRED)
         @Valid
         private ResponseDataHeader dataHeader;
@@ -253,7 +247,7 @@ public class NiceCiDTO {
     @NoArgsConstructor
     @AllArgsConstructor
     @SuperBuilder
-    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    @JsonInclude(JsonInclude.Include.NON_NULL)
     @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
     public static class PublickeyReqDataBody {
         /**
@@ -275,7 +269,7 @@ public class NiceCiDTO {
     @NoArgsConstructor
     @AllArgsConstructor
     @SuperBuilder
-    @JsonInclude(Include.NON_EMPTY)
+    @JsonInclude(Include.NON_NULL)
     @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
     public static class PublickeyResDataBody {
         /**
@@ -354,7 +348,7 @@ public class NiceCiDTO {
     @NoArgsConstructor
     @AllArgsConstructor
     @SuperBuilder
-    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    @JsonInclude(JsonInclude.Include.NON_NULL)
     public static class SymmetrickeyRegRequest {
         @Schema(requiredMode = RequiredMode.REQUIRED)
         @Valid
@@ -375,7 +369,7 @@ public class NiceCiDTO {
     @NoArgsConstructor
     @AllArgsConstructor
     @SuperBuilder
-    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    @JsonInclude(JsonInclude.Include.NON_NULL)
     public static class SymmetrickeyRegResponse implements IApiResponse {
         @Schema(requiredMode = RequiredMode.REQUIRED)
         @Valid
@@ -397,7 +391,7 @@ public class NiceCiDTO {
     @NoArgsConstructor
     @AllArgsConstructor
     @SuperBuilder
-    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    @JsonInclude(JsonInclude.Include.NON_NULL)
     @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
     public static class SymmetrickeyRegReqDataBody {
         /**
@@ -438,7 +432,7 @@ public class NiceCiDTO {
     @NoArgsConstructor
     @AllArgsConstructor
     @SuperBuilder
-    @JsonInclude(Include.NON_EMPTY)
+    @JsonInclude(Include.NON_NULL)
     @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
     public static class SymmetrickeyRegResDataBody {
         /**
@@ -506,7 +500,7 @@ public class NiceCiDTO {
     @NoArgsConstructor
     @AllArgsConstructor
     @SuperBuilder
-    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    @JsonInclude(JsonInclude.Include.NON_NULL)
     @JsonNaming(PropertyNamingStrategies.UpperSnakeCaseStrategy.class)
     public static class RequestDataHeader {
 
@@ -540,7 +534,7 @@ public class NiceCiDTO {
     @NoArgsConstructor
     @AllArgsConstructor
     @SuperBuilder
-    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    @JsonInclude(JsonInclude.Include.NON_NULL)
     @JsonNaming(PropertyNamingStrategies.UpperSnakeCaseStrategy.class)
     public static class ResponseDataHeader {
         /**
diff --git a/mens-core/src/main/java/kr/xit/core/spring/config/support/CustomJacksonConfig.java b/mens-core/src/main/java/kr/xit/core/spring/config/support/CustomJacksonConfig.java
new file mode 100644
index 0000000..d251612
--- /dev/null
+++ b/mens-core/src/main/java/kr/xit/core/spring/config/support/CustomJacksonConfig.java
@@ -0,0 +1,75 @@
+package kr.xit.core.spring.config.support;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.cfg.CoercionAction;
+import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import kr.xit.core.spring.util.SpringUtils;
+import kr.xit.core.support.utils.ConvertHelper;
+import kr.xit.core.support.utils.JsonUtils;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+/**
+ * 
+ * description : Jackson(ObjectMapper) 설정 재정의
+ *               모든 ObjectMapper 사용시 이 설정이 적용되도록 해야 한다.
+ *               - Cannot coerce empty String("") to ...
+ *                 value(but could if coercion was enabled using `CoercionConfig`) 에러 방어
+ * packageName : kr.xit.core.spring.config.support
+ * fileName    : CustomJacksonConfig
+ * author      : julim
+ * date        : 2023-09-13
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2023-09-13    julim       최초 생성
+ *
+ * 
+ * @see SpringUtils#getObjectMapper() + * @see JsonUtils + * @see ConvertHelper + * @see WebClientConfig + */ +@Configuration +public class CustomJacksonConfig { + + /** + *
+     * 기본 설정에 override
+     * Cannot coerce empty String("") to ...
+     * value(but could if coercion was enabled using `CoercionConfig`) 에러 방어
+     * 
+ * + * @return + */ + @Bean + @Primary + public ObjectMapper objectMapper() { + ObjectMapper om = new ObjectMapper() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .setSerializationInclusion(Include.NON_NULL) + .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, false) + .registerModule(new JavaTimeModule()); + + om.coercionConfigDefaults() + .setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull); +// om.coercionConfigFor(LogicalType.Enum) +// .setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull); +// om.coercionConfigFor(LogicalType.POJO) +// .setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull); + + return om; + } +} diff --git a/mens-core/src/main/java/kr/xit/core/spring/config/support/WebClientConfig.java b/mens-core/src/main/java/kr/xit/core/spring/config/support/WebClientConfig.java index e6ad12a..0800fd9 100644 --- a/mens-core/src/main/java/kr/xit/core/spring/config/support/WebClientConfig.java +++ b/mens-core/src/main/java/kr/xit/core/spring/config/support/WebClientConfig.java @@ -1,16 +1,24 @@ package kr.xit.core.spring.config.support; +import com.fasterxml.jackson.databind.ObjectMapper; import io.netty.channel.ChannelOption; import io.netty.handler.timeout.ReadTimeoutHandler; import io.netty.handler.timeout.WriteTimeoutHandler; import java.time.Duration; import java.util.concurrent.TimeUnit; +import kr.xit.core.spring.util.error.ClientError; +import kr.xit.core.spring.util.error.ServerError; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.codec.LoggingCodecSupport; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.client.WebClient; @@ -35,6 +43,7 @@ import reactor.netty.resources.ConnectionProvider; *
*/ @Slf4j +@RequiredArgsConstructor @Configuration public class WebClientConfig { @@ -43,57 +52,40 @@ public class WebClientConfig { @Value("${contract.connection.readTimeout:5000}") private int readTimeout; + private final ObjectMapper objectMapper; DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(); - // HttpClient 총 연결 시간 - HttpClient httpClient = HttpClient.create() - .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout) - .responseTimeout(Duration.ofMillis(this.connectTimeout)) - .doOnConnected(conn -> - conn.addHandlerLast(new ReadTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS)) - .addHandlerLast(new WriteTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS))); - - /** * setEncodingMode : GET 요청의 파라미터 셋팅을 하기 위한 URI 템플릿의 인코딩을 위한 설정 * @return */ @Bean public WebClient webClient() { - // 256KB 보다 큰 HTTP 메시지를 처리 시도 → DataBufferLimitException 에러 발생 방어 - ExchangeStrategies es = ExchangeStrategies.builder() - .codecs(configurer -> { - configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024); - //configurer.customCodecs().register(new Jackson2JsonDecoder()); - //configurer.customCodecs().register(new Jackson2JsonEncoder()); - }) - .build(); - - //FIXME::rest call async 로깅 - // ExchangeStrategies를 통해 setEnableLoggingRequestDetails(true)로 설정 - // boot에서 로깅 org.springframework.web.reactive.function.client.ExchangeFunctions: DEBUG 하여 활성 - es.messageWriters() - .stream() - .filter(LoggingCodecSupport .class::isInstance) - .forEach(writer -> ((LoggingCodecSupport)writer).setEnableLoggingRequestDetails(true)); - - - factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY); + return WebClient.builder() .uriBuilderFactory(factory) - .clientConnector(new ReactorClientHttpConnector(httpClient)) + .clientConnector(new ReactorClientHttpConnector(defaultHttpClient())) .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)) - .exchangeStrategies(es) + .exchangeStrategies(defaultExchangeStrategies()) .filters(exchangeFilterFunctions -> { - exchangeFilterFunctions.add(logRequest()); - exchangeFilterFunctions.add(logResponse()); - //TODO::에러발생시 점검필요 - //exchangeFilterFunctions.add(errorHandler()); + exchangeFilterFunctions.add(requestFilter()); + exchangeFilterFunctions.add(responseFilter()); }) .build(); } + + @Bean + public HttpClient defaultHttpClient() { + return HttpClient.create(connectionProvider()) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout) + .responseTimeout(Duration.ofMillis(this.connectTimeout)) + .doOnConnected(conn -> + conn.addHandlerLast(new ReadTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS)) + .addHandlerLast(new WriteTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS))); + } + /** * maxConnections : connection pool의 갯수 * pendingAcquireTimeout : 커넥션 풀에서 커넥션을 얻기 위해 기다리는 최대 시간 @@ -107,12 +99,33 @@ public class WebClientConfig { .maxConnections(100) .pendingAcquireTimeout(Duration.ofMillis(0)) .pendingAcquireMaxCount(-1) - .maxIdleTime(Duration.ofMillis(1000L)) + .maxIdleTime(Duration.ofMillis(2000L)) .build(); } + @Bean + public ExchangeStrategies defaultExchangeStrategies() { + // 256KB 보다 큰 HTTP 메시지를 처리 시도 → DataBufferLimitException 에러 발생 방어 + ExchangeStrategies es = ExchangeStrategies.builder() + .codecs(config -> { + config.defaultCodecs().maxInMemorySize(2 * 1024 * 1024); + config.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON)); + config.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON)); + }) + .build(); - private ExchangeFilterFunction logRequest() { + //FIXME::rest call async 로깅 + // ExchangeStrategies를 통해 setEnableLoggingRequestDetails(true)로 설정 + // boot에서 로깅 org.springframework.web.reactive.function.client.ExchangeFunctions: DEBUG 하여 활성 + es.messageWriters() + .stream() + .filter(LoggingCodecSupport .class::isInstance) + .forEach(writer -> ((LoggingCodecSupport)writer).setEnableLoggingRequestDetails(true)); + + return es; + } + + private ExchangeFilterFunction requestFilter() { return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> { if (log.isDebugEnabled()) { StringBuilder sb = new StringBuilder("\n>>>>>>>>>> Http Rest Request <<<<<<<<<<<<<\n"); @@ -125,13 +138,34 @@ public class WebClientConfig { }); } - private ExchangeFilterFunction logResponse() { + /** + * reponse logging && error Handling + * @return + */ + private ExchangeFilterFunction responseFilter() { return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { - StringBuilder sb = new StringBuilder("\n>>>>>>>>>> Http Rest Response <<<<<<<<<<<<<\n"); - clientResponse.headers() - .asHttpHeaders() - .forEach((name, values) -> values.forEach(value -> sb.append(name).append(": ").append(value).append("\n"))); - log.debug(sb.toString()); + + HttpStatus status = clientResponse.statusCode(); + + if(clientResponse.statusCode().is4xxClientError()) { + return clientResponse.bodyToMono(String.class) + .flatMap(errorBody -> Mono.error(new ClientError(status, errorBody))); + + } else if(clientResponse.statusCode().is5xxServerError()) { + return clientResponse.bodyToMono(String.class) + .flatMap(errorBody -> Mono.error(new ServerError(status, errorBody))); + + } + + if(log.isDebugEnabled()) { + StringBuilder sb = new StringBuilder( + "\n>>>>>>>>>> Http Rest Response <<<<<<<<<<<<<\n"); + clientResponse.headers() + .asHttpHeaders() + .forEach((name, values) -> values.forEach( + value -> sb.append(name).append(": ").append(value).append("\n"))); + log.debug(sb.toString()); + } return Mono.just(clientResponse); }); } diff --git a/mens-core/src/main/java/kr/xit/core/spring/util/ApiWebClientUtil.java b/mens-core/src/main/java/kr/xit/core/spring/util/ApiWebClientUtil.java index 5ceb62a..6752cdf 100644 --- a/mens-core/src/main/java/kr/xit/core/spring/util/ApiWebClientUtil.java +++ b/mens-core/src/main/java/kr/xit/core/spring/util/ApiWebClientUtil.java @@ -12,6 +12,8 @@ import kr.xit.core.spring.util.error.ClientError; import kr.xit.core.spring.util.error.ErrorParse; import kr.xit.core.spring.util.error.ServerError; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -19,6 +21,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClientResponseException; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Mono; @@ -40,6 +43,7 @@ import reactor.core.publisher.Mono; * @see ClientError * @see ServerError */ +@Slf4j @Component @RequiredArgsConstructor public class ApiWebClientUtil { @@ -66,7 +70,7 @@ public class ApiWebClientUtil { return webClientConfig.webClient().method(HttpMethod.POST) .uri(url) .headers(httpHeaders -> getHeaders(httpHeaders, headerMap)) - .bodyValue(requestDto) + .bodyValue(Objects.requireNonNullElse(requestDto, "")) .retrieve() .onStatus( status -> status.is4xxClientError() || status.is5xxServerError(), @@ -103,10 +107,51 @@ public class ApiWebClientUtil { .uri(url) .headers(httpHeaders -> getHeaders(httpHeaders, headerMap)) .bodyValue(Objects.requireNonNullElse(body, "")) - .exchangeToMono(res -> res.bodyToMono(rtnClzz)) + .exchangeToMono(res -> res.bodyToMono(rtnClzz) + .map(dto -> { + if (res.statusCode().is2xxSuccessful()) { + log.info("API 요청에 성공했습니다."); + return dto; + } + + if (res.statusCode().is4xxClientError()) { + log.error("API 요청 중 4xx 에러가 발생했습니다. 요청 데이터를 확인해주세요."); + throw BizRuntimeException.create(String.format("4xx 외부 요청 오류. statusCode: %s, response: %s, header: %s", res.rawStatusCode(), res.bodyToMono(String.class), res.headers().asHttpHeaders())); + } + + log.error("API 요청 중 Tree 서버에서 5xx 에러가 발생했습니다."); + throw BizRuntimeException.create(String.format("5xx 외부 시스템 오류. %s", res.bodyToMono(String.class))); + }) + ) .block(); } +// public T exchange(final String url, final HttpMethod method, final Object body, final Class rtnClzz, final Map headerMap) { +// +// return webClientConfig.webClient() +// .method(method) +// .uri(url) +// .headers(httpHeaders -> getHeaders(httpHeaders, headerMap)) +// .bodyValue(Objects.requireNonNullElse(body, "")) +// .exchangeToMono(res -> res.bodyToMono(rtnClzz) +// .map(dto -> { +// if (res.statusCode().is2xxSuccessful()) { +// log.info("API 요청에 성공했습니다."); +// return dto; +// } +// +// if (res.statusCode().is4xxClientError()) { +// log.error("API 요청 중 4xx 에러가 발생했습니다. 요청 데이터를 확인해주세요."); +// throw BizRuntimeException.create(String.format("4xx 외부 요청 오류. statusCode: %s, response: %s, header: %s", res.rawStatusCode(), res.bodyToMono(String.class), res.headers().asHttpHeaders())); +// } +// +// log.error("API 요청 중 Tree 서버에서 5xx 에러가 발생했습니다."); +// throw BizRuntimeException.create(String.format("5xx 외부 시스템 오류. %s", res.bodyToMono(String.class))); +// }) +// ) +// .block(); +// } + public ApiResponseDTO sendError(final Throwable e) { Map map = ErrorParse.extractError(e.getCause()); @@ -119,7 +164,7 @@ public class ApiWebClientUtil { } private HttpHeaders getHeaders(final HttpHeaders headers, final Map map) { - + if(ObjectUtils.isEmpty(map)) return headers; for(Map.Entry e : map.entrySet()){ headers.add(e.getKey(), e.getValue()); } diff --git a/mens-core/src/main/java/kr/xit/core/spring/util/SpringUtils.java b/mens-core/src/main/java/kr/xit/core/spring/util/SpringUtils.java index 351937d..6aa5a87 100644 --- a/mens-core/src/main/java/kr/xit/core/spring/util/SpringUtils.java +++ b/mens-core/src/main/java/kr/xit/core/spring/util/SpringUtils.java @@ -1,5 +1,6 @@ package kr.xit.core.spring.util; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.AccessLevel; import lombok.NoArgsConstructor; import org.apache.commons.configuration.PropertiesConfiguration; @@ -86,4 +87,8 @@ public class SpringUtils { public static CorsProperties getCorsProperties(){ return (CorsProperties)getBean(CorsProperties.class); } + + public static ObjectMapper getObjectMapper(){ + return (ObjectMapper)getBean(ObjectMapper.class); + } } diff --git a/mens-core/src/main/java/kr/xit/core/support/utils/ConvertHelper.java b/mens-core/src/main/java/kr/xit/core/support/utils/ConvertHelper.java index 9c19fc7..99696de 100644 --- a/mens-core/src/main/java/kr/xit/core/support/utils/ConvertHelper.java +++ b/mens-core/src/main/java/kr/xit/core/support/utils/ConvertHelper.java @@ -3,6 +3,7 @@ package kr.xit.core.support.utils; import java.io.StringWriter; import java.util.Map; +import kr.xit.core.spring.util.SpringUtils; import lombok.AccessLevel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,7 +35,7 @@ import lombok.NoArgsConstructor; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class ConvertHelper { private static final Logger log = LoggerFactory.getLogger(ConvertHelper.class); - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = SpringUtils.getObjectMapper(); private static final JsonFactory JSON_FACTORY = new JsonFactory(); /** diff --git a/mens-core/src/main/java/kr/xit/core/support/utils/JsonUtils.java b/mens-core/src/main/java/kr/xit/core/support/utils/JsonUtils.java index d78215e..36d2247 100644 --- a/mens-core/src/main/java/kr/xit/core/support/utils/JsonUtils.java +++ b/mens-core/src/main/java/kr/xit/core/support/utils/JsonUtils.java @@ -1,5 +1,8 @@ package kr.xit.core.support.utils; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.cfg.CoercionAction; +import com.fasterxml.jackson.databind.cfg.CoercionInputShape; import java.io.IOException; import java.util.List; import java.util.Map; @@ -13,6 +16,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import kr.xit.core.exception.BizRuntimeException; +import kr.xit.core.spring.util.SpringUtils; import lombok.AccessLevel; import lombok.NoArgsConstructor; import org.apache.commons.lang3.StringUtils; @@ -23,20 +27,16 @@ import org.apache.commons.lang3.StringUtils; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class JsonUtils { + private static final ObjectMapper OM = SpringUtils.getObjectMapper(); + /** * Object -> json string * @return String * @param obj Object */ public static String toJson(Object obj) { - ObjectMapper mapper = new ObjectMapper(); - // null 필드 제 - mapper.setSerializationInclusion(Include.NON_EMPTY); - // No serializer found for class 에러 - private 필드 - mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); -// mapper.setSerializationInclusion(Include.NON_EMPTY); try { - return obj != null ? mapper.writeValueAsString(obj) : null; + return obj != null ? OM.writeValueAsString(obj) : null; } catch (JsonProcessingException e) { throw BizRuntimeException.create(e.getLocalizedMessage()); } @@ -49,10 +49,8 @@ public class JsonUtils { * @param cls Class */ public static T toObject(String str, Class cls) { - ObjectMapper om = new ObjectMapper(); - om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); try { - return str != null ? om.readValue(str, cls) : null; + return str != null ? OM.readValue(str, cls) : null; } catch (JsonProcessingException e) { throw BizRuntimeException.create(e.getLocalizedMessage()); } @@ -66,13 +64,7 @@ public class JsonUtils { */ public static T toObjByObj(Object obj, Class cls) { String str = toJson(obj); - ObjectMapper om = new ObjectMapper(); - om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - try { - return str != null ? om.readValue(str, cls) : null; - } catch (JsonProcessingException e) { - throw BizRuntimeException.create(e.getLocalizedMessage()); - } + return str != null ? toObject(str, cls) : null; } /** @@ -83,11 +75,9 @@ public class JsonUtils { * @throws IOException */ public static List toObjectList(String str, Class cls) { - ObjectMapper om = new ObjectMapper(); - om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); if(str != null){ try { - return om.readValue(str, om.getTypeFactory().constructCollectionType(List.class, cls)); + return OM.readValue(str, OM.getTypeFactory().constructCollectionType(List.class, cls)); } catch (JsonProcessingException e) { throw BizRuntimeException.create(e.getLocalizedMessage()); } @@ -102,9 +92,8 @@ public class JsonUtils { * @return Map */ public static Map toMap(String str) { - ObjectMapper om = new ObjectMapper(); try { - return om.readValue(str, new TypeReference>(){}); + return OM.readValue(str, new TypeReference>(){}); } catch (JsonProcessingException e) { throw BizRuntimeException.create(e.getLocalizedMessage()); } @@ -124,6 +113,25 @@ public class JsonUtils { } } + private static ObjectMapper getObjectMapper() { + ObjectMapper om = new ObjectMapper(); + om.setSerializationInclusion(Include.NON_NULL); + // No serializer found for class 에러 - private 필드 + om.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + om.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + om.configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, false); + + om.coercionConfigDefaults() + .setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull); +// om.coercionConfigFor(LogicalType.Enum) +// .setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull); +// om.coercionConfigFor(LogicalType.POJO) +// .setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull); + + return om; + } + /** * Json 데이터 보기 좋게 변환. * @param json String json