Book - 객체지향의 사실과 오해

책 정보

객체지향의 사실과 오해 - 위키북스
저자: 조영호
총 페이지 수: 260

완독 기록

완독일: 2024-04-11
난이도 (1~5): ⭐⭐⭐☆☆
이해도 (1~5): ⭐⭐⭐⭐☆

누군가 나에게 “객체 지향이 뭐야?” 라고 물어보면, 나는 그저 구글에 검색했을 때 나오는 “현실 세계를 투영하는 방식” 이라는 흔하고 모호한 답변밖에 할 수 없었다.
객체란 무엇인지, 이러한 객체를 지향하는 것은 무엇인지, 객체가 가진 책임이 무엇인지, 객체지향 설계 원칙이나 디자인 패턴은 왜 생겨났고, 그걸 왜 써야하는지…
객체 지향과 관련된 개념들은 단편적으로만 알고 있을 뿐, 핵심과 연결 고리를 제대로 이해하지 못하고 있었다.

『객체지향의 사실과 오해』는 이러한 물음에 대해 정리해주는 책이다. 이 책은 객체의 본질이 무엇인지부터 시작해, 그로부터 연관되고 파생된 개념들을 차근차근 짚어준다. 단순한 기법이나 이론이 아니라, 객체지향이 왜 필요한지를 현실 세계와의 유사성을 통해 자연스럽게 설명하며 본질적인 이해를 도와준다.


1. 협력하는 객체들의 공동체

핵심 키워드

협력: 여러 객체가 메시지를 주고받으며 공동의 목표를 달성하는 과정
객체: 다른 객체와 협력하여 문제를 해결하는 자율적인 존재
역할: 객체가 협력 안에서 수행하는 책임의 집합
책임: 객체가 수행해야 할 작업 또는 행동
자율성: 객체는 내부 상태를 스스로 관리하고, 자신이 할 일은 자신이 알아서 결정 메시지: 협력을 위해 요청/응답 하기 위한 수단
메서드: 객체가 메시지를 처리하는 구체적인 방법

주요 내용

객체지향이 모방하는 실세계의 의미

"객체지향의 목표는 실세계를 모방하는 것이 아니다. 오히려 새로운 세계를 창조하는 것이다. 소프트웨어 개발자의 역할은 단순히 실세계를 소프트웨어 안으로 옮겨 담는 것이 아니라 고객과 사용자를 만족시킬 수 있는 신세계를 창조하는 것이다."
  • 객체지향은 “실세계의 모방” 이라고 표현된다. 이것은 본질을 이해하기에 효과적인 은유이지, 실무적인 관점에서는 부적합 하다.

협력, 역할, 책임

"협력의 핵심은 특정한 책임을 수행하는 역할들 간의 연쇄적인 요청과 응답을 통해 목표를 달성한다는 것이다. 일상생활에서 목표는 사람들의 협력을 통해 달성되며, 목표는 더 작은 책임으로 분할되고 책임을 수행할 수 있는 적절한 역할을 가진 사람에 의해 수헹된다. 협력에 참여하는 각 개인은 책임을 수행하기 위해 다른 사람에게 도움을 요청하기도 하며, 이를 통해 연쇄적인 요청과 응답으로 구성되는 협력 관계가 완성된다."
  • 객체는 목표를 달성하기위해 요청(request) 과 응답(response)로 협력한다.
  • 협력 과정에서 객체는 역할(role)을 부여받고, 이러한 역할들은 책임(responsibility)을 갖는다.
  • 주문한 커피를 제조하는 협력 안에는 캐셔와 바리스타 라는 역할이 존재한다. 캐셔는 주문을 받는 책임을, 바리스타는 커피를 제조해야하는 책임을 가지고 있다.

객체가 가져야 하는 덕목

"객체는 충분히 '협력적'이어야 한다.― 객체가 충분히 '자율적'이어야 한다는 것이다.― 객체가 협력에 참여하는 과정 속에서 스스로 판단하고 스스로 결정하는 자율적인 존재로 남기 위해서는 필요한 행동(behavior)과 상태(state)를 함께 지니고 있어야 한다.―  객체의 관점에서 자율성이란 자신의 상태를 직접 관리하고 상태를 기반으로 스스로 판단하고 행동할 수 있음을 의미한다."
  • 객체는 열린 마음으로 요청을 하고, 요청을 받아야한다. 모든 역할을 혼자 수행하고자 한다면, 내부적인 복잡도가 올라간다.
  • 객체는 요청에 응할지, 응답을 어떤 방식으로 처리할지 스스로 결정한다.

메시지, 메서드

"풍부한 메커니즘을 이용해 요청하고 응답할 수 있는 인간들의 세계와 달리 객체지향의 세계에서는 오직 한 가지 의사소통 수단만이 존재한다. 이를 메시지라고 한다. 한 객체가 다른 객체에게 요청하는 것을 메시지를 전송한다고 말하고 다른 객체로부터 요청을 받는 것을 메시지를 수신한다고 말한다."    
"객체가 수신된 메시지를 처리하는 방법을 메서드(method) 라고 부른다."
  • 객체 간 요청과 응답을 위해 사용되는 수단은 메시지(message)이고, 객체가 수신된 메시지를 처리하는 방법은 메서드(method) 이다.
  • 메시지와 메서드를 분리하는 것은 객체의 자율성을 높히는 핵심이다.

    메시지가 요청하고자 하는 의도가 아닌 구체적인 메서드나 구현을 포함하면, 요청 객체는 응답 객체의 내부 구조를 알아야하기 때문에 결합도는 올라가고, 그로인해 캡슐화, 자율성이 약화된다.

객체 지향 설계를 위한 관점

"객체지향은 객체를 지향하는 것이지 클래스를 지향하는 것이 아니다."
  • 어떤 객체들이 어떤 메시지를 주고받으면 협력하는지 고민해야 한다.
  • 클래스는 객체들의 협력 관계를 코드로 옮기는 도구일 뿐이다.

고찰

객체 지향 언어를 사용하면서도 그동안 설계를 할 때는 객체의 관점이 아니라 코드 중심의 관점에서 출발했었다. 하지만 이번에 본질을 다시 생각해보고 관점을 전환해보니, 객체지향 설계의 실마리가 보이기 시작한 것 같다.
MSA 아키텍처에서 서비스 간 통신에 메시지를 사용하는 것도, 객체지향에서 비롯된 개념일 수 있겠다는 생각이 들었다. 그렇다면 서비스도 하나의 객체처럼 볼 수 있을 것 같은데?

feat.AI

Q. 서비스도 하나의 객체처럼 볼 수 있을 것 같은데?
A. “스케일”과 “컨텍스트”가 다를 뿐이에요.

개념 객체 서비스 (MSA)
자율성 필드 + 메서드를 스스로 관리
다른 객체에 최소한으로 의존
자기 데이터 + 로직을 스스로 처리
외부 서비스에 최소 의존
메시지 메서드 호출 HTTP/gRPC/RabbitMQ 등으로 통신
캡슐화 내부 필드와 구현 숨김 내부 DB나 로직은 외부에 노출 안 함
책임 하나의 도메인 역할 수행 하나의 비즈니스 도메인 책임 수행
독립성 재사용 가능한 모듈 독립적으로 배포 및 운영 가능

2. 이상한 나라의 객체

핵심 키워드

객체: 식별 가능한 개체, 사물
식별자: 어떤 객체를 다른 객체와 구분하는데 사용하는 객체의 프로퍼티
값: 객체의 특성을 표현하는데 사용되는 단순한 값
상태: 특정 시점에 객체가 가지고 있는 정보의 집합
행동: 외부의 요청 또는 수신된 메시지에 응답하기 위해 동작하고 반응하는 활동

주요 내용

객체와 값

"객체란 식별 가능한 개체 또는 사물이다.― 객체는 구별 가능한 식별자, 특징적인 행동, 변경 가능한 상태를 가진다. 소프트웨어 안에서 객체는 저장된 상태와 실행 가능한 코드를 통해 구현 된다."   
"값과 객체의 가장 큰 차이점은 갑은 식별자를 가지지 않지만 객체는 식별자를 가진다는 점이다."  
"숫자, 문자열, 양, 속도, 시간, 날짜, 참/거짓과 같은 단순한 값들은 객체가 아니다. 단순한 값들은 그 자체로 독립적인 의미를 가지기보다는 다른 객체의 특성을 표현하는데 사용된다."
  • 가변 상태(mutable state): 객체는 행동을 통해 상태를 변경한다.
  • 불변 상태(immutable state): 값의 상태는 변하지 않는다.
  • 동일성(identical): 객체는 식별자를 기반으로 객체가 같은지 판단할 수 있다.
  • 동등성(equality): 값은 상태를 이용해 값이 같은지 판단할 수 있다.
  • 값은 상태를 이용한 동등성 검사, 객체는 식별자를 이용한 동등성 검사를 통해 두 인스턴스를 비교할 수 있다.

// 동일성: 주소 비교

public class IdentityExample {
    public static void main(String[] args) {
        String a = new String("hello");
        String b = new String("hello");

        System.out.println(a == b); // false (동일하지 않음)
    }
}

// 동등성: 값 비교

public class EqualityExample {
    public static void main(String[] args) {
        String a = new String("hello");
        String b = new String("hello");

        System.out.println(a.equals(b)); // true (동등함, 내용 같음)
    }
}
참고

String의 경우 equals() 메서드가 오버라이딩 되어 있어 동등성 비교가 가능하나, 객체(Object)를 비교할 때는 객체의 주소를 비교하므로 ==과 동일하다. 따라서 객체 간의 실제 값 비교를 원할 경우, equals() 메서드를 오버라이딩하여 원하는 방식으로 비교 로직을 구현해야 한다.

상태(State)

"상태를 이용하면 과거의 모든 행동 이력을 설명하지 않고도 행동의 결과를 쉽게 예측하고 설명할 수 있다."  
"객체는 스스로의 행동에 의해서만 상태가 변경되는 것을 보장함으로써 객체의 자율성을 유지한다."   
"상태를 잘 정의된 행동 집합 뒤로 캡슐화하는 것은 객체의 자율성을 높이고 협력을 단순화하고 유연하게 만든다. 이것이 상태를 캡슐화 해야하는 이유다."  
  • 프로퍼티(property): 객체의 상태를 구성하는 모든 특징으로 프로퍼티 값은 변경되기 때문에 ‘동적’ 이다.
  • 속성(attribute): 객체를 구성하는 단순한 값
  • 링크(link): 객체와 객체 사이의 의미있는 연결
  • 쿼리(query): 객테의 상태를 조회하는 작업
  • 명령(command): 객체의 상태를 변경하는 작업

행동(Behavior)

"객체의 행동에 의해 객체의 상태가 변경된다는 것은 행동이 부수 효과(side effect)를 초래한다는 것을 의미한다."  
"객체의 행동은 객체의 상태를 변경시키지만 행동의 결과는 객체의 상태에 의존적이다."  
"객체가 다른 객체와 협력하는 유일한 방법은 다른 객체에게 요청을 보내는 것이다."  
"객체가 외부에 노출하는 것은 행동뿐이며, 외부에서 객체에 접근할 수 있는 유일한 방법 역시 행동 뿐이다."   
public class Order {  
    // 캡슐화: 필드는 private으로 숨기고, 접근은 메서드를 통해 제한함
    private String orderId; // 속성: Order 서비스의 ID
    private int quantity;  // 속성: 주문 수량
    private int price;     // 속성: 총 가격
    private String userId; // 링크: User 서비스의 유저 ID

    ...
    // 프로퍼티
    public String getOrderId() {  // 쿼리
        return orderId;
    }

    public void setOrderId(String orderId) {  // 커맨드
        this.orderId = orderId;
    }

    ...

    ...
    // 행동 (Behavior): 주문 총액을 계산
    public int calculateTotalPrice() {
        return quantity * price;
    }
    ...
}
참고

MSA구조에서는 링크를 ID 기반으로 설계하는 게 일반적, 서비스 간 강한 결합을 줄이기 위해 객체 자체를 들고 있지 않고, 필요한 정보는 API를 통해 조회하거나, CQRS/이벤트를 활용

“행동이 상태를 결정한다.”

"상태를 먼저 결정하고 행동을 나중에 결정하는 방법은 설계에 나쁜 영향을 끼친다."  
"첫째, 상태를 먼저 결정할 경우 캡슐화가 저해된다."  
"둘째, 객체를 협력자가 아닌 고립된 섬으로 만든다."  
"셋째, 객체의 재사용성이 저하된다."  
"객체지향 설계는 애플리케이션에 필요한 협력을 생각하고 협력에 참여하는 데 필요한 행동을 생각한 후 행동을 수행할 객체를 선택하는 방식으로 수행된다."
public class Player {
    private int hp;
    private boolean isDead;

    public Player() {
        this.hp = 100;
        this.isDead = false;
    }

    // 행동(takeDamage)을 통해 상태(hp, isDead)를 내부에서 결정
    // 요청하는 객체는 구체적인 방법은 신경쓰지 않고, 결과만 기대하면 됨(협력에 최적화)
    // 객체 스스로 행동에 따라 상태를 결정하므로, 객체 내부 로직이 '응집'되고, '일관성' 있게 유지되기 때문에 내부 수정이 외부에 영향을 주지 않아 재사용성이 높아짐
    public void takeDamage(int amount) { 
        if (isDead) return;
        this.hp -= amount;

        if (this.hp <= 0) { 
            die();
        }
    }

    private void die() {  // '죽는다'는 행동 내부 캡슐화
        this.isDead = true;
    }

    public boolean isDead() {
        return isDead;
    }
}

현실 객체와 소프트웨어 객체

"추상화(abstraction)란 실제의 사물에서 자신이 원하는 특성만 취하고 필요 없는 부분을 추려 핵심만 표현하는 행위"  
"현실 속의 객체와 소프트웨어 객체 사이의 가장 큰 차이점 그것은 현실 속에서 수동적인 존재가 소프트웨어 객체로 구현될 떄는 능동적으로 변한다는 것"    
"객체지향 세계를 구축할 때 현실에서 가져온 객체들은 현실 속에서는 할수 없는 어떤 일이라도 할 수 있는 전지전능한 존재가 된다."  

고찰

현실에서의 객체는 단지 물건에 불과하지만, 객체지향 프로그래밍에서는 객체에 상태와 행동을 부여함으로써 살아 움직이는 존재로 만들 수 있다. 마치 미녀와 야수의 포트 부인(주전자)이나 뤼미에르(촛대)처럼, 코드라는 마법을 활용해서 말이다. 이처럼 객체가 단순한 데이터 저장소가 아니라, 스스로 책임을 지고 행동할 수 있는 주체가 될수 있도록 하는 것이 객체 지향이라니, 이것을 깨달으니 객체 지향이 얼마나 매력적인 것인지 알게 되었다.


3. 타입과 추상화

핵심 키워드

추상화: 세부사항을 제거하고 본질만 남기는 과정
타입: 데이터와 그 연산을 정의하는 분류
다형성: 같은 인터페이스로 다른 결과를 얻는 능력
서브타입: 상위 타입을 상속받아 확장한 타입
슈퍼타입: 공통 특성을 정의한 상위 타입

주요 내용

추상화

"지하철 노선도의 핵심은 '정확성'을 버리고 그 '목적'에 집중한 결과"  
"지하철을 갈아탈 때 지형 때문에 골치 아플 필요가 있을까요? 지형은 중요한 것이 아닙니다. 중요한 것은 연결, 즉 열차를 갈아타는 것"   
"추상화란 현실에서 출발하되 불필요한 부분을 도려내가면서 사물의 놀라운 본질을 드러나게 하는 과정"   
  • 실제 지하철 노선은 복잡하지만, 우리가 흔히 보는 노선도는 단순한 선들로 이루어져 있다. ‘정차, 환승 구간 확인’ 등 목적에만 집중하여 불필요한 부분을 제거하고 목적 본질만 표현했기 때문이다. 이러한 과정을 추상화라고 하는데, 다음과 같은 방법으로 이루어진다.
    1. 일반화: 공통점은 유지하고, 차이점은 제거하여 대상을 단순화한다.
    2. 세부 제거: 중요한 부분을 강조하기 위해, 덜 중요한 세부 정보를 생략한다.

타입

"개념은 객체들의 복잡성을 극복하기 위한 추상화 도구다."  
"타입은 개념과 동일하다. 따라서 타입이란 우리가 인식하고 있는 다양한 사물이나 객체에 적용할 수 있는 아이디어나 관념을 의미한다. 어떤 객체에 타입을 적용할 수 있을 때 그 객체를 타입의 인스턴스라고 한다."    
"타입은 데이터가 어떻게 사용되느냐에 관한 것이다.―데이터 타입은 메모리 안에 저장된 데이터의 종류를 분류하는 데 사용하는 메모리 집합에 관한 메타데이터다. 데이터에 대한 분류는 암시적으로 어떤 종류의 연산이 해당 데이터에 대해 수행될 수 있는지를 결정한다."
  • 객체지향에서 데이터 타입은 추상화이며, 객체의 타입은 해당 객체가 표현하는 개념적 특성(속성과 행위)을 나타낸다. 이때 객체의 행위는 메서드로 구현되며, 이는 곧 해당 타입이 가진 연산자라고 볼 수 있다.
public class Barista { // 타입 (Barista라는 역할을 추상화)

    public void makeCoffee(String coffeeType) { // 연산자 (바리스타가 할 수 있는 행동)
        System.out.println(coffeeType + "를 만들었다.");
    }
}

슈퍼 타입, 서브 타입

"어떤 객체가 어떤 타입에 속하는지를 결정하는 것은 객체가 수행하는 행동이다."    
"객체의 내부적인 표현은 외부로부터 철저하게 감춰진다."   
"다형성이란 동일한 요청에 대해 서로 다른 방식으로 응답할 수 있는 능력을 뜻한다."  
"일반적인 타입을 슈퍼타입(Supertype)이라고 하고, 좀 더 특수한 타입을 서브 타입(Subtype)이라고 한다.―서브 타입은 슈퍼타입의 행위와 호환되기 때문에 서브 타입은 슈퍼타입을 대체할 수 있어야 한다."   
"클래스는 타입을 구현하는 여러 매커니즘 중 하나일 뿐이다."    
  • 객체의 타입을 결정하는 것은 그 객체가 수행할 수 있는 행동이며, 이러한 내부 연산자는 캡슐화를 통해 외부에 감춰지고, 다형성을 가능하게 한다.
  • 타입이 가진 일반적인 행동은 서브타입으로 확장될 수 있으며, 서브타입은 슈퍼타입의 행동을 모두 상속받고, 추가적인 특수 행동을 가질 수 있다. 이때 슈퍼타입은 서브타입으로 대체 가능(리스코프 치환 원칙)해야 한다.
// 슈퍼타입 - Animal
public class Animal {
    // 모든 동물이 하는 공통 행동
    public void eat() {
        System.out.println("냠냠");
    }
    
    public void sleep() {
        System.out.println("쿨쿨");
    }
}

// 서브타입 - Bird
public class Bird extends Animal {
    // Animal의 모든 행동을 상속받음
    
    // Bird만의 특수한 행동
    public void fly() {
        System.out.println("푸드덕푸드덕");
    }
}

...
// 메인 클래스
public class Main {
    public static void main(String[] args) {
        // 다형성: 슈퍼타입으로 서브타입 참조
        Animal bird = new Bird();
        
        // 공통 행동 호출
        bird.sleep(); 
        
        // 서브타입의 특수 행동을 사용하려면 형변환 필요
        ((Bird) bird).fly();  // "푸드덕푸드덕"
    }
}
...

고찰

현실 세계에서 참새, 비둘기, 펭귄처럼 다양한 새들을 하나의 “조류”라는 타입으로 분류하듯, 객체지향에서도 추상화를 통해 서로 다른 객체들을 공통의 타입으로 묶을 수 있다.
이 과정을 통해 객체지향은 현실 세계를 모방하는 것이 아니라, 현실을 기반으로 ‘새로운 세계’를 창조하는 행위라는 것을 다시 한번 깨달았다.

개의 검색결과가 있습니다. ""

    검색결과가 없습니다. ""