요약
작성하는 코드의 80%정도를 구체 클래스가 아닌 인터페이스를 이용해야 하며, 이는 요구사항이 변경되었을 때 프로그램을 쉽게 바꿀 수 있도록 해주는 유연성을 제공해준다.
우리는 객체를 인스턴스화하는 실제 클래스 대신 클래스가 구현하고 있는 인터페이스를 통해 객체를 생성하도록 돕는 여러 패턴을 보았다.
Singleton - 제한된 수의 객체를 생성한다.
Abstract Factory - 관련된 객체 군(family)중 하나를 생성하도록 해주는 '팩토리'. 이때 생성되는 객체의 구체 타입은 인터페이스를 통해 가려진다. 구체 타입이 아닌 인터페이스를 통해 프로그래밍하기 때문이다.
Template Method - 기반 클래스에서 일반 알고리즘을 정의하고, 파생 클래스에서 알고리즘을 사용하는 추상 메소드를 오버라이딩한다.
Factory Method - 구체 클래스가 알려지지 않은 객체를 생성하는 Template Method이다. Factory Method는 객체를 생성하는 Template Method일 뿐이다.
Command - 알려지지 않은 알고리즘을 캡슐화하는 객체이다.
Strategy - 알려진 문제를 해결하는 전략을 캡슐화하는 객체이다. 어떤 알고리즘을 다양한 전략을 통해 해결할 수 있도록 해준다.
Singleton은 매우 간단하지만, 이를 올바르게 구현하기란 쉽지 않으므로 여러 상황에 따라 여러 구현 방법 중 적절한 방법을 선택해야한다.
그리고 실제 코드에서 패턴들이 어떻게 합쳐지고 오버랩되는지도 살펴보았다.
네개의 패턴 중 Template Method와 Factory Method는 거의 사용하지 않는다. 구현 상속에 의존하므로 깨지지 쉬운 기반 클래스 문제가 발생할 수 있기 때문이다.
Singelton과 Abstract Factory가 생성 패턴 중에서는 가장 자주 사용된다.
1. 캡슐화
보통 캡슐화만 이야기 할때는 은닉(hiding)과 캡슐화(encapsulation)를 하나로 묶어 이야기 하는 것이다. 둘은 구분된다.
캡슐화는 데이터와 연산을 한데 묶는 것을 의미한다. 프로그램에서 흘러 다니는 데이터와 결합도는 보통 비례 관계에 있다.
C와 같은 절차 지향 언어에서는 함수가 상태를 저장할 수 없기 때문에 데이터가 OO언어에 비해 많이 흘러 다니게 되고 결합도가 높게 된다.
캡슐화는 이와 같은 데이터 흐름으로 인한 결합도 증가를 막아주는 언어적 장치이다. OO언에서는 클래스가 캡슐화를 가능하도록 해준다.
은닉은 내부데이터, 내부 연산을 외부에서 접근하지 못하도록 은닉 혹은 격리(isolation)시키는 것을 의미한다.
private, public 등 접근 지정자가 은닉을 도와준다. 객체는 '무엇을 하는가'하는 연산으로 정의되어야한다.
객체는 '서비스 제공자(Service Provider)'이어야 하는 것이다.
그러므로 '어떻게' 연산을 수행하는가를 철처히 은닉되어야 하며, 외부로 공개된 인터페이스 혹은 계약을 통해서만 프로그래밍해야 한다.
캡슐화와 은닉이 잘되려면 객체에 책임을 적절히 배분해 주어야 한다. '객체는 하나의 책임만 맡아야 한다'는 단일 책임원칙(SRP: Single Responsibility Principle)은 좋은 기준이 되어준다.
getter/setter는 가능한 사용을 자제해야 한다.
이는 '데이터를 요청하지 말고 도움을 요청하라'라는 OO금언과 관련있다.
구현이 잘 은닉되어 있고, 책임이 제대로 분배되어 있다면 gettter/setter는 그리 필요치 않다.
데이터를 이곳 저곳에서 가져와 조합하는 대신 필요로 하는 대부분의 데이터를 갖고 있는 객체에 일부 데이터를 넘겨주고 일을 해달라고 요청(위임)하자
인터페이스는 은닉과 관련있다. 인터페이스를 사용하면 클라이언트는 구체 클래스에 대해 알지 못해도 된다.
이는 디자인 패턴의 내면에 흐르는 철학이자 OO의 철학이기도 하다. 구체클래스는 인터페이스보다 변하기 쉽다.
그러므로 인터페이스를 이용하라는 의존관계 역전의 원칙(DIP: Dependency Inversion Principle)을 생각하자
인터페이스를 통해 구체 클래스를 은닉하다록 하자. 인터페이스에 대해서는 상속과 다형성에도 관련있다.
하나의 클래스가 전체적으로는 하나의 역할만을 맡고 있지만 관점에 따라서는 2개 이상의 인터페이스를 구현하고 있을 수 있다.
이런 경우에는 하나의 클래스가 여러 타입이 된다. 그리고 이러한 클래스를 이용하는 클라이언트는 자신이 필요로 하는 인터페이스를 사용하면된다. 하나의 거대한 인터페이스보다는 작은 여러개의 인터페이스가 좋다는 인터페이스 분리 원칙(ISP: Interface Segregation Principle)을 생각하자.
2. 상속
상속은 그 자체가 OO의 핵심이라기보다는 다형성을 위한 것이다.
상속에는 구현상속(extends)와 인터페이스 상속(implements)이 있다.
구현상속에는 재사용과 다형성 획득이라는 두가지 기능이 있다.
인터페이스 상속에는 다형성 획득과 인터페이스를 통한 은닉이라는 기능이 있다.
구현 상속에는 '깨지기 쉬운 기반 클래스 문제'가 있기 때문에 다형성 획득이라는 측면에서 보자면 인터페이스 상속이 안전하고 낫다.
재사용 측면에서 보자면 보통 상속보다는 합성이 좋다. 상속은 불필요한 결합도를 증가시키기 때문이다.
그러므로 다형성을 위해서는 인터페이스 상속을 사용하고, 재사용을 위해서는 합성을 사용하자.
구현상속은 언제? 여러 클래스가 공통의 연산 집합을 공유하고 있을 때 정규화를 위해 사용하면 된다.
물론 깨지기 쉬운 기반 클래스 문제가 있다는 것은 알고 사용해야 한다.
하지만 자바에서 모든 객체가 Object를 구현상속하고, JUnit을 이용할 때 테스트 케이스들이 TestCase를 상속하듯 적절히 사용하면 도움이 된다.
상속을 할때는 상속한 메서드에서 예외를 던지면 안된다. 예외를 던지면 다형성을 이용하기 힘들기 때문이다. 상속의 존재 가치 중 하나가 다형성을 얻기 위함인데, 상속이 다형성을 해치면 어떻게 하는가?LSP(Liskov Substitution Principle)를 지키자.
3.다형성
OO의 꽃이다.
캡슐화와 은닉은 '무엇을'과 '어떻게'를 분리시켜 준다.
상속은 '어떻게'를 다양하게 정의하도록 해준다.
다형성은 이 둘을 조합하여 런타임에 '무엇을', '어떻게' 실행시킬 것인지를 동적으로 정하게 된다.
이는 확장에는 열려있고 수정에는 닫혀있다는 OCP(Open Closed Principle)을 지킬수 있도록 해준다.
예를들어 로그 메시지를 다양한 방식(콘솔, 하나의 파일, 날짜별 파일, DB, Mail, TCP/IP)으로 출력할수 있을것이다.
심지어 신속한 장애 대처를 위해 ERROR 레벨 로그를 SMS로 보내느 방법도 생각해볼수 있다.
이곳이 바로 다형성이 등장할 곳이다.
로깅 라이브러리는 '무엇'What에 해당하는 로그 찍기 메소드만 제공하면된다.
logger.info("msg"); 와 같은 식이다.
그리고 설정 파일 등을 통해 '어떻게 how'에 해당하는 로그 출력방식을 다양하게 변경할 수 있도록 한다.
만약 TCP/IP를 이용해 로깅 메시지를 출력하는 클래스에 버그가 있다고 하더라도 인터페이스로 분리되어 있기 때문에 이 클래스를 수정해도 다른 클래스에는 영향을 미치지 않는다.
또한 에러 LEVEL의 로그 메시지를 SMS를 통해 발송하는 클래스를 추가한다 하더라도 다른 클래스에는 영향을 미치지 않는다.
수정에는 닫혀있고 확장에는 열려 있는 구조, OCP를 만족시키는 구조가 되는 것이다.
4.정리
객체 지향은 기본적으로 책임을 적절히 분배하여 객체 간의 결합도를 최소한으로 줄이는 것을 목적으로 한다.
이를 통해 유연하고 유지보수하기 쉬운 프로그램을 만들수 있게 된다.
또한 변하는 부분과 변하지 않는 부분을 분리하여 변화하는 부분에 다형성을 통해 확장과 수정의 가능성을 열어 두는 것이 핵심이다.
변하지 않을 부분까지 너무 추상화시키게 되면 시스템이 지나치게 복잡해진다는 사실을 명심하자.
패턴을 미리 도입하는 것은 복잡함을 지나치게 증가시킬 수 있지만, 변화가 예상되는 장소에 인터페이스를 삽입해 놓은 것은 복잡함을 크게 증가 시키지 않는다. 인터페이스를 적극 도입하여 사용하라.
'JAVA > Design Patterns' 카테고리의 다른 글
상태패턴 (0) | 2018.11.27 |
---|---|
전략패턴 (0) | 2018.11.26 |
인터페이스로 프로그래밍하기 #깨지기 쉬운 기반 클래스 문제3 (0) | 2018.02.14 |
인터페이스로 프로그래밍하기 #깨지기 쉬운 기반 클래스 문제2 (0) | 2018.02.14 |
인터페이스로 프로그래밍하기 #깨지기 쉬운 기반 클래스 문제1 (0) | 2018.02.08 |