Backend/Spring

Spring 레거시 환경에서 snake_case JSON → camelCase DTO 매핑 문제 해결하기

cojoop 2025. 7. 24. 08:58

Spring 레거시 프로젝트에서 외부 JSON API를 연동하다 보면 snake_casecamelCase 매핑 문제가 자주 발생한다.

Spring Boot라면 한 줄 설정으로 끝날 일이지만, 레거시 환경에서는 DTO 바인딩이 깨져 필드가 전부 null로 들어오는 난감한 상황을 맞이하게 된다.

 

여러 대안을 시도했지만 한계가 있었고, 결국 DTO 대신 JsonNodeJSON을 먼저 받고 이후 DTO로 변환하는 방식으로 우회했다.

이번 글에서는 이 과정을 간단히 공유하려 한다.


문제 상황

요청 JSON은 아래와 같이 들어온다.

{
  "sender_key": "asa5gew9...",
  "message_type": "BASIC",
  "template_code": "A001_01",
  "callback_number": "1234-5478"
}

하지만 DTO는 Java 스타일의 camelCase로 작성되어 있다.

@Data
public class MessageRequestDto {
    private String senderKey;
    private String chatBubbleType;
    private String templateCode;
    private String callbackNumber;
}

Spring Boot라면 @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)로 해결되지만,
Spring 3.x + Jackson 1.x + Lombok 미적용 환경에서는 다음과 같은 문제가 있었다.

  • snake_case → camelCase 매핑 자동 지원 X
  • DTO 직바인딩 시 필드가 전부 null
  • Lombok 버전 충돌로 @Data도 사용 어려움

결과적으로 @RequestBody MessageRequestDto dto 방식이 작동하지 않는 상황이었다.


시도했던 해결 방법

1. Jackson 전역 NamingStrategy 설정

  • ObjectMapper.setPropertyNamingStrategy(SNAKE_CASE) 시도
  • 하지만 레거시 Jackson 버전이라 일부 옵션 미지원

 

2. DTO 필드명을 snake_case로 변경

  • Java 컨벤션과 맞지 않고 유지보수성이 떨어짐
  • 내부 서비스/로직 전반 수정 필요 → 포기

 

3. 커스텀 Deserializer 작성

  • JSON 필드를 직접 매핑하는 코드를 작성
  • DTO가 많을수록 반복 부담 → 유지보수 지옥

결국 DTO 직바인딩은 현실적으로 불가능하다는 결론에 도달했다.


최종 선택: JsonNode로 파싱 후 DTO 변환

그래서 최종적으로 선택한 방식은 JsonNode → DTO 변환이다.

1️⃣ @RequestBody를 JsonNode로 받고
2️⃣ Jackson ObjectMapper를 이용해 DTO로 변환

 

Controller 코드

@RequestMapping(value = "/sendMessage", method = RequestMethod.POST)
@ResponseBody
public ResponseEntity<?> sendMessage(@RequestBody JsonNode json) {

    // 1. 요청 JSON 로그 출력
    log.info("Raw Request: {}", json.toString());

    // 2. JsonNode → DTO 변환
    MessageRequestDto dto = JsonUtil.toDto(json, MessageRequestDto.class);

    // 3. DTO 검증 및 서비스 호출
    messageValidator.validate(dto);
    MessageResponseDto response = messageService.send(dto);

    return ResponseEntity.ok(response);
}

 

JsonUtil

public class JsonUtil {
    private static final ObjectMapper mapper = new ObjectMapper();

    static {
        // snake_case → camelCase 매핑
        mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
    }

    public static <T> T toDto(JsonNode node, Class<T> clazz) {
        try {
            return mapper.treeToValue(node, clazz);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("DTO 변환 실패", e);
        }
    }
}

이렇게 하면 snake_case JSON → camelCase DTO 변환이 깔끔하게 처리된다.


왜 이 방법이 현실적이었나?

레거시 환경 그대로 유지 가능
Spring Boot 업그레이드 없이 Spring 3.x에서 바로 적용 가능

 

원본 JSON 로깅 용이
JsonNode로 받으면 요청 JSON 그대로 저장 가능 → 디버깅 편리

 

DTO 구조 유지
Java 컨벤션을 해치지 않고 camelCase 유지 가능

 

유연성 확보
JsonNode를 쓰면 옵션 필드나 확장 필드가 있어도 유연하게 대응 가능

 

이 방식의 한계

  • DTO 자동 검증(@Valid, @NotNull)과 자연스러운 결합이 어려움
  • 변환 단계(JsonNode → DTO)가 한 번 더 생김 → 약간의 오버헤드
  • 컨트롤러 코드가 @RequestBody DTO 직바인딩보다 다소 장황

 

마무리

레거시 Spring 프로젝트에서는 작은 JSON 필드 네이밍 문제조차 예상보다 복잡하게 꼬일 수 있다는 것을 알게 되었고, DTO 대신 JsonNode로 우회해야 하는 현실이 다소 아쉬웠다.

 

하지만 이번 경험 덕분에 레거시 환경의 한계를 더 깊이 이해할 수 있었고, 추후 스프링 부트로 고도화할 계획이 있어 결국 좋은 경험이었던 것 같다.