본문 바로가기
Concepts/객체지향

객체지향의 사실과 오해 - 개념, 명세, 구현

by ocwokocw 2021. 8. 29.

- 이 글은 조영호의 객체지향의 사실과 오해를 기반으로 작성되었습니다. (가능하면 꼭 읽어보는것을 추천드립니다.)

 


- 개념, 명세, 구현

UML 관련 서적중 마틴 파울러의 UML Distilled 2판 에서는 객체 지향 설계를 개념 관점, 명세 관점, 구현 관점으로 구분한다.

 

우선 개념 관점은 도메인안에 존재하는 개념과 개념간의 관계를 나타낸것이다. 도메인은 앞에서 말했듯이 사용자가 관심을 가지는 분야를 의미한다. 이 관점에서는 실제 도메인의 규칙을 최대한 유사하게 반영하는것이 핵심이다.

 

명세 관점부터는 소프트웨어의 세계로 넘어온다. 명세 관점에서는 What/Who 사이클에서 What 에 해당하는 무엇에 초점을 맞추어서 객체의 인터페이스에 해당하는 관점에 집중한다.

 

구현 관점은 우리와 같은 프로그래머가 가장 익숙한 관점이다. 어떻게에 초점을 맞추어서 인터페이스를 구현하여 실제로 동작하는 코드를 만든다.

 

객체지향의 사실과 오해에서는 UML Distilled 2판을 언급하면서 명세/구현을 나누는것을 중요하다고 강조하고있다. 하지만 UML Distilled 3판 에서 마틴 파울러는 명세와 구현을 나누었던 2판과 관점이 바뀌었다고 말하고 있는데, 이전판(2판)까지는 자신이 명세/구현을 나누었지만 더 이상 이런 구분은 무의미하다고 말하고 있다. 하지만 그러면서도 자신은 구현보다 명세에 집중하는 경향이 있다고 말한다.

 

2판과 3판 중 어느것이 옳은것인가에 너무 집착하지 말자. UML Distilled 를 읽다보면 마틴 파울러는 UML 자체에 표기법과 엄격한 명세보다는 이해관계자가 이해가 가능한 선에서 의미있는 의사소통을 강조하고 있다. 방법론적인 접근 보다는 실용성에 중심을 둔다는 느낌을 많이 받는다. 이런 관점들이 있다는것만 알면 될 것 같다.

 

다시 돌아와서 마치 위의 개념, 명세, 구현을 순서대로 설계해야 한다고 말하는 것 같지만 이는 그런 관점들이 있다는것일 뿐 반드시 순서대로 설계하라고 말하는 것은 아니다. 3 가지 관점을 식별할 수 있도록 드러내게 설계하는것이 중요하다는것을 말한다.


- 커피 전문점 예제

작은 커피전문점 예제를 통해 개념에서 구현 관점까지의 과정을 살펴보자.

 

작은 커피전문점이 있다. 메뉴판에는 아메리카노, 카푸치노, 카라멜 마끼아또, 에스프레소 4 가지 메뉴가 있다. 손님은 메뉴판을 보고 바리스타에게 커피 제조를 요청한다. 바리스타는 주문 받은 커피를 제조한다.

 

우선 도메인의 개념과 관계를 살펴보기전에 도메인의 구성요소를 먼저 판별해보도록 하자.

  • 손님: 커피전문점의 존재이유이다. 손님은 메뉴판을 보고 바리스타에게 커피를 주문하는 객체이다.
  • 메뉴판: 손님은 메뉴판이 있어야 주문을 할 수 있다. 메뉴판에는 항목들이 존재한다. 예제에서는 아메리카노, 카푸치노, 카라멜 마끼아또, 에스프레소 4 가지 커피 메뉴 항목이 존재한다.
  • 메뉴 항목: 메뉴판에 구성된 항목 역시 객체이다.
  • 바리스타: 손님이 요청한 커피를 제조한다.
  • 커피: 해당 메뉴 항목의 주문을 받은 바리스타가 실제로 제조한 커피객체이다.

 

위의 도메인 구성요소와 시나리오를 바탕으로 도메인의 개념과 관계를 나타내면 아래 다이어그램처럼 나타낼 수 있다.

 

 

일자로된 선은 개념들간의 관계(Association)이 있음을 나타낸다. 손님이 메뉴판을 볼 때 손님과 메뉴판은 관계를 갖는다. 또한 메뉴판은 메뉴 항목을 포함하므로 둘의 관계, 손님이 메뉴를 주문할 때 바리스타와 관계를 갖는다. 또한 바리스타는 커피를 제조한다.

 

마름모 안에 색이 채워진 메뉴판과 메뉴 항목은 합성(Composition) 관계이다. 마름모는 둘의 관계에서 다른 하나를 포함하는 객체에 표시해준다. (더 집합이 큰 개념쪽에 표시한다.) 합성 말고 집합(Aggregation) 관계도 있다. 이 둘은 생명주기와 연관이 있는데 메뉴판이 없어질 때 해당 메뉴 항목도 없어지므로 메뉴 항목은 메뉴판의 생명주기와 연관되어 합성이 된다. 서로 생명주기가 연관이 없는데 다른 하나를 포함한다면 이는 집합 관계이다. 다만 도메인 모델링시에는 이런 것을 너무 엄격하게 따질 필요는 없다.


- 협력 메시지 추출

이제 도메인의 개념과 관계를 알았으니 곧바로 이를 클래스로 변환하면 될까? 여러번 강조했지만 메시지를 먼저 식별해야 한다. 메시지를 먼저 식별하고 이를 적절한 객체에 할당해야 한다.

 

예제에서 커피전문점의 목적은 커피를 주문하는것이다. '커피를 주문하라' 는 것이 첫번째로 발생한 메시지이다. 이 메시지를 수신한다는것은 그에 대한 책임이 있다는것이다. 따라서 커피를 주문하는것은 손님의 책임이므로 첫번째 메시지는 손님 객체가 수신해야 한다.

 

메시지를 수신받은 손님은 커피를 주문해야 하는데 메뉴에 대해 모르는 상태이다. 자신이 스스로 처리할 수 없으므로 메뉴에 대해 가장 잘 알고 있는 메뉴판 객체에 '메뉴를 찾아라'는 메시지를 전달한다. 메뉴 항목을 찾으면 커피를 받기 위해서는 커피를 제조할 수 있는 바리스타 객체에게 '커피를 제조하라'는 메시지를 전달해야 한다.

 

바리스타는 커피 객체에게 '생성하라'는 메시지를 전달한다. 커피 객체를 생성할 때에는 정적 Factory 메소드의 메소드를 호출하는 형태일 수도 있지만 new 생성자를 사용할 수도 있을것이다.

 

이런 과정을 협력 다이어그램으로 나타내면 아래와 같이 나타낼 수 있다.

 

 

위의 다이어그램에서 표기법은 아래와 같은 형식이다.

 

[Return] := [MessageName][Arguments]

 

이정도면 손님 객체에게 '커피를 주문하라'는 메시지를 전달한 후, 연쇄적으로 발생하는 메시지를 모두 추출한 것 같다. 


- 인터페이스 정리

객체가 어떤 메시지를 수신받는다는것은 그러한 책임이 있다는것이고 이는 곧 공용(public) 인터페이스가 된다고 이야기했었다. 위의 다이어그램에서 각 객체가 받은 메시지를 정리하면 아래와 같다.

 

  • 손님: 커피를 주문하라(메뉴 이름)
  • 메뉴판: 메뉴 := 메뉴를 찾아라(메뉴 이름)
  • 바리스타: 커피 := 커피를 제조하라(메뉴)
  • 커피: 생성하라

 

이 메시지를 기반으로 공용 인터페이스를 아래와 같은 방식으로 만들어볼 수 있다.

 

public class Customer {

	public void order(String menu) {}
}

.........

public class MenuBoard {

	public MenuItem findMenu(String menuItem) {
		return null;
	}
}

...........

public class Barista {

	public Coffee makeCoffee(MenuItem menu) {
		return null;
	}
}

.........

public class Coffee {

	public Coffee createCoffee(MenuItem coffee) {
		return null;
	}
}

- 구현하기

우선 손님 객체의 '커피를 주문하라'는 메시지부터 구현해보자. 인터페이스로 추출된 메시지는 order(String menu) 이지만 이를 그대로 사용하기에는 무리가 있다. 손님은 메뉴판 객체에게 메뉴를 찾아달라고 부탁하고 바리스타에게 커피를 제조하라고 메시지를 보내야 한다. 다른 객체에게 메시지를 보낸다는 것은 해당 객체를 참조해야 된다는 얘기가 된다.

 

스프링 프레임워크는 DI(의존성 주입)을 지원하여 @Autowired 나 @Inject 를 사용할 수 있어서 order(String menu) 메소드 시그니처를 변경하지 않아도 될것이다.(하지만 Spring 도 생성자에 의한 의존성 주입을 추천하고 있다.) 우리는 의존성 주입을 생성자를 이용하여 손님 객체가 메뉴판과 바리스타를 참조할 수 있도록 한다. 따라서 메소드 시그니처를 변경한다.

 

public class Customer {

	public void order(String menu, MenuBoard menuBoard, Barista barista) {
		
		MenuItem menuItem = menuBoard.findMenu(menu);
		Coffee coffee = barista.makeCoffee(menuItem);
		//Do somthing
	}
}

 

고객 객체 구현에서 알 수 있듯이 인터페이스를 추출했다고 해서 그 메시지가 구현단계에서도 그대로 유지되는것은 아니다. 인터페이스를 아무리 완벽하게 추출하더라도 구현 단계에서는 변할 수 있으므로 인터페이스를 적절하게 추출했다고 생각하면 구현을 해보면서 변경되는 부분은 없는지 확인해보아야 한다.

 

다음으로 메뉴판을 구현해본다.

 

public class MenuItem {

	private String menuName;
	private long price;

..............

public class MenuBoard {

	private List<MenuItem> menuItems;
	
	public MenuBoard() {
		super();
		
		menuItems = new ArrayList<>();
		
		menuItems.add(new MenuItem("Americano", 1000));
		menuItems.add(new MenuItem("Cappuccino", 1100));
		menuItems.add(new MenuItem("CaramelMacchiato)", 1200));
		menuItems.add(new MenuItem("Espresso", 1300));
	}

	public Optional<MenuItem> findMenu(String menuItemName) {
		
		return menuItems.stream()
			.filter(menuItem -> menuItem.getMenuName().equals(menuItemName))
			.findAny();
	}
}

 

메뉴항목인 MenuItem 은 메뉴이름과 가격을 갖고 있다. 메뉴판인 MenuBoard 에서는 생성시 4 가지 메뉴를 추가한다. findMenu 에서는 메뉴명에 해당하는 항목을 찾는데, 고객이 메뉴판에 없는 메뉴를 주문할 수도 있으므로, 존재하지 않을 수도 있다고 명시적으로 알려주도록 반환형을 MenuItem 이 아닌 Optional<MenuItem>로 변경하였다.

 

public class Barista {

	public Coffee makeCoffee(MenuItem menuItem) {
		
		return new Coffee(menuItem);
	}
}

.............

public class Coffee {

	private String name;
	private long price;
	
	public Coffee(MenuItem menuItem) {
		super();
		
		this.name = menuItem.getMenuName();
		this.price = menuItem.getPrice();
	}
}

 

바리스타와 커피는 각각 위와 같이 구현한다.

 

아래는 코드를 기반으로 클래스 다이어그램을 그려본것이다. UML 적으로 엄격하거나 완벽하지 않을 수도 있지만 핵심적인 요소는 모두 들어있다. 

 

 

간략히 설명하자면 Customer 가 MenuBoard 나 Barista 에게 요청할 때, 해당 객체 참조시 field 로 저장하지 않고 메소드 안에서만 이용하므로 연관(Association)이 아닌 의존(Dependency) 점선 화살표로 표현한다. 만약 Customer 클래스의 order 메소드에 해당 객체를 인자로 넘기지 않고 속성으로 참조하고 있었다면 의존이 아닌 연관이 되었을것이다.

 

구현하기 과정을 따라가보면서 느낀점이 있을것이다. 도메인 모델링을 기반으로 추상화도 잘하고 인터페이스도 잘 추출했다고 생각했지만 많은 것이 변경되었다. 인터페이스 추출에서 모든것을 완벽하게 추출할 수는 없다. 어느정도 추출했다면 구현에 곧바로 돌입하다보면 자연스럽게 테스트-주도 설계가 될 것이다.

댓글