Notice
Recent Posts
Recent Comments
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

DeFacto-Standard IT

일반화 관계, 다형성 본문

Design Pattern/References

일반화 관계, 다형성

defacto standard 2017. 10. 28. 18:08

일반화 관계

일반화는 또 다른 캡슐화

객체지향 개념에서 가장 많이 오해하고 오용되는 것이 일반화 관계다. 일반화 관계는 객체지향 프로그래밍 광점에서는 상속 관계라 한다. 따라서 속성이나 기능의 재사용만 강조해서 사용하는 경우가 많다. 이는 일반화 관계를 극히 한정되게 바라보는 시각이다.


일반화를 하면 각각의 종류에 신경쓰지않고 전체를 다룰 수 있는 수단을 얻을 수 있다.

장바구니에 있는 과일 가격의 총합을 구하는 함수는 다음과 같이 작성할 수 있으며, 이미 추상화 개념에서 본 적이 있다.

가격 총합 = 0
while(장바구니에 과일이 있다) {
switch(과일 종류)
case 사과:
가격 총합 = 가격 총합 + 사과 가격
case 배:
가격 총합 = 가격 총합 + 배 가격
case 바나나:
가격 총합 = 가격 총합 + 바나나 가격
case 오렌지:
가격 총합 = 가격 총합 + 오렌지 가격
}

위 코드는 '키위'와 같은 새로운 과일의 종류가 추가되었을 때 '키위'를 처리할 수 있도록 case문을 추가할 수 있어야 한다.

가격 총합 = 0
while(장바구니에 과일이 있다) {
switch(과일 종류)
case 사과:
가격 총합 = 가격 총합 + 사과 가격
case 배:
가격 총합 = 가격 총합 + 배 가격
case 바나나:
가격 총합 = 가격 총합 + 바나나 가격
case 오렌지:
가격 총합 = 가격 총합 + 오렌지 가격
case 키위:
가격 총합 = 가격 총합 + 키위
}

이렇게 코드를 작성하면 새로운 과일의 종류가 나타날 때마다 항상 코드를 수정해야 하므로 변경사항에 유연성 있게 대처하지 못한다.

따라서 우리가 작성해야 할 실제 코드는 다음과 같이 새로운 과일의 종류가 추가되더라도 코드를 수정할 필요가 없도록 바꿔야 한다.

int computeTotalPrice(LinkedList<Fruit> f) {
int total = 0;
Iterator<Fruit> itr = f.iterator();

while (itr.hasNext()) {
Fruit curFruit = itr.next();
total = total + curFruit.calculatePrice();
}

return total;
}


여기에서 calculatePrice 메서드는 실제 과일 객체의 종류에 따라 다르게 실행된다. 이는 다형성에 따른 것이다.


지금까지 살펴본 일반화 관계는 외부 세계에 자식 클래스를 캡슐화(또는 은닉)하는 개념으로 볼 수 있으며, 이때 캡슐화 개념은 한 클래스 안에 있는 속성 및 연산들의 캡슐화에 한정되지 않고 일반화 관계를 통해 클래스 자체를 캡슐화하는 것으로 확장된다.

이러한 서브 클래스 캡슐화는 외부 클라이언트가 개별적인 클래스들과 무관하게 프로그래밍을 할 수 있게 한다.


다음 그림은 '사람'이 자동차를 사용(운전)하는 상황을 묘사한 것이다.

 

'사람'클래스 관점에서는 구체적인 자동차의 종류가 숨겨져 있다. 대리 운전을 예로 들면, 대리 운전자가 자동차의 종류에 따라 운전에 영향을 받지는 않는다. 이와 같이 새로운 자동차를 운전해야 하는 경우도 '사람'클래스는 영향을 받지 않는다.


일반화 관계와 위임

많은 사람이 일반화 관계를 속성이나 기능의 상속, 즉 재사용을 위해 존재한다고 생각하지만 아니다.

ArrayList 클래스를 상속받아 Stack 클래스를 만들었다. 프로그래머의 의도는 ArrayList 클래스에 정의된 isEmpty, size, add, remove 메서드를 자신이 구현하지 않고 그대로 사용하길 원했을 것이다. 기능의 재사용이라는 측면으로만 보면 결론은 성공적이다.

 

그러나 ArrayList 클래스에 정의된 스택과 전혀 관련 없는 수많은 연산이나 속성도 같이 상속(extends)받게 된다.

실제로도 이런 불필요한 속성이나 연산은 도움이 되기보다는 물려받고 싶지 않은 빚이 될 가능성이 크다.


이런 경우 다음과 같이 push나 pop 메서드를 통하지 않고 스태그이 자료구조에 직접 접근할 수 있게 만들 수도 있으나, 스택의 무결성 조건인 LIFO에 위배된다.

class MyStack<String> extends ArrayList<String>{
public void push(String element) {
add(element);
}

public String pop() {
return remove(size() - 1);
}
}


기본적으로 일반화 관계는 'is a kind of 관계'가 성립되어야 한다.

두 클래스 사이에 'is a kind of 관계'가 성립됨을 확인할 수 있는 가장 간단한 방법은 다음 문장이 참인지를 판단하는 것.

Stack "is a kind of" ArrayList.

 

즉, 배열 목록(ArrayList 클래스) 대신에 스택(Stack 클래스)을 사용할 수 있는지를 평가하면 된다.

프로그램에서 배열 목록 대신 스택을 사용할 수 없으므로 위 명제는 거짓이다.

두 자식 클래스 사이에 'is a kind of관계'가 성립되지 않을 때 상속을 사용하면 불필요한 속성이나 연산(빚)도 물려받게 된다.


어떤 클래스의 일부 기능만 재사용하고 싶은 경우에는 위임을 사용하면 된다.

위임은 자신이 직접 기능을 실행하지 않고 다른 클래스의 객체가 기능을 실행하도록 위임하는 것이다. 따라서 일반화 관계는 클래스 사이의 관계지만 위임은 객체 사이의 관계다.


다음은 위임을 사용해 일반화(상속)을 대신하는 과정.

1. 자식 클래스에 부모 클래스의 인스턴스를 참조하는 속성을 만든다. 이 속성 필드를 this로 초기화.

2. 서브 클래스에 정의된 각 메서드에 1번에서 만든 위임 속성 필드를 참조하도록 변경

3. 서브 클래스에서 일반화 관계 선언을 제거하고 위임 속성 필드에 슈퍼 클래스의 객체를 생성해 대입

4. 서브 클래스에서 사용된 슈퍼 클래스의 메서드에도 위임 메서드를 추가.

5. 컴파일하고 잘 동작하는지 확인


다음은 위 코드 MyStack 클래스를 위임을 사용하는 코드로 변환하는 데 위 절차를 적용한 코드이다.

class MyStack<String> extends ArrayList<String>{
private ArrayList<String> arList = this;

public void push(String element) {
add(element);
}

public String pop() {
return remove(size() - 1);
}
}

우선 MyStack 클래스에 Arrayist 클래스의 인스턴스를 참조하는 속성인 arList 객체를 만든 후 이 속성 필드를 this로 초기화

class MyStack<String> extends ArrayList<String> {
private ArrayList<String> arList = this;

public void push(String element) {
arList.add(element);
}

public String pop() {
return arList.remove(arList.size());
}
}

MyStack 클래스의 push와 pop메서드에 arList 객체를 참조하도록 변경

class MyStack<String> extends ArrayList<String> {
private ArrayList<String> arList = new ArrayList<string>();

public void push(String element) {
arList.add(element);
}

public String pop() {
return arList.remove(arList.size());
}
}

ArrayList와 MyStack 클래스 사이의 일반화를 제거하고 arList릉 ArrayList 객체로 생성해 초기화.

class MyStack<String> extends ArrayList<String> {
private ArrayList<String> arList = new ArrayList<string>();

public void push(String element) {
arList.add(element);
}

public String pop() {
return arList.remove(arList.size());
}

public boolean isEmpty() {
return arList.isEmpty();
}

public int size() {
return arList.size();
}
}

MyStack 클래스에서 사용된 arList 객체의 isEmpty와 size 메서드에 위임 메서드를 서브 클래스에 추가.


*Vector 클래스를 사용(위임)해 Stack 클래스를 구현

class VectorStack<String> extends ArrayList<String> {
private Vector<String> v = new Vector<String>();

public void push(String element) {
v.add(element);
}

public String pop() {
return v.remove(v.size() - 1);
}

public boolean isEmpty() {
return v.isEmpty();
}

public int size() {
return v.size();
}
}


집합론 관점으로 본 일반화 관계

일반화 관계는 수학에서 배우는 집합론과 매우 밀접한 관계가 있다. 집합론적인 관점에서 일반화 관계를 해석하는 데는 어려운 수학 지식이 필요 없다.


부모 클래스 A는 전체 집합 A에 해당하고 그 부분 집합 A1, A2, A3는 각각 A의 자식 클래스에 해당한다. 이때 다음 관계가 성립해야 한다.

- A 는 A1, A2, A3의 합집합과 같다

- A1, A2, A3의 교집합은 공집합이다.

또한 다음과 같은 제약조건도 존재한다.

일반화 관계에서의 제약 조건 - 자식 클래스간 변경 불가


위 제약 조건을 일반화 관계에 적용하려면 제약 조건{disjoint, complete}를 사용한다.

 

제약 {disjoint}는 자식 클래스 객체가 동시에 두 클래스에 속할 수 없다는 의미.

제약 {complete}는 자식 클래스의 객체에 해당하는 부모 클래스의 객체와 부모 클래스의 객체에 해당하는 자식 클래스의 객체가 하나만 존재한다는 의미.


또한 집합론 과점에서 일반화 관계를 만들면 연관 관계를 단순하게 할 수 있다.

어떤 웹 쇼핑몰에서 구매액을 기준으로 회원을 VIP 회원과 일반 회원(Ordinary Member 클래스)으로 분류했다고 하자.

VIP 회원과 일반 회원을 각각 자식 클래스로 생각해 물건(Item 클래스)과 연관 관계를 맺게 할 수 있지만 기본적으로 회원은 회원 등급과 관계 없이 물건을 구매할 수 있다.

 

즉, 물건 클래스와의 연관 관계는 모든 자식 클래스에서 공통적으로 갖는 연관 관계이므로 다음과 같이 부모 클래스인 회원 클래스로 연관 관계를 이동하는 것이 클래스 다이어그램을 간결하게 만든다.

집합론을 통한 연관 관계의 일반화


집합론적인 관점에서 일반화는 상호 배타적인 부분 집합으로 나누는 과정으로 간주할 수 있으며, 이를 이용해 상호 배타적인 특성이 요구되는 상황에 일반화 관계를 적용할 수 있다.

 

학생은 '놀기'와 '공부' 중 어느 한 상태에만 있을 수 있다. '공부'상태라면 책만 볼 수 있고, '놀기'상태라면 장난감만 다룰 수 있다.

이런 경우에는 전형적으로 상호 배타적인 수 당태를 모델링해야 하며 이때 일반화 관계가 유용하게 사용된다.

일반화 관계를 이용한 상호 배타적 관계 모델링

 


*특수화는 일반화의 역관계, 즉 부모 클래스에서 자식 클래스를 추출하는 과정이다.

특수화가 필요한 경우는 어떤 속성이나 연관 관계가 특정 자식 클래스에서만 관련이 있고 다른 자식 클래스에서는 관련이 없는 경우다.

이 관점에서 회원이 왜 등급으로 구분되어야 하는지 알 수가 없는데, 이는 VIP 회원과 일반 회원의 차이가 없기 때문이다.

반대로 말해 VIP 회원만 할인 쿠폰을 받을 수 있다고 하면, 이는 회원을 VIP 회원과 일반 회원으로 특수화하는 이유가 된다.

 

집합을 여러 기준에서 분류할 수도 있다. 그림 2.9는 집합 A를 A1, A2, A3로 분류했다.

그러나 경우에 따라서는 A를 A1, A2, A3뿐만 아니라 B1, B2로 분류해야 할 수도 있다.

혹은 웹 쇼핑몰의 회원 등급을 구매액에 따라 VIP 회원과 일반 회원으로 분류했지만 쇼핑몰과 동일한 지역에 사는 지역 주민(Local 클래스)이냐, 아니냐(Non Local 클래스)에 따라서도 분류할 수 있다.

UML에서는 이러한 분류 기준을 변별자라 하며 일반화 관계를 표시하는 선 옆에 변별자 정보를 표시한다.


그런데 여러 개의 변별자를 사용해 집함을 부분 집합으로 나눌 때 고려해야 할 사항이 있다.

회원을 '구매액'과 '지역 주민'이라는 변별자에 따라 분류하면 회원의 한 인스턴스는 VIP Member와 Ordinary Member 중 하나의 자식 클래스에 속하는 동시에 Local과 Non Local 중 하나의 자식 클래스에도 속하게 된다는 사실이다.

이와 같이 한 인스턴스가 동시에 여러 클래스에 속할 수 있는 것을 다중 분류라 하며

'<<다중>>'이라는 스테레오 타입을 사용해 표현한다.


*동적 분류는 한 클래스의 인스턴스가 다른 클래스의 인스턴스로 할당될 수 있음을 의미하며 '<<동적>>'이라는 스테레오 타입을 사용해 표현한다.


일반적으로 각 변별자에 따른 일반화 관계가 완전하게 독립적일 때는 별다른 문제가 없다.

하지만 요구사항의 변경이나 새로운 요구사항의 추가에 따라 두 일반화 관계가 더 이상 독립적이지 않는 상황도 고려해야 한다.

 

"VIP 회원에게만 할인 쿠폰을 지급한다"는 말의 의미는 회원이 지역 주민인지의 여부에 관계 없이 할인 쿠폰을 지급한다는 뜻이다.

그런데 "일반 회원이지만 지역 주민에게는 경품을 제공"한다는 새로운 요구사항을 추가하면 난처해진다.

연관 관계를 위한 속성을 Ordinary Member 클래스에 두면 비지역민에게도 경품이 제공될 수 있고 Local 클래스에 두면 VIP 회원에게도 경품이 제공되기 때문이다. 이는 의도한 바와 다른 잘못된 모델링이다.

변별자와 다중분류

 

이를 처리하는 한 가지 방법으로 모든 분류 가능한 조합에 대응하는 클래스를 만드는 방법이 있다.

Member 클래스의 자식 클래스로 다음과 같은 4개의 클래스를 만들 수 있다


다형성

서로 다른 클래스의 객체가 같은 메시지를 받았을 때 각자의 방식으로 동작하는 능력.

일반화 관계와 함께 자식 클래스를 개별적으로 다룰 필요 없이 한 번에 처리할 수 있게 하는 수단을 제공.


고양이, 강아지, 앵무새 모두 'talk'라는 연산을 실행하지만 행동 방식은 모두 다르다.

public abstract class Pet {
public abstract void talk();
}
public class Cat extends Pat {
public void talk() {
System.out.println("야옹");
}
}
public class Parrot extends Pat {
public void talk(){
System.out.println("안녀엉");
}
}

그리고 Parrot 클래스(자식)의 객체를 Pet 클래스(부모) 타입으로 지정한다.

Pet p = new Parrot();


p에 바인딩된 객체 tak 메서드의 메시지를 전달하면(p.talk()) 현재 p가 실제 참조하는 객체에 따라 실행되는 talk 메서드의 동작이 달라진다. 위 코드의 경우에는 p가 Parrot 클래스의 객체를 참조하기 때문에 Parrot 클래스에 정의된 talk 메서드가 실행된다. 이를 다형성이라 한다.


다음 두 코드의 비교를 통해 다형성이 얼마나 코드를 단순하게 만들며 변경에 유연하게 대처할 수 있는지 보여준다.

Class Dog {
public void brak() {...}
}

Class Cat {
public void meow() {...}
}

Class Parrot {
public void sing() {...}
}

public class Main() {
public static void main(String[] args) {
Dog d = new Dog();
Cat c = new Cat();
Parrot p = new Parrot();

d.bark();
c.meow();
p.sing();
}
}
abstract class Pet {
public abstract void talk();
}

Class Dog extends Pet {
public void talk() {...}
}

Class Cat extends Pet {
public void talk() {...}
}

Class Parrot extends Pet {
public void talk() {...}
}

public class Main() {
public static void main(String[] args) {
Pet[] p = { new Cat(), new Dog(), new Parrot() };
for (int i = 0, i < 3; i++)
p.talk();
}
}

다형성을 사용하지 않는 경우는 클래스별로 다르게 처리해주어야 하지만 다형성을 사용하는 경우에는 구체적으로 현재 어떤 클래스 객체가 참조되는지와 무관하게 프로그래밍을 할 수 있다. 따라서 새로운 자식 클래스가 추가되도 코드는 영향을 받지 않는다.

이것이 가능한 이유는 일반화 관계에 있을 때 부모 클래스의 참조 변수가 자식 클래스의 객체를 참조할 수 있기 때문이다.

단, 부모 클래스의 참조 변수가 접근할 수 있는 것은 부모 클래스가 물려준 변수와 메서드 뿐이다.

 


*다음 코드의 문제점은?

public abstract class Animal {
public abstract void printName();
}

public class Cat extends Animal {
public vid printName() {
System.out.println("고양이");
}
}

public class Dog extends Animal {
public vid printName() {
System.out.println("개");
}
}

public class Snake extends Animal {
public vid printName() {
System.out.println("뱀");
}
}

public class Lion extends Animal {
public vid printName() {
System.out.println("사자");
}

public void ride() {
System.out.println("사자를 탄다");
}
}

public class Main {
public static void main(String[] args) {
Animal[] animals = { new Cat(), new Dog(), new Lion(), new Snake() };
((Cat)animals[0]).printName();
((Cat)animals[1]).printName(); //에러
((Snake)animals[2]).printName(); //에러
animals[2].ride(); //에러
animals[3].printName();
}
}


에러 구문 중 첫 두개의 구문에서는 형제 상속 관계에서 형제 클래스 사이에 형변환을 할 수 없는데도 강제로 형변환을 하여 에러가 발생.

첫 번째 구문에서는 Dog 객체를 Cat 객체로 강제 형변환했기 때문에 에러가 발생.

두 번째 구문에서는 Lion 객체를 Snake 객체로 강제 형변환했기 때문에 에러가 발생.

마지막 구문에서는 상속 관계에 있을 때 부모 클래스의 참조 변수(animal[2])가 자식 클래스의 객체(Lion 객체)를 참조할 수 있지만 이때 부모 클래스의 참조 변수가 접근할 수 있는 것은 부모 클래스가 물려준 변수와 메서드 뿐이다.

이 경우 animal[2]로 참조할 수 있는 것은 printName 메서드 뿐이지만 ride 메서드를 호출해 사용하므로 에러가 발생.

ride 메서드를 사용하려면 Lion 객체로 형변환을 해야 한다.


위 코드를 에러없이 처리하려면 Main 클래스를 다음과 같이 수정해야 한다.

public class Main {
public static void main(String[] args) {
Animal[] animals = { new Cat(), new Dog(), new Lion(), new Snake() };
((Cat)animals[0]).printName();
((Dog)animals[1]).printName(); // 또는 animals[1].printName();
animals[2].printName();
((Lion))animals[2].ride();
animals[3].printName();
}
}


 

*다음 프로그램의 실행 결과를 통해 상속과 정적 메서드의 관계를 유추.

public class A {
public static void doIt() {
System.out.println("DoIt : A class method");
}

public void doThat() {
System.out.println("Dothat : A class method");
}
}
public class A1 extends A {
public static void doIt() {
System.out.println("DoIt : A1 class method");
}

public void doThat() {
System.out.println("Dothat : A1 class method");
}
}
public class Main {
public static void main(String[] args) {
A a1 = new A1();
A1 a2 = new A1();

a1.doIt();
a1.doThat();
a2.doIt();
}
}



정적 메서드는 상속을 통해 오버라이드 되지 않음.

정적 메서드를 실행할 때 동적 바인딩을 실행하는 것이 아니고 컴파일할 때 결정된 객체의 타입에 따라 실행된 메서드가 결정된다는 의미.

 

 

'Design Pattern > References' 카테고리의 다른 글

SOLID (1) - 단일 책임 원칙, SRP  (0) 2017.10.28
피터 코드의 상속 규칙  (0) 2017.10.28
추상화, 캡슐화  (0) 2017.10.28
인터페이스, 실체화 관계  (0) 2017.10.28
의존관계  (0) 2017.10.28
Comments