Chapter 1. 객체, 설계

Posted by yunki kim on January 1, 2022

티켓 판매 애플리케이션 구현

  소극장에서 이벤트를 통해 공연을 무료로 관람할 수 있는 관객과 표를 구매해야 하는 관객이 있다고 가정하자. 이벤트에 당첨된 관객은 당첨을 입증하는 표를 연극표로 교환한 뒤에 입장할 수 있다. 따라서 연극 당일날 이 두 종류의 관객을 입장시킬때 당첨여부를 확인하고 당첨자가 아니라면 티켓을 판매한 뒤 입장해야 한다. 이를 구현하기 위해 다음과 같은 클래스들을 설계하자

1
2
3
4
5
6
7
/**
 * 이벤트 당첨자에게 발송되는 초대장
 */
public class Invitation {
    /** 초대 일자 */
    private LocalDateTime when;
}
cs
1
2
3
4
5
6
7
8
9
10
11
12
/**
 * 공연을 관람하는 사람들이 가지고 있어야 하는 티켓
 */
public class Ticket {
    
    private Long fee;
    
    public Long getFee() {
        return fee;
    }
}
 
cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
 * 이벤트 당첨 여부와 티켓 보유 여부를 판단하기 위해
 * 관객의 가방 내부 정보를 담는다
 */
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;
    }
}
 
cs
1
2
3
4
5
6
7
8
9
10
11
12
13
/** 관객 정보 */
public class Audience {
    private Bag bag;
    
    public Audience(Bag bag) {
        this.bag = bag;
    }
 
    public Bag getBag() {
        return bag;
    }
}
 
cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/** 티켓 판매소 */
public class TicketOffice {
 
    /** 티켓의 판매 금액 */
    private Long amount;
 
    /** 관객에게 판매할 티켓 */
    private List<Ticket> tickets = new ArrayList<>();
 
    public TicketOffice(Long amount, Ticket ... tickets) {
        this.amount = amount;
        this.tickets.addAll(Arrays.asList(tickets));
    }
 
    public Ticket getTicket() {
        return tickets.remove(0);
    }
 
    public void minusAmount(Long amount) {
        this.amount -= amount;
    }
 
    public void plusAmount(Long amount) {
        this.amount += amount;
    }
}
cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/** 티켓 판매원 */
public class TicketSeller {
    
    /** 티켓 판매원은 자신이 일하는 티켓 판매소를 알고 있어야 한다 */
    private TicketOffice ticketOffice;
    
    public TicketSeller(TicketOffice ticketOffice) {
        this.ticketOffice = ticketOffice;
    }
 
    public TicketOffice getTicketOffice() {
        return ticketOffice;
    }
}
 
cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/** 관객을 입장시키는 클래스 */
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

  이 애플리케이션의 구조는 다음과 같다

티켓 판매 애플리케이션의 문제

  우선 모듈은 크기와 상관 없이 클래스, 패키지, 라이브러리 같이 프로그램을 구성하는 임의의 요소를 말한다. 좋은 모듈은 제대로 실행되어야 하고, 변경이 용이해야 하며, 이해하기 쉬워야 한다. 하지만 위의 코드는 변경 용이와 이해 측면에서 부족하다.

  Theater객체의 경우 다음과 같은 문제를 포함하고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Theater {
    // ...
 
    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

  실제 세계에서는 절대 저런 식으로 동작하지 않는다. 현실에서는 관람객이 직접 가방에서 초대장을 꺼내 판매원에게 건낸다. 티켓을 구매하는 관람객은 가방 안에서 돈을 직접 꺼내 판매원에게 지불한다. 그 후 판매원은 매표소에 있는 티켓을 직접 꺼내 관람객에게 건내고 관람객에게서 직접 받은 돈을 받아 매표소에 보관한다. 따라서 현실과 코드 사이에는 엄청난 차이가 존재하고 이는 코드를 읽는 사람과 제대로 의사소통을 하지 못하는 문제를 야기한다.

  코드를 다른 관점에서 봐보자. 이 코드를 이해하기 위해서는 Audience, Bag, TicketSSeller, TicketOffice 객체의 상태와 메서드를 모두 이해하고 있어야 한다. 즉, 하나의 클래스나 메서드에서 너무 많은 세부 사항을 다루기 때문에 코드를 작성하는 사람과 읽는 사람 모두에게 큰 부담을 준다. 또 한, 다른 클래스에 의존도가 높아 Audience와 TicketSeller를 변경하면 Theater도 변경해야 한다.

  이 코드의 또 다른 문제는 변경에 취약하는 것이다. 이 코드는 관객이 가방을 가지고 있고 관객이 현금을 가지고 다는 등의 가정을 한다. 만약 관객이 가방이 없고, 현금이 아닌 다른 지불수단을 사용한다면 어떨까? 여러 클래스를 변경해야할 것이다. 이를 다른 말로 설명하면 의존성이 너무 높다는 문제가 된다. 의존성이라는 단어는 어떤 객체가 변경될 때 그 객체에게 의존하는 다른 객체를 변경해야 한다는 것을 암시한다.

  의존성이 높으면 안되지만 의존성이 아예 없는것 역시 좋지 않다. OOP 설계는 서로 의존하면서 협력하는 객체들의 공동체를 구축하는 것이다. 따라서 애플리케이션의 기능을 구현하는데 필요한 최소한의 의존성만 유지해야 한다.

 

설계 개선

  위의 예제는 코드를 이해하기 어렵고 변경하기도 어렵다. 이해하기 어려운 이유는 Theater객체가 Audience, TicketSeller 객체에 직접 접근하기 때문이다. 이는 관람객이, 티켓 판매원이 스스로의 일을 직접 처리하지 못한다는 직관에서 벗어난다. 즉, 의도를 정확하게 의사소통 하지 못한다.

  이를 해결하는 방법은 Theater가 Audience, TicketSeller에 대한 세부적 부분을 알지 못하게 차단하고 각 객체가 각자의 역할에 맞는 일을 하는 자율적인 존재로 만들면 된다.

  Audience와 TicketSeller가 Bag과 TicketOffice를 처리하는 자율적인 존재로 만들어 주면 된다.

  이를 달성하기 위해 우선 Theater와 TicketSeller의 코드를 다음과 같이 바꾸자. 

* '/**/'로 주석처리된 코드는 기존코드, '//'와 '//'사이에 있는 코드는 새로 변경한 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/** 관객을 입장시키는 클래스 */
public class Theater {
    private TicketSeller ticketSeller;
 
    public Theater(TicketSeller ticketSeller) {
        this.ticketSeller = ticketSeller;
    }
 
    /* enter 내부 로직이 TicketSeller의 sellTo method로 이동
    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);
        }
    }
    */
    
    //
    public void enter(Audience audience) {
        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 = ticketOffice.getTicket();
            audience.getBag().setTicket(ticket);
        } else {
            Ticket ticket = ticketOffice.getTicket();
            audience.getBag().minusAmount(ticket.getFee());
            ticketOffice.plusAmount(ticket.getFee());
            audience.getBag().setTicket(ticket);
        }
    }
    //
}
 
 
cs

  이렇게 코드를 변경하면 TicketSeller클래스에서 getTicketOffice()메서드가 삭제됬기 때문에 오직 TicketSeller에서만 TicketOffice 객체에 접근할 수 있다. 따라서 TicketSeller는 ticketOffice에서 티켓을 꺼내거나 판매 요금 적립을 직접 해야 한다. 이처럼 개념적이나 물리적으로 객체 내부에 세부적인 사항을 감추는 것을 캡슐화(encapsulation)이라 한다. 캡슐화의 목적은 변경하기 쉬운 객체를 만드는 것이다. 캡슐화로 객체에 대한 접근을 제한하면 객체와 객체 사이의 결합도를 낮출 수 있기 때문에 설계를 더 편하게 할 수 있다.

  바뀐 Theater객체를 보면 이 객체는 ticketOffice가 TicketSeller 내부에 존재한다는 사실을 알지 못한다. 따라서 Theater는 오직 TicketSeller의 인터페이스(interface)에만 의존한다. TicketSeller 내부에 TicketOffice 인스턴스가 존재한다는 사실은 구현(implementation) 영역에 속한다.

  객체를 인터페이스와 구현으로 나누고 인터페이스만을 공개하는 것은 객체 사이의 결합도를 낮추고 변경하기 쉬운 코드를 작성하기 위해 따라야 하는 가장 기본적인 설계 원칙중 하나이다.

Theater의 결합도를 낮춘 설계

  이제 TicketSeller와 같은 방식으로 Audience의 캡슐화를 진행해 보자. TicketSeller 객체의 sellTo 메서드를 보면 TicketSeller가 Audience 객체의 가방에 접근한다(getBag()). 이를 다음과 같이 바꾸어 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/** 티켓 판매원 */
public class TicketSeller {
 
    /** 티켓 판매원은 자신이 일하는 티켓 판매소를 알고 있어야 한다 */
    private TicketOffice ticketOffice;
 
    public TicketSeller(TicketOffice ticketOffice) {
        this.ticketOffice = ticketOffice;
    }
 
    /* 내부 로직이 Audience 클래스의 buy메서드로 이동
    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);
        }
    }
    */
    
    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();
        }
    }
    //
}
 
cs

Audience를 캡슐화 한 후의 설계

  위와 같은 수정 과정을 거친 결과 TicketSeller와 Audience는 각자 자신이 필요한 소지품을 스스로 관리하게 되었고 이는 우리의 예상과 일치한다. 또 한 Audience와 TicketSeller내부 로직을 변경해도 더이상 맨 처음의 코드 처럼 Theater를 변경하지 않아도 된다. 이를 통해 객체의 자율성을 높여서 이해하기 쉽고 유연한 설계를 얻을 수 있다.

캡슐화와 응집도

  이런 유연한 설계를 하기 위해서는 객체 내부의 상태를 캡슐화 하고 객체 간에 오직 메시지를 통해서만 상호작용 하도록 하는 것이 핵심이다. 위에서 개선한 코드를 다시 보자. Theater는 TicketSeller의 내부에 대해서는 알지 못한다. 오직 TicketSeller가 sellTo메시지를 이해하고 응답할 수 있다는 사실만 알고 있다. TicketSeller역시 Audience의 내부에 대해서는 알지 못한다. 단지 Audience가 buy메시지에 응답할 수 있고 자신이 원하는 결과를 반환할 것이라는 사실만 알고 있다.

  밀접하게 연관된 작업만을 수행하고 연관성 없는 작업은 다른 객체에게 위임하는 객체를 가리켜 응집도(cohesion)가 높다고 한다. 자신의 데이터를 스스로 처리하는 자율적인 객체를 만들면 결합도를 낮출 수 있고 응집도를 높일 수 있다.

  객체의 응집도를 높이기 위해서는 객체 스스로 자신의 데이터를 책임져야 한다. 이느 객체의 응집도를 높이는 첫 걸음이다. 외부의 간섭을 최대한 줄이고 메시지를 통해서만 협력하는 자율적인 객체들의 공동체를 만드는 것이 훌륭한 OOP설계를 얻는 지름길이다.

 

절차지향과 객체지향

  수정하기 전의 코드를 보면 Theater의 enter메서드 안에서 관람객을 입장시키는 절차를 구현했다. Audience, TicketSeller, Bag, TicketOffice는 관람객을 입장시키는 데 필요한 정보를 제공하고 모든 처리는 Theater의 enter메서드 안에 존재했다. 여기서 enter메서드는 프로세스(Process)이고 Audience, TicketSeller, Bag, TicketOffice는 데이터(Data)이다. 이처럼 프로세스와 데이터를 별도의 모듈에 위치시키고 프로세스가 필요한 모든 데이터에 의존하는 것이 절차지향적 프로그래밍(Procedural Programming)이다.

  절차지향적 프로그래밍은 사람의 직관에 위배된다. 또 한 데이터의 변경으로 인한 영향을 지역적으로 고립시키기 어렵다. 변경은 버그를 야기하고 버그에 대한 두려움은 코드를 변경하기 어렵게 한다. 따라서 절차적 프로그래밍의 세상은 변경하기 어려운 코드를 양산하는 경향이 있다.

  변경하기 쉬운 설계는 한번에 하나의 클래스만 변경할 수 있어야 한다. 이를 할 수 있는 방법은 데이터와 프로세스가 동일한 모듈 내부에 위치해 데이터를 스스로 처리하게 하는 것이다. 이 방식이 객체지향 프로그래밍이다. 훌율한 객체지향 설계의 핵심은 캡슐화를 이용해 의존성을 적절히 관리해 객체 사이의 결합도를 낮추는 것이다. 결합도가 낮아야 객체 내부의 변경이 외주로 파급되지 않는다.

 

책임의 이동

  절차지향과 객체지향의 근본적 차이는 책임의 이동(shift of responsibility)에 있다. 아래는 각각 절차지향과 객체지향적으로 작성한 티켓 판매 애플리케이션의 데이터 흐름이다. 

절차지향 프로그래밍 데이터 흐름
객체지향적 프로그래밍 데이터 흐름

  절차지향형 프로그래밍은 책임이 Theater에 집중되 있다. 그에 반해 객체지향형 프로그래밍은 책임이 개별객체로 이동되 있다. 즉 , 책임의 이동이 되었다. 객체지향 설계의 핵심은 적절한 객체에 적절한 책임을 할당하는 것이다. 객체는 다른 객체와의 협력이라는 문맥 안에서 특정 역할을 수행하는데 필요한 적절한 책임을 수행해야 한다. 따라서 객체가 어떤 데이터를 가지느냐보다는 객체에 어떤 책임을 할당하는 것이냐에 초점을 맞추어야 한다.

  의존성을 설계를 어렵게 만든다. 설계를 용이하게 하기 위해서는 불필요한 의존성을 제거해 결합도를 낮춰야 한다. 결합도를 낮추기 위해서는 객체의 불필요한 세부 사항을 객체 내부로 캡슐화 하해 객체의 자율성을 높이고 응집도 높은 객체를 만들어야 한다.

 

더 개선할 수 있다.

  Audience클래스를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/** 관객 정보 */
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();
        }
    }
}
cs

  위의 설명을 기준으로 봤을 때 Audience는 여전히 Bag의 객체와 강력히 결합되 있다. 이는 다음과 같이 수정할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/** 관객 정보 */
public class Audience {
    private Bag bag;
 
    public Audience(Bag bag) {
        this.bag = bag;
    }
 
    /* Bag 클래스의 hold 메서드로 이동
    public Long buy(Ticket ticket) {
        if (bag.hasInvitation()) {
            bag.setTicket(ticket);
            return 0L;
        } else {
            bag.setTicket(ticket);
            bag.minusAmount(ticket.getFee());
            return ticket.getFee();
        }
    }
    */
 
    //
    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 -> private 으로 변경
    private boolean hasInvitation() {
        return invitation != null;
    }
 
    public boolean hasTicket() {
        return ticket != null;
    }
 
    //public -> private 으로 변경
    private void setTicket(Ticket ticket) {
        this.ticket = ticket;
    }
 
    //public -> private 으로 변경
    private void minusAmount(Long amount) {
        this.amount -= amount;
    }
 
    public 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();
        }
    }
    //
}
 
cs

 

  같은 이유로 TicketSeller가 TicketOffice의 자율권을 침해하고 있다. TicketSeller는 TicketOffice에 있는 Ticket을 마음대로 꺼내서 팔고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** 티켓 판매원 */
public class TicketSeller {
 
    /** 티켓 판매원은 자신이 일하는 티켓 판매소를 알고 있어야 한다 */
    private TicketOffice ticketOffice;
 
    public TicketSeller(TicketOffice ticketOffice) {
        this.ticketOffice = ticketOffice;
    }
 
    public void sellTo(Audience audience) {
        ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
    }
}
cs

  이를 다음과 같이 개선해 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/** 티켓 판매원 */
public class TicketSeller {
 
    /** 티켓 판매원은 자신이 일하는 티켓 판매소를 알고 있어야 한다 */
    private TicketOffice ticketOffice;
 
    public TicketSeller(TicketOffice ticketOffice) {
        this.ticketOffice = ticketOffice;
    }
 
    /* TicketOffice의 sellTicketTo로 로직 이동
    public void sellTo(Audience audience) {
        ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
    }
     */
 
    //
    public void sellTo(Audience audience) {
        ticketOffice.sellTicketTo(audience);
    }
    //
 
}
 
/** 티켓 판매소 */
public class TicketOffice {
 
    /** 티켓의 판매 금액 */
    private Long amount;
 
    /** 관객에게 판매할 티켓 */
    private List<Ticket> tickets = new ArrayList<>();
 
    public TicketOffice(Long amount, Ticket ... tickets) {
        this.amount = amount;
        this.tickets.addAll(Arrays.asList(tickets));
    }
 
    // public -> private 으로 변경
    private Ticket getTicket() {
        return tickets.remove(0);
    }
 
    // public -> private으로 변경
    private void plusAmount(Long amount) {
        this.amount += amount;
    }
 
    public void sellTicketTo(Audience audience) {
        plusAmount(audience.buy(getTicket()));
    }
 
 
    public void minusAmount(Long amount) {
        this.amount -= amount;
    }
 
}
 
cs

  이 개선은 TicketOffice의 자율성은 높였지만 전체적인 설계 관점에서는 결합도를 높였다.

개선 이전
개선 이후

  이를 통해 다음과 같은 사실을 알 수 있다. 

    1. 어떤 기능을 설계하는 방법은 한 가지 이상일 수 있다.

    2. 동일한 기능을 한 가지 이상의 방법으로 설계할 수 있기 때문에 설계는 trade-off의 산물이다.

  설계는 균형의 예술이다. 훌륭한 설계는 적절한 trade-off의 결과물이다.

 

그래, 거짓말이다!

  앞에서 티켓 판매 애플리케이션을 처음 개선할때 우리가 세상을 바라보는 직관과 일치하게 코드를 작성해야 한다고 했다. 그때문에 Audience와 TicketSeller가 자신의 일을 책임지며 자율성을 가져야 한다고 했다. 하지만 그 뒤를 보면 Theater, Bag, TicketOffice객체들, 다시말해 실생활에서 무생물인 것들(현실 세계에서 수독적인 것들) 역시 자율성을 갖게 해주었다. 비록 현실에서는 수동적인 존재라도 객체지향의 세계에 들어오면 모든 것이 능동적이고 자율적인 존재로 바뀐다. 레베카 워프스브록(Rebecca Wirfs-Brock)은 이처럼 능동적이고 자율적인 존재로 소프트웨어 객체를 설계하는 원칙을 의인화(anthropomorphism)이라 했다.

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

 

객체지향 설계

  설계는 코드를 배치하는 것이다. 또 한 설계와 구현은 바늘과 실 같은 존재이다. 설계는 코드를 작성하는 매 순간 코드를 어떻게 배치할 것인지를 결정하는 과정에서 나온다. 설계는 코드 작성의 일부이고 코드를 작성하지 않으면 검증할 수 없다.

  좋은 설계는 오늘의 요구사항을 온전히 수행하면서 내일의 변경을 매끄럽게 수용해야 하는 것이다. 변경을 용이하게 해야 하는 이유는 요구사항이 항상 변경되기 때문이다. 개발을 시작하는 시점에서 모든 요구사항을 수집하는 것은 거의 불가능하다. 따라서 요구사항은 개발 도중 언제든지 바뀔 수 있다.

  코드가 변경에 용이해야 하는 또다른 이유는 코드 변경 시 사이트이펙트가 발생할 가능성을 줄이기 위해서이다. 

  객체지향 프로그래밍은 의존성을 효율적으로 통제할 수 있는 다양한 방법을 제공해 요구사항 변경에 좀 더 수월하게 대응할 수 있는 가능성을 높여준다. 변경 가능한 코드는 이해하기 쉬운 코드다. 코드를 이해하기 쉬워야 수정하고 싶은 마음이 든다.

  객체지향 패러다임은 개발자가 세상을 바라보는 코드를 작성할 수 있게 돕는다. 세상에 존재하는 모든 자율적인 존재처럼 객체 역시 자신의 데이터를 스스로 책임지는 자율적인 존재다. 객체지향은 세상에 대해 예상하는 대로 객체가 행동하리라는 것을 보장해 코드를 좀 더 쉽게 이해할 수 있다.

  객체지향 패러다임을 사용할 때 단순히 데이터와 프로세스를 객체 내부에 밀어 넣기만 하면 되는다고 생각 해서는 안된다. 객체지향에서는 객체들이 서로 상호작용을 하고 상호작용은 객체 사이에 주고 받는 메시지로 표현된다. 

  객체들이 메시지를 주고 받을때 객체들 사이에는 의존성이 생성된다. 훌율한 객체지향 설계는 협력하는 객체 사이의 의존성을 적절하게 관리하는 설계다. 

 

출처 - 오브젝트