객체지향의 사실과오해

객체지향이란 실세계를 직접적이고 직관적으로 모델링할 수 있는 패러다임 이런 식의 설명은 객체지향 프로그래밍이란 현실 속에 존재하는 사물을 최대한 유사하게 모방해 소프트웨어 내부로 옮겨오는 작업이기 때문인데 그 결과물인 객체지향 소프트웨어는 실셰게의 투영이며, 객체란 현실 세계에 존재하는 사물에 대한 추상화라는 것

실세계 모방이라는 개념은 객체지향의 기반을 이루는 철학적인 개념을 설명하는 데는 적합하지만 유연하고 실용적인 관점에서 객체지향 분석, 설계를 설명하기에 적합하지않다.

애플리케이션을 개발하면서 객체제 직접적으로 대응되는 실세계의 사물을 발견할 확률은 그다지 높지 않다. 비록 그런 객체가 존재한다고 해도 객체와 사물간의 개념적 거리는 유사성을 찾기 어려울 정도록 매우 먼 것이 일반적이다.

-> 일반적인 설명 (잘못된 접근):
"객체지향은 현실을 그대로 모방하는 것"

자동차 → Car 클래스
사람 → Person 클래스
동물 → Animal 클래스

실제 개발에서는 현실에 없는 객체들이 대부분이다.

userService
PaymentProcessor
DatabaseConnection

현실에 사용자 서비스라른 사물이 있는가? 결제 처리기 실물이 존재하는가 ? 데이터베이스 연결이 실물인가 ?

올바른 이해: 객체지향은 “책임과 협력을 중심으로 소프트웨어를 구조화”하는 것이지, 현실을 단순히 복사하는 게 아닙니다.

핵심: 현실 모방보다는 문제 해결을 위한 추상화가 더 정확한 표현입니다!

모듈화 (기능을 작은 단위로 나누기) 책임 분산 (각 객체가 맡을 일 정하기) 재사용성 (코드 중복 줄이기)

“현실 모방” 설명은 교육용 비유일 뿐, 실제 객체지향의 본질과는 거리가 멀죠.

객체지향은 소프트웨어 구조화 방법론이지, 현실 복사기가 아닙니다.

역할, 책임, 협력 관점에서 본 객체지향

객체지향 설계라는 예술은 적절한 객체에게 적절한 책임을 할당하는 것에 시작된다. 책임은 객체지향 설계의 품질을 결정하는 가장 중요한 요소다 책임이 불분명한 객체는 애플리케이션의 미래 역시 불분명하게 만든다. 얼마나 적절한 책임을 선택하는냐가 애플리케이션의 아름다움을 결정한다

역할은 커피 주문에 참여하는 캐시어나 바리스타와 같이 협력에 참여하는 객체에 대한 일종의 페르소나다. 역할은 관련성 높은 책임의 집합이다.

역할이 정해지면 “이 객체가 무엇을 해야 하고, 무엇을 하지 말아야 하는지” 경계가 명확해집니다. 역할은 객체 설계의 나침반 역할을 합니다. “이 기능이 이 객체에 있는 게 맞나?” 고민될 때 역할을 보면 답이 나오죠. 결국 역할 → 책임 범위 설정 → 응집도 높은 객체 설계로 이어집니다.

프로퍼티와 속성 그리고 객체

상태는 특정 시점에 객체가 가지고 있는 정보의 집합으로 객체의 구조적 특징을 표현한다. 객체의 상태는 객체에 존재하는 정적인 프로퍼티와 동적인 프로퍼티 값으로 구성된다. 객체의 프로퍼티는 단순한 값과 다른 객체를 참조하는 링크로 구분할 수 있다.

객체의 행동

  • 객체 자신의 상태 변경
  • 행동 내에세 협력하는 다른 객체에 대한 메시지 전송
값이 같은지 여부는 상태가 같은지를 이용해 판단한다. 값의 상태가 같으면 두 인스턴스 는 동일한 것으로 판단하고 상태가 다르면 두 인스턴스는 다른 것으로 판단한다.
이 처럼 상태를 이용해 두 값이 같은지 판단할 수 있는 성질을 동응성 이라고 한다.
상태를 이용해 동등성을 판단할 수 있는 이유는 값의 상태가 변하지 않기 때문이다.
값의 상태는 결코 변하지 않기 때문에 어떤 시점에 동일한 타입의 두 값이 같다면 언제까지라도 두 값은 동등한 상태를 유지할 것이다. 
값은 오직 상태만을 이용해 동등성을 판단하기 때문에 인스턴스를 구별하기 위한 별도의 식별자를 필요로 하지않느다.

핵심: 값(Value) vs 객체(Object)의 차이

값(Value)

Integer a = 100;
Integer b = 100;

// a와 b는 다른 인스턴스지만 "같다"고 판단
System.out.println(a.equals(b));  // true

상태만 비교: 둘 다 “100”이라는 상태 식별자 필요 없음: “누가 만들었는지”, “언제 만들었는지” 상관없음 불변성: 100은 절대 101로 바뀌지 않음

객체(Object)

Person kim1 = new Person("김철수", 25);
Person kim2 = new Person("김철수", 25);

// 상태는 같지만 "다른 사람"
System.out.println(kim1 == kim2);  // false (다른 인스턴스)
// 의도적으로 equals를 오버라이드하지 않는 한

결론:

값 = 상태로만 판단 (“무엇인가”) 객체 = 식별자로 판단 (“누구인가”)

메모리 관점에서 보면:

Person kim1 = new Person("김철수", 25);  // 메모리 주소: 0x1000
Person kim2 = new Person("김철수", 25);  // 메모리 주소: 0x2000

System.out.println(kim1 == kim2);  // false (다른 메모리 주소)

하지만 값(Value)은 다르게 처리:

String s1 = "Hello";  // 메모리 주소: 0x3000
String s2 = "Hello";  // 같은 주소 0x3000 (String Pool)

Integer a = 100;  // 메모리 주소: 0x4000  
Integer b = 100;  // 같은 주소 0x4000 (Integer Cache)

글에서 “식별자가 필요 없다”는 말: 객체: “1층 자판기 vs 2층 자판기” → 위치(식별자)로 구분 필요 값: “100 vs 100” → 어디 있든 그냥 100은 100

결론: 글이 말하고자 하는 건 “값은 메모리 주소가 달라도 내용이 같으면 동등하다”는 것입니다. 메모리는 다르지만 상태 기반으로 동등성을 판단한다는 게 핵심이에요!재시도Claude는 실수를 할 수 있습니다. 응답을 반드시 다시 확인해 주세요.

의인화

현실 속의 객체와 소프트웨어 객체 사이의 가장 큰 차이점은? 현실 속에서는 수동적인 존재가 소프트웨어 객체로 구현될 때는 능동적으로 변하는 것

소프트웨어 객체를 창조할 때 우리는 결코 현실 세계의 객체를 모방하지 않는다. 오히려 소프트웨어 안에 창조하는 객체에게 현실 세계의 객체와는 전혀 다른 특징을 부여하는 것이 일반적이다. 소프트웨어 객체가 현실 객체의 부분적인 특징을 모방하는 것이 아닌 현실 객체가 가지지 못한 추가적인 능력을 보유하게 된다.

현실 속 트럼프 카드는 스스로 뒤집지도, 말을 할 수도, 걸을 수도 없다. 계좌는 스스로 금액을 이체할 수 없다. 스스로 판매 금액을 계산해서 종이에 기입하는 현실 속의 상품을 상상해보자

개발자는 객체지향 세계를 구축할 때 현실에서 가져온 객체들은 현실 속에서는 할 수 없는 어떤일이라도 할 수 있는 전지전능한 존재가 된다.

은유

객체지향에서의 은유는 은유 관계에 있는 실제 객체의 이름을 소프트웨어 객체의 이름으로 사용하면 표현적 차이를 줄여 소프트웨어의 구조를 쉽게 예측할 수 있다. 따라서 소프트웨어 객체에 대한 현실 ㄱ객체의 은유를 효과적로 사용할 경우 표현적 차이를 줄일 수 있으며, 이해하기 쉽고 유지보수가 용이한 소프트웨어를 만들 수 있다. 바로 이러한 이유로 모든 객체지향 지침서에서는 현실 세계인 도메인에서 사용되는 이름을 객체에게 부여하라고 가이드하는 것 이다.

좋은 은유적 이름:

class Customer {          // "고객" - 직관적
    void makeOrder() { }  // "주문하다" - 예측 가능
}

class PaymentProcessor {     // "결제 처리기" - 역할 명확
    void processPayment() { } // "결제를 처리하다" - 행동 예측 가능
}

나쁜 이름:

class DataManager {       // 뭘 관리하는지 모호
    void doProcess() { }  // 무엇을 처리하는지 불분명
}

class Handler {           // 뭘 다루는지 알 수 없음
    void execute() { }    // 무엇을 실행하는지 모름
}

은유의 효과:

  1. 표현적 차이 감소
    // 도메인 용어 그대로 사용
    Order order = new Order();        // "주문"
    order.cancel();                   // "취소하다"
    

    → 업무 담당자와 개발자가 같은 언어 사용

  2. 구조 예측 가능
    class Library {
     Book findBook(String title) { }     // 도서관에서 책을 찾는다
     void borrowBook(Book book) { }      // 책을 빌린다
     void returnBook(Book book) { }      // 책을 반납한다
    }
    // 메서드 이름만 봐도 기능 예측 가능
    

이로써 좋은 이름 == 코드 자체가 문서가 된다. 이해하기 쉽고 유지보수가 쉬워진다.

심볼(Symbol) - 이름

VendingMachine machine1;

내연(Instension) - 개념/정의

class VendingMachine {
    private int money;
    private int colaCount;
    
    public void insertMoney(int amount) { }
    public String buyDrink() { }
}

외연(Extension) - 실제 집합

VendingMachine machine1 = new VendingMachine();  // 1층 자판기
VendingMachine machine2 = new VendingMachine();  // 2층 자판기
VendingMachine machine3 = new VendingMachine();  // 3층 자판기

// machine1, machine2, machine3 모두 = VendingMachine의 외연

타입과 추상화

리차드 파인만은 “현상은 복잡하다. 법칙은 단순하다. 버릴게 무엇인지 알아내라”

추상화
어떤 양상, 세부 사항, 구조를 좀 더 명확하게 이해하기 위해 특정 절차나 물체를 의도적으로 생략하거나 감춤으로써
복잡도를 극복하는 방법

복잡성을 다루기 위해 추상화는 두 차원에서 이뤄진다.
- 첫 번재 차원은 구체적인 사물들 간의 공통점은 취하고 차이점은 버리는 일반화를 통해 단순하게 만드는 것이다.
- 두 번째 차원은 중요한 부분을 강조하기 위해 불필요한 세부 사항을 제거함으로써 단순하게 만드는 것이다.

객체와 타입

  1. 어떤 객체가 어떤 타입에 속하는지를 결정하는 것은 객체가 수행하는 행동이다. 어떤 객체들이 동일한 행동을 수행할 수 있다면 그 객체들은 동일한 타입으로 분류 될 수 있다.

  2. 객체의 내부적인 표현은 외부로부터 철저하게 감춰진다. 객체의 행동을 가장 효과적으로 수행할 수만 있다면 객체 내부의 상태를 어떤 방식으로 표현하더라도 무방하다.

일반화/특수화 관계 일반적인 타입은 특수한 타입에 비해 더 적은 수의 해동을 가지며 특수한 타입은 일반적인 타입에 비해 더많은 행동을 가진다. 단, 특수한 타입은 일반적인 타입이 할 수 있는 모든 행동을 동일하게 수행할 수 있어야 한다.

일반화/특수화 관계 = 리스코프 치환 원칙의 기반

리스코프 치환 원칙 (LSP): “특수화 타입은 일반화 타입을 완전히 대체할 수 있어야 한다”

// 일반화 타입으로 선언
Animal animal = new Dog();  // ✓ Dog는 Animal을 대체 가능

// Animal이 할 수 있는 모든 것을 Dog도 할 수 있어야 함
animal.eat();    // ✓ 동작
animal.sleep();  // ✓ 동작

핵심 정리: 일반화/특수화 올바르게 설계 → LSP 자동 준수 특수화는 일반화가 할 수 있는 모든 것을 할 수 있다 ↓ 리스코프 치환 원칙 만족 ↓ 안전한 다형성

타입은 추상화이다.

VendingMachine machine = new VendingMachine();

// 시간에 따라 계속 변함
시간 0분: money=0,    colaCount=10
시간 1분: money=1000, colaCount=10
시간 2분: money=0,    colaCount=9
시간 3분: money=500,  colaCount=9
// ... 계속 변화 (복잡함!)

→ “지금 이 자판기의 상태가 뭐지?” 계속 추적해야 함

VendingMachine machine = new VendingMachine();

// 상태가 어떻게 변하든
machine.insertMoney(1000);  // 돈을 넣는다
machine.buyDrink("콜라");   // 음료를 산다

// 내부 상태는 신경 안 써도 됨!

결론: 타입 = “복잡하게 변하는 객체를 간단한 개념으로 묶는 것”

타입 없이: 매 순간 상태 추적 (복잡) 타입으로: “뭘 할 수 있는지”만 알면 됨 (단순)

“자판기 내부가 어떻게 변하든, 돈 넣고 음료 나오는 게 자판기다”

동적 모델과 정적 모델, 그리고 타입 모델

정적 모델 (Static Model):

  • 시스템의 구조를 표현
  • 클래스, 객체, 속성, 관계(연관, 의존, 상속 등)
  • “무엇이 존재하는가?”
  • 예: 클래스 다이어그램, 객체 다이어그램

동적 모델 (Dynamic Model):

  • 시스템의 행동을 표현
  • 객체 간 상호작용, 메시지 흐름, 시간에 따른 상태 변화
  • “어떻게 동작하는가?”
  • 예: 시퀀스 다이어그램, 상태 다이어그램

Java로 개발할 때 우리가 작성하는 것: 클래스 정의 필드(속성) 선언 메서드 시그니처 클래스 간 관계(상속, 구현, 연관)

이 모든 것이 시스템의 구조를 표현하는 정적 모델입니다. 하지만 중요한 점: 코드는 정적 모델이지만 실행 시 객체들의 협력은 동적 모델입니다 “객체지향의 사실과 오해”에서 강조하는 것은 동적 모델(협력)을 먼저 설계하고, 그에 맞춰 정적 모델(클래스)을 만들라는 것입니다

동적 모델을 먼저 상상 - “어떤 객체들이 어떤 메시지를 주고받을까?”

정적 모델로 구현 - 그 협력을 지원하는 클래스 구조를 코드로 작성

타입 모델 (Type Model): 객체를 분류하는 관점에서 바라보는 모델입니다. 핵심 개념:

동일한 퍼블릭 인터페이스(공통 행동)를 가진 객체들의 범주 클래스가 타입을 구현하는 수단 “이 객체는 어떤 타입에 속하는가?”

동적 모델 - 객체들의 협력과 행동
정적 모델 - 시스템의 구조 (클래스, 속성, 관계)
타입 모델 - 객체의 분류 체계 (인터페이스, 다형성)