스터디

[객체지향의 사실과 오해] 1장, 2장 정리

nkdev 2025. 8. 9. 17:38

1장

객체지향의 핵심은 '클래스'나 '상속'이 아닌 '자율적인 객체들 간의 협력'이다.

 

객체지향의 목표는 실세계를 소프트웨어로 끌고와서 모방하는 게 아니라, 고객이 원하는 요구사항을 새로운 세계로 만드는 것이다. 따라서 실세계의 모방이라는 개념은 설계/구현할 때는 부적합하다. 그래도 객체지향이라는 세계를 이해하고 사상을 학습하는 데는 효과적이므로, 이 관점으로 1장을 설명해보겠다.

 

 

손님, 캐시어, 바리스타 = 역할

주문, 주문 접수, 제조 = 책임

 

특정 역할은 특정한 책임을 암시한다.

역할 > 책임 (포함관계)

 

각 역할을 가진 객체들은 서로 협력해서 문제를 해결한다.

협력 : 연쇄적인 요청과 연쇄적인 응답이 발생하고, 각 역할을 맡은 객체들이 요청을 성실히 이행하는 것

 

  • 요청한 역할만 정확히 수행해준다면, 어떤 객체가 책임을 수행했든 상관 없음 -> 역할은 대체 가능하며, 여러 명일 수 있다.
  • 요청 받은 객체는 다형성을 통해 동일 요청에 대해 서로 다른 방식으로 응답할 수 있다.
  • 한 객체가 여러 역할을 맡기도 한다.

시스템(애플리케이션) 기능은 여러 책임들로 이뤄지므로, 좋은 객체지향설계를 하려면 

1) 적절한 객체에게

2) 적절한 책임을 할당해야 한다.

-> 유연하고 재사용가능하도록! (다형성과 관련있음)

 

'역할'의 특징들

  • 여러 객체가 동일한 역할을 수행할 수 있다.
  • 역할은 대체 가능성을 의미한다.
  • 각 객체는 책임을 수행하는 방법을 자율적으로 선택할 수 있다.
  • 하나의 객체가 동시에 여러 역할을 수행할 수 있다.

 

애플리케이션 기능 구현을 위해서는 객체 간의 협력이 필수 ('객체'지향인 이유가 여기에 있음)

결국 객체 품질이 협력 품질을 결정한다.

 

'객체'가 갖춰야 하는 두 가지 덕목

 

1) 충분히 협력적

다른 객체의 명령에 무조건 복종하는 수동적 존재 X -> 내부적인 복잡도가 너무 심해짐

어떻게 응답할지 스스로 판단하고 결정해야 함. 그저 요청에 '응답'해야 함

 

2) 충분히 자율적

객체 본인의 행동을 스스로 결정하고 책임짐

 

객체란 상태(data)와 행동(function)을 함께 가진 존재 -> 어떤 일을 하기 위해서는 상태를 알고 있어야 함

객체는 자율적인 존재 -> 객체는 자기 일을 스스로 판단하고 처리해야 함. 다른 객체가 '어떻게 하라'고 내부 로직에 간섭하면 안 됨. 다른 객체가 what을 하는지는 알 수 있지만 how는 몰라야 함

 

객체를 '상태+행동'으로 묶어서 '자율적인 존재'로 만들어야 유지보수가 용이하고 재사용성이 올라가며, 확장이 쉬워진다.

 

객체지향 세계에서 객체는 협력을 위해 메시지를 전송/수신한다.

메시지 : 객체 간의 의사소통 수단

 

객체는 수신된 메시지를 처리하기 위해 메서드를 사용한다.

메서드 : 함수 또는 프로시저를 통해 구현됨

 

절차지향 언어 -> 프로시저 호출 시 실행할 코드를 컴파일 시간에 결정

객체지향 언어 -> 프로시저 호출 시 실행 시간에 메서드를 선택할 수 있음

 

2장

객체지향 패러다임의 목적은 현실 세계를 모방하는 것이 아니라 현실 세계를 기반으로 새로운 세계를 창조하는 것이다.

 

상태는 행동의 결과이다.

행동의 결과로 나타난 상태는 이전 상태에 의존적이다.

 

객체가 될 수 있는 것?

하나의 개별적인 실체로 식별 가능한 물리적인 또는 개념적인 사물들이라면 모두 객체가 될 수 있다.

 

'상태'라는 개념이 왜 필요한가?

현재시점의 객체와 행동의 결과는 과거에 어떤 행동들이 객체에게 일어났는가에 따라 달라지는데, 과거 행동들을 다 기억하려면 복잡성이 늘어나기 때문에 상태라는 개념이 발생. -> 상태를 쓰면 과거 이력에 얽매이지 않고 현재를 깁반으로 객체의 행동결과를 예측 가능

 

'상태'가 될 수 있는 것?

1) 단순한 값 -> 속성(attribute)이라고 함

2) 객체 그 자체 -> 링크(link)로 연결되어있다.

링크 : 객체가 다른 객체를 참조할 수 있다는 의미. 일반적으로 한 객체가 다른 객체의 식별자를 알고있는 상태를 의미함. 객체 사이에 링크가 있어야 서로 메시지를 주고 받으며 협력할 수 있다.

 

즉 상태 = 값(attribute) + 객체(link)

그리고 이를 객체의 '프로퍼티'라고 함. 

프로퍼티는 변하지 않음 (정적) 그러나 프로퍼티의 값 자체는 변함 (동적)

 

객체는 자율적인 존재이다! 즉 스스로의 행동에 의해서만 상태가 변경되어야 한다.

그래서 [상태+상태를 조작하는 행동]을 하나로 묶는 것이 객체지향의 기본 사상이다.

다른 객체에 의해 간접적으로 특정 객체의 상태를 변경/조회하려면 '행동'을 사용하면 된다.

 

행동에 의해 상태가 변경되는데, 이를 행동의 부수효과(side effect)라고 함

그리고 그 변경된 결과는 이전 상태에 의존적이다.

 

객체는 협력과정에서 자신 뿐만아니라 다른 객체의 상태까지 변화시킬 수 있다.

객체 간에 협력하는 유일한 방법은 다른 객체에게 요청을 보내는 것이기 때문이다.

 

다른 객체에 의해 상태가 변경되더라도, 요청을 전달할 뿐이지 객체 상태를 변화시키는 것은 본인이 직접 해야 함

 

캡슐화 : 객체는 상태를 감춰두고 외부에 노출하지 않아야 함. 행동을 노출하여 외부객체가 접근할 수 있게 해야 함

캡슐화의 장점 -> 자율성이 높아지고, 협력이 유연하고 간결해짐. 스스로 판단하고 결정하기 때문에

 

객체는 '식별자'가 다르면 다르다고 판단되는 성질이 있음 (동등성, equality) -> 시간에 따라 상태가 변함/ 행동이 상태를 변화시킴

값은 '상태'가 다르면 다르다고 판단됨

 

// 한 파일로 돌릴 수 있는 간단한 예시들 (javac CafeDemo.java && java CafeDemo)

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

/* ===========================
 * 1) 역할(Role)과 책임(Responsibility), 협력(Collaboration)
 * =========================== */

// 역할은 인터페이스로, 책임은 메서드 시그니처로 표현
interface CashierRole {
    Receipt takeOrder(Customer customer, Order order); // 주문 접수 책임
}

interface BaristaRole {
    Drink makeDrink(Order order); // 제조 책임 (다형성의 대상)
}

interface CustomerRole {
    void placeOrder(CashierRole cashier, Order order); // 주문 책임
}

/* ===========================
 * 2) 자율적 객체: 상태 + 행동 + 캡슐화
 * =========================== */

final class Order {
    enum Status { NEW, ACCEPTED, MAKING, DONE }
    private final String menu;
    private Status status = Status.NEW;              // 상태(값)
    private BaristaRole assignedBarista;             // 링크(다른 객체 참조)

    Order(String menu) { this.menu = menu; }

    // 상태 변경은 자신의 행동으로만 허용(캡슐화)
    void acceptBy(CashierRole cashier) {
        ensure(Status.NEW);
        this.status = Status.ACCEPTED;
    }

    void assign(BaristaRole barista) {
        ensure(Status.ACCEPTED);
        this.assignedBarista = barista;
    }

    Drink startMaking() {
        ensure(Status.ACCEPTED);
        this.status = Status.MAKING;
        ensureAssigned();
        Drink d = assignedBarista.makeDrink(this); // 협력: 메시지 전송
        this.status = Status.DONE;
        return d;
    }

    String menu() { return menu; }
    Status status() { return status; }

    private void ensure(Status expected) {
        if (this.status != expected) throw new IllegalStateException("잘못된 상태 전이: " + status + " -> 기대: " + expected);
    }
    private void ensureAssigned() {
        if (assignedBarista == null) throw new IllegalStateException("바리스타 미배정");
    }
}

final class Receipt {
    private static final AtomicInteger SEQ = new AtomicInteger(1);
    private final int no = SEQ.getAndIncrement();
    private final String menu;
    Receipt(String menu) { this.menu = menu; }
    public String toString() { return "영수증#" + no + " (" + menu + ")"; }
}

final class Drink {
    private final String name;
    private final String note; // 다형적 제조 방식의 결과
    Drink(String name, String note) { this.name = name; this.note = note; }
    public String toString() { return name + " [" + note + "]"; }
}

/* ===========================
 * 3) 역할 대체 가능성 & 다형성
 * =========================== */

// 서로 다른 방식으로 동일 책임을 수행하는 두 바리스타
final class FastBarista implements BaristaRole {
    public Drink makeDrink(Order order) {
        // 행동의 부수효과(사이드 이펙트): 인벤토리 차감 등은 내부에서 결정
        return new Drink(order.menu(), "초고속 추출");
    }
}
final class CarefulBarista implements BaristaRole {
    public Drink makeDrink(Order order) {
        return new Drink(order.menu(), "정밀 계량·온도 제어");
    }
}

/* ===========================
 * 4) 한 객체가 여러 역할 수행 (동시 다역)
 * =========================== */

final class Manager implements CashierRole, BaristaRole {
    private final Inventory inventory = new Inventory();

    // Cashier 역할
    public Receipt takeOrder(Customer customer, Order order) {
        order.acceptBy(this);
        // 상황에 따라 자신(매니저)이 바리스타 역할도 수행하거나, 다른 바리스타에 위임 가능
        order.assign(this); // 여기서는 자신을 배정
        System.out.println("[Manager/Cashier] 주문 접수: " + order.menu());
        return new Receipt(order.menu());
    }

    // Barista 역할
    public Drink makeDrink(Order order) {
        inventory.consume(order.menu()); // 내부 상태 변화(캡슐화)
        return new Drink(order.menu(), "매니저 스페셜 블렌드");
    }
}

/* ===========================
 * 5) 구체 Cashier: 바리스타 교체가 쉬움(유연성)
 * =========================== */

final class Cashier implements CashierRole {
    private BaristaRole barista; // 링크: 협력 대상 (역할에만 의존)

    Cashier(BaristaRole barista) { this.barista = barista; }
    void changeBarista(BaristaRole newOne) { this.barista = newOne; } // 역할 대체

    public Receipt takeOrder(Customer customer, Order order) {
        order.acceptBy(this);
        order.assign(barista);
        System.out.println("[Cashier] 주문 접수: " + order.menu());
        return new Receipt(order.menu());
    }
}

/* ===========================
 * 6) 고객: 자율적 판단으로 메시지 전송(what만 요청)
 * =========================== */

final class Customer implements CustomerRole {
    private final String id; // 식별자(엔티티의 동일성)
    Customer(String id) { this.id = id; }

    public void placeOrder(CashierRole cashier, Order order) {
        Receipt receipt = cashier.takeOrder(this, order);
        System.out.println("[Customer " + id + "] " + receipt + " 수령");
        Drink drink = order.startMaking();
        System.out.println("[Customer " + id + "] 음료 픽업: " + drink);
    }
}

/* ===========================
 * 7) 값(Value)과 엔티티(Entity) 예시
 * =========================== */

final class Money {
    private final long amount; // 원화 가정
    Money(long amount) { this.amount = amount; }
    public long amount() { return amount; }
    // 값 객체: 상태가 같으면 같음
    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Money)) return false;
        return amount == ((Money) o).amount;
    }
    @Override public int hashCode() { return Objects.hash(amount); }
    public String toString() { return amount + "원"; }
}

final class Ticket { // 엔티티: 식별자가 같으면 같음
    private final UUID id = UUID.randomUUID();
    private Money price;
    Ticket(Money price) { this.price = price; }
    public UUID id() { return id; }
    public Money price() { return price; }
    public void changePrice(Money newPrice) { this.price = newPrice; } // 상태 변경(부수효과)
    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Ticket)) return false;
        return id.equals(((Ticket) o).id);
    }
    @Override public int hashCode() { return id.hashCode(); }
}

/* ===========================
 * 8) 내부 상태(Inventory) 캡슐화 예시
 * =========================== */

final class Inventory {
    private final Map<String, Integer> stock = new HashMap<>();
    Inventory() {
        stock.put("아메리카노", 10);
        stock.put("라떼", 8);
    }
    void consume(String menu) {
        int remain = stock.getOrDefault(menu, 0);
        if (remain <= 0) throw new IllegalStateException("재고 없음: " + menu);
        stock.put(menu, remain - 1);
    }
    @Override public String toString() { return stock.toString(); }
}

/* ===========================
 * 9) 데모
 * =========================== */

public class CafeDemo {
    public static void main(String[] args) {
        // 바리스타 두 명(다형성)
        BaristaRole fast = new FastBarista();
        BaristaRole careful = new CarefulBarista();

        // 캐시어는 역할에만 의존 → 바리스타 교체 쉬움
        Cashier cashier = new Cashier(fast);

        // 손님은 캐시어에게 "무엇을"만 요청 (어떻게 만드는지는 몰라도 됨)
        Customer alice = new Customer("Alice");
        alice.placeOrder(cashier, new Order("아메리카노"));

        // 역할 교체(대체 가능성)
        cashier.changeBarista(careful);
        Customer bob = new Customer("Bob");
        bob.placeOrder(cashier, new Order("라떼"));

        // 한 객체가 여러 역할 수행(Manager)
        Manager manager = new Manager();
        Customer chris = new Customer("Chris");
        chris.placeOrder(manager, new Order("라떼"));

        // 값 vs 엔티티
        Money m1 = new Money(3000);
        Money m2 = new Money(3000);
        System.out.println("[값 동등성] m1==m2 ? " + m1.equals(m2)); // true

        Ticket t1 = new Ticket(new Money(4500));
        Ticket t2 = new Ticket(new Money(4500));
        System.out.println("[엔티티 동일성] t1==t2 ? " + t1.equals(t2)); // false

        // 상태 변화(부수효과): 티켓 가격 변경
        t1.changePrice(new Money(5000));
        System.out.println("t1 price: " + t1.price() + ", t2 price: " + t2.price());
    }
}