오브젝트; 객체, 설계

🗓️

  • 이론보다 실무가 먼저다.
    • 설계에 관한 이론은 1970년이 되서야 등장했다.
  • 설계에 대한 이론은 있으나 유지보수에 대한 이론은 없다.

객체지향 프로그램을 설계하고 유지보수하는 데 필요하는 원칙과 기법을 설명하는것이 목적이다.

티켓 판매 어플리케이션

코드 보기
public class Invitation {
    private LocalDateTime when;
}

public class Ticket {
    private Long fee;

    public Long getFee() {
        return fee;
    }
}

public class Bag {
    priavte Long amount;
    private Invitation invitation;
    private Ticket ticket;

    public Bag(long amount) {
        this(null, amount);
    }

    public Bag(Invitation invitation, long amount) {
        this.invitation = invitation;
        this.amount = amount;
    }

    public boolean hasInvitation() {
        return invitation != null;
    }

    public boolean hasTicket() {
        return ticket != null;
    }

    public void setTicket(Ticket ticket) {
        this.ticket = ticket;
    }

    public void minusAmount(Long amount) {
        this.amount -= amount;
    }

    public void plusAmount(Long amount) {
        this.amount += amount;
    }
}

public class Audience {
    private Bag bag;

    public Audience (Bag bag) {
        this.bag = bag;
    }

    public Bag getBag() {
        return bag;
    }
}

publc class TicketOffice {
    private Long amount;
    private List<Ticket> tickets = new ArrayList<>();

    public TicketOffice(Long amount, Tocket ... tickets) {
        this.amount = amount;
        this.tocket.addAll(Arrays.asList(tickets));
    }

    public Ticket getTicket() {
        return tickets.remove(0);
    }

    public void minusAmount(Long amount) {
        this.amount -= amount;
    }

    public void plusAmount(Long amount) {
        this.amount += amount;
    }
}

public class TicketSeller {
    private TicketOffice ticketOffice;

    public TicketSeller(TicketOffice ticketOffice) {
        this.ticketOffice = ticketOffice;
    }

    public TicketOffice getTicketOffice() {
        return ticketOffice;
    }
}

public class Theater {
    private TicketSeller ticketSeller;

    public Theater(TicketSeller ticketSeller) {
        this.ticketSeller = ticketSeller;
    }

    public void enter(Audience audience) {
        if (audience.getBag().hasInvitation()) {
            Ticket ticket = ticketSeller.getTicketOffice().getTicket();
            audience.getBag().setTicket(ticket);
        } else {
            Ticket ticket = ticketSeller.getTicketOffice().getTicket();
            audience.getBag().minusAmount(ticket.getFee());
            ticketSeller.getTicketOffice().plusAmount(ticket.getFee());
            audience.getBag().setTicket(ticket);
        }
    }
}

무엇이 문제인가

소프트웨어 모듈의 목적

  1. 실행 중에 제대로 동작하는 것이다.
  2. 변경을 위해 존재한다. 대부분의 모듈은 생명주기 동안 변경되기 때문에 간단한 작업 만으로도 변경이 가능해야 한다.
  3. 코드를 읽는 사람과 의사소통 하는것.

예상을 빗나가는 코드

  • 현실의 프로세스와 벗어난 비즈니스 로직은 코드를 읽는 사람과 제대로된 소통이 되지 않는다.
  • 하나의 메서드(또는 클래스)에서 너무 많은 세부사항을 다루면 코드를 작성하는 사람뿐만 아니라 코드를 읽고 이해해야 하는 사람 모두에게 큰 부담을 준다.
  • 너무 많은 사항을 다루면 변경시 연관된 부분을 함께 변경해야 한다.

변경에 취약한 코드

  • 관람객이 현금과 초대장을 보관하기 위해 항상 가방을 들고 다녀야 한다.
  • 판매원이 매표소에서만 티켓을 판매한다고 가정한다.
  • 현금이 아니라 신용카드라면? 매표소 밖에서 판매해야 한다면? 이런 경우 코드 전체가 흔들리게 된다.

이것은 객체 사이의 의존성과 관련된 문제다. 의존성은 변경과 관련돼 있다.

  • 의존성이라는 말 속에는 어떤 객체가 변경될 때 그 객체에게 의존하는 다른 객체도 함께 변경될 수 있다는 사실이 내포되어 있다.

객체 사이의 의존성이 과한 경우를 가리켜 결합도 Coupling 가 높다고 한다.

  • 설계의 목표는 객테 사이의 결합도를 낮춰 변경이 용이한 설계를 만드는 것이어야 한다.

설계 개선하기

  1. 변경 취약
  2. 의사소통 어려움 (이해하기 어려운 코드)

자율성을 높이자

여러 객체가 서로에 접근할 수 있기 때문에 교통정리를 한다.
여기서는 다음과 같이 정리한다.

  • Theater.enter() 에서 TicketOffice에 접근하는 모든 코드를 TicketSeller 내부로 숨긴다.
public class Theater {
    private TicketSeller ticketSeller;

    public Theater(TicketSeller ticketSeller) {
        this.ticketSeller = ticketSeller;
    }

    public void enter(Audience audience) {
        ticketSeller.sellTo(audience);
    }
}

public class TicketSeller {
    private TicketOffice ticketOffice;

    public TicketSeller(TicketOffice ticketOffice) {
        this.ticketOffice = ticketOffice;
    }

    public void sellTo(Audience audience) {
        if (audience.getBag().hasInvitation()) {
            Ticket ticket = ticketSeller.getTicketOffice().getTicket();
            audience.getBag().setTicket(ticket);
        } else {
            Ticket ticket = ticketSeller.getTicketOffice().getTicket();
            audience.getBag().minusAmount(ticket.getFee());
            ticketSeller.getTicketOffice().plusAmount(ticket.getFee());
            audience.getBag().setTicket(ticket);
        }
    }
}

TicketSeller.getTicketOffice()를 제거함으로써 외부에서 ticketOffice에 직접 접근할 수 없게 되었다.

이렇게 객체 내부의 세부적인 사항을 감추는 것을 캡슐화 Encapsulation 라고 한다.

  • 객체 내부로의 접근을 제한하면 결합도를 낮출 수 있다.
  • 설계를 좀 더 쉽게 변경할 수 있다.

Theater의 경우 오직 TicketSeller의 인터페이스에만 의존한다. TicketSeller 가 내부에 TicketOffice 인스턴스를 포함하고 있다는 사실은 구현 Implementation영역에 속한다.

객체를 인터페이스와 구현으로 나누고 인터페이스만을 공개하는 것은 객체 사이의 결합도를 낮추고 변경하기 쉬운 코드를 작성하기 위해 따라야 하는 가장 기본적인 설계 원칙이다.

다음은 AudienceTicketSeller에 대한 정리다.

public class Audience {
    private Bag bag;

    public Audience (Bag bag) {
        this.bag = bag;
    }

    public Long buy(Ticket ticket) {
        if(bag.hasInvitation()){
            bag.setTicket(ticket);
            return 0L;
        } else {
            bag.setTicket(ticket);
            bag.minusAmount(ticket.getFee());
            return ticket.getFee();
        }
    }
}

public class TicketSeller {
    private TicketOffice ticketOffice;

    public TicketSeller(TicketOffice ticketOffice) {
        this.ticketOffice = ticketOffice;
    }

    public void sellTo(Audience audience) {
        ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
    }
}

위와 마찬가지로 결합도가 낮아졌다.

무엇이 개선됐는가

  • AudienceTicketSeller는 자신이 가지고 있는 소지품을 스스로 관리한다.
  • AudienceTicketSeller의 내부 구현을 변경 하더라도 Theater를 함께 변경할 필요가 없어졌다.
  • 변경 용이성 측면에서 개선되었다.

어떻게 한 것인가

  • TicketOffice를 사용하는 모든 부분을 TicketSeller 내부로 옮겼다
  • Bag을 사용하는 모든 부분을 Audience 내부로 옮겼다.
  • 자기 자신의 문제를 스스로 해결하도록 변경하여 객체의 자율성을 높히는 방향으로 설계했다.

캡슐화와 응집도

객체 내부의 상태를 캡슐화 하고 객체간에 오직 메시지를 통해서만 상호작용하도록 만드는 것이다.
밀접하게 연관된 작업만을 수행하고 연관성 없는 작업은 다른 객체에게 위임하는 객체를 가리켜 응집도 Cohesion 가 높다고 한다.
자신의 데이터를 스스로 처리하는 자율적인 객체를 만들면 결합도를 낮출 수 있을 뿐더러 응집도를 높일 수 있다.

절차지향과 객체지향

맨 처음 Theater.enter()프로세스 Process 다. 거기에 호출되는 Audience, TicketSeller, Bag, TicketOffice데이터 Data 다. 프로세스와 데이터를 별도의 모듈에 위치시키는 방식을 절차적 프로그래밍 Procedural Programming 이라고 한다.

  • 절차적 프로그래밍은 직관과 위배된다
  • 절차적 프로그래밍의 세계에서는 관람객과 판매원이 수동적인 존재일 뿐이다.
  • 절차적 프로그래밍의 세상은 예상을 너무나도 쉽게 벗어나기 때문에 코드를 읽는 사람과 원할하게 의사소통 하지 못한다.
  • 데이터 변경으로 인한 영향을 지역적으로 고립시키기 어렵다.

자신의 데이터를 스스로 처리하도록 프로세스의 적절한 단계를 AudienceTicketSeller 로 이동시켜 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍 하는 방식을 객체지향 프로그래밍 (OOP) 라고 한다.

  • 훌륭한 객체지향의 설계 핵심은 캡슐화를 이용해 의존성을 적절히 관리함으로써 객체 사이의 결합도를 낮추는 것이다. 이렇게 하면 절차지향에 비해 변경에 유연해 진다.
  • 객체지향 코드는 자신의 문제를 스스로 처리해야한다는 예상을 만족해야한다.
  • 객체 내부의 변경이 객체 외부에 파급되지 않도록 제어할 수 잇다.

책임의 이동

절차지향과 객체지향의 근본적인 차이를 만드는 것은 책임의 이동 Shift of responsibility다. 책임은 기능을 가리킨다.

  • 객체지향 설계에서는 각 객체에 책임이 적절하게 분배된다.
  • 각 객체는 자신을 스스로 책임진다.

훌륭한 객체지향 설계

  • 불필요한 의존성을 제거함으로써 객체 사이의 결합도를 낮춘다.
  • 외부의 객체가 몰라도 되는 세부사항을 내부로 감춰 캡슐화 한다.
  • 자율적인 객체들이 낮은 결합도와 높은 응집도를 가지고 협력하도록 최소한의 의존성만을 남기는 것.

객체지향 설계

비록 현실에서 수동적인 존재라도 객체지향의 세계에서는 모든것이 능동적이고 자율적인 존재가 된다.

이 원칙을 가리켜 의인화 Anthropomorphism이라고 한다.

설계가 왜 필요한가

설계란 코드를 배치하는 것이다.

  • 코드 작성은 기능을 구현하는 것과 동시에 쉽게 변경할 수 있게 작성해야 한다.
  • 변경을 수용할 수 있는 설계가 중요한 이유는 요구사항이 항상 변경되기 때문이다.
  • 코드를 변경할 때 버그가 추가될 가능성이 높기 대문에 변경을 수용할 수 있어야 한다.

1장 스터디 토론 내용 : Issues · IDIOT-s/object-book-study (github.com)