안드로이드/에러

Gson vs Kotlinx Serialization — 누락 키·null·추가 키·예외 한 번에 끝내기

코딩하는후운 2025. 8. 18. 16:26
반응형

문제 : Kotlinx Serialization 에서 MissingFieldException가 발생 하였다.

DTO에 키가 있는데 Response에 key가 없어서 발생한 사례입니다.

안드로이드 개발에서 많이 쓰이는 Gson과 Kotlinx Serialization, 두 라이브러리는 이 과정에서 눈에 띄는 차이를 보입니다.
특히 JSON에 특정 키가 없거나 null 값이 올 때 어떻게 동작하는지 이해하는 것이 중요합니다.
잘못하면 앱이 죽는(크래시) 원인이 될 수 있거든요.

그리고, 추가키(정의되지 않는 키)가 내려와도 UnknownFieldException이 발생한다.

왜 예외가 나나? (kotlinx.serialization의 규칙)

1. 필드가 빠지면: MissingFieldException

  • DTO에 정의된 프로퍼티(필드)가 JSON 응답에 아예 없으면 MissingFieldException이 발생합니다.
  • [❌ 가장 많이 혼동하는 부분] val name: String? 처럼 타입을 널러블(?)로 선언하는 것만으로는 충분하지 않습니다.
    • val name: String? : "이 필드는 null 값을 허용한다"는 뜻이지, "이 필드가 없어도 된다(Optional)"는 뜻이 아닙니다. 이 필드는 **여전히 필수(Required)**입니다. JSON에 name 키가 아예 없으면 MissingFieldException이 발생합니다.
    • val name: String? = null : "이 필드는 null 값을 허용하며, JSON에 키가 없어도 된다"는 뜻입니다. 키가 없으면 기본값인 null이 할당됩니다.
  • 결론: 필드를 '옵션(Optional)'으로 만들고 싶다면, 반드시 = null 이나 = -1 같은 명시적인 기본값을 할당해야 합니다.

2. 추가(모르는) 키가 있으면: UnknownFieldException

  • Gson은 DTO에 정의되지 않은 키(e.g., "new_ad_field": "...")가 JSON에 있어도 조용히 무시하고 파싱을 성공시킵니다.
  • 하지만 kotlinx.serialization은 기본적으로 DTO에 정의되지 않은 키가 오면 "이건 내가 모르는 필드"라며 UnknownFieldException 예외를 발생시킵니다.
  • 이는 서버 API 명세가 바뀌었음을 개발자에게 알려주는 안전장치 역할을 하기도 합니다.

해결 방법 : ignoreUnknownKeys 추가. 아래 방법 있습니다.

Gson은 왜 안 터지지?

Gson은 기본적으로 관대(lenient) 합니다.
없는 항목은 각 타입의 기본값으로 채우고(객체=null, 숫자=0, 불린=false), 파싱을 이어갑니다.
그래서 “파싱 시점”엔 잘 지나가고, 나중에 그 값을 쓸 때 NPE가 날 수 있죠.

Gson = 관대: 누락/null/추가 키가 와도 파싱은 대체로 성공. 대신 값이 기본값/null로 들어가고, 나중 사용 시 NPE가 터질 수 있음.
kotlinx.serialization = 엄격: 기본값 없는 프로퍼티는 필수로 취급 → 누락/null이면 즉시 예외. 대신 DTO 기본값이나 옵션을 명시하면 안전.

JSON 상황 Gson 결과 kotlinx.serialization 결과(기본설정) 이유 실무 해결
키가 없음 ("order" 자체 없음) 파싱 성공. 프리미티브는 기본값(0/false), 레퍼런스는 null 예외: MissingFieldException (기본값이 없으면 필수 필드) Gson은 관대/리플렉션, Kotlinx는 생성자기반+옵셔널 판정 DTO에 기본값 넣어 옵션화 val order: Int = -1
값이 null ("order": null) 프리미티브(비-null Int/Boolean)는 기본값(0/false), 레퍼런스/nullable은 null (파싱 성공) 예외: JsonDecodingException (non-null에 null 불가) non-null은 null 불허 1. explicitNulls=false로 null을 ‘부재’처럼 처리 → 기본값 적용,
2. 또는 Int?로 받고 매퍼에서 ?: -1
추가(모르는) 키 보통 무시 예외 (기본값) 엄격 모드라 미정의 키 오류 Json { ignoreUnknownKeys = true }
타입 불일치 ("order":"5") 케이스에 따라 시도/실패 예외: JsonDecodingException 타입 안 맞음 서버 수정 or 커스텀 시리얼라이저로 보정

- Kotlinx에서 non-null이면 예외를 “던진다”(자동 예외처리 X). DTO 기본값이 없고 키가 없으면 예외를 “던진다”.

 

설정/코드로 안전하게 쓰는 법 (kotlinx.serialization)

1) “키 없음”에 안전해지는 가장 쉬운 방법

@Serializable
data class HomeSectionDto(
  @SerialName("order") val order: Int = -1,    // 기본값 → 옵션 필드로 간주
  @SerialName("section") val section: String = "",
  // 하위 DTO도 기본값 제공 필요
)

- 기본값이 있으면 → 그 프로퍼티는 옵셔널이라 키가 없어도 통과.

2) "order": null도 예외 없이 받으려면
옵션 A — 전역 설정 완화

val json = Json {
  explicitNulls = false   // null을 '없는 것'처럼 취급 → 기본값 사용
}

- explicitNulls = false → 명시적 null부재처럼 취급해 기본값/nullable 로 대입.

옵션 B — 프로퍼티를 nullable로 받고 매퍼에서 보정

@Serializable
data class HomeSectionDto(@SerialName("order") val order: Int? = null)

fun HomeSectionDto.asDomain() = HomeSection(order = order ?: -1)

- 전역 설정을 건드리지 않고 명시적으로 제어할 수 있음.

3) 추가 키가 와도 무시하고 싶다면 UnknownFieldException

val json = Json { ignoreUnknownKeys = true }

Retrofit 설정 전역 Kotlinx 설정 예

@OptIn(ExperimentalSerializationApi::class)
@Provides
fun provideResponseJsonConverterFactory(): Converter.Factory =
    Json {
        ignoreUnknownKeys = true  // 추가 키 무시
        explicitNulls = false     // "null"을 부재 취급 → 기본값/nullable로
    }.asConverterFactory("application/json".toMediaType())

 

궁금했던 것

Q. Gson이면 null-safe 안 해도 되나요?
A. 아니요. 파싱은 통과하지만 레퍼런스 타입에 null이 들어오면 사용 시 NPE로 죽습니다.

Q. Kotlinx에서 non-null 프로퍼티면 null은?
A. 예외를 던집니다. 자동 예외처리(흡수) 아닙니다.

Q. "order": null을 기본값으로 바꾸고 싶어요.
A. explicitNulls=false + val order: Int = -1 or val order: Int? = null + 매퍼 보정.

반응형