Code/OOP

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

noahkim_ 2025. 4. 2. 01:51

조영호 님의 "오브젝트" 책을 정리한 글입니다.

 

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

관람객

구분 입장 조건 필요한 절차
이벤트 당첨자 초대장을 티켓으로 교환한 후 입장
초대장 → 티켓 교환 → 입장
이벤트 미당첨자 티켓을 구매한 후 입장 티켓 구매 → 입장

 

극장

  • 관람객 입장 담당 (이벤트 당첨자/미당첨자에 따른 입장)

 

더보기

코드

public class Invitation {
    private LocalDateTime when;
}
public class Ticket {
    private Long fee;

    public Long getFee() { return fee; }
}
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 hasTicket() { return ticket != null; }
    public void setTicket(Ticket ticket) { this.ticket = ticket; }
    public void minusAmount(Long fee) { this.amount -= amount; }
    public void plusAmount(Long fee) { this.amount += amount; }
}
public class Audience {
    // 관람객: 가방 소지
    private Bag bag;

    public Audience(Bag bag) { this.bag = bag; }
    
    public Audience getBag() { return bag; }
}
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;
    }

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

 

2. 무엇이 문제인가

모듈이 가져야 할 세 가지 기능

  • 로버트 마틴은 <클린 소프트웨어>에서 소프트웨어 모듈이 가져야 할 세 가지 기능에 대해 설명함
  1. 실행중에 제대로 동작할 것
  2. 변경에 용이해야 함
  3. 이해하기 쉬워야 함

 

위 코드는 첫번째만 만족함

즉, 두번째, 세번째 기능은 만족시키지 못함

 

예상을 빗나가는 코드

Theater의 enter()
  • 소극장이 초대장 여부를 파악 (관람객의 가방을 직접 열음)
  • 판매원이 티켓 제공 (관람객의 가방에 직접 넣음)
  • ⚠️  관람객과 판매원이 모두 수동적으로 동작함

 

단점
개념 설명
이해하기 어려움 직관적으로 이해하기 어려움 
- 상식과 다르게 동작함
- 관람객이 스스로 자신의 일을 처리함 ↔️ 소극장이 관람객의 가방을 직접 엶
세부 내용 필요
구체적인 정보를 알아야 이해 가능
- TicketOffice에 돈과 티켓이 보관되어 있음
- Audience는 Bag을 가지고 있음

 

변경에 취약한 코드

Audience와 TicketSeller를 변경할 경우, TicketOffice도 변경해야 함
  • 특정 객체와의 협력을 가정하기 때문
    • Audience는 Money와 Invitation을 보관하기 위해 항상 Bag을 가지고 다닌다 가정
    • TicketSeller는 TicketOffice에서만 Ticket을 판매한다 가정

 

의존성과 결합도
개념 설명 예시
의존성 어떤 객체가 다른 객체에 기능적으로 의존하는 관계
A 객체가 B 객체의 메서드를 호출함
결합도 객체들 간 의존성의 정도
강한 결합: 직접 인스턴스 생성
약한 결합: 인터페이스 사용 및 주입

 

3. 설계 개선하기

  • Theater가 Audience의 Bag과 TicketSeller에 직접 접근한다는 것은 결합됨을 의미
  • Theater가 Audience와 TicketSeller에 관해 너무 세세한 부분까지 알지 못하도록 정보를 차단하기
    • Audience가 스스로 Bag안의 Money와 Invitation을 처리하기
    • TicketSeller가 스스로 TicketOffice 작업을 처리하기

 

자율성을 높이자

  • 설계를 변경하기 어려운 이유는 Theater가 Audience와 TicketSeller뿐만 아니라 각각의 의존성까지 접근할 수 있기 때문
더보기

코드

public class Audience {
    // 관람객: 가방 소지
    private Bag bag;

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

    // getBag() 삭제
    
    // buy() 추가: 스스로 티켓을 관리
    public Long buy(Ticket ticket) {
        if (bag.hasInvitation()) {        
            bag.setTicket(ticket);
            return 0L;
        } else {
            bag.setTicket(ticket);
            bag.setMinusAmount(ticket.getFee());
            return ticket.getFee();
        }
    }
}
public class TicketSeller {
    // 판매원: 매표소에서 티켓을 교환 및 판매 담당
    private TicketOffice ticketOffice;

    public TicketSeller(TicketOffice ticketOffice) { this.ticketOffice = ticketOffice; }
    
    // getTicketOffice() 삭제

    // audience에서 티켓 구매 요청 
    public void sellTo(Audience audience) {	
        ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
    }
}
public class Theater {
    private TicketSeller ticketSeller;

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

    public void enter(Audience audience) { ticketSeller.sellTo(audience); }
}
객체 캡슐화 인터페이스 분리
TicketSeller getTicketOffice() 삭제 Theater는 오직 TicketSeller의 인터페이스에만 의존
Audience getBag() 삭제 TicketSeller는 Audience의 인터페이스에만 의존

 

무엇이 개선됐는가

개념 설명 목적 효과
캡슐화 내부 구현을 감추기 결합도 감소
가독성 향상
유지보수 용이
(내부 변경이 외부에 영향 X)
인터페이스 분리 인터페이스만 공개하고, 구현은 감춤 결합도 감소
변경 용이성 증가
구현체 변경 시 외부 영향 최소화

 

어떻게 한 것인가

  • 캡슐화: 자기 자신이 할일을 스스로 하도록 변경

 

캡슐화와 응집도

응집도
  • 밀접하게 연관된 작업을 하나의 모듈에서 수행하도록 하는 정도
응집도를 높이는 방법 설명
자신의 데이터를 스스로 책임짐 객체 내부의 상태와 행동이 일관되도록 유지
외부 데이터 의존 최소화 외부 객체나 시스템의 변화에 영향을 받지 않도록 설계
메시지를 통한 협력 직접 접근이 아닌 메시지를 주고받으며 협력

 

절차지향과 객체지향

구분 절차지향 객체지향
개념 프로세스와 데이터를 별도의 모듈에 위치 프로세스와 데이터를 동일한 모듈 내에 위치
특징 순차적인 흐름에 따라 실행됨 객체 간 메시지를 주고받으며 동작
캡슐화 없음 (데이터와 로직이 분리됨) 있음 (데이터를 객체 내부에 숨김)
재사용성 낮음 (코드를 복사하여 사용) 높음 (객체 단위로 재사용 가능)
유지보수성 변경 시 전체 코드 수정 가능성이 큼 변경이 용이 (객체 단위 수정 가능)
확장성 낮음 (새로운 기능 추가가 어렵고 복잡) 높음 (객체를 추가하거나 확장하기 용이)
장점 직관적이고 실행 속도가 빠름 유지보수성과 확장성이 뛰어남
단점 코드 중복이 많고 유지보수가 어려움 상대적으로 실행 속도가 느릴 수 있음

 

책임의 이동

  • 두 방식의 근본적인 차이를 만드는 것은 책임의 이동임
  • 절차 지향은 책임이 한쪽에 몰려있음 ↔️ 객체 지향은 각 객체에 적절히 분산되어 있음

 

더 개선할 수 있다

Bag를 자율적인 존재로 변경하기
  • 현재 Bag은 과거의 Audience처럼 수동적으로 동작함
더보기
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 {
    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 void setTicket(Ticket ticket) { this.ticket = ticket; }
    public void minusAmount(Long fee) { this.amount -= amount; }
    public Long hold(Ticket ticket) {
        setTicket(ticket);

        if (hasInvitation()) {
            return 0L;
        } else {
            minusAmount(ticket.getFee());
            return ticket.getFee();
        }
    }
}

 

 

TicketOffice가 자율적으로 처리하도록 하기
  • TicketOffice는 수동적으로 사용됨 (TicketSeller에 의해 내부 작업이 이루어짐)
더보기
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;
    }

    public Ticket getTicket() { return tickets.remove(0); }
    public void minusAmount(Long amount) { this.amount -= amount; }
    public void plusAmount(Long amount) { this.amount += amount; }   
    public void sellTicketTo(Audience audience) { plusAmount(audience.buy(getTicket())); }    
}
public class TicketSeller {
    // 판매원: 매표소에서 티켓을 교환 및 판매 담당
    private TicketOffice ticketOffice;

    public TicketSeller(TicketOffice ticketOffice) { this.ticketOffice = ticketOffice; }
    
    // ticketOffice에서 티켓 구매 요청 
    public void sellTo(Audience audience) { ticketOffice.sellTicketTo(audience); }
}

 

그래, 거짓말이다!

  • 실세계의 생물처럼 스스로 생각하고 행동하도록 이해하기 쉽게 짜는 것
  • 더 나아가 그 대상이 실세계에서 생명이 없는 수동적인 존재라 하더라도 객체지향의 세계에서는 생명을 가진 존재로 다시 태어남

 

4. 객체지향 설계

설계가 왜 필요한가

  • 설계는 코드를 배치하는 것

 

좋은 설계가 필요한 이유
  • 변경 용이성을 확보할 수 있음
    • 항상 요구사항이 변경되기 때문에 코드 변경은 필연적임
    • 변경 용이성을 확보하면 버그 발생 가능성을 줄일 수 있음 (코드 변경 시 버그가 추가될 가능성이 높기 때문)