프로그래밍/Python

🔄 DRF Serializer 오류: 데이터 직렬화/역직렬화 과정에서 타입 불일치

Tiboong 2026. 2. 9. 23:05

🚨 이런 상황, 겪어보셨나요?

  • "프론트엔드에서 보낸 데이터가 분명 맞는데, Serializer에서 자꾸 ValidationError가 터집니다."
  • "API 응답에서 날짜가 문자열로 나와야 하는데, datetime 객체 그대로 나가서 프론트가 깨져요."
  • "같은 Serializer인데 POST할 때는 되고 PATCH할 때는 안 됩니다. 왜죠?"
  • "정수를 보냈는데 문자열로 인식하고, 문자열을 보냈는데 정수를 기대한다고 합니다."

Django REST Framework(DRF)를 사용하다 보면 Serializer의 타입 불일치 문제와 반드시 마주하게 됩니다. 프론트엔드와 백엔드 사이, JSON과 Python 사이, 데이터베이스와 API 사이에서 데이터의 형태가 변환되는 과정에서 예상치 못한 오류가 발생하죠.

특히 이 문제가 까다로운 이유는, 개발 환경에서는 잘 되다가 실제 사용자 데이터가 들어오면 터지는 경우가 많기 때문입니다.


🎯 DRF Serializer 타입 불일치, 왜 이렇게 자주 발생할까?

직렬화의 본질: "언어가 다른 사람들의 대화"

Serializer의 타입 불일치 문제를 이해하려면 국제 무역의 통관 절차를 떠올려보세요.

 

직렬화(Serialization) = 수출 포장

  • 한국에서 물건을 보낼 때, 국제 규격에 맞게 포장합니다
  • Python 객체(내수용)를 JSON(국제 규격)으로 변환하는 과정
  • 한국에서만 쓰는 규격(datetime, Decimal)을 국제 규격(문자열, 숫자)으로 바꿈

역직렬화(Deserialization) = 수입 통관

  • 외국에서 온 물건을 국내 규격에 맞게 검사합니다
  • JSON(국제 규격)으로 들어온 데이터를 Python 객체(내수용)로 변환
  • 허용되지 않는 물건(잘못된 타입)은 통관 거부(ValidationError)

문제는 이 "변환 과정"에서 발생합니다:

  • 수출할 때 포장을 잘못하면 (직렬화 오류) → API 응답이 깨짐
  • 수입 물건의 라벨이 잘못되어 있으면 (역직렬화 오류) → 데이터 저장 실패
  • 통관 규정을 잘못 설정하면 (필드 타입 불일치) → 정상 물건도 거부됨
  • 나라마다 규격이 다른 걸 모르면 (JSON vs Python 타입 차이) → 양쪽 다 혼란

결국 Serializer는 서로 다른 세계 사이의 통역사입니다. 통역사가 실수하면, 양쪽 모두 엉뚱한 소통을 하게 되죠.


🔍 DRF Serializer 타입 불일치의 7가지 주요 원인

1. JSON과 Python의 타입 차이 무시 - "환율 계산을 안 한 거래"

가장 기본적이면서도 가장 흔한 실수입니다.

증상:

  • Decimal 값이 API 응답에서 "12.50" 문자열로 나감
  • datetime"2025-01-15T09:30:00Z" 문자열 대신 에러 발생
  • UUID 필드가 직렬화 시 오류
  • None 처리 불일치

원인:

JSON은 기본적으로 6가지 타입만 지원합니다: 문자열, 숫자, 불리언, null, 배열, 객체. 반면 Python에는 Decimal, datetime, UUID, set, tuple 등 훨씬 다양한 타입이 있습니다. 이 간극을 Serializer가 메꿔야 하는데, 적절한 필드 타입을 지정하지 않으면 변환 과정에서 오류가 납니다.

 

비유로 이해하기:

한국에서 "만 이천오백 원"이라고 적힌 가격표를 그대로 미국으로 보내는 것과 같습니다. 환전(타입 변환)을 안 했으니, 받는 쪽에서는 이 숫자를 이해할 수 없죠.


2. 필드 타입과 모델 타입 불일치 - "주민등록증에 여권 번호 적기"

Serializer의 필드 타입이 실제 모델의 필드 타입과 맞지 않는 경우입니다.

 

증상:

  • IntegerField로 정의했는데 모델은 CharField
  • CharField에 정수가 들어와서 의도하지 않은 변환 발생
  • BooleanField"true" 문자열이 들어와서 혼란
  • FloatField vs DecimalField 정밀도 손실

원인:

ModelSerializer를 쓰면 자동으로 매핑되지만, 직접 Serializer를 정의하거나, SerializerMethodField를 사용할 때 반환 타입을 잘못 지정하면 발생합니다. 특히 프론트엔드에서 보내는 데이터 타입과 Serializer가 기대하는 타입이 다를 때 많이 발생하죠.

 

비유로 이해하기:

서류 양식에 "숫자만 입력"이라고 되어 있는데 "열두 살"이라고 적는 것과 같습니다. 의미는 통하지만, 시스템이 처리할 수 없습니다.


3. Nested Serializer의 데이터 구조 불일치 - "박스 안의 박스가 열리지 않음"

관계형 데이터를 다룰 때 중첩 Serializer에서 자주 발생하는 문제입니다.

 

증상:

  • 응답은 중첩 객체인데 요청은 ID만 보내야 함
  • {"author": {"id": 1, "name": "kim"}} 응답을 기대했는데 {"author": 1}만 나옴
  • 중첩 객체 생성 시 This field is required 에러
  • 읽기와 쓰기에서 서로 다른 데이터 구조 필요

원인:

DRF에서 관계 필드는 읽을 때와 쓸 때 동작이 다릅니다. PrimaryKeyRelatedField는 ID만 주고받고, NestedSerializer는 전체 객체를 보여줍니다. 하나의 Serializer에서 읽기와 쓰기를 모두 처리하려 하면 타입 충돌이 발생합니다.

 

비유로 이해하기:

택배를 보낼 때는 "서울시 강남구 테헤란로 123"이라는 주소(ID)만 적으면 되지만, 받는 사람은 실제 건물 이름, 층수, 호수까지 전부 알아야 찾아갈 수 있는 것과 같습니다. 보내는 형식과 받는 형식이 다른 거죠.


4. 날짜/시간 포맷 불일치 - "시차를 계산 안 한 약속"

API에서 가장 흔하고 가장 성가신 타입 문제입니다.

 

증상:

  • "2025-01-15" vs "2025/01/15" vs "15-01-2025" 포맷 혼란
  • 타임존 정보 유무에 따른 에러
  • DateField에 datetime 문자열이 들어옴
  • 프론트엔드의 JavaScript Date.toISOString()과 포맷 불일치

원인:

날짜와 시간은 표현 방법이 수십 가지입니다. DRF는 기본적으로 ISO 8601 포맷(YYYY-MM-DDTHH:MM:SSZ)을 사용하지만, 프론트엔드나 외부 시스템은 다른 포맷을 보내는 경우가 많습니다. settings.py의 날짜 포맷 설정, Serializer의 format 인자, input_formats 인자가 모두 영향을 미칩니다.

 

비유로 이해하기:

"1월 15일 오후 3시에 만나자"라고 했는데, 상대방은 미국 동부 시간으로 이해하고 나는 한국 시간으로 이해하는 상황입니다. 같은 "오후 3시"인데 실제로는 14시간 차이가 나죠.


5. 쓰기 전용/읽기 전용 필드 혼동 - "입구와 출구를 헷갈린 건물"

데이터의 방향성을 잘못 설정한 경우입니다.

 

증상:

  • read_only 필드에 데이터를 보냈는데 무시됨 (에러는 안 나지만 저장 안 됨)
  • write_only 필드가 응답에 안 나와서 프론트엔드 혼란
  • password 필드가 API 응답에 노출
  • 생성 시에는 필요하지만 수정 시에는 필요 없는 필드 처리

원인:

read_only=True, write_only=True, required 설정의 조합이 복잡해지면 혼란이 생깁니다. 특히 같은 Serializer를 생성(POST)과 수정(PATCH)에 함께 쓸 때, 필수/선택 필드 규칙이 달라야 하는 경우가 까다롭습니다.

 

비유로 이해하기:

공항에서 출국 게이트로 입국하려는 것과 같습니다. 구조물은 같지만, 진행 방향이 다르면 전혀 다른 규칙이 적용됩니다.


6. many=True와 단일 객체 혼동 - "한 명에게 보낼 편지를 단체 메일로"

리스트 데이터와 단일 데이터의 처리를 혼동하는 경우입니다.

 

증상:

  • 단일 객체를 보냈는데 Expected a list of items 에러
  • 리스트를 보냈는데 첫 번째 항목만 처리됨
  • Bulk 생성 시 일부만 성공하고 나머지는 무시
  • 단일 객체 응답인데 배열로 감싸서 내보냄

원인:

Serializer(data=[request.data](http://request.data))Serializer(data=[request.data](http://request.data), many=True)는 완전히 다른 동작을 합니다. many=True는 리스트를 기대하고, 없으면 단일 객체를 기대합니다. 프론트엔드가 때에 따라 단일 객체 또는 배열을 보내면 처리가 어려워집니다.

 

비유로 이해하기:

우체국에서 "일반 편지"와 "소포"는 접수 창구가 다릅니다. 편지를 소포 창구에 가져가면 처리가 안 되죠.


7. Custom Validation과 타입 강제 변환 충돌 - "검수 기준이 제각각"

DRF의 자동 타입 변환과 커스텀 검증 로직이 충돌하는 경우입니다.

 

증상:

  • validate_<field> 메서드에서 이미 변환된 타입을 받아 혼란
  • to_internal_value에서 처리한 것을 validate에서 다시 처리
  • 커스텀 필드의 to_representationto_internal_value 불일치
  • 에러 메시지가 실제 문제와 동떨어짐

원인:

DRF의 유효성 검사는 단계별로 진행됩니다: 필드 레벨 → validate_<field>validate. 각 단계에서 데이터 타입이 달라질 수 있는데, 이를 인지하지 못하면 잘못된 시점에 잘못된 타입의 데이터를 검증하게 됩니다.

 

비유로 이해하기:

공장에서 원자재(문자열)가 1차 가공(타입 변환)을 거쳐 부품(Python 객체)이 되었는데, 품질 검사팀(validate)이 원자재 기준으로 검사하는 것과 같습니다.


🛠 체계적인 해결 접근법

접근법 1: 읽기/쓰기 Serializer 분리

원칙: "입구와 출구에 각각 다른 안내판을"

가장 효과적이고 실무에서 가장 많이 쓰는 패턴입니다. 하나의 Serializer로 모든 것을 처리하려 하지 말고, 목적에 따라 분리하세요.

구분 읽기(Response) Serializer 쓰기(Request) Serializer
용도 API 응답 데이터 포맷팅 요청 데이터 검증 및 변환
관계 필드 Nested Serializer (상세 정보) PrimaryKeyRelatedField (ID만)
포함 필드 계산된 필드, 관련 정보 포함 저장에 필요한 필드만
예시 {"author": {"id": 1, "name": "Kim"}} {"author": 1}

 

View에서의 활용 패턴:

// ViewSet 패턴
get_serializer_class()에서 분기:
  - list, retrieve → ReadSerializer (응답용)
  - create, update → WriteSerializer (요청용)

// 또는 한 Serializer 안에서
to_representation() 오버라이드로 응답 형식 커스터마이즈

이렇게 하면 읽기와 쓰기의 타입 불일치 문제를 근본적으로 해결할 수 있습니다.


접근법 2: 명시적 타입 선언

핵심: "암묵적 변환에 의존하지 마라"

DRF는 편의를 위해 많은 자동 변환을 수행합니다. 하지만 이 자동 변환이 예상치 못한 결과를 만들 수 있습니다.

 

자동 변환의 함정:

  • CharField에 정수 123이 들어오면 → "123"으로 자동 변환 (의도한 건가?)
  • BooleanField"false" 문자열 → False로 변환 (하지만 "0"은?)
  • IntegerField"42" 문자열 → 42로 변환 (하지만 "42.5"는?)

명시적 타입 선언의 원칙:

  1. ModelSerializer를 쓰더라도 핵심 필드는 직접 선언
  2. source 인자를 명확히 지정
  3. SerializerMethodField 사용 시 반환 타입을 일관되게 유지
  4. coerce_to_string 옵션을 프로젝트 전체에 통일

접근법 3: 날짜/시간 처리 표준화

원칙: "하나의 규칙, 모든 곳에"

날짜/시간 관련 타입 문제를 근본적으로 해결하려면 프로젝트 전체에 걸친 표준이 필요합니다.

 

추천 표준:

  1. 저장: 항상 UTC로 저장
  2. 전송: ISO 8601 형식 (2025-01-15T09:30:00Z)
  3. 표시: 프론트엔드에서 사용자 로컬 시간으로 변환
  4. 입력: 다양한 포맷 허용하되, 내부적으로 통일

**settings.py 설정:**

// REST_FRAMEWORK 설정에서
DATETIME_FORMAT: "iso-8601"
DATE_FORMAT: "iso-8601"
DATETIME_INPUT_FORMATS: ["iso-8601", 추가 허용 포맷들]

 

핵심: 출력은 엄격하게, 입력은 유연하게. 보내는 데이터는 항상 일관된 포맷이어야 하지만, 받는 데이터는 여러 포맷을 허용하면 호환성이 높아집니다.


접근법 4: 에러 응답 표준화

핵심: "프론트엔드가 이해할 수 있는 에러 메시지"

Serializer의 기본 에러 메시지는 개발자에게는 유용하지만, 프론트엔드에서 사용자에게 보여주기에는 부적절한 경우가 많습니다.

 

기본 에러 메시지 문제:

// DRF 기본 에러
{"age": ["A valid integer is required."]}
{"email": ["This field must be unique."]}
{"date": ["Date has wrong format."]}

// 사용자 친화적 에러
{"age": ["나이는 숫자로 입력해주세요."]}
{"email": ["이미 사용 중인 이메일입니다."]}
{"date": ["날짜를 YYYY-MM-DD 형식으로 입력해주세요."]}

 

표준화 방법:

  • error_messages 인자로 필드별 에러 메시지 커스터마이즈
  • 공통 에러 처리 Mixin을 만들어 프로젝트 전체 적용
  • EXCEPTION_HANDLER를 커스텀하여 일관된 에러 응답 구조 유지

🐛 디버깅 실전 가이드

Step 1: 데이터 흐름 추적

Serializer 타입 문제의 디버깅은 "데이터가 각 단계에서 어떤 타입인지" 추적하는 것이 핵심입니다:

[ 프론트엔드 ] → [ HTTP 요청 ] → [ request.data ] → [ Serializer ] → [ validated_data ] → [ Model/DB ]
     ↓               ↓               ↓                 ↓                ↓                  ↓
  JS 타입        JSON 문자열       Python dict        필드별 변환         Python 객체           DB 타입
  (Number)       ("42")       ({"age": "42"})      (int: 42)         (age=42)           (INTEGER)

 

각 단계에서 확인할 것:

  1. request.data의 실제 값과 타입
  2. serializer.initial_datarequest.data가 같은지
  3. serializer.validated_data에서 타입이 올바르게 변환되었는지
  4. serializer.errors의 상세 내용

Step 2: 흔한 함정 체크리스트

확인 항목 흔한 실수 해결 방법
Content-Type 헤더 form-data로 JSON 보냄 application/json 확인
문자열 숫자 "42" vs 42 프론트엔드에서 parseInt() 처리
빈 문자열 vs null "" vs null 처리 차이 allow_blank, allow_null 명시
boolean 문자열 "true" vs true BooleanField 자동 변환 확인
배열 단일값 [1] vs 1 many=True 여부 확인
중첩 객체 vs ID 응답 구조를 요청에 그대로 사용 읽기/쓰기 Serializer 분리

Step 3: DRF 내부 처리 순서 이해

Serializer 내부의 유효성 검사 순서를 이해하면 디버깅이 훨씬 쉬워집니다:

1. to_internal_value()  → 각 필드의 타입 변환 (문자열 → Python 타입)
2. Field.run_validators() → 각 필드의 validators 실행
3. validate_<field>()   → 필드별 커스텀 검증 (이미 변환된 타입)
4. validate()           → 전체 데이터에 대한 교차 검증

 

핵심 포인트: validate_<field>() 메서드에 도달하는 데이터는 이미 Python 타입으로 변환된 상태입니다. 문자열 "42"가 아니라 정수 42가 들어옵니다. 이 순서를 모르면 잘못된 타입을 기준으로 검증 로직을 작성하게 됩니다.


✅ DRF Serializer 타입 안정성 베스트 프랙티스 체크리스트

설계 원칙

  • 읽기(Response)와 쓰기(Request) Serializer를 분리했는가
  • 관계 필드의 읽기/쓰기 동작을 명확히 정의했는가
  • ModelSerializer 사용 시에도 핵심 필드를 명시적으로 선언했는가
  • SerializerMethodField의 반환 타입이 일관적인가

타입 처리

  • 날짜/시간 포맷이 프로젝트 전체에서 통일되었는가
  • Decimal vs Float 사용 기준이 명확한가
  • allow_null, allow_blank, required 조합이 의도대로인가
  • coerce_to_string 설정이 프로젝트 수준에서 통일되었는가

관계 필드

  • Nested Serializer의 읽기/쓰기 동작을 구분했는가
  • many=True 사용이 적절한가
  • source 인자가 올바르게 설정되었는가
  • 역참조 관계의 직렬화 깊이를 제한했는가

에러 처리

  • 커스텀 에러 메시지가 사용자 친화적인가
  • 에러 응답 형식이 프로젝트 전체에서 일관적인가
  • 타입 에러의 원인을 쉽게 파악할 수 있는 로그가 있는가

테스트

  • 다양한 입력 타입에 대한 테스트가 있는가 (문자열 숫자, null, 빈 문자열 등)
  • 잘못된 타입 입력에 대한 에러 응답 테스트가 있는가
  • Nested Serializer의 생성/수정/조회 모두 테스트했는가
  • 날짜/시간 다양한 포맷 입력 테스트가 있는가

API 문서화

  • 각 필드의 예상 타입이 API 문서에 명시되었는가
  • 요청(Request)과 응답(Response)의 구조가 별도로 문서화되었는가
  • 필수/선택 필드 구분이 명확한가
  • 에러 응답 형식이 문서화되었는가

📊 Serializer 타입 문제 빠른 참조표

증상 가능한 원인 첫 번째 확인 사항
A valid integer is required 문자열이 IntegerField에 들어옴 프론트엔드 데이터 타입 확인
Date has wrong format 날짜 포맷 불일치 input_formats 설정 확인
This field is required (중첩) Nested Serializer에 ID만 전달 읽기/쓰기 Serializer 분리
Expected a list of items 단일 객체에 many=True 요청 데이터 구조 확인
필드가 응답에 없음 write_only=True 설정 필드의 read/write 설정
데이터 저장 안 됨 (에러 없음) read_only=True 필드에 쓰기 시도 필드의 read_only 설정
Decimal이 문자열로 나감 coerce_to_string=True 기본값 REST_FRAMEWORK 설정
PATCH에서 필수 필드 에러 partial=True 미설정 View에서 partial 인자 전달

💡 핵심 요약

DRF Serializer 타입 불일치 문제를 다루는 핵심은 "JSON과 Python은 다른 언어이며, Serializer는 그 사이의 통역사"라는 점을 항상 의식하는 것입니다.

  1. 읽기와 쓰기를 분리하세요: 하나의 Serializer로 모든 것을 해결하려는 유혹을 버리세요. 입력과 출력의 형태가 다른 것은 자연스러운 일입니다.
  2. 명시적으로 선언하세요: 자동 변환에 의존하지 말고, 필드 타입을 직접 선언하세요. 코드가 조금 길어지더라도 예측 가능성이 높아집니다.
  3. 날짜/시간은 ISO 8601로 통일하세요: 프로젝트 전체에서 하나의 포맷을 쓰면, 타입 문제의 절반은 사라집니다.
  4. 데이터 흐름을 추적하세요: 문제가 생기면 각 단계에서 데이터의 타입과 값을 확인하세요. 대부분의 문제는 "어디서 타입이 바뀌었는지" 찾으면 해결됩니다.

Serializer는 API의 "현관문"입니다. 현관문이 제대로 작동하면 집 전체가 안전하고, 현관문이 고장 나면 모든 것이 무너집니다.


🤝 Django REST API, 전문가와 함께하세요

Serializer 설계부터 API 아키텍처까지, REST API 개발은 세심한 설계가 필수입니다.

이런 고민이 있으시다면:

  • Serializer 구조를 어떻게 잡아야 할지 막막하다면
  • API 타입 에러가 반복되어 프론트엔드와 소통이 어렵다면
  • 프로젝트 규모가 커지면서 Serializer 관리가 복잡해졌다면
  • 성능과 유지보수성을 모두 갖춘 API를 설계하고 싶다면

크몽에서 Django 전문 컨설팅을 제공합니다. 실제 운영 경험을 바탕으로, 여러분의 서비스에 맞는 최적의 API 아키텍처를 함께 설계해 드립니다.

👉 [크몽에서 Django 컨설팅 문의하기]