# Integration Serialization / Deserialization 기준 ## 1. 목적 이 문서는 외부 API / integration 호출에서 request serialization과 response deserialization 기준을 정의한다. 이 문서의 목표는 다음과 같다. - provider 계약과 내부 모델을 분리한다 - 외부 payload 변화에 대한 내성을 높인다 - media type, 필드명, null/absent, 에러 바디, 날짜/시간 포맷을 일관되게 처리한다 - serialization concern이 application/domain으로 번지지 않게 한다 ## 2. 근거 수준 - Official: Spring Framework / Spring Boot / Jackson 공식 문서에서 직접 확인되는 내용 - Official + Practice: 공식 기능 위에 Tolerant Reader 같은 실무 패턴을 결합한 내용 - Project Recommendation: 이 프로젝트 구조와 운영 방식에 맞춘 규칙 ## 3. 기본 원칙 ### 3.1 외부 serialization은 provider contract가 결정한다 Spring의 REST client는 HTTP 본문을 상위 Java 객체로 읽고 쓰며, JSON 외에도 application/x-www-form-urlencoded, multipart, byte array, XML 같은 형식을 converter로 다룹니다. 따라서 외부 연동 직렬화 기준은 우리 내부 JSON 취향이 아니라 provider가 요구하는 content type, field shape, wire format 이 먼저다. 프로젝트 규칙: - provider가 JSON을 요구하면 JSON으로 - provider가 form-urlencoded를 요구하면 form으로 - provider가 XML/byte/binary를 요구하면 그 형식으로 보낸다 - “우리 서비스는 JSON 표준이 있으니 외부도 JSON으로 통일”을 금지한다 ### 3.2 외부 DTO와 내부 모델은 반드시 분리한다 Spring client가 DTO 변환을 쉽게 해 준다고 해서, provider DTO를 내부 모델처럼 써도 된다는 뜻은 아니다. Tolerant Reader 관점에서도 payload를 읽는 코드는 한 경계에 모아야 나머지 시스템이 변경에 덜 민감해진다. 프로젝트 규칙: - 외부 request DTO / response DTO는 provider 계약 전용 타입 - application/domain은 외부 DTO를 직접 모른다 - adapter 경계에서 내부 command/result/failure reason으로 변환한다 ### 3.3 읽기는 느슨하게, 쓰기는 명시적으로 한다 Jackson의 ignoreUnknown=true는 외부 응답이 필드를 추가해도 파싱을 덜 깨뜨리게 해 준다. 반면 outbound request는 provider가 받지 않는 필드를 보내거나, null과 absent를 헷갈리게 보내면 계약 오류가 생긴다. 따라서 외부 연동에서는 response는 tolerant reader, request는 explicit writer 전략을 기본으로 둔다. 프로젝트 규칙: - 외부 response DTO는 additive change에 관대할 수 있다 - 외부 request DTO는 보내는 필드를 명시적으로 통제한다 - 내부 객체를 그대로 직렬화해서 provider에 보내지 않는다 ## 4. Request serialization 표준 ### 4.1 outbound request는 provider 계약에 정확히 맞춘다 Spring REST client는 DTO를 바탕으로 본문을 직렬화하지만, 실제 field name과 media type은 adapter가 정해야 한다. Jackson의 @JsonProperty는 외부 필드명을 DTO 경계에서 맞추는 공식 수단이다. 프로젝트 규칙: - provider field name mismatch는 외부 request DTO에서 해결한다 - 내부 필드명/도메인 용어를 provider 계약에 맞춰 바꾸지 않는다 - provider-specific enum/string/value shape를 request DTO에 국소화한다 ### 4.2 request DTO는 allowlist 방식으로 설계한다 프로젝트 규칙: - provider에 보낼 필드만 request DTO에 둔다 - 내부 계산값, 디버그 값, 서버 내부 상태를 request DTO에 섞지 않는다 - “언젠가 쓸 수 있으니 같이 보내자”를 금지한다 ### 4.3 null과 absent는 provider 계약 기준으로 명시한다 프로젝트 규칙: - provider가 null과 필드 omission을 다르게 해석하면 반드시 구분한다 - 기본 정책은 “의미가 다르면 DTO와 mapper에서 명시적으로 처리” - 전역 NON_NULL 같은 설정으로 provider별 의미를 무심코 바꾸지 않는다 ### 4.4 media type은 명시적으로 맞춘다 Spring converter는 JSON, form, multipart, byte array, XML 등을 지원한다. 외부 연동에서는 특히 OAuth/token 발급, webhook, 파일 업로드, binary download처럼 JSON이 아닌 형식이 흔하다. 프로젝트 규칙: - application/json을 기본 추정값으로 두지 않는다 - provider가 요구하는 Content-Type과 Accept를 adapter에서 명시한다 - form 요청은 JSON DTO를 억지로 보내지 않는다 ## 5. Response deserialization 표준 ### 5.1 external response DTO는 tolerant reader를 기본 검토한다 Jackson의 @JsonIgnoreProperties(ignoreUnknown = true)는 인식하지 못한 필드를 deserialization에서 무시한다. Fowler의 Tolerant Reader도 producer가 필드를 추가해도 consumer가 덜 깨지도록 payload reading을 느슨하게 설계하라고 설명한다. 프로젝트 규칙: - third-party response DTO는 ignoreUnknown=true를 기본 검토한다 - provider가 필드를 추가해도 우리 파싱이 즉시 깨지지 않게 한다 - 단, first-party API request DTO까지 이 정책을 일반화하지 않는다 ### 5.2 success body와 error body를 분리한다 프로젝트 규칙: - 성공 응답 DTO와 오류 응답 DTO를 따로 둔다 - provider error JSON을 success DTO에 억지로 파싱하지 않는다 - error body는 adapter가 읽고 내부 failure reason/exception으로 번역한다 ### 5.3 raw Map/JsonNode는 마지막 수단이다 Spring/Jackson은 상위 객체 매핑과 custom deserializer를 지원한다. 따라서 외부 응답 구조가 완전히 동적이지 않다면 typed DTO가 기본이다. raw map/tree는 계약이 너무 불안정하거나 일부 필드만 읽을 때의 마지막 수단으로 본다. 프로젝트 규칙: - 기본은 typed response DTO - 정말 불안정한 payload만 JsonNode/Map 허용 - raw tree를 application/domain까지 들고 가지 않는다 - boundary에서 읽고 안정적인 내부 모델로 바꾼다 ### 5.4 외부 enum은 바로 domain enum에 연결하지 않는다 프로젝트 규칙: - provider enum/string 값은 외부 DTO 또는 mapper 단계에서 해석한다 - domain enum에 provider 값을 직접 박아 넣지 않는다 - provider가 새 enum 값을 추가할 수 있으면 UNKNOWN/기본 처리 전략을 둔다 ## 6. 날짜/시간/숫자 규칙 ### 6.1 날짜/시간 형식은 provider 계약을 따른다 Jackson의 JavaTimeModule은 java.time 타입을 지원하고, timestamps 기능이 꺼져 있으면 보통 ISO-8601 문자열을 사용한다. 하지만 외부 연동에서는 provider가 epoch millis, string, custom format 중 무엇을 쓰는지가 더 중요하다. 프로젝트 규칙: - provider가 ISO-8601을 쓰면 Instant/OffsetDateTime 등으로 명시적으로 읽는다 - provider가 epoch number를 쓰면 그 계약을 DTO/커스텀 deserializer에서 처리한다 - 내부 표준 시간 타입을 provider wire format 때문에 오염시키지 않는다 ### 6.2 숫자/정밀도는 domain 의미를 잃지 않게 한다 프로젝트 규칙: - 금액, 환율, 정산 수치처럼 정밀도가 중요한 값은 double을 기본값으로 두지 않는다 - provider가 문자열 금액을 보내면 문자열 → 안전한 내부 수치 타입으로 변환한다 - 숫자 파싱 실패는 provider parsing failure로 다루고 domain 예외와 섞지 않는다 ## 7. Jackson / mapper / module 규칙 ### 7.1 메서드 안에서 new ObjectMapper()를 만들지 않는다 Spring Boot는 auto-configured JSON mapper와 RestClient.Builder/WebClient.Builder를 제공하고, 그 builder에는 converter/codecs와 적절한 공통 구성이 반영된다. 메서드마다 새 mapper를 만들면 그 구성을 우회하게 된다. 프로젝트 규칙: - adapter 메서드 안 new ObjectMapper() 금지 - 공통 builder와 공통 mapper를 우선 사용한다 - provider 특수 규칙이 있으면 adapter configuration에서 분리해 구성한다 ### 7.2 전역 @JacksonComponent / @JacksonMixin은 진짜 공통 규칙에만 쓴다 Spring Boot는 @JacksonComponent를 자동 등록하고, @JacksonMixin도 auto-configured mapper에 등록한다. 즉, 이 둘은 전역 영향 이 있다. 따라서 provider 하나만을 위한 특수 직렬화 규칙을 전역에 뿌리는 것은 신중해야 한다. 프로젝트 규칙: - 여러 연동/여러 DTO에 공통인 serializer/deserializer만 전역 등록 - 특정 provider 전용 weird format은 adapter-local configuration 우선 - provider 하나 때문에 전체 애플리케이션 JSON 규칙을 바꾸지 않는다 ### 7.3 imperative/reactive client가 쓰는 JSON mapper 경계를 의식한다 Boot는 imperative HTTP clients와 reactive HTTP clients에 대해 각각 pre-configured builder를 제공하고, preferred JSON mapper 설정도 분리해 둔다. 프로젝트 규칙: - RestClient/WebClient에서 provider-specific codec/mapper를 바꿀 때 범위를 명시한다 - imperative client용 변경이 reactive client 전체에 번지지 않게 한다 - “한 군데 바꾸면 다 되겠지” 식 전역 변경을 지양한다 ## 8. 검증 / 번역 규칙 ### 8.1 파싱 성공과 비즈니스 성공을 같은 것으로 보지 않는다 프로젝트 규칙: - JSON/XML/form parsing 성공은 “wire format 해석 성공”일 뿐 - provider가 business failure body를 200으로 줄 수도 있다 - adapter는 파싱 후에 success/error semantics를 다시 해석한다 ### 8.2 deserialization 예외는 provider parsing failure로 번역한다 프로젝트 규칙: - malformed payload, required field missing, unexpected type mismatch는 integration parsing failure로 번역한다 - application/domain이 Jackson 예외 타입을 직접 보지 않게 한다 - provider contract drift 여부를 운영에서 추적 가능하게 한다 ## 9. 관측 가능성 규칙 ### 9.1 payload 전문 로그를 기본 금지한다 외부 payload는 PII, 토큰, 비밀값, 내부 식별자 등을 포함할 수 있다. 이전 observability 기준과 마찬가지로, serialization/deserialization 문제를 추적한다는 이유로 request/response 전문을 기본 로그에 남기지 않는다. 이 점은 OWASP의 민감정보 로그 금지 원칙과도 맞다. 프로젝트 규칙: - 기본 로그는 provider, operation, status, contentType, payloadBytes, parse failure type 정도만 - payload 원문은 기본 금지 - 꼭 필요하면 테스트/격리 환경에서 제한적으로 남긴다 ### 9.2 parse failure는 contract drift 신호로 남긴다 프로젝트 규칙: - deserialization 실패는 단순 예외로 묻지 않는다 - provider, operation, content type, failing field/shape 정도를 안전하게 남긴다 - “provider contract가 변했을 수 있음”을 운영에서 추적할 수 있어야 한다 ## 10. 테스트 규칙 ### 10.1 외부 DTO는 fixture 기반 계약 테스트를 둔다 프로젝트 규칙: - 대표 성공 응답 - 대표 오류 응답 - provider가 필드를 추가한 응답 - 일부 필드 누락 응답 - 에 대한 parsing 테스트를 둔다 - provider 예시 payload나 실제 캡처 샘플을 fixture로 관리할 수 있다 ### 10.2 request serialization도 golden sample로 확인한다 프로젝트 규칙: - provider에 보내는 JSON/form/XML shape를 golden sample로 검증한다 - field name, null/absent, date/time format, enum value가 계약대로 직렬화되는지 확인한다 - “직렬화는 framework가 알아서 하겠지”에 기대지 않는다 ## 11. 다른 문서와의 경계 이 문서는 외부 provider payload의 serialization/deserialization 만 다룬다. 아래 주제의 source of truth는 별도 문서다. - external API client structure - timeout - retry - outbound idempotency - fallback - exception translation 이 문서는 위 문서를 반복하지 않고, payload contract를 읽고 쓰는 경계 규칙 만 정의한다. ## 12. 금지 규칙 다음은 기본 금지다. - 내부 domain/entity를 외부 request/response DTO로 직접 사용 - provider response DTO를 application/domain 시그니처에 그대로 전달 - 메서드 안 new ObjectMapper() 생성 - 특정 provider 응답 대응을 위해 전역 mapper 규칙을 무심코 변경 - first-party API strict 정책과 external response tolerant 정책을 혼동 - success/error body를 같은 DTO로 억지 파싱 - payload 전문 로그를 기본으로 남김 - 외부 enum/string 값을 바로 domain enum에 박아 넣음 ## 13. 체크리스트 다음 질문에 “예”로 답할 수 있어야 한다. - provider request/response DTO와 내부 모델이 분리되어 있는가? - outbound request가 provider contract를 정확히 반영하는가? - external response는 additive change에 대해 필요한 만큼 tolerant한가? - success body와 error body DTO가 분리되어 있는가? - null과 absent 의미를 provider 계약 기준으로 다루는가? - provider-specific weird format이 adapter 경계 안에 갇혀 있는가? - RestClient/WebClient의 공통 builder/mapper 구성을 우회하지 않는가? - serialization/deserialization fixture 테스트가 있는가?