gg 카테고리 없음 2013. 4. 16. 16:15

  In software engineering, coupling or dependency is the degree to which each program module relies on each one of the other modules.

http://en.wikipedia.org/wiki/Coupling_%28computer_programming%29


  • entity classes classes are your typical business entities like "person" and "bank account"

  • control classes implement some business logic or other



1.      도입

2.      SRP (단일 책임 원칙)

3.      OCP (개방-폐쇄 원칙)

4.      LSP (리스코프 치환 원칙)

5.      DIP (의존 관계 역전 원칙)

6.      ISP (인터페이스 분리 원칙)

7.      결론

 

 

 

1. 도입

 

객체지향 프로그램을 잘 설계하기 위해서는 몇 가지 원칙을 지켜야 한다. 잘 설계한 시스템은 이해하기 쉽고, 바꾸기 쉽고, 재사용하기도 쉽다. 개발하는데 특별히 어렵지도 않고, 단순하고 간결하며 경제적이다. 잘못된 설계에는 일반적으로 다음과 같은 문제들이 있다.

 

1.     경직성

- 무엇이든 하나를 바꿀 때마다 반드시 다른 것도 바꿔야 하며, 그러고 나면 또 다른 것도 바꿔야 하는 변화의 사슬이 끊이지 않기 때문에 시스템을 변경하기 힘들다.

2.     부서지기 쉬움

- 시스템에서 한 부분을 변경하면 그것과 전혀 상관없는 다른 부분이 작동을 멈춘다.

3.     부동성

- 시스템을 여러 컴포넌트로 분해해서 다른 시스템에 재사용하기 힘들다.

4.     끈끈함(점착성)

- 개발 환경이 배관용 테이프나 풀로 붙인 것처럼 꽉 달라붙은 상태다. 편집 - 컴파일 - 테스트 순환을 한 번 도는 시간이 엄청나게 길다.

5.     쓸데없이 복잡함

- 괜히 머리를 굴려서 짠 코드 구조가 굉장히 많다. 이것들은 대개 지금 당장 하나도 필요 없지만 언젠가는 굉장히 유용할지도 모른다고 기대하며 만들었다.

6.     필요 없는 반복

- 코드를 작성한 프로그래머 이름이 마치 복사붙여넣기같다.

7.     불투명함

- 코드를 만든 의도에 대한 설명을 볼 때 그 설명에 표현이 꼬인다라는 말이 잘 어울린다.

 

이런 문제점들을 없애야 좋은 코드가 된다. 잘못 관리한 의존 관계가 이런 문제점들의 원인인 경우가 많은데 다음 원칙들을 따르면 좋은 코드를 설계할 수 있다.

 

 

2. 단 하나의 책임 원칙(The Single Responsibility Principle, SRP)

 

어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다.’


1
2
3
4
5
6
7
8
9
10
11
12
<font face="'맑은 고딕'" size="2">public class Employee {
    public double calculatePay();
    public double calculateTaxes();
    public void wroteToDisk();
    public void readFromDisk();
    public String createXML();
    public void parseXML(String xml);
    public void displayOnEmployeeReport(PrintStream stream);
    public void displayOnPatrollReport(PrintStream stream);
    public void diaplayOnTaxReport(PrintStream stream);
}
</font>


 


위 클래스는 너무 많은 것을 안다. 부서지기 쉽다는 문제점을 가지고 있다. 이 설계는 결합도가 너무 높다. 모든 개념을 각기 다른 클래스로 분리하여 클래스마다 변경해야 하는 이유가 오직 하나만 있도록 만드는 것이 바람직하다. Employee 클래스는 세금과 임금만 다루고, XML 관련 클래스는 Employee 인스턴스를 XML로 바꾸거나 XML에서 읽어 들인다. EmployeeDatabase 클래스는 Employee 인스턴스를 데이터베이스에 저장하거나 읽어 들이는 역할을 담당하고, 보고서 정류마다 클래스를 하나씩 만들면 좋을 것이다.




다음과 같이 바꾸어 주는 것이 좋다.




UML다이어그램을 보면 이 원칙을 지키지 않는 예를 무척 발견하기 쉽다. 둘 이상의 주제 영역에 의존 관계인 클래스를 찾아보면 된다. 가장 쉽게 찾을 수 있는 것이 특정 속성을 부여하는 인터페이스를 하나나 그 이상으로 구현하는 클래스다.



위의 두 다이어그램을 보면 왼쪽 다이어그램은 Persitable Employee 사이를 단단하게 결합한다. Employee의 모든 사용자는 이 결합 때문에 Persitable에도 의존하게 된다. 이 의존성의 영향이 그렇게 크지 않을지도 모르지만, 그래도 의존성이 생기긴 한다. Persitable 인터페이스를 변경하면 모든 Employee 사용자에게 영향을 미칠지도 모른다.

오른쪽 다이어그램이라면 Employee Persitable에게 독립적이면서도 왼쪽의 다이어그램과 마찬가지로 영속성을 사용할 수 있다. Persistable-Employee의 인스턴스는 시스템 안에서 Employee로써 돌아다닐 수 있다. 시스템의 나머지 부분은 이 결합을 알지 못한다. , Persistable Employee의 결합이 있긴 하지만, 이 결합은 시스템 영역의 대부분이 알지 못한다.

 

SRP는 원칙 중에서 가장 간단한 것 중 하나임과 동시에 제대로 적용하기 가장 어려운 것 중 하나이다. 책임을 결합하는 것은 너무나 자연스럽게 해 버리고 마는 일이다. 소프트웨어 설계에서는 이런 책임을 찾고 하나씩 분리하는 것이 실제로 하는 일의 대부분이라고 한다.

 

 

3. 개방 - 폐쇄 원칙(The Open - Closed Principle, OCP)

 

소프트웨어 엔티티(클래스, 모듈, 함수 등등)는 확장에 대해서는 개방되어야 하지만, 변경에 대해서는 폐쇄되어야 한다.’

 

모듈 자체를 변경하지 않고도 그 모듈을 둘러싼 환경을 바꿀 수 있어야 한다는 원칙이다. 다음 그림은 EmployeeDB라는 데이터베이스 façade를 통해 Employee객체를 다루는 간단한 애플리케이션이다. façade는 데이터베이스 API를 직접 다룬다. 바로 이것이 OCP를 위반하는 경우인데, EmployeeDB 클래스의 구현이 변경되면 Employee 클래스도 다시 빌드 해야 할지도 모르기 때문이다. Employee EmployeeDB를 통해 데이터베이스 API에게도 묶인 셈이다. Employee클래스를 포함하는 시스템은 반드시 TheDatabaseAPI까지도 포함해야 한다.



단위 테스트를 할 때는 종종 환경에 생기는 변화를 제어하고 싶은 경우가 생긴다. 예를 들어 Employee를 어떻게 테스트할지 생각해 보자. Employee 객체는 데이터베이스를 변경한다. 하지만 테스트 환경에서 실제 데이터베이스를 바꾸고 싶지 않다. 그렇다고 해서 단위 테스트를 하기 위해 테스트 환경으로 환경을 변경해서 테스트할 때 Employee가 데이터베이스에 하는 모든 호출을 가로챈 다음 이 호출들이 올바른지 검증하면 좋을 것이다.



위 그림처럼 EmployeeDB를 인터페이스로 바꾸면 호출이 올바른지 검증할 수 있다. 이 인터페이스에서 파생한 두 가지 구현을 만들되, 하나는 진짜 데이터베이스를 호출하도록 하고 하나는 우리 테스트를 지원하도록 하면 된다. 이렇게 인터페이스를 만들면 데이터베이스 API Employee를 분리할 수 있고, Employee를 손대지 않고도 Employee를 둘러싼 데이터베이스 환경을 변경할 수 있다.

 

많은 면에서, OCP는 객체 지향 설계의 심장부라 할 수 있다. 이 원칙을 따르는 것은 객체 지향 기술에서 당연하게 요구되는 최상의 효용을 낳는다(유연성, 재사용성, 유지보수성). 하지만 객체 지향 프로그래밍 언어를 사용하는 것만으로는 이 원칙을 따른다고 할 수 없다. 또한 애플리케이션의 모든 부분에 마구 추상화를 적용하는 것도 좋은 생각이 아니다. 그보다는, 프로그램에서 자주 변경되는 부분에만 추상화를 적용하는 개발자 쪽에서의 헌신이 필요하다. 어설픈 추상화를 피하는 것은 추상화 자체만큼이나 중요하다.

 

 

4. 리스코프 교체(치환) 원칙(Liskov Substitution Principle, LSP)

 

서브타입은 언제나 자신의 기반 타입(Base Type)으로 교체할 수 있어야 한다.’

 

if 문장과 instanceof 표현식이 수도 없이 많은 코드는 보통 LSP를 지키지 않아서 생기는데, LSP를 지키지 않았다는 말은 곧 OCP도 지키지 않았다는 말이다.

LSP에 따르면, 기반 클래스의 사용자는 그 기반 클래스에서 유도된 클래스를 기반 클래스로써 사용할 때, 특별한 것을 할 필요 없이 마치 원래 기반 클래스를 사용하는 양 그대로 사용할 수 있어야 한다. 더 자세히 말하자면, instanceof나 다운캐스트를 할 필요가 없어야 한다. 사용자는 파생 클래스에 대해서 아무것도 알 필요가 없어야 한다.

다음은 간단한 임금 지급 애플리케이션이다. Employee 클래스는 추상 클래스이며 calcPay라는 추상 메소드를 가진다. SalariedEmployee 클래스는 분명히 월급을 리턴하도록 이 메소드를 분명히 구현할 것이다. 그리고 HourlyEmployee는 분명히 이 메소드를 이번 주 출퇴근 카드에서 알아낸 근무 시간 수 곱하기 시간당 임금을 리턴하도록 구현할 것이다.



1
2
3
4
5
public class VolunteerEmployee extends Employee {
    public double calcPay() {
        return 0;
    }
}

위의 샘플소스처럼 VolunteerEmployee(자원 봉사 직원)를 추가하는 경우 calPay를 어떻게 구현해야 될지 생각해 보자.

사실 위의 코드처럼 0을 리턴하면 자원 봉사 직원에게 임금을 줄 수 있다는 의미가 내포되고 0원이 찍힌 임금 명세서를 출력메일 전송한다는 문제가 발생하게 된다.

아래와 같이 고친다면 예외(UnpayableEmployeeException)를 처리하기 위해 파생 클래스의 제약이 기반 클래스의 사용자에게도 영향을 미치는 상황이 되어버린다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class VolunteerEmployee extends Employee {
    public double calcPay() {
        throw new UnpayableEmployeeException();
    }
}
  
for (int i = 0; i < employees.size(); i++) {
    Employee e = (Employee) employees.elementAt(i);
    try {
        totalPay += e.calcPay();
    } catch (UnpayableEmployeeException e1) {
    }
}
 
return totalPay;

위의 코드가 복잡해서 다음과 같이 바꾸고 싶은 유혹이 들 수도 있다.

1
2
3
4
5
6
7
for (int i = 0; i < employees.size(); i++) {
    Employee e = (Employee) employees.elementAt(i);
    if (!(e instanceof VolunteerEmployee))
        totalPay += e.calcPay();
}
 
return totalPay;

하지만 이것은 더 나쁘다기반 클래스로 작업하던 코드에서 유도된 클래스까지도 명시해야 하기 때문이다.

  이 모든 혼란은 LSP를 어겼기 때문이며 이 문제를 해결하기 위해서는 애초에 자원봉사자를 Employee에서 파생해서는 안되고 함수 내부에서 calcPay를 호출하는 함수에게 전달해서도 안된다.

 

OCP ODD를 위해 논의된 수많은 의견 중에서도 핵심이다. 이 원칙이 효력을 가질 때, 애플리케이션은 보다 더 유지보스 가능하고, 재사용 가능하며 견고한 것이 된다. LSP OCP를 가능하게 하는 주요 요인 중 하나로, 기반 타입으로 표현된 모듈을 수정 없이도 확장 가능하게 만드는 하위 타입의 치환 가능성을 말한다. 이 치환 가능성은 개발자가 암암리에 의존할 수 있는 그 어떤 것이 되어야 한다. 따라서 기반 타입의 계약 사항은 명시적으로 강제되지 않은 경우, 코드에서 분명하고 뚜렷해야 한다.

‘IS-A’라는 용어는 하위타입의 정의가 되기에는 지나치게 넓은 의미를 가진다. 하위타입의 진실된 정의는 치환 가능성이다. 여기에서 치환 가능성은 명시적 또는 묵시적인 계약에 의해 정의된다.

 

 

5. 의존 관계 역전 원칙(Dependency Inversion Principle, DIP)

 

고차원 모듈은 저차원 모듈에 의존하면 안 된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야 한다.’

상화된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야 한다.’

DIP에서는 다음 원칙들을 지켜야 한다.

l  자주 변경되는 컨크리트 클래스(concrete class)에 의존하지 마라.

l  어떤 클래스에서 상속받아야 한다면, 기반 클래스를 추상 클래스로 만들어라.

l  어떤 클래스의 참조를 가져야 한다면, 참조 대상이 되는 클래스를 추상 클래스로 만들어라.

l  어떤 함수를 호출해야 한다면, 호출되는 함수룰 추상 함수로 만들어라.

l  추상 클래스와 인터페이스는 자신에게서 유도된 구체적인 클래스보다 훨씬 덜 변하므로 구체적인 것보다는 추상적인 것에 의존하는 편이 낫다.

l  하지만 앞으로 변하지 않을 큰크리트 클래스에 의존하는 것은 안전하나 자주 변경되는 컨크리트 클래스는 의존하면 안된다.

l  UML 다이어그램의 화살표마다 따라가서 모두 인터페이스나 추상 클래스를 가리키는지 확인하면 쉽게 원칙을 지키는지 발견할 수 있다.

 

 

전통적인 절차적 프로그래밍 방식은 정책이 구체적인 것에 의존하는 의존성 구조를 만든다. 이런 경우 정책은 구체적인 사항의 변경에 따라 같이 변하기 때문에 불행한 일이다. 객체 지향 프로그래밍은 이런 의존성 구조를 역전시켜 구체적인 사항과 정책이 모두 추상화에 의존하고, 대개 그 클라이언트가 서비스 인터페이스를 소유하게 만든다.

 

사실, 좋은 객체 지향 설계의 증명이 바로 이와 같은 의존성의 역전이다. 프로그램이 어떤 언어로 작성되었는가는 상관없다. 프로그램의 의존성이 역전되어 있다면, 이것은 OO 설계를 갖는 것이다. 그 의존성이 역전되어 있지 않다면, 절차적 설계를 갖는 것이다.

 

의존성 역전의 원칙은 객체 지향 기술에서 당연하게 요구되는 많은 이점 뒤에 있는 하위 수준에서의 기본적인 메커니즘이다. 재사용 가능한 프레임워크를 만들기 위해서는 이것의 적절한 응용이 필수적이다. 이 원칙은 또한 변경에 탄력적인 코드를 작성하는 데 있어 결정적으로 중요하다. 추상화와 구체적 사항이 서로 고립되어 있기 때문에, 이 코드는 유지보수하기가 훨씬 쉽다.

 

 

6. 인터페이스 격리 원칙(Interface Segregation Principle, ISP)

 

클라이언트는 자신이 사용하지 않는 메소드에 의존 관계를 맺으면 안 된다.

 

메소드를 수십개 선언한 비대한 클래스(fat class)에서 단지 두세 개만 호출하는 경우, 호출하지도 않는 메소드에 생긴 변화로 인해 영향을 받는다.

 

예를들어, 아래 그림에서 StudentEnrollment postPayment메소드의 인자를 추가하는 경우 postPayment를 사용하지도 않는EnrollmentReportGenerator도 재컴파일 및 재배포를 해야만 한다.


아래 그림과 같이 클라이언트에게 딱 필요한 메소드만 있는 인터페이스를 제공해서 필요하지 않는 메소드로부터 사용자를 보호해야지만 불행한 의존관계를 막을 수 있다.



 

StudentEnrollment 객체를 사용하는 사용자마다 자신이 관심 있는 메소드들만 있는 인터페이스를 제공받는다. 이렇게 하면 사용자가 관심 없는 메소드에서 생긴 변화로부터 사용자를 보호할 수 있다. 그리고 사용자가 자신이 사용하는 객체를 너무 많이 알게 되는 일도 막을 수 있다.

 

비대한 클래스는 그 클라이언트 사이에 이상하고 해로운 결합이 생기게 만든다. 한 클라이언트가 이 비대한 클래스에 변경을 가하면, 모든 나머지 클래스가 영향을 받게 된다. 그러므로 클라이언트는 자신이 실제로 호출하는 메소드에만 의존해야만 한다. 이것은 이 비대한 클래스의 인터페이스를 여러 개의 클라이언트 특징적(client-specific) 인터페이스로 분해함으로써 성취될 수 있다. 각 클라이언트 특징적 인터페이스는 자신의 특정한 클라이언트나 클라이언트 그룹이 호출하는 함수만 선언한다. 그러면 비대한 클래스가 모든 클라이언트 특징적 인터페이스를 상속하고 그것을 구현할 수 있게 된다. 이렇게 하면 호출하지 않는 메소드에 대한 클라이언트의 의존성을 끊고, 클라이언트가 서로에 대해 독립적이 되게 만들 수 있다.

 

 

7. 결론

 

전체 시스템이 언제나 모든 원칙을 따르게끔 노력하는 것은 현명하지 못하다. OCP를 적용할 상이한 환경을 모두 상상해내거나 SRP를 적용할 모든 변경의 이유를 생각해내려면 시간이 끝도 없이 걸릴 것이다. ISP를 지키기 위해서 자잘한 인터페이스를 몇십 몇백 개 만들게 될 것이고, DIP를 지키기 위해 쓸모없는 추상을 무수히 만들게 될 것이다.

이 원칙들을 적용하는 가장 좋은 방법은 능동적으로 적극 적용하는 것이 아니라, 문제가 생겼을 때 그에 대한 반응으로써 적용하는 것이다. 코드의 구조적인 문제를 처음 발견했거나, 어떤 모듈이 다른 모듈에서 생긴 변화에 영향을 받음을 처음 깨달았을 때 그때에야 비로소 원칙 가운데 하나 또는 여러 개를 써서 이 문제를 해결할 수 있는지 알아보아야 한다.

물론, 이렇게 문제가 생겨야 비로소 반응하는 접근 방법을 쓰기로 한다면, 초기에 고통을 느낄 수 있도록 시스템에 적극적으로 압력을 가해야 한다. 어디가 아픈지 부지런히 눌러봐야 문제들을 충분히 발견할 수 있기 때문이다.

 

 

 

 

 

참고문헌

로버트 C. 마틴, Java 프로그래머를 위한 UML 실전에서는 이것만 쓴다!, 인사이트

로버트 C. 마틴, 제임스 W. 뉴커크, 로버트 S. 코스, 소프트웨어 개발의 지혜, 야스미디어

사이트

http://jakarta.tistory.com/

http://blog.naver.com/PostView.nhn?blogId=lsy94307