설계

[오브젝트] 객체 지향 설계

mint* 2024. 12. 1. 19:36
728x90

절차지향 vs 객체지향

절차지향

  • 프로세스와 데이터를 별도의 모듈에 위치시키는 방식
    • 모든 처리가 하나의 클래스 안에 위치하고 나머지 클래스는 단지 데이터의 역할만 수행
public class Theater {
    public void enter(Audience audience, TicketSeller seller) {
        if (audience.getBag().hasInvitation()) { // 초대장이 있으면
            Ticket ticket = seller.getTicketOffice().getTicket();
            audience.getBag().setTicket(ticket); // Theater가 Audience의 가방을 직접 확인
        } else { // 초대장이 없으면 구매
            Ticket ticket = seller.getTicketOffice().getTicket();
            audience.getBag().minusAmount(ticket.getFee());
            seller.getTicketOffice().plusAmount(ticket.getFee());
            audience.getBag().setTicket(ticket);
        }
    }
}

// 단순 데이터 보관
class Audience {
    private Bag bag;
    public Bag getBag() { return bag; }
}

// 단순 데이터 보관
class TicketSeller {
    private TicketOffice ticketOffice;
    public TicketOffice getTicketOffice() { return ticketOffice; }
}

 

문제점

  • Theater가 TicketSeller, TicketOffice, Audience, Bag 모두에 의존하고 있다.
  • 자율적으로 객체가 행동한다 는 직관에 위배된다.
  • 데이터의 변경으로 인한 영향을 지역적으로 고립시킬 수 없다.
    • Audience와 TicketSeller의 외부 구현을 변경하려면 Theater의 enter 메서드를 함께 변경해야한다.
    • 변경하기 어려운 코드를 양산한다.

 

객체지향 프로그래밍

  • 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍하는 방식
    • 자신의 데이터를 스스로 처리하도록 한다.
    • 데이터를 사용하는 프로세스가 데이터를 소유하고 있는 클래스 내부로 옮겨진다.
  • 책임을 이동한다.
    • 하나의 변경으로 인한 여파가 다른 클래스로 전파되는 것을 억제한다.
    • 변경하기 수월하다.

코드

/** 극장. */
public class Theater {
  private TicketSeller ticketSeller;

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

  /**
   * 관람객을 맞이한다.
   *
   * @param audience 관람객
   */
  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) {
    ticketOffice.sellTicketTo(audience);
  }
}
/** 매표소. */
public class TicketOffice {
  /** 매표소에서 판매한 모든 티켓의 금액. */
  private Long amount;
  /** 매표소에서 판매하거나 초대장과 교환해 줄 티켓의 목록. */
  private List<Ticket> tickets = new ArrayList<>();

  public TicketOffice(Long amount, List<Ticket> tickets) {
    this.amount = amount;
    this.tickets = tickets;
  }

  /** 판매할 티켓을 꺼내준다. */
  private Ticket getTicket() {
    // 편의상 티켓 컬렉션에서 첫 번째 위치에 저장된 티켓을 리턴한다.
    return tickets.remove(0);
  }

  /** 판매 금액을 차감한다. */
  private void minusAmount(Long amount) {
    this.amount -= amount;
  }

  /** 판매 금액을 더한다. */
  private void plusAmount(Long amount) {
    this.amount += amount;
  }

  public void sellTicketTo(Audience audience) {
    plusAmount(audience.buy(getTicket()));
  }
}
/** 관람객. */
public class Audience {
  private Bag bag;

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

  public Long buy(Ticket ticket) {
    return bag.hold(ticket);
  }
}
/** 관람객이 소지품을 보관할 가방. */
public class Bag {
  @Getter
  private Long amount;
  private Invitation invitation;
  private Ticket ticket;

  /**
   * 이벤트에 당첨되지 않은 관람객. 초대장이 없다.
   *
   * @param amount 현금
   */
  public Bag(Long amount) {
    this.amount = amount;
  }

  /**
   * 이벤트에 당첨된 관람객. 현금과 초대장이 있다.
   *
   * @param amount     현금
   * @param invitation 초대장
   */
  public Bag(Long amount, Invitation invitation) {
    this.amount = amount;
    this.invitation = invitation;
  }

  /** 관람객이 초대장을 갖고 있다면 true 를 리턴한다. */
  private boolean hasInvitation() {
    return invitation != null;
  }

  /** 관람객이 티켓을 갖고 있다면 true 를 리턴한다. */
  public boolean hasTicket() {
    return ticket != null;
  }

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

  /** 현금을 감소시킨다. */
  private void minusAmount(Long amount) {
    this.amount -= amount;
  }

  /** 현금을 증가시킨다. */
  private void plusAmount(Long amount) {
    this.amount += amount;
  }

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

 

결합도

  • 객체 사이의 의존성이 강한 것
  • 설계를 어렵게 만드는 것은 의존성이다.
    • 불필요한 의존성을 제거함으로써 객체 사이의 결합도를 낮춘다.
  • 결합도를 낮추기 위해 선택한 방법은 Theater가 몰라도 되는 세부사항을 Audience와 TicketSeller 내부로 감춰 캡슐화하는 것이다.
    • 불필요한 세부사항을 객체 내부로 캡슐화하는 것은 객체의 자율성을 높이고 응집도 높은 객체들의 공동체를 창조한다.

 

객체지향 설계의 핵심 : 데이터를 가진 객체가 행동을 결정한다

분리 전 (TicketSeller 클래스)

public void sellTo(Audience audience) {
    if (audience.getBag().hasInvitation()) { // audience의 bag을 들춰서 초대장을 확인
        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);
    }
}

문제점

  1. TicketSeller가 Audience의 내부 구조(Bag)를 너무 자세히 알고 있음
  2. Audience의 캡슐화가 깨짐 (내부 상태에 직접 접근)
  3. 티켓 판매와 관련된 모든 책임이 TicketSeller에 집중됨

 

분리 후

// TicketSeller 클래스
public void sellTo(Audience audience) {
    ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
}

// Audience 클래스
public Long buy(Ticket ticket) {
    if (bag.hasInvitation()) {
        bag.setTicket(ticket);
        return 0L;
    } else {
        bag.setTicket(ticket);
        bag.minusAmount(ticket.getFee());
        return ticket.getFee();
    }
}
  • Audience가 자신의 Bag을 직접 관리
  • TicketSeller는 단순히 티켓을 판매하고 금액을 받는 역할만 수행한다.
  • TicketSeller가 Audience의 내부 구조를 알 필요가 없어짐
    • 객체 간 의존성이 줄어듦

 

객체지향적 메서드 분리

1. 작업 단위로 분리

  • 잘못된 방식: 흐름(if/else) 기준으로 메서드 분리
public class TicketSeller {
    private TicketOffice ticketOffice;

    // 흐름(if/else)에 따라 메서드를 분리 - 좋지 않은 방식
    public void sellTo(Audience audience) {
        if (audience.getBag().hasInvitation()) {
            processWithInvitation(audience);
        } else {
            processWithoutInvitation(audience);
        }
    }

    private void processWithInvitation(Audience audience) {
        Ticket ticket = ticketOffice.getTicket();
        audience.getBag().setTicket(ticket);
    }

    private void processWithoutInvitation(Audience audience) {
        Ticket ticket = ticketOffice.getTicket();
        audience.getBag().minusAmount(ticket.getFee());
        ticketOffice.plusAmount(ticket.getFee());
        audience.getBag().setTicket(ticket);
    }
}

public class Audience {
    private Bag bag;

    public Bag getBag() {
        return bag;
    }
}

하나의 완결된 작업 단위로 분리 ⭕

public class TicketSeller {
    private TicketOffice ticketOffice;

    // 티켓 판매라는 하나의 완결된 작업
    public void sellTo(Audience audience) {
        ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
    }
}

public class Audience {
    private Bag bag;

    // 티켓 구매라는 하나의 완결된 작업
    public Long buy(Ticket ticket) {
        return bag.hold(ticket);
    }
}

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

    // 티켓 보관이라는 하나의 완결된 작업
    public Long hold(Ticket ticket) {
        if (hasInvitation()) {
            setTicket(ticket);
            return 0L;
        } else {
            setTicket(ticket);
            minusAmount(ticket.getFee());
            return ticket.getFee();
        }
    }

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

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

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

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

    // 티켓 발행이라는 하나의 완결된 작업
    public Ticket getTicket() {
        return tickets.remove(0);
    }

    public void plusAmount(Long amount) {
        this.amount += amount;
    }
}
  • 데이터를 외부에서 조작하는 것이 아닌, 객체 스스로 책임지고 처리한다.
    • 데이터를 가진 객체가 관련 행동도 함께 수행해야 한다.
      • 코드의 응집도가 높아진다.
      • 변경이 필요할때 관련 객체만 수정하면 된다.
      • 다른 객체에 미치는 영향이 최소화된다.
  • 메서드를 분리할 때는 "이 객체가 무슨 작업을 해야 하는가?" 를 기준으로 삼아야 한다.
    • 단순히 프로그램의 흐름(if/else)을 기준으로 분리하면 안된다.

 

Reference

https://wikibook.co.kr/object/

 

오브젝트: 코드로 이해하는 객체지향 설계

역할, 책임, 협력을 향해 객체지향적으로 프로그래밍하라! 객체지향으로 향하는 첫걸음은 클래스가 아니라 객체를 바라보는 것에서부터 시작한다. 객체지향으로 향하는 두 번째 걸음은 객체를

wikibook.co.kr

 

 

https://github.com/johngrib/study-objects

 

GitHub - johngrib/study-objects: 조영호 님의 책 [오브젝트]를 학습하는 저장소.

조영호 님의 책 [오브젝트]를 학습하는 저장소. Contribute to johngrib/study-objects development by creating an account on GitHub.

github.com

 

728x90