본문 바로가기
개발 노트/Backend,CMS,API

[API 개발] DB Model을 그대로 반환하면 안 되는 이유 (DTO와 Serializer로 응답 계약 고정하기)

by pposooj 2026. 5. 20.

웹이랑 모바일 앱, 서버까지 이래저래 같이 잡고 운영하다 보면 API 응답 구조 때문에 꼭 한 번씩 골치 아픈 일이 생깁니다.

처음 API 만들 때는 그냥 DB에서 조회한 모델(Model) 객체를 그대로 JSON으로 던지는 게 가장 편합니다. 코드 한두 줄이면 끝나고 개발 속도도 엄청 빠르니까요. 저도 예전에는 초기 빌드할 때 종종 그렇게 고집을 부렸습니다.

하지만 모바일 앱과 연동되는 API라면 이 방식은 머지않아 시한폭탄이 됩니다. DB 컬럼 하나 바꿨다고 이미 배포된 앱이 죽어버리거나, 관리자용 내부 메모 같은 민감한 필드가 앱 패킷에 그대로 노출되는 사고가 터지기 때문입니다.

자꾸 까먹기도 하고, 다음 프로젝트에서 실수하지 않기 위해 DTO와 Serializer를 활용해 API 응답 형식을 안전하게 고정하는 기준을 정리해 둡니다.

1. 내가 겪었던 문제: Model을 그대로 반환할 때의 위험성

라라벨(Laravel)이나 다른 프레임워크를 쓸 때 아래처럼 짜는 경우가 제일 흔합니다.

PHP
 
public function show(int $id)
{
    // DB 모델을 그대로 조회해서 바로 JSON으로 반환
    $notice = Notice::findOrFail($id);

    return response()->json($notice);
}

이러면 당장은 잘 돌아가는 것처럼 보입니다. 하지만 운영하다 보면 아래와 같은 문제가 무조건 터집니다.

  • 내부 운영 필드 노출: DB 테이블에 있던 admin_memo(관리자 메모), created_by(작성자 ID), is_deleted 같은 앱 화면에 전혀 필요 없는 값들이 통째로 내려갑니다. 보안 검수할 때 무조건 지적받는 지점입니다.
  • 앱 코드와의 강한 결합: 기획이 바뀌어서 DB 컬럼명을 published_at에서 opened_at으로 바꾸는 순간, 구버전 앱을 쓰는 사용자들은 공지사항 날짜가 안 나오는 버그를 겪게 됩니다. 앱은 웹과 달라서 한 번 배포하면 사용자가 업데이트를 안 할 수도 있기 때문에 응답 계약(Contract)을 유지하는 게 핵심입니다.

2. 해결 방법: Serializer로 공개 필드만 필터링하기

가장 좋은 방법은 DB 모델이 앱 응답으로 직접 나가지 못하게 중간에 차단벽을 세우는 것입니다. DTO(Data Transfer Object)나 Serializer를 사용해 "앱 화면에 딱 필요한 데이터만 골라서" 응답 구조를 짜야 합니다.

아래는 이해하기 쉽게 구조를 단순화한 예시 코드입니다.

PHP
 
final class NoticeSerializer
{
    // 목록 화면용 응답 구조
    public static function listItem(object $notice): array
    {
        return [
            'id' => (int) $notice->id,
            'title' => (string) $notice->title,
            'summary' => (string) ($notice->summary ?? ''),
            'publishedAt' => optional($notice->published_at)->toIso8601String(),
        ];
    }

    // 상세 화면용 응답 구조
    public static function detail(object $notice): array
    {
        return [
            'id' => (int) $notice->id,
            'title' => (string) $notice->title,
            'body' => (string) ($notice->body ?? ''),
            'publishedAt' => optional($notice->published_at)->toIso8601String(),
        ];
    }
}

이렇게 분리해 두면 DB 컬럼명이 어떻게 바뀌든 프론트엔드나 모바일 앱이 받는 JSON 키 값(publishedAt 등)은 그대로 유지할 수 있습니다. 백엔드 내부 사정 때문에 앱 코드까지 건드릴 필요가 없어지는 거죠.

3. 실무 운영 시 놓치기 쉬운 체크포인트

이래저래 서비스를 운영하면서 깨달은 몇 가지 기준들입니다. API 설계할 때 규칙을 미리 정해두는 편이 좋습니다.

① 목록과 상세 응답은 무조건 분리하기

하나의 Serializer로 목록과 상세를 같이 퉁치려고 하면 성능이나 데이터 낭비가 심해집니다.

  • 목록 API: 카드나 리스트만 빠르게 보여줘야 하므로 id, title, summary 같은 필수 필드만 가볍게 담아야 합니다. (페이징 필수)
  • 상세 API: 본문 전체(body)나 고화질 이미지 리스트 등 무거운 데이터를 포함합니다.

② null과 default 처리 기준 고정하기

값이 없을 때 어떤 API는 null을 주고, 어떤 API는 아예 필드를 누락시키거나 빈 문자열("")을 주면 앱 개발자분이 파싱하다가 에러를 내기 십상입니다.

PHP
 
final class ImageSerializer
{
    public static function fromModel(?object $image): ?array
    {
        if (!$image) {
            return null; // 데이터 자체가 없을 땐 명확히 null 반환
        }

        return [
            'url' => (string) $image->public_url,
            'alt' => (string) ($image->alt_text ?? ''), // null 대신 빈 문자열로 기본값 처리
        ];
    }
}

값이 아예 없는 것과, 값은 있지만 내용이 비어있는 것을 화면에서 구분해야 하는지 기획에 맞춰 기준을 똑같이 맞춰야 앱 코드가 깔끔해집니다.

③ 페이지네이션(Pagination) meta 구조 통일

목록 API를 만들 때 페이징 정보 구조가 제각각이면 앱에서 공통 모듈을 만들 수가 없습니다. 항상 data와 meta 영역을 나누어 고정하는 게 편합니다.

PHP
 
return response()->json([
    'data' => $items->map(fn ($notice) => NoticeSerializer::listItem($notice))->values(),
    'meta' => [
        'page' => $page,
        'limit' => $limit,
        'hasNext' => $hasNext,
    ],
]);

4. 어떤 필드를 숨기고 어떤 필드를 보낼 것인가?

기준은 단순합니다. "숨겨도 되는 값을 찾는 게 아니라, 앱 화면에 꼭 필요한 값만 허용 리스트(White-list) 방식으로 공개한다"고 접근해야 안전합니다.

필드 유형 앱 응답 포함 여부 이유
관리자 메모, 내부 workflow 상태 제외 내부 운영 정보이므로 노출 시 보안 위험
작성자/수정자 계정 ID 제외 시스템 내부 식별자는 앱에 보여줄 이유가 없음
삭제 여부 (is_deleted) 제외 API 조회 단계에서 이미 필터링해서 내려주어야 함
제목, 본문, 이미지 URL 포함 실제 앱 화면을 그리는 데 필수적인 리소스

5. 테스트 코드 작성할 때 팁

Serializer를 따로 분리해 두면 좋은 점이 또 하나 있습니다. 굳이 무거운 통합 테스트나 DB 전체를 띄우지 않아도 단위 테스트(Unit Test) 수준에서 응답 구조를 아주 쉽게 검증할 수 있다는 점입니다.

  • 응답 포맷에 필요한 필드가 다 들어있는지 확인
  • (중요) admin_memo 같은 내부 필드가 실수로라도 포함되어 나오지 않는지 검증 (assertArrayNotHasKey 활용)
  • 날짜 포맷이 전부 ISO-8601(YYYY-MM-DDTHH:mm:ssZ) 형식으로 일관되게 찍히는지 확인

특히 내려가면 안 되는 필드가 차단되었는지 반대 케이스를 테스트하는 버릇을 들이면 API 보안 사고를 정말 많이 막을 수 있습니다.

짧은 마무리

초기 개발 단계에서는 모델을 그대로 던지는 게 한없이 편하게 느껴집니다. 하지만 서비스가 오픈하고 배포된 이후에는 그 API 응답 형식 자체가 깨뜨려서는 안 되는 엄격한 '계약'이 됩니다.

조금 귀찮더라도 처음부터 DTO와 Serializer를 구축해서 구조를 떼어놓는 게 장기적으로 내 야근을 줄이고 서버와 앱 모두 평화로워지는 가장 확실한 방법입니다.