객체지향의 사실과 오해를 읽으며

객체지향의 사실과 오해

image

‘객체지향적인 관점으로 개발’ 그러니까 객체지향이 뭔데? 라는 식으로 객체지향은 항상 추상적으로 다가왔었다. 여태 살아오며 “객체지향”에 대해서 나는 전혀 모르는 사람이었다고 해도 과언이 아니다. 그러다가 우연히 멘토님을 뵙게 되어 가르침 얻었고 그리고 권유해 주신 ‘객체지향의 사실과 오해’ 라는 책을 통해 조금은 객체지향에 대해서 알게 되는 감사함을 느끼게 되었다. 책에는 정말 많은 내용들이 담겨있지만, 그중에서도 인상 깊게 읽었던 내용들을 정리해 보고자 이 글을 작성하게 되었다.

객체지향은 현실세계를 모방했다?

현실 세계에서 식별가능한 개체 또는 사물은 모두 “상태” 와 “행동”을 가진 객체로 볼 수 있으며, 이를 소프트웨어 세계에 모방해 놓은 것이 “객체지향”이다.

객체지향에 대해서 한 번쯤 공부를 해봤다면 들어봤을 법한 내용이다. 이 말이 맞는 말 이기는 하다만, 오류가 존재한다. 현실 세계에서의 객체와, 소프트웨어 세계에서의 객체는 분명한 차이점이 존재한다.

예전에 멘토님이 말씀해 주셨는데, 현실 세계에서의 물은 스스로 양을 줄이거나 늘릴 수 없는 수동적인 존재지만, 소프트웨어 세계에서는 스스로 물의 양을 늘리거나 줄일 수 있는 자율적인 존재라고 하셨었다.

또한 물은 스스로의 상태를 관리하고 필요에 따라 추가적인 능력도 갖출 수 있다.

객체지향의 목적은 현실 세계를 모방하는 것이 아닌 즉 새로운 세계를 창조하는 것이고, 이 부분을 이해하는 것이 객체지향 적인 관점에서의 설계이다.

클래스는 객체를 찍어내는 설계도(틀)이다.

우리는 위에 말이 너무 당연하다는 듯이 머릿속에 있지만, 객체지향이라는 세계에서의 주인공은 ‘클래스’가 아닌 ‘객체’이다.

객체지향에서의 설계에서 중요한 것은 어떤 클래스를 만들어낼까? 가 아닌 어떤 객체들이 메시지를 주고 받는 것이 좋을까?를 고민해야 하는 것이 맞는 선택인 것이다.

객체의 행동이 상태를 결정한다.

책을 읽기전 나는 ,객체의 필요한 상태 즉 데이터를 정의내리고 이와 관련된 메서드가 어떤 것이 있을까? 를 고민했었다. 하지만 책에서는 이러한 설계의 방식을 지적하며 상태를 먼저 생각하고 행동을 결정하는 방식은 협력에 있어서 적합한 객체를 만들지 못할 가능성이 크다고 말하고 있다.

객체의 존재이유는 협력하기 위함이다. 그리고 이를 위해서는 객체들은 메시지를 주고 받는다. 따라서 메시지를 처리해야 할 행동이 먼저 정의가 되어야 하고, 그 다음에 행동을 뒷받침 할 데이터가 필요하다.

메시지는 수신자(객체)의 책임을 결정한다.

객체들의 가장 중요한 키포인트는 ‘메시지’이다. 객체들은 자신에게 할당된 책임을 완수하기 위해, 다른 객체들의 도움을 요청하기도 하며, 이 행위를 위해서 수신하기 전 적합한 객체를 선택하고, 선택한 객체에게 메시지를 전송한다. 수신을 받은 객체는 반드시 메시지를 처리해야 하는 책임을 가지게 된다.

이와 같은 방식은 객체들은 소통하며 수신받은 객체는 또 다른 객체에게 도움을 요청하며 메시지를 보내게 된다.

주고받는 메시지가 어느정도 ‘정의’되고 나면 어느새 객체들의 책임들이 모여 하나의 ‘애플리케이션’ 을 이루게 된다. 객체가 메시지를 선택하는 것이 아닌 메시지가 객체를 선택한다.

메소드는? 메시지는?

책에서는 메시지를 ‘what’ 즉 무엇을 실행할 것인지를 정의해 두었고, 메소드는 ‘How’ 어떻게 실행할지를 정의해 두었다. 동일한 메시지라고 하더라도 각각의 메소드로 처리될 수 있으며 이러한 메시지와 메소드간의 1:N 방식을 다형성이라고 책에서는 가르친다.

객체지향 설계 방식

1) 도메인에 필요한 객체 정리

2) 필요한 메시지 추가

3) 메시지를 수신할 객체 선택

4) 더이상 필요한 메시지가 없을 때까지 2-3 과정 반복

5) 객체들이 수신하는 메시지를 바탕으로 객체들의 인터페이스 구성

6) 객체들의 메소드를 구현

예를 들어 “붕어빵 가게”라는 도메인 모델의 객체지향 설계를 한다고 가정한다면.

도메인에 필요한 객체들 정리

image

우선 ‘붕어빵’이라는 객체가 있어야될 것이고, ‘붕어빵’을 구매하는 ‘손님’, 또 ‘붕어빵’을 만드는 ‘붕어빵 장수’ 객체가 필요할 것이다. 또 ‘붕어빵 장수’는 ‘손님’한테 받은 돈을 저장하기 위해 ‘금고’라는 객체를 하나 가져야할 것이다.

메시지 추가, 객체 선택

image

첫번째로 필요한 메시지는 ‘ 붕어빵을 주문하라’ 이다.

그럼 이 메시지는 누가 받는 것일까? 당연히 손님일 것이다.

하지만 ‘손님’은 붕어빵을 만들 수 없기 때문에 다른 객체에게 도움을 요청해야 하므로 ‘붕어빵 장수’에게 메시지를 요청해야 한다. 그 다음 ‘붕어빵 장수’는 붕어빵을 만들기 전에 ‘손님’에게 받을 돈을 지정해야 하고, ‘금고’에게 메시지를 요청한다.

책의 내용들을 바탕으로 이해한 설계를 해보자.

Barista

public class Barista {

    void takeOrder(String coffeeType, int price) {
        System.out.println("바리스타 : 고객님 께서 주문하신 " + coffeeType + " " + price + "원 되겠습니다. 곧 준비해드리겠습니다");
    }

    void makeCoffee(String coffeeType) {
        System.out.println("손님 : 음~ 이 맛은 " + coffeeType + " 맛이야~ 맛있는걸");
    }
}

Coffee

public class Coffee {

    private String name;
    private int price;

    public Coffee(MenuItem menuItem){
        this.name = menuItem.getName();
        this.price = menuItem.getPrice();
    }
}

CoffeeMain

public class CoffeeMain {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        // 메뉴 아이템 생성
        MenuItem americano = new MenuItem("아메리카노", 1500);
        MenuItem cappuccino = new MenuItem("카푸치노", 2000);
        MenuItem caramelMacchiato = new MenuItem("카라멜 마키야또", 2500);
        MenuItem espresso = new MenuItem("에스프레소", 2500);

        // 메뉴 생성
        MenuItem[] menuItems = {americano, cappuccino, caramelMacchiato, espresso};
        Menu menu = new Menu(menuItems);

        // 손님 생성
        System.out.print("이름을 입력하세요: ");
        String customerName = scanner.nextLine();
        System.out.print("소지금을 입력하세요: ");
        int customerWallet = scanner.nextInt();
        Customer customer = new Customer(customerName, customerWallet);

        // 바리스타 생성
        Barista barista = new Barista();

        while (true) {
            // 메뉴판 출력
            menu.printMenu();

            // 손님이 커피 선택
            System.out.print("커피를 선택하세요 (번호 입력): ");
            int choice = scanner.nextInt();

            // 선택한 메뉴 아이템 가져오기
            MenuItem selectedMenuItem = menu.getMenuItem(choice - 1);

            if (selectedMenuItem != null) {
                // 손님이 커피 주문
                customer.orderCoffee(selectedMenuItem, barista);

                // 손님의 현재 소지금 출력
                customer.printWallet();

                // 커피를 마셨다면 종료
                System.out.print("더 주문하시겠습니까? (1: 예, 2: 아니오): ");
                int continueOrder = scanner.nextInt();
                if (continueOrder != 1) {
                    System.out.println("프로그램을 종료합니다.");
                    break;
                }
            } else {
                System.out.println("잘못된 선택입니다. 다시 선택하세요.");
            }
        }
    }
}

Customer

public class Customer {

    private String name;
    private int wallet;

    Customer(String name, int wallet) {
        this.name = name;
        this.wallet = wallet;
    }

    void orderCoffee(MenuItem menuItem, Barista barista) {
        int price = menuItem.getPrice();
        if (wallet < price) {
            System.out.println(name + " : 소지금이 적어 주문할 수 없어");
        } else {
            barista.takeOrder(menuItem.getName(), price);
            barista.makeCoffee(menuItem.getName());
            wallet -= price;
        }
    }

    void printWallet() {
        System.out.println(name + "의 현재 소지금: " + wallet + "원");
    }
}

Menu

public class Menu {

    private MenuItem[] items;

    Menu(MenuItem[] items) {
        this.items = items;
    }

    void printMenu() {
        for (int i = 0; i < items.length; i++) {
            System.out.println((i + 1) + ". " + items[i].getName() + " - " + items[i].getPrice() + "원");
        }
    }

    MenuItem getMenuItem(int index) {
        if (index >= 0 && index < items.length) {
            return items[index];
        } else {
            return null;
        }
    }
}

MenuItem

public class MenuItem {

    private String name;
    private int price;

    MenuItem(String name, int price) {
        this.name = name;
        this.price = price;
    }

    String getName() {
        return name;
    }

    int getPrice() {
        return price;
    }
}

다시 객체지향이라는 단어로 돌아오자면

  • 시스템을, 상호작용하는 자율적인 객체들의 공동체로 바라보고 객체를 이용해 시스템을 분할하는 방법이다.
  • 자율적인 객체는 상태와 행위를 함께 지니며 스스로 자신을 책임진다.
  • 객체는 시스템의 행위를 구현하기 위해 다른 객체와 협력한다.
  • 객체는 협력하기 위해 다른 객체에게 메시지를 전송하고 이를 수신한 객체는 적합한 메서드를 자율적으로 선택한다.

책을 읽고 난 이후

사실 아직도 객체지향적인 설계는 쉽지 않으며 많이 부딪쳐보고 설계해 봐야 알 수 있는 개념이라고 생각한다.

예제 코드가 조금 있어서 개념에 이해에 도움이 된 것은 사실이지만 실제 구현 부분이 더 명확하게 있었다면 더욱 나에게 맞지 않았을까 생각한다.

이러한 아쉬운 점에도 불구하고 ’객체지향’ 이라는 주제가 주어졌을 때 초등학생에게도 이에 대한 개념을 설명해 줄 수 있다면 좋은 책임에는 분명하다.

결론은 객체지향은 ‘객체를 지향하라는 있는 그대로의 말 뜻으로 작성하는 것이다’

클래스 중심설계에서 객체를 위한 코드를 작성하게 되는 것 또한 많은 경험을 통한 레거시 코드들을 작성해보며 이해가 되지 않을까 생각한다.

Reference

조영호님의 객체지향의 사실과 오해



© 2022. by taewoo

Powered by taewoo