[디자인 패턴이란?]
소프트웨어 공학에서의 디자인 패턴(Design pattern)이란 프로그램 개발 시에 자주 마주하는 애로 사항에 대한 일반적이고 재사용 가능한 추상화된 해결책을 말한다.
간단하게 예를 들자면 어떤 시스템에 최고 관리자가 존재한다고 가정하자. 이 사람에게만 주어지는 슈퍼 계정을 만들어야 하는데 이러한 슈퍼 계정은 두 개 이상 만들 이유도 없고, 해서도 안된다(다른 사람이 해당 계정을 사용할 수 있으므로). 이런 문제 상황에서 이를 해결하기 위한 디자인 패턴으로 Singleton 패턴이 존재한다.
디자인 패턴은 일반화된 해결책이고(특정한 구현이 아님) 추후 재사용, 호환, 유지 보수시 발생하는 문제 해결을 위해 만들어 둔 것을 의미한다.
* 원칙 (객체지향 설계 원칙)
- Single Responsibility Principle
하나의 클래스는 하나의 역할만 해야 함.
- Open Closed Principle
확장(상속)에는 열려있고, 수정에는 닫혀 있어야 함.
높은 응집도(Cohesion)와 낮은 결합도(Coupling)
- Liskov Substitution Principle
자식이 부모의 자리에 항상 교체될 수 있어야 함.
- Interface Segregation Principle
인터페이스가 잘 분리되어서, 클래스가 꼭 필요한 인터페이스만 구현하도록 해야함.
- Dependency Inversion Property
상위 모듈이 하위 모듈에 의존하면 안됨.
둘 다 추상화에 의존하며, 추상화는 세부 사항에 의존하면 안됨.
* 분류
- 3가지 패턴의 목적을 이해하기
1) 생성 패턴(Creational) : 객체의 생성 방식 결정
- Class-creational patterns(상속), Object-creational patterns(위임) (상속과 위임)
예) DBConnection을 관리하는 Instance를 하나만 만들 수 있도록 제한하여, 불필요한 연결을 막음.
2) 구조 패턴(Structural) : 객체간의 관계를 조직
예) 2개의 인터페이스가 서로 호환이 되지 않을 때, 둘을 연결해주기 위해서 새로운 클래스를 만들어서 연결시킬 수 있도록 함.
3) 행위 패턴(Behavioral) : 객체의 행위를 조직, 관리, 연합
예) 하위 클래스에서 구현해야 하는 함수 및 알고리즘들을 미리 선언하여, 상속이 이를 필수로 구현하도록 함.
1. 어댑터 패턴 (Adapter Pattern)
- 용도 : 클래스를 바로 사용할 수 없는 경우(다른 곳에서 개발했거나, 수정할 수 없을 때) 중간에서 변환 역할을 해주는 클래스가 필요 -> 어댑터 패턴
- 사용 방법 : 상속
- 한 클래스의 인터페이스를 클라이언트에서 사용하고자하는 다른 인터페이스로 변환한다.
- 어댑터를 이용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있다.
[클래스 다이어그램]
기존의 3.5파이 이어폰은 아이폰에 사용할 수 없다.
기존 이어폰을 아이폰에 사용하기 위해서는 변환 어댑터를 따로 구매해서 연결해야 한다.
이처럼 어댑터는 필요로 하는 인터페이스로 바꿔주는 역할을 한다.
이처럼 업체에서 제공한 클래스가 기존 시스템에 맞지 않을 경우?
기존 시스템을 수정하지 말고, 어댑터를 활용해서 유연하게 해결하자
[코드를 이용한 어댑터 패턴 이해하기]
오리와 칠면조 인터페이스 생성
만약 오리 객체가 부족해서 칠면조 객체를 대신 사용해야 한다면?
두 객체는 인터페이스가 다르므로, 바로 칠면조 객체를 사용하는 것은 불가능하다
따라서, 칠면조 어댑터를 생성해서 활용해야 한다.
(Duck.java)
1
2
3
4
|
public interface Duck{
public void quack();
public void fly();
}
|
cs |
(Turkey.java)
1
2
3
4
|
public interface Turkey{
public void gobble();
public void fly();
}
|
cs |
(HelloDuck.java)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class HelloDuck implements Duck{
@Override
public void quack() {
System.out.println("Quack Quack");
}
@Override
public void fly() {
System.out.println("Duck Flying");
}
}
|
cs |
(ByeTurkey.java)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class ByeTurkey implements Turkey{
@Override
public void gobble() {
System.out.println("Gobble Gobble");
}
@Override
public void fly() {
System.out.println("Turkey Flying");
}
}
|
cs |
(TurkeyAdapter.java)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public class TurkeyAdapter implements Duck{
Turkey turkey;
public TurkeyAdapter(Turkey turkey) {
this.turkey=turkey;
}
@Override
public void quack() {
turkey.gobble();
}
@Override
public void fly() {
turkey.fly();
}
}
|
cs |
(DuckTest.java)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public class DuckTest {
public static void main(String[] args) {
HelloDuck duck = new HelloDuck();
ByeTurkey turkey = new ByeTurkey();
Duck turkeyAdapter = new TurkeyAdapter(turkey);
System.out.println("The turkey says...");
turkey.gobble();
turkey.fly();
System.out.println();
System.out.println("The duck says..");
testDuck(duck);
System.out.println("The turkeyAdapter says...");
testDuck(turkeyAdapter);
}
public static void testDuck(Duck duck) {
duck.quack();
duck.fly();
System.out.println();
}
}
|
cs |
(실행 결과)
2. 싱글톤 패턴(Singleton Pattern)
- 애플리케이션이 시작될 때, 어떤 클래스가 최초 한 번만 메모리를 할당하고 해당 메모리에 인스턴스를 만들어서 사용하는 패턴
- 즉, 싱글톤 패턴은 '하나'의 인스턴스만 생성하여 사용하는 디자인 패턴이다. (인스턴스가 필요할 때, 똑같은 인스턴스를 만들지 않고 기존의 인스턴스를 활용하는 것)
- 생성자가 여러번 호출되어도, 실제로 생성되는 객체는 하나이며 최초로 생성된 이후에 호출된 생성자는 이미 생성한 객체를 반환시키도록 만드는 것이다.
- java에서는 생성자를 private으로 선언해 다른 곳에서 생성하지 못하도록 만들고, getInstance() 메소드를 통해 받아서 사용하도록 구현한다.
- 한 번의 new를 통해 객체를 생성하기 때문에 메모리 낭비를 방지할 수 있다.
- 싱글톤으로 구현한 인스턴스는 '전역'이므로, 다른 클래스의 인스턴스들이 데이터를 공유하는 것이 가능하다는 장점이 있다.
[언제 많이 사용할까?]
데이터베이스에서의 커넥션풀, 스레드풀, 캐시, 로그 기록 객체 등
또한 인스턴스가 절대적으로 한 개만 존재하는 것을 보증하고 싶을 때 사용
[단점]
- 만약 싱글톤 인스턴스가 혼자 너무 많은 일을 하거나, 많은 데이터를 공유시키면 다른 클래스들 간의 결합도가 높아지게 되는데, 이는 개방-폐쇄 원칙(OCP)에 위배된다.
- 결합도가 높아지게 되면, 유지보수가 힘들고 테스트도 원활하게 진행할 수 없는 문제점이 발생한다.
- 또한, 멀티 스레드 환경에서 동기화 처리를 하지 않았을 때, 인스턴스가 2개가 생성되는 문제가 발생할 수도 있다.
- 따라서, 반드시 싱글톤이 필요한 상황이 아니면 지양하는 것이 좋다고 한다.
[코드]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class Printer {
private static Printer printer = null;
private Printer(){}
public static Printer getInstance(){
if(printer == null) {
printer = new Printer();
}
return printer;
}
public void print(String input){
System.out.println(input);
}
}
|
cs |
3. 템플릿 메서드 패턴(Template Method Pattern)
- 어떤 작업을 처리하는 일부분을 서브 클래스로 캡슐화해 전체 일을 수행하는 구조는 바꾸지 않으면서 특정 단계에서 수행하는 내역을 바꾸는 패턴
- 즉, 전체적으로는 동일하면서 부분적으로는 다른 구문으로 구성된 메서드의 코드 중복을 최소화 할 때 유용하다.
- 다른 관점에서 보면 동일한 기능을 상위 클래스에서 정의하면서 확장/변화가 필요한 부분만 서브 클래스에서 구현할 수 있도록 한다.
- 추상 클래스(Abstract Class)
- 템플릿 메서드를 정의하는 클래스
- 하위 클래스에 공통 알고리즘을 정의하고 하위 클래스에서 구현될 기능을 primitive 메서드 또는 hook 메서드로 정의하는 클래스
- 구현 클래스(Concrete Class)
- 물려받은 primitive 메서드 또는 hook 메서드를 구현하는 클래스
- 상위 클래스에 구현된 템플릿 메서드의 일반적인 알고리즘에서 하위 클래스에 적합하게 primitive 메서드나 hook 메서드를 오버라이드하는 클래스
[장점]
- 구현 클래스에서는 추상 클래스에 선언된 메소드만 사용하므로, 핵심 로직 관리가 용이
- 객체 추가 및 확장 가능
[단점]
- 추상 메소드가 많아지면, 클래스 관리가 복잡
[코드]
(HouseTemplate.java)
Template 추상 클래스를 하나 생성
이 HouseTemplate을 사용할 때는, "HouseTemplate houseType = new WoodenHouse()" 이런 식으로 사용
HouseTemplate 내부에 **buildHouse** 라는 변해서는 안되는 핵심 로직을 만들어 놓고(장점 1)
Template 클래스 내부의 **핵심 로직 내부의 함수**를 상속하는 클래스가 직접 구현하도록, abstract를 지정해 둠.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public abstract class HouseTemplate{
// 이런 식으로 buildHouse라는 함수(핵심 로직)를 선언해 둠.
public final void buildHouse(){
buildFoundation();
buildPillars();
buildWalls();
buildWindows();
System.out.println("House is built");
}
// buildFoundation(); 정의 부분 (1)
// buildPillars(); 정의 부분 (2)
// 위의 두 함수와는 다르게 이 클래스를 상속 받는 클래스가 별도로
// 구현했으면 하는 메소드들은 abstract로 선언하여, 정의하도록 함
public abstract void buildWalls(); // (3)
public abstract void buildWindows(); // (4)
}
|
cs |
(WoodenHouse.java)
HouseTemplate을 상속받는 클래스.
Wooden이나, Glass에 따라서 buildHouse 내부의 핵심 로직이 바뀔 수 있으므로,
이 부분을 반드시 선언하도록 지정해둠.
1
2
3
4
5
6
7
8
9
10
11
12
|
public class WoodenHouse extends HouseTemplate{
@Override
public void buildWalls(){
System.out.println("Building Wooden Walls");
}
@Override
public void buildPillars(){
System.out.println("Building Pillars with Wood coating");
}
}
|
cs |
4. 팩토리 메소드 패턴(Factory Method Pattern)
객체를 생성하기 위해 인터페이스를 정의하지만, 어떤 클래스의 인스턴스를 생성할지에 대한 결정은 서브클래스가 내리도록 한다.
- 객체 생성을 캡슐화하는 패턴이다.
- Creator의 서브클래스에 팩토리 메소드를 정의하여, 팩토리 메소드 호출로 적절한 ConcreteProduct 인스턴스를 반환하게 한다.
- 팩토리 메소드 패턴을 사용하는 이유는 클래스간의 결합도를 낮추기 위해서다.
- 직접 객체를 생성해 사용하는 것을 방지하고 서브 클래스에 위임함으로써 보다 효율적인 코드 제어를 할 수 있고 의존성을 제거한다. 결과적으로 결합도가 낮춰진다.
[코드]
* Product 인터페이스
1
2
3
|
abstract class Product{
public abstract void use();
}
|
cs |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class IDCard extends Product{
private String owner;
public IDCard(String owner){
System.out.println(owner + "의 카드를 만듭니다.");
this.owner = owner;
}
@Override
public void use(){
System.out.println(owner + "의 카드를 사용합니다.");
}
public String getOwner(){
return owner;
}
}
|
cs |
* createProduct 메소드라 팩토리 메소드이다.
1
2
3
4
5
6
7
8
9
10
|
abstract class Factory {
public final Product create(String owner){
Product p = createProduct(owner);
registerProduct(p);
return p;
}
protected abstract Product createProduct(String owner);
protected abstract void registerProdcut(Product p);
}
|
cs |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class IDCardFactory extends Factory{
private List<String> owners = new ArrayList<>();
@Override
protected Product createProduct(String owner){
return new IDCard(owner);
}
@Override
protected void registerProduct(Product p){
owners.add(((IDCard) p).getOwner());
}
public List<String> getOwners(){
return owners;
}
}
|
cs |
사용은 다음과 같이 한다.
1
2
3
4
5
6
7
|
Factory factory = new IDCardFactory();
Product card1 = factory.create("부정수");
Product card2 = factory.create("장윤정");
Product card3 = factory.create("최원대");
card1.use();
card2.use();
card3.use();
|
cs |
5. 옵저버 패턴(Observer Pattern)
상태를 가지고 잇는 주체 객체 & 상태의 변경을 알아야 하는 관찰 객체가 존재하며 이들의 관계는 1:1 또는 1:N이 될 수 있다.
서로의 정보를 넘기고 받는 과정에서 정보의 단위가 클 수록, 객체들의 규모가 클 수록, 각 객체들의 관계가 복잡할 수록 점점 구현하기 어려워지고 복잡성이 매우 증가할 것이다.
이 때 가이드라인을 제시해 주는 것이 바로 옵저버 패턴이다.
[옵저버 패턴에서 말하는 주제 객체와 관찰 객체의 예는?]
잡지사 : 구독자
우유배달업체 : 고객
구독자, 고객들은 정보를 얻거나 받아야 하는 주체와 관계를 형성하게 된다. 관계가 지속되다가 정보를 원하지 않으면 해제할 수도 있다.
이때, 객체와의 관계를 맺고 끊는 상태 변경 정보를 Observer에 알려줘서 관리하는 것을 말한다.
- Publisher 인터페이스
Observer들을 관리하는 메소드를 가지고 있음
옵저버 등록(add), 제외(delete), 옵저버들에게 정보를 알려줌(notifyObserver)
1
2
3
4
5
|
public interface Publisher{
public void add(Observer observer);
public void delete(Observer observer);
public void notifyObserver();
}
|
cs |
- Observer 인터페이스
정보를 업데이트(update)
1
2
3
|
public interface Observer{
public void update(String title, String news);
}
|
cs |
- NewsMachine 클래스
Publisher를 구현한 클래스로, 정보를 제공해주는 퍼블리셔가 됨.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
public class NewsMachine implements Publisher{
private ArrayList<Observer> observers;
private String title;
private String news;
public NewsMachine(){
observers = new ArrayList<>();
}
@Override
public void add(Observer observer){
observers.add(observer);
}
@Override
public void delete(Observer observer){
int index = observers.indexOf(observer);
observers.remove(index);
}
@Override
public void notifyObserver(){
for(Observer observer : observers){
observer.update(title, news);
}
}
public void setNewsInfo(String title, String news){
this.title = title;
this.news = news;
notifyObserver();
}
public String getTitle(){ return title; }
public String getNews() { return news; }
}
|
cs |
- AnnualSubscriber, EventSubscriber 클래스
Observer를 구현한 클래스들로, notifyObserver()를 호출하면서 알려줄 때마다 update가 호출됨
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public class AnnualSubscriber implements Observer{
private String newsString;
private Publisher publisher;
public AnnualSubscriber(Publisher publisher){
this.publisher = publisher;
publisher.add(this);
}
@Override
public void update(String title, String news){
newsString = title + " \n ========== \n " + news;
display();
}
public void withdraw(){
publisher.delete(this);
}
public void display(){
System.out.println("\n\n오늘의 뉴스 \n=======================\n\n" + newsString);
}
}
|
cs |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public class EventSubscriber implements Observer{
private String newsString;
private Publisher publisher;
public EventSubscriber(Publisher publisher){
this.publisher = publisher;
publisher.add(this);
}
@Override
public void update(String title, String news){
newsString = title + " \n ===================\n " + news;
display();
}
public void withdraw(){
publisher.delete(this);
}
public void display(){
System.out.println("\n\n=== 이벤트 유저 ===");
System.out.println("\n\n" + newsString);
}
}
|
cs |
- main 함수
1
2
3
4
5
6
7
8
9
10
|
public class MainClass{
public static void main(String[] args){
NewsMachine newsMachine = new NewsMahcine();
AnnualSubscriber as = new AnnualSubscriber(newsMachine);
EvenrSubscriber es = new EvenrSubscriber(newsMachine);
newsMachine.setNewsInfo("오늘 한파", "전국 영하 18도 입니다.");
newsMachine.setNewsInfo("벚꽃 축제합니다.", "다같이 벚꽃보러~");
}
}
|
cs |
[정리]
옵저버 패턴은, 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들에게 연락이 가고, 자동으로 정보가 갱신되는 1:N 관계(혹은 1:1)를 정의한다.
인터페이스를 통해 연결하여 느슨한 결합성을 유지하며, Publisher와 Observer 인터페이스를 적용한다.
6. 전략 패턴(Strategy Pattern)
동일 계열의 알고리즘군을 정의하고, 각 알고리즘을 캡슐화하며, 이들을 상호교환이 가능하도록 만든다.
알고리즘을 사용하는 클라이언트와 상관없이 독립적으로 알고리즘을 다양하게 변경할 수 있게 한다.
[코드]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public class Duck{
QuackBehavior quackBehavior; // interface
FlyBehavior flyBehavior; // interface
public void performQuack(){
quckBehavior.quack();
}
public void performFly(){
flyBehavior.fly();
}
public void setQuackBehavior(QuackBehavior quackBehavior){
this.quackBehavior = quackBehavior;
}
public void setFlyBehavior(FlyBehavior flyBehavior){
this.flyBehavior = flyBehavior;
}
}
|
cs |
오리가 "소리를 내는 행동"과 "나는 행동"의 구현이 분리되어 있고, setter를 통해 바꿀 수도 있다는 것을 알 수 있다. 즉, 소리를 내는 알고리즘과 나는 알고리즘이 위임되어 있는 것이다.
이번엔 FlyBehavior 인터페이스와 구현체를 살펴보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public interface FlyBehavior{
void fly();
}
public class FlyWithWings implements FlyBehavoior{
@Override
public void fly(){
System.out.println("날개로 날아간다");
}
}
public class FlyRocketPowered implements FlyBehavior{
@Override
public void fly(){
System.out.println("로켓 추진으로 날아간다");
}
}
|
cs |
QuackBehavior는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
|
public interface QuackBehavior{
void quack();
}
public class KoreanQuack implements QuackBehavior{
@Override
public void quack(){
System.out.println("꽥꽥");
}
}
|
cs |
새로운 종류의 오리를 만들 때, flyBehavior의 설정에 따라서 나는 방법을 다르게 설정할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class HelloDuck extends Duck{
public HelloDuck(){
quackBehavior = new KoreanQuack();
flyBehavior = new FlyWithWings(); // 날개로 날아간다.
}
}
public class HelloDuck extends Duck{
public HelloDuck(){
quackBehavior = new KoreanQuack();
flyBehavior = new FlyRocketPowered(); // 로켓 추진으로 날아간다.
}
}
|
cs |
setter가 있으므로 중간에 전략을 바꿀 수도 있다.
1
2
3
4
5
6
7
8
9
10
11
12
|
Duck myDuck = new HelloDuck();
myDuck.performFly(); // 날개로 날아간다.
myDuck.performQuack(); // 꽥꽥
myDuck.setFlyBehavior(new FlyRocketPowered());
myDuck.performFly(); // 로켓 추진으로 날아간다.
// lambda
myDuck.setFlyBehavior(
() -> System.out.println("바람을 타고 날아간다");
);
myDuck.performFly(); // 바람을 타고 날아간다.
|
cs |
[정리]
"클라이언트가 전략을 생성해 전략을 실행할 컨텍스트에게 주입하는 패턴이다."
※ 참고
https://johngrib.github.io/wiki/strategy-pattern/
https://gmlwjd9405.github.io/2018/07/13/template-method-pattern.html
https://shoark7.github.io/programming/knowledge/what-is-design-pattern
https://johngrib.github.io/wiki/factory-method-pattern/#fn:java-example
https://github.com/gyoogle/tech-interview-for-developer
'CS' 카테고리의 다른 글
Inheritance와 Delegation (상속과 위임) (0) | 2020.09.05 |
---|---|
PCB (0) | 2020.08.29 |
Process / Thread (0) | 2020.08.28 |
REST (1) | 2020.08.22 |
Gradle (0) | 2020.08.22 |
댓글