JavaScript 합성(Composition) - 상속보다 유연하게 객체 설계하기

2025. 9. 9. 15:30·FrontEnd/JavaScript

상속은 잘 쓰면 편한데, 한 번 꼬이기 시작하면 "누가 누구를 상속했고, 어디서 뭐가 섞였는지" 추적하는 데만 시간이 다 간다.

그래서 모던 개발에서 자주 나오는 말이 하나 있다.

"상속보다 합성(Composition)을 먼저 떠올려라."

이번 글은 그 말이 무엇을 의미하는지, 코드에서 어떻게 쓰는지 정리하는 글이다.


결론 먼저

  • 합성(Composition): 객체가 다른 객체를 포함해서(has-a) 기능을 조합하는 방식
  • 상속(is-a) 대신 합성을 쓰면
    • 결합도 ↓ (서로 덜 얽힘)
    • 재사용성 ↑ (독립 모듈 재사용)
    • 유지보수 쉬움 (교체/확장 유연)
  • 클래스든, 생성자 함수든, 자바스크립트 어디에서나 쓸 수 있는 설계 아이디어

한 줄 요약하자면

"복잡한 상속 계층 짜기 전에, 먼저 합성으로 조합할 수 없는지 생각해 보자."


합성(Composition)이 뭐냐?

정의부터 짚고 가자

합성은 객체가 다른 객체를 속성으로 포함해서(has-a) 기능을 조합하는 방식이다.

  • A가 B를 포함한다 → A has a B
  • A가 B를 상속한다 → A is a B

상속은 "A는 B다"라는 관계를 강하게 만들어 버린다.
반대로 합성은 "A는 B를 가진다"라서, 서로 조금 더 느슨하게 묶인다.

합성의 장점 키워드

  • 캡슐화: 포함된 객체(Logger 등)의 내부 구현을 외부에서 신경 쓸 필요 없다.
  • 유연성: 나중에 Logger를 다른 구현으로 갈아끼우기 쉽다.
  • 재사용성: Logger, Storage, Validator 같은 모듈을 여러 곳에서 재사용하기 좋다.

간단히 말하면

상속은 "클래스 계층"에 기능을 얹어 가는 느낌이고,
합성은 "레고 블록"처럼 기능을 조합해서 완성하는 느낌이다.


합성은 어떻게 동작하냐?

구조는 생각보다 단순하다.

  1. 어떤 객체 안에 다른 객체를 속성으로 포함한다.
  2. 그 포함된 객체의 메서드를 호출하거나, 그대로 위임(delegate)한다.
  3. 필요하면 나중에 그 포함된 객체만 교체해서 기능을 바꾼다.

이 패턴은

  • class 문법에서도 쓸 수 있고
  • 예전 스타일의 생성자 함수에서도 그대로 쓸 수 있다.

예시 1 - class로 합성하기

class Logger {
  log(message) {
    console.log(`[LOG] ${message}`);
  }
}

class Calculator {
  constructor() {
    // 합성: Calculator가 Logger를 "가지고" 있다.
    this.logger = new Logger();
  }

  add(x, y) {
    this.logger.log(`Calculating ${x} + ${y}`);
    return x + y;
  }
}

const calc = new Calculator();
calc.add(5, 3);
// [LOG] Calculating 5 + 3
// 8

여기서 관계를 정리해 보면

  • Calculator is a Logger? → 아니다.
  • Calculator has a Logger? → 맞다.

그래서 상속보다는 합성이 자연스럽다.

만약 Logger 구현을 나중에 바꾸고 싶다면?

class SilentLogger {
  log(message) {
    // 아무것도 안 함
  }
}

class Calculator {
  constructor(logger = new Logger()) {
    this.logger = logger; // 의존성 주입
  }

  add(x, y) {
    this.logger.log(`Calculating ${x} + ${y}`);
    return x + y;
  }
}

const calc1 = new Calculator(new Logger());
const calc2 = new Calculator(new SilentLogger());

이렇게 로그 전략만 갈아끼우는 식으로 유연하게 가져갈 수 있다.


예시 2 - 생성자 함수로 합성하기

class를 쓰지 않는 스타일에서도 똑같이 합성을 적용할 수 있다.

function Logger() {
  this.log = function (message) {
    console.log(`[LOG] ${message}`);
  };
}

function Calculator() {
  this.logger = new Logger(); // 합성: Logger 포함

  this.add = function (x, y) {
    this.logger.log(`Calculating ${x} + ${y}`);
    return x + y;
  };
}

const calc = new Calculator();
calc.add(10, 5);
// [LOG] Calculating 10 + 5
// 15

문법만 다를 뿐, 구조는 class 예시와 똑같다.

  • Calculator는 Logger를 상속하지 않는다.
  • 단지 내부에 this.logger로 가지고 있을 뿐이다.

합성 vs 상속, 테이블로 정리

관점 합성 (Composition) - has-a  상속 (Inheritance) - is-a
관계 A는 B를 가진다 (has-a) A는 B이다 (is-a)
결합도 낮음 (서로 상대적으로 독립적) 높음 (부모 변경이 자식에 바로 영향)
유연성 구성 요소만 교체하면 동작 변경 가능 계층 구조 자체를 손봐야 하는 경우 많음
재사용성 모듈 단위 재사용에 강함 계층 전체를 상속받아야 하는 경우 많음
복잡도 구조가 비교적 단순, 추론 쉬움 계층이 깊어질수록 추적이 어려워짐

상속이 항상 나쁜 건 아니다. 다만

  • 계층이 조금만 꼬여도 어디서 뭐가 물려왔는지 추적하는 데 시간이 많이 들고
  • 공통 로직을 올리다 보면 나중에 애매한 Base/Abstract 클래스가 쌓이기 쉽다.

"is-a 관계가 정말 분명한 경우가 아니라면, 일단 합성을 먼저 떠올려라"


언제 합성을, 언제 상속을 쓸까? (선택 가이드)

합성을 우선 고려할 때

  • 유연한 기능 조합이 필요할 때
    • 예: 다양한 Logger, Storage, Transport, Strategy를 끼워 넣고 싶은 경우
  • 테스트를 쉽게 하고 싶을 때
    • 가짜 Logger, Mock API Client 등으로 갈아끼우기 좋다.
  • 단일 책임 원칙(SRP)을 지키고 싶을 때
    • 각 역할을 작은 객체로 나누고, 합성해서 조립하면 책임이 분리된다.

상속을 고려해도 되는 경우

  • is-a 관계가 정말 명확할 때
    • 예: Triangle is a shape, AdminUser is a User 등
  • 이미 상속 기반으로 잘 설계된 레거시 코드와 호환해야 할 때
    • 구조를 크게 흔들 수 없을 때는 기존 패턴을 따르는 것도 현실적인 선택

내 체크리스트

실제 코드 짤 때, 머릿속에서 대충 이렇게 물어본다.

  • "이 관계, 진짜 is-a인가? 아니면 has-a가 더 자연스럽나?"
  • "이 기능을 나중에 다른 구현으로 갈아끼울 일이 있을까?"
  • "이 객체 하나가 너무 많은 책임을 지고 있진 않나? (로그 + 저장 + 렌더링 + 검증 등)"
  • "테스트에서 진짜 객체 대신 가짜 객체를 넣어보고 싶은가?"

대부분의 경우, 이 질문들에 "그렇다"가 섞여 있으면
상속보다는 합성이 더 자연스러운 선택이다.


오늘 내용 한 줄씩 다시 정리

  • 합성(Composition): 객체가 다른 객체를 "포함"해서(has-a) 기능을 조합하는 설계 방식.
  • 상속(Inheritance): 객체가 다른 객체를 "상속"해서(is-a) 기능을 이어받는 설계 방식.
  • 합성은
    • 결합도를 낮추고
    • 재사용성을 높이고
    • 테스트/유지보수를 쉽게 만들어 준다.
  • 상속은
    • 계층이 깊어질수록 복잡성이 급격히 올라가기 때문에
    • "정말 is-a 관계가 분명한가?"를 항상 한 번 더 의심해 보는 게 좋다.

정리하자면,
새 구조를 설계할 때 "상속부터 생각하는 습관" 대신,
"작게 쪼개고, 합성으로 조립할 수는 없을까?"를 먼저 떠올리면
코드가 훨씬 덜 꼬이고, 나중에 손댈 때도 편해진다.

저작자표시 변경금지 (새창열림)

'FrontEnd > JavaScript' 카테고리의 다른 글

JavaScript 반복문 한 번에 정리하기  (0) 2025.09.09
JavaScript new 연산자  (0) 2025.09.09
ES6 클래스 vs 생성자 함수 - 요즘 자바스크립트에서 뭘 써야 할까?  (0) 2025.09.09
DOM으로 HTML 다루기 - innerHTML, insertAdjacentHTML, createContextualFragment  (0) 2025.09.09
JavaScript Function.call / apply / bind - this  (0) 2025.09.09
'FrontEnd/JavaScript' 카테고리의 다른 글
  • JavaScript 반복문 한 번에 정리하기
  • JavaScript new 연산자
  • ES6 클래스 vs 생성자 함수 - 요즘 자바스크립트에서 뭘 써야 할까?
  • DOM으로 HTML 다루기 - innerHTML, insertAdjacentHTML, createContextualFragment
프론트엔드 개발자 jbeat
프론트엔드 개발자 jbeat
프론트엔드 개발자 블로그인데 일상도 쪼그으믐
  • 프론트엔드 개발자 jbeat
    jbeat 님의 블로그
    프론트엔드 개발자 jbeat
  • 전체
    오늘
    어제
    • 분류 전체보기 (44)
      • FrontEnd (43)
        • TypeScript (6)
        • JavaScript (18)
        • Next.js (3)
        • React (1)
        • Testing (2)
        • Third Party (1)
        • web (10)
        • Tooling (1)
        • coding test (0)
        • A.I (1)
      • 일상 (1)
        • wedding (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 인기 글

  • 태그

    고차함수
    pick
    playwright
    코테
    Android
    yjs
    omit
    CrossOrigin
    Utility
    CRDT
    배열
    preconnect
    컬렉션
    Next.js
    TypeScript
    javascript
    주니어
    WebSocket
    타입스크립트
    이터러블
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
프론트엔드 개발자 jbeat
JavaScript 합성(Composition) - 상속보다 유연하게 객체 설계하기
상단으로

티스토리툴바