솜이의 데브로그

Ch1 ) 객체, 설계 본문

책을 읽자/오브젝트 : 코드로 이해하는 객체지향 설계

Ch1 ) 객체, 설계

somsoming 2022. 11. 28. 21:31

책 : 오브젝트

서론

  • 티켓 판매 시스템이라는 간단한 도메인을 예로 들어 전체적인 주제를 함축 및 전달

기본적으로 패러다임에 대해서 설명하고 있고 이 중 프로그래밍 패러다임의 경우 기존의 것을 새로쓰는 것이 아니라 별개의 패러다임으로 존재한다.

과거의 패러다임의 단점을 보완하는 발전적인 과정이라고 보인다.

→ 프로그래밍 패러다임은 새로운 것으로 바꾸는 혁명적이라기 보다 발전적이라고 볼 수 있다.

객체지향 패러다임과, 절차지향 패러다임의 경우를 나눈다기 같이 배우면 좋은 영역이라 보면 좋을 거 같다.

 

이론이 먼저일까?, 실무가 먼저일까?

로버트 L 글래스는 학습에 있어서 대부분 이론을 먼저 배우고 정립한 후 실무로 넘어가는 것이 더 발전할 것이라는 견해와는 다르게 실무가 선행 되고 이론을 정립하는 것이 더 발전적이라고 말한다.

→ 여기서 하고자 하는 말은 실무를 통해 자세히는 모르지만 대략의 흐름을 파악하고 그에 대해 자세히 이론을 배우는 것이 이해하기가 쉽고 성장할 수 있기 때문이 아닐까 싶다.

이 책에서는 소프트웨어 유지와 설계에 있어서 글래스의 주장을 기반으로 이론보다는 실무에 맞춰 책을 소개하고 있고 장황한 설명보다 코드를 통해 대화하는 형식으로 우리에게 말해준다.

 

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

  • 작은 이벤트를 진행
    • 추첨을 통해 선정된 관람객에게 공연을 무료로 관람할 수 있는 초대장을 발송하는 것
  • 일반 관람객, 이벤트 당첨 관람객은 다른 방식으로 입장해야한다.
    • 일반 관람객 : 티켓을 구매 해야만 입장
    • 이벤트 당첨 관람객
      • 이벤트 당첨 여부를 확인!
        • **당첨(O)** → 바로 입장
        • **당첨(X)** → (일반 관람객 처럼) 티켓을 구매 해야만 입장

Step 1

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);
        }
    }
}
  • 테스트를 해봤을땐 전혀 문제되는 에러 없이 잘 돌아간다.
  • enter 메서드에서 봤을때 로직이 처리되는 방식이 자동화되어 처리되고 있단 느낌이 없다.
    • 특히, 일반 관람객의 경우 티켓을 받기 전 티켓 부스에서 티켓하나를 가져오고 금액을 지불하고 티켓 판매원은 판매 금액(amount)를 증가시키고 그제서야 티켓을 받는 형태를 띄게된다.

02 무엇이 문제인가?

로버트 마틴은 소프트웨어 모듈이 가져야 하는 세가지 기능에 관해 설명했습니다.(클린 소프트웨어 : 애자일 원칙과 패턴, 실천방법)

모듈이란? 크기와 상관없이 클래스나 패키지, 라이브러리와 같이 프로그램을 구성하는 임의의 요소를 의미한다.

마틴의 3가지 목적

  1. 실행 중에 제대로 동작하는 것
  2. 변경을 위해 존재하는 것
    1. 대부분의 모듈은 생명주기 동안 변경되기 때문에 간단한 작업 만으로도 변경이 가능해야한다.
    2. 변경하기 어렵다 하면 이는 개선해야한다!
  3. 코드를 읽는 사람과 의사소통 하는 것
    1. 결국 개발은 혼자하는 것이 아니라 같이작업하기 때문에 다른 사람이 봐도 파악하기 쉬워야한다.

→ 위 목적을 step1에 대입해보면 1번의 항목만 만족하고 나머지는 만족하지 않는다.

 

02-1 예상을 빗나가는 코드

예상을 빗나간다는 의미는 우리가 실생활에서 처리되는 방식과 다르다는 것을 의미한다. 먼저 문제는 관람객과 판매원이 소극장의 통제를 받는다.

1. 왜 통제를 받을까?

  • 코드를 잘보게되면 소극장(theater)이 enter메서드를 통해 관람객한테서 초대장이 있는지 없는지를 직접 확인하고 관람객 돈을 차감하고 매표소(ticket office)의 판매금액을 높이고 있다.
  • 이 상황을 좀더 쉽게 풀어보면 소극장안에 판매원과 관람객이 있다고 했을때 서로 가만히 있는데 티켓을 누군가 판매하고 그 돈이 쌓이고 차감하고의 형태가 된다. (마치 기계가 대신 해주는 느낌?)

2. 이해 가능한 코드인가?

  • 원래 우리가 극장에서 티켓을 구매한다고 했을때 내 가방에서 직접 초대장을 꺼내거나 돈을 꺼내어 지불하고 티켓 판매자는 초대장을 확인하고 직접 티켓을 가져와서 관람객에게 전해주게된다.
  • → 비교해 봤을때 일상생활에서 겪는 프로세스와 다르게 보인다. 그래서 이책에서도 우리의 상식과 다르게 동작하여 의사소통을 제대로 하지 못한다 나와있다.
  • enter 메서드만 봐도 판매원과 관람객의 행위가 각각 나뉘어져 있기 때문에 일련의 기능을 전부 알고있어야만 하고 하나의 기능을 빼먹는 경우가 생길 수 있는 구조이다.
  • 기억에만 의존하게 되는 경우가 생겨 실수를 할 수 있고 이 코드를 처음본 사람의 경우 혼동을 줄 수 있는 구조가 된다.

 

02-2 변경의 취약한 코드

step1의 코드에서는 무조건 현금이라는 하나의 결제 수단만 한정지어 구성되어 있다. 이러한 경우 관람객이 다른 결제수단을 늘린다 했을때 관람객 class와 연관된 소극장(theater)도 같이 수정되어야한다.

→ 프로젝트를 하다보면 이런 경우가 한번씩 발생하게 되는데 이럴때 어떤 관계냐에 따라 같이 수정해줘야하는 경우와 아닌 경우를 나눠서 잘 설계해야한다고 생각한다.(무조건 변경에 있어 독립적인 것은 아니라고 생각한다.)

 

Theater가 의존하는 객체

→ Audience, Bag, TicketSeller, TicketOffice, Ticket (많은 클래스를 의존하고 있다)

  • 이와 같은 문제는 **의존성(dependency)**와 연관이 있다.
    • 의존성은 변경에 대한 영향을 암시한다.
    • 객체의 변경이 있을때 그 객체에게 의존하는 다른 객체도 함께 변경될 수 있다는 사실을 내포하고 있다.
  • 객체 사이의 의존성을 완전히 없애는 것이 정답은 아니다.
    • 객체간 관계에서 필요한 의존성은 유지하지만 불필요한 의존성은 제거하는 것이 좋다.
  • 결합도와의 연관성
    • 객체 사이의 의존성이 너무 강한 경우를 결합도가 높다고 말한다.
    • 결합도는 의존성과 관련돼 있고 결합도가 높을 수록 변경에도 자유롭지 못하게된다.
    설계의 목표는 객체 사이의 결합도를 낮춰 변경이 용이한 설계를 만드는 것이다.

03 설계 개선하기

코드가 이해하기가 어렵고 변경하기가 쉽지않기 때문에 개선이 필요하다. 여기서는 변경과 의사소통이라는 문제가 서로 엮여 있다는 점에 주목한다.

  • 이해하기 어려운 이유
    • 소극장(Theater)이 관람객의 가방과 판매원의 매표소에 직접 접근하기 때문.
    • 관람객과 판매원의 각자의 일을 해야만 우리가 생각하는 일반적인 예상과 일치하게 된다.

→ 여기서의 개선에서는 변경의 있어 자유롭고 소극장(Theater)의 의존성을 낮추기 위해 관람객과 판매원을 각각 독립적인 형태로 리팩토링을 진행한다.

 

03-1 자율성을 높이다

소극장(Theater)이 판매소에 접근하는 것을 막는다(판매소는 판매자만 접근 가능하도록 수정)

(기존) 소극장에서 여러 클래스를 접근, 의존하는 형태 → (변경) 오직 판매자에만 접근하도록 변경

  • TicketOffice에 대한 접근을 판매자만 접근할 수 있게 변경함으로서 티켓의 판매, 금액 적립은 판매자에게만 일임하게 된다.
  • 개념적이나 물리적으로 객체 내부의 세부적인 사항을 감추는 캡슐화 적용
    1. 캡슐화를 통해 객체 내부로의 접근을 제한하게 되면 객체간의 의존성(결합도)을 낮출 수 있게된다.
    2. 변경에 있어서도 한 부분만 수정해도 된다.

→ TheaterSeller의 캡슐화가 진행되었고 이 다음은 Audience의 캡슐화를 진행해보자!

(기존) 판매원의 캡슐화로 관람객 bag은 판매자가 접근하는 형태 → (변경) bag은 오직 관람객만 접근 가능

  • 변경된 코드에서는 관람객은 자신의 가방 안에 초대장 유무를 스스로 확인할 수 있게된다.
    • 판매자와 같은 제 3자는 접근을 허용하지 않는다.

→ Audience 클래스에서 getBag()은 필요가 없어지고 Bag을 내부로 캡슐화할 수 있게 된다.

  • 코드 수정 결과, 판매자와 관람객 사이의 의존성(결합도)이 낮아진걸 확인할 수 있다.
    • 이번 코드 수정을 통해 관람객과 판매자는 자신의 역할에 대해선 자신이 책임을 갖게되고 독립적으로 해결한다. (자율적인 존재!)

 

03-2 무엇이 개선됐는가?

마틴의 3가지 목적에 대해서 어느정도 만족하게 되었다.

  1. 코드는 잘 동작하는 것을 만족하였다.
  2. 변경에 있어서 판매자, 관람객에 의존성을 낮춰 자유도를 높였다.
  3. 코드를 처음 읽는 사람도 어떤 클래스가 무엇을 하는지에 대한 의도를 파악하기 쉬워졌다.

 

03-3 어떻게 한 것인가?

처음 코드의 경우 객체의 경우 잘 나누었지만 객체간의 연관성을 생각하지 못한 설계였다. 소극장 자체가 직접 판매자와 관람객의 행동을 제한한 형태를 가지고 있었다. → 수정한 후 소극장판매자에게 티켓 판매만을 전달해주고 판매자는 티켓을 판매하고 금액을 정산 하기만 하고 관람객은 자신의 초대장을 제시하거나 티켓을 구매하는 행위를 각자 역할에 맞춰 나누고 관리할 수 있게 되었다.

→ 각각의 객체는 자율성이 높아지도록 개선되었고 이로써 유연한 설계를 얻을 수 있었다.

 

03-4 캡슐화와 응집도

응집도(cohesion)

  • 밀접하게 연관된 작업만을 수행하고 연관성 없는 작업은 다른 객체에게 위임하는 객체를 가리켜 응집도가 높다고 말한다.
    • 이런 응집도를 높이기 위해서는 객체 스스로 자신의 데이터를 책임져야한다.

→ 외부의 간섭을 최대한 배제하고 메시지를 통해서만 협력하는 자율적인 객체들의 공동체를 만드는 것이 훌륭한 객체지향 설계를 얻을 수 있는 지름길이다

 

03-5 절차지향과 객체지향

절차지향 프로그래밍

프로세스와 데이터를 별도의 모듈에 위치시키는 방식을 절차적 프로그래밍이라고 부른다.

  • 소극장(Theater)의 예시
    • enter 메서드는 **프로세스(Process)**에 해당한다.
    • enter 메서드 내부의 Audience, Bag, TicketSeller, TicketOffice는 **데이터(Data)**에 해당한다.

→ 소극장 프로젝트의 초기 코드에서는 Theater라는 한 클래스 내부에서 일련의 모든 처리를 담당하였고 데이터에 해당하는 클래스들을 모두 의존하고 있다.

책에서는 이런 일반적으로 절차적 프로그래밍은 우리의 **직관에 위배**된다. 우리 일상에서는 관람객과 판매원이 각자 역할이 나뉘어져 자신의 일을 한다. 그렇지만 절차적 방식에 있어서는 능동적인 존재가 아니라 수동적인 존재로 여겨진다.

→ 이런 방식은 우리가 생각하는 예상되는 일들을 쉽게 벗어나게 된다.

절차적 방식은 변경에 취약하다.

  • 초기 코드에서 Theater가 여러 클래스들을 의존하고 있기 때문에 이 여러 클래스중 하나라도 변경이 발생하면 Theater도 변경해야하는 경우가 생긴다.

→ 변경하기 쉬운 설계란 한번의 하나의 클래스만 변경할 수 있어야 한다

객체지향 프로그래밍

데이터와 프로세스가 동일한 모듈 내부에 위치하도록 하는 방식을 객체지향 프로그래밍이라 부른다.

  • 소극장(Theater)의 예시
    • Theater는 오직 TicketSeller에만 의존한다.

→ 의존성은 적절히 통제되고 있고 하나의 변경으로 인한 여러 클래스의 변경은 막을 수 있다.

훌륭한 객체지향 설계의 핵심은 캡슐화를 이용해 의존성을 적절히 관리함으로써 객체 사이의 결합도를 낮추는 것이다.

 

03-6 책임의 이동

앞서 말한 절차지향, 객체지향 프로그래밍의 차이점은 책임의 이동이라고 말할 수 있다.

  • 책임이란? 어떤 일을 수행할때 그 일을 맡아서 직접 맡아서 처리하는 역할이라고 생각한다.
  1. 절차지향 방식의 경우 티켓을 판매하는 판매자, 판매소, 관람객, 가방에 해당하는 행동들이 Theater가 전부 처리하고 있고
  2. 객체지향 방식의 경우 행동들의 주체를 각각의 객체로 분산함으로서 Theater가 분담하는 책임을 나누었다.

→ 이런 객체지향 설계의 핵심은 객체에 적절한 책임을 할당하여 서로의 의존성을 낮추고 각 객체는 자신과 관련된 항목의 행동만을 수행하여 높은 응집력을 가질 수 있게된다.

(캡슐화로 높은 응집력을 만들 수 있고 객체마다 결합도를 낮출 수 있다. )

 

03-7 더 개선할 수 있다

더 개선할 수 있는 사항으로는 Audience와 Bag사이의 역할 분리 TicketOffice 와 TicketSeller의 역할 분리 정도가 남아있다.

(기존)Audience에서 Bag의 행동(초대장 유무,계산)까지 처리 → (변경)Bag의 행동을 따로 분리해준다.

  • 이렇게 하면 Bag에서의 초대장 유무, 티켓 구매, 정산을 분리하여 관리 할 수 있게 된다.→ 이렇게 생각하면 복잡해지지만 레베카 워프스브록은 객체지향의 세계에서는 이러한 수동적인 사물의 객체로 사람처럼 행동하여 자유로운 존재로 생각하여 설계해야한다고 한다.
  • → 여기서 처음에 의문이 들었던건 객체지향이란 우리가 이해할 수 있는 코드여야 한다고 했다 그렇지만 가방은 사람이 아니고 사물이기 때문에 티켓을 구매하는 행동이라는것이 이해가 되지 않는다.

🤔그렇다면 모든 객체에 대해 책임을 지도록 설계하면 무조건 맞는걸까??

  • 기존에는 판매원을 통해 관람객의 티켓을 구매하는 것과 판매소의 티켓을 가져오고 정산하는 과정이었다.
  • 여기서 객체마다의 역할로 바꾸게 되면 티켓을 가져오고 정산하는걸 오로지 팬매소에 위임하게되는 형태가 되는데 이때 판매소는 관람객에게 티켓을 판매하는 형식이 된다.→ 객체지향적으로 응집도를 높이려 했지만 객체간의 결합력은 높아진 형태가 되었다. 이런 경우에는 다른 동료들의 생각을 들어보고 같이 유지보수, 설계에 있어 이해하기 쉬운 방향을 선택하는게 좋다고 생각한다.
  • (책에서는 이부분의 개선은 안하는 쪽으로 기존 왼쪽의 방식을 가져간다.)
  • → 지금까지 객체지향에 대해 들어봤을땐 사물을 사람처럼 의인화해서 생각하면 개선된 코드도 틀리지 않는다 하지만 여기서 좀더 생각해봐야할건 개선했을때 불필요한 의존성이 생기기 때문에 차라리 전에 코드가 더 좋을 수 있게 된다.

04 객체지향 설계

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

좋은 설계란 정확히 무엇일까?

요구사항을 전부 만족하고 변경에 있어서 수용할 수 있는 설계이다.

  • 변경을 수용할 수 있는 설계가 왜 중요할까?
    1. 요구사항은 항상 변경되기 때문에 그럴때마다 쉽게 변경하여 반영할 수 있어야한다.
    2. 코드를 변경할때마다 버그가 생기기 마련인데 이런 부분을 최소화 하고 사이드 이펙트가 적게 나오게 하기 위함이다.
  •