[좋은 코드 01] OOP - 메시지를 주고받는 객체

객체지향을 처음 배울 때 클래스와 상속 문법부터 외우면 정작 중요한 것을 놓친다. class 키워드를 쓴다고 객체지향이 되는 것은 아니고, 클래스 문법이 없는 언어로 짜도 객체지향의 정신은 지킬 수 있다. 문법이 아니라 설계 관점에서, 절차지향과 무엇이 다른지부터 짚는다.


절차지향은 데이터와 그 데이터를 처리하는 함수를 나눠 놓는다. 데이터는 수동적인 그릇으로 남고, 로직은 그 그릇을 받는 함수 여러 개에 흩어진다. 작은 규모에서는 이 구조가 오히려 깔끔하다. 문제는 그 그릇을 만지는 함수가 하나둘 늘어날 때부터 시작된다. 몇 해 전 작은 쇼핑몰 서비스를 넘겨받았을 때 재고 하나를 바꾸는 함수가 파일 여덟 개에 흩어져 있었다. 어느 함수가 규칙을 지키고 어느 함수가 규칙을 빼먹었는지 코드를 다 읽기 전에는 알 도리가 없었다.

절차지향이 규칙을 지키지 못하는 이유

재고를 예로 든다. 절차지향으로 짜면 물건은 수량 하나를 담은 자료구조에 불과하고, 예약이나 취소나 입고 같은 기능은 저마다 그 수량을 직접 건드리는 함수로 짜인다.


class Item {
  id: string;
  stock: number;
  constructor(id: string, stock: number) { this.id = id; this.stock = stock; }
}

function reserve(item: Item, qty: number) {
  if (item.stock < qty) throw new Error("재고 부족");
  item.stock -= qty;
}


수량이 음수로 떨어지면 안 된다는 규칙이 있다면, 절차지향에서는 수량을 만지는 모든 함수가 저마다 이 규칙을 지켜야 한다. 한 곳이라도 빠뜨리면 재고가 음수로 떨어지는 사고가 난다. 데이터는 방어막 없이 열려 있고, 그것을 올바르게 다룰 책임은 그것을 만지는 모든 코드에 떠넘겨진다. 새로 온 개발자가 재고를 직접 고치는 코드를 한 줄 추가하면 규칙은 그 순간 깨진다. 주문 상태나 결제 승인처럼 지켜야 할 규칙이 있는 데이터라면 어디든 똑같은 일이 벌어진다. 규칙 하나를 강제하는 자리가 여러 개라는 사실 자체가 버그의 씨앗이다.

데이터와 행위를 하나로 묶는다는 것

객체지향은 이 문제를 정반대 방향에서 접근한다. 데이터와 그 데이터를 다루는 로직을 하나의 단위로 묶어, 수량은 재고 객체 안에 감추고 수량을 바꾸는 유일한 통로를 메서드로 좁힌다. 외부는 수량을 직접 만질 수 없다. 재고에게 예약해 달라고 요청할 뿐이다.


class Item {
  #stock: number;
  constructor(private readonly id: string, stock: number) { this.#stock = stock; }
  reserve(qty: number) {
    if (qty <= 0) throw new Error("수량은 양수여야 함");
    if (this.#stock < qty) throw new Error("재고 부족");
    this.#stock -= qty;
  }
  get stock() { return this.#stock; }
}


차이는 규칙이 강제되는 자리의 개수다. 여기서는 규칙이 재고 객체 안 한 곳에만 있다. 호출하는 코드가 아무리 늘어도 수량을 직접 볼 방법이 없으므로 규칙을 빠뜨릴 여지가 없다. #stock은 자바스크립트의 진짜 사적 필드라 클래스 바깥에서 건드리는 것 자체가 문법 오류가 된다.


객체지향을 정의할 때 흔히 객체들이 메시지를 주고받는다고 말한다. 어떤 객체에게 어떻게 하라고 명령하는 대신, 무엇을 해 달라고 요청하고 그 방법은 객체 자신에게 맡긴다는 뜻이다. 회원 등급에 따라 적립 포인트를 계산하는 코드가 흔한 반례다. 밖에서 등급을 꺼내 분기하면, 등급이 하나 늘거나 적립률이 바뀔 때마다 흩어진 화면마다 찾아 고쳐야 한다. 판단을 회원 객체 안으로 들여보내면 고칠 곳은 하나로 줄어든다. 묻지 말고 시켜라(Tell, Don't Ask)라는 표현이 이 습관을 가리킨다. 조회는 문제없다. 경계는 꺼낸 값으로 판단하고 그 판단으로 다시 객체를 조작하느냐에 있다.

클래스를 썼다고 객체지향은 아니다

가장 흔한 오해가 객체지향은 클래스를 쓰는 것이라는 생각이다. 필드마다 게터와 세터만 잔뜩 달린 클래스는 객체가 아니라 이름만 클래스인 자료구조다. 데이터만 담고 로직은 전부 별도 서비스 계층에 몰아넣는 구조를 빈약한 도메인 모델(Anemic Domain Model)이라 부른다. setStock(number) 하나로 외부가 재고에 아무 값이나 채워 넣을 수 있다면, 필드를 private로 감춘 의미가 없다.


반대쪽 극단은 갓 오브젝트(God Object)다. 재고 하나가 수량 관리, 가격 계산, 배송 조건, 알림 발송까지 다 떠안으면 무엇을 고치든 전체를 이해해야 한다. 응집도(Cohesion)는 낮고 결합도(Coupling)는 높은, 설계에서 가장 피해야 할 상태다. 이 클래스를 바꿔 달라고 요청할 사람이 몇 명인지 세어 보면 답이 나온다. 재고 규칙을 바꾸는 사람과 알림 문구를 바꾸는 사람이 다르다면, 그 둘은 애초에 서로 다른 객체여야 했다.


객체지향이 절차지향보다 언제나 낫다는 뜻은 아니다. 짧은 스크립트나 일회성 변환에서는 절차적인 코드가 더 짧고 읽기도 쉽다. 지킬 규칙이 거의 없는 데이터에 캡슐화를 씌우면 간접 계층만 늘어난다. 판단 기준은 하나다. 이 데이터에 반드시 지켜야 할 규칙이 있는가. 있다면 규칙과 데이터를 묶어 안에서 강제하고, 없다면 억지로 객체로 포장하지 않는다. 재고 객체 하나를 제대로 감싸는 습관부터 붙이면, 나머지 설계 감각은 코드를 고쳐 나가는 과정에서 자연스럽게 따라온다.