API 연동을 하다 보면 응답 JSON의 구조가 일정하지 않은 경우가 많다.
예를 들어 아래처럼 DATA 안의 필드가 매번 달라지는 스타일이다.
{
"DATA": [
{
"A": "10",
"B": "20",
"X1": "100",
"X2": "abc"
}
]
}
이런 형태는 DTO에 필드를 고정으로 박아 둘 수 없다.
그래서 동적 필드(dynamic fields)를 Map으로 수집하는 방식이 필요하고,
Jackson에서는 이를 위해 @JsonAnySetter라는 강력한 기능을 제공한다.
1. @JsonAnySetter로 동적 필드 수집하기
가장 간단한 DTO는 아래처럼 만든다.
@Getter
@Setter
public class MyDto {
private final Map<String, Object> fields = new HashMap<>();
@JsonAnySetter
public void setFields(String key, Object value) {
fields.put(key, value);
}
}
동작 방식 핵심
- JSON에서 MyDto 클래스에 존재하지 않는 필드가 들어오면
Jackson은 setFields(key, value)를 호출한다. - 그래서 DATA 안에 어떤 이름의 필드가 오든 모두 fields Map에 들어간다.
- DTO에 필드가 하나도 없어도 문제 없다.
Jackson은 "모르는 키"를 전부 이 Map에 넣어준다.
결과적으로 MyDto는 완전한 동적 구조를 가진 DTO가 된다.
API 응답의 스키마가 달라져도 DTO를 수정할 필요가 없다.
2. 배열(DATA) 매핑 – TypeReference 필수
DATA가 배열이라면 아래처럼 변환한다.
ObjectMapper mapper = new ObjectMapper();
List<MyDto> list = mapper.readValue(
dataArray.toString(),
new TypeReference<List<MyDto>>() {}
);
여기서 중요한 건 TypeReference<List<MyDto>>() {} 부분이다.
왜 TypeReference가 필요한가?
자바는 제네릭 정보를 런타임에 삭제한다(Type Erasure).
그래서 단순히 List.class만 넘기면 Jackson은
“리스트 안에 뭐가 들어가는지” 판단할 수 없다.
결과적으로 List<Map> 형태로 만들어버린다.
TypeReference<List<MyDto>>() {}는 런타임에도
List<MyDto>라는 타입 정보를 보존하는 객체다.
그래서 반드시 이 방식으로 매핑해야 한다.
3. DATA가 빈 배열일 때 처리
DATA가 빈 배열이면 List<MyDto>도 비어 있는 리스트가 된다.
{
"DATA": []
}
이건 정상 동작이다.
문제 없이 아래와 같이 처리되기 때문이다.
if (list.isEmpty()) {
// DATA 없음 → 원하는 로직만 처리하면 된다.
}
예외는 발생하지 않는다.
빈 배열은 그냥 빈 List로 끝난다.
4. 왜 @JsonAnySetter 방식이 실무에서 유용한가?
동적 필드를 그대로 Map에 받는 구조는
API 스키마가 자주 바뀌는 환경에서 특히 강력하다.
- 필드 추가돼도 DTO 수정 필요 없음
- 필드 이름이 매번 바뀌어도 문제 없음
- JSON 구조가 변해도 안정적으로 파싱됨
- 유지보수 비용이 아주 낮음
실제로 여러 공공 API, 외부 B2B API, 로그성 데이터 파싱에서도
이 패턴이 가장 많이 쓰인다.
정리
오늘 구현한 구조의 핵심은 두 가지다.
- @JsonAnySetter → DTO에 없는 필드도 모두 Map으로 안전하게 수집
- TypeReference<List<MyDto>> → 동적 DTO 리스트를 정확하게 매핑
이 조합을 쓰면 JSON 구조가 비정형적이어도 DTO를 바꾸지 않고 안정적으로 처리할 수 있다.
API 스키마가 자주 바뀌거나, KEY 숫자가 많거나, 예측 불가능한 응답을 다뤄야 한다면
이 방식이 가장 안전하고 유지보수도 편하다.
'IT' 카테고리의 다른 글
| 낙관적 락 vs 비관적 락 (0) | 2025.12.15 |
|---|---|
| 같은 서버에서 톰캣 별도 인스턴스 띄우기 (0) | 2025.12.11 |
| RestTemplate 응답 타입에 따른 20초 타임아웃 트러블슈팅 (0) | 2025.12.04 |
| 스프링 의존성 주입: @Autowired vs @RequiredArgsConstructor + 단위 테스트 예제 (0) | 2025.09.24 |
| [Java] 특정 문자 포함 여부 확인하기 String.contains() vs StringUtils.contains() (0) | 2025.07.23 |
댓글