본문 바로가기
Concepts/UML

UML - 클래스 다이어그램과 일반화

by ocwokocw 2021. 2. 10.

- 이 글은 UML Distilled (마틴 파울러)책을 기반으로 작성하였습니다.

- 일반화(Generalization)

일반화의 가장 전형적인 예제는 사업에 대해 개인 고객과 기업 고객에 관한 예제이다. 이 둘의 공통점은 상위 타입(고객)으로 추출하고, 차이점은 하위 타입에서 표현한다.

 

모델링의 관점에서 고객에 관련된 모든것(연관, 속성, 오퍼레이션)은 기업 고객에 대해서도 성립하는것이다. 소프트웨어 관점에서 일반화는 상속과 관련있다. 상속에서 가장 중요한 원칙은 치환 가능성이다. 치환 가능성이란 만약 고객의 타입으로 참조하여 수행한 오퍼레이션이 있다면, 이 참조하는 인스턴스가 기업 고객이어도 적절하게 동작해야 한다는 의미이다.

 

고객 Customer와 기업 고객 EnterpriseCustomer가 있다고 하자. 고객은 특정상품을 100개 이상 주문하지 못하는 반면 기업 고객은 주문수에 제한이 없다.

 

public class Customer {

	public void orderProducts(Product product, int volumn) {
		
		if(volumn >= 100) {
			System.out.println("This order is blocked.");
			return;
		}
		
		System.out.println("Order has been completed.");
	}
}

public class EnterpriseCustomer extends Customer{

	@Override
	public void orderProducts(Product product, int volumn) {
		
		System.out.println("Order has been completed.");
	}
}

public static void main(String[] args){
	
	Product computer = new Product("computer");
	
	Customer customer = new Customer();
	Customer enterpriseCustomer = new EnterpriseCustomer();

	customer.orderProducts(computer, 100);
	customer.orderProducts(computer, 99);
	enterpriseCustomer.orderProducts(computer, 1000);
	
}

 

main() 함수에서 고객, 기업고객 인스턴스 모두 참조형은 Customer 이고, 주문을 했을 때(orderProducts() 호출) 상세한 일부 로직은 차이가 있지만 오퍼레이션을 수행한다. 이 때 오퍼레이션에 이상이 없으므로 "Customer는 EnterpriseCustomer로 치환 가능하다."고 말할 수 있다.

 

객체지향 5대 원칙 SOLID - 리스코프 치환 원리(LSP)를 알 고 있다면 위의 문구가 익숙할 것이다. 그런데 한 가지 의문점이 있을 수 있다. 그냥 상위 타입을 상속받은 후 상위 타입의 메소드를 재정의 해서 오퍼레이션을 수행하면 무조건 치환가능성을 만족하는게 아닌가? 이에 대한 반례 및 LSP에 대해 더 자세히 알고 싶다면 ocwokocw.tistory.com/32 를 참조하라.


- 하위 타입(Subtyping)과 하위 클래스(SubClass)

상속은 강력하지만 불필요한 짐을 떠안을 수 있다. 자바 초기에 벡터 클래스를 사용하지 않고 가벼운 클래스를 사용하고 싶을 때에도, 벡터 클래스를 대체하는 방법은 하위 클래스를 만드는것뿐이었다. 이렇게 되면 벡터의 불필요한 속성 및 행동을 모두 상속해야 한다.

 

대체 가능한 클래스를 만들 때에는 두 가지를 구분해야 한다.

  • 하위 타입: 상속여부와 상관 없이 상위 타입을 치환할 수 있는 클래스
  • 하위 클래스: 상속을 사용한 클래스

상속을 사용하지 않고 어떻게 상위 타입을 치환할 수 있는가? 표준 디자인 패턴이나 인터페이스 구현과 같은 경우가 있다. 

 

위의 예제를 변경해보자. 위의 예제에서는 고객이라면 기본적으로 구매를 하는 행위를 했다. 만약 구매를 하지 않는 특수한 고객이 이 있어서 더 이상 제품 구매가 기본행위가 아니라고 가정하자. 그렇다면 고객들 중 일부만 구매를 할 것이다. 구매전략을 결정하는 OrderStrategy 인터페이스를 추가하여 고객 및 기업 고객 모두 구매전략을 구현한다.

 

public interface OrderStrategy {

	void orderProducts(Product product, int volumn);
}

public class Customer implements OrderStrategy{

	@Override
	public void orderProducts(Product product, int volumn) {
		
		if(volumn >= 100) {
			System.out.println("This order is blocked.");
			return;
		}
		
		System.out.println("Order has been completed.");
	}
}

public class EnterpriseCustomer implements OrderStrategy{

	@Override
	public void orderProducts(Product product, int volumn) {
		
		System.out.println("Order has been completed.");
	}
}

public static void main(String[] args){
	
	Product computer = new Product("computer");
	
	OrderStrategy customerOrderStrategy = new Customer();
	OrderStrategy enterpriseCustomerOrderStrategy = new EnterpriseCustomer();

	customerOrderStrategy.orderProducts(computer, 100);
	customerOrderStrategy.orderProducts(computer, 99);
	enterpriseCustomerOrderStrategy.orderProducts(computer, 1000);
	
}

 

위의 코드에서 고객과 기업 고객은 서로 상속 관계는 아니지만 둘 다 OrderStrategy 라는 인터페이스를 구현함으로써 orderProducts 행위와 관련해 치환 가능성을 가진다. 상속을 받는 하위 클래스는 아니지만 하위 타입 인것이다.

댓글