본문 바로가기

JAVA/Design Patterns

인터페이스로 프로그래밍하기 #인터페이스 vs 클래스

반응형

인터페이스 관점에서 프로그래밍하는 것은 OO(object-oriented) 시스템의 기본 개념이며 GoF의 디자인 패턴은 이의 구체적인 예가 된다.

보통 인터페이스를 사용하지 않고 extends 관계를 남용하고 있다.


extends 키워드

디자인 패턴은 크게 보면 구현상속(extends)을 인터페이스 상속(implements)으로 바꾸는 방법을 설명하고 있다.


extends는 적절히 사용하면 값진 도구이지만 많은 개발자들이 남용하고 있다.

extends와 같은 언어 기능은 분명 OO시스템 구현을 용이하게 해주지만,단순히 상속을 사용하는 것이 시스템을 객체 지향적으로 만들어주지는 않는다.


OO언어를 사용하여 프로그래밍을 했다고 OO시스템이 되는 것이 아니다. 언어가 제공하는 기능을 목적에 맞게 사용해야 된다.


테이터 추상화-객체 안에 구현 상세를 캡슐화 하는것- 역시 다형성 만큼이나 중요한 OO의 핵심 개념이다.

물론 절차 지향 시스템도 데이터를 추상화할 수 있지만, 꼭 그렇게 하는 것은 아니다. OO시스템에서는 데이터 캡슐화가 선택이 아닌 필수이다.

다형성(기반 클래스의 행동을 이를 상속한 클래스에서 재정의하는 것)은 객체 지향의 핵심이며, 다형성을 얻기 위해서는 상속이 필요하다.

그리고 extends와 implements가 상속을 지원한다.

implements를 이용하여 인터페이스를 구현하는 클래스 역시 extends를 이용하여 클래스를 상속하는 것과 같이 상속 관계라 할 수 있다.


다형성은 객체지향의 핵심이며, extends와 implements가 다형성을 지원한다. 이둘을 용도에 맞게 사용하는 것이 중요하며, 대부분의 경우 implements가 바람직하다.


인터페이스 vs 클래스


유연성의 상실

명시적으로 구체 클래스의 이름을 사용하면 특정 구현에 종속되는데 이는 결과적으로 수정을 필요 이상으로 어렵게 만든다.

현대의 '애자일' 개발 방법론의 핵심 개념 중 하나는 디자인과 개발을 병행, 즉 함께 한다는 것이다.

애자일 방법론에서는 완벽한 요구 사항을 도출하기 전에 프로그래밍을 시작한다.

이 방식은 프로그래밍을 시작하기전에 프로그램에 대한 완벽한 명세를 만들라던 고전적인 지혜의 말씀과 대치된다.

하지만 많은 성공적인 프로젝트들은 애자일한 방법이 고전적 방법을 취한 프로젝트보다 더 빨리 더 적은 비용으로, 더 좋은 품질의 코드를 생성할 수 있다는 것을 증명해주었다.

애자일 방법론이 모든 프로젝트에 들어맞는건 아니지만, 개발 기간 동안 요구 사항의 변화가 있는 중소 규모 프로젝트에는 매우 효과적이다.

애자일 병행 개발의 중심에는 유연성이란 개념이 있다.

새로 추가된 요구사항을 가능한 쉽게 반영할 수 있는 코드를 만드는 것이다.

또한 아마도 필요할지 모르는 기능을 구현하기보다는 꼭 필요한 기능을 구현하되, 프로그램은 변화를 수용할 수 있어야 한다.

유연한 프로그램이 아니라면 병행 개발은 불가능하다. 그리고 인터페이스를 통해 프로그래밍하는 것은 유연한 구조를 만드는 핵심이다.


void f() {

LinkedList list = new LinkedList();


modify(list);

}


void modify(LinkedList list) {

list.add();

doSomthingWith(list);

}


조회를 빨리 해야 한다는 새로운 요구 사항이 출현했다면?

LinkedList는 적합하지 않으며, 이를 Hashset으로 바꿔야 할 것이다.

현재 코드는 f()에서의 선언 뿐 아니라 modify()에서 파라미터 선언까지 고쳐주어야 하므로 변화가 국지적이지 않다.

아마도 somethingWith도 고쳐 주어야 할 것이고, 변화는 이런 식으로 파급된다.

modify()가 인자로 LinkedList 대신 Collection을 받도록 다음과 같이 수정해보자


void f() {

Collection list = new LinkedList();


modify(list);

}


void modify(Collection list){

list.add();

doSomethingWith(list);

}

이제 LinkedList를 Hashset으로 바꾸고 싶다면 변수가 정의되어 있는 f() 메서드에서 new LinkedList()를 new Hashset()으로 바꾸어 주기만 하면된다.


두 코드를 비교해 보자.

examine() 메소드는 어떤 컬렉션의 모든 멤버를 살펴보는 기능을 담당한다.

f() {

Collection c = new Hashset();

examine(c);

}


void examine(Collection c) {

for(Iterator i = c.iterator(); i.hasNext();) {

i.next()

}

}


이 코드를 좀 더 일반화된 버전으로 바꾸면

f() {

Collection c = new Hashset();

examine(c);

}


void examine(Iterator i) {

for(; i.hasNext();) {

i.next();

}

}


examine() 메소드는 인자로 Collection 대신 Interator를 받기 때문에 Collection의 구현체 뿐아니라 Map을 통해 얻은 키-값 리스트 또한 처리할 수 있다.

또한 컬렉션은 순회하는 대신 데이터를 생성하는 Iterator를 만들 수 도, 파일로부터 프로그램으로 정보를 가져오는 Iterator를 만들 수도 있다.

일반화된 버전은 수정 없이 이러한 변화를 모두 수용할 수 있다. 뛰어난 유연성 아닌가!


결합도

구현 상속과 관련해 좀더 중요한 문제는 결합도(coupling)이다.

결합도는 프로그램의 어느 부분이 다른 부분과 연관을 맺는 정도라 정의할 수 있으며, 일반적으로 낮을 수록 좋다.

전역 변수는 왜 강한 결합이 나쁜지를 보여주는 좋은 예이다.

만약 전역 변수의 타입을 바꾼다면 이 변수를 사용하고 있는, 즉 이 변수와 결합되어 있는 모든 변수가 영향을 받게 되며 이러한 코드들은 모두 검사, 수정, 재 테스트를 해야한다.

더구나 이 변수를 사용하는 모든 메소드들은 변수를 통해 서로 결합되어 있다.

즉 변수의 타입 하나를 바꾸었는데 한 메소드가 다른 메소드에까지 영향을 미칠 수도 있게 된다.

문제는 멀티 스레드 프로그램에서 특히 골치 아프다.


결합 관계는 항상 최소화하려 노력해야 한다.

물론 한 객체가 다른 객체를 호출하는 것이 결합을 의미하기 때문에 결합을 완전히 제거 할 수는 없다.

적절한 결합 없이는 프로그램도 없다.

하지만 OO의 개념들을 충실히 지키면 결합도를 상당히 줄일 수 있으며, 객체의 구현을 이를 사용하는 객체로부터 완전히 숨긴다는 것이 가장 중요하다.

예를 들어 상수가 아닌 모든 필드는 항상 private이어야 한다.

예외는 없다.! 종종 protected 메소드를 통해 좋은 효과를 볼 수는 있다. 하지만 protected 변수는 public을 의미하는 다른 방법일 뿐이다.


필드에 접근하는 기능만을 제공할 뿐인 get/set 메소드 역시 같은 이유로 사용하면 안된다.

이들은 필드를 pulbic으로 만드는 복잡한 방법일 뿐이다.

기본형 대신 객체를 반환하는 getter 메소드를 반환되는 객체가 디자인의 핵심 추상화(key abstraction)일 경우에는 합당하다.

비슷한 이유로 getSomething()과 같은 메소드는 객체가 정보를 제공하기 위해 필수적일 때 역시 용납할 수 있다.

(Thermometer 객체의 getTemperature() 메소드가 Temperature 객체를 반환하는 것은 합당할 것이다.)

또한 어떤 메소드를 구현하는 가장 쉬운 방법이 단순히 필드를 반환하는 것이라면 이 역시 좋다.

하지만 이와 같은 경우를 제외하면 getter/setter는 사용하지 말아야 한다.


getter/setter는 객체간의 결합도를 높여 유지보수를 어렵게 만들기 때문이다. 꼭 필요한 경우를 제외하고 사용하지 말아야 한다.


개발하면서 OO원칙 적용의 '엄격성(strictness)'과 코드를 빨리 작성하고 유지 보수하기 좋은 코드를 작성하는 것 사이에 높은 상관관계가 있다는 사실을 깨달았다.

구현 은닉과 같은 핵심 OO원칙을 어겼을 경우에는 코드를 재작성을 해야한다.

(대부분은 코드를 디버깅하기 불가능했기 때문이다.)프로그램을 두번 작성할 만큼의 충분한 시간이 없기 때문에 항상 OO원칙들을 충실히 지키려고 노력한다.

물론 OO원칙을 적용할 때 '순수를 위한 순수'에는 관심이 없다.

오로직 '실용성'이기 때문이다.

앞으로 실용적으로 OO원칙과 디자인 패턴을 적용하고 활용하는 방법이 중요하다.





반응형