오브젝트 1장

변은서·2022년 12월 10일
0

오브젝트

목록 보기
1/3

SW마에스트로 13기 spring 스터디(S3)에서 오브젝트 책을 읽고 블로그를 작성하기로 했다.

1장을 읽고 정리해보고자 한다.

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

작은 소극장을 경영하고 있다고 상상해보자. 고객에게 무료 공연 관람할 수 있는 초대장을 보내는 이벤트를 진행하고자 한다. 초대장을 가지고 있는 관람객은 티켓으로 교환 후 입장할 수 있고 일반 관람객은 티켓을 구매해야만 입장할 수 있다.

먼저, 당첨자에게 발송되는 초대장을 구현해보자. 관람 날짜 when이 포함된 클래스다.

Invitation.java

public class Invitation{
	private LocalDateTime when;
}

공연을 관람하기 위한 Ticket 클래스가 필요하다.

Ticket.java

public class Ticket{
	private Long fee;
    
    public Long getFee(){
    	return fee;
    }
}

이제 관람객이 가지고 있는 Bag 클래스가 필요하다. 초대장, 티켓, 현금을 인스턴스 변수로 포함한다.

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 -= amount;
    }
    
    public void plusAmount(Long amount){
    	this.amount += amount;
    }
}

관람객 Audience 클래스가 필요하다. 관람객은 가방을 가지고 있을 수 있다.

Audience.java

public class Audience{
	private Bag bag;
    
    public  Audience(Bag bag){
		this.bag = bag;
    }
    
    public Bag getBag(){
    	return bag;
    }
}

매표소 TicketOffice 클래스가 필요하다.

TicketOffice.java

public class TicketOffice {
	private Long amount;
    private List<Ticket> tickets = new ArrayList<>();
    
    public TicketOffice(Long amount, Ticket ... tickets){
    	this.amount = amount;
        this.tickets.remove(0);
    }
    
    public void minusAmount(Long amount){
    	this.amount -= amount;
    }
    
    public void plusAmount(Long amount){
    	this.amount += amount;
    }
}

티켓 판매원 TicketSeller 클래스는 자신이 일하고 있는 매표소를 알고 있어야 한다.

TicketSeller.java

public class TicketSeller {
	private TicketOffice ticketOffice;
    
    public TicketSeller(TicketOffice ticketOffice){
    	this.ticketOffice = ticketOffice;
    }
    
    public TicketOffice getTicket(){
    	return ticketOffice;
    }
}


핵심 클래스를 보면 위와 같다.

마지막으로 극장을 완성해보자.

Theater.java

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. 무엇이 문제인가

로버트 마틴은 <<클린 소프트웨어>>에서 소프트웨어 모듈이 가져야 하는 세 가지 기능에 관해 설명한다.

첫째, 모듈은 실행 중에 제대로 동작한다.
둘째, 모듈은 변경을 위해 존재하는 것이다.
셋째, 모듈은 코드를 읽는 사람과 의사소통 하기 위함이다.

마틴에 따르면 모든 모듈은 제대로 실행되어야 하고 변경 용이해야 하며 이해하기 쉬워야 한다.

Theater 클래스의 enter가 하는 일을 보면

소극장은 관람객의 가방을 열어 초대장이 있는지 살펴본다 ...

관람객과 판매원이 소극장의 통제를 받는 수동적인 존재가 된다. 현실에서는 관람객이 직접 자신의 가방에서 돈을 직접 꺼내 판매원에게 건넨다. 판매원은 매표소에 있는 티켓을 직접 꺼내 관람객에게 건네고 관람객에서 직접 돈을 받아 매표소에 보관한다. 코드 속 관람객은 그렇게 하지 않는 것이 문제가 된다.

또,Audience가 Bag을 가지고 있고 TicketSeller가 TicketOffice에서 티켓을 판매하고 ... 를 기억해야 한다. 이는 코드를 읽고 이해해야 하는 사람에게 큰 부담을 준다

가장 심각한 문제는 Audience와 TicketSeller을 변경할 경우 Theater도 함께 변경해야 한다는 점이다.

관람객이 가방을 들고 있다는 가정이 바뀌었다고 생각했을 때, 여러 클래스를 함께 바꾸어야 한다. 이는 의존성관련 문제이다. 애플리케이션의 기능을 구현하는 데 필요한 최소한의 의존성만 유지하고 불필요한 의존성을 제거하는 것이 좋다.

객체 사이의 의존성이 과한 경우를 가리켜 결합도(coupling) 가 높다고 말한다. 두 객체 사이 결합도가 높으면 높을수록 변경이 어려워져 결합도를 낮춰 변경이 용이한 설계를 만들어야 한다.

03. 설계 개선하기

Theater가 Audience와 TicketSeller에 관해 너무 세세한 부분까지 알지 못하도록 정보를 차단하자. 관람객과 판매원을 자율적인 존재로 만드는 작업이다.

TicketSeller.java

public class TicketSeller {
    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);
        }
    }
}

TicketSeller에서 getTicketOffice 메서드가 사라졌다. 우리는 직접 ticketOffice에 접근할 수 없고 TicketSeller만이 접근 가능하므로 티켓을 꺼내거나 판매 요금을 적립하는 일을 스스로 수행할 수밖에 없다.

이처럼 개념적이나 물리적으로 객체 내부의 세부적인 사항을 감추는 것을 캡슐화(encapsulation) 라고 한다. 목적은 변경하기 쉬운 객체를 만드는 것이다. 객체 내부로의 접근을 제한하면 객체와 객체 사이의 결합도를 낮출 수 있기 때문에 설계를 좀 더 쉽게 변경할 수 있게 된다.

Theater.java

public class Theater {
    private TicketSeller ticketSeller;

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

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

enter 메서드는 sellTo 메서드를 호출하는 간단한 코드로 바뀌었다. Theater은 이제 어디에서도 ticketOffice에 접근하지 않는다. 이는 TicketSeller의 인터페이스에만 의존한다. TicketSeller가 내부에 TicketOffice 인스턴스를 포함하고 있다는 사실은 구현의 영역에 속한다. 객체를 인터페이스와 구현으로 나누고 인터페이스만을 공개하는 것은 결합도를 낮추고 변경하기 쉬운 코드를 작성하기 위해 따라야 하는 가장 기본적인 설계 원칙이다.

Audience.java

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

TicketSeller.java

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에서 getBag 메서드를 제거하고 TicketSeller가 접근하지 못하도록 한다. Audience는 자신의 가방 안에 초대장이 들어있는지 스스로 확인하고 외부인이 자신의 가방을 열어보도록 허용하지 않는다.

무엇이 개선됐는가

이제 의사소통의 문제는 개선되었다. 또 Audience나 TicketSeller의 내부 구현을 변경하더라도 Theater을 함께 변경할 필요가 없어졌다. 변경 용이성의 측면에서도 확실히 개선되었다

어떻게 한 것인가

자기 자신의 문제를 스스로 해결하도록 변경한 것이다. 자율성을 높이는 방향으로. 그 결과 이해하기 쉽고 유연한 설계를 얻을 수 있었다.

캡슐화와 응집도

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

절차지향과 객체지향

수정하기 전에는 관람객을 입장시키는 절차를 구현했다. 이는 절차지향 프로그래밍 방식으로 작성된 코드의 전형적인 의존성 구조를 보여주었다. 우리의 직관에 위배되며 코드를 읽기 어렵고 변경하기 어려워진다. 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍하는 방식을 객체지향 프로그래밍이라고 부른다. 훌륭한 객체지향 설계의 핵심은 캡슐화를 이용해 의존성을 적절히 관리함으로써 객체 간의 결합도를 낮추는 것이다. 객체지향 코드는 코드를 이해하기 쉽고 변경하기 수월하다.

책임의 이동

수정하기 전에는 책임이 Theater에 집중되어있었다. 수정 후에는 책임이 각 객체에 적절히 분산되어 있음을 알 수 있다. 책임의 이동이 이뤄낸 것이다. 객체지향 설계의 핵심은 적절한 객체에 적절한 책임을 할당하는 것이다.

더 개선할 수 있다

코드에서 Bag은 여전히 Audience에 의존된다. 이를 자율적인 존재로 바꾸는 작업을 해보자.

Bag.java

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

	//여기
    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;
    }
}

Audience.java

public class Audience {
    public Long buy(Ticket ticket) {
        return bag.hold(ticket);
    }
}

TicketOffice.java

public class TicketOffice {
	
    public void sellTicketTo(Audience audience) {
        plusAmount(audience.buy(getTicket()));
    }

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

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

TicketSeller.java

public class TicketSeller {
    public void sellTo(Audience audience) {
        ticketOffice.sellTicketTo(audience);
    }
}

TicketOffice와 Audience 사이에 의존성이 추가되었다. 더 나은 방안을 찾지 못하고 Audience와 Bag 사이의 의존성과 트레이드오프 시점이 온 것이다. 고민 끝에 개발팀은 TicketOffice의 자율성보다 Audience에 대한 결합도를 낮추는 것이 더 중요하다는 결론에 도달했다.

어떤 기능을 설계하는 방법은 한가지 이상일 수 있다.
동일한 기능을 여러 방법으로 설계할 수 있기 때문에 설계는 트레이드오프의 산물이다.

어떤 경우에도 모두를 만족시키는 설계를 만들 수는 없다. 설계는 균형의 예술이다. 훌륭한 설계는 적절한 트레이드오프의 결과물이다.

그래, 거짓말이다!

레베카 워프스록은 능동적이고 자율적인 존재로 소프트웨어 객체를 설계하는 원칙을 가리켜 의인화라고 부른다.

훌륭한 객체지향 설계란 소프트웨어를 구성하는 모든 객체들이 자율적으로 행동하는 설계를 가리킨다.

04. 객체지향 설계

설계가 왜 필요한가

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

설계는 코드를 어떻게 배치할 것인지를 결정하는 과정에서 나온다. 앞 예시도 수정 전과 수정 후 실행 결과는 같으나 코드를 배치하는 방법은 완전히 다르다. 즉, 서로 다른 설계를 가진 것이다. 그렇다면 좋은 설계는 무엇인가? 오늘 요구하는 기능을 온전히 수행하면서 내일의 변경을 매끄럽게 수용할 수 있는 설계다.

변경을 수용할 수 있는 설계가 중요한 또 다른 이유는 요구사항이 항상 변경되기 때문이다. 또, 코드를 변경할 때 버그가 추가될 가능성이 높기 때문이다. 버그는 코드 수정 의지를 꺾는다. 요구사항 변경으로 인해 버그를 추가할지도 모른다는 두려움에 코드 수정을 회피하고자 한다.

객체지향 설계

훌륭한 객체지향 설계란 협력하는 객체 사이의 의존성을 적절하게 관리하는 설계다.

정리

의존성, 결합도는 낮게 응집도는 높게 설계하자.
설계는 트레이드오프를 얼마나 잘하느냐에 따라 달렸다.

profile
은또

0개의 댓글