- 티켓 판매 애플리케이션으로 소프트웨어 설계
- 문제점
- 설계 개선하기
- 절차지향 설계와 객체지향 설계의 차이
- Trade-Off
- 설계의 존재의의
- 객체지향 설계
프로그램을 설계하는 것에 앞서서 로버트 L. 글래스는 다음과 같은 질문을 우리에게 던졌다.
"이론이 먼저일까, 실무가 먼저일까?"
대부분의 사람들은 이론이 먼저 정립된 후 실무가 뒤를 따라서 발전한다고 한다. 그러나 글래스는 반대로, 이론이 정립되지 않은 초기에는 오히려 실무가 먼저 발전하고 나서야 실무에서 나온 정보들을 바탕으로 이론이 정립된다고 말한다.
실제로 다른 다른 공학 분야(ex. 건축, 기계 등)에 비해 상대적으로 역사가 짧은 소프트웨어 공학은 이론보다 실무가 앞서는 분야이다.
오브젝트 - 코드로 이해하는 객체지향 설계 책은 이론보다는 직접 코드를 보고 객체지향 설계에 대해서 설명을 하고 있다.
1. 티켓 판매 애플리케이션으로 소프트웨어 설계
소극장을 운영하는 프로그램을 만든다고 상상해보자. 소극장에서는 이벤트를 열어 추첨을 통해 무료 입장 쿠폰을 발급해준다. 여기서 관람객이 소극장에 입장하는 일련의 과정을 설계한다면??

다음과 같이 작성할 수 있을 것이다. 그럼 하나씩 살펴보자.
A. Invitation 클래스
해당 클래스는 이벤트에 당첨된 관람객에게 발급한 무료쿠폰의 사용가능일자를 변수로 포함한 간단한 클래스다.
B. Ticket 클래스
관람객이 티켓을 소지하고 있어야 한다. 티켓의 금액과 그 금액을 알려주는 메소드로 이루어진 간단한 클래스다.
C. Bag 클래스
관람객이 현재 소지하고 있는 초대장, 티켓, 현금을 인스턴스 변수로 두고있다. 처음에 관람객은 초대장과 초대장이 없으면 구매하기위한 현금을 소지하고 있어야 하므로 생성자를 만들어준다. 그리고 초대장이 있는지 판단하는 메소드, 티켓이 있는지 판단하는 메소드, 초대장이 없으면 티켓을 구매하기 위한 현금 증감 메소드, 마지막으로 초대장을 티켓으로 교환하는 메소드가 있다.
D. Audience 클래스
관람객은 가방을 들고 있어야 하므로 생성자로 가방 인스턴스 변수를 입력하고 가방을 보여주는 메소드를 생성한다.
E. TicketOffice 클래스
매표소에서는 가지고 있는 티켓과 메표소가 가지고 있는 금액을 인스턴스 변수로 표현했다. 그리고 티켓을 판매하는 메소드와 메표소가 가지고 있는 금액을 관리하는 현금 증감 메소드로 클래스를 이루어져있다.
F. TicketSeller 클래스(왼쪽 하단의 Audience 클래스에 오타가 난 것 같다.)
티켓판매원은 매표소에서 티켓을 교환하기 위해 매표소에 들어와 매표소의 일은 진행한다.
그렇게 최종적으로 Theater 클래스에서 관람객을 맞이할 수 있도록 코드를 만든다.
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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);
}
}
}
|
cs |
간단하게 설명하면 티켓판매원을 세워두고(생성자) 관람객이 왔을 경우(enter 메소드) 초대장이 있으면 관람객의 가방에 티켓을 넣어준다. 그리고 초대장이 없을 경우 관람객의 가방에서 티켓의 금액만크 돈을 삭감해서 매표소에 돈을 넣고 티켓을 넣어주는 메소드다.
이렇게 설계된 프로그램은 간단하고 예상대로 동작할 것이다. 그러나 이렇게 설계한 프로그램에는 몇가지 문제가 있다.
2. 문제점

위의 프로그래밍에서는 Theater 클래스가 너무 많은 클래스에 의존하고 있다는 문제점을 가지고 있다.
로버트 마틴의 말에 의하면 소프트웨어 모듈은 3가지 특성을 가져야한다고 말한다.
- 실행 중에 제대로 동작해야 한다.
- 변경이 용이해야 한다.
- 특별한 노력없이 쉽게 읽고 이해할 수 있어야 한다
위에서 보여준 프로그램은 분명 첫번째 특성은 만족한다. 그러나 두번째, 세번째 특성은 만족하지 못한다.
1. 2-1번 이슈(변경의 어려움)
해당 프로그램은 theater 클래스에 모든 기능이 집중되어 있다. 만약 관람객이 가방을 들고있지 않다면? 그리고 관람객이 현금이 아니라 카드를 사용한다면? 그렇다면 코드를 추가,변경하기 위해서는 theater 클래스를 변경해야 하고 더 나아가 Audience 클래스와 그 밖에 다른 클래스까지 변경해야 할 가능성이 생긴다.
이를 보고 우리는 객체 사이의 의존성이 크다고 말하며, 다른말로 결합도가 높다고도 말한다. 결합도가 높을수록 기능을 추가, 변경할 때 함께 변경해야 할 가능성이 커지기 때문에 객체지향 설계를 할 때에는 클래스 사이의 결합도를 낮춰줄 필요가 있다/
2. 3-1번 이슈(현실세계의 동작과 프로그램의 동작의 주체가 다름)
현재 프로그램에서 문제점은 관람객과 판매원이 소극장 클래스의 통제를 받는 수동적인 클래스라는 것이다. theater 클래스를 살펴보면 소극장이 관람객의 가방에서 돈을 빼고 티켓을 넣어주는 동작을 한다. 현실에서 이러한 상황이 발생한다면 상당히 불쾌한 기분을 느낄 것이다.
판매원의 경우에도 마찬가지다. 돈을 적립하고 티켓을 발급하는건 판매원이 아닌 소극장이 진행한다. 현실에서 이러한 상황이 발생한다면 판매원의 역할은 단순 경비원에 불과할 것이다.(판매원의 직업 만족도는 다소 높을 것 같아 보인다.)
현실세계의 동작과 프로그램의 동작의 주체가 다른것이 문제가 되는 이유는 프로그램이 현실에서 이해할 수 없는 행동을 하기 때문이다. 이는 우리의 예상을 벗어나고, 더 나아가 프로그램 개발자가 아닌 다른 개발자가 프로그램을 변경하려 할 때 이해하기 매우 어려워 코드 수정이 대단히 어려울 것이다.
3. 3-2번 이슈(프로그램 안쪽의 여러 세부적인 내용을 알고 있어야 함)
다른 문제로는 모든 동작을 Theater 클래스에서 이뤄지고 있다. 만약 enter 매소드를 이해하기 위해서는 다른 클래스에 있는 메소드의 동작을 전부 이해해야 enter 메소드의 이해가 가능하다. 하나의 메소드를 이해하기 위해서 너무 많은 클래스의 이해가 필요하며 이 역시 코드를 수정하기 위해서 다른 클래스의 참조, 더 심할 경우 하나의 메소드를 수정하기 위해서 생각보다 더 많은 클래스의 수정이 필요할 수 있다.
3. 설계 개선하기
"그럼 이렇게 많은 문제를 가지고 있는 프로그램을 어떻게 다시 설계하면 좋을까?"
개선해야 할 사항은 "변경이 용이해야 한다.", "특별한 노력없이 쉽게 읽고 이해할 수 있어야 한다." 부분이다.
먼저 변경이 어려운 이유는 클래스끼리 결합도가 높을 때 발생하는 문제다. 클래스가 다른 클래스에 직접 접근하면 변경할 클래스와 그 클래스에서 접근하는 다른 클래스를 모두 수정해야 할 필요가 생긴다. 그렇기 때문에 클래스가 다른 클래스에 대해 알아야 할 정보를 최소한으로 함으로써 각 클래스의 자율성을 높이는 방법이 존재한다.
다음으로 특별한 노력없이 쉽게 읽고 이해하게 할 가장 좋은 방법은 프로그램의 동작을 현실세계의 동작과 최대한 비슷하게 맞춰주는 것이다. 변경을 하기 위해서는 동작을 이해해야 한다. 만약 프로그램의 동작이 현실세계의 동작(우리가 알고있는 상식)과 다르다면 코드 개발자의 생각을 이해하기 어려울 것이다.
위의 프로그램도 마찬가지다. 티켓 판매 애플리케이션의 문제점은 Theater 클래스에 관람객과 판매원의 동작이 같이 들어가 있다는 부분이다. 소극장이 행동하는 범위가 우리의 예상을 벗어나 관람객과 판매원이 해야 할 역할을 수행하고 있고, 이는 프로그램을 변경할 때 큰 문제로 다가올 것이다.
그렇다면 Theater 클래스에 작성된 기능을 TicketSeller 클래스와 Audience 클래스에 재분배함으로 각자의 역할에 자율성을 더해주는 방향으로 문제점의 개선이 가능해보인다.
현실에서 판매원의 주된 업무는 매표소에서 표를 판매하는 것이다. Theater 클래스에서 표를 판매하는 기능을 TicketSeller 클래스에 옮긴다면 결합도가 낮아질 것이다. 추가로 티켓판매는 판매원이 매표소에서 진행하는 것이며 Theater는 관여할 필요가 없으므로 TicketSeller 클래스의 TicketOffice를 참조하는 메소드를 삭제하는 것은 클래스 간의 정보를 차단하는데 도움이 된다.
관람객은 돈이나 초대장을 내고 티켓을 받는 역할을 한다. 관람객의 가방에서 초대장이 있다면 별도의 금액 지불 없이 티켓을 받고, 초대장이 없다면 티켓의 가격만큼 금액을 지불한다. 또한 가방을 가지고 해야 할 일을 만들었기 때문에 Bag 클래스에 직접 접근하는 메소드는 삭제하는 것이 클래스 간의 정보를 차단하는데 도움이 된다.
기능 재분배가 완료된 Theater 클래스의 모습이다.
|
1
2
3
4
5
6
7
8
9
10
11
|
public class Theater {
private TicketSeller ticketSeller;
public Theater(TicketSeller ticketSeller) {
this.ticketSeller = ticketSeller;
}
public void enter(Audience audience) {
ticketSeller.toSell(audience);
}
}
|
cs |
가방(Bag 클래스)과 매표소(TicketOffice 클래스)에 직접 관여하지 않고 소극장 내부에서 판매원이 관람객에게 티켓을 판매한다는 간단한 코드만이 남았다.
프로그램 전체 설계는 다음과 같다.

관람객의 가방을 직접 건들고 판매원의 표를 판매하는 역할을 수행하는 원래의 모습에서 관람객과 판매원이 각자의 소지품을 스스로 관리하고 해야할 역할 또한 소극장에서 관여하지 않고 직접 수행한다.
이는 현실과 일치하며 코드 개발자와 코드를 읽는 사람간의 의사소통에도 문제가 없다. 더 중요한 점은 TicketSeller 클래스와 Audience 클래스의 기능이 변경되어도 Theater 클래스의 기능은 변경할 필요가 없다는 부분이다.
비록 TicketSeller 입장에서는 Audience 클래스에 의존성이 추가로 생겼지만 전에 설계한 것과 비교한다면 이 코드는 확실하게 전보다 개선되었다.
개선의 핵심은 캡슐화를 통해 응집도를 높여준 점이다.
각 클래스의 상태를 캡슐화 시켜주고 클래스간의 상호작용을 오로지 메시지로만 수행하게 만들어준 점이다.
Theater 클래스는 TicketSeller 클래스와 Audience 클래스 내부의 상태를 알지 못하고 각 클래스의 메소드만을 사용해서 원하는 결과를 얻어낸다. 이렇게 자신과 연관된 작업만 수행하고 연관이 없는 작업은 다른 클래스에 넘기는 것으로 클래스의 응집도를 높여줄 수 있다.
이렇게 클래스의 응집도를 높이기 위해서는 클래스 스스로 자신의 데이터를 책임져야 한다. 이를 위해서 클래스는 자기 스스로 데이터를 처리하고 반환하는 자율적인 존재일 필요가 있다.
이런 자율성 높은 클래스간의 메시지만을 통해 상호작용하도록 만드는것이 휼륭한 객체지향 설계의 방향이다.
4. 절차지향 설계와 객체지향 설계의 차이

해당 그림은 절차지향적 설계로 프로그래밍 된 프로그램이다. 보는 것 처럼 중앙에서 모든 데이터를 처리하고 다른 클래스는 데이터의 역할만을 수행한다. 절차적 프로그램은 현실의 상식에 위배되는 특징이 있다. 중앙에 있는 소극장이 관람객의 가방을 함부로 뒤지고 판매자의 역할을 빼앗아도 아무런 불만을 표하지 않기 때문이다.

해당 그림은 지금까지 설명했던 객체지향적 설계로 프로그래밍 된 프로그램이다. 보는 것 처럼 각 클래스가 메시지를 통해 소통하며 들어온 데이터를 스스로 처리해 다음 클래스에 반환하는 모습을 보인다. 서로가 서로의 역할만을 수행하며, 변경또한 각자의 클래스만 변경하면 되기 때문에 코드 수정에도 용이하다.
5. Trade-Off
위에서도 설명했듯이 전체적으로 코드는 객체지향적으로 개선되었지만 TicketSeller 입장에서는 Audience 클래스에 의존성이 추가로 생겼다. 이처럼 어떠한 행위에 있어서 항상 장점과 단점이 공존한다.
우리는 항상 Trade-Off 속에서 가장 현명한 답을 찾아야 한다. 이러한 Trade-Off 는 객체지향 설계에서도 피해갈 수 없었다.
6. 설계의 존재의의
책에서는 설계의 정의를 다음과 같이 적혀있다.
"설계란 코드를 배치하는 것이다."
설계에 특별한 뜻을 붙이지 않으며 설계에 대해 잘 정의했다는 부분에서 이 구절이 굉장히 인상깊게 다가왔다.
좋은 설계를 하는 방법이란
- 오늘 완성해야 하는 기능을 구현하는 코드
- 내일 쉽게 변경할 수 있는 코드
를 말한다.
여기서 쉽게 변경할 수 있는 코드 = 이해하기 쉬운 코드로도 해석할 수 있다. 코드를, 더 나아가 설계를 이해하기 쉽다면 코드를 변경하는건 그리 어려운 일이 아닐 것이기 때문이다.
프로그램의 요구사항은 항상 변화하고, 그렇기 때문에 변경이 쉬워야 하는건 굉장히 다양한 것이다. 또한 변경이 용이해야 하는 또 다른 이유는 코드를 변경하면 버그가 발생한 가능성이 커지기 때문이다. 변경이 어렵다면 그만큼 변경했을 때 버그가 발생할 확률이 올라가고 이는 코드를 수정하려는 의지를 꺽는다는 것이다.
7. 객체지향 설계
객체지향 설계란 객체 각자에 자율성을 보장해서 코드의 이해를 쉽게하는 것이다. 위에서 계속 말했던 것 처럼 코드의 이해를 쉽게 한다는 것은 다시말해 코드의 변경이 용이해아 한다는 것이다.
그러나 단순히 데이터와 프로세스를 밀어넣어둔다고 자율성이 보장되고 코드 변경이 쉬운것은 아니다.
세상을 바라보고 생각했을 때 객체지향 프로그램은 사람이 예상할 수 있는 방식대로 객체간에 상호협력할 수 있는 방향으로 설계가 진행되어야 한다.
객체간에 의존성이 높을수록 애플리케이션의 수정이 어렵다고는 하지만 그 의존성을 완전히 없앨수는 없다.
중요한건 객체 사이의 의존성을 적절하게 관리하는 것이고 이것이 훌륭한 객체지향 설계인 것이다.