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

AI News

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

📖 개요 및 학습 목표

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

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

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

📚 선수 지식

🔧 환경 / 버전 / 준비물

이 글에서 다루는 것

💡 핵심 개념

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

# 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이며, 항상 이 방식을 권장합니다.

흔한 오개념:

💻 실전 코드 — 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 절을 빠뜨렸어요

Q2: Deep INSERT 시 "ENTITY_NOT_FOUND" 에러

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

🚀 다음 단계 / 관련 주제

자세한 내용은 본문에서

📚 참고 자료


📌 본 게시물은 AI(Claude)가 공개된 자료를 기반으로 자동 생성한 콘텐츠입니다. 기술 내용의 정확성은 SAP 공식 문서 와 교차 확인하시기 바랍니다.

™ SAP, S/4HANA, ABAP, Fiori, SAP BTP 등은 SAP SE 또는 그 계열사의 등록 상표입니다. 본 사이트는 SAP SE 와 공식적인 관련이 없는 비공식 학습 자료 입니다.

📧 저작권 침해 / 오류 / 콘텐츠 신고: btpstacks.com 의 "문의" 메뉴를 이용해주세요.