참조 (Reference) 패턴 - NoSQL 문서 크기 한계와 외부 링크 저장

핵심 인사이트 (3줄 요약)

  1. 본질: 참조(Reference) 패턴은 MongoDB 같은 문서형(Document) NoSQL에서 연관된 데이터(1:N)를 부모 문서 안에 통째로 집어넣는 임베디드(Embedded) 패턴의 한계를 극복하기 위해, 연관 데이터를 별도의 독립된 컬렉션(Collection)에 저장하고 자식 문서의 ID(Object ID)만 부모 문서에 링크(참조) 형태로 저장하는 데이터 모델링 기법이다.
  2. 가치: NoSQL의 치명적 약점인 "한 번에 저장할 수 있는 단일 문서의 물리적 크기 한계(예: 16MB)"를 돌파하게 해주며, 부모와 자식 배열이 무한히 자라나는 언바운디드 어레이(Unbounded Array)로 인해 발생하는 메모리 폭발(OOM)과 쓰기/수정 병목(Write Amplification)을 원천적으로 차단한다.
  3. 융합: 참조 패턴은 관계형 DB(RDBMS)의 정규화(Normalization) 철학과 매우 유사하지만, 분산 환경에서 DB 수준의 조인(JOIN) 연산을 피하기 위해 백엔드 코드 단에서 데이터를 두 번 호출해 조립하는 애플리케이션 조인(Application-level Join) 아키텍처와 반드시 융합되어야 한다.

Ⅰ. 개요 및 필요성 (Context & Necessity)

  • 개념: NoSQL 설계의 황금률이 '임베디드(내장)'라지만, 언제나 그럴 수는 없다. 게시글(Post) 하나에 댓글(Comment)이 1,000만 개가 달렸다고 치자. 1,000만 개의 댓글 객체를 게시글 JSON 덩어리 하나에 전부 [{}, {}, ...] 배열로 우겨 넣으면(임베딩), 이 게시글을 조회할 때마다 수 기가바이트(GB)의 거대한 파일이 메모리로 올라오며 서버가 뻗는다. 따라서 댓글 데이터는 다른 창고(컬렉션)에 저장하고, 게시글 안에는 "comments": [id_1, id_2, id_3] 처럼 ID 리스트(포인터)만 적어두는 것이 바로 참조(Reference) 패턴이다.

  • 필요성: 클라우드 시대의 데이터는 기하급수적으로 자라난다. MongoDB는 설계상 단일 BSON 문서 크기를 16MB로 엄격하게 제한(Limit)한다. 한 문서를 너무 크게 만들면 네트워크 전송 비용과 메모리 직렬화/역직렬화 비용이 디스크 I/O 이점을 모조리 갉아먹기 때문이다. 따라서 "배열(Array)의 크기를 예측할 수 없거나, 무한히 커질 수 있는(Unbounded) 데이터 모델"에서는 억지로 데이터를 한 덩어리로 합치지 말고 논리적으로 찢어내는(정규화하는) 유연성이 절대적으로 필요하다.

  • 💡 비유: 백팩(부모 문서) 안에 여행 짐을 싸는 상황을 상상해 봅시다.

    • 임베디드 패턴: 백팩 안에 속옷, 칫솔, 여권(작은 데이터)을 통째로 집어넣어 한 번에 매고 다닙니다. 아주 빠르고 편합니다.
    • 참조(Reference) 패턴: 내가 키우는 100kg짜리 코끼리(무한히 커지는 데이터)를 백팩에 억지로 쑤셔 넣으면 가방이 찢어집니다(16MB Limit 에러). 코끼리는 동물원에 따로 놔두고, 백팩 안에는 "코끼리는 동물원 3번 우리에 있음"이라는 포스트잇(참조 ID)만 하나 넣어두는 현명한 분리 보관법입니다.
  • 등장 배경 및 발전 과정:

    1. 임베디드 만능주의의 부작용: 초기 NoSQL 도입 시, 개발자들이 RDBMS의 외래키(FK)를 버리고 무조건 모든 데이터를 JSON 하나로 뚱뚱하게 임베딩했다가 운영 중 문서 폭발(Document Too Large) 장애를 무수히 겪었다.
    2. 패턴의 세분화: 1:N 관계에서 N이 수십 개인 One-to-Few(임베딩), N이 수만 개인 One-to-Many(자식 ID 참조), N이 수억 개인 One-to-Squillions(부모 ID 참조) 등으로 참조 방향을 바꾸는 고도화된 스키마 디자인 원칙이 정립되었다.
    3. Application Join의 성숙: 참조 패턴으로 찢어진 데이터를 백엔드의 GraphQL이나 비동기 프로미스(Promise.all)를 통해 찰나의 순간에 메모리에서 엮어버리는(API Composition) 개발 패턴이 표준화되었다.
  • 📢 섹션 요약 비유: 두꺼운 백과사전(단일 문서) 1권에 세상의 모든 지식을 다 쑤셔 넣으면 책이 찢어지거나 무거워서 들 수가 없습니다. 차라리 1권, 2권, 3권(별도 컬렉션)으로 가볍게 나누고 권말에 색인(Reference ID)을 만들어 찾는 책만 꺼내 보게 하는 것이 오래가는 도서관(DB) 정리법입니다.


Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)

One-to-Many 관계의 3가지 참조 패턴 전략

NoSQL에서 1:N 관계를 마주했을 때 N의 크기(데이터 증가 폭)에 따라 참조하는 '방향'과 '전략'을 완전히 다르게 가져가야 한다.

  ┌───────────────────────────────────────────────────────────────┐
  │         N의 크기에 따른 NoSQL 참조(Reference) 아키텍처 전략          │
  ├───────────────────────────────────────────────────────────────┤
  │                                                               │
  │  [ 1. One-to-Few (N이 작고 고정됨) ─▶ Embedded 패턴 ]             │
  │    - 예: 유저 1명과 집/회사 주소 2개. (더 이상 늘어나지 않음)           │
  │    [ User Doc ]                                               │
  │    { name: "Alice", addresses: [ {서울..}, {부산..} ] }        │
  │                                                               │
  │  [ 2. One-to-Many (N이 수천 개) ─▶ Child-Referencing 패턴 ]       │
  │    - 예: 쇼핑몰 상품(1)과 달린 리뷰(1000개).                        │
  │    - 부모가 자식들의 ID 배열을 가짐.                              │
  │    [ Product Doc ]                                            │
  │    { name: "노트북", review_ids: [ ObjectId(1), ObjectId(2)... ] }│
  │    [ Review Doc 1 ], [ Review Doc 2 ] ... (별도 저장)             │
  │    ▶ 배열이 수천 개면 좀 크지만, 16MB 한도를 뚫지는 않으므로 타협 가능.    │
  │                                                               │
  │  [ 3. One-to-Squillions (N이 수억 개!) ─▶ Parent-Referencing 패턴 ]│
  │    - 예: 유명 연예인(1)과 천만 명의 팔로워(10,000,000명).             │
  │    - 부모 문서에 1천만 개의 ID 배열을 넣으면 문서 크기 16MB 폭발! 💥     │
  │    - 해결: RDBMS처럼 자식이 부모의 ID 딱 1개만 갖도록 역참조 설계.         │
  │    [ User Doc (부모) ] { name: "아이유" }  // (배열 자체가 없음)    │
  │    [ Follower Doc 1 (자식) ] { name: "홍길동", target_id: "아이유" }│
  │    [ Follower Doc 2 (자식) ] { name: "김철수", target_id: "아이유" }│
  └───────────────────────────────────────────────────────────────┘

[다이어그램 해설] NoSQL 설계의 꽃은 **"배열(Array)이 무한히 자라날 가능성이 있는가(Unbounded)?"**를 사전에 예측하는 것이다. 상품 리뷰가 1,000개 수준이라면 부모 쪽에 review_ids 배열을 만들어 자식 ID를 1,000개 넣는 것(Child Referencing)이 애플리케이션 코딩하기 편하다. 하지만 인스타그램 팔로워나 수년간 쌓이는 IoT 센서 로그처럼 자식이 수억 개로 커지면, 부모 문서의 뱃속 배열이 폭발한다. 이때는 무조건 3번(Parent Referencing) 방식으로 넘어가 자식 쪽에 부모의 ID(외래키 역할)를 달아주고 페이징(Pagination)이나 인덱스(Index) 기반으로 찢어서 읽어와야(정규화) 시스템이 생존할 수 있다.


애플리케이션 레벨 조인 (Application-Level Join)

참조(Reference) 패턴으로 데이터를 찢어놓았으니, 화면에 보여주려면 데이터를 다시 합쳐야 한다. DB 엔진(MongoDB)의 $lookup 연산자나 백엔드 코드가 그 역할을 짊어진다.

  1. Step 1: 부모 컬렉션 조회 user = db.users.findOne({_id: 1})
  2. Step 2: 꺼낸 자식 ID 목록으로 자식 컬렉션 일괄 조회 db.reviews.find({_id: { $in: user.review_ids }})
  3. 이 두 번의 네트워크 왕복(Round Trip)은 비동기 Non-blocking I/O (예: Node.js)를 통해 찰나의 순간에 이루어지므로 RDBMS의 무거운 B-Tree JOIN보다 훨씬 가볍고 빠를 수 있다.

Ⅲ. 실무 적용 및 기술사적 판단

실무 시나리오

  1. 시나리오 — MongoDB 문서 16MB 폭발 장애 (OOM 크래시): 소셜 데이팅 앱 백엔드를 MongoDB로 짰다. 채팅방(Room) 문서 하나에 과거부터 현재까지 오간 채팅 메시지 내용(messages: [...])을 전부 임베딩했다. 오픈 1년 뒤, 10만 건의 메시지가 쌓인 특정 VIP 채팅방을 누군가 입장(Read)하려 하자, 그 순간 DB 서버의 RAM이 요동치더니 해당 워커 프로세스가 OOM(Out of Memory)으로 죽어버리는 현상 발생.

    • 판단: 완벽한 Unbounded Array(끝없이 자라는 배열)의 저주다. 10만 건의 텍스트가 박힌 JSON 문서는 용량이 수십 MB를 훌쩍 넘어 DB 엔진의 한도(Limit)를 뚫고 네트워크 대역폭을 질식시켰다.
    • 해결책: 즉각적인 참조(Reference) 패턴 기반 스키마 리팩토링이 필요하다. Room 컬렉션과 Message 컬렉션을 물리적으로 분리한다. Room 문서에는 방 이름과 마지막 접속 시간만 둔다. 그리고 수천만 개의 Message 문서에는 room_id라는 부모 참조 필드를 넣는다(Parent Referencing). 클라이언트가 입장하면 방 정보를 1번 읽고, 메시지 컬렉션에서 room_id로 인덱스를 타 최신 50개만 끊어오는(Limit/Offset) 가벼운 쿼리로 시스템을 영구히 살려내야 한다.
  2. 시나리오 — 데이터 불일치(Inconsistency)와 캐싱의 딜레마: 이커머스에서 Order(주문)User(고객)를 참조 패턴으로 찢었다. 주문 내역 화면을 띄울 때마다 Order를 읽고 그 안의 user_id를 빼서 User 컬렉션을 두 번 조회(Application Join)하려니 트래픽 폭주 시 성능이 안 나오는 상황.

    • 판단: 완벽한 정규화(참조 분리)는 조회 시 N번의 네트워크 쿼리를 유발한다. 읽기 성능이 극도로 중요하다면 다시 역정규화의 마법을 섞어야 한다.
    • 해결책: **하이브리드 패턴 (확장된 참조, Extended Reference)**을 도입한다. 완전히 데이터를 찢는 것이 아니라, 자주 쓰이는 껍데기(예: 사용자 이름, 프로필 사진 URL)는 부모 문서 안에 **중복으로 복사(임베딩)**해두고, 자주 안 쓰이는 세부 정보(비밀번호, 상세 주소)만 참조(Reference)로 남겨둔다. 이를 통해 조인 쿼리 횟수를 0으로 만들면서도 무거운 문서 사이즈 폭발을 막는 아키텍처적 줄타기를 완성해야 한다.

도입 체크리스트

  • 비즈니스적: 참조된(찢어진) 데이터를 화면에 다 띄울 필요가 있는가? 넷플릭스 영화 상세 페이지를 열 때, 영화 정보는 당장 보여주더라도 '추천 리뷰 10,000개'는 사용자가 스크롤을 내려야만 보인다. 그렇다면 리뷰는 아예 다른 컬렉션에 찢어두고 무한 스크롤(Infinite Scroll)이 발생할 때만 백그라운드 API(AJAX)로 조금씩 떼어와(참조 패턴) 초기 렌더링 속도를 광속으로 끌어올리는 게 맞다.

Ⅳ. 기대효과 및 결론

정량/정성 기대효과

구분무리한 전체 임베디드 (Anti-Pattern)상황에 맞는 참조(Reference) 패턴 분리개선 효과
정량 (문서 크기)데이터 증가 시 JSON 크기 기하급수 팽창부모 문서 사이즈를 수십 KB 내외로 묶어둠16MB 한도 초과 및 OOM 장애 100% 원천 차단
정량 (I/O 병목)읽기/쓰기 시 거대 파일 전체를 디스크에 직렬화쪼개진 작은 데이터만 개별적 업데이트단일 컬렉션 쓰기 병목(Lock) 해소로 처리량(Throughput) 상승
정성 (설계 유연성)배열 크기가 무한히 자랄까 봐 전전긍긍함페이징(Paging) 처리를 통한 무한 스크롤 구현 용이대용량 분산 아키텍처 환경의 안정적 확장성(Scalability) 확보

NoSQL 모델링은 "모두 다 합치거나(Embed), 모두 다 찢거나(Reference)" 하는 모 아니면 도의 세계가 아니다. 기술사는 시스템의 데이터가 생명주기(Lifecycle)에 따라 얼마나 크고 빠르게 자라날지(Growth Rate) 예측하고, 한 덩어리로 묶었을 때의 읽기 쾌감과 무한히 자라나는 배열이 안겨줄 메모리 폭발의 공포 사이에서 칼날 같은 균형을 잡아, 하이브리드 스키마 디자인(Extended Reference)을 이끌어내는 정교한 데이터 조율사가 되어야 한다.


📌 관련 개념 맵 (Knowledge Graph)

개념 명칭관계 및 시너지 설명
임베디드 도큐먼트 (Embedded)참조 패턴과 완벽한 대척점에 서 있는 모델링 기법. 조인(Join)을 죽이고 성능을 얻지만, 배열이 무한히 커지면 시스템이 함께 죽는 양날의 검이다.
Application-Level JoinRDBMS의 DB 엔진이 해주던 JOIN 연산을 버리고, 참조 패턴으로 찢어진 데이터를 백엔드 코드(서버) 메모리로 끌고 와 Map이나 반복문으로 직접 합쳐버리는 개발 아키텍처다.
언바운디드 어레이 (Unbounded Array)끝없이 자라나는 배열. SNS 팔로워, 센서 로그 등을 임베딩할 때 발생하며 NoSQL 모델링 실패 원인 1순위. 이 단어가 보이면 무조건 참조 패턴으로 찢어야 한다.
BSON 16MB LimitMongoDB가 단일 문서의 크기를 16MB로 막아둔 물리적 제약. 이를 뚫으면 예외(Exception)가 터지므로 방어적 참조(Reference) 설계의 가장 큰 기술적 명분이다.
GraphQL클라이언트가 "영화 제목(부모)이랑 리뷰 목록(참조된 자식) 다 가져와!"라고 한 번에 요청하면 백엔드에서 찢어진 DB들을 기가 막히게 조립해서 1개의 JSON으로 내려주는 BFF의 진화형 통신 규약이다.

👶 어린이를 위한 3줄 비유 설명

  1. 뚱뚱한 백과사전 1권(부모 문서) 안에 세상의 모든 공룡 사진(하위 배열) 수만 장을 다 붙여 넣으려니 책이 너무 무거워서 찢어지려 해요.
  2. 그래서 백과사전에는 공룡 이름과 "공룡 사진은 창고 5번 앨범에 있음!"이라는 '찾아보기 번호(참조 ID)' 딱 하나만 적어두기로 했어요.
  3. 책이 다시 가벼워져서(16MB 이내) 들고 다니기 짱 편해지고, 진짜 사진이 보고 싶을 때만 그 번호를 들고 창고(다른 컬렉션)에 가서 앨범을 찾아보는 똑똑한 방법이 바로 '참조 패턴'이랍니다!