---
term: 'BaaS 데이터 모델링의 Pointer vs. Relation 필드'
seoTitle: 'Pointer vs. Relation 필드: BaaS 데이터 모델링'
headline: 'BaaS 데이터 모델링의 Pointer vs. Relation 필드'
slug: pointer-vs-relation-fields
category: database
shortDefinition: 'Pointer 필드는 단일 객체를 가리키는 타입 지정 참조이고, Relation 필드는 여러 객체를 다대다로 잇는 관리형 조인입니다.'
relatedTerms:
  - data-modeling
  - relational-queries-document-databases
  - database-schema
  - auto-generated-database-apis
contrastsWith:
  - relational-queries-document-databases
aboutTerms:
  - 'Pointer Fields'
  - 'Relation Fields'
faq:
  - question: 'BaaS 데이터 모델에서 Pointer와 Relation의 차이는 무엇입니까?'
    answer: 'Pointer는 객체 안에 참조 하나를 저장합니다. 타입이 있는 외래 키이므로, 각 객체는 정확히 하나의 대상을 가리킵니다. Relation은 플랫폼이 관리하는 숨겨진 조인 구조에 크기 제한이 없는 참조 집합을 저장합니다. Pointer는 일대일과 일대다를 모델링하고, Relation은 소속 목록이 한없이 늘어나는 다대다를 모델링합니다.'
  - question: 'Pointer로 일대다 관계를 어떻게 모델링합니까?'
    answer: 'Pointer를 "다" 쪽에 둡니다. 외래 키와 똑같이 각 Comment가 자신의 Post를 가리키는 Pointer를 가집니다. 게시물의 댓글을 나열하려면 Pointer 값이 해당 게시물인 Comment 클래스를 쿼리합니다. 모든 객체가 작게 유지되고 쓰기 비용도 낮으며, 이 패턴은 어떤 규모에서도 작동합니다. 자식이 아무리 많아도 부모가 비대해지지 않습니다.'
  - question: '언제 Relation 대신 Pointer 배열을 사용해야 합니까?'
    answer: '목록이 작고, 크기가 제한되어 있으며, 보통 부모와 함께 읽힐 때입니다. 레시피의 재료 열 가지, 주문의 품목 같은 경우입니다. 배열은 객체 안에 함께 실려 다니므로 include() 호출 한 번으로 모두 가져올 수 있지만, 요소 하나하나가 객체를 키우고 수백 개를 넘으면 읽기와 저장이 느려집니다. 크기 제한이 없거나 공유되는 목록은 Relation에 두어야 합니다.'
  - question: '두 객체를 따로 가져오지 않고 Pointer를 가로질러 쿼리할 수 있습니까?'
    answer: '예, 그것이 include()가 하는 일입니다. Post를 쿼리하면서 include(''author'')를 지정하면 플랫폼이 서버 측에서 각 Pointer를 해석해 완전한 저자 객체를 한 번의 응답으로 반환합니다. include(''author.company'')처럼 점 표기법으로 더 깊이 들어갈 수도 있습니다. 필터링은 반대 방향으로도 작동합니다. matchesQuery()는 가리키는 대상 객체의 조건으로 부모를 선택합니다.'
  - question: 'Relation 필드는 내부적으로 어떻게 작동합니까?'
    answer: '플랫폼이 Relation 필드마다 객체 ID 쌍을 저장하는 숨겨진 조인 테이블을 유지합니다. 관계형 스키마라면 연결 테이블이라고 부를 구조와 같지만, 사용자가 직접 설계하거나 이름을 붙일 필요가 없습니다. 소속 쿼리는 이 구조를 직접 조회하므로, 연결이 아무리 많이 쌓여도 관계의 어느 쪽도 크기가 커지지 않습니다.'
  - question: 'Pointer 필드에는 인덱스가 자동으로 생성됩니까?'
    answer: '그렇다고 가정하면 안 됩니다. Pointer 필드를 다른 쿼리 필터와 똑같이 다루고, 쿼리가 거쳐 가는 필드에는 인덱스를 생성해야 합니다. 일대다 조회는 일치하는 Pointer를 찾기 위해 자식 클래스를 스캔하며, 인덱스가 없으면 컬렉션 전체 스캔이 됩니다. 데이터베이스 인덱싱의 기본 규칙이 그대로 적용됩니다. 쿼리하는 대상, 특히 조인 경로에는 인덱스를 생성해야 합니다.'
  - question: 'Pointer와 Relation 중 어느 쪽이 더 빠릅니까?'
    answer: '일반적으로 Pointer입니다. 참조할 조인 구조 없이 include()로 같은 쿼리 안에서 해석되기 때문입니다. Relation은 추가 쿼리나 숨겨진 테이블에 대한 내부 조인이 필요하며, 이것이 크기 제한 없는 카디널리티의 대가입니다. 실용적인 규칙은 카디널리티가 허용하는 가장 저렴한 도구를 고르는 것입니다. Pointer가 먼저, 배열이 그다음, Relation이 마지막입니다.'
codeLanguages: [javascript, dart, swift, kotlin]
externalAuthorities:
  - name: 'Parse SDK guide — relational data'
    url: 'https://docs.parseplatform.org/js/guide/'
  - name: 'One-to-many data model (Wikipedia)'
    url: 'https://en.wikipedia.org/wiki/One-to-many_(data_model)'
  - name: 'MongoDB — model relationships between documents'
    url: 'https://www.mongodb.com/docs/manual/applications/data-models-relationships/'
  - name: 'Cardinality in data modeling (Wikipedia)'
    url: 'https://en.wikipedia.org/wiki/Cardinality_(data_modeling)'
cta:
  title: '조인 테이블을 설계하지 않고 관계 모델링하기'
  text: 'Back4app은 Pointer와 Relation 필드를 일급 열 타입으로 제공합니다. 대시보드에서 일대다와 다대다를 연결하고, 어느 SDK에서든 include()로 관계를 가로질러 쿼리하며, 조인 구조의 유지 관리는 플랫폼에 맡길 수 있습니다.'
  linkText: '무료로 시작하기'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-11-03'
translationKey: pointer-vs-relation-fields
---

**Pointer 필드는 단일 객체를 가리키는 타입 지정 참조이고, Relation 필드는 여러 객체를 다대다로 잇는 관리형 조인입니다.** 둘 다 모든 스키마가 마주하는 질문, 즉 레코드가 서로를 어떻게 참조하는가에 답하지만, 카디널리티와 비용이 다릅니다. 둘 중 하나를 고르는 것이 BaaS에서 [데이터 모델링](/glossary/ko/data-modeling/)의 핵심 결정이며, 다행히 이 결정은 하나의 질문으로 압축됩니다. 양쪽에 각각 몇 개가 있는가?

## 핵심 요약

| 질문 | 답변 |
| --- | --- |
| Pointer | 객체 *안에* 저장되는 타입 지정 참조 하나 – 클래스가 있는 외래 키 |
| Relation | 플랫폼이 관리하는 조인 테이블에 담긴 크기 제한 없는 참조 집합 |
| 일대다 | 항상 "다" 쪽에 Pointer |
| 다대다 | Relation – 목록이 작고 크기가 제한되어 있다면 Pointer 배열 |
| 탐색 도구 | `include()`는 Pointer를 해석하고, Relation 쿼리는 조인 집합을 가져옴 |

## 코드로 보는 두 가지 형태

**JavaScript:**

```javascript
// JavaScript / Node.js — Back4app JS SDK
// Pointer: one query, one hop — include() resolves the reference server-side
const posts = new Parse.Query('Post');
posts.equalTo('status', 'published');
posts.include('author');                  // pointer → full Author object
const page = await posts.find();
const name = page[0].get('author').get('displayName');

// Relation: the unbounded join set gets its own query
const tags = await page[0].relation('tags').query().find();
// tags is a plain array of Tag objects — the join table stays invisible
```

**Flutter:**

```dart
// Flutter / Dart — Back4app Flutter SDK
// Pointer: one query, one hop — includeObject resolves the reference server-side
final posts = QueryBuilder<ParseObject>(ParseObject('Post'))
  ..whereEqualTo('status', 'published')
  ..includeObject(['author']);            // pointer → full Author object
final response = await posts.query();
final post = response.results!.first as ParseObject;
final author = post.get<ParseObject>('author');

// Relation: the unbounded join set gets its own query
final relation = post.getRelation('tags');
final tags = await relation.getQuery().query();
// tags.results is a plain list of Tag objects — the join table stays invisible
```

**Swift:**

```swift
// iOS / Swift — Back4app Swift SDK
// Pointer: one query, one hop — include() resolves the reference server-side
let posts = Post.query("status" == "published")
  .include("author")                      // pointer → full Author object
posts.find { result in
  if case .success(let page) = result {
    print(page.first?.author?.displayName ?? "")
  }
}

// Relation: the unbounded join set gets its own query
let tags = Tag.query(related(key: "tags", object: try post.toPointer()))
tags.find { result in
  if case .success(let tagList) = result { render(tagList) }
}
```

**Kotlin:**

```kotlin
// Android / Kotlin — Back4app Android SDK
// Pointer: one query, one hop — include() resolves the reference server-side
val posts = ParseQuery.getQuery<ParseObject>("Post")
posts.whereEqualTo("status", "published")
posts.include("author")                   // pointer → full Author object
posts.findInBackground { page, e ->
    val author = page?.firstOrNull()?.getParseObject("author")
    println(author?.getString("displayName"))
}

// Relation: the unbounded join set gets its own query
val relation = post.getRelation<ParseObject>("tags")
relation.query.findInBackground { tags, e ->
    if (e == null) render(tags)           // the join table stays invisible
}
```

관계형 데이터베이스의 대응물을 보면 매핑이 명확해집니다. Pointer는 타입이 있는 외래 키이고, Relation은 직접 만들 필요가 없는 연결 테이블입니다:

```sql
-- Pointer: 자식 행의 열(일대다)
CREATE TABLE comment (
  id         serial PRIMARY KEY,
  post_id    integer REFERENCES post(id),   -- ← "pointer"에 해당
  body       text
);

-- Relation: 연결 테이블(다대다) — BaaS가 대신 만들어 줍니다
CREATE TABLE post_tags (
  post_id    integer REFERENCES post(id),
  tag_id     integer REFERENCES tag(id),
  PRIMARY KEY (post_id, tag_id)
);
```

## 쿼리 시점에 각각이 해석되는 방식

```mermaid
flowchart TB
  accTitle: Pointer 해석 vs. Relation 탐색
  accDescr: Pointer 쿼리는 include를 통해 참조된 객체를 인라인으로 해석해 한 번의 응답으로 반환합니다. Relation 쿼리는 먼저 ID 쌍을 담은 숨겨진 조인 테이블을 조회한 다음, 반대편에서 일치하는 객체를 가져옵니다.
  Q1["Post 쿼리<br/>include('author')"] --> P["Pointer를 인라인으로 해석"]
  P --> R1["한 번의 응답:<br/>게시물 + 전체 저자"]
  Q2["post.relation('tags')<br/>.query()"] --> J["숨겨진 조인 테이블<br/>(postId, tagId) 쌍"]
  J --> R2["두 번째 조회:<br/>일치하는 Tag 객체"]
```

Pointer 경로가 저렴한 쪽입니다. [`include()`](https://docs.parseplatform.org/js/guide/)는 서버에 응답하기 전에 각 참조를 완전한 객체로 바꾸라고 지시합니다. 왕복은 한 번이고, 점 표기법(`include('author.company')`)으로 깊이에 제한 없이 들어갈 수 있으며, 루프 안에서 자식을 가져오는 N+1 패턴의 표준 해결책입니다. 필터링도 같은 단계를 가로질러 작동합니다. `equalTo('author', pointer)`는 게시물의 댓글을 찾고, `matchesQuery()`는 다른 클래스의 조건으로 한 클래스를 필터링합니다. 이 도구 전체는 [문서 데이터베이스의 관계형 쿼리](/glossary/ko/relational-queries-document-databases/)에서 다룹니다.

Relation 경로가 주는 것은 다릅니다. 소속 정보가 어느 객체 안도 아닌 조인 구조에 있으므로, 한 사용자가 1만 개의 그룹에 속하고 한 그룹이 100만 명의 사용자를 담아도 어느 문서도 단 1바이트도 커지지 않습니다. 대가는 추가 단계입니다. `include()`는 Relation을 탐색하지 않으며, 조인 집합에는 별도의 쿼리가 필요합니다.

그 사이에 **Pointer 배열**이 있습니다. 타입이 지정된 참조를 담는 목록 필드입니다. 작은 집합에 Pointer의 경제성을 적용한 것으로, `include()` 한 번으로 모든 요소를 가져옵니다. 하지만 배열은 객체 안에 있으므로, 요소 하나하나가 객체를 읽고, 저장하고, 동기화하는 부담을 키웁니다. 수백 개를 넘으면 배열을 담은 객체가 병목이 되며, 이는 Relation을 배열로 모델링했다는 신호입니다.

## Pointer vs. Pointer 배열 vs. Relation

### 어떤 관계 도구를 선택해야 할까?

| 기준 | Pointer | Pointer 배열 | Relation |
| --- | --- | --- | --- |
| 카디널리티 | 대상 하나 | 소수, 제한됨 | 제한 없음, 다대다 |
| 저장 위치 | 객체 안 | 객체 안 | 숨겨진 조인 테이블 |
| 부모와 함께 가져오기 | `include()` | `include()` | 별도의 Relation 쿼리 |
| 객체 크기 증가 | 없음 | 요소마다 | 전혀 없음 |
| 순서 | 해당 없음 | 보존됨 | 보장되지 않음 |
| 대표 예시 | Comment → Post | Order → 품목 | Users ↔ Groups |

주의할 만한 세부 사항이 두 가지 있습니다. 배열은 요소 순서를 보존하지만 Relation은 그렇지 않으므로, 순위가 있는 목록(재생 목록의 트랙, 워크플로 단계)은 크기 부담과 관계없이 배열로 다뤄야 할 문제입니다. 그리고 일대다에는 두 가지 인코딩이 있습니다. 자식 쪽 Pointer는 무한히 확장되고, 부모 쪽 배열은 읽기가 더 편합니다. 결정하는 것은 취향이 아니라 [카디널리티](https://en.wikipedia.org/wiki/Cardinality_(data_modeling))의 상한입니다.

## 일반적인 사용 사례

- **소유권과 작성자.** `createdBy`, `author`, `owner` 같은 일대일 및 일대다 Pointer입니다. 결국 모든 클래스가 갖게 되는 참조입니다.
- **댓글 스레드와 활동 피드.** 자식에 Pointer(`comment.post`)를 두고 부모로 쿼리합니다. 크기 제한 없는 일대다의 주력 패턴입니다.
- **태깅과 분류.** 게시물 ↔ 태그, 상품 ↔ 컬렉션처럼 양쪽 모두 제한 없는 다대다이므로 Relation입니다.
- **팔로워와 그룹 소속.** 전형적인 소셜 그래프입니다. 인기 계정의 팔로워 목록이 계정 객체 안에 있어서는 안 되므로 Relation을 씁니다.
- **품목과 순서가 있는 작은 집합.** 크기가 제한되고, 부모와 함께 읽히며, 순서가 중요합니다. 여기서는 Pointer 배열의 편리함이 제값을 합니다.

## Pointer와 Relation 중 무엇을 선택해야 할까? 결정 매트릭스

| Pointer를 선택할 때… | Relation을 선택할 때… |
| --- | --- |
| 각 객체가 정확히 하나의 대상을 참조할 때 | 양쪽 모두 제한 없이 늘어날 수 있을 때 |
| 일대다일 때(자식에 Pointer) | 진정한 다대다일 때 |
| `include()`로 부모와 함께 가져오고 싶을 때 | 집합을 부모와 함께가 아니라 단독으로 쿼리할 때 |
| 참조가 필터와 정렬에 쓰일 때 | 소속 확인이 주를 이룰 때(X가 그룹 Y에 속하는가?) |
| 제한되고 순서가 있는 목록이라면 대신 배열이 맞을 때 | 배열 필드가 눈에 띄게 객체를 비대하게 만들 때 |

압축된 경험칙은 이렇습니다. **Pointer가 먼저, 배열이 그다음, Relation이 마지막**이며, 카디널리티가 강요할 때만 단계를 올립니다. 대부분의 스키마는 결국 압도적으로 Pointer 기반이 되고, 소셜이나 분류 체계의 연결을 담는 진짜 Relation은 몇 개에 불과합니다. 실제로 실행할 쿼리를 중심으로 [스키마](/glossary/ko/database-schema/)를 설계한 다음, 그 쿼리가 거쳐 가는 Pointer 필드에 인덱스를 생성해야 합니다.

## 한계와 트레이드오프

- **Relation에는 추가 단계가 필요합니다.** Relation을 통과하는 `include()`는 없습니다. 구성원을 가져오는 것은 두 번째 쿼리이며, 규모가 커지면 서버 측에서 세는 것이 유일하게 합리적인 방법입니다.
- **배열은 조용히 비대해집니다.** 실패는 점진적으로 찾아옵니다. 20개의 Pointer를 담던 객체가 1년 뒤에는 2,000개를 담고, 모든 읽기가 그 대가를 치릅니다. 배열을 선택할 때 상한을 정해 두어야 합니다.
- **Pointer에도 다른 필터처럼 인덱스가 필요합니다.** 인덱스 없이 부모 Pointer로 자식을 쿼리하는 것은 편리한 API의 옷을 입은 컬렉션 스캔입니다.
- **연쇄 삭제가 없습니다.** 게시물을 삭제해도 댓글이 삭제되거나 Relation이 정리되지 않습니다. 고아 레코드 처리는 사용자의 몫이며, 보통 서버 측 삭제 트리거에서 처리합니다.
- **참조 무결성은 권고 사항일 뿐입니다.** Pointer는 삭제된 객체를 참조할 수 있으며, 플랫폼은 이를 막지 않습니다. 끊어진 참조를 코드가 마주칠 수 있는 상태로 다뤄야 합니다.

## Back4app에서의 Pointer와 Relation

Back4app은 관리형 데이터베이스, 자동 생성되는 REST 및 GraphQL API, 인증, 파일 스토리지, Cloud Code 서버리스 함수를 결합한 오픈소스 Backend as a Service(BaaS) 플랫폼입니다. Pointer와 Relation은 스키마의 일급 열 타입입니다. 대시보드에서 둘 중 무엇이든 만들면 [자동 생성 API](/glossary/ko/auto-generated-database-apis/)가 모든 SDK에서 `include()`, Relation 쿼리, 클래스 간 필터를 즉시 지원하며, 위의 코드 탭은 수정 없이 그대로 실행됩니다. Relation 뒤의 조인 테이블은 플랫폼이 생성하고 이름을 붙이고 유지하며, 모델 자체가 사용자에게 맡기는 연쇄 삭제와 고아 레코드 정리 로직은 Cloud Code 트리거에 두는 것이 자연스럽습니다.
