JSON Toolkit

JSON을 Java 클래스로: 생성기는 뼈대만 줍니다, 프로덕션까지 채울 다섯 가지

핵심 요약: 이 사이트 생성기로 JSON → Java의 기본 출력을 실측했습니다——숫자는 Double, 중첩 객체는 static 내부 클래스, Jackson 어노테이션은 하나도 생성되지 않습니다. 금액 정밀도, 필드 이름 매핑, 제네릭 소거, 알 수 없는 필드, 날짜와 기본 생성자까지 다섯 가지를 복사해 쓸 수 있는 코드와 함께 정리했습니다.

중급약 10분 소요2026-11-04 발행2026-11-04 업데이트
JSON을 Java POJO 클래스 코드로 변환하는 도구 화면

결론: 생성기가 10초 만에 주는 클래스 구조는 컴파일은 통과하지만 프로덕션에 그대로 쓸 수는 없습니다. 이 사이트 생성기의 실측 기본 출력은 number→Double, 중첩 객체→static 내부 클래스, Jackson 어노테이션 없음. 금액 정밀도, 필드 이름 매핑, 제네릭 소거, 알 수 없는 필드, 날짜——다섯 가지 모두 생성기가 대신해 주지 않습니다.

API 응답을 Java 클래스로 바꾸는 일은 순수한 기계 작업입니다. JSON을 보고 필드를 베끼고 getter/setter를 붙이죠. 기계 작업의 대가는 보이지 않는 칼입니다——프로퍼티 이름을 한 글자 잘못 베껴도 컴파일 오류가 나지 않고, 필드는 조용히 null로 남아 있습니다. 깨닫는 시점은 프로덕션 로그 속입니다. 클래스 구조를 생성하는 이유는 첫날부터 이름과 타입을 맞추기 위해서입니다. 다만 맞출 수 있는 것은 "샘플에서 보이는 부분"뿐입니다.

1. 먼저 생성기를 돌려봅니다: 실제로 무엇이 나오나

대표값을 담은 API 응답을 이 사이트의 JSON → Java 도구 페이지에 붙여 넣습니다:

{
  "order_id": 202611040001,
  "buyer": {
    "name": "王五",
    "vip": true
  },
  "items": [
    { "sku": "A-1", "price": 19.99, "qty": 2 }
  ],
  "created_at": "2026-11-04T09:30:00+08:00"
}

생성된 뼈대(기본 옵션, useLombok 끔):

public class Root {
    private Double orderId;
    private Buyer buyer;
    private List<Item> items;
    private String createdAt;

    public Double getOrderId() { return orderId; }
    public void setOrderId(Double orderId) { this.orderId = orderId; }
    // ...나머지 getter/setter도 같은 방식
}

Buyer와 Item은 별개의 static 내부 클래스입니다. 매핑 규칙을 하나씩 대조해 보겠습니다:

JSON의 형태 생성되는 Java 타입 비고
"name": "王五" String 문자열은 항상 String
"qty": 2 Double 모든 숫자가 Double, 2절 참조
"vip": true Boolean 래퍼 타입, null 허용
"buyer": {...} Buyer(static 내부 클래스) 중첩 객체는 독립 클래스로
"items": [...] List<Item> 요소 타입은 첫 요소에서 추정
order_id orderId snake_case를 자동으로 camelCase로

클래스 이름은 PascalCase(Root), 필드는 camelCase(orderId). Java 키워드(class, new, int)와 충돌하면 접미사를 붙여 자동으로 회피합니다. 옵션은 packageName, className, useLombok(@Data 생성 여부) 세 가지. Lombok을 켜면 보일러플레이트 전체가 어노테이션 몇 개로 수렴합니다.

이 뼈대에는 두 가지 경계가 있습니다. 첫째, Jackson/Gson 어노테이션을 전혀 생성하지 않습니다——@JsonProperty도 @JsonIgnoreProperties도 @JsonFormat도 없습니다. 어노테이션은 "역직렬화를 어떻게 실행할지"를 결정하는데, 샘플에서는 실행 정책을 추정할 수 없기 때문입니다. 둘째, 역변환이 없습니다: Java 클래스에서 JSON을 도출하는 것은 범위 밖이며, JSON → TypeScript Interface와 같은 단방향 설계입니다.

2. 첫 번째 보완: Double은 금액의 타입이 아닙니다

생성기는 "price": 19.99를 Double로 매핑합니다. 컴파일도 대입도 문제없지만 이 타입에 돈을 담아서는 안 됩니다. 2진 부동소수점은 0.1을 표현할 수 없고, 따라서 대부분의 10진 소수도 정확히 표현하지 못합니다:

System.out.println(19.99);          // 19.99 —— 문제 없어 보임
double a = 0.1 + 0.2;
System.out.println(a);              // 0.30000000000000004
System.out.println(a == 0.3);       // false

19.99를 단독으로 출력할 때는 최단 표현 미화가 작동해 문제가 안 보입니다. 연산에 들어가는 순간 오차가 드러납니다. 영어권 세 편의 튜토리얼이 이 지점에서 완전히 일치합니다: 가격·금액·요율은 모두 BigDecimal로 변경하고, 생성 방식은 new BigDecimal("19.99")(문자열 생성자)으로 고정할 것. new BigDecimal(19.99)는 double의 오차를 그대로 끌어들입니다.

기계적인 보완은 생성 후 치환입니다: 금액 필드에 한해 Double → BigDecimal. 같은 뿌리의 문제도 한마디: 2^53을 넘는 정수(주문번호, Snowflake ID)를 Double로 받으면 하위 자릿수가 조용히 반올림되어, 20자리 숫자의 끝 세 자리가 사라지는 것이 실측으로도 확인됩니다. 메커니즘은 JSON 큰 정수가 정밀도를 잃는 이유와 같습니다. 이런 필드는 String이나 Long으로 바꿔 숫자를 텍스트로 오가게 하세요.

필드의 성격 생성기 출력 변경 대상
금액·요율 Double BigDecimal(문자열 생성자)
주문번호·ID(2^53 초과 가능) Double String 또는 Long
수량·페이지 번호 같은 정수 Double Integer / Long
실제 과학 계산에 쓰는 부동소수 Double 유지

3. 두 번째: 이름이 안 맞으면 @JsonProperty로 고정

order_id를 orderId로 바꾸는 것은 "선의의 추측"입니다. Java 쪽은 카멜케이스를 원할 거라는 가정이죠. 그러나 Jackson은 기본적으로 JSON 키와 필드 이름을 그대로 대조하고, 맞지 않아도 오류가 나지 않으며 orderId는 null로 남습니다. 수정 방법은 두 가지:

필드 단위——@JsonProperty 하나로 하나의 매핑을 고정:

public class Root {
    @JsonProperty("order_id")
    private Long orderId;
}

전역 단위——ObjectMapper의 네이밍 전략을 바꾸면 프로젝트 전체에서 필드별 어노테이션이 불필요해집니다:

ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);

선택은 접점의 수로 결정합니다. 고정된 단일 API만 상대하면 필드 단위가 명확하고, 팀 전체가 snake_case 외부 서비스와 맞다면 전역 전략. 여기서 강조할 뒷면의 사실——이름을 잘못 쓴 실패는 "조용한 null"이지 예외가 아닙니다. jsonformater 도구 페이지는 이것을 생성기가 존재하는 이유로 꼽습니다. 기계적인 베끼기가 가장 많이 틀리는 지점이 바로 이 단계이기 때문입니다. 어노테이션을 보완한 뒤에는 최소한 핵심 필드가 null이 아님을 확인하는 단정 테스트를 하나 작성하세요.

4. 세 번째: 제네릭 소거——List를 벗겨서 역직렬화하지 마세요

생성기는 List<Item> 필드 선언을 주지만, JSON 배열을 그 List에 채우는 것은 Jackson입니다. 최상위에서 이렇게 쓰면 제네릭 소거 함정에 빠집니다:

// 오류: 런타임에 꺼내지는 요소는 LinkedHashMap
List<Item> items = mapper.readValue(json, List.class);

// 올바름: TypeReference가 제네릭 인자를 보존
List<Item> items = mapper.readValue(json, new TypeReference<List<Item>>() {});

readValue(json, List.class)는 컴파일도 실행도 됩니다. 하지만 Java 제네릭은 컴파일 시점에만 존재하고, 런타임에 Jackson이 보는 것은 List뿐이라 요소는 LinkedHashMap으로 떨어집니다. 오류는 즉시 터지지 않고, 어딘가의 item.getSku()에서 비로소 ClassCastException이 날아옵니다. 실패 위치와 보고 위치가 멀리 떨어지는 함정입니다. 올바른 세 가지 작성법:

// 1. 최상위가 배열
List<Item> items = mapper.readValue(json, new TypeReference<List<Item>>() {});

// 2. 최상위 객체의 특정 필드
JsonNode root = mapper.readTree(json);
List<Item> items = mapper.convertValue(root.get("items"), new TypeReference<List<Item>>() {});

// 3. JavaType 명시적 생성
JavaType type = mapper.getTypeFactory().constructCollectionType(List.class, Item.class);
List<Item> items = mapper.readValue(json, type);

Gson 쪽은 TypeToken이 같은 역할을 합니다. 세 편의 튜토리얼 모두 별도 항목으로 꼽는 함정입니다. 늦게 실패하고 멀리 보고되기 때문입니다.

5. 네 번째: 알 수 없는 필드——API 진화가 역직렬화를 망가뜨리게 두지 마세요

API에 필드가 하나 추가되는 것은 지극히 평범한 진화입니다. 그런데 Jackson의 기본 동작은 알 수 없는 필드에서 UnrecognizedPropertyException을 던지는 것——상대가 필드를 추가했을 뿐인데 서비스가 에러를 내기 시작합니다. 두 계층에서 끕니다:

// 클래스 단위: 이 클래스의 알 수 없는 필드만 허용
@JsonIgnoreProperties(ignoreUnknown = true)
public class Root { ... }

// 전역: mapper 전체에서 허용
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

끄면 거울상의 문제도 나타납니다: 허용하면 필드 이름의 오타도 보고되지 않습니다. 이전 절의 "조용한 null" 실패 모드가 사방으로 통합니다. 절충안은 프로덕션은 허용 끔, 테스트는 허용 켬으로 핵심 API를 한 바퀴 돌리는 것. 오타는 테스트 기간에 드러냅니다. Gson은 기본적으로 알 수 없는 필드를 무시합니다. 빠른 연동에서 편한 이유가 이것이고, 그 대가는 상대가 무엇을 추가했는지 영원히 모른다는 것입니다.

6. 다섯 번째: 날짜와 기본 생성자

생성기는 created_at을 String으로 매핑하는데, 사실 이것이 가장 안전한 기본값입니다. JSON에서 날짜는 문자열이고 형식은 ISO-8601, Unix 타임스탬프, 커스텀 세 가지가 있어 샘플로는 무엇을 원하는지 추정할 수 없습니다. 진짜 날짜 타입이 필요하면 두 줄을 보탭니다:

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
// 직렬화 시 타임스탬프 배열 대신 ISO-8601로 출력
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
public class Root {
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createdAt;
}

기본 생성자도 필수 점검 항목입니다. Jackson은 기본 생성자로 객체를 만든 뒤 필드를 하나씩 set합니다. 매개변수 있는 생성자를 직접 쓰면 기본 생성자가 사라지고 역직렬화는 즉시 실패합니다. Lombok이라면 @NoArgsConstructor를 보태고, Java 17 record를 쓴다면 Jackson 2.12+의 record 지원에 맡겨 생성자 매개변수로 매칭하므로 이 요구사항 자체가 사라집니다.

7. 프로덕션의 두 가지: ObjectMapper 재사용과 입력 검증

생성기는 클래스를 주지만, 런타임에는 아키텍처 차원의 이야기가 두 개 더 있습니다. 첫째, ObjectMapper는 생성 비용이 크고 스레드 세이프하므로 애플리케이션 전체에서 인스턴스 하나를 공유해야 합니다. 메서드 안의 new ObjectMapper()는 가장 흔한 성능 안티패턴이며, Spring Boot가 기본 주입하는 인스턴스가 바로 이 이유로 존재합니다. 네이밍 전략, JavaTimeModule, 알 수 없는 필드 정책은 모두 이 단일 인스턴스에 모으고 여기저기 흩뿌리지 마세요.

둘째, 역직렬화 성공은 타입이 맞았다는 것만 말할 뿐 값이 합법인지는 말하지 않습니다. 음수 가격, 0인 수량, 형식이 틀린 이메일——Jackson은 관여하지 않습니다. 입력 검증은 Bean Validation(hibernate-validator)에 맡기고 생성된 뼈대에 어노테이션을 보태면 됩니다:

public class CreateOrderRequest {
    @NotNull
    @DecimalMin(value = "0.01")
    private BigDecimal price;

    @Min(1)
    private Integer qty;

    @Email
    private String contact;
}

Spring에서는 @Valid만 붙이면 비즈니스 로직 진입 전에 걸러집니다. 이 계층은 생성기가 영원히 제공하지 않는 것입니다. 샘플에서 읽을 수 있는 것은 "필드의 생김새"지 "이 필드의 업무 제약"이 아니기 때문입니다.

8. 생성과 손작성의 경계

언제 생성기를 쓰고 언제 손으로 쓰는가. 경계는 의외로 분명합니다:

  • 생성기를 쓸 때: 필드가 많은 응답, 일회성 서드파티 API 연동, 빠른 프로토타입. 제거하는 것은 "기계적 베끼기 실수"라는 한 종류의 오류뿐이며, 이름과 타입이 맞춰진 뼈대를 10초에 얻습니다.
  • 손으로 쓰거나 깊이 고칠 때: 금액과 큰 정수의 타입 변경, 네이밍 전략의 전역 결정, 핵심 도메인 객체에 검증 어노테이션 추가. 이것들은 샘플에서 나올 수 없는 업무 판단입니다.
  • 양쪽의 공통 전제: 원본 JSON 자체가 유효한지 먼저 확인. 생성기는 깨진 입력으로도 그대로 생성하며 함정은 역직렬화 단계에 남습니다. 도구 페이지에 붙이기 전에 이 사이트 검증 페이지를 한 번 거치세요. 어느 쪽이든 10초입니다.

생성기는 "0에서 뼈대까지"를 해결하고, 본문의 다섯 가지는 "뼈대에서 프로덕션까지"를 해결합니다. 순서는 뒤집을 수 없고 생략도 안 됩니다.

자주 묻는 질문

생성된 클래스를 Spring Boot에서 바로 쓸 수 있나요?

컴파일은 됩니다. 런타임 전에 세 가지를 보태길 권합니다: 금액 필드를 BigDecimal로, snake_case API에는 네이밍 전략이나 @JsonProperty를, 그리고 @JsonIgnoreProperties(ignoreUnknown = true)로 API 진화에 대비. Bean Validation 어노테이션은 업무에 맞게.

왜 숫자가 전부 Double이고 long이나 BigDecimal이 아닌가요?

샘플은 "정수인지 소수인지"는 구별해도 업무적 의미는 구별하지 못합니다. 2는 수량일 수도 번호일 수도 있고, 20자리 주문번호와 19.99원은 JSON에서 모두 number입니다. 의미 판단을 사용자에게 맡기는 것이 순방향 생성기의 합리적 경계이고, 대가로 2절의 치환표를 직접 통과해야 합니다.

Lombok의 @Data와 손으로 쓴 getter/setter, 무엇을 고를까요?

팀이 이미 Lombok을 쓰면 @Data가 보일러플레이트가 가장 적습니다. 컴파일 시점 플러그인이나 다른 어노테이션 프로세서와의 충돌을 피하고 싶으면 손으로 쓰세요. Jackson에게는 차이가 없습니다. getter/setter나 필드 가시성만 봅니다.

중첩 객체가 static 내부 클래스로 생성되는데 문제없나요?

Jackson에는 영향이 없습니다. static 내부 클래스는 독립적으로 인스턴스화할 수 있습니다. 다만 장기 유지보수에서는 독립된 최상위 클래스로 패키지에 분리하는 것이 흔합니다. 재사용과 단위 테스트를 위해서입니다. 생성기의 내부 클래스 구조는 빠른 확인용이지 키워가는 코드베이스용이 아닙니다.

List 필드 역직렬화에서 ClassCastException이 나면 어떻게 찾나요?

먼저 벌거벗은 List.class 역직렬화가 아닌지 확인합니다. 요소는 실제로 LinkedHashMap이며 필드를 꺼내는 시점에 터집니다. TypeReference로 바꿔도 나면, 이 사이트의 오류 메시지 역검색 페이지에 예외를 붙여 넣고 언어별 항목을 확인하세요.