들어가기

지저분해지는 코드

단순한 if-else 블록의 복사 붙여넣기의 반복은 코드를 복잡하게 만들며, 개발자가 코드를 추가하거나 수정할 위치를 찾는데 점점 오랜 시간이 걸리게 된다. 심한 경우 코드를 추가하지 않고 눌가하는 경우도 발생한다.

수정하기 좋은 구조를 가진 코드

객체 지향 기법을 적용하면 소프트웨어를 더 쉽게 변경할 수 이쓴 유연함을 얻을 수 있게 되고 이는 곧 요구 사항의 변화를 더 빠르게 수용할 수 있다는 것을 뜻한다.

소프트웨어의 가치

새로운 요구 사항을 적용하기 어려우면 소프트웨어는 점점 뒤쳐지게 되는데, 이는 결곡 소프트웨어의 죽음으로 이어질 수 잇다. 변화 가능한 유연한 구조를 만들어 주는 핵심 기법 중의 하나가 바로 객체 지향이다.

객체 지향

절차 지향과 객체 지향

  • 절차 지향 프로그래밍: 데이터를 조작하는 코드를 별도로 분리해서 함수나 프로시저와 같은 형태로 만들고, 각 프로시저들이 데이터를 조작하는 방식으로 코드를 작성할 수 있을 것이다. 프로시저는 다른 프로시저를 사용할 수도 있고, 각각의 프로시저가 같은 데이터를 사용할 수도 있다. 데이터와 그 데이터를 사용하느 프로시저를 작성하는 것은 자연스러운 과정이기 때문에, 최초에 절차 지향적으로 코드를 구현하는 것은 쉽다. 하지만, 프로그램 규모가 커져서 데이터 종류가 증가하고 이를 사용하는 프로시저가 증가하게 되면, 다음과 같은 문제들이 발생하게 된다.
    • 데이터 타입이나 의미를 변경해야 할 때, 함께 수정해야 하는 프로시저가 증가한다.
    • 같은 데이터를 프로시저들이 서로 다른 의미로 사용하는 경우가 발생한다.
  • 객체 지향 프로그래밍: 데이터 및 데이터와 관련된 프로시저를 객체라고 불리는 단위로 묶는다. 객체는 프로시저를 실행하는데 필요한 만큼의 데이터를 가지며, 객체들이 모여 프로그램을 구성한다. 객체 지향은 최초에는 객체 지향적으로 설계하는데 더 많은 노력이 들어갈 수 있지만 프로그램을 상대적으로 쉽게 수정할 수 있는 유연함을 제공하기 때문에, 변화된 요구 사항을 빠르게 반영할 수 있도록 만들어 준다.

객체

  • 객체를 정의할 때 사용되는 것은 객체가 제공해야 할 기능이며, 객체가 내부적으로 어떤 데이터를 갖고 있는 지로는 정의되지 않는다. 보통 객체가 제공하는 기능을 오퍼레이션이라고 한다.
  • 객체가 제공하는 모든 오퍼레이션 집합을 객체의 인터페이스라고 한다.
  • 오퍼레이션의 실행 요청하는 것을 메시지를 보낸다고 한다.

객체의 책임과 크기

  • 객체마다 자신만의 책임이 있다. 상황에 따라 객체가 가져야 할 기능의 종류와 개수가 달라지기 때문에, 모든 상황에 들어맞는 객체-책임 구성 규칙이 존재하는 것은 아니다.
  • 한 객체에 많은 기능이 포랗ㅁ되면, 그 기능과 관련된 데이터들도 한 객체에 모두 포함된다.
  • 따라서 객체가 갖는 책임의 크기는 작아질수록 객체 지향의 장점인 변경의 유연함을 얻을 수 있게된다. 객체의 크기와 관련된 원칙이 있는데 그 원칙은 바로 단일 책임 원칙(Single Responsibility Principle; SRP)이다.

의존

  • 한 객체가 다른 객체를 생성하거나 다른 객체의 메소드를 호출하는 경우, 메소드의 파라미터로 전달받는 경우를 의존한다고 표현한다.
  • 의존의 영향은 꼬리에 꼬리를 문 것처럼 전파되는 특징을 갖는다.
  • 의존이 순환해서 발생할 경우 다른 방법이 없는지 고민해야 한다. 한 객체의 변경의 여파가 다시 자신의 객체의 변화를 유발할 수도 있기 때문이다.
  • 의존 관계는 다음과 같이 상호간의 영향을 준다.
    • 내가 변경되면 나에게 의존하고 있는 코드에 영향을 준다.
    • 나의 요구가 변경되면 내가 의존하고 있는 타입에 영향을 준다.

캡슐화

  • 객체 지향은 기본적으로 캡슐화를 통해서 한 곳의 변화가 다른 곳에 미치는 영향을 최소화한다.
  • 캡슐화는 객체가 내부적으로 기능을 어떻게 구현하는지를 감추는 것이다.
  • 캡슐화를 위한 두 개의 규칙
    • Tell, Don’t Ask: 데이터를 물어보지 않고, 기능을 실행해 달라고 말하라
    • 데미테르의 법칙:
      • 메서드에서 생성한 객체의 메서드만 호출
      • 파라미터로 받은 객체의 메서드만 호출
      • 필드로 참조하는 객체의 메서드만 호출
  • 데미테르의 법칙을 지키지 않는 전형적인 증상 두 가지가 있다.
    • 연속된 get 메서드 호출
    • 임시 변수의 get 호출이 많음임시 변수의 get 호출이 많음
  • 객체 지향 설계란 다음의 작업을 반복하는 과정이라고 볼 수 있다.
    1. 제공해야 할 기능을 찾고 또는 세분화하고, 그 기능을 알맞은 객체에 할당한다.
      1. 기능을 구현하는데 필요한 데이터를 객체에 추가한다. 객체에 데이터를 먼저 추가하고 그 데이터를 이용하는 기능을 넣을 수도 있다.
      2. 기능은 최대한 캡슈로하해서 구현한다.
    2. 객체 간에 어떻게 메시지를 주고받을 지 결정한다.
    3. 과정1과 과정2를 개발하는 동안 지속적으로 반복한다.

다형성과 추상 타입

상속 개요

  • 다형성: 한 객체가 여러 가지 모습을 갖는다는 것을 의미한다. 여기서 모습이란 타입을 뜻하는데, 즉, 다형성이란 한 객체가 여러 타입을 가질 수 있다는 것을 뜻한다.
  • 자바와 같은 정적 타입 언어에서는 타입 상속을 통해서 다형성을 구현한다.
  • 구현 상속은 보통 상위 클래스에 정의된 기능을 재사용하기 위한 목적으로 사용된다.
  • 클래스 상속은 구현을 재사용하면서 다형성도 함께 제공해 준다.

추상 타입과 유연함

  • 추상화는 데이터나 프로세스 등의 의미가 비슷한 개념이나 표현으로 정의하는 과정이다.
  • 상속을 이용해서 추상 타입을 실제 구현을 제공하는 클래스를 ‘콘크리트 클래스’라고 부른다.
  • 추상화는 공통된 개념을 도출해서 추상 타입을 정의해 주기도 하지만, 또한, 많은 책임을 가진 객체로부터 책임을 분리하는 촉매제가 되기도 한다.
  • 요구 사항이 바뀔 때 변화되는 부분은 이후에도 변경될 소지가 많다. 이런 부분을 추상 타입으로 교체하면 향후 변경에 유연하게 대처할 수 있는 가능성이 높아진다.
  • 인터페이스에 대고 프로그래밍하기. 실제 구현을 제공하는 콘크리트 클래스를 사용해서 프로그래밍하지 말고, 기능을 정의한 인터페이스를 사용해서 프로그래밍하라는 뜻이다.
  • 모든 곳에서 인터페이스를 사용해서는 안 된다. 오히려 불필요하게 프로그램의 복잡도만 증가시킬 수 있다.
  • 인터페이스의 이름은 그 인터페이스를 사용하는 코드 입장에서 작성해야 한다.
  • 인터페이스로 추상화하면 좀 더 쉽게 Mock 객체를 만들어 코드의 완성을 기다릴 필요 없이 먼저 빠르게 테스트 할 수 있도록 해준다.

재사용: 상속보단 조립

상속과 재사용

  • 상속은 변경의 유연함이라는 측면에서 치명적인 단점을 갖는다.
  • 상속은 상위 클래스의 변경을 어렵게 만든다. 상속 계층을 따라 상위 클래스의 변경이 하위 클래스에 영향을 주기 때문에, 최악의 경우 상위 클래스의 변화가 모든 하위 클래스에 영향을 줄 수 있다.
  • 상속을 통한 기능 재사용은 유사한 기능을 확장하는 과정에서 클래스의 개수가 불필요하게 증가할 수 있다.
  • 상속을 통한 기능 재사용은 상속 자체를 잘못 사용할 수 있다. 상속은 IS-A 관계가 성립할 때에만 사용해야 한다.

조립을 이용한 재사용

  • 조립을 사용하면 위의 문제점들을 해결할 수 있다.
  • 상속에 비해 조립을 통한 재사용의 단점은 상대적으로 런타임 구조가 복잡해진다는 것이다. 또한 상속보다 구현이 더 어렵다.
  • 하지만, 장기적 관점에서 구현/구조의 복잡함보다는 변경의 유연함을 확보하는 데서 오는 장점이 더 크다.
  • 위임은 내가 할 일을 다른 객체에게 넘긴다는 의미를 담고있으며, 보통 조립 방식을 이용해서 위임을 구현한다.
  • 위임을 사용하면 메서드 호출이 추가되기 때문에 실행 시간은 다소 증가한다. 하지만 대부분의 경우에는 위임으로 인해 발생하는 성능저하보다 위임을 통해서 얻을 수 있는 유연함/재사용의 장점이 크다.
  • 상속을 사용할 때에는, 재사용이라는 관점이 아닌 확장이라는 관점에서 상속을 적용해야 한다. 또한 추가로 명확한 IS-A 관계가 성립되어야 한다.

설계 원칙: SOLID

SOLID 설계 원칙

  • 단일 책임 원칙 (Single responsibility principle; SRP)
  • 개방-폐쇄 원칙 (Open-closed principle; OCP)
  • 리스코프 치환 원칙 (Liskov substitution principle; LSP)
  • 인터페이스 분리 원칙 (Interface segregation principle; ISP)
  • 의존 역전 원칙 (Dependency inversion principle; DIP)

단일 책임 원칙

  • 클래스는 단 한 개의 책임을 가져야 한다.
  • 한 클래스에 책임의 개수가 많아질수록 한 책임의 기능 변화가 다른 책임에 주는 영향은 비례해서 증가하게 되는데, 이는 결국 코드를 절차 지향적으로 만들어 변경을 어렵게 만든다.
  • 단일 책임 원칙을 어기면 재사용을 어렵게 만든다.
  • 각각의 책임은 서로 다른 이유로 변경되고, 서로 다른 비율로 변경되는 특징이 있다.
  • 서로 다른 이유로 변경되는 것을 쉽게 알아채는 법은 메서드를 실행하는 것이 누구인지 확인해 보는 것이다.

개방 폐쇄 원칙

  • 기능을 변경하거나 확장할 수 있으면서, 그 기능을 사용하는 코드는 수정하지 않는다.
  • 개방 폐쇄 원칙은 인터페이스 또는 상속을 이용하는 방법이 있다.
  • 개방 폐쇄 원칙이 깨질 때의 주요 증상으로는 다운 캐스팅을 하고, 비슷한 if-else 블록들이 존재하는 것이다.
  • 개방 폐쇄 원칙은 변화가 예상되는 것을 추상화해서 변경의 유연함을 얻도록 해준다.

리스코프 치환 원칙

  • 상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.
  • 리스코프 치환 원칙은 기능의 명세에 대한 내용이다. 기능 실행의 계약과 관련해서 흔히 발생하는 위반 사례로는 다음과 같은 것들이 있다.
    • 명시된 명세에서 벗어난 값을 리턴한다.
    • 명시된 명세에서 벗어난 익셉션을 발생한다.
    • 명시된 명세에서 벗어난 기능을 수행한다.
  • 리스코프 치환 원칙은 확장에 대한 것이다. 리스코프 치환 원칙을 어기면 개방 폐쇄 원칙을 어길 가능성이 높아진다. 개방 폐쇄 원칙을 지키지 않으면 기능 확장을 위해 더 많은 부분을 수정해야 하므로, 리스코프 치환 원칙을 지키지 않으면 기능을 확장하기가 어렵게 된다.

인터페이스 분리 원칙

  • 인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다.
  • 인터페이스를 분리하지 않으면 한 클라이언트의 영향이 다른 클라이언트에 영향을 줄 수 있다.
  • 인터페이스 분리 원칙은 단일 책임 원칙과도 연결된다. 클라이언트 입장에서 사용하는 기능만 제공하도록 인터페이스르 분리함으로써 한 기능에 대한 변경의 여파를 최소화할 수 있게 된다.

의존 역전 원칙

  • 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.
  • 고수준 모듈은 어떤 의미 있는 단일 기능을 제공하는 모듈이다.
  • 저수준 모듈은 고수준 모듈의 기능을 구현하기 위해 필요한 하위 기능의 실제 구현으로 정의할 수 있다.
  • 의존 역전 원칙은 리스코프 치환 원칙과 함께 개방 폐쇄 원칙을 따르는 설계를 만들어 주는 기반이 되는 것이다.
  • 고수준 모듈이 저수준 모듈에 의존하면 프로그램의 변경을 어렵게 만든다. 저수준 모듈이 변경되더라도 고수준 모듈은 변경되지 않게 해야된다.
  • 저수준 모듈이 고수준 모듈을 의존하게 만드는 것은 추상화에 있다.
  • 의존 역전 원칙은 런타임의 의존이 아닌 소스 코드의 의존을 역전시킴으로써 변경의 유연함을 확보한다는 것이다.
  • 의존 역전 원칙은 타입의 소유도 역전시킨다.

SOLID 정리

  • SOLID 원칙을 한 마디로 정의하면 변화에 유연한게 대처할 수 있는 설계 원칙이다.
  • 단일 책임 원칙과 인터페이스 분리 원칙은 객체가 커지지 않도록 막아준다.
  • 리스코프 치환 원칙과 의존 역전 원칙은 개방 폐쇄 원칙을 지원한다.
  • SOLID 원칙은 사용자 입장에서의 기능 사용을 중시한다. 인터페이스 분리 원칙은 클라이언트 입장에서 인터페이스를 분리하고 있으며, 의존 역전 원칙 역시 저수준 모듈을 사용하는 고수준 모듈입장에서 추상화 타입을 도출하도록 유도한다.

DI(Dependency Injection)와 서비스 로케이터

어플리케이션 영역과 메인 영역

  • 메인 영역의 역할
    • 어플리케이션 영역에서 사용될 객체를 생성한다.
    • 각 객체 간의 의존 관계를 설정한다.
    • 어플리케이션을 실행한다.
  • 모든 의존은 메인 영역에서 어플리케이션 영역으로 향한다. 반대의 경우는 존재하지 않는다.
  • 사용할 객체를 제공하는 책임을 갖는 객체를 서비스 로케이터(Service Locator)라고 부른다.
  • 서비스 로케이터 방식은 로케이터를 통해서 필요로 하는 객체를 직접 찾는 방식인데, 이 방식은 몇 가지 단점이 존재한다. 그래서 서비스 로케이터를 사용하기보다는 외부에서 사용할 객체를 주입해 주는 DI(Dependency Injection)방식을 사용하는 것이 일반적이다.

DI(Dependency Injection)을 이용한 의존 객체 사용

  • DI: 필요한 객체를 직접 생성하거나 찾지 않고 외부에서 넣어 주는 방식이다.
  • DI를 통해서 의존 객체를 관리할 때에는 객체를 생성하고 각 객체들을 의존 관계에 따라 연결해 주는 조립 기능이 필요하다.
  • 조립기를 별도로 분리하면 향후에 조립기 구현 변경의 유연함을 얻을 수 있다.

생성자 방식과 설정 메서드 방식

  • 생성자 방식: 생성자를 통해서 의존 객체를 전달받는 방식이다.
  • 설정 메서드 방식: 메서드를 이용해서 의존 객체를 전달받는다. ex) setXXX()
  • 생성자 방식은 생성자를 통해서 필요한 의존 객체를 전달받기 때문에, 객체를 생성하는 시점에서 의존 객체가 정상인지 확인할 수 있다.
  • 설정 메서드 방식은 객체를 생성한 이후에 의존 객체를 설정할 수 있기때문에, 어떤 이유로 인해 의존할 객체가 나중에 생성된다면 설정 메서드 방식을 사용해야한다.
  • 의존할 객체가 많을 경우, 설정 메서드 방식은 메서드 이름을 통해서 어떤 의존 객체가 설정되는지 보다 쉽게 알 수 있으며, 이는 코드 가독성을 높여주는 효과가 있다.

DI와 테스트

  • DI를 통해서 의존 객체를 관리하면 단위 테스트 작성시 Mock 객체를 사용해서 테스트할 수 있다.
  • DI를 사용하지 않으면 테스트 때문에 다른 클래스의 코드를 변경해 주는 상황이 발생한다.

스프링 프레임워크 예

  • DI 프레임워크인 스프링 프레임워크는 생성자 방식과 설정 메서드 방식을 모두 지원하고 있다.
  • XML 설정 파일을 상요하는 방식은 개발자가 입력한 오타에 다소 취약하다. 이 문제점을 해소하기 위한 방안으로 스프링 3 버전부터는 자바 코드 기반의 설정 방식이 추가되었다. 하지만 자바 코드를 수정해서 다시 컴파일하고 배포해 주어야 하는 단점이 있다.

서비스 로케이터를 이용한 의존 객체 사용

  • 안드로이드 실행 환경은 정해진 메서드만을 호출할 뿐, 안드로이드 프레임워크가 DI 처리를 위한 바업을 제공하지는 않는다. 따라서 서비스 로케이터를 이용하여 의존 객체를 찾는다.
  • 서비스 로케이터의 구현
    • 객체 등록 방식의 서비스 로케이터 구현: 서비스 로케이터에 객체를 등록하는 인터페이스가 노출되어 있기 때문에 어플리케이션 영역에서 얼마든지 의존 객체를 바꿀 수 있다.
    • 상속을 통한 서비스 로케이터 구현: 객체를 구하는 추상 메서드를 제공하는 상위 타입 구현과 상위 타입을 상속받은 하위 타입에서 사용할 객체를 설정할 수 있다. 하지만 이는 인터페이스 분리 원칙을 여전히 위반하고 있다.
    • 지네릭/템플릿을 이용한 서비스 로케이터 구현: 위의 문제점을 해결하기 위하여 Map<Class<?>, Object> objectMapT get(Class<T> klass) , void regist(Class<?> klass, Object obj) 을 이용하여 어느 정도는 인터페이스 분리 원칙을 지키는 효과를 볼 수 있다.
  • 서비스 로케이터의 단점
    • 동일 타입의 객체가 다수 필요할 경우, 각 객체 별로 제공 메서드를 만들어 주어야 한다는 점이다.
    • 서비스 로케이터는 인터페이스 분리 원칙을 위배한다.

주요 디자인 패턴

디자인 패턴이란?

  • 객체 지향 설계는 재설계 없이 또는 재설계를 최소화하면서 요구 사항의 변화를 수용할 수 있도록 만들어 준다.
  • 객체 지향 설계를 하다 보면, 이전과 비슷한 상황에서 사용했던 설계를 재사용하는 경우가 종종 발생한다.

전략(Strategy) 패턴

  • 콘텍스트 코드의 변경 없이 새로운 전략을 추가할 수 있다.
  • if-else로 구성된 코드 블록이 비슷한 기능(비슷한 알고리즘)을 수행하는 경우에 전략 패턴을 적용함으로써 코드를 확장 가능하도록 변경할 수 있다.

템플릿 메서드 패턴

  • 템플릿 메서드 패턴은 다음과 같은 두 가지로 구성된다.
    • 실행 과정을 구현한 상위 클래스
    • 실행 과정의 일부 단계를 구현한 하위 클래스
  • 상위 타입의 템플릿 메서드가 모든 실행 흐름을 제어하고, 하위 타입의 메서드는 템플릿 메서드에서 호출되는 구조를 갖게 된다.
  • 훅 메서드: 이렇게 상위 클래스에서 실행 시점이 제어되고, 기본 구현을 제공하면서, 하위 클래스에서 알맞게 확장할 수 있는 메서드를 훅 메서드라고 부른다.
  • 전략 패턴을 함께 사용하면 상속이 아닌 조립 방식으로 템플릿 메서드 패턴을 활용할 수 있다.

상태 패턴

  • 상태에 따라 다르게 동작해야 할 때 상요할 수 있는 패턴이다. 상태 패턴은 상태 객체가 기능을 제공한다.
  • 상태 패턴은 새로운 상태가 추가되더라도 콘텍스트 코드가 받는 영향은 최소화된다는 장점이 있다.
  • 상태 패턴은 상태에 따른 동작을 구현한 코드가 각 상태 별로 구분되기 때문에 상태 별 동작을 수정하기가 쉽다.
  • 상태 변경은 누가?
    • 콘텍스트의 상태 변경: 비교적 상태 개수가 적고 상태 변경 규칙이 거의 바뀌지 않는 경우에 유리하다.
    • 상태 객체에서 콘텍스트의 상태를 변경: 콘텍스트에 영향을 주지 않으면서 상태를 추가하거나 상태 변경 규칙을 바꿀 수 있게 된다. 하지만, 상태 변경 규칙이 여러 클래스에 분산되어 있기 때문에, 상태 변경 구현 클래스가 많아질 수록, 상태 변경 규칙을 파악하기가 어려워지는 단점이 있다.

데코레이터 패턴

  • 데코레이터 패턴은 상속이 아닌 위임을 하는 방식으로 기능을 확장해 나간다.
  • 데코레이터 패턴의 자점은 데코레이터를 조합하는 방식으로 기능을 확장할 수 있다. 각 확장 기능들의 구현이 별도의 클래스로 분리되기 때문에, 각 확장 기능 및 원래 기능을 서로 영향 없이 변경할 수 있도록 만들어 준다.
  • 데코레이터 구현을 할 때 ,정의되어 있는 메서드가 증가하게 되면 그 만큼 데코레이터의 구현도 복잡해진다.
  • 데코레이터 구현에서 데코레이터 객체가 비정상적으로 동작할 때 어떻게 처리할 것인지 고려해야된다.
  • 데코레이터의 단점은 사용자 입장에서 데코레이터 객체와 실제 구현 객체의 구분ㅇ ㅣ되지 않기 때문에 코드만으로는 기능이 어떻게 동작하는지 이해하기 어렵다.

프록시 패턴

  • 프록시 패턴은 실제 객체를 대신하는 대신 프록시 객체를 사용해서 실제 객체의 생성이나 접근 등을 제어할 수 있도록 해주는 패턴이다.
  • 필요한 순간에 실제 객체를 생성해주는 프록시를 가상 프록시, 실제 객체에 대한 접근을 제어하는 프록시는 보호 프록시라고 부른다.
  • 프록시를 구현할 때 고려할 점은 실제 객체를 누가 생성할 것이냥에 대한 것이다.
  • 가상 프록시는 필요한 순간에 실제 객체를 생성하는 경우가 많기 때문에, 실제 생성할 객체의 타입을 사용하게 된다.
  • 보호 프록시는 객체를 생성할 때 실제 객체를 전달하면 되므로, 실제 객체의 타입을 알 필요 없이 추상화 타입을 사용하면 된다.

어댑터 패턴

  • 어댑터 패턴: 클라이언트가 요구하는 인터페이스와 재사용하려는 모듈의 인터페이스가 일치하지 않을 때 사용할 수 있는 패턴
  • 어댑터 클래스는 재사용하려는 모듈 인터페이스를 클라이언트 인터페이스에 맞춰 주는 책임을 갖는다.
  • 상속을 이용하여 어댑터를 구현하는 경우, 클라이언트 인터페이스가 아닌 일부 구현이 포함된 추상 클래스라면, 자바와 같이 단일 상속만 지원하는 언어에서는 제약을 받게 된다.

옵저버 패턴

  • 옵저버 패턴: 한 객체의 상태 변화를 정해지지 않은 여러 다른 객체에 통지하고 싶을 때 사용되는 패턴이 옵저버 패턴이다.
  • 옵저버 패턴에는 크게 주제 객체와 옵저버 객체가 등장한다. 주제 객체는 다음의 두가지 책임을 갖는다.
    • 옵저버 목록을 관리하고, 옵저버를 등록하고 제거할 수 있는 메서드를 제공한다.
    • 상태의 변경이 발생하면 등록된 옵저버에 변경 내역을 알린다.
  • 옵저버 객체의 메서드를 호출할 때 전달한 객체만으로는 옵저버의 기능을 구현할 수 없을 수도 있다. 이렇게 되면 옵저버 클래스가 다시 콘크리트 주제 클래스에 의존하게 된다.
  • 한 개의 옵저버 객체는 여러 주제 객체에 등록할 수도 있다.
  • 옵저버 패턴 구현의 고려 사항
    • 주제 객체의 통지 기능 실행 주체: 필요에 따라 주제 객체를 사용하는 코드에서 통지 기능을 수행할 수도 있다. 하지만 객체의 상태가 바뀔 때마다 옵저버에게 통지를 해주어야 한다면, 주제 객체에서 직접 통지기능을 실행하는 것이 구현에 유리하다. 상태를 변경하는 모든 코드에서 통지 기능을 함께 호출해 주어야 하는데, 이런 방식은 통지 기능을 호출하지 않는 등 개발자의 실수를 유발할 수 있기 때문이다. 반대로, 한 개 이상의 주제 객체의 연속적인 상태 변경 이후에 옵저버에게 통지를 해야 한다면, 주제 객체가 아닌 주제 객체의 상태를 변경하는 코드에서 통지 기능을 실행해 주도록 구현하는 것이 통지 시점을 관리하기 편하다.
    • 옵저버 인터페이스의 분리: 주제 객체가 통지할 수 있는 상태 변경 내역의 종류가 다양한 경우에는 각 종류 별로 옵저버 인터페이스를 분리해서 구현하는 것이 좋다. 주제 객체 입장에서도 각 상태마다 변경의 이유가 다르기 때문에, 이들은 한 개의 옵저버 인터페이스로 관리하는 것을 향후에 변경을 어렵게 만드는 요인이 될 수 있다.
    • 통지 시점에서의 주제 객체 상태: 통지 시점에서 주제 객체의 상태에 결함이 없어야 한다.
    • 옵저버 객체의 실행 제약 조건: 옵저버 객체를 실행할 때 옵저버 마다 십 분 이상 걸린다면 성능에 치명적이다. 따라서 옵저버 인터페이스를 정의할 때에는 옵저버 메서드의 실행 제한에 대한 명확한 기준이 필요하다. 5초 이내에 응답을 처리하던가, 옵저버 자체를 비동기로 실행하게 하는 등의 방법이 잇다.

미디에이터 패턴

  • 객체 간의 메시지 흐름을 각 클래스에 직접적인 의존으로 구현하게 되면, 개별 클래스의 재사용이 어려워지고 메시지 흐름을 변경하려면 관련된 클래스들을 모두 변경해야주어야 하는 문제가 발생하게 된다.
  • 위의 문제를 해결하기 위해 미디에이터 패턴을 사용한다. 각 객체들이 직접 메시지를 주고받는 대신, 중간에 중계 역할을 수행하는 미디에이터 객체를 두고 미디에이터를 통해서 각 개체들이 간접적으로 메시지를 주고받도록 한다.
  • 미디에이터 패턴을 적용할 떄 협업 객체 간의 메시지 흐름이 서로 다른 기능에 반복해서 사용될 경우, 미디에이터 추상 클래스를 사용함으로써 미디에이터 자체의 재사용을 높일 수 있다.

파사드 패턴

  • 여러 개의 Dao를 접근하는 클래스가 많은 경우 코드 중복과 직접적인 의존을 맺게되는 문제점을 가진다. 이를 해결하기위해 파사드 패턴을 사용한다.
  • 파사드 패턴은 서브 시스템에 속한 각 Dao를 이용해서 클라이언트가 원하는 데이터를 제공하기 위한 인터페이스를 제공한다.
  • 파사드 패턴을 이용하면 클라이언트의 영향 없이 알맞은 콘크리트 파사드 패턴을 구현할 수 있다.

추상 팩토리 패턴

  • 조건에 따라 생성 규칙이 달라지는 경우 추상 팩토리 패턴을 사용할 수 있다. 객체 생성 책임을 분리한다.
  • DI를 사용할 수도 있다. DI를 사용하면 생성자나 설정 메서드를 통해 Factory 객체를 전달받게 되므로, Factory 클래스에 getFactory() 메서드를 정의할 필요가 없어진다.
  • 팩토리가 생성하는 객체가 늘 동일한 상태를 갖는다면, 프로토타입 방식으로 팩토리를 구현할 수 있다. 프로토타입 방식은 생성할 객체의 원형 객체를 등록하고, 객체 생성 요청이 있으면 원형 객체를 복제해서 생성한다.

컴포지트 패턴

  • 객체의 그룹을 만들고 그룹에서 한꺼번에 동작하는 메서드를 추가하면 동일한 코드가 중복된다는 단점이있다. 이 단점을 해소하기 위해 사용되는 패턴이 컴포지트 패턴이다.
  • 컴포지트 패턴에서 컴포지트는 다음의 책임을 갖는다.
    • 컴포넌트 그룹을 관리한다.
    • 컴포지트에 기능 실행을 요청하면, 컴포지트는 포함하고 있는 컴포넌트에게 기능 실행 요청을 위임한다.
  • 전체냐 부분이냐에 상관없이 클라이언트는 단일 인터페이스로 기능을 실행할 수 있는 장점이 생긴다.
  • 컴포지트 패턴을 구현할 때 고려할 점은 컴포넌트를 관리하는 인터페이스를 어디서 구현할지에 대한 여부다.
    • 인터페이스에 컴포넌트 관리하는 메서드를 추가하고, 그룹이 아닐시 익셉션을 던지는 방법이 있다.
    • 익셉션을 발생시키는 방법보다 조금 더 나은 방법으로, 컴포넌트를 추가할 수 있는지 여부를 판단해주는 기능을 정의하는 것이다.

널 객체 패턴

  • null 검사 코드 누락을 피하기 위하여 null 대신 사용될 클래스를 구현한다.
  • 이 클래스는 상위 타입을 상속받으며, 아무 기능도 수행하지 않는다.
  • null을 리턴하는 대신, null을 대체할 클래스의 객체를 리턴한다.

댓글남기기