Template Method 패턴은 실행 순서(sequence)는 고정하면서 세부 실행 내용(operation)은 다양화 될 수 있는 경우에 사용하는 패턴이다.

구현 시 자주 발생하는 문제들 중에서 실행 순서가 고정되어야 하는 경우가 있다. 예를 들어 input() -> calculate() -> output() 순서에 따라서 실행되어야 하거나 init() -> do() -> release(), 또는 lock() -> do() -> unlock()과 같이 정확한 순서에 따라 실행되어야만 올바른 결과를 얻을 수 있는 경우가 있다. 이럴 때 보통 사용하는 방법은 외부에는 상세 단계들을 정해진 순서대로 실행시키는 함수를 public으로 제공하고, 각 단계를 실행하는 함수는 private으로 감춰 두는 것이다.

Template Method 패턴은 이것과 매우 유사한 방식이다. 다만, 실행 방법만 정해져 있을 뿐 각 순서에 따라 해야 할 일들이 서로 다를 수 있는 경우에 이 세부 순서를 구현할 수 있는 추상 메소드를 제공한다. 따라서 보통 Template Method 패턴은 추상 클래스로 구현되는 경우가 많다.

아래는 Template Method 패턴을 구현한 상위 클래스의 형식이다.

abstract public class AbstractTemplate {

    public final void templateMethod(){

        sequence1();

        sequence2();

        sequence3();

        sequence4();

    }

   

    abstract protected void sequence1();

    abstract protected void sequence2();

    abstract protected void sequence3();

    abstract protected void sequence4();

}

먼저 templateMethod()를 살펴보자. 이 패턴의 목적은 일단 순서를 고정하자는 것이다. 따라서 하위 클래스가 이 클래스를 상속 받은 후 templateMethod()를 재정의 하는 것을 방지하기 위해서 final 키워드를 사용해 주어야 한다. 그리고 설계상에서 이미 정해 놓은 순서를 내부적으로 구현한다. 각 순서를 메소드화 한 후, templateMethod() 에서 이 메소드들을 순서에 맞게 호출하는 것이다.

세부 메소드는 하위 클래스들이 구현할 수 있도록 abstract 함수로 제공한다. 이들 abstract 함수들은 templateMethod()에서 호출되어야 하므로 private이 아닌 protected로 선언되어야 한다. 

세부 순서 함수 중 일부는 모든 하위 클래스에게 동일할 수 있다. 이 경우에는 상위 클래스가 이를 구현하고 하위 클래스들은 아예 볼 수 없도록 private으로 선언하는 편이 좋다.

'5.디자인패턴' 카테고리의 다른 글

Memento 패턴  (2) 2016.09.13
Bridge 패턴  (0) 2016.09.13
Singleton 패턴  (0) 2016.09.10
Builder 패턴  (0) 2016.09.10
Holder 패턴  (0) 2016.08.28
Posted by 이세영2
,

Singleton 패턴

5.디자인패턴 2016. 9. 10. 18:13

Singleton 패턴을 이야기 하기 전에 먼저 주의사항부터 이야기 할 필요가 있다.

1. 가급적 쓰지 마라.

이것은 실제로 자주 겪는 문제이기도 한데, Singleton으로 객체를 생성하는 경우, 이 객체는 "전역 변수"와 같다. 단 하나의 객체만이 생성되고, static 함수(getInstance() 함수)를 통해 접근이 가능하고, static 함수가 public 이기 때문에, 전역 변수와 같은 특성을 지니게 된다. 의도한 바가 아니더라도 말이다. 그래서 마치 전역 변수처럼 사용되어 버리는 문제가 생긴다. 테스트 주도 개발(켄트 벡)이라는 책에는 싱글톤에 대해 이렇게 나와 있다.

"싱글톤 : 전역 변수를 제공하지 않는 언어에서 전역 변수를 사용하려면 어떻게 해야 할까? 사용하지 마라. 프로그램은 당신이 전역 변수를 사용하는 대신 설계에 대해 고민하는 시간을 가졌던 점에 대해 감사할 것이다."

싱글톤은 전역 변수처럼 무분별하게 사용될 여지가 있고, 특히 Unit Test를 작성하기 어렵게 만든다. 전역 변수라서 이곳 저곳에서 접근이 가능하고 static final 변수로 선언되기 때문에 대체가 불가능하다. 비슷한 문제가 enum이나 Holder 패턴에서도 발생한다. 하지만 Singleton 객체는 이들 패턴보다 훨씬 무질서하게 사용되는 경향이 있다.

Singleton 패턴에는 크게 두가지 종류가 있다. 하나는 static 변수 선언 시 바로 생성하는 방식이고, 하나는 getInstance() 함수 호출 시 생성하는 방식이다.


static으로 생성하는 방식

public class StaticSingleton {

    private static final StaticSingleton instance = new StaticSingleton();

    private StaticSingleton() {}   // 규칙 1. private 생성자.

    // instance, getInstance, singleton, shared

    public static StaticSingleton getInstance()  // 규칙 2. 오직 한개만 만듦

    {

        return instance;

    }

   

    public static void main(String[] args) {

        StaticSingleton instance = StaticSingleton.getInstance();

    }

}

이 방식은 특별히 구현에 신경 쓸 것이 별로 없다. 생성자를 private으로 만들어서 객체를 외부에서 생성하지 못하도록 하는 것, 그리고 getInstance()라는 메소드를 static으로 제공해 줌으로써 외부에서 객체에 접근할 수 있도록 하는 것이다.

getInstance() 함수를 통해 생성하는 방식

public class Singleton {

    private volatile static Singleton instance = null;

    public static Singleton getInstance(){

        if (instance == null) {                     // A

            synchronized(Singleton.class) {

                if (instance == null) {             // B

                    instance = new Singleton();

                }

            }

        }

        return instance;

    }

    private Singleton(){} // 생성자를 통한 생성 방지

   

    public static void main(String[] args) {

        Singleton instance = Singleton.getInstance();

    }

} 

앞서 static 생성시와의 차이점 중 하나는 instance 변수 선언 시 Singleton 객체가 생성되지 않는다는 점이다. Singleton 객체가 매우 크고, 쓰일 가능성이 매우 낮은 경우에 보통 사용하는 방식이다. 객체의 생성은 getInstance() 함수가 호출 될 때 이루어진다.

이 경우 객체가 이미 생성되었는지 그렇지 않은지(null 여부)를 체크함으로써 중복 생성을 방지할 필요가 있다. 이 때 멀티 쓰레드 상태에서 발생할 수 있는 레이싱 문제도 고려해 주어야 한다. 

위에서 보면 instance 변수가 null인지 체크하는 부분이 두 곳(A와 B)이 있다. 

첫번째(A)는 일단 instance가 null이 아닐 경우 synchronized 블럭 내부로 들어가는 것을 미리 차단함으로써 Singleton 객체를 얻기 위해 발생하는 부하를 줄여 주는 역할을 한다. 

일단 null임이 확인된 이후에는 synchronized 블럭으로 넘어가게 되는데, 이 때는 이 블럭이 단 하나의 쓰레드만 내부로 접근할 수 있도록 막는다. 

이 후 instance 객체의 null 체크를 한번 더 한다(B). 이는 맨 처음 synchronized 블럭에 들어온 쓰레드를 위한 것이 아니고 그 다음에 들어오는 경우를 위한 것이다. 

즉, A에서 두 쓰레드가 instance를 null로 판단했고, 둘 다 synchronized 블럭에 접근했다고 하자. 첫번째 쓰레드는 일단 instance 객체를 생성하고 종료 한다. 

두번째 쓰레드가 B 구문에 도달했을 때에는 이미 첫번째 쓰레드가 생성을 마치고 난 다음이다. 따라서 두번째 쓰레드는 B에서 instance 객체가 null이 아니라고 판단하게 된다. 그리고 그냥 블럭을 빠져 나온다. 이를 통해서 instance 객체가 중복 생성되는 것을 막을 수 있다.


사실상 최근의 컴퓨팅 파워를 고려할 경우 두번째 방식은 실제로는 거의 의미가 없다시피 하다. 어쨌든 필요한 경우 사용해 보려면 정확한 구현 메카니즘을 이해하고 있는 것이 도움이 될 것이다.

다시 한번 강조하지만 가급적 Singleton은 사용하지 않는 편이 좋다.

'5.디자인패턴' 카테고리의 다른 글

Bridge 패턴  (0) 2016.09.13
Template Method 패턴  (0) 2016.09.10
Builder 패턴  (0) 2016.09.10
Holder 패턴  (0) 2016.08.28
Adapter 패턴  (0) 2016.08.23
Posted by 이세영2
,

Builder 패턴

5.디자인패턴 2016. 9. 10. 17:36

빌더 패턴은 객체의 생성과 객체 생성 시 필요한 매개 변수 설정 과정을 분리함으로써 다음과 같은 이점을 확보해 주는 패턴이다.

1. 매개 변수가 많은 경우(특히 연속된 동일 타입의 매개 변수를 설정할 경우)에 발생할 수 있는 설정 오류를 방지할 수 있는 가독성을 제공한다.

2. 디폴트 값이 존재하는 매개 변수를 생략할 수 있도록 한다.

3. (immutable 변수나 final 키워드가 있는 변수처럼) 꼭 생성자를 통해서 설정 되어야 하는 변수를 지원할 수 있는 방법을 제공한다.


최종 결과물

class Hero{

    private int exp;

    private int cash0;

    private final String name; // 생성자를 통해 설정되어야 할 변수

    private final int level;   // 생성자를 통해 설정되어야 할 변수

    private Hero(Builder builder){

        this.exp = builder.exp;

        this.cash = builder.cash;

        this.name = builder.name;

        this.level = builder.level;

    }

    public void print(){

        System.out.println(exp + " " + cash + " " + name + " " + level);

    }

    static class Builder{

        private int exp = 0;

        private int cash = 0;

        private final String name;

        private final int level;

        public Builder(String name, int level){

            this.name = name;

            this.level = level;

        }

        public Builder exp(int exp){ this.exp = exp; return this;}

        public Builder cash(int cash){ this.cash = cashreturn this;}

        public Hero build(){

            return new Hero(this);

        }

    }

} 


실행 방법(객체 생성)

public static void main(String[] args) {

    Hero hero = new Hero.Builder("ceasar", 10).cash(20).exp(20).build();

    hero.print();

}


이 예제에서는 Hero라는 객체를 생성하고자 한다. Hero는 총 4개의 변수를 가지고 있고, 이 중에서 name과 level은 꼭 생성자를 통해서 설정 되어야 하는 변수이다. 그리고 나머지 exp와 cash 변수는 디폴트 값인 0을 가지고 있다.

따라서 다음과 같은 경우의 수를 가지고 있다.

1. name과 level만을 매개변수로 받아 생성하는 경우

2. name + level + exp

3. name + level + cash

4. name + level + exp + cash


만약 이러한 조합을 Telescoping Parameter Pattern으로 구현한다면 총 4개의 생성자를 구현해야 한다. 여기서 매개 변수가 더 추가된다면 생성자 수는 조합적으로 증가하게 될 것이다.

또한 level과 exp, 그리고 cash 변수는 모두 int 타입이다. 만약 단일 생성자에서 4개의 변수를 모두 설정한다면

    생성자("이름", 1, 2, 3);

과 같은 형식으로 변수 값을 할당하게 될 것이다. 하지만 이 경우 생성자의 변수 입력 순서를 정확히 알고 있지 않다면 여러 값을 넣으면서 착오에 의한 에러를 발생시킬 수 있다. 그리고 코드만으로는 쉽게 오류를 찾아내기가 힘들다.

이런 문제점을 해결하는 것이 빌더 패턴이다. 우선 Hero 클래스를 만들어 보자.

class Hero{

    private int exp;

    private int cash;

    private final String name; // 꼭 생성자를 통해 설정되어야 할 변수

    private final int level;   // 꼭 생성자를 통해 설정되어야 할 변수

간단히 변수만 선언한 모습이다. 빌더 패턴에서는 생성자에 바로 매개 변수를 입력하도록 하지 않고, 내부 클래스인 빌더 클래스를 이용하게 되어 있다. 빌더 객체인 Builder는 Hero의 내부 클래스로 선언이 된다. 그리고 외부 클래스인 Hero 객체를 생성하는 것이므로 Hero 객체가 없이 Builder 객체를 생성, 이용할 수 있도록 static 클래스로 선언해 주어야 한다. 그 모양은 아래와 같다.

class Hero{

    private int exp;

    private int cash;

    private final String name; // 생성자를 통해 설정되어야 할 변수

    private final int level;   // 생성자를 통해 설정되어야 할 변수

    static class Builder{

        private int exp = 0;

        private int cash = 0;

        private final String name;

        private final int level;

    }

}

일단 Builder 클래스가 가지고 있는 내부 변수는 모두 Hero가 가지고 있는 것과 일치한다. 여기서 중요한 점은 Builder 객체가 가지고 있는 변수 값이 Hero 객체의 초기값이 될 예정이므로 Builder 객체가 가진 변수들의 초기값을 원하는 값으로 맞춰 주어야 한다는 점이다. 즉, exp와 cash는 0이라는 초기값을 가지고 있는데, 이 초기값을 Hero가 아닌 Builder 클래스의 초기값으로 설정해 주어야 한다. 그래야 나중에 Builder 객체의 초기값이 Hero에 덮어 씌워지면서 원하는 초기값을 가질 수 있게 된다.

다음으로는 생성자를 만들 차례다. 생성자는 Builder 객체가 제공해 준다. 이 때 Hero의 변수 중 name과 level이 final로 설정되어 있으므로, 이 두 변수를 필수적으로 설정해 주도록 생성자에 매개 변수를 선언해야 한다. 구현을 해 보면 다음과 같다.

class Hero{

    private int exp;

    private int cash;

    private final String name; // 꼭 생성자를 통해 설정되어야 할 변수

    private final int level;   // 꼭 생성자를 통해 설정되어야 할 변수



    static class Builder{

        private int exp = 0;

        private int cash = 0;

        private final String name;

        private final int level;


        public Builder(String nameint level){

            this.name = name;

            this.level = level;

        }

    }

}

위와 같이 Builder의 생성자를 선언하여 name과 level을 매개 변수로 입력하도록 강제 할 수 있다.

다음은 옵션으로 입력 받을 수 있는 exp와 cash 변수에 대한 setter를 선언한다. 일반적인 setter 함수는 set+변수명 형식이지만 Builder 패턴에서는 가독성을 좋게 하면서도 setter와의 다른 특성을 가지고 있는 점을 알리기 위해서 변수명 그대로를 setter 이름으로 사용한다.(Java에서는 이것이 가능하지만 다른 언어라면 언더 바('_') 등을 활용할 수 있다.) 이를 선언해 보면 아래와 같다.

class Hero{

    private int exp;

    private int cash;

    private final String name; // 꼭 생성자를 통해 설정되어야 할 변수

    private final int level;   // 꼭 생성자를 통해 설정되어야 할 변수



    static class Builder{

        private int exp = 0;

        private int cash = 0;

        private final String name;

        private final int level;


        public Builder(String nameint level){

            this.name = name;

            this.level = level;

        }


        public Builder exp(int exp){ this.exp = exp; return this;}

        public Builder cash(int cash){ this.cash = cashreturn this;}

    }

}

여기서 주목할 부분은 각 함수 마지막 구문인 return this;이다. 여기서 this는 Builder 객체를 말한다. Builder 객체 자신을 리턴함으로써 생성자 호출 후 옵션 변수 setter 함수들을 연속적으로 호출할 수 있다. 가령

Builder("이름", 10).exp(값).cash(값).... 형태로 연속 호출이 가능하다는 말이다. 이를 통해 각 변수 값이 어떤 변수에 셋팅되게 되는지를 쉽게 알 수 있게 된다.

Builder 클래스에서는 최종적으로 build() 함수를 제공해 주어야 한다. build() 함수는 생성자와 setter를 통해 설정된 매개 변수들을 이용하여 Hero 객체를 생성하는 함수이다.

class Hero{

    private int exp;

    private int cash;

    private final String name; // 꼭 생성자를 통해 설정되어야 할 변수

    private final int level;   // 꼭 생성자를 통해 설정되어야 할 변수



    static class Builder{

        private int exp = 0;

        private int cash = 0;

        private final String name;

        private final int level;


        public Builder(String nameint level){

            this.name = name;

            this.level = level;

        }


        public Builder exp(int exp){ this.exp = expreturn this;}

        public Builder cash(int cash){ this.cash = cash;  return this;}


        public Hero build(){

            return new Hero(this);

        }

    }

}

이제 build() 함수가 호출하는 Hero 클래스의 생성자를 만들어 주어야 한다. build() 함수 내에서의 Hero 생성자는 this, 즉 Builder 객체를 받도록 되어 있다. 따라서 Builder 객체를 받는 생성자를 선언해 주어야 한다. 그리고 Builder 객체가 가지고 있는 변수 값들을 모두 가지고 와서 자신의 변수 값으로 셋팅하는 과정을 포함해야 한다.

class Hero{

    private int exp = 0;

    private int cash = 0;

    private final String name; // 생성자를 통해 설정되어야 할 변수

    private final int level;   // 생성자를 통해 설정되어야 할 변수


    static class Builder{

        private int exp;

        private int cash;

        private final String name;

        private final int level;

        public Builder(String name, int level){

            this.name = name;

            this.level = level;

        }

        public Builder exp(int exp){ this.exp = exp; return this;}

        public Builder cash(int cash){ this.cash = cashreturn this;}

        public Hero build(){

            return new Hero(this);

        }

    }


    private Hero(Builder builder){

        this.exp = builder.exp;

        this.cash = builder.cash;

        this.name = builder.name;

        this.level = builder.level;

    }

    public void print(){

        System.out.println(exp + " " + cash + " " + name + " " + level);

    }

}

위와 같이 Hero 클래스의 생성자를 구현해 주었다. 그리고 추가로 내부 변수 값을 확인할 수 있는 print() 함수를 구현해 주었다. 실행은 글의 첫머리에 나오는 실행 함수를 실행해 보면 된다.

이처럼 Builder 패턴은 객체 생성시 초기 설정 값들에 의해 발생할 수 있는 여러 문제점들을 해결해준다. 종종 setter 함수의 경우에는 가독성 지원 문제를 해결하기 위해 생성 과정과는 별도로 구현해서 사용하기도 한다.

'5.디자인패턴' 카테고리의 다른 글

Template Method 패턴  (0) 2016.09.10
Singleton 패턴  (0) 2016.09.10
Holder 패턴  (0) 2016.08.28
Adapter 패턴  (0) 2016.08.23
Decorator 패턴(synchronizedList의 구현 패턴)  (0) 2016.08.23
Posted by 이세영2
,