sayu.day

객체 지향 다시 보기: 개체 vs 객체, 캡슐화, 데코레이터

개체객체, 둘 다 Object를 가리키는 말인데 왜 매체마다 다르게 쓰일까요. 사소해 보이는 이 번역어 논쟁을 짚어보면 OOP가 실제로 무엇을 모델링하려는지가 더 선명해집니다. 이 글은 용어 고찰에서 시작해 OOP 4원칙과 상태 일관성 중심의 도메인 모델링을 본론으로, 디자인 패턴 Decorator와 TypeScript 데코레이터의 차이를 응용편으로 다룹니다.

개체인가 객체인가

사전을 찾아보면 두 단어는 뉘앙스가 다릅니다.

객체

  1. 철학: 의사나 행위가 미치는 대상
  2. 언어: 문장 내에서 동사의 행위가 미치는 대상
  3. 철학: 작용의 대상이 되는 쪽

개체

  1. 전체나 집단에 상대하여 하나하나의 낱개를 이르는 말
  2. 생명: 하나의 독립된 생물체. 살아가는 데 필요한 독립적인 기능을 갖고 있음
  3. 철학: 단일하고 독립적인 통일적 존재

정리하면 객체는 "행위가 미치는 대상"이라는 수동적인 뉘앙스가 강하고, 개체는 "독립적인 기능을 가진 단일 존재"라는 능동적인 뉘앙스가 강합니다. 실제 클래스는 다른 클래스와 상호작용하면서 영향을 받기도 하고 주기도 하는 존재이므로, 어의만 놓고 보면 개체 쪽이 더 정확한 번역이라고 볼 수 있습니다. 다만 업계 관용적으로는 객체지향프로그래밍(OOP)이라는 표현이 굳어져 있어 실무에서는 두 용어가 혼용됩니다.

OOP의 4가지 핵심과 상태 일관성

OOP를 구성하는 네 가지 원칙은 다음과 같습니다.

  1. 추상화: 도메인의 본질만 모델링
  2. 캡슐화: 상태 변경 경로를 제한
  3. 상속: 공통 동작 재사용
  4. 다형성: 같은 인터페이스로 다른 구현 사용

실무에서는 이 네 가지 원칙 자체보다 "상태 일관성"이 더 중요합니다. 즉, 어떤 규칙에 의해서만 객체의 상태가 바뀌도록 강제하는가가 설계 품질을 좌우합니다.

  • 필드를 직접 노출하기보다 메서드로 불변 조건을 강제합니다.
  • 생성 시점에 유효한 상태를 보장합니다.
  • 책임이 큰 객체는 애그리거트로 분리합니다.

예시: BankAccount

public class BankAccount {
  public string Number { get; }
  public string Owner { get; private set; }
  public decimal Balance { get; private set; }

  public BankAccount(string owner, decimal initialBalance) {
    if (initialBalance < 0) throw new ArgumentException();
    Number = Guid.NewGuid().ToString("N");
    Owner = owner;
    Balance = initialBalance;
  }

  public void Deposit(decimal amount) {
    if (amount <= 0) throw new ArgumentException();
    Balance += amount;
  }

  public void Withdraw(decimal amount) {
    if (amount <= 0) throw new ArgumentException();
    if (Balance < amount) throw new InvalidOperationException();
    Balance -= amount;
  }
}

Balanceprivate set으로 막혀 있어서 외부에서 직접 바꿀 수 없고, 오직 DepositWithdraw를 통해서만 변합니다. 핵심은 메서드가 비즈니스 규칙을 보호한다는 점입니다. 잔액이 음수가 되거나, 유효하지 않은 금액이 들어오는 경우를 객체 스스로 막아냅니다.

자주 하는 실수

  • getter/setter만 많은 빈약한 도메인 모델
  • 상속 계층이 깊어져 변경 비용 폭증
  • 상태 변경을 여러 서비스에서 임의로 수행

이런 실수는 공통적으로 "상태를 누가, 어떤 규칙으로 바꾸는가"에 대한 통제를 잃었을 때 발생합니다.

응용: 두 개의 데코레이터

이름은 같지만 목적이 전혀 다른 두 개념이 있습니다. 바로 디자인 패턴 Decorator와 TypeScript의 언어 기능 Decorator입니다.

  • 디자인 패턴 Decorator: 객체 기능을 런타임에 감싸 확장하는 패턴
  • TypeScript Decorator: 클래스/메서드에 메타데이터나 동작을 주입하는 언어 기능

디자인 패턴 Decorator

기존 객체를 감싸서 로깅, 캐시, 인증 같은 부가 기능을 붙입니다. 원본 코드를 수정하지 않고도 여러 기능을 조합할 수 있는 것이 장점입니다.

TypeScript Decorator

function log(target: any, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`[${key}]`, args);
    return original.apply(this, args);
  };
}

class UserService {
  @log
  find(id: string) {
    return { id };
  }
}

복수의 데코레이터를 적용할 때 평가 순서에 주의해야 합니다. 표현식은 위에서 아래로 평가되지만, 실제 적용은 아래에서 위로 이뤄집니다.

실무 주의점

  • 런타임 메타프로그래밍은 디버깅 난이도를 올립니다.
  • 공통 관심사는 미들웨어/인터셉터로 대체 가능한지 먼저 검토합니다.
  • 데코레이터 기반 DI/validation은 팀 숙련도가 없으면 유지보수 비용이 큽니다.

데코레이터는 강력하지만 투명성이 떨어질 수 있습니다. "보이지 않는 동작"이 늘어나는 순간부터 테스트와 문서화 규칙이 필수입니다.

정리

개체 vs 객체 논쟁은 결국 "객체가 수동적 대상인가, 능동적 존재인가"라는 관점 차이이고, OOP 설계에서 실제로 중요한 것은 이 관점을 상태 일관성으로 구현하는 것입니다. 필드 노출을 줄이고 메서드로 불변 조건을 강제하는 것이 OOP 4원칙보다 실무적으로 더 큰 차이를 만듭니다. 여기에 부가 기능을 조합하는 디자인 패턴 Decorator나 횡단 관심사를 다루는 TypeScript 데코레이터를 쓸 때도, 핵심 로직을 숨기지 않고 투명하게 유지하는 원칙은 동일하게 적용됩니다.