나를 기록하다
article thumbnail
반응형

객체지향의 사실과 오해(출처: yes24)

코드와 모델을 밀접하게 연관시키는 것은 코드에 의미를 부여하고 모델을 적절하게 한다.
 - 에릭 에반스

객체지향 설계 안에 존재하는 세 가지 상호 연관된 관점

개념 관점(Conceptual Perspective)

  • 설계는 도메인 안에 존재하는 개념과 개념들 사이의 관계를 표현
  • 도메인은 사용자들이 관심을 가지고 있는 특정 분야나 주제를 말하며 소프트웨어는 도메인에 존재하는 문제를 해결하기 위해 개발
  • 사용자가 도메인을 바라보는 관점을 반영
  • 실제 도메인의 규칙과 제약을 최대한 유사하게 반영하는 것이 핵심

명세 관점(Specification Perspective)

  • 사용자의 영역인 도메인의 개념이 아니라 실제로 개발자의 영역인 소프트웨어 안에서 살아 숨쉬는 객체들의 책임에 초점
  • 객체의 인터페이스를 바라보게 됨
  • 프로그래머는 객체가 협력을 위해 무엇을 할 수 있는가에 초점
  • 인터페이스와 구현을 분리하는 것은 훌륭한 객체지향 설계를 낳는 가장 기본적인 원칙

구현 관점(Implementation Perspective)

  • 실제 작업을 수행하는 코드와 연관
  • 객체들이 책임을 수행하는 데 필요한 동작하는 코드를 작성하는 것에 초점
  • 객체의 책임을 어떻게 수행할 것인가에 초점을 맞추며 인터페이스를 구현하는 데 필요한 속성과 메서드를 클래스에 추가

개념 관점, 명세 관점, 구현 관점은 동일한 클래스를 세 가지 다른 방향에서 바라보는 것을 의미한다.

클래스는 세 가지 관점을 통해 설계와 관련된 다양한 측면을 드러낼 수 있다.

  1. 클래스가 은유하는 개념은 도메인 관점을 반영
  2. 클래스의 공용 인터페이스는 명세 관점을 반영
  3. 클래스의 속성과 메서드는 구현 관점을 반영

협력 안에서 메시지를 선택하고 메시지를 수신할 객체를 선택하는 것은 객체의 인터페이스, 즉 명세 관점에서 객체를 바라보는 것이다.


커피 전문점 도메인

커피 주문

  • 예제의 목적: 커피 전문점에서 커피를 주문하는 과정을 객체들의 협력 관계로 구현

[예제] 커피 제조하기

커피 전문점에서는 아메리카노, 카푸치노, 카라멜 마키야또, 에스프레소의 네 가지 커피를 판매하고 있다. 판매하는 커피도 몇 종류 되지 않고 내부도 그리 넓지 않지만 하늘색 페인트로 칠해진 벽과 코를 간지럽히는 고소한 커피 향이 어우러져 전체적으로 아늑하고 편안한 분위기를 풍긴다. 조촐한 가게 내부를 둘러보니 하얀 색의 테이블들이 커피 향으로 가득찬 공간을 채우듯 옹기종기 놓여져 있고, 그 위에는 커피의 이름과 가격이 적힌 작은 메뉴판이 비치돼 있다.

--------------------------------
                    M E N U                    
아메리카노                          1,500원
카푸치노                             2,000원
카라멜 마키야또                  2,500원
에스프레소                         2,500원
--------------------------------

손님이 테이블에 앉아 메뉴판을 잠시 훑어본 후 커피를 주문한다. 이제 주문받은 커피를 제조하는 것은 바리스타의 몫이다.
커피 전문점은 작고, 단순하고, 고려해야 할 사항도 거의 없는 아주 간단한 도메인이다. 우리의 최종 목표는 손님이 커피를 주문하는 시간을 컴퓨터 안에 재구성하는 것이다. 물론 객체를 이용해서 말이다.

커피 전문점이라는 세상

  • 객체지향의 관점에서 메뉴판은 하나의 객체, 메뉴 항목들도 객체
  • 손님은 메뉴판을 보고 바리스타에게 원하는 커피를 주문
    → 손님 객체는 메뉴판 객체 안에 적힌 메뉴 항목 객체들 중에서 자신이 원하는 메뉴 항목 객체 하나를 선택해 바리스타 객체에게 전달
  • 바리스타는 주문을 받은 메뉴에 따라 적절한 커피를 제조(커피의 종류: 아메리카노, 카푸치노, 카라멜 마키야또, 에스프레소)
    • 바리스타는 자율적으로 커피를 제조하는 객체
    • 바리스타가 제조하는 커피 역시 메뉴판, 메뉴 항목, 바리스타와 구별되는 자신만의 경계를 가지므로 객체로 볼 수 있음
  • 객체지향의 관점에서 커피 전문점이라는 도메인은 손님 객체, 메뉴 항목 객체, 메늎나 객체, 바리스타 객체로 구성된 작은 세상

객체로 구성된 커피 전문점 세상

객체들간의 관계
  • 손님은 메뉴판에서 주문할 커피를 선택할 수 있어야 함.
  • 손님은 어떤 식으로든 메뉴판을 알아야 한다. → 두 객체 사이에 관계 존재
  • 손님은 바리스타에게 주문을 해야 함 → 손님과 바리스타 사이에 관계 존재
  • 바리스타는 커피를 제조하는 사람 → 커피와 관계를 맺음
우리가 할 수 있는 일은 동적인 객체를 정적인 타입으로 추상화해서 복잡성을 낮추는 것이다.
타입은 분류를 위해 사용된다.
상태와 무관하게 동일하게 행동할 수 있는 객체들은 동일한 타입으로 분류 가능하다.
객체와 인스턴스
  • 손님 객체 - '손님 타입'의 인스턴스
  • 바리스타 객체 - '바리스타 타입'의 인스턴스
  • 아메리카노 커피, 에스프레소 커피, 카라멜 마키야또 커피, 카푸치노 커피 - '커피 타입'의 인스턴스
  • 메뉴판 객체
    • '메뉴판 타입'의 인스턴스
    • 아메리카노, 에스프레소, 카라멜 마키야또, 카푸치노 네 개의 메뉴 항목 객체 포함
    • 네 개의 메뉴 항목 객체 모두 동일한 '메뉴 항목 타입' 의 인스턴스로 모델링 가능
포함 관계 - 메뉴판 타입과 메뉴 항목 타입
  • 하나의 메뉴판 객체는 다수의 메뉴 항목 객체로 구성
  • 메뉴판과 메뉴 항목 객체는 하나의 단위로 움직임
  • 메뉴 항목이 메뉴판에 포함된다.

메뉴판 타입과 메뉴 항목 타입 간의 포함 관계

  • 속이 찬 마름모는 포함(containment) 관계 또는 합성(composition) 관계를 나타내는 것
  • 4는 메뉴판에 포함되는 메뉴 항목이 4개라는 것을 의미
연관 관계 - 손님 타입과 메뉴판 타입
  • 손님 타입은 메뉴판 타입을 알고 있어야 원하는 커피 선택 가능
  • 한 타입의 인스턴스가 다른 타입의 인스턴스를 포함하지는 않지만 서로 알고 있어야 할 경우 이를 연관(association) 관계라고 함

손님과 메뉴판 사이의 연관 관계

도메인 모델 - 커피 전문점

커피 전문점을 구성하는 타입들

  • 실제로 도메인 모델을 작성하는 단계에서 어떤 관계가 포함 관계이고 어떤 관계가 연관 관계인지는 중요하지 않다.
  • 초점은 어떤 타입이 도메인을 구성하느냐와 타입들 사이에 어떤 관계가 존재하는지를 파악함으로써 도메인을 이해하는 것이다.

설계하고 구현하기

커피를 주문하기 위한 협력 찾기

  • 객체지향 설계의 첫 번째 목표는 훌륭한 객체를 설계하는 것이 아닌 훌륭한 협력을 설계하는 것.
  • 훌륭한 객체는 훌륭한 협력을 설계할 때만 얻을 수 있다.
  • 협력 설계 시 객체가 메시지를 선택하는 것이 아닌 메시지가 객체를 선택하게 해야 함
  • 메시지를 먼저 선택하고 그 후에 메시지를 수신하기에 적절한 객체를 선택해야 한다
  • 메시지를 수신할 객체 - 메시지를 처리할 책임을 맡게 됨
  • 객체가 수신하는 메시지 - 객체가 외부에 제공하는 공용 인터페이스에 포함
현재 설계하고 있는 협력은 커피를 주문하는 것
  • 첫 번째 메시지는 '커피를 주문하라'일 것

협력을 시작하게 하는 첫 번째 메시지

  • 메시지 위에 붙은 화살표는 메시지에 담아 전달될 부가적인 정보인 인자를 의미
  • '아메리카노를 주문하라' → '커피를 주문하라(아메리카노)'와 같이 인자를 포함하는 형식으로 구현
메시지를 처리할 적합한 객체를 선택
  • '커피를 주문하라'라는 메시지를 수신할 객체는? = 어떤 객체가 커피를 주문할 책임을 져야 하는가?
    → 손님
  • 따라서, 메시지를 처리할 객체는 손님 타입의 인스턴스, 손님 객체는 커피를 주문할 책임을 할당 받음

첫 번째 메시지가 손님 객체 선택

손님이 커피를 주문하는 도중에 스스로 할 수 없는 일은?
  • 할당된 책임 수행 중 스스로 할 수 없는 일은 다른 객체에게 이를 요청해야 함
  • 이 요청이 손님 객체에서 외부로 전송되는 메시지를 정의
  • 손님은 메뉴 항목에 대해서 알지 못함 → 메뉴 항목은 메뉴판의 일부
  • 고객은 자신이 선택한 메뉴 항목을 누군가가 제공해 줄 것을 요청 → '메뉴 항목을 찾아라'라는 새로운 메시지

스스로 할 수 없는 일은 다른 객체에게 도움 요청

  • 이 경우 '메뉴 이름'이라는 인자를 포함해 함께 전송
  • '메뉴 항목을 찾아라'라는 메시지를 수신한 객체는 '메뉴 이름'에 대응되는 '메뉴 항목'을 반환
  • 메뉴 항목을 찾을 책임은 메뉴 항목을 가장 잘 알고 있는, 메뉴 항목 객체를 포함하는 메뉴판 객체에게 할당

두 번째 객체를 찾음

메뉴 항목을 얻은 후, 메뉴 항목에 맞는 커피를 제조해달라고 요청
  • 새로운 요청 → 새로운 메시지가 필요하다는 신호
  • 손님은 커피를 제조하는 메시지의 인자로 메뉴 항목을 전달하고 반환값으로 제조된 커피를 받아야 함

커피를 제조하라는 메시지가 바리스타라는 객체 선택

  • 바리스타는 커피를 제조하는 데 필요한 모든 정보와 기술을 구비하고 있다.
  • 아메리카노를 만들기 위한 지식은 바리스타의 상태, 기술은 바리스타의 행동

→ 바리스타는 스스로의 판단과 지식에 따라 행동하는 자율적인 존재

커피 주문을 위한 객체 협력

  • 남은 것은 메시지를 정제하여 각 객체의 인터페이스를 구현 가능할 정도로 상세하게 정제하는 것

 

인터페이스 정리하기

  • 객체가 수신한 메시지가 객체의 인터페이스를 결정한다.
  • 메시지가 객체 선택, 선택된 객체는 메시지를 자신의 인터페이스로 받아들임
  • 각 객체를 협력이라는 문맥에서 떼어내어 수신 가능한 메시지만 추려내면 객체의 인터페이스가 된다.
객체가 어떤 메시지를 수신할 수 있다는 것은?
그 객체의 인터페이스 안에 메시지에 해당하는 오퍼레이션이 존재한다는 것을 의미
  • 손님 객체의 인터페이스 안에는 '커피를 주문하라'라는 오퍼레이션이 포함
  • 메뉴판 객체의 인터페이스는 '메뉴 항목을 찾아라'라는 오퍼레이션 제공
  • 바리스타 객체의 인터페이스는 '커피를 제공하라'라는 오퍼레이션 제공
  • 커피 객체는 '생성하라'라는 오퍼레이션 제공
각 객체들이 수신하는 메시지는 객체의 인터페이스를 구성

손님
메뉴판
바리스타
커피

객체의 타입을 구현하는 일반적인 방법 - 클래스
  • 인터페이스에 포함된 오퍼레이션은 외부에서 접근 가능하도록 public으로 선언돼 있어야 한다.
class Customer {
	public void order(String menuName) {}
}

class MenuItem {
}

class Menu {
	public MenuItem choose(String name) {}
}

class Barista {
	public Coffee makeCoffee(MenuItem menuItem) {}
}

class Coffee {
	public Coffee(MenuItem menuItem) {}
}

 

구현하기

객체 참조를 얻는 방법 - Customer의 order() 메서드의 인자로 Menu와 Barista 객체를 전달받는다.
  • 결과적으로 Customer의 인터페이스를 변경
public class Customer {
	public void order(String menuName, Menu menu, Barista barista) {}
}
  • order() 메서드 구현을 채운다
public class Customer {
	public void order(String menuName, Menu menu, Barista barista) {
		MenuItem menuItem = menu.choose(menuName);
		Coffee coffee = barista.makeCoffee(menuItem);
	}
}
  • 구현 도중에 객체의 인터페이스가 변경될 수 있다.
구현하지 않고 머릿속으로만 구상한 설계는 코드로 구현하는 단계에서 대부분 변경된다. 설계 작업은 구현을 위한 스케치를 작성하는 단계지 구현 그자체일 수는 없다. 중요한 것은 설계가 아니라 코드다. 따라서 협력을 구상하는 단계에 너무 오랜 시간을 쏟지 말고 최대한 빨리 코드를 구현해서 설계에 이상이 없는지, 설계가 구현 가능한지를 판단해야 한다. 코드를 통한 피드백 없이는 깔끔한 설계를 얻을 수 없다.
Menu의 책임
  • Menu는 menuItem에 해당하는 MenuItem을 찾아야 하는 책임이 있다.
  • 책임을 수행하기 위해 Menu가 내부적으로 MenuItem을 관리하고 있어야 한다.
  • Menu가 MenuItem의 목록을 포함하게 하자.
  • Menu의 choose() 메서드는 MenuItem의 목록을 하나씩 검사해가면서 이름이 동일한 MenuItem을 찾아 반환
public class Menu {
	private List<MenuItem> items;

	public Menu(List<MenuItem> items) {
		this.items = items;
	}

	public MenuItem choose(String name) {
		for (MenuItem each : items) {
			if (each.getName().equals(name)) {
				return each;
			}
		}
		return null;
	}
}
MenuItem 목록을 Menu의 속성으로 포함시킨 결정은 클래스를 구현하는 도중에 내려졌다. 객체의 속성은 객체의 내부 구현에 속하기 때문에 캡슐화돼야 한다. 인터페이스는 객체 내부 속성에 대한 어떤 힌트도 제공돼서는 안된다. 이를 위한 가장 좋은 방법은 인터페이스를 정하는 단계에서는 객체가 어떤 속성을 가지는지, 또 그 속성이 어떤 자료 구조로 구현됐는지를 고려하지 않는 것이다. 객체에게 책임을 할당하고 인터페이스를 결정할 때는 가급적 객체 내부의 구현에 대한 어떤 가정도 하지 말아야 한다. 객체가 어떤 책임을 수행해야 하는지를 결정한 후에야 책임을 수행하는 데 필요한 객체의 속성을 결정하라. 이것이 객체의 구현 세부 사항을 객체의 공용 인터페이스에 노출시키지 않고 인터페이스와 구현을 깔끔하게 분리할 수 있는 기본적인 방법이다.
Barista는 MenuItem을 이용해서 커피를 제조
public class Barista {
	public Coffee makeCoffee(MenuItem menuItem) {
		Coffee coffee = new Coffee(menuItem);
		return coffee;
	}
}
Coffee는 자기 자신을 생성하기 위한 생성자 제공
  • 커피 이름과 가격을 속성으로 가지고 생성자 안에서 MenuItem에 요청을 보내 커피 이름과 가격을 얻은 후 Coffee의 속성에 저장
public class Coffee {
	private String name;
	private int price;

	public Coffee(String name, int price) {
		this.name = name;
		this.price = price;
	}
}
MenuItem은 getName()과 cost() 메시지에 응답할 수 있도록 메서드 구현
public class MenuItem {
	private String name;
	private int price;

	public MenuItem(String name, int price) {
		this.name = name;
		this.price = price;
	}
	
	public int cost() {
		return price;
	}
	
	public String getName() {
		return name;
	}
}

커피 전문점을 구현한 최종 클래스 구조


코드와 세 가지 관점

개념 관점
  • 클래스: Customer, Menu, MenuItem, Barista, Coffee
  • 클래스가 도메인 개념의 특성을 최대한 수용하면 변경을 관리하기 쉽고 유지보수성을 향상시킬 수 있다.

[예시] 커피를 제조하는 과정을 변경해야 한다면 어디를 수정해야 하나?

  • 현실 세계에서 커피를 제조하는 사람: 바리스타 → 소프트웨어 안에서도 Barista라는 클래스가 커피를 제조할 것이라고 유추
명세 관점
  • 클래스의 인터페이스를 바라본다.
  • 클래스의 public 메서드는 다른 클래스가 협력할 수 있는 공용 인터페이스를 드러낸다.
  • 공용 인터페이스는 외부의 객체가 해당 객체에 접근할 수 있는 유일한 부분
  • 인터페이스 수정 시 해당 객체와 협력하는 모든 객체에게 영향을 미친다.
  • 최대한 변화에 안정적인 인터페이스를 만들기 위해서는 인터페이스를 통해 구현과 관련된 세부 사항이 드러나지 않게 해야 함
  • 변화에 탄력적인 인터페이스를 만들 수 있는 능력은 객체지향 설계자의 수준을 가늠하는 중요한 척도
구현 관점
  • 클래스의 내부 구현을 바라본다.
  • 클래스의 메서드와 속성은 구현에 속함(공용 인터페이스의 일부가 아님)
  • 메서드의 구현과 속성의 변경은 원칙적으로 외부의 객체에게 영향을 미쳐서는 안된다.
    → 메서드와 속성이 철저하게 클래스 내부로 캡슐화돼야 한다는 것을 의미

 

도메인 개념을 참조하는 이유

메시지를 수신할 객체를 선택하는 방법 - 도메인 개념 중에서 가장 적절한 것을 선택
  • 도메인에 대한 지식을 기반으로 코드의 구조와 의미를 쉽게 유추할 수 있게 함 → 시스템의 유지보수성에 커다란 영향을 미침
설계는 변경을 위해 존재한다
  • 여러 개의 클래스로 기능을 분할
  • 클래스 안에서 인터페이스와 구현을 분리하는 이유
    • 변경 발생 시 코드를 보다 수월하게 수정하기 위함
  • 도메인 개념을 따르면 변화에 쉽게 대응 가능

인터페이스와 구현을 분리하라

명세 관점은 클래스의 안정적인 측면을 드러내야 한다.
구현 관점은 클래스의 불안정한 측면을 드러내야 한다.
클래스를 봤을 때 클래스를 명세 관점과 구현 관점으로 나눠볼 수 있어야 한다.
캡슐화를 위반해서 구현을 인터페이스 밖으로 노출해서도 안되고, 인터페이스와 구현을 명확하게 분리하지 않고 흐릿하게 섞어놓아서도 안 된다. 세 가지 관점 모두에서 클래스를 바라볼 수 있으려면 훌륭한 설계가 뒷받침돼야 한다.
반응형
profile

나를 기록하다

@prao

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...