01. 객체, 설계

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

대부분의 사람들은 이론이 먼저 정립된 후 실무가 그 뒤를 따라 발전한다고 생각한다. 하지만, 소프트웨어에서는 오히려 반대인 상황이다. 대표적인 분야로 '소프트웨어 설계'와 '소프트웨어 유지보수'를 들 수 있다.

훌륭한 설계에 관한 최초의 이론은 1970년대가 돼서야 드러났으며, 대부분의 설계 원칙과 개념 역시 실무에서 반복적으로 적용되던 기법들을 이론화한 것들이 대부분이다.

이 책을 통해서는 위에서 언급했듯이 객체지향 패러다임을 설명하기 위해 '추상적인 개념'이나 '이론'을 앞세우기보단 코드를 이용해 객체지향의 다양한 측면을 느끼려고 해보자.


무엇이 문제인가

로버트 마틴은 소프트웨어 모듈이 가져야 하는 세 가지 기능에 대해 아래와 같이 설명한다.

  • 첫 번째 목적은 실행 중 제대로 동작하는 것이다.

    • 이것은 모듈의 존재 이유이다.

  • 두 번째 목적은 변경을 위해 존재하는 것이다.

    • 대부분의 모듈은 생명주기 동안 변경되기 때문에 간단한 작업만으로 변경 가능해야 한다.

  • 세 번째 목적은 코드를 읽는 사람과 의사소통하는 것이다.

    • 모듈은 특별한 훈련 없이도 개발자가 쉽게 읽고 이해할 수 있어야 한다. 읽는 사람과 의사소통할 수 없는 모듈은 개선해야 한다.

예상을 빗나가는 코드

여기서 세 번째 목적, '이해할 수 있는 코드'란 그 동작이 우리의 예상에서 크게 벗어나지 않는 코드를 말한다. 즉, 우리의 상식과 동일하게 코드가 동작하여 코드를 읽는 사람과 의사소통을 할 수 있는 상태를 만들어야 한다.

또한, 코드를 이해하기 위해 여러가지 세부적인 내용을 기억하고 있어야 한다면, 코드를 이해하기 어렵게 만든다.

변경에 취약한 코드

더 큰 문제는 변경에 취약한 코드이다. 이 내용은 객체 사이의 의존성(dependency)과 관련된 문제로, 의존성이 변경과 관련돼 있다는 점이다. 의존성은 어떤 객체가 변경될 때 그 객체에게 의존하고 있는 다른 객체도 함께 변경될 수 있다는 사실을 말한다.

그렇다고해서 객체 사이의 의존성을 완전히 없애는 것이 정답이 아니다. 객체지향 설계는 서로 의존하면서 협력하는 객체들의 공동체를 구축하는 것으로, 애플리케이션의 기능을 구현하는데 필요한 최소한의 의존성만 유지하고 불필요한 의존성을 제거하면 된다.

객체 사이의 의존성이 과한 경우를 결합도(coupling)가 높다고 한다. 두 객체 사이의 결합도가 높으면 높을수록 함께 변경될 확률도 높아지기 때문에 변경하기 어려워진다.

따라서 설계의 목표는 객체 사이의 결합도를 낮춰 변경이 용이한 설계를 만드는 것이어야 한다.

설계 개선하기

앞서 로버트 마틴이 언급한 세 가지 기능을 갖춘 모듈로 개선을 하는 것이 필요하다.

자율성을 높이자

객체를 자율적인 존재로 바라봐야 한다. 다른 객체에 관해 세세한 부분까지 알지 못하도록 정보를 차단하는 방식으로 자율적인 객체로 만들어야 한다.

개념적이나 물리적으로 객체 내부의 세부적인 사항을 감추는 것을 캡슐화(encapsulation)라고 부른다. 이러한 캡슐화를 통해 자율적인 객체로 만들 수 있는 것이다.

getter/setter의 역할에 대해서 깊게 고민해볼 필요가 있다. 내부 필드에 대해서 노출을 하는 것이 과연 캡슐화를 했다고 할 수 있을까? 내부 필드에 대한 접근 제어자를 private 으로 선언한 것일 뿐, 접근이 가능해져버린다.

객체의 자율성을 높이는 방향으로 설계한다면 이해하기 쉽고 유연한 설계를 얻을 수 있는 것이다.

캡슐화와 응집도

객체 내부의 상태를 캡슐화하고 객체 간에 오직 '메시지'를 통해서만 상효작용하도록 만드는 것이다. (getter/setter를 통해서가 아니다!) 다른 객체의 내부에 대해서는 전혀 알지 못하지만, 처리해야할 메시지를 이해하고 응답할 수 있다는 사실만을 알고 있는 것이다.

밀접하게 연관된 작업만을 수행하고 연관성 없는 작업은 다른 객체에게 위임하는 객체를 가리켜 응집도(cohesion)가 높다고 말한다.

객체의 응집도를 높이기 위해서는 객체 스스로 자신의 데이터를 책임져야 한다. 자신이 소유하고 있지 않은 데이터를 이용해 작업을 처리하는 객체에게 어떻게 연관성 높은 작업들을 할당할 수 있겠는가?

객체는 자신의 데이터를 스스로 처리하는 자율적인 존재여야 하며, 이는 객체의 응집도를 높이는 첫 걸음이다.

// AS-IS (절차지향적이다.)
public class Theater {
    private TicketSeller seller;
    
    /*
        이 코드의 구조를 보자
        import 문을 보더라도 Theater 클래스가 알고있는 내용들이 너무 많을 것이다.
     */
    public void enter(Audience audience) {
        if (audience.getBag().hasInvitation()) {
            Ticket ticket = seller.getTicketOffice().getTicket();
            audience.getBag().setTicket(ticket);
        } else {
            Ticket ticket = seller.getTicketOffice().getTicket();
            audience.getBag().setTicket(ticket);
            ticketSeller.getTicketOffice().plusAmount(ticket.getFee());
            audience.getBag().setTicket(ticket);
        }
    }
}

// TO-BE (객체지향적이다.)
public class Theater {
    private TicketSeller seller;
    
    /*
        단순하게 판매원에게 티켓 판매 책임을 넘기게 된다.
     */
    public void enter(Audience audience) {
        seller.sellTo(audience);
    }
}

책임의 이동

객체지향과 절차지향 두 방식 사이에 근본적인 차이를 만드는 것은 '책임의 이동(shift of responsibility)'이다. 기존 코드는 작업의 흐름이 Theater 에 모두 집중되어 있다. 반면, 객체지향 설계에서는 제어 흐름이 적절하게 분산되어 있으며, 필요한 책임이 여러 객체에 분산되어있는 것을 말한다.

객체지향 설계

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

좋은 설계란 무엇일까? 우리가 짜는 프로그램은 두 가지 요구사항을 만족해야 한다.

  • 우리는 오늘 완성해야 하는 기능을 구현하는 코드를 짜야 한다.

  • 내일 쉽게 변경할 수 있는 코드를 짜야 한다.

즉, 좋은 설계란 오늘 요구하는 기능을 온전히 수행하면서 내일의 변경을 매끄럽게 수용할 수 있는 설계를 말한다.

항상 유연한, 변경을 수용할 수 있는 설계에 강조하는 이유는 요구사항이 항상 변경되기 때문이다. 또한, 코드를 변경할 때 버그가 추가될 가능성이 높기 때문이다. 코드를 수정하지 않으면 버그는 발생하지 않지만 변경된 요구사항에 대응하기 위해 코드를 수정하지 않을 수 있는가?

정리하자면,

  • 객체는 자신의 데이터를 스스로 책임지는 자율적인 존재다.

  • 객체들 간의 상호작용을 통해 객체지향 애플리케이션이 구현된다.

  • 객체 사이의 상호작용은 객체 사이에 주고 받는 메시지로 표현된다.

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

Last updated

Was this helpful?