[Jackson] Bug - JSON 응답이 {}(빈 객체)로 나오는 문제.

하쮸·2025년 12월 30일

Error, Why, What, How

목록 보기
61/68

1. 현재 상황.

  • JSON 응답이 위처럼 {} 빈 객체로 응답되는 문제점.

  • 디버깅을 통해 확인해 봐도 정상적으로 응답객체에 값이 있음.

1-1. 문제의 핵심.

  • Java 객체에는 정상적으로 값이 들어있지만 JSON으로 응답이 안됨.
    • 즉, 직렬화 (Serialization)가 제대로 안 되고 있음.
      • JSON ----> Java Object (역직렬화, Deserialization)
      • JSON <---- Java Object (직렬화, Serialization)

2. Jackson.

  • Spring Boot의 경우 JSON 매핑 라이브러리로 Jackson이 기본(default)값으로 되어 있음.
    • 또한 Jackson 3에 대한 자동 설정(auto-configuration)이 제공되며 Jackson은 spring-boot-starter-json에 포함되어 있음.
    • 클래스패스(classpath)에 Jackson이 존재하면 JsonMapper 빈(Bean)이 자동으로 구성됨.
      • 즉, implementation 'org.springframework.boot:spring-boot-starter-json'으로 의존성 등록이 되어 있으면 Spring Boot가 개발자 설정 없이도 JSON 변환용 객체(JsonMapper)를 자동으로 만들어서 관리해 준다는 의미임.

2-1. 직렬화/역직렬화.

  • 필드(멤버변수)를 직렬화(JSON <-- Java Object), 역직렬화(JSON --> Java Object) 모두 가능하게 만드는 가장 간단한 방법은 해당 필드를 public으로 선언하는 것.
public class MyDtoAccessLevel {
    private String stringValue;
    int intValue;
    protected float floatValue;
    public boolean booleanValue;
    // setter / getter 없음
}
  • 위 클래스의 필드 중 public 접근제어자가 사용된 필드인 booleanValue만 JSON으로 직렬화(JSON <-- Java Object)됨.

  • 만약 Getter가 있으면 Non-Public 필드직렬화(JSON <-- Java Object)와 역직렬화(JSON --> Java Object)가 가능함.
    • 즉, 접근제어자가 public이 아니여도 getter를 추가하면 직렬화(JSON <-- Java Object) 대상이 됨.
public class MyDtoWithGetter {
    private String stringValue;
    private int intValue;

    public String getStringValue() {
        return stringValue;
    }
}
  • 위 경우 stringValue는 getter가 있으므로 직렬화(JSON <-- Java Object)가 되지만 intValue는 getter가 없으므로 직렬화(JSON <-- Java Object)되지 않음.

  • 여기서 중요한 점은 getter가 존재하면 해당 private 필드는 역직렬화(JSON --> Java Object)도 가능해짐.

    • 왜냐하면 Jackson에서 getter가 있는 필드는 하나의 “property”로 인식되기 때문.

  • 반대로 setter만 존재하는 경우에는 역직렬화(JSON --> Java Object)만 가능하고 직렬화(JSON <-- Java Object)는 되지 않음.
public class MyDtoWithSetter {
    private int intValue;

    public void setIntValue(int intValue) {
        this.intValue = intValue;
    }

    public int accessIntValue() {
        return intValue;
    }
}
  • 위의 경우 accessIntValue()는 표준 getter(getIntValue)가 아니므로 Jackson은 이를 직렬화(JSON <-- Java Object)용 getter로 인식하지 않음.

  • 직렬화(JSON <-- Java Object) 또는 역직렬화(JSON --> Java Object) 중 한쪽만 무시하기

  • 일반적으로 필드를 완전히 무시하려면 @JsonIgnore를 사용함.

    • 하지만 직렬화(JSON <-- Java Object) 또는 역직렬화(JSON --> Java Object) 중 한쪽만 제외해야 하는 경우도 있음.
    • Ex) 비밀번호처럼 JSON 응답에는 포함되면 안 되지만 요청에서는 받아야 하는 경우.
@JsonIgnore
public String getPassword() {
    return password;
}

@JsonProperty
public void setPassword(String password) {
    this.password = password;
}
  • getter에 @JsonIgnore 에노테이션을 추가해서 응답 JSON에는 포함되지 않도록 설정하였고 setter에 @JsonProperty 에노테이션을 사용하여 해당 필드에 대한 역직렬화를 활성화 한 것.
    • 즉, 직렬화(JSON <-- Java Object)는 안 되지만 역직렬화(JSON --> Java Object)는 됨.

2-2. Jackson의 Property

  • Java와 Jackson 라이브러리 관점에서 필드(Field)프로퍼티(Property)는 엄연히 다른 개념임.

    • 필드(Field)
      • 클래스 내부에 선언된 변수 그 자체를 의미.
        • Ex) private String name;
        • 이는 메모리 공간에 변수를 할당했음을 나타냄.
    • 프로퍼티(Property)
      • 객체의 데이터 상태를 의미.
      • 일반적으로 메서드(Getter/Setter)를 통해 외부에 노출되는 개념임.
  • Jackson은 기본적으로 자바의 Bean 규약을 따름.

    • 이 규약에서 프로퍼티(Property)는 '필드'가 아니라 Getter와 Setter 메서드를 기준으로 결정됨.

  • Jackson에서 getter가 있는 필드는 하나의 “property”로 인식되기 때문.
    • Jackson이 클래스를 훑어 볼 때 private String name;이라는 필드만 보고
      "아 name이라는 데이터를 처리해야지"라고 생각하지 않음.
  • 아래와 같은 과정을 거침.
    • 클래스에 getName()이라는 메서드가 있는걸 발견.
    • Jackson은 "어? get 다음에 Name이 붙었네? 그럼 이 객체는 name이라는 프로퍼티(데이터)를 가지고 있구나!"라고 판단함.
  • 이렇게 프로퍼티(property)로 인식되는 순간 Jackson은 해당 데이터를 JSON의 "name" 키(key)와 매핑할 권한을 갖게 됨.

2-2-1. Getter만으로 역직렬화가 어떻게 가능?

  • 왜 Getter만 있는데 역직렬화(JSON --> Java Object)가 가능한가?

    • 원래 역직렬화(JSON --> Java Object)는 값을 넣어줘야 하니 Setter가 필요할 것 같지만 Jackson은 똑똑하게 동작함.
  • 위에서 언급한 getName()이라는 getter 덕분에 name이 프로퍼티로 등록되면 Jackson은 "이 객체는 name 데이터를 취급하겠다"라고 동작함.

    • 이후 JSON 데이터를 Java 객체로 바꿀 때, 즉 역직렬화할 때 name 프로퍼티에 값을 넣어야 하는데 Setter가 없다면?
      • 이미 프로퍼티로 등록된 상태이기 때문에 Jackson은 내부적으로 리플렉션(Reflection) API 기능을 사용해 private String name 필드에 값을 직접 삽입할 수 있게 됨.

3. 해결.

  • WeatherResponse 클래스에만 @Getter를 추가한 경우.

  • WeatherResponse, CurrentDto 클래스에 @Getter를 추가한 경우.

  • WeatherResponse, CurrentDto, ForecastDto 클래스에 @Getter를 추가한 경우.

4. 참고.

profile
Every cloud has a silver lining.

0개의 댓글