-
Jackson JSON 트러블슈팅을 위한 각 오류별 구체적인 예제와 Spring Boot 기반의 통합 테스트기술과 산업/언어 및 프레임워크 2025. 5. 19. 11:17728x90
1. 테스트 커버리지 확장
- 날짜 역직렬화 처리 테스트 (LocalDateTime)
- 순환 참조 직렬화 테스트 (@JsonManagedReference, @JsonBackReference)
2. Spring REST Docs 기반 API 문서화 예제 추가
- /user POST 요청을 requestFields()로 문서화
- 문서 생성용 document() 블록 포함
3. MockMvc 기반 통합 테스트로 구성
- JSONPath 기반 응답 구조 검증
- Jackson 설정 (ACCEPT_CASE_INSENSITIVE_ENUMS) 반영
// Jackson 트러블슈팅 – Spring Boot 통합 테스트 기반 예제 (확장) @SpringBootTest @AutoConfigureMockMvc public class JacksonErrorIntegrationTest { @Autowired private MockMvc mockMvc; @RestController static class TestController { @PostMapping("/user") public ResponseEntity<String> createUser(@RequestBody User user) { return ResponseEntity.ok("OK"); } @PostMapping("/role") public ResponseEntity<String> assignRole(@RequestBody RoleUser roleUser) { return ResponseEntity.ok("OK"); } @PostMapping("/date") public ResponseEntity<String> parseDate(@RequestBody DateUser dateUser) { return ResponseEntity.ok("Parsed: " + dateUser.createdAt); } @PostMapping("/parent") public ResponseEntity<Parent> getParent() { Parent parent = new Parent("부모", null); Child child = new Child("자식", parent); parent.setChild(child); return ResponseEntity.ok(parent); } } static class User { public String name; public int age; } static class RoleUser { public Role role; } enum Role { ADMIN, USER } static class DateUser { public LocalDateTime createdAt; } @JsonIgnoreProperties(ignoreUnknown = true) static class SafeUser { public String name; public int age; } @JsonManagedReference static class Parent { public String name; public Child child; public Parent() {} public Parent(String name, Child child) { this.name = name; this.child = child; } public void setChild(Child child) { this.child = child; } } @JsonBackReference static class Child { public String name; public Parent parent; public Child() {} public Child(String name, Parent parent) { this.name = name; this.parent = parent; } } @Test void unrecognizedFieldReturns400() throws Exception { String invalidJson = "{\"name\":\"홍길동\", \"age\":30, \"unknown\":\"???\"}"; mockMvc.perform(post("/user") .contentType(MediaType.APPLICATION_JSON) .content(invalidJson)) .andExpect(status().isBadRequest()); } @Test void invalidEnumValueReturns400() throws Exception { String json = "{\"role\":\"invalid\"}"; mockMvc.perform(post("/role") .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isBadRequest()); } @TestConfiguration static class CustomConfig { @Bean public Jackson2ObjectMapperBuilderCustomizer customizer() { return builder -> builder.featuresToEnable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS); } } @Test void validEnumWithInsensitiveEnabled() throws Exception { String json = "{\"role\":\"admin\"}"; mockMvc.perform(post("/role") .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isOk()); } @Test void validDateParsing() throws Exception { String json = "{\"createdAt\":\"2025-05-10T15:00:00\"}"; mockMvc.perform(post("/date") .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isOk()) .andExpect(content().string(org.hamcrest.Matchers.containsString("Parsed: 2025"))); } @Test void cyclicReferenceSerializesSafely() throws Exception { mockMvc.perform(post("/parent") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("부모")) .andExpect(jsonPath("$.child.name").value("자식")) .andExpect(jsonPath("$.child.parent").doesNotExist()); } // Spring REST Docs 예시 (문서화) @Test void documentUserApi(@Autowired MockMvc mockMvc, @Autowired ObjectMapper objectMapper) throws Exception { String userJson = objectMapper.writeValueAsString(new User()); mockMvc.perform(post("/user") .contentType(MediaType.APPLICATION_JSON) .content(userJson)) .andExpect(status().isOk()) .andDo(document("user-create", requestFields( fieldWithPath("name").description("이름"), fieldWithPath("age").description("나이") ) )); } }
1. 테스트 커버리지 확장
- WebTestClient를 활용한 /user POST 테스트 추가
- RestAssured 기반 HTTP 테스트 추가 (Swagger 연동 예비용)
- 기존 MockMvc 테스트와 병행 가능하도록 구성
2. Spring REST Docs 문서화
- @AutoConfigureRestDocs 설정 추가
- 문서 출력 경로: build/generated-snippets
- documentUserApi() 테스트에서 requestFields 문서 생성 유지
3. Swagger (OpenAPI) 연동 예시 주석 추가
- Swagger 설정용 SwaggerConfig 예제 주석 제공
- @EnableOpenApi + OpenAPI bean 정의 구조 제공
// Jackson 트러블슈팅 – Spring Boot 통합 테스트 기반 예제 (확장) @SpringBootTest @AutoConfigureMockMvc @AutoConfigureRestDocs(outputDir = "build/generated-snippets") @Import({JacksonErrorIntegrationTest.CustomConfig.class}) public class JacksonErrorIntegrationTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @RestController static class TestController { @PostMapping("/user") public ResponseEntity<String> createUser(@RequestBody User user) { return ResponseEntity.ok("OK"); } @PostMapping("/role") public ResponseEntity<String> assignRole(@RequestBody RoleUser roleUser) { return ResponseEntity.ok("OK"); } @PostMapping("/date") public ResponseEntity<String> parseDate(@RequestBody DateUser dateUser) { return ResponseEntity.ok("Parsed: " + dateUser.createdAt); } @PostMapping("/parent") public ResponseEntity<Parent> getParent() { Parent parent = new Parent("부모", null); Child child = new Child("자식", parent); parent.setChild(child); return ResponseEntity.ok(parent); } } // 기존 DTO 및 Enum 정의 생략 (동일 유지) // WebTestClient 기반 테스트 예제 추가 @Autowired private WebApplicationContext context; private WebTestClient webTestClient; @BeforeEach void setUp() { this.webTestClient = WebTestClient.bindToApplicationContext(context).configureClient().baseUrl("/").build(); } @Test void testUserPostWithWebTestClient() { webTestClient.post() .uri("/user") .contentType(MediaType.APPLICATION_JSON) .bodyValue("{\"name\":\"홍길동\", \"age\":30}") .exchange() .expectStatus().isOk(); } // RestAssured 기반 예제 (Swagger 연동 예비) @Test void testUserPostWithRestAssured() { RestAssured.given() .contentType("application/json") .body("{\"name\":\"홍길동\", \"age\":30}") .when() .post("/user") .then() .statusCode(200); } // Swagger 연동 예시 주석 (실제 사용 시 SwaggerConfig 클래스 필요) // @EnableOpenApi // @Configuration // public class SwaggerConfig { // @Bean // public OpenAPI apiInfo() { // return new OpenAPI() // .info(new Info().title("Jackson API").version("v1")); // } // } // Spring REST Docs 문서 생성 테스트 예시 유지 @Test void documentUserApi() throws Exception { String userJson = objectMapper.writeValueAsString(new User()); mockMvc.perform(post("/user") .contentType(MediaType.APPLICATION_JSON) .content(userJson)) .andExpect(status().isOk()) .andDo(document("user-create", requestFields( fieldWithPath("name").description("이름"), fieldWithPath("age").description("나이") ) )); } }
1. 테스트 커버리지 확장
- @Order, @Tag, @DisplayName을 사용하여 테스트 분류 및 리포팅 정리
- 날짜 처리 (LocalDateTime) 파싱 성공 케이스 추가
- 순환 참조 직렬화 테스트 (@JsonManagedReference, @JsonBackReference) 추가
2. API 문서 연동 (Spring REST Docs)
- @AutoConfigureRestDocs 설정 유지
- documentUserApi() 테스트에서 requestFields() 기반 문서 생성
- 문서 출력 위치: build/generated-snippets
3. Swagger (springdoc-openapi) 설정 예시 주석 제공
- @OpenAPIDefinition, GroupedOpenApi 설정 예시 추가
- 실제 적용 시 swagger-ui 경로: /swagger-ui.html 또는 /v3/api-docs
// Jackson 트러블슈팅 – Spring Boot 통합 테스트 기반 예제 (확장 + 문서화 + 리포트) @SpringBootTest @AutoConfigureMockMvc @AutoConfigureRestDocs(outputDir = "build/generated-snippets") @Import({JacksonErrorIntegrationTest.CustomConfig.class}) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class JacksonErrorIntegrationTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @RestController static class TestController { @PostMapping("/user") public ResponseEntity<String> createUser(@RequestBody User user) { return ResponseEntity.ok("OK"); } @PostMapping("/role") public ResponseEntity<String> assignRole(@RequestBody RoleUser roleUser) { return ResponseEntity.ok("OK"); } @PostMapping("/date") public ResponseEntity<String> parseDate(@RequestBody DateUser dateUser) { return ResponseEntity.ok("Parsed: " + dateUser.createdAt); } @PostMapping("/parent") public ResponseEntity<Parent> getParent() { Parent parent = new Parent("부모", null); Child child = new Child("자식", parent); parent.setChild(child); return ResponseEntity.ok(parent); } } // WebTestClient 기반 테스트 예제 추가 @Autowired private WebApplicationContext context; private WebTestClient webTestClient; @BeforeEach void setUp() { this.webTestClient = WebTestClient.bindToApplicationContext(context).configureClient().baseUrl("/").build(); } @Test @Order(1) @Tag("web") void testUserPostWithWebTestClient() { webTestClient.post() .uri("/user") .contentType(MediaType.APPLICATION_JSON) .bodyValue("{\"name\":\"홍길동\", \"age\":30}") .exchange() .expectStatus().isOk(); } @Test @Order(2) @Tag("restassured") void testUserPostWithRestAssured() { RestAssured.given() .contentType("application/json") .body("{\"name\":\"홍길동\", \"age\":30}") .when() .post("/user") .then() .statusCode(200); } // Swagger 연동 예시 (springdoc-openapi) // @OpenAPIDefinition(info = @Info(title = "Jackson API", version = "v1")) // @Configuration // public class SwaggerConfig { // @Bean // public GroupedOpenApi userApi() { // return GroupedOpenApi.builder() // .group("user") // .pathsToMatch("/user/**") // .build(); // } // } // REST Docs 문서 생성 테스트 예시 유지 @Test @Order(3) @Tag("docs") void documentUserApi() throws Exception { String userJson = objectMapper.writeValueAsString(new User()); mockMvc.perform(post("/user") .contentType(MediaType.APPLICATION_JSON) .content(userJson)) .andExpect(status().isOk()) .andDo(document("user-create", requestFields( fieldWithPath("name").description("이름"), fieldWithPath("age").description("나이") ) )); } // Allure 리포트용 태깅 예시 포함 가능 @Test @Order(4) @Tag("date") @DisplayName("날짜 파싱 성공 케이스") void validDateParsing() throws Exception { String json = "{\"createdAt\":\"2025-05-10T15:00:00\"}"; mockMvc.perform(post("/date") .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isOk()) .andExpect(content().string(org.hamcrest.Matchers.containsString("Parsed: 2025"))); } @Test @Order(5) @Tag("cycle") @DisplayName("순환 참조 직렬화 테스트") void cyclicReferenceSerializesSafely() throws Exception { mockMvc.perform(post("/parent") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("부모")) .andExpect(jsonPath("$.child.name").value("자식")) .andExpect(jsonPath("$.child.parent").doesNotExist()); } }
1. 테스트 커버리지 구조화
- @Order, @Tag, @DisplayName, @Description을 활용한 테스트 메타 정보 부여
- 테스트 목적이 명확히 드러나도록 함수명/어노테이션 구성 개선
2. Spring REST Docs 문서화 연동
- @AutoConfigureRestDocs(outputDir = "build/generated-snippets") 설정 유지
- documentUserApi() 테스트에 REST Docs 문서 스니펫 생성 포함
3. Swagger / springdoc-openapi 연동 예시 포함
- SwaggerConfig 예시 주석 포함 (@OpenAPIDefinition, GroupedOpenApi)
- 실제 적용 시 /v3/api-docs, /swagger-ui.html로 API 문서 확인 가능
4. Allure 리포트 통합 준비
- @Tag("allure"), @Description 사용
- 테스트 리포트 추적/분석용 메타 정보 정의
이 코드는 다음을 만족합니다:
- 기능별 분리된 테스트
- 문서 자동 생성
- Swagger 문서 연동 구조
- Allure 등 외부 리포팅 도구와의 통합 가능성
// Jackson 트러블슈팅 – Spring Boot 통합 테스트 기반 예제 (완성: 테스트 커버리지 + REST Docs + Swagger + Allure 리포트) @SpringBootTest @AutoConfigureMockMvc @AutoConfigureRestDocs(outputDir = "build/generated-snippets") @Import({JacksonErrorIntegrationTest.CustomConfig.class}) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class JacksonErrorIntegrationTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @RestController static class TestController { @PostMapping("/user") public ResponseEntity<String> createUser(@RequestBody User user) { return ResponseEntity.ok("OK"); } @PostMapping("/role") public ResponseEntity<String> assignRole(@RequestBody RoleUser roleUser) { return ResponseEntity.ok("OK"); } @PostMapping("/date") public ResponseEntity<String> parseDate(@RequestBody DateUser dateUser) { return ResponseEntity.ok("Parsed: " + dateUser.createdAt); } @PostMapping("/parent") public ResponseEntity<Parent> getParent() { Parent parent = new Parent("부모", null); Child child = new Child("자식", parent); parent.setChild(child); return ResponseEntity.ok(parent); } } @Autowired private WebApplicationContext context; private WebTestClient webTestClient; @BeforeEach void setUp() { this.webTestClient = WebTestClient.bindToApplicationContext(context).configureClient().baseUrl("/").build(); } @Test @Order(1) @Tag("web") void WebTestClient_를_이용한_유저_등록_성공() { webTestClient.post() .uri("/user") .contentType(MediaType.APPLICATION_JSON) .bodyValue("{\"name\":\"홍길동\", \"age\":30}") .exchange() .expectStatus().isOk(); } @Test @Order(2) @Tag("restassured") void RestAssured_를_이용한_JSON_전송_테스트() { RestAssured.given() .contentType("application/json") .body("{\"name\":\"홍길동\", \"age\":30}") .when() .post("/user") .then() .statusCode(200); } // Swagger springdoc 설정 클래스 (별도 파일로도 분리 가능) // @OpenAPIDefinition(info = @Info(title = "Jackson API", version = "v1")) // @Configuration // public class SwaggerConfig { // @Bean // public GroupedOpenApi jacksonApi() { // return GroupedOpenApi.builder() // .group("jackson") // .pathsToMatch("/**") // .build(); // } // } @Test @Order(3) @Tag("docs") @DisplayName("Spring REST Docs를 통한 API 문서 생성") void document_user_api() throws Exception { String userJson = objectMapper.writeValueAsString(new User()); mockMvc.perform(post("/user") .contentType(MediaType.APPLICATION_JSON) .content(userJson)) .andExpect(status().isOk()) .andDo(document("user-create", requestFields( fieldWithPath("name").description("이름"), fieldWithPath("age").description("나이") ) )); } @Test @Order(4) @Tag("allure") @DisplayName("날짜 파싱 성공 케이스 (LocalDateTime)") @Description("정상적인 날짜 포맷이 들어왔을 때 LocalDateTime으로 역직렬화되는지 확인합니다.") void valid_date_parsing_with_allure() throws Exception { String json = "{\"createdAt\":\"2025-05-10T15:00:00\"}"; mockMvc.perform(post("/date") .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isOk()) .andExpect(content().string(org.hamcrest.Matchers.containsString("Parsed: 2025"))); } @Test @Order(5) @Tag("cycle") @DisplayName("순환 참조 직렬화 테스트 (Jackson @JsonManagedReference)") @Description("Jackson이 순환 참조 구조를 안전하게 직렬화하는지 확인합니다. Child → Parent 역참조는 생략되어야 합니다.") void cyclic_reference_serialization_test() throws Exception { mockMvc.perform(post("/parent") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("부모")) .andExpect(jsonPath("$.child.name").value("자식")) .andExpect(jsonPath("$.child.parent").doesNotExist()); } }
728x90'기술과 산업 > 언어 및 프레임워크' 카테고리의 다른 글
Spring Boot 시리즈 32편 – OpenFeign을 활용한 외부 API 연동 아키텍처 설계 (3) 2025.05.19 Spring Boot 시리즈 31편 – Spring Scheduler: 정기 작업 자동화 및 서비스 운영 효율화 전략 (0) 2025.05.19 Jackson JSON 트러블슈팅을 위한 각 오류별 구체적인 예제와 JUnit 기반 테스트 코드 (1) 2025.05.19 Java JSON 처리 실전 – Jackson 트러블슈팅 가이드: 자주 발생하는 직렬화/역직렬화 오류와 해결 방법 (0) 2025.05.19 Java JSON 처리 실전 시리즈 – Jackson 트러블슈팅 가이드: 흔한 에러 7가지와 해결 방법 (0) 2025.05.19