'OCP'에 해당되는 글 2건

  1. 2019.08.18 :: 디자인패턴 - 데커레이터 패턴(Decorator Pattern)
  2. 2018.04.28 :: SOLID 원칙이란?

decoration은 '장식(포장)'이란 뜻이다. 빵집에서 케이크를 만들 때 먼저 둥근 모양의 빵을 만든다. 이 위에 초콜릿을 바르면 초콜릿 케이크가 되고, 치즈를 바르면 치즈 케이크가 된다. 또 생크림을 바르고 과일을 많이 올려놓으면 과일 생크림 케이크가 된다.

 

이처럼 기존에 구현되어 있는 클래스(둥근 모양의 빵)에 그때그때 필요한 기능(초콜릿, 치즈, 생크림)을 추가(장식, 포장)해나가는 설계 패턴을 decorator 패턴이라고 한다. 이것은 기능 확장이 필요할 때 상속의 대안으로 사용한다.

그림에서 decorator 클래스가 기존에 구현되어 있는 클래스(둥근 모양의 빵)에 해당되고, concreteDecorator클래스는 그때그때 필요한 기능(초콜릿, 치즈, 생크림)을 추가(장식, 포장)해나가는 것에 해당된다.

 

카페 음료 가격을 계산하기 위해 우리는 클래스를 설계해야 한다고 하자. 보통 가장 많이 생각할 수 있는 방안이 상속을 이용한 구현이다. 아래와 같이 쉽게 구현할 수 있다.

 

기본적으로 커피를 제조할때 에스프레소를 내리고 해당 에스프레소에 다른 첨가물을 첨가하여 커피를 제조한다. 예를 들어 카페모카는 에스프레소에 우유,초코가 들어가므로 에스프레소 가격(2)+우유,초코(2)로 총 4라는 가격이 책정된다고 생각해보자. 그렇다면 상속을 이용하여 아래와 같이 구현할 수 있다.

 

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
37
38
39
40
41
public abstract class Beverage {
    
    private String description;
    
    public String getDescription() {
        return description;
    }
    
    public void setDescription(String description) {
        this.description = description;
    }
 
    public abstract int cost();
}
 
public class Espresso extends Beverage{
    
    public Espresso() {
        super.setDescription("에스프레쏘");
    }
 
    @Override
    public int cost() {
        return 2;
    }
    
}
 
public class CafeMocha extends Beverage{
    
    public CafeMocha() {
        super.setDescription("카페모카");
    }
    
    @Override
    public int cost() {
        return 2+2;
    }
    
    
}
cs

 

위와 같이 새로운 음료가 추가되었으므로, 새로운 클래스가 추가된다. 만약 휘핑크림이 추가된 카페모카라면? 아래와 같이 추가적인 클래스가 생성되어야 할 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CafeMochaWithWhip extends Beverage{
 
    public CafeMochaWithWhip() {
        super.setDescription("카페모카 휘핑크림 추가");
    }
    
    @Override
    public int cost() {
        return 2+2+1;
    }
    
    
}
cs

 

얼핏보면 괜찮은 것 같지만, 만약 음료의 종류가 엄청나게 많고 선택옵션이 엄청나게 많다면? 클래스수는 기하급수적으로 늘어날 것이다. 이것은 역시 디자인 원칙중 OCP를 위반하고 있는 것이다.(개방-폐쇄 원칙)

 

이러한 상황에서 사용하는 디자인 패턴이 데코레이터 패턴이다!

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class Beverage {
    
    private String description;
    
    public String getDescription() {
        return description;
    }
    
    public void setDescription(String description) {
        this.description = description;
    }
 
    public abstract int cost();
}
cs

 

데코레이터 패턴은 구성이라는 방법을 사용한다. 즉, 추상 타입을 가지는 데코레이터들 혹은 기본구성 클래스들이어야만 데코레이터를 이용할 수 있다. 왜냐하면 뒤에서 설명하겠지만 데코레이터 역할을 클래스들이 모두 추상타입의 구현체들을 감싸게 되는 구조이기 때문이다. 즉, 위 추상클래스는 기본구성(에스프레쏘) 그리고 데코레이터(선택옵션,모카,휘핑이 들어간 모카 등)들의 타입을 마춰주기 위한 추상타입의 역할을 할 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
public class Espresso extends Beverage{
    
    public Espresso() {
        super.setDescription("에스프레쏘");
    }
 
    @Override
    public int cost() {
        return 2;
    }
    
}
cs

 

추상타입의 Beverage를 상속하는 기본구성을 구현한 예제이다. 데코레이터 패턴의 핵심은 기본 구성으로부터 출발하여 이것저것 데코레이터 옵션을 덧붙여나가는 것이다. 즉, 데코레이터 패턴의 시작이 되는 기본구성은 바로 추상타입의 Beverage를 상속받아 구현한다.

 

1
2
3
public abstract class BeverageDecorator extends Beverage {
    
}
cs

 

데코레이터(추가옵션)들이 상속하게 될 추상클래스이다. 데코레이터들의 모든 타입 그리고 기본구성과 데코레이터들의 타입이 동일해야하므로 Beverage를 상속한다. 만약 데코레이터들의 추가적인 행위를 구현하려면 이쪽에 추상메소드로 선언해도 무방할듯하다.

 

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
public class Mocha extends BeverageDecorator{
 
    private Beverage beverage;
    
    public Mocha(Beverage beverage) {
        this.beverage=beverage;
    }
    
    @Override
    public int cost() {
        return 2+this.beverage.cost();
    }
    
}
 
public class Whipping extends BeverageDecorator{
    
    private Beverage beverage;
    
    public Whipping(Beverage beverage) {
        this.beverage=beverage;
    }
    
    @Override
    public int cost() {
        return 1+this.beverage.cost();
    }
    
}
cs

 

데코레이터 패턴의 핵심인듯하다. 데코레이터 객체들의 구현체이다. 잘보면 기본구성 혹은 데코레이터 객체들을 래핑하기 위해 하나의 인스턴스 변수를 가지고 있다는 것이 핵심이며 기본구성과 동일한 행위(가격계산,cost())를 오버라이드하여 구현하였으며 래핑한 객체의 동일한 행위 메소드를 호출하고 있는 것을 볼 수 있다. 이렇게 기본구성과 데코레이터 객체들의 상위타입을 마춰줌으로써 계속해서 생성자로 객체를 래핑하여 옵션을 추가 할 수 있다.

 

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 DecoratorMain {
 
    public static void main(String[] args) {
        
        Beverage espresso = new Espresso();
        System.out.println("description : "+espresso.getDescription() +" cost : "+espresso.cost());
        
        Beverage mocha = new Mocha(new Espresso());
        mocha.setDescription("카페모카");
        System.out.println("description : "+mocha.getDescription() +" cost : "+mocha.cost());
        
        Beverage mochaWithWhip = new Whipping(new Mocha(new Espresso()));
        mochaWithWhip.setDescription("카페모카 휘핑추가");
        System.out.println("description : "+mochaWithWhip.getDescription() +" cost : "+mochaWithWhip.cost());
        
    }
 
}
 
=>result
description : 에스프레쏘 cost : 2
description : 카페모카 cost : 4
description : 카페모카 휘핑추가 cost : 5
 
cs

 

메인클래스에서 데코레이터 패턴이 적용된 클래스를 사용하고 있는 것을 보니 많이 낯이 익다. 무엇일까? 바로 Java I/O들을 구현할 때, 데코레이터 패턴이 적용되었다! 

 

InputStream is = new BufferedInputStream(new FileInputStream("filepath"));

 

지금까지 간단히 데코레이터 패턴을 다루어보았다.

posted by 여성게
:


SOLID 원칙이란?



S : SRP(Single Responsibility Principle) - 단일 책임 원칙


O : OCP(Open Closed Principle) - 개방-폐쇄 원칙


L : LSP(Liskov Substitution Principle) - 리스코프 치환 원칙


I : ISP(Interface Segregation Principle) - 인터페이스 분리 원칙


D : DIP(Dependency Inversion Principle) - 의존 역전 원칙




S : SRP(Single Responsibility Principle) - 단일 책임 원칙



단일 책임 원칙이란 말 그대로, 하나의 객체는 하나의 책임만 가져야 한다는 원칙이다. 만약 많은 기능을 한 객체에 다 쑤셔 넣는다면? 그만큼 그 객체와 강하게 결합된 객체들이 많아질 것이다. 또한 많은 기능들이 과연 변경이 하나도 없을 수가 있을까? 아니다. 변경이 있을 때마다 객체에 변경이 요해지는 것이다. 결론적으로 단일 책임 원칙을 지킨다면 변경에 있어서 유연한 대처가 가능 해진다.(결합도가 낮아진다.) 그러면서 회귀 테스트 비용 또한 줄일 수 있다. 여기서 회귀 테스트란(regression test) 시스템에 변경이 발생하였을 때, 기존 기능에 영향을 주는지 평가하는 테스트이다. 만약 단일 책임 원칙을 지켜서 변경에 유연한 코드를 만든다면 회귀 테스트 비용을 대폭 줄일 수 있는 효과까지 나타난다. 


단일 책임 원칙과 연관되는 단어에 산탄총 수술이라는 것이 있다. 만약 하나의 기능(책임)이 여러개의 클래스들로 분산된 경우 그 책임에 변경이 있다면 그 책임을 의존하고 있는 여러 클래스에 대해 변경이 요해진다. 여기서 나온 것이 산탄총 수술이라는 용어이다. 만약 산탄총을 이용해 동물을 쐈다면? 하나의 총알(책임)에서 여러개의 총알이 산탄되어 박힐 것이다. 그렇다면 그 동물을 수술하려면 흩어진 모든 총알이 박힌 환부(책임을 의존하고 있는 클래스들)를 치료해야 할것이다.(여기에서 산탄총 수술이라는 용어가 나왔다.) 여기서 해결 할 방법이란? 하나의 책임을 한 클래스로 분리해서 흩어진 공통 책임을 한 곳에 모아 응집도를 높히는 일이다. 이것은 관점 지향 프로그래밍 기법으로 해결한다. 여러 조인포인트(쉽게 이벤트발생시점) 중에서 특정 포인트 컷에서(특정 이벤트)  실행하는 어드바이스(책임)를 가진 애스팩트(포인트컷 + 어드바이스)를 만들어 위빙(특정 시점에 어드바이스를 실행시키는 역할)하여 해결하는 것이다. 





O : OCP(Open Closed Principle) - 개방-폐쇄 원칙


개방-폐쇄 원칙이란 간단히 확장에는 열려있고 변경에는 닫혀있는 형태로 개발하는 원칙이다. 즉, 기존 코드에는 변경이 없으면서 기능을 추가 할 수 있도록 설계하는 것이다. 여기서 이용하는 것이 인터페이스이다. 어떠한 기능을 사용하는 클라이언트(여기서 말하는 클라이언트는 사용자가 아닌 기능을 사용하는 클래스)는 인터페이스 타입으로 의존 주입을 받는다. 그렇다면 클라이언트 코드에는 변경 없이 인터페이스의 메소드를 이용해 구체적인 구현 클래스를 이용하고 개발자 입장에서는 인터페이스를 실체화한 클래스를 만들어 계속 해서 기능을 확장하면 되므로 확장에는 열려있고(기능추가) 변경에는 닫혀있는(클라이언트코드) 형태의 원칙을 지키게 되는 것이다. 이런 개방-폐쇄 원칙은 단위 테스트에도 많이 이용된다. 테스트 더블(테스트를 위한 가짜객체)을 구현하기 위해 실제 인터페이스를 상속받아서 더미 객체, 테스트스텁, 테스트 스파이, 가짜 객체, 목객체 등을 간단히 구현하여 객체간의 관계, 정보전달 여부등을 확인 할때 사용한다.   





L : LSP(Liskov Substitution Principle) - 리스코프 치환 원칙



리스코프 치환 원칙이란 일반화관계(is a kind)에서 "자식클래스는 최소한 자신의 부모클래스에서 가능한 행위는 수행할 수 있어야한다."라는 원칙이다. 즉, 코드에서 부모클래스 인스턴스에서 자식 클래스 인스턴스로 변경이 되어도 프로그램의 의미는 변화되지 않아야 한다. 여기서 중요한 것이 "일관성"이다. 일관성을 지키는 코드를 작성하는 가장 쉬운 방법이 무엇일까? 바로 슈퍼클래스에서 상속받은 메소드들이 서브 클래스에서 오버라이드, 즉 재정이 되지 않도록 하면 되는 것이다. 추가 기능이 필요할 때는 절대 부모클래스에서 상속받은 메소드를 오버라이드하는 것이 아니라 별도의 메소드로 정의해 사용하는 것이다. 






I : ISP(Interface Segregation Principle) - 인터페이스 분리 원칙


인터페이스 분리 원칙이란 클라이언트는 여러 기능을 가진 클래스를 사용할 때 특정 기능만을 사용하는 경우가 많기 때문에 특정 기능만을 위한 인터페이스를 만들어 놓고 분리해 다른 기능이 변경되도 사용자의 기능에 아무런 영향을 받지 않도록 하는 방법이다. 


만약 한 객체에 많은 기능이 들어가 있다면 기능들 사이에 연관이 있을 확률이 아주 높아지게 된다. 그렇다면 만약 팩스를 사용하는 클라이언트가 copy()메소드의 변경으로 인해 fax()에 이상이 생겨서 팩스를 사용하는데 오류가 발생할 수도 있게 되는 것이다. 여기서 나온 원칙이 인터페이스 분리 원칙이다.


이런 식으로 인터페이스로 핵심 기능을 분리하게 되면 프린트를 사용하는 클라이언트는 다른 기능의 변경에 대해 아무런 영향을 받지 않고 자신이 사용할 기능을 안전히 사용가능 하게된다.






D : DIP(Dependency Inversion Principle) - 의존 역전 원칙


의존 역전 원칙이란 의존 관계를 맺을 때 변화하기 쉬운 것보다는 변화하기 어려운 것에 의존하라는 원칙이다. 여기서 변화하기 쉬운 것은 실체화된 클래스를 이야기하면 변화하기 어려운 것은 추상적인 인터페이스를 이야기 한다. 



이런식으로 아이가 가지고 놀 장난감을 구체적인 구현 클래스로 두는 것이 아니라 인터페이스 타입으로 두어서 구체적으로 가지고 놀 장난감을 외부에서 의존 주입받는 방식으로 개발하는 원칙이다. 즉, 변화를 의존주입으로 쉽게 받아 드릴 수 있는 것이다.


1
2
3
4
5
6
7
8
9
10
11
12
public class kid{
    private Toy toy;
 
    public void setToy(Toy toy){
        this.toy=toy;
    }
    public void playing(){
        toy.play();
    }    
}
 
 
cs


이렇게 인터페이스 타입의 인스턴스 변수를 선언하면 결합도도 느슨해지고 변화에 유연하게 대처할 수 있는 코드가 될 수 있다. 이렇게 외부에서 의존주입을 받는 형식이 "역전"이 되었다고 표현한다.

posted by 여성게
: