Skip to content

변경이 쉬운 소프트웨어를 개발하는 방법 - 불필요한 의존성을 없애자

잘 만든 소프트웨어의 조건

로버트 마틴 은 <<클린 소프트웨어: 애자일 원칙과 패턴, 그리고 실천 방법>>에서 소프트웨어 모듈이 가져야 하는 세 가지 기능에 관하여 설명한다. 여기서 모듈이란 클래스, 패키지, 라이브러리 같이 프로그램을 구성하는 요소를 의미한다.

1. 실행 중 제대로 동작해야 한다.

2. 변경을 위해 존재해야 한다.

3. 코드를 읽는 사람과 의사소통 하는 것이다.

위의 3가지의 내용의 의미를 필자는 3가지 기능을 아래와 같은 의미로 이해하였다.

1. 원하는 데로 동작해야 하고,

2. 쉬운 유지보수 및 기능 확장이 가능해야 함.

3. 프로그램이 문제해결 의도가 들어나도록 작성해야 한다.

지금부터 위의 3가지 원칙 중 2, 3번이 위배된 예시를 보면서 어떻게 해야 유지보수가 쉽고 확장이 가능하게 변경하는지 살펴 보겠다.

티켓 판매 시스템

<<오브젝트>>에서는 티켓 판매 시스템이라는 간단한 예제를 통해 잘짜여진 소프트웨어가 무엇인지 알려준다. 우리가 해결해야하는 문제는 아래와 같다.

티켓 판매 시스템 흐름

  1. 소극장관람객을 맞이한다.
  2. 관람객티켓을 통해서 공연를 관람할 수 있다.
  3. 티켓 판매자관람객초대권을 가지고 있으면 티켓을 무료로 증정해준다.
  4. 초대권을 가지고 있지 않으면 관람객티켓구입해야 한다.

초대장

관람객에 보낼 초대장을 아래와 같이 구현한다. 관람 가능한 초대 일자를 변수로 받자.

Invitation.java
public class Invitation {
    private LocalDateTime when;
}

티켓

공연을 관람하기 위해선 관람객 모두는 티켓이 필요한다. 티켓 마다 가격이 있기 때문에 티켓이 가격을 알려주도록 설계하면 아래와 같다.

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

가방

관람객은 초대권, 티켓, 티켓을 구매할 돈이 존재할 것이다. 이를 하나의 객체로 관리하기 위해 가방 객체를 설계한다. 가방객체에서 초대권이 있는지 확인하는 hasInvitation(), 돈을 지불하고 가져갈 수 있는 plusAmount(), minusAmount(), 티켓을 설정할 수 있는 setTicket() 및 티켓을 가지고 있는지 확인하기 위한 hasTicket()이 있다.

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

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

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

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

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

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

관람객

관람객은 소지품을 보관하기 위해 가방을 들고 다니도록 설계한다.

Auidience.java
public class Audience {
    private Bag bag;

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

    public Bag getBag() {
        return bag;
    }
}

티켓 오피스

티켓을 관리하기 위한 티켓 오피스를 만들자. 티켓 판매자가 티켓을 판매한 후 얻은 금액을 티켓 오피스에 저장하도록 설계한다.

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

    public TicketOffice(Long amount, Ticket ... tickets) {
        this.amount = amount;
        this.ticket.addAll(Arrays.atList(tickets))
    }

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

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

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

티켓 판매자

티켓 판매자는 티켓 오피스를 통해 티켓을 판매하도록 설계한다. 아래의 예제는 티켓 판매소를 소개시켜주는 형태로 구성되어 있다.

TicketSeller.java
public class TicketSeller {
    private TicketOffice ticketOffice;

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

    public TicketOffice getTicketOffice() {
        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);
        }
    }
}

이로서 구현을 완료되었으나, 아래와 같은 문제점이 존재하게 된다.

1. 소극장 안에서 입장이 이루어질 때, 티켓 판매자와 관람객이 수동적으로 티켓을 구매하고 판매하게 된다. 실제로 티켓판매자와 관람객이 적극적으로 티켓을 구입하고 있지 않음
    - 소극장이 가방을 열고 초대권이 있는지 확인하고 있음 
    - 티켓 판매자를 통해 티켓 오피스를 불러와 티켓을 구매하는 단계를 직접 수행하고 있음 (티켓 판매자가 하는 일은 티켓 오피스를 소개시켜주는 일 뿐임)

2. 소극장이 알고 있어야 하는 클래스가 너무 많다. 즉 소극장이 의존하는 클래스가 많다.
    - audience, ticketSeller, ticketOffice, Ticket, Bag

클래스 다이어그램을 그리게되면 아래의 그림처럼 소극장은 enter 메소드를 통해 각 객체를 의존하는 정도가 많은 것을 확인할 수 있다.

Image title

그림 1 - 티켓 판매 시스템 클래스 다이어그램 1

의존도가 높은 코드는 코드를 수정하기 어렵게 만든다. 예로 들어 관람객이 가방이 들고 있지 않아도 들어갈 수 있도록 수정하거나, 신용카드로 지불할 수 있도록 변경한다던가, 티켓 오피스가 아닌 티켓 판매자가 직접 판매하도록 변경하는 등 다른 객체가 변경되었을 때, 소극장도 수정해야 하는 문제점이 발생하게 된다.

그렇다면 어떻게 하면 불필요한 의존성을 제거할 수 있는지 알아보자

의존성을 제거하는 방법 : 책임을 옮기기

티켓 판매자가 티켓을 판매하도록 분리하기

소극장은 티켓을 판매할 수 있도록 티켓 판매자를 통해 직접 티켓을 판매하였다. 티켓 판매의 책임은 티켓 판매자에 있음으로 티켓 판매의 책임을 전가하여 불필요한 의존도를 제거해보자.

아래의 코드는 티캣 판매자가 sellTo() 메소드를 통해 티켓 판매의 책임을 가져간 예제이다.

책임 옮기기 - 1
public class Theater {
    private TicketSeller ticketSeller;

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

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

public class TicketSeller {
    private TicketOffice ticketOffice;

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

    public TicketOffice getTicketOffice() {
        return 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);
        }        
    }
}

책임을 가져갔기 때문에 불필요한 의존성을 제거할 수 있었고 소극장이 훨씬 변경할 수 있도록 수정되었다. 또한 티켓 판매자가 sellTo를 통해 좀 더 적극적인 주체로써 활동한다.

작가가 말하는 의존성을 분리하는 방법이란 정리하자면 아래와 같다.

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

이를 클래스 다이어그램으로 다시 그리자면 아래와 같이 티켓 판매자의 책임이 좀 더 늘어나게 되고 소극장의 의존도가 일부 없어짐을 알 수 있다.

Image title

그림 2 - 티켓 판매 시스템 클래스 다이어그램 2

그러나, 관람객은 여전히 티켓 판매자를 통해 가방을 수동적으로 열어 초대권이 있는지 확인하고 있으며, 지불도 여전히 티켓 판매자에 의해 소극적으로 수행되어지고 있다. 이를 변경해보자.

관람객이 티켓을 구매하도록 분리하기

이번엔 관람객이 티켓을 구매하도록 buy() 메소드를 추가한다. buy() 메소드로 티켓의 구입을 관람객에게 책임을 옮김으로써 티켓 판매자는 더 이상 불필요한 의존성인 가방에 직접 접근하지 않아도 된다.

책임 옮기기 - 2
public class TicketSeller {
    private TicketOffice ticketOffice;

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

    public TicketOffice getTicketOffice() {
        return ticketOffice;
    }

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


public class Audience {
    private Bag bag;

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

    public Bag getBag() {
        return 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();
        }
    }
}

수정한 결과를 클래스 다이어그램을 통해 그려보면 아래와 같이 가방에 대한 접근이 없어졌지만, Audience에서 Ticket에 대한 의존성이 추가되었다. 아래의 빨간색 의존성(Ticket에대한 buy, sell)은 필요한지 불필요한지 확인하는 방법은 다음 시간에 확인하자.

Image title

그림 3 - 티켓 판매 시스템 클래스 다이어그램 3


Comments