CAP for Node

[CAP for Node] Association vs Composition — 데이터 모델 관계 설계 완전 가이드

▶ YouTube에서 보기

[CAP for Node] Association vs Composition — 데이터 모델 관계 설계 완전 가이드

Moderator · 2026. 4. 25. · 조회 5

[CAP for Node] Association vs Composition — 데이터 모델 관계 설계 완전 가이드

📖 개요 및 학습 목표

CAP(Cloud Application Programming) CDS에서 엔티티 간 관계를 정의하는 두 가지 핵심 키워드: AssociationComposition. 둘 다 "관계"를 표현하지만 데이터의 생명주기와 소유권에서 결정적인 차이가 있습니다. 이 차이를 모르면 Deep Insert가 안 되거나 부모 삭제 시 고아 데이터가 남는 등 실무에서 골치 아픈 버그를 만납니다.

이 글을 읽으면 다음을 할 수 있습니다:

  • ✅ Association과 Composition의 의미적·기술적 차이를 명확히 구분
  • ✅ Managed Association으로 외래키를 자동 관리하는 패턴 적용
  • ✅ Composition으로 Deep INSERT/UPDATE/DELETE 구현
  • ✅ One-to-One, One-to-Many, Many-to-Many 관계를 올바르게 설계

대상 독자: CAP 프로젝트에서 schema.cds를 작성하기 시작한 초·중급 개발자

📚 선수 지식

  • CDS(Core Definition Language) 기본 문법 — entity, type, aspect
  • 관계형 데이터베이스의 외래키(FK) 개념
  • OData V4 기본 CRUD 이해
  • Node.js + CAP 프로젝트 생성 경험 (cds init)

🔧 환경 / 버전 / 준비물

  • CAP: @sap/cds 8.x (2024년 10월 이후, capire 공식 문서 기준)
  • Node.js: 20 LTS 이상
  • 데이터베이스: SQLite (개발, --in-memory) 또는 SAP HANA Cloud (프로덕션)
  • 도구: VS Code + SAP CDS Language Support 확장
  • 프로젝트 생성: cds init my-bookshop && cd my-bookshop && npm i

이 글에서 다루는 것

💡 핵심 개념

AssociationComposition의 차이를 회사 조직도로 비유하면:

  • Association = "같이 일하는 사이" — 저자(Author)와 책(Book)의 관계. 저자가 퇴사해도 책은 도서관에 남아있습니다. 서로 독립적으로 존재할 수 있으며, 삭제가 전파되지 않습니다.
  • Composition = "소속 관계" — 주문(Order)과 주문항목(OrderItem)의 관계. 주문이 삭제되면 주문항목도 함께 삭제됩니다. 자식은 부모 없이 존재할 수 없으며, 부모를 통해서만 생성/수정/삭제됩니다(Deep Operations).
# Association vs Composition 비교표
┌──────────────────┬─────────────────────┬─────────────────────┐
│                  │ Association          │ Composition         │
├──────────────────┼─────────────────────┼─────────────────────┤
│ 생명주기          │ 독립적               │ 부모에 종속          │
│ 삭제 전파         │ X (부모 삭제해도 유지)│ O (부모 삭제 시 함께)│
│ Deep INSERT      │ X                   │ O                   │
│ Deep UPDATE      │ X                   │ O                   │
│ OData $expand    │ O                   │ O                   │
│ 외래키 위치       │ 참조하는 쪽          │ 자식 쪽              │
│ 대표 예시         │ Book → Author       │ Order → OrderItems  │
└──────────────────┴─────────────────────┴─────────────────────┘

Managed Association: CDS에서 author : Association to Authors라고만 쓰면, CAP이 자동으로 author_ID 외래키 필드를 생성하고 JOIN 조건을 추가합니다. 이것이 "Managed" Association이며, 항상 이 방식을 권장합니다.

흔한 오개념:

  • ❌ "Composition은 to-many에서만 쓴다" → ⭕ to-one Composition도 가능합니다. 예: Order : Composition of one ShippingAddress
  • ❌ "Association으로도 Deep INSERT가 된다" → ⭕ 일반적으로 Association에서는 Deep INSERT가 지원되지 않습니다. 자식을 함께 생성하려면 Composition을 사용해야 합니다.

💻 실전 코드 — 3단계

1단계: 기본 예제 — Association과 Composition 정의

// db/schema.cds
namespace bookshop;
using { cuid, managed } from '@sap/cds/common';

// Authors — 독립 엔티티
entity Authors : cuid, managed {
  name    : String(100);
  country : String(2);
  books   : Association to many Books on books.author = $self;
}

// Books — Author와 Association (독립적 존재)
entity Books : cuid, managed {
  title  : String(200);
  price  : Decimal(10,2);
  stock  : Integer;
  author : Association to Authors;  // → author_ID 자동 생성 (Managed)
  genre  : String(50);
  reviews: Composition of many Reviews on reviews.book = $self;  // Composition!
}

// Reviews — Book에 종속 (Composition)
entity Reviews : cuid, managed {
  book   : Association to Books;  // 역참조 (자동 생성)
  rating : Integer;
  text   : String(1000);
}

결과: Books 테이블에 author_ID 컬럼이 자동 생성됩니다. Reviews는 Books와 Composition 관계이므로, Book을 통해서만 CRUD가 가능합니다.

2단계: 실무 시나리오 — Deep INSERT와 Cascade DELETE

// srv/cat-service.js
const cds = require('@sap/cds');

module.exports = class CatalogService extends cds.ApplicationService {
  async init() {
    const { Books, Authors } = this.entities;

    // Deep INSERT — Composition 덕분에 Book + Reviews 동시 생성
    this.on('CREATE', 'Books', async (req) => {
      // 이런 요청이 가능:
      // POST /catalog/Books
      // { "title": "CAP 입문", "reviews": [
      //     { "rating": 5, "text": "최고!" },
      //     { "rating": 4, "text": "좋아요" }
      // ]}
      // → Book 1건 + Review 2건이 한 트랜잭션으로 생성됨
      console.log('Deep INSERT:', JSON.stringify(req.data));
      return super.onCreate(req);
    });

    // Association은 Deep INSERT 불가 — 별도 처리 필요
    this.on('createBookWithAuthor', async (req) => {
      const { title, authorName } = req.data;
      const db = await cds.connect.to('db');

      // 1) Author 먼저 생성
      const author = await db.run(
        INSERT.into('bookshop.Authors').entries({ name: authorName })
      );
      // 2) Book에 author_ID 연결
      const book = await db.run(
        INSERT.into('bookshop.Books').entries({
          title: title,
          author_ID: author.req.data.ID  // 외래키 직접 지정
        })
      );
      return book;
    });

    // Cascade DELETE 테스트 — Book 삭제 시 Reviews 자동 삭제
    this.after('DELETE', 'Books', (_, req) => {
      console.log(`Book 삭제됨 → 연관 Reviews도 자동 삭제 (Composition)`);
    });

    await super.init();
  }
};

3단계: 고급 — Many-to-Many와 프로덕션 고려사항

// db/schema.cds — Many-to-Many 관계 (링크 테이블)
entity Books : cuid {
  title      : String(200);
  categories : Composition of many Books2Categories on categories.book = $self;
}

entity Categories : cuid {
  name  : String(50);
  books : Association to many Books2Categories on books.category = $self;
}

// 링크 테이블 — Many-to-Many를 위한 중간 엔티티
entity Books2Categories {
  key book     : Association to Books;
  key category : Association to Categories;
}
// 프로덕션 고려사항: $expand 성능
// BAD: 3단계 중첩 $expand → 성능 저하
// GET /catalog/Authors?$expand=books($expand=reviews($expand=...))

// GOOD: 필요한 단계만 확장 + $select로 필드 제한
// GET /catalog/Books?$expand=author($select=name),reviews($select=rating)&$top=20

// Draft 지원 시 Composition은 자동으로 Draft 전파
// @odata.draft.enabled 활성화하면 부모-자식 모두 Draft 상태 관리

⚠️ 흔한 실수 / 트러블슈팅

Q1: "Association to many"에서 on 절을 빠뜨렸어요

  • 증상: CDS 컴파일 에러 또는 $expand 시 빈 결과
  • 원인: to-many Association에는 반드시 on 절이 필요합니다. 어느 필드로 연결할지 CDS가 추론할 수 없기 때문입니다.
  • 해결: books : Association to many Books on books.author = $self; — 반드시 역방향 참조를 on 절에 명시하세요.

Q2: Deep INSERT 시 "ENTITY_NOT_FOUND" 에러

  • 증상: Book과 Reviews를 동시에 POST 했는데 에러
  • 원인: Reviews가 Association으로 정의되어 있을 수 있습니다. Deep INSERT는 Composition에서만 작동합니다.
  • 해결: Association to many ReviewsComposition of many Reviews로 변경하세요.

Q3: 부모 삭제 시 자식이 안 지워져요

  • 증상: Order 삭제 후 OrderItems가 DB에 남아있음
  • 원인: Association으로 정의하면 Cascade DELETE가 작동하지 않습니다.
  • 해결: Composition으로 변경하면 CAP이 자동으로 자식 엔티티를 함께 삭제합니다. 또는 @Before 핸들러에서 수동 삭제를 구현합니다.

🚀 다음 단계 / 관련 주제

  • CDS Aspectscuid, managed 같은 공통 aspect를 커스텀으로 만들어 재사용
  • OData V4 Draft — Composition과 Draft를 함께 사용하는 Fiori Elements 패턴
  • @restrict 인가 — 엔티티별 역할 기반 접근 제어와 Composition의 상호작용
  • HANA Cloud 배포 — SQLite → HANA 전환 시 Managed Association의 FK 매핑
  • Remote Service 소비 — 외부 API를 CAP 서비스에서 Association으로 연결

자세한 내용은 본문에서

📚 참고 자료


⚠️ 비공식 콘텐츠 안내

본 게시글은 btpstacks.com의 독립 학습 콘텐츠이며 SAP SE와 무관합니다. 공식 문서는 help.sap.com을 참고하세요.

SAP, ABAP, SAP BTP, SAPUI5, SAP Fiori는 독일 및 기타 국가에서 SAP SE의 상표 또는 등록상표입니다.

댓글 0

아직 댓글이 없습니다.