전체/Java

객체지향 디자인 패턴(팩토리, 빌더, 싱글톤, 파사드, Iterator, MVC)

effortDev 2019. 1. 27. 15:52


1. 객체지향 디자인 패턴


객체지향 프로그램이 복잡해지면서 이를 간결하게 정리할 필요성이 생긴 관계로 '디자인 패턴'이라는 것이 생겼다. 

디자인 패턴 프로그래밍 형식을 정하는 일종의 약속이다.


객체 지향 프로그래밍 설계를 할 때 자주 발생하는 문제들을 피하기 위해 사용되는 패턴.


여러 사람이 협업해서 개발할 때 다른 사람이 작성한 코드, 기존에 존재하는 코드를 이해하는 것은 어렵다. 

이런 코드를 수정하거나 새로운 기능을 추가해야 하는데 의도치 않은 결과나 버그를 발생시키기 쉽고 

성능을 최적화시키기도 어렵다. 이로 인해 시간과 예산이 소모된다. 


디자인 패턴은 의사소통 수단의 일종으로서 이런 문제를 해결해준다. 

예를 들어 문제 해결의 제안에 있어서도 “기능마다 별도의 클래스를 만들고, 

그 기능들로 해야할 일을 한번에 처리해주는 클래스를 만들자.”라고 제안하는 것보다 

"Facade 패턴을 써보자."라고 제안하는 쪽이 이해하기 쉽다.


일반 프로그래머가 만나는 문제가 지구상에서 유일한 문제일 확률은 거의 없다. 

이미 수많은 사람들이 부딪힌 문제다. 따라서 전문가들이 기존에 해결책을 다 마련해 놓았다. 



2. 생성 패턴(추상 객체 인스턴스화)


2.1 팩토리(Factory Method)


상위 클래스와 하위 클래스가 있을 때, 팩토리 클래스를 사용하여 하위 클래스의 인스턴스를 생성하는 패턴.


Animal.java


1
2
3
4
5
6
public class Animal {
    Animal(){
        // 생성자
    }
    // 이하 Animal의 메소드들
}
cs


Dog.java


1
2
3
4
5
6
7
8
9
10
11
12
public class Dog extends Animal{
    Dog(){
        // 생성자
    }
    public Dog(String data){
        return;
    }
    // 이하 Dog의 메소드들
    public String toString(){
        return "Dog";
    }
}
cs


Cat.java


1
2
3
4
5
6
7
8
9
10
11
12
public class Cat extends Animal{
    Cat(){
        // 생성자
    }
     public Cat(String string) {
         return;
    }
    // 이하 Cat의 메소드들
    public String toString(){
        return "Cat";
    }
}
cs


Map.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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import java.util.Iterator;
import java.util.LinkedList;
 
public class Map {
    
    public static void main(String[] args){
        
        // 저장된 파일로부터 동물을 가져오는 '맵 로드' 기능을 
        // lnkList에 초기 샘플데이터 넣었다고 가정
        LinkedList<String> lnkList = new LinkedList<String>();
        lnkList.add("Dog");
        lnkList.add("Cat");
 
        // Map은 말 그대로 맵의 구현 방법에 대해서만 서술되어야 하는데, 
        // 파일을 읽는 부분에서 '동물을 분류하는' 추가적인 책임이 포함되어있다. 
        // 만약 새 동물 Rat을 넣어야 한다면 전혀 상관없는 Map 클래스를 수정해야 할 것이다.
        
        // 1. 개선 전 방법 
        Iterator<String> iter = lnkList.iterator();        
        while (iter.hasNext()) {
            if(iter.equals("Dog")){
                Dog dog = new Dog();
                System.out.println(dog);
            }else if(iter.equals("Cat")){
                Cat cat = new Cat();
                System.out.println(cat);
            }
        }
 
        // 그래서 다양한 하위 클래스들을 생성하는(Factory : 공장) 클래스를 만들어 
        // 그 클래스에 책임을 위임하는 것이다.
        // 그러면 새 클래스 AnimalFactory를 만들어보자.
        
        // 2. 개선 후 방법 
        Iterator<String> factoryIter = lnkList.iterator();    
        while (factoryIter.hasNext()) {
           Animal animal = AnimalFactory.create(factoryIter.next());
           System.out.println(animal);
        }
    }
}
cs


AnimalFactory.java


1
2
3
4
5
6
7
8
9
10
11
12
13
public class AnimalFactory {
    static Animal create(String string) {
        if(string.equals("Dog")) {
            return new Dog(string);
        } else if(string.equals("Cat")) {
            return new Cat(string);
        } else if(string.equals("Rat")){
//            return new Rat(string);
        }
       //기타 동물의 생성자들
        return null;
    }
}
cs



팩토리를 사용하면 새 동물을 추가하는지의 여부에 상관없이 

다른 클래스를 수정할 필요가 없어져 단일 책임 원칙을 잘 지키는 코드가 된다.



2.2 빌더(Builder)


빌더 클래스는 인스턴스를 생성자를 통해 직접 생성하지 않고, 빌더라는 내부 클래스를 통해 간접적으로 생성하게 하는 패턴이다.


Jacket.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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Jacket {
 
    int number;
    String name;
    double size;
 
    private Jacket(int number, String name, double size) {
        //Jacket 클래스 초기화
        this.number = number;
        this.name = name;
        this.size = size;
    }
 
    // 빌더 클래스는 인스턴스를 생성자를 통해 직접 생성하지 않고, 
    // 빌더라는 내부 클래스를 통해 간접적으로 생성하게 하는 패턴이다
    public static class Builder {
        int number=0;
        String name=null;
        double size=0d;
 
        public Builder() {
            //Builder 초기화
        }
 
        public Builder setNumber(int number) {
            this.number = number;
            return this;
        }
 
        public Builder setName(String name) {
            this.name = name;
            return this;
        }
 
        public Builder setSize(double size) {
            this.size = size;
            return this;
        }
 
        public Jacket build() {
            return new Jacket(number, name, size);
        }
    }
}
cs


JacketProduction.java


1
2
3
4
5
6
7
8
9
10
public class JacketProduction {
    public static void main(String args[]){
        createJacket();
    }
 
    public static void createJacket() {
        Jacket jacket = new Jacket.Builder().setNumber(1).setName("NorthFace").setSize(105).build();
        System.out.println(jacket.number+"  "+ jacket.name +"  "+ jacket.size);
    }
}
cs


1
1  NorthFace  105.0
cs


Builder 사용 이유


1. 클래스와 사용 대상의 결합도를 낮추기 위해


private 등의 접근 제한자로 제한하여 외부에서 임의로 접근하는 것을 막아 클래스와 사용대상의 결합도를 떨어뜨리고, 

대신 Builder라는 내부 클래스를 통해 해당 클래스를 간접적으로 생성한다.


2. 생성자에 전달하는 인수에 의미를 부여하기 위해


예를 들어서, 위에 제시된 예시에서 빌더 패턴이 없다고 가정하고 인스턴스를 생성하려면 

Jacket jacket = new Jacket(number, name, size); 이렇게 코드를 작성하여야 한다.


위의 예시에서는 인수가 세 개니까 그냥 저렇게 써도 큰 문제는 없지만, 

생성자에 전달하는 인수의 가짓수가 열 종류 가까이 되는 클래스의 경우에는 

고전적인 생성자 패턴으로는 인수를 전달하는 것이 상당히 비직관적이 된다.

(인수의 종류를 외워서 써넣어야되는 것뿐만 아니라, 인수의 순서까지 고려해야 한다!)

그래서 빌더 패턴을 통해 setXXX 형식으로 인수를 전달하면 한 눈에 보기에도 이것이 무슨 인수인지를 파악하기가 쉽다.




2.3 싱글톤(Singleton)



1
2
3
4
5
6
7
8
9
10
11
12
class Singleton {
 
    static final Singleton instance = new Singleton();
 
    private Singleton() {
        //초기화
    }
 
    public Singleton getInstance() {
        return instance;
    }
}
cs


키보드 리더, 프린터 스풀러, 점수기록표 등 클래스의 객체를 하나만 만들어야 하는 경우 사용한다. 

클래스 내에서 인스턴스가 단 하나뿐임을 보장하므로, 프로그램 전역에서 해당 클래스의 인스턴스를 바로 얻을 수 있고, 

불필요한 메모리 낭비를 최소화한다.


이 패턴에서는 생성자를 클래스 자체만 사용할 수 있도록 private 등의 접근제한자를 통하여 제한하여야 한다. 

생성자를 다른 곳에서도 사용할 수 있으면 그 곳에서도 인스턴스를 만들 수 있기 때문.


싱글톤 패턴을 사용하기 위해서는 반드시 접근제한자를 이용하여 외부의 접근을 막거나, 

final로 reference를 변경 불가능하게 설정하여야 한다. 

물론 생성자에 접근제한자를 사용하면 최소한 다른 인스턴스로 레퍼런스시키지는 못하겠지만, 

ClassName.singleton = null; 처럼 레퍼런스 자체를 지워버릴 수 있기 때문.

구현 방법에는 사전 초기화, 늦 초기화 등이 있다.


Eager initialization(사전 초기화)


클래스 로딩시에 인스턴스를 생성하는 방법이다. 위의 예시가 사전 초기화. 멀티스레드 환경에서의 

이중 객체 생성 문제가 없지만, 인스턴스를 호출하지 않아도 무조건 클래스를 초기화하기에 메모리 효율이나 연산 효율은 낮다.

Java에서는 static block initialization이라는 변종도 있다. 

클래스가 로딩될 때 최초 1회만 실행되는 static block을 통해 싱글톤 인스턴스를 초기화하는 방법인데, 구조적으로는 크게 다르지 않다.


Lazy initialization(늦 초기화)


인스턴스를 실제로 사용할 시점에서 인스턴스를 생성하는 방법이다. 세심한 방법을 쓰지 않으면 

위에 언급한 이중 객체 생성 문제가 발생할 가능성이 높으나, 

인스턴스를 실제로 사용하지 않는다면 메모리와 연산량을 아낄 수 있다는 장점이 있다.



3. 구조 패턴(객체 결합)


3.1 파사드(Facade)


파사드는 프랑스어 Façade에서 차용된 단어로 보통 건물의 출입구로 이용되는 정면 외벽 부분을 가리키는 말이다.

파사드 패턴은 시스템의 복잡성을 감추고, 사용자(Client)가 시스템에 접근할 수 있는 인터페이스(Interface)를 사용자(Client)에게 제공한다. 

따라서 파사드 패턴은 기존의 시스템에 인터페이스를 추가함으로써, 

복잡성을 감추기 위해 사용된다. 파사드 패턴은 구조적 패턴(Structural Pattern)에 포함된다.


1단계: 인터페이스를 생성한다.


1
2
3
public interface Coffee {
    void make();
}
cs



2단계: 그 인터페이스를 구현하기 위한 구체적인 클래스를 생성한다.


Americano.java


1
2
3
4
5
6
public class Americano implements Coffee {
   @Override
   public void make() {
      System.out.println("Americano::make()");
   }
}
cs


BanillaLatte.java


1
2
3
4
5
6
public class BanillaLatte implements Coffee {
   @Override
   public void make() {
      System.out.println("BanillaLatte::make()");
   }
}
cs


Cappuccino.java


1
2
3
4
5
6
public class Cappuccino implements Coffee {
   @Override
   public void make() {
      System.out.println("Cappuccino::make()");
   }
}
cs



3단계: 파사드 클래스를 생성한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CoffeeMaker {
    private Coffee americano;
    private Coffee banillaLatte;
    private Coffee cappuccino;
 
    public CoffeeMaker() {
        americano = new Americano();
        banillaLatte = new BanillaLatte();
        cappuccino = new Cappuccino();
    }
 
    public void makeAmericano(){
        americano.make();
    }
    
    public void makeBanillaLatte(){
        banillaLatte.make();
    }
 
    public void makeCappuccino(){
        cappuccino.make();
    }
}
cs



4단계: 다양한 종류의 형태를 만들기 위해 파사드를 사용한다.


1
2
3
4
5
6
7
8
9
public class FacadePatternDemo {
    public static void main(String[] args) {
        CoffeeMaker coffeeMaker = new CoffeeMaker();
 
        coffeeMaker.makeAmericano();
        coffeeMaker.makeBanillaLatte();
        coffeeMaker.makeCappuccino();
    }
}
cs



5단계: 결과값을 확인한다.


1
2
3
Americano::make()
BanillaLatte::make()
Cappuccino::make()
cs



4. 행위 패턴(객체 간 커뮤니케이션)


4.1 반복자(iterator)


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
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
 
public class IteratorTest {
    public static void main(String args[]){
        
        // List for each
        List<Integer> list = new ArrayList<Integer>();
        list.add(1); list.add(2); list.add(3);
        
        for(Integer a : list){
            System.out.println(a);
        }
        
        // Iterator
        LinkedList<String> lnkList = new LinkedList<String>();
        lnkList.add("Dog"); lnkList.add("Cat");
        
        Iterator<String> iter = lnkList.iterator();
        
        while(iter.hasNext() == true) {
          Object object = iter.next();
          System.out.println(object);
        }
    }
}
cs


1
2
3
4
5
1
2
3
Dog
Cat
cs



5. 기타


5.1 MVC


최근 애플리케이션 개발에 있어 상당히 중요한 모델이 된 패턴으로 Model, View, Controller 세 가지 부분으로 이루어져 있다. 

Model은 자료(Data)를 생성, 저장, 처리하는 역할을 하는 부분이다. 

View는 Model로부터 받은 자료를 여러 가지 형태로 사용자에게 보여주는 역할을 한다. 

Controller란 소프트웨어의 흐름을 제어하는 것으로 View와 Model 사이에서 관계를 설정하여 주는 부분을 말한다. 

Controller는 Model이나 View가 바뀌더라도 수정 없이 작동되어야 한다.



테스트 작성한 소스 첨부


myTest5.zip