디자인 패턴
- 객체지향 프로그래밍 설계를 할 때 자주 발생하는 문제들을 피하기 위해 사용하는 패턴
- 목적
- 재사용성, 호환성, 유지보수성
- 원칙 - SOLID(객체지향 설계 원칙)
- Single Reponsibility Principle(SRP) : 하나의 클래스는 하나의 동작만
- Open - Close Principle : 확장(상속)에는 열려있고, 수정에는 닫기
- Liskov Substitution Principle : 자식이 부모의 자리에 항상 교체될 수 있어야 한다.
- Interface Segregation Principle : 인터페이스가 잘 분리되서, 클래스가 꼭 필요한 인터페이스만 구현하도록 제어
- Dependency Inversion Property : 상위모듈이 하위 모듈에 의존적 X. 둘 다 추상화에 의존해야 한다.
디자인 패턴의 3가지 분류
1. 생성 패턴 : 객체의 생성 방식 결정
- 싱글톤 패턴(Singleton)
- 하나의 인스턴스만 사용할 수 있도록 객체를 생성하는 방법
- 객체가 여러 번 생성되지 않고, 최초 하나의 인스턴스만 생성 후 이 인스턴스만 참조하게 된다.
- 커넥션 풀, 스레드 풀, 디바이스 설정 객체 등의 경우 인스턴스를 여러 개 만들게 되면 자원을 낭비하게 된다.
- 버그를 발생시킬 수 있는 위험으로 오직 하나만 생성하고 그 인스턴스를 사용하는 것이 목적
public class Singleton {
private static Singleton singletonObject;
private Singleton() {}
public static Singleton getInstance() {
if (singletonObject == null) {
singletonObject = new Singleton();
}
return singletonObject;
}
}
- 멀티 스레드 환경에서는 싱글턴 패턴을 사용하다가 동시 접근을 해야 하는 상황이 생길 경우, 인스턴스가 두 개 생성될 위험이 잇다. synchronzied 사용
- 동기화 사용 : 멀티 코어에서 하나의 CPU를 제외한 다른 CPU가 Lock에 걸릴 위험이 있다.
- 사전 초기화 : 클래스 로딩 시점에 미리 객체를 생성하고, 그 객체를 반환한다.
- volatile 키워드 사용 : 캐시에 저장된 값 X -> main memory 값 사용
- 메모리 효율이냐 연산 효율은 사후 초기화보다 낮다.
public class Singleton {
private static volatile Singleton singletonObject = new Singleton();
private Singleton() {}
public static Singleton getSingletonObject() {
return singletonObject;
}
}
- 프로토타입 패턴(Prototype)
- 기존의 인스턴스를 그대로 복제(clone)해서 새로운 객체를 생성하는 기법
- 싱글톤과 반대의 개념
- 데이터베이스에서 받아온 데이터 인스턴스를 하나 더 만들어야 하는 경우 사용
- 팩토리 패턴(Factory)
- 인스턴스를 만드는 공장(Factory)를 통해 객체를 생성하는 방법
- 인스턴스를 직접 생성하지 않고, 객체 반환 함수를 (생성자 대신) 제공
- 초기화 과정을 외부에서 보지 못하게 숨기고 반환 타입을 제어할 수 있다.
- 비슷한 분류의 클래스들의 생성을 모아 처리하는게 편한 경우 사용
- 추상 팩토리(Abstract Factory)
- 공장을 만드는 상위 공장을 먼저 정의한다.
- 많은 수의 연관된 서브 클래스를 특정 그룹으로 묶어 한 번에 교체할 수 있다.
- 빌더(Builder)
- 객체를 생성할 때 필요한 파라미터가 많은 경우, 인스턴스를 생성자를 통해 직접 생성하지 않고 빌더라는 내부 클래스로 간접적으로 생성하게 하는 패턴
- 클래스와 사용 대상의 결합도를 낮추기 위해 사용
- 생성자에 전달하는 인수에 의미를 부여하기 위해 사용
2. 구조 패턴 : 객체간의 관계를 조직
- 어댑터 패턴(Adapter)
- 서로 다른 클래스의 인터페이스를 연결하고자 사용하는 패턴
- 인터페이스는 바뀌어도, 변경 내역은 어댑터에 캡슐화 되어 클라이언트는 바뀔 필요가 없다.
- ex. 110V -> 220V 플러그
- 컴포지트 패턴(Composite)
- 객체의 hierarchies(계층) 표현
- 각각의 객체를 독립적으로 동일한 인터페이스를 통해 처리할 수 있게 한다.
- 여러 개의 클래스가 크게 보면 같은 요소(Component)에 속하지만, 여기에 속한 어떤 클래스(Composite)가 자신이나 다른 클래스(Leaf)를 가질 수 있는 구조
- 트리 형태의 클래스
- 데코레이터와 비교
- 데코레이터 패턴 : 객체가 상황에 따라 다양한 기능이 추가되거나 삭제되어야 할 경우(크리스마스 트리 장식)
- 공통점 : composition이 재귀적으로 발생
- 차이점 : 데코레이터 패턴은 responsibilityes를 추가하는 것이 목적
- composite 패턴은 계층구조를 표현하기 위해서 사용
- 프록시(Proxy) 패턴
- 연산을 스스로 객체가 직접 처리하는 것이 아니라, 숨겨진 객체를 통해 처리하는 방법
- 구체적 업무 담당 클래스 접근 전에, 간단 사전 작업 처리 클래스(Proxy)를 두는 구조
3. 행위 패턴 : 객체의 행위를 조직, 관리, 연합
- 스트레티지(Strategy) 패턴
- 어떤 동작을 하는 로직을 정의하고, 이것들을 하나로 묶어(캡슐화) 관리하는 패턴
- 새로운 로직을 추가하거나 변경할 때, 한 번에 효율적으로 변경이 가능하다.
- 미사일 쏘기, 폭탄 사용 캡슐화
- 전투기와 헬리콥터를 묶을 Unit 추상 클래스 생성
[ 슈팅 게임을 설계하시오 ]
유닛 종류 : 전투기, 헬리콥터
유닛들은 미사일을 발사할 수 있다.
전투기는 직선 미사일을, 헬리콥터는 유도 미사일을 발사한다.
필살기로는 폭탄이 있는데, 전투기에는 있고 헬리콥터에는 없다.
- 템플릿 메소드(Template Method) 패턴
- 로직을 단계 별로 나누어야 하는 상황에서 사용한다.
- 상위 클래스가 뼈대
- 하위 클래스가 각각 로직 구현
- 단계별로 나눈 로직들이 앞으로 수정될 가능성이 있을 때 효율적이다.
- 조건
- 클래스는 추상 클래스로 만든다. -> 자식 클래스에서 선택적 메소드 오버라이딩 가능
- abstract : 부모의 기능을 자식에서 확장시켜나가고 싶을 때
- interface : 해당 클래스가 가진 함수의 기능을 활용하고 싶을 때
- 단계를 진행하는 메소드는 수정이 불가능하도록 final 키워드 사용
- 각 단계들은 외부는 막고, 자식들만 활용 가능하도록 protected 으로 선언
- 클래스는 추상 클래스로 만든다. -> 자식 클래스에서 선택적 메소드 오버라이딩 가능
- 예시 (반죽 -> 토핑 -> 굽기)
- 피자 종류에 따라 토핑만 바꿔준다.
- 로직을 단계 별로 나누어야 하는 상황에서 사용한다.
- 옵저버(Observer) 패턴
- 서로의 정보를 주고 받는 과정에서 단위가 크면, 객체 규모가 크면 복잡성 증가
- 객체의 변경은 복잡한 코드에서 객체의 변경 추적을 어렵게 한다.
- 객체의 변경을 추적하기 위해 옵저버 패턴을 사용하여 객체를 참조하는 모든 이들에게 변경 사실을 알리고 대처하는 추가 대응이 필요하다.
- 인터페이스를 통해 연결하여 느슨한 결합성을 유지하고, Publisher와 Observer 인터페이스 적용