[오브젝트] #1. 객체, 설계

bien·2024년 9월 9일
0

오브젝트

목록 보기
1/13

01. 티켓 판매 애플리케이션 구현하기

  • 작은 소극장 프로젝트
    • 추첨을 통해 공연 무료 관람 초대장을 발송
    • 이벤트 당첨 고객은 무료로, 비당첨 고객은 가격을 지불하고 입장해야 한다.

Invitation.java

public class Invitation {
    private LocalDateTime when;
}

Ticket

package theater;

public class Ticket {
    private Long fee;

    public Long getFee() {
        return fee;
    }

}

Bag

package theater;

public class Bag {
    private 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 = this.amount - amount;
    }

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

Audience

package theater;

public class Audience {
    private Bag bag;

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

    public Bag getBag() {
        return bag;
    }
}

TicketOffice

package theater;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

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

    public TicketOffice(Long amount, Ticket ... tickets) {
        this.amount = amount;
        this.tickets.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;
    }

}

TicketSeller

package theater;

public class TicketSeller {
    private TicketOffice ticketOffice;

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

    public TicketOffice getTicketOffice() {
        return ticketOffice;
    }
}

Theater

package theater;

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);
        }
    }
}


02. 무엇이 문제인가

  • 로버트 마틴(Robert C. Martin); 소프트웨어 모듈이 가져야 하는 3가지 기능
    1. 실행 중에 제대로 동작하는 것.
    2. 변경을 위해 존재.
    3. 코드를 읽는 사람과 의사소통 하는 것.

예상을 빗나가는 코드

마지막에 소개한 Theater 클래스의 enter 메서드가 수행하는 일을 풀어 설명하면 다음과 같다.

소극장은 관람객의 가방을 열어 그 안에 초대장을 살펴본다.
가방 안에 초대장이 있으면 판매원은 매표소에 보관돼 있는 티켓을 관람객의 가방으로 옮긴다.
가방 안에 초대장이 들어있지 않으면 관람객의 가방에서 티켓 금액만큼의 현금을 꺼내 매표소에 적립한 후, 
	매표소에 보관돼 있는 티켓을 관람객의 가방 안으로 옮긴다.
  • 관람객판매원이 수동적인 존재이다.
    • 우리의 상식과는 다르게, 소극장이 허락 없이 티켓과 현금에 마음대로 접근하고 있다.
      • 이로 인해 코드를 읽는 사람과 제대로 의사소통하지 못한다.
  • enter메서드를 이해하기 위해서는, 아래의 세부사항을 모두 알고 있어야 한다.
    1. AudienceBag을 가지고 있음.
    2. Bag안에는 현금과 티켓이 들어 있음.
    3. TicketSellerTicketOffice에서 티켓을 판매함.
    4. TicketOffice안에 돈과 티켓이 보관되어 있음.
  • 하나의 클래스에서 너무 많은 세부사항을 다뤄, 코드를 읽고 이해하는 모두에게 큰 부담을 준다.
  • AudienceTicketSeller를 변경할 경우 Theater도 함께 변경해야 한다.

변경에 취약한 코드

  • 의존성(dependency)
    • 어떤 객체가 변경될 때 그 객체에게 의존하는 다른 객체도 함께 변경될 수 있다는 사실이 내포되어 있다.
    • 객체사이의 의존성을 완전히 없애는 것이 정답은 아니다.
      • 객체지향 설계; 서로 의존하며 협력하는 객체들의 공동체를 구축하는 것.
      • 목표; 애플리케이션의 기능 구현에 필요한 최소한의 의존성만 유지하고, 불필요한 의존성을 제거하는 것.
  • 결합도(coupling)
    • 결합도가 높다; 객체 사이의 의존성이 과한 경우를 의미
      • 두 객체 사이의 결합도가 높을수록, 함께 변경될 확률도 높아져, 변경이 어려워진다.
    • 목표; 객체 사이의 결합도를 낮춰 변경이 용이한 설계를 만드는 것.

03. 설계 개선하기

자율성을 높이자

  • 변경이 어려운 이유
    • TheaterAudienceTicketSeller뿐 아니라 내부의 BagTicketOffice까지 마음껏 접근할 수 있는것이 문제이다.
  • 해결 방법
    • Audience가 직접 Bag을 조작하고, TicketSeller가 직접 TicketOffice를 처리하는 자율적인 존재가 되도록 설계를 변경한다.

💻 결과코드; 판매 기능을 TicketSeller에게

Theater

package theater;

public class Theater {
    private TicketSeller ticketSeller;

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

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

TicketSeller

package theater;

public class TicketSeller {d
    private TicketOffice ticketOffice;

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

    public void sellTo(Audience audience) {
        if (audience.getBag().hasInvitation()) {
            Ticket ticket = ticketOffice.getTicket();
            audience.getBag().setTicket(ticket);
        } else {
            Ticket ticket = ticketOffice.getTicket();
            audience.getBag().minusAmount(ticket.getFee());
            ticketOffice.plusAmount(ticket.getFee());
            audience.getBag().setTicket(ticket);
        }
    }
}
  • 캡슐화(encapsulation)
    • 개념적이나 물리적으로 객체 내부의 세부적인 사항을 감추는 것.
    • 목적; 변경하기 쉬운 객체를 만드는 것.
    • 객체 내부로의 접근이 제한되어, 객체와 객체 사이의 결합도가 낮아지므로, 설계를 더 쉽게 변경할 수 있다.
  • private 접근제어자의 사용으로 ticketSellerticketOffice의 접근이 제한된다.
    • ticketSeller 객체 내부에서만 접근 가능하므로, ticketOffice 를 조작하는 일을 스스로 수행할 수 밖에 없어졌다.
  • 인터페이스(interface)
    • Theater는 단지 ticketSellersellTo를 이해하고 응답할 수 있다는 사실만 알고 있다.
    • 즉, TheaterticketSeller인터페이스에만 의존한다.
  • 구현(implementation)
    • ticketSeller가 내부에 TicketOffice 인스턴스를 포함하고 있다는 사실은 구현영역에 속한다.

객체를 인터페이스와 구현으로 나누고, 인터페이스만을 공개하는 것은 객체 사이의 결합도를 낮추기 위한 기본적인 설계 원칙이다.

💻 결과코드; 구매 기능을 Audience에게

Audience.java

package theater;

public class Audience {
    private Bag bag;

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

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

TicketSeller

package theater;

public class TicketSeller {
    private TicketOffice ticketOffice;

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

    public void sellTo(Audience audience) {
        ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
    }
}
  • Audience가 자신의 Bag을 스스로 관리한다.
  • TicketSellerAudience의 인터페이스에만 의존한다.
    • 둘 사이의 결합도가 낮아졌다.
    • Audience의 내부 구현이 캡술화 되었으므로, 구현이 변경되더라도 TicketSeller에는 영향을 미치지 않는다.
      • 변경이 Audience, TicketSeller 내부만으로 제한되었으므로, 변경 용이성의 측면에서 개선되었다.

캡슐화와 응집도

  • 자신의 데이터를 스스로 처리하는 자율적인 객체 생성
    • 밀접하게 연관된 작업만 수행하고, 연관성 없는 작업은 다른 객체에게 위임하는 객체가 생성됨.
      • 이러한 객체를 응집도(cohension)가 높다고 이야기 한다.
  • 훌륭한 객체지향 설계의 초석
    1. 외부의 간섭을 최대한 배제하고
    2. 메시지를 통해서만 협력하는
    3. 자율적인 객체들의 공동체

절차지향과 객체지향

프로세스와 메서드

수정하기 전의 코드

Theaterenter메서드에서 AudienceTicketSeller로부터 BagTicketOffice를 가져와서 관람객을 입장시킨다.

위 코드의 프로세스와 데이터는 아래와 같다.

  • 프로세스(Process)
    • Theaterenter메서드.
  • 데이터(Data)
    • 필요한 정보를 제공
    • Audience, TicketSeller, TicketOffice, Bag

절차지향적 프로그래밍(Procedual Programming)

  • 절차적 프로그래밍 (Procedual Programming)
    • 프로세스와 데이터를 별도의 모듈에 위치시키는 방식
      • 처리의 대상이 되는 데이터와 로직이 별도의 클래스에 있으므로, 로직을 가지는 클래스에서 필연적으로 의존성을 갖게 된다.
  • 아래의 그림은 절차적 프로그래밍 방식으로 작성된 코드의 전형적인 의존성 구조를 보여준다.
    • 프로세스를 담당하는 Theater에서 나머지 모든 클래스에 대한 의존성을 가지고 있다.

절차적 프로그래밍의 책임 구조

  • 책임이 Theater에 집중되어 있다.

1. 절차지향적 프로그래밍은 우리의 직관에 위배된다.

  • 우리는 각 클래스가 자신의 역할을 수행할 것이라 기대하지만, 절차적 프로그래밍 세계에서는 클래스들이 수동적이다.
    • 객체가 스스로의 역할을 수행하지 않으므로, 코드를 읽는 개발자로 하여금 원활한 의사소통이 힘들게 한다.

2. 데이터의 변경으로 인한 영향을 지역적으로 고립하기 어렵다.

  • 예시에서 AudienceTicketSeller의 내부 구현을 변경하려면 Theaterenter 메서드를 변경해야 한다.
    • 객체 필드의 변경이 외부에서 이뤄지므로, 이 변경이 어디에서 어떻게 적용되고 있을지 파악하기가 어렵다.
    • 변경 로직이 객체 내부에 머무르지 않고 외부로 노출되는 것이 가능해지는 순간부터, 해당 로직이 어디서 어떻게 사용되고 있을지 쉽게 추측하기 어렵고, 해당 필드의 변경을 특정 영역내부로 제한하기가 어려워진다.

절차지향의 한계; 변경의 어려움

  • 변경하기 쉬운 설계는 한 번에 하나의 클래스만 변경할 수 있는 설계다.
    • 절차적 프로그래밍은 프로세스가 필요한 모든 데이터에 의존해야 한다는 근본적인 문제점 때문에 변경에 취약할 수 밖에 없다.

객체지향 프로그래밍(Object-Oriented Programming)

  • 객체지향 프로그래밍 (Procedual Programming)
    • 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍하는 방식
  • 아래의 그램은 객체지향 프로그래밍의 방식으로 구현된 구조를 표현한다.
    • Theater는 오직 TicketSeller에만 의존한다.
    • TickestSeller입장에서 Audience에 대한 의존성이 생겼지만, 적절한 트레이드 오프이다.
      • 최종적으로 의존성은 적절히 통제되고 있으며, 하나의 변경으로 인한 여파가 여러 클래스로 전파되는 것을 효율적으로 억제한다.

객체지향의 장점; 변경에 유연함

  • 캡슐화를 이용해 의존성을 적절히 관리 ▶️ 객체 사이의 결합도 낮춤
    • 절차지향에 비해 변경에 유연한 이유
  • 문제를 스스로 처리한다는 예상을 만족 ▶️ 이해가 쉬움
  • 객체 내부의 변경이 객체 외부에 파급되지 않음 ▶️ 변경이 쉬움

객체지향 설계의 책임 구조

  • 제어 흐름이 각 객체에 적절하게 분산되어 있다.

책임의 이동

  • 책임의 이동 (shift of responsebility); 두 방식 사이의 근본적 차이 생성
    • 책임; 객체지향 세계의 기능을 가리키는 용어
  • 객체지향 설계; Theater에 몰려 있던 책임을 개별 객체로 이동시켰다.
    • 객체지향 설계에서는 독재자가 존재하지 않고, 각 객체에 책임이 적절하게 분배된다.
    • 이런 관점으로, 객체지향 프로그래밍을 데이터와 프로세스를 하나의 단위로 통합해 놓는 방식으로 표현하기도 한다.
      • 객체지향을 구현 관점에서만 바라본 편협한 시각이긴 하나, 입문자에게 실용적인 조언일 수 있다.
      • 객체지향 안에는 단순히 데이터와 프로세스를 하나의 객체 안으로 모으는 것 이상의 무언가가 있다.
    • 객체지향 설계의 핵심은, 적절한 객체에 적절한 책임을 할당하는 것이다.
      • 객체는 다른 객체와의 협력이라는 문맥 안에서 특정한 역할을 수행하는데 필요한 적절한 책임을 수행해야 한다.
      • 따라서, 객체가 어떤 데이터를 가지느냐 보다, 객체에 어떤 책임을 할당할 것이냐에 초점을 맞춰야 한다.
  • 책임 분배로 얻을 수 있는 장점;
    • 변경에 탄력적으로 대응 가능한 견고한 설계
    • 이해하기 쉬운 구조 & 읽기 쉬운 코드
  • 설계를 어렵게 만드는 것은 의존성
    • 해결방법; 불필요한 의존성을 제거해 객체 사이의 결합도를 낮추기
      • 방법; 세부사항을 객체 내부로 감춰 캡슐화

더 개선할 수 있다

  • TicketOffice의 자율성을 높이기 위해서는 Audience의 결합도를 증가시켜야 한다.
    1. 어떤 기능을 설계하는 방법은 1가지 이상일 수 있다.
    2. 설계는 트레이드 오프의 산물이다.

그래, 거짓말이다!

  • 의인화 (anthropomorphism)
    • 능동적이고 자율적인 존재로 소프트웨어 객체를 설계하는 원칙
  • 훌륭한 객체지향 설계
    • 소프트웨어를 구성하는 모든 객체들이 자율적으로 행동하는 설계

04. 객체지향 설계

설계가 왜 필요한가

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

  • 설계는 코드를 작성하는 매 순간 코드를 어떻게 배치할 것인지를 결정하는 과정에서 나온다.
  • 좋은 설계
    • 두 가지 요구사항을 만족시키는 설계
      1. 오늘 완성해야하는 기능을 구현하는 코드
      2. 동시에 내일 쉽게 변경할 수 있는 코드
    • 변경을 수용할 수 있는 설계가 중요한 이유; 코드 변경 시 버그 추가 가능성이 높기 때문
      • 코드를 수정하지 않는다면 버그는 발생하지 않는다.

객체지향 설계

  • 진정한 목표; 변경에 유연하게 대응하는 코드
    • 변경가능한 코드; 이해하기 쉬운 코드
  • 객체지향 프로그래밍; 의존성 효율적 통제를 위한 다양한 방법 제공
    • 요구사항 변경에 더 수월한 대응 가능성 제공.
    • 애플리케이션의 기능은 객체들 간의 상호작용을 통해 구현된다.
      • 객체 간 협력 과정에서 다른 객체에 의존하게 된다.
  • 훌륭한 객체지향 설계; 협력하는 객체 사이의 의존성을 적절하게 관리하는 설계
    • 객체들 사이의 의존성을 적저하게 조절함으로써 변경에 용이한 설계를 만드는 것

Reference

  • 오브젝트 | 조영호
profile
Good Luck!

0개의 댓글