지난 시간 Instance 수를 제한하는 Singleton 패턴을 배웠었고, 과제로 한 개의 Instance 만 제한하는 것이 아닌 특정 개수만큼의 Instance 를 제한하는 Multiton 을 만들어 보도록 하였습니다.


관련 내용은 아래 포스팅을 참고!



똑똑한 우리 스터디 구성원 모두 과제를 잘해왔고, 정리하는 차원에서 제가 만든 Multiton 을 Review 해보도록 하겠습니다.


일단 가정은 요구사항의 SetInstanceCount 시점에 Instance pool 을 만드는 것이 아닌(Eager-binding), 개수만 먼저 제한하고 필요시점마다 Instance 를 생성하는 게으른 할당(Lazy-binding)으로 제작해 보겠습니다.


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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class Multiton {
    // INSTANCE POOL 관리.
    // 어떤 세팅도 하지 않았으면, Singleton 으로 관리하도록 처리.
    private static int INSTANCE_LIMIT_COUNT = 1;
 
    // Instance pool.
    private static HashMap<Integer, Multiton> POOL = new HashMap<>();
 
    private static Integer toggleCount = 0;
 
    private String toStringMessage;
 
    /**
     * 외부에서 Instance 를 만들지 못하도록 접근제한.
     */
    private Multiton(){
        // Instance 의 생성과 함께 POOL 에 넣는다.
        Integer number = POOL.size();
 
        toStringMessage = String.format("%d번 인스턴스", number);
        POOL.put(number, this);
    }
 
    /**
     * Instance 출력.
     *
     * <pre>
     *     Lock 을 메소드에 모두 걸어 Thread-safe 를 보장.
     * </pre>
     *
     * @return
     */
    public static synchronized Multiton GetInstance() {
        final Multiton result;
 
        if (POOL.size() < INSTANCE_LIMIT_COUNT) {
            // Instance 개수가 아직 제한된 POOL 만큼 생성안된 경우.
            result = new Multiton();
        } else {
            // Instance 개수가 아직 제한된 POOL 만큼 생성 된 경우.
            result = POOL.get(toggleCount);
            toggleCount = (toggleCount + 1) % INSTANCE_LIMIT_COUNT;
        }
 
        return result;
    }
 
    /**
     * 인스턴스의 개수 제한
     *
     * @param limitCount
     */
    public static final void SetInstanceCount(int limitCount) {
        if (limitCount < 0) {
            throw new RuntimeException("[에러] Instance 개수는 0보다 커야 합니다.");
        }
        INSTANCE_LIMIT_COUNT = limitCount;
    }
 
    @Override
    public String toString(){
        return toStringMessage;
    }
}
cs


코드 내부의 구체적인 주석을 명시 하였고, 주의깊게 봐야할 부분은 아래와 같습니다.


1. Thread-safe 보장을 위해 Instance 를 유일하게 외부로 내보낼 수 있는 GetInstance 메소드에 synchronized 키워드를 달았습니다.


2. 사용자가 SetInstanceCount 를 사용하지 않을 수 있기 때문에 Instance 개수 제한을 명시적으로 1로 초기화하였습니다.

(INSTANCE_LIMIT_COUNT = 1)


이렇게 제작된 코드를 Thread-safe 가 보장이 되는지 확인해보도록 하겠습니다.


아래 테스트는 Thread 를 50 개 제작하여, 실행하는 메소드입니다. 각 스레드에서는 각 Instance 의 toStringMessage 를 출력하도록 합니다.


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
// Thread Test using JAVA7
{
    System.out.println("멀티톤 테스트!!");
    Multiton.SetInstanceCount(5);
 
    // Thread 목록 생성.
    final ArrayList<Thread> multiThreadTestList = new ArrayList<>();
    {
         for (int i = 0; i < 50++i) {                    
            Thread newThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Multiton.GetInstance());
                }
            });
 
            multiThreadTestList.add(newThread);
        }
    }
 
    // Thread 실행
    {
        // Thread 생성 즉시 바로 실행하지 않고, 목록을 생성하고 실행하는 이유는 최대한 동시에 Thread 를 실행하고 싶기 때문!
        // Thread Instance 를 만드는 시간 때문에 제대로 테스트가 안될 가능성 존재.
        for (Thread thread : multiThreadTestList) {
            thread.start();
        }
    }
}
 
// Thread Test using JAVA8
{
    IntStream.rangeClosed(050).mapToObj(n -> new Thread(() -> System.out.println(Multiton.GetInstance()))).forEach(Thread::start);
}
 
// CONSOLE LOG
// 멀티톤 테스트!!
// 0번 인스턴스
// 1번 인스턴스
// 2번 인스턴스
// 3번 인스턴스
// 4번 인스턴스
// 1번 인스턴스
// 3번 인스턴스
cs


CONSOLE LOG 를 보면, Instance 가 꼭 순서대로 나오지는 않는 것을 볼 수 있습니다. 

비록 GetInstance 에서 LOCK 처리는 하였지만 Thread 의 실행 상태에 따라 순서가 바뀔 수는 있습니다. 

순서를 보장해야만 한다면 Blocking 처리 등 더 복잡한 과정이 필요하겠지만, 이번 주제는 Instance 의 개수를 제한하는 것이니 다루지 않을 예정입니다.


이 블로그가 스터디를 참여하고 있는 모두에게 좋은 정보가 되길 바랍니다. :-)

반응형
Posted by N'

5. 실무 활용 패턴 (하).pdf



디자인 패턴과 관련된 마지막 STUDY 입니다.


모두들 수고하셨습니다. @.@ [꾸벅][꾸벅]


참고자료 

- 한국기술교육대학교 - 객체지향개발론및실습 (김상진 교수님)

- [Head First] Design Pattern

반응형
Posted by N'

지난 스터디에서 다룬 Singleton 은 Instance 를 한 개로 제한함을 알 수 있었습니다.


이를 이용해서 우리는 Instance 를 두개로 제한하는 아래와 같은 Doubleton 을 구현할 수 있었습니다.


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
45
46
47
48
49
50
51
52
53
54
enum Doubleton {
    SINGLE("1번 인스턴스"), DOUBLE("2번 인스턴스");
 
    public static Main.Doubleton GetInstance() {
          // 토글
        a = (a + 1) % 2;
 
        Doubleton result = null;
            
        switch (a) {
        case 0:
            result = DOUBLE;
            break;
        
        case 1:
            result = SINGLE;
            break;
        
        default:
            new RuntimeException(String.format("[에러] 예상못한 TOGGLE 변수 : %d", a));
        }
        
        return result;
    }
 
    private static int a = 0;
    private String toStringMessage;
 
    Doubleton(String toStringMessage) {
        this.toStringMessage = toStringMessage;
    }
 
    @Override
    public String toString(){
        return toStringMessage;
    }
}
 
// TEST CODE
for (int i = 0; i < 100000++i) {
    new Thread(() -> {
        String message = Doubleton.GetInstance().toString();
        System.out.println(message);
    }).
    start();
}
 
// CONSOLE LOG
// 1번 인스턴스
// 2번 인스턴스
// 1번 인스턴스
// 2번 인스턴스
// 1번 인스턴스
// .........
cs


하지만 아쉽게도 enum 이용한 Doubleton 은 Instance 의 갯수를 동적을 조절할 수 없어보이네요. 

우리는 Instance 의 개수를 동적으로 조절할 수 있는 Multiton 을 만들어보는 것이 목적입니다.


아래와 같이 동적으로 미리 Instance 개수를 입력받도록 세팅을 먼저 했으면 좋겠습니다.

물론 Thread-safe 도 보장하면 좋겠죠? 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Multiton.SetInstanceCount(5);
 
for (int i = 0; i < 100000++i) {
    new Thread(() -> {
        String message = Multiton.GetInstance().toString();
        
        System.out.println(message);
    }).start();
}
 
// CONSOLE LOG
// 1번 인스턴스
// 2번 인스턴스
// 3번 인스턴스
// 4번 인스턴스
// .....
cs


그럼 우리 스터디 멤버들 파이팅!!!! :-)

반응형
Posted by N'

이번 주에는 실무에서 아마 가장 많이 사용하는 패턴 중 하나인 Singleton 패턴을 다뤄보았습니다.


자료는 아래 포스팅에서 참고!



물론 웹 프로젝트에서는 직접 Singleton 을 만들기보다는 Bean 등록을 통해 Component 들을 사용하지만, 안드로이드 프로젝트 혹은 자바가 아닌 다른 환경에서는 직접 만들어야 할 일이 있을 수 있습니다.


그렇기 때문에 세 번째 실무 주제로 Singleton 을 선택하였습니다.


Singleton 은 클래스 내에서 한 개의 instance 만 생성하여, 그 인스턴스를 광역적으로 사용하기 위한 패턴입니다. 

(웹 프로젝트에서는 각종 Component 를 Controller 가 광역적으로 사용하고 있습니다.)


1. Singleton 의 기본적인 컨셉


기본적인 Singleton 의 컨셉은 아래와 같이 정의해 볼 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * 싱글톤 예제
 *
 * Created by Doohyun on 2017. 5. 14..
 */
public class Singleton {
    // 인스턴스는 클래스 내부에서 공용으로 한개만 관리.
    private static Singleton instance = null;
 
    // 외부에서 인스턴스를 생성 못하도록 접근제한
    private Singleton() {}
 
    // 인스턴의 접근 제한은 아래 메소드로만 가능.
    public static Singleton GetInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
 
        return instance;
    }
}
cs


instance 를 한 개만 생성하고 관리하는 것이 목적이므로, 클래스 내부에서 instance 를 공용으로 관리하고 instance 를 접근할 수 있는 정적 메소드를 제공해줍니다.


하지만 이 기본적인 컨셉은 안타깝게도 동시성 처리에 취약합니다.

내부 GetInstance 를 동시에 여러 Thread 가 접근할 시, instance 의 null 체크의 Thread-safe 를 보장할 수 없습니다.



2. Thread-safe 한 Singleton


Singleton 의 Thread-safe 문제를 해결하기 위해 생각해 볼 수 있는 방법은 공용으로 관리하려는 instance 에 대한 명시적 초기화를 해주는 것입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * 명시적 초기화한 싱글톤 예제
 *
 * Created by Doohyun on 2017. 5. 14..
 */
public class Singleton {
    // 인스턴스의 명시적 초기화
    private static Singleton instance = new Singleton();
 
    // 외부에서 인스턴스를 생성 못하도록 접근제한
    private Singleton() {}
 
    // 인스턴의 접근 제한은 아래 메소드로만 가능.
    public static Singleton GetInstance() {
        return instance;
    }
}
cs


클래스 로드 타임에 미리 instance 가 생성되기 때문에 Thread-safe 합니다. 

이런  방식을 Eager-binding (부지런한 할당)이라 합니다. 하지만 Eager-binding  방식은 싱글톤의 instance 를 사용하지 않음에도 바로 초기화가 되어버릴 수 있습니다.


아래 예제 처럼 말이죠.


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 Singleton {
    // 인스턴스의 명시적 초기화
    private static Singleton instance = new Singleton();
 
    // 외부에서 인스턴스를 생성 못하도록 접근제한
    private Singleton() {
        System.out.println("Create Singleton!!");
    }
 
    // 인스턴의 접근 제한은 아래 메소드로만 가능.
    public static Singleton GetInstance() {
        return instance;
    }
 
    /**
     * 싱글톤의 정적 메소드
     */
    public static void RunStaticTask() {
    }
}
 
Singleton.RunStaticTask();
 
// CONSOLE LOG
// Create Singleton!!
cs


하지만 만약 Singletone 을 만들기 위한 비용이 크고, 많은 Singleton 들이 Eager-binding을 시도한다면 부하가 클 수도 있습니다. 즉 Instance 를 사용할 때 생성하는 Lazy-binding(게으른 할당)이 필요해 보입니다.


Eager-binding 이 무조건 나쁜 것은 아닙니다. 서버 프로젝트의 경우 오히려 서버가 켜질 때 각 컴포넌트들이 미리 Instance 를 생성하고, Client 의 요청에 딜레이 없이 바로 응답해주는 것이 좋습니다.


3. Lazy-binding 방식의 Singleton 생성


다시 기본적인 컨셉으로 돌아와서 생각해 봤을 때, 직관적인 방법은 오직 한 개의 스레드 만 접근하도록 Lock 을 걸어주는 것입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * 스레드의 Lock 을 이용한 싱글톤 예제 (1)
 *
 * Created by Doohyun on 2017. 5. 14..
 */
public class Singleton {
    // 인스턴스의 명시적 초기화
    private static Singleton instance = null;
 
    // 외부에서 인스턴스를 생성 못하도록 접근제한
    private Singleton() {
    }
 
    // synchronized 로 Lock 을 건다.
    // 이 메소드는 오직 한 스레드 씩 접근 가능
    public static synchronized Singleton GetInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
 
        return instance;
    }
}
cs


메소드에 Lock 을 걸었기 때문에 Thread-safe 함은 보장합니다. 

하지만 매번 사용할 때마다 한 개의 Thread 만 접근 가능하다는 것은 조금 불편한 진실인 듯 합니다. ㅡㅡ^


그렇기 때문에 정말 Thread-safe 한 부분만 Lock 이 걸리도록 변경해보려 합니다. Thread-safe 함이 보장되야할 부분은 instance 의 생성 부분입니다.


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
/**
 * 스레드의 Lock 을 이용한 싱글톤 예제 (2)
 *
 * Created by Doohyun on 2017. 5. 14..
 */
public class Singleton {
    // 스레드 간 최산 값을 읽어주기 위해 volatile 을 이용한 가시성 변수 사용
    private static volatile Singleton instance = null;
 
    // 외부에서 인스턴스를 생성 못하도록 접근제한
    private Singleton() {
    }
    
    // 개선된 Lock 처리
    public static Singleton GetInstance() {
        if (instance == null) {
            // instance 의 생성 부분만을 Lock 처리
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
        }
 
        return instance;
    }
}
cs


하지만 이 방식은 가시성 변수를 사용하고 있습니다. 

instance 변수는 모든 스레드가 최신 값을 읽게 하기 위해 CPU 의 캐시를 사용하지 않으며, 그렇기 때문에 무조건 메모리로부터 데이터를 불러오게 됩니다.


- 간략한 운영체제 속 내용 소개 ㅡㅡ^


데이터는 모두 메모리(Heap, Data, Stack 영역) 위에 저장되어 있고, 보통은 CPU가 계산을 하기 위해 데이터를 읽어온 뒤 캐싱 작업을 합니다. 

캐싱작업을 하는 이유는 CPU 에서 미리 데이터를 저장함으로써, 보다 빠르게 읽고 계산하기 위해서죠.


하지만 여러 스레드가 변수를 동시접근을 하여 데이터를 조작하면, CPU 캐시의 값이 최신 값임을 보장할 수 없게 됩니다. 

즉 변경된 값을 못 읽어올 수 있죠.


그렇기 때문에 변수에 volatile 키워드를 붙여 가시성 변수임을 선언하며, 그 변수는 항상 메모리에서 읽어오게 됩니다. 

하지만 CPU 의 캐시에서 읽어오는 것보다 비용이 언제나 크겠죠? ㅜㅡㅜ



3. Lock 을 사용하지 않은 Lazy-bindig 방식의 Singleton


MARYLAND 대학의 연구원인 Bill puge 는 Lock 을 사용하는 방식은 단점을 해결하기 위해 새로운 Singleton 생성방식을 고안했습니다.


그 방식의 이름은 "Initialization on demand holder idiom" 으로 내부 클래스를 이용한 Holder 를 사용하는 기법은 클래스 로더 규칙에 의해 Lazy-binding 을 보장합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
 * Initialization on demand holder idiom 방식의 싱글톤 생성
 *
 * Created by Doohyun on 2017. 5. 14..
 */
public class Singleton {
    // Instance 를 감싸는 홀더 생성. 클래스로더 규칙에 의해, ManagerHolder 가 로드될 때 Instance 생성
    private static final class ManagerHolder {
        private static final Singleton Instance = new Singleton();
    }
 
    // 외부에서 인스턴스를 생성 못하도록 접근제한
    private Singleton() {
    }
 
    // 개선된 Lock 처리
    public static Singleton GetInstance() {
        return ManagerHolder.Instance;
    }
}
cs


내부에 있는 ManagerHolder 는 Private 으로 접근제한 되어 있으며, 오직 GetInstance 를 통해서만 사용할 수 있습니다. 즉 GetInstance 가 호출 될 때 ManagerHolder 클래스가 로드되며 그 순간 명시적 초기화를 하고있는 내부의 Instance 가 생성됩니다.



4. Enum 을 사용하는 Singleton


Enum(열거형) 을 통해서도 Singleton 을 만들 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 * ENUM 을 이용한 싱글톤
 *
 * Created by Doohyun on 2017. 5. 14..
 */
public enum Singleton {
    INTANCE;
    
    public void doSometing(){}
}
 
// Enum 싱글톤 사용법
Singleton.INTANCE.doSometing();
cs


간결한 사용법 때문에 최근 권장하는 스타일입니다. 


이 방식은 열거형 특성 상, 컴파일 시점에 미리 Instance 가 생성되기 때문에 Thread-safe 함을 보장합니다. 하지만 무조건 Eager-binding 입니다.


Singleton 을 만드는 여러 방식을 알아보았습니다. 우리는 Spring 이 아니더라도 Singleton 을 직접만들어 사용할 수 있게 되었습니다. 


하지만 주의할 것은 스터디에서도 살펴본 것 처럼 싱글톤 병에는 안 걸리게 조심합시다. ㅡㅡ^

반응형
Posted by N'

지난 주에 이어, 계속 디자인 패턴 종류를 실습하고 있습니다.


이번 주에는 지난 스터디에서 실습한 전략패턴과 비슷한 구조의 Template Method 패턴을 다뤄보았습니다.


자료는 아래 포스팅에서 참고! 



이번 추가 내용 포스팅 역시 스터디에서 진행한 코드를 기반으로 진행하고자 합니다.


Template Method 패턴 역시 변경소지가 있는 부분을 분리하고, 위임을 통해 각 클래스가 행위를 하도록 하는 전략패턴과 비슷한 구조를 취하고 있습니다. 차이점은 전략패턴의 경우 각 클래스의 행위가 크게 다른 반면, Template Method 의 경우에는 크게 보면 비슷한 알고리즘을 사용하지만 세부 행위가 조금씩 다를 경우 사용할 수 있는 패턴입니다.


조금 더 쉬운 예제를 생각해보면, 우리가 흔히 사용하는 Collections 클래스의 sort 는 동일한 정렬 알고리즘을 사용하지만 비교하는 부분인 Comparable 은 다르기 때문에 따로 구체화하여 사용합니다.


Template Method 의 정의는 이 정도로 하고, 스터디에서 진행했던 코드를 리뷰해보도록 하겠습니다.


우리는 DB 접근의 편의성 때문에 ORM 을 사용하곤 합니다.

ORM 을 사용하면, Table 과 실제 객체간의 관계를 호스트 코드에서 쉽게 관리할 수 있기 때문에 생산성 면에서 좋다고 생각할 수 있습니다.


하지만 종종 아래와 같이 비슷한 구조의 필드를 가진 테이블이 존재할 수 있으며, ORM 클래스들은 아쉽게도 다른 테이블을 표현한 클래스들과 상속 또는 구현 관계를 취할 수 없습니다.


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
45
/**
 * 제너레이트 클래스 예제1
 *
 * Created by Doohyun on 2017. 5. 12..
 */
public class GenerateModel1 {
    // 공통필드
    private Integer memberSubjectSn;
    private String name;
    private String commonField1;
    private String commonField2;
    private String commonField3;
    private String commonField4;
 
    // 클래스 고유 필드
    private String model1SpecialField1;
    private String model1SpecialField2;
    private String model1SpecialField3;
 
    // setter, getter 는 생략
}
 
/**
 * 제너레이트 클래스 예제2
 *
 * Created by Doohyun on 2017. 5. 12..
 */
public class GenerateModel2 {
    // 공통필드
    private Integer memberSubjectSn;
    private String name;
 
    private String commonField1;
    private String commonField2;
    private String commonField3;
    private String commonField4;
 
    // 클래스 고유 필드
    private String model2SpecialField1;
    private String model2SpecialField2;
    private String model2SpecialField3;
    private String model2SpecialField4;
 
    // setter, getter 는 생략
}
cs


ORM 의 특성 상 ORM 라이브러리에 의존해서 데이터의 CRUD 를 하기 위해서는 위의 클래스를 이용하는 방법밖에 없습니다. 그렇기 때문에 우리는 비지니스 로직 처리 중 이러한 문제를 겪을 수 있습니다.


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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
/**
 * Model1 과 Model2 를 사용하는 서비스 클래스
 * 
 * Created by Doohyun on 2017. 5. 14..
 */
@Service
public class ModelSampleService {
    @Autowired
    private Model1Dao model1Dao;
 
    @Autowired
    private Model2Dao model2Dao;
 
    /**
     * 데이터를 저장한다.
     *
     * @param addTargetMemberSnList
     * @param excludeTargetMemberSnList
     */
    public void createByGenerateModel(
            List<Integer> addTargetMemberSnList
            , List<Integer> excludeTargetMemberSnList) {
 
        // GenericModel1 로직.
        createByGenerateModel1(addTargetMemberSnList, excludeTargetMemberSnList);
 
        // GenericModel2 로직.
        createByGenerateModel2(addTargetMemberSnList, excludeTargetMemberSnList);
    }
 
    /**
     * GenerateModel1 에서
     *
     * <pre>
     * "addTargetMemberSnList" 의 구성원를 저장하고,
     * "excludeTargetMemberSnList" 를 제외한다.
     * </pre>
     *
     * @param addTargetMemberSnList
     * @param excludeTargetMemberSnList
     */
    private void createByGenerateModel1(
            List<Integer> addTargetMemberSnList
            , List<Integer> excludeTargetMemberSnList){
        /**
         * 새로 저장된 대상자를 순번으로 그룹핑한다
         *
         * key : 구성원순번
         * value : 모델 객체
         */
        final HashMap<Integer, GenerateModel1> groupByMemberSnMemberMap = new HashMap<>();
        {
            for (Integer memberSn : addTargetMemberSnList) {
                // 일단은 MemberSn 만 넣는다고 가정.
                GenerateModel1 generateModel1 = new GenerateModel1();
                generateModel1.setMemberSubjectSn(memberSn);
                groupByMemberSnMemberMap.put(memberSn, generateModel1);
            }
        }
 
        // 이미 존재하는 구성원이거나 제외대상자는 입력 대상에서 제외.
        {
            // 이미 존재하는 구성원순번 또는 제외 타겟 순번 집합.
            HashSet<Integer> excludeTargetMemberSnSet = new HashSet<>();
            {
                // 이미 존재하는 구성원 순번 목록 삽입.
                List<GenerateModel1> existList = model1Dao.selectList(groupByMemberSnMemberMap.keySet());
                for (GenerateModel1 model1 : existList) {
                    excludeTargetMemberSnSet.add(model1.getMemberSubjectSn());
                }
 
                // 제외 대상 파라미터도 추가.
                excludeTargetMemberSnSet.addAll(excludeTargetMemberSnList);
            }
 
            // 추가대상 그룹에서 제외 대상 집합을 삭제한다.
            groupByMemberSnMemberMap.keySet().removeAll(excludeTargetMemberSnSet);
        }
 
        // 데이터 트랜잭션
        {
            // 데이터 삽입.
            for (GenerateModel1 model1 : groupByMemberSnMemberMap.values()) {
                model1Dao.create(model1);
            }
 
            // 제외대상 삭제.
            model1Dao.deleteByMemberSnList(excludeTargetMemberSnList);
        }
    }
 
    /**
     * GenerateModel2 에서
     *
     * <pre>
     * "addTargetMemberSnList" 의 구성원를 저장하고,
     * "excludeTargetMemberSnList" 를 제외한다.
     * </pre>
     *
     * @param addTargetMemberSnList
     * @param excludeTargetMemberSnList
     */
    private void createByGenerateModel2(
            List<Integer> addTargetMemberSnList
            , List<Integer> excludeTargetMemberSnList){
 
        /**
         * 새로 저장된 대상자를 순번으로 그룹핑한다
         *
         * key : 구성원순번
         * value : 모델 객체
         */
        final HashMap<Integer, GenerateModel2> groupByMemberSnMemberMap = new HashMap<>();
        {
            for (Integer memberSn : addTargetMemberSnList) {
                // 일단은 MemberSn 만 넣는다고 가정.
                GenerateModel2 generateModel2 = new GenerateModel2();
                generateModel2.setMemberSubjectSn(memberSn);
                groupByMemberSnMemberMap.put(memberSn, generateModel2);
            }
        }
 
        // 이미 존재하는 구성원이거나 제외대상자는 입력 대상에서 제외.
        {
            // 이미 존재하는 구성원순번 또는 제외 타겟 순번 집합.
            HashSet<Integer> excludeTargetMemberSnSet = new HashSet<>();
            {
                // 이미 존재하는 구성원 순번 목록 삽입.
                List<GenerateModel2> existList = model2Dao.selectList(groupByMemberSnMemberMap.keySet());
                for (GenerateModel2 model1 : existList) {
                    excludeTargetMemberSnSet.add(model1.getMemberSubjectSn());
                }
 
                // 제외 대상 파라미터도 추가.
                excludeTargetMemberSnSet.addAll(excludeTargetMemberSnList);
            }
 
            // 추가대상 그룹에서 제외 대상 집합을 삭제한다.
            groupByMemberSnMemberMap.keySet().removeAll(excludeTargetMemberSnSet);
        }
 
        // 데이터 트랜잭션
        {
            // 데이터 삽입.
            for (GenerateModel2 model2 : groupByMemberSnMemberMap.values()) {
                model2Dao.create(model2);
            }
 
            // 제외대상 삭제.
            model2Dao.deleteByMemberSnList(excludeTargetMemberSnList);
        }
    }
}
 
cs


해당 서비스에서는 Model1, Model2 에 추가하고자하는 대상자순번 목록과 삭제하고자하는 대상자 순번 목록을 파라미터로 받고 있습니다.


내부 로직인 createByGenerateModel1, createByGenerateModel2 에서는 


1. 추가하고자 하는 대상자순번으로 추가대상 ORM 객체를 생성.

2. 이미 있는 데이터를 조회 후, 추가대상에서 제외

3. 제외대상에 있는 데이터를 추가대상에서 제외

4. 최종으로 남아있는 추가대상 데이터를 저장하고, 제외대상 데이터를 삭제


하고 있습니다.


두 메소드의 내부역할은 비슷한 알고리즘을 사용하지만,

두 ORM 클래스는 상속관계를 가질 수 없고, 더군다나 데이터 관리 Dao 도 다르기 때문에 copy&paste 로 다시 정의하게 되었습니다.


하지만 우리는 이 메소드를 작성하면서, 코드 내에서 변화하는 부분과 변하지 않는 부분을 찾아볼 수 있었습니다. 

(단순하게 바로 패턴을 적용하기 보다는 이 것을 찾아내는 능력이 중요합니다. ㅡㅡ^)


두 메소드는 동일한 알고리즘을 사용하지만 아래와 같은 차이점을 찾아낼 수 있습니다.


- ORM 객체를 만들어내는 과정.

- 이미 있는 데이터를 찾아내는 과정.

- 데이터를 추가하거나 삭제하는 과정.


즉 우리는 변하지 않는 알고리즘 베이스는 남기고, 변화하는 부분을 추상적으로 생각할 수 있을 것 같습니다. 다시말하면, 변하지 않는 부분을 정의하는 클래스변하는 부분을 구체화시킨 클래스를 작성하여 각 행위에 대한 책임을 나눠볼 수 있을 것 같습니다. 

(책임의 분리면에 있어서 SRP 를 생각해볼 수 있습니다. ㅡㅡ^) 


일단은 알고리즘 베이스에서 변화하는 부분을 추상화시킨 베이스 클래스를 아래와 같이 제작해 볼 수 있을 것 같습니다.


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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
/**
 * Model 객체의 알고리즘을 템플릿메소드화 시킨 베이스 클래스.
 *
 * Created by Doohyun on 2017. 5. 12..
 */
public abstract class BaseTargetComponent<T> {
 
    /**
     * 해당 테스크를 통해 DB 에 데이터를 저장.
     *
     * <pre>
     *     해당 메소드를 자식클래스들이 재정의하지 못하도록 final 화
     * </pre>
     *
     * @param addTargetMemberSnList
     * @param excludeTargetMemberSnList
     */
    public final void createByGenerateModel(
            List<Integer> addTargetMemberSnList
            , List<Integer> excludeTargetMemberSnList) {
        /**
         * 새로 저장할 구성원을 순번으로 그룹핑한다
         *
         * key : 구성원순번
         * value : 모델 객체
         */
        final HashMap<Integer, T> groupByMemberSnMemberMap = new HashMap<>();
        {
            for (Integer memberSn : addTargetMemberSnList) {
                // 일단은 MemberSn 만 넣는다고 가정.
                T generateModel = toGenerateModel(memberSn);
                groupByMemberSnMemberMap.put(memberSn, generateModel);
            }
        }
 
        // 이미 존재하는 구성원이거나 제외대상자는 입력 대상에서 제외.
        {
            // 이미 존재하는 구성원순번 또는 제외 타겟 순번 집합.
            HashSet<Integer> excludeTargetMemberSnSet = new HashSet<>();
            {
                // 이미 존재하는 구성원 순번 목록 삽입.
                List<T> existList = selectList(groupByMemberSnMemberMap.keySet());
                for (T model : existList) {
                    excludeTargetMemberSnSet.add(toMemberSubjectSn(model));
                }
 
                // 제외 대상 파라미터도 추가.
                excludeTargetMemberSnSet.addAll(excludeTargetMemberSnList);
            }
 
            // 추가대상 그룹에서 제외 대상 집합을 삭제한다.
            groupByMemberSnMemberMap.keySet().removeAll(excludeTargetMemberSnSet);
        }
 
        // 데이터 트랜잭션
        {
            // 데이터 삽입.
            for (T model : groupByMemberSnMemberMap.values()) {
                insertData(model);
            }
 
            // 제외대상 삭제.
            deleteByMemberSnList(excludeTargetMemberSnList);
        }
    }
 
    /**
     * 구성원 주체순번으로 모델 생성.
     *
     * @param memberSn
     * @return
     */
    protected abstract T toGenerateModel(Integer memberSn);
 
    /**
     * 모델로부터 구성원주체순번 추출
     *
     * @param t
     * @return
     */
    protected abstract Integer toMemberSubjectSn(T t);
 
    /**
     * 구성원 순번목록으로 모델 데이터 조회
     *
     * @param memberSnList
     * @return
     */
    protected abstract List<T> selectList(Collection<Integer> memberSnList);
 
    /**
     * 데이터 추가.
     *
     * @param t
     */
    protected abstract void insertData(T t);
 
    /**
     * 구성원순번 목록으로 데이터를 삭제.
     *
     * @param memberSnList
     */
    protected abstract void deleteByMemberSnList(List<Integer> memberSnList);
}
 
cs


앞서 살펴본 변화하는 후보 요구사항들은 추상 메소드로 정의하였으며, 변하지 않는 알고리즘은 정의된 추상 메소드를 사용하도록 하였습니다.


주목할 점은 알고리즘이 정의된 메소드는 final 입니다. 

자식클래스들이 혹시나 알고리즘이 정의된 메소드를 재정의할 수 없도록 의도적으로 막았습니다.


이제 정의된 베이스 클래스를 사용하여, Model1 과 Model2 에 특화된 클래스를 만들어 볼 수 있을 것 같습니다. 아래와 같이 말이죠!


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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
 * GenerateModel1 에 특화된 구체화 클래스.
 *
 * Created by Doohyun on 2017. 5. 12..
 */
public class Model1Compoent extends BaseTargetComponent<GenerateModel1> {
 
    private Model1Dao model1Dao;
 
    /**
     * 구성원순번 을 이용하여, GenerateModel1 을 생성한다.
     *
     * @param memberSn
     * @return
     */
    @Override
    protected GenerateModel1 toGenerateModel(Integer memberSn) {
 
        GenerateModel1 generateModel1 = new GenerateModel1();
        generateModel1.setMemberSubjectSn(memberSn);
 
        return generateModel1;
    }
 
    /**
     * GenerateModel1 으로 부터 구성원순번을 추출한다.
     *
     * @param model1
     * @return
     */
    @Override
    protected Integer toMemberSubjectSn(GenerateModel1 model1) {
        return model1.getMemberSubjectSn();
    }
 
    /**
     * 구성원순번 목록을 이용하여, 모델목록을 조회한다.
     *
     * @param memberSnList
     * @return
     */
    @Override
    protected List<GenerateModel1> selectList(Collection<Integer> memberSnList) {
        return model1Dao.selectList(memberSnList);
    }
 
    /**
     * 데이터를 추가한다.
     *
     * @param model1
     */
    @Override
    protected void insertData(GenerateModel1 model1) {
        model1Dao.create(model1);
    }
 
    /**
     * 구성원순번 목록으로 데이터를 삭제한다.
     *
     * @param memberSnList
     */
    @Override
    protected void deleteByMemberSnList(List<Integer> memberSnList) {
        model1Dao.deleteByMemberSnList(memberSnList);
    }
}
 
cs


Model1Component 는 변화하는 추상적인 개념을 Model1 클래스 전용으로 구체화만 시킨 클래스입니다. 


이제 아래와 같이 ModelSampleService 의 로직을 작성된 컴포넌트를 활용하여 완성해보도록 하죠.


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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/**
 * Model1 과 Model2 를 사용하는 서비스 클래스
 *
 * Created by Doohyun on 2017. 5. 14..
 */
@Service
public class ModelSampleService {
    @Autowired
    private Model1Dao model1Dao;
 
    @Autowired
    private Model2Dao model2Dao;
 
    private Model1Compoent model1Compoent;
 
    /**
     * 데이터를 저장한다.
     *
     * @param addTargetMemberSnList
     * @param excludeTargetMemberSnList
     */
    public void createByGenerateModel(
            List<Integer> addTargetMemberSnList
            , List<Integer> excludeTargetMemberSnList) {
 
        // GenericModel1 로직.
        model1Compoent.createByGenerateModel(addTargetMemberSnList, excludeTargetMemberSnList);
 
        /**
         * GenericModel2 로직.
         *
         * <pre>
         *     구체화상태를 즉시 정의하는 방식.
         *     함수형 프로그래밍에서는 기존의 클래스에만 의존하던 동작을 즉시 정의하는 것이 핵심.
         * </pre>
         */
        new BaseTargetComponent<GenerateModel2>() {
            @Override
            protected GenerateModel2 toGenerateModel(Integer memberSn) {
 
                GenerateModel2 generateModel2 = new GenerateModel2();
                generateModel2.setMemberSubjectSn(memberSn);
 
                return generateModel2;
            }
 
            @Override
            protected Integer toMemberSubjectSn(GenerateModel2 generateModel2) {
                return generateModel2.getMemberSubjectSn();
            }
 
            @Override
            protected List<GenerateModel2> selectList(Collection<Integer> memberSnList) {
                return model2Dao.selectList(memberSnList);
            }
 
            @Override
            protected void insertData(GenerateModel2 generateModel2) {
                model2Dao.create(generateModel2);
            }
 
            @Override
            protected void deleteByMemberSnList(List<Integer> memberSnList) {
                model2Dao.deleteByMemberSnList(memberSnList);
            }
        }.createByGenerateModel(addTargetMemberSnList, excludeTargetMemberSnList);
    }
}
cs


리팩토링한 코드의 라인 수만 본다면, 기존 코드에 비해 코드가 크게 줄어들지는 않았습니다. 오히려, 호스트 코드의 파편화가 생겨 더 복잡해졌다고 생각할 수도 있습니다.


하지만, 이 방식은 변하지 않는다고 생각한 알고리즘 코드에 문제가 있다고 생각되었다면 한 번에 모든 코드를 수정할 수 있고, 비슷한 테이블이 생겼다고 했을 경우 확장에도 유연할 수 있습니다. (OCP 역시 지켜졌다고 생각할 수 있습니다.)


추가적으로 Template method 의 전문적인 용어를 조금 말하면, 각각의 정의된 메소드 역할을 아래와 같이 나눠볼 수 있습니다.


- Concrete method 

알고리즘의 뼈대가 작성된 곳입니다. 

final 화하여, 자식클래스들이 상속을 받지 못하게 합니다.


- Abstract method

변하는 부분을 추상적인 형태로 정의합니다. (추상 메소드)

자식클래스들은 모두 이 메소드를 반드시 구현해야 합니다.


- Hooker

Abstract method 와 같이 변하는 부분을 추상적으로 정의합니다.

그러나 자식클래스들은 이 메소드를 선택적으로 구현하도록 합니다.


포스팅을 마치고 나니, 샘플코드의 양이 조금 복잡함을 느꼈습니다.

모르는 부분이 있으면 언제나 질문해도 좋습니다. 피드백을 남겨주면 더욱 감사 :-)





반응형
Posted by N'

4. 실무 활용 패턴 (중).pdf



이어지는 패턴이야기입니다.


프린트해오면 최고, 사실 굳이 안해와도 되지 않을까? ~~ @.@ ~~


참고자료 

- 한국기술교육대학교 - 객체지향개발론및실습 (김상진 교수님)

- [Head First] Design Pattern

반응형
Posted by N'

이번주는 본격적인 디자인패턴 종류를 실습해보았습니다. 


책에 있는 내용을 그대로 공부하기 보다는, 실무에서 활용되는 예제를 보면서 진행하는 것이 참신하지 않을까 고민을 했었습니다.


그렇기 때문에 프로젝트를 진행하면서, 느꼈던 노하우들을 같이 정리할 생각입니다. 최대한 현재 직장에 의존적이지 않게 작성하려고 합니다. :-)


자료는 아래 포스팅에서 참고.



서버쪽 프로그램을 작성할 때 우리는 DB 에 어떤 논리적인 상태를 표현하고자 할 경우가 많이 있으며, 보통은 개발자 사이의 약속으로 CODE 를 정의하고 CODE 에 따라 일처리를 다르게 합니다.


일반적으로 아래와 같이 CODE 만을 따로 한 군데에 정의하거나, 혹은 기타 파일로 뺄 수 있겠죠.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static final CODE {
    /**
     * SAMPLE TEST 로 진행할 코트 정의
     */
    public static final class CODE_FOR_SAMPLE {
        public static final String CODE_A   = "CODE_A";                     // 코드 A
        public static final String CODE_A_1 = "CODE_A_1";                   // 코드 A 의 하위 개념1
        public static final String CODE_A_2 = "CODE_A_2";                   // 코드 A 의 하위 개념2
 
        public static final String CODE_B   = "CODE_B";                     // 코드 B
        public static final String CODE_B_1 = "CODE_B_1";                   // 코드 B 의 하위 개념1
        public static final String CODE_B_2 = "CODE_B_2";                   // 코드 B 의 하위 개념2
        public static final String CODE_B_3 = "CODE_B_3";                   // 코드 B 의 하위 개념3
    }
}
 
cs


보통의 비지니스 로직들은 일반적으로 CODE에 의존적일 수 밖에 없습니다. 

코드에 따라 행위가 달라져야 하기 때문이죠. 

아마 아래와 같이 직접적으로 분기처리를 하게 될 것입니다. (각 switch 에서 하는 행위는 중요하지 않습니다.)


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
public class CommonService {
    /**
     * Code 종류에 따라서 업데이트를 수행한다.
     *
     * @param codeForSample
     */
    public void saveData(final String codeForSample) {
        switch (codeForSample) {
            case CODE.CODE_FOR_SAMPLE.CODE_A: {
                System.out.printf("%s관련 테이블 업데이트\n", CODE.CODE_FOR_SAMPLE.CODE_A);
 
                System.out.println("하위 컬럼 업데이트");
                System.out.printf("%s관련 테이블 업데이트\n", CODE.CODE_FOR_SAMPLE.CODE_A_1);
                System.out.printf("%s관련 테이블 업데이트\n", CODE.CODE_FOR_SAMPLE.CODE_A_2);
            }
 
            break;
 
            case CODE.CODE_FOR_SAMPLE.CODE_B: {
                System.out.printf("%s관련 테이블 업데이트\n", CODE.CODE_FOR_SAMPLE.CODE_B);
 
                System.out.println("하위 컬럼 업데이트");
                System.out.printf("%s관련 테이블 업데이트\n", CODE.CODE_FOR_SAMPLE.CODE_B_1);
                System.out.printf("%s관련 테이블 업데이트\n", CODE.CODE_FOR_SAMPLE.CODE_B_2);
                System.out.printf("%s관련 테이블 업데이트\n", CODE.CODE_FOR_SAMPLE.CODE_B_3);
            }
 
            break;
        }
    }
}
cs


일단 직관적인 면은 좋아보입니다. 각 분기 따라 무슨 일을 하는지 알겠군요. 


그러나 아쉽게도 유연해보이지는 않습니다. CODE_FOR_SAMPLE 에 CODE 가 추가되거나, 삭제되면 이를 사용하고 있는 로직은 모두 수정해야합니다. 


예제의 경우에는 saveData 한 가지를 언급했지만, 저런 형태의 분기문이 많거나 혹은 아래처럼 CODE 간의 상하관계 표현 등의 행위가 많다면 수정이 쉽지만은 않을 것입니다. (빨리 퇴근해야죠. ㅡㅡ^)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
  * 하위코드 목록을 출력한다.
  *
  * <pre>
  *     잘못된 입력에 대하여 에러가 나타남을 보장.
  * </pre>
  *
  * @param codeForSample
  * @return
  */
public List<String> getChildDataList(final String codeForSample) {
    switch (codeForSample) {
    case CODE.CODE_FOR_SAMPLE.CODE_A.CODE_FOR_SAMPLE.CODE_A:
        return Arrays.asList(CODE.CODE_FOR_SAMPLE.CODE_A_1, CODE.CODE_FOR_SAMPLE.CODE_A_2);
    case CODE.CODE_FOR_SAMPLE.CODE_B:
        return Arrays.asList(CODE.CODE_FOR_SAMPLE.CODE_B_1, CODE.CODE_FOR_SAMPLE.CODE_B_2, CODE.CODE_FOR_SAMPLE.CODE_B_3);
    }
 
    throw new RuntimeException(String.format("[에러] 코드의 입력이 잘못되었음!!!", codeForSample));
}
cs


이러한 비지니스 로직의 CODE 직접의존성을 제거하기 위해, SOLID 의 원칙 중 DIP(의존성 역전 원칙)에 따라 상위개념과 하위개념 모두 추상적인 것에 의존해볼 생각입니다. 


이 문제에서는 비지니스 로직을 작성하는 클래스가 CODE를 사용하고 있다는 느낌이니, 비지니스 로직을 상위개념 CODE는 하위개념인 상황으로 생각해 볼 여지가 있습니다.


또한 생각해볼 것은 비지니스로직은 CODE 가 중요하기보다는 CODE 를 통해 어떤 일을 통한 결과가 중요함을 생각해 볼 수도 있을 것 같습니다.




1. 전략패턴을 통한 리팩토링


직접적인 의존성을 제거하기 위해 추상적인 인터페이스 한 개를 작성할 생각이며, 해당 인터페이스는 일단 예시에서 작성한 saveData와 getChildDataList 를 지원할 생각입니다.


작성한 인터페이스는 아래와 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * CODE_FOR_SAMPLE 을 위한 전략 클래스
 * 
 * Created by Doohyun on 2017. 4. 25..
 */
public interface CodeForSampleStrategy {
 
    /**
     * CommonService 의 saveData 메소드를 지원
     */
    void saveData();
 
    /**
     * 본인의 항위 코드를 출력.
     * 
     * @return
     */
    List<String> getChildCodeList();
}
cs


이들의 구현체는 아래정도로 작성해 볼 수 있겠군요. 

편의 상, CODE_A, CODE_A1, CODE_B 정도만 구현해 볼 생각입니다. ㅡㅡ^


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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
 * CODE A 의 구체화 클래스
 * 
 * Created by Doohyun on 2017. 4. 25..
 */
public class CodeAStrategy implements CodeForSampleStrategy {
    @Override
    public void saveData() {
        System.out.printf("%s 관련 테이블 업데이트\n", CODE.CODE_FOR_SAMPLE.CODE_A);
 
        System.out.println("하위 컬럼 업데이트");
        for (String code :  getChildCodeList()) {
            System.out.printf("%s 관련 테이블 업데이트\n", code);
        }
    }
 
    @Override
    public List<String> getChildCodeList() {
        return Arrays.asList(CODE.CODE_FOR_SAMPLE.CODE_A_1, CODE.CODE_FOR_SAMPLE.CODE_A_2);
    }
}
 
/**
 * CODE A_1 의 구체화 클래스
 * 
 * Created by Doohyun on 2017. 4. 25..
 */
public class CodeA_1Strategy implements CodeForSampleStrategy {
    @Override
    public void saveData() {
        // 하위코드는 saveData 를 지원하지 않음.
    }
 
    @Override
    public List<String> getChildCodeList() {
        // 하위코드는 getChildCodeList 를 지원하지 않음.
    }
}
 
/**
 * CODE B 의 구체화 클래스
 * 
 * Created by Doohyun on 2017. 4. 25..
 */
public class CodeBStrategy implements CodeForSampleStrategy {
    @Override
    public void saveData() {
        System.out.printf("%s 관련 테이블 업데이트\n", CODE.CODE_FOR_SAMPLE.CODE_B);
        System.out.println("하위 컬럼 업데이트");
        for (String code :  getChildCodeList()) {
            System.out.printf("%s 관련 테이블 업데이트\n", code);
        }
    }
    @Override
    public List<String> getChildCodeList() {
        return Arrays.asList(CODE.CODE_FOR_SAMPLE.CODE_B_1, CODE.CODE_FOR_SAMPLE.CODE_B_2, CODE.CODE_FOR_SAMPLE.CODE_B_3);
    }
}
cs


기존 switch 문에 있던 구체행위들을 각 구체화 클래스로 이주하였습니다. 

더이상 비지니스로직이 갓클래스가 아닌 책임을 수행해야 할 각각의 클래스로 적절한 분배가 이루어 진 듯 합니다. (SRP : 단일 책임의 원칙)


이렇게 각 속성에 따라 책임을 지니고 행위를 수행하는 클래스를 지금부터 "전략클래스"라고 정의하겠습니다.


이제 실제 비지니스로직들은 아래와 같이 사용이 가능할 것 같습니다.


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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class CommonService {
 
    private Map<String, CodeForSampleStrategy> codeForSampleStrategyMap;
 
    private static final class ManagerHolder {
        private static final CommonService unique = new CommonService();
    }
 
    /**
     * CommonService 가 초기화 될 때 해야할 일 정의.
     */
    private CommonService() {
        codeForSampleStrategyMap = new HashMap<>();
        codeForSampleStrategyMap.put(CODE.CODE_FOR_SAMPLE.CODE_A, new CodeAStrategy());
        codeForSampleStrategyMap.put(CODE.CODE_FOR_SAMPLE.CODE_A_1, new CodeA_1Strategy());
        codeForSampleStrategyMap.put(CODE.CODE_FOR_SAMPLE.CODE_B, new CodeBStrategy());
        
    }
 
    public static final CommonService GetInstance() {
        return ManagerHolder.unique;
    }
    
    /**
     * 각 코드에 대한 전략 인스턴스를 출력한다
     * 
     * @param code
     * @return
     */
    public CodeForSampleStrategy getStrategyMapper(String code) {
        if (codeForSampleStrategyMap.containsKey(code)) {
            return codeForSampleStrategyMap.get(code);
        } else {
            throw new RuntimeException("[에러] : 잘못된 코드 요청");
        }
    }
 
    /**
     * Code 종류에 따라서 업데이트를 수행한다.
     *
     * @param codeForSample
     */
    public void saveData(final String codeForSample) {
        getStrategyMapper(codeForSample).saveData();
    }
 
    /**
     * 하위코드 목록을 출력한다.
     *
     * <pre>
     *     잘못된 입력에 대하여 에러가 나타남을 보장.
     * </pre>
     *
     * @param codeForSample
     * @return
     */
    public List<String> getChildDataList(final String codeForSample) {
        return getStrategyMapper(codeForSample).getChildCodeList();
    }
}
cs


현재 구조에서는 더이상 비지니스 로직들은 더이상 CODE 를 직접적으로 의존하지 않고 있습니다. CODE 가 변경된다면, 그에 대한 전략클래스들만 변경하고 Mapper 만 조정해주면 될 것 같아보입니다.


즉 추가,확장에 대해서 비지니스 로직을 건드리지 않아도 되니, 개방-폐쇄원칙(OCP)도 지켜진 듯 보입니다. 상-하위개념도 모두 추상적인 것을 바라보니, 앞써 언급한 DIP 도 지켜졌습니다.


하지만 안타깝게도 현재 상황에서 두 가지 안 좋은 냄새를 느꼈습니다.


- CODE 만큼 클래스가 늘어날 여지가 존재 (디버깅, 관리가 복잡)

- 전략을 쓰고자하는 곳은 모두 Mapper 를 일일이 초기화해야 할 필요 존재


특히, Mapper 문제는 CODE 의 추가,삭제가 될 때마다 해당 전략을 사용하는 클래스 전부 Mapper 과정을 수정해야합니다. 

물론 이에 대한 해결책을 많지만, 이번 포스팅에서는 Enum(열거형)이란 개념을 이용하여 수정해보려 합니다.



2. Enum 을 통한 전략 클래스 관리


Enum 자체는 어떤 개념을 나열하기 위한 형태라고 생각해 볼 수 있습니다. 

어떻게 생각해보면, CODE 의 개념과 같을 수 있습니다. 


Java 에서는 이러한 열거형에 행위(메소드)를 추가할 수 있습니다. 

즉 우리는 위에 작성된 CODE 개념에 행위를 추가할 수 있어 보이며, 이는 각 전략클래스들을 만드는 공수를 덜 수 있어보입니다.


아래는 Enum 을 이용한 각 전략의 정의를 한 형태입니다.


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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/**
 * CODE_FOR_SAMPLE 의 Enum 전략
 * 
 * Created by Doohyun on 2017. 4. 25..
 */
public enum EnumCodeForSampleStrategy {
 
    CODE_A (CODE.CODE_FOR_SAMPLE.CODE_A){
        @Override
        public void saveData() {
            saveDataForRootCode();
        }
 
        @Override
        public List<String> getChildCodeList() {
            return Arrays.asList(CODE.CODE_FOR_SAMPLE.CODE_A_1, CODE.CODE_FOR_SAMPLE.CODE_A_2);
        }
    },
 
    CODE_B (CODE.CODE_FOR_SAMPLE.CODE_B){
        @Override
        public void saveData() {
            saveDataForRootCode();
        }
 
        @Override
        public List<String> getChildCodeList() {
            return Arrays.asList(CODE.CODE_FOR_SAMPLE.CODE_B_1, CODE.CODE_FOR_SAMPLE.CODE_B_2, CODE.CODE_FOR_SAMPLE.CODE_B_3);
        }
    },
 
    /**
     * 기존 아무 일도 하지 않던 CODE A_1 은 아무것도 재정의하지 않음.
     */
    CODE_A_1 (CODE.CODE_FOR_SAMPLE.CODE_A_1);
 
    String code;
 
    EnumCodeForSampleStrategy(String code) {
        this.code = code;
    }
 
    public String getCode() {
        return code;
    }
 
    /**
     * 오직 루트코드만이 해야할 일 정의
     *
     * <pre>
     *     기존의 공통된 루트코드의 기능을 Concrete 메소드로 제작
     * </pre>
     */
    protected final void saveDataForRootCode() {
        System.out.printf("%s 관련 테이블 업데이트\n", getCode());
 
        System.out.println("하위 컬럼 업데이트");
        for (String code :  getChildCodeList()) {
            System.out.printf("%s 관련 테이블 업데이트\n", code);
        }
    }
 
    /**
     * 아무 행위도 하지 않는 Hooker
     *
     * <pre>
     *     선택적으로 기능을 정의할 것!
     * </pre>
     */
    public void saveData(){
    }
 
    /**
     * 아무 행위도 하지 않는 Hooker
     *
     * <pre>
     *     선택적으로 기능을 정의할 것!
     * </pre>
     */
    public List<String> getChildCodeList() {
        return Collections.emptyList();
    }
}
 
cs


Enum 을 통하여 전략 패턴을 했을 경우, 각 클래스로 분산된 행위들을 한 곳에 모을 수 있어보입니다. 즉 CODE 개수만큼 클래스를 만들지 않아도 되며, 열거형에 항목만 추가해주면 됩니다. (첫 번째 문제는 해결.)


Enum 에서는 또한 values() 같은 각 열거항목을 관리하기 위한 메소드가 존재합니다. 이를 이용하여, 위의 Mapping 과정을 아래와 같이 수정해볼 수 있을 것 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class CommonService {
 
    private Map<String, EnumCodeForSampleStrategy> codeForSampleStrategyMap;
 
    /**
     * CommonService 가 초기화 될 때 해야할 일 정의.
     *
     */
    private CommonService() {
        codeForSampleStrategyMap = new HashMap<>();
        
        // values 를 활용한 모든 전략 맵핑.
        // 전략이 추가되었다고, 더 이상 이 곳을 수정하지 않아도 됨!!
        for (EnumCodeForSampleStrategy enumStrategy : EnumCodeForSampleStrategy.values()) {
            codeForSampleStrategyMap.put(enumStrategy.getCode(), enumStrategy);
        }
 
    }
}
cs


또한 values 같은 속성의 유틸 메소드 역시 작성이 가능해보입니다. 


아래와 같이 필요에 따라 상위코드만 출력하거나, 하위코드만 출력하는 유틸(util)성 기능을 만들 수 있습니다. 


물론 이 것 역시 Enum 의 속성을 이용하여, 유연하게 개발을 해야겠죠! ㅡㅡ^


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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public enum EnumCodeForSampleStrategy {
 
    CODE_A (CODE.CODE_FOR_SAMPLE.CODE_A){
        @Override
        public void saveData() {
            saveDataForRootCode();
        }
 
        @Override
        public List<String> getChildCodeList() {
            return Arrays.asList(CODE.CODE_FOR_SAMPLE.CODE_A_1, CODE.CODE_FOR_SAMPLE.CODE_A_2);
        }
 
        @Override
        public Boolean getRootCodeYn() {
            return true;
        }
 
        @Override
        public Boolean getChildCodeYn() {
            return false;
        }
    },
    
    /**
     * 기존 아무 일도 하지 않던 CODE A_1 은 아무것도 재정의하지 않음.
     */
    CODE_A_1 (CODE.CODE_FOR_SAMPLE.CODE_A_1) {
        @Override
        public Boolean getRootCodeYn() {
            return false;
        }
 
        @Override
        public Boolean getChildCodeYn() {
            return true;
        }
    };
    
    /**
     * 부모 코드 여부
     * @return
     */
    public abstract Boolean getRootCodeYn();
 
    /**
     * 자식 코드 여부
     * @return
     */
    public abstract Boolean getChildCodeYn();
 
    /**
     * 루트 코드 목록 출력.
     * 
     * @return
     */
    public static List<EnumCodeForSampleStrategy> GetRootCodeList() {
        return Arrays.stream(values()).filter(EnumCodeForSampleStrategy::getRootCodeYn).collect(Collectors.toList());
    }
 
    /**
     * 자식 코드 목록 출력.
     *
     * @return
     */
    public static List<EnumCodeForSampleStrategy> GetChildCodeList() {
        return Arrays.stream(values()).filter(EnumCodeForSampleStrategy::getChildCodeYn).collect(Collectors.toList());
    }
}
 
cs



이로써, CODE 와 비지니스 로직간의 직접적인 의존성을 제거하였고 보다 유연한 코드가 탄생한 듯 보입니다. 이런 방식으로 전략 클래스를 만들어, 그 클래스가 행위를 하도록 위임한 것을 전략패턴이라 칭합니다. 


즉, 어플리케이션에서 변경소지가 있는 부분을 분리하여 캡슐화하고, 위임을 통하여 행위를 지정하는 것을 말합니다. 해당 사례의 경우 CODE 의 개념에 따라 행위를 캡슐화하였고, Mapper 를 이용해 각 전략클래스에게 행위를 위임하였습니다.


첫 번째 패턴 부터 긴 포스팅이 된 것 같습니다. 이해가 안가는 부분이 있다면, 언제든 찾아와서 질문을 해주시면 감사합니다. 


그리고, 여기까지 읽어줬으면, 공감한 번 눌러 주면 감사! ^^ 





반응형
Posted by N'

지난 주에는 객체지향의 개념을 재검토 및 SOLID 개념을 한 번 맛보는 자리였었습니다. 또한 배운 주제를 통하여 과제를 진행했었고, 예상대로 멋진고 인텔리한 우리 스터디 멤버들은 문제를 잘 해결해주었습니다. 


지난 주차에 했던 내용에 대한 포스팅은 아래 링크에서 확인! 



해당 글에서는 과제에 대한 리뷰를 진행해 보고자 합니다.


1. 기존 구현된 Task 클래스에 goMBM() 이라는 메소드를 추가.


스터디에서 진행했던, Task 에 요구사항 추가가 있었습니다. 

우리 회사에 다니는 모든 직군은 MBM 이라는 행사에 참여할 가능성이 있지만, 보통은 마케팅 팀에서 대부분 진행을 하는 것으로 압니다. 

(맞나요? ㅡㅡ^)


무튼, 아래와 같이 코드를 작성하려 하였습니다.


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
/**
 * 업무 처리
 *
 * Created by Doohyun on 2017. 4. 17..
 */
public abstract class Task {
 
    /**
     * 일을 한다는 약속
     *
     * <pre>
     *     이 메소드를 실행시키면 일을 하는 것.
     * </pre>
     */
    public abstract void runTask();
 
    /**
     * MBM 을 수행하는 메소드
     * 
     * <pre>
     *     구체적인 것이 추상적인 상태로 올라옴.
     * </pre>
     */
    public void goMBM() {
        if (this instanceof Marketing) {
            // 마켓팅 main.
        } else if (this instanceof Devlopment) {
            // 일단 여지는 있어보임.
        }
    }
 
    /**    
     * 면접관을 담당하는 메소드
     * 
     * <pre>
     *     면접관을 수행하는 메소드도 추가되길 바람.
     *     물론 이 업무 역시, 특정 직군 (마켓팅,개발) 만 할 수도 있음.
     * </pre>
     * 
     */
    public void goInterview() {
    }
}
cs


구현된 goMBM 을 보니, 좋지 않은 냄새가 납니다. 


일단 추상적인 것이 구체적인 것에 의존하면 안된다는 DIP (의존성 역전 원칙) 을 지키지 않고 있으며, 만약 해당 소스를 사용하는 어플리케이션에서 Task 의 구현체를 변경한다면, Task 자체 소스도 변경해 줘야하기 때문에 OCP (개발-폐쇄 원칙)도 지키지 않는 것으로 보입니다.


[Marketing 이 삭제가 된다면 Task 내부 메소드를 수정해야 하며, 새로운 작업을 MBM 으로 추가하고 싶다면 또 다른 구현체에 의존해야 합니다.]


이 소스의 리팩토링 여지는 있으며, 어떻게 생각해보면 간단하게 해결될 수 도 있습니다. 위 소스의 가장 큰 문제는 추상적인 것이 구체적인 것에 의존한고 있다는 것이며, 그 고리만 끊어주면 파생되는 문제는 해결될 것으로 보입니다.


일단 아래와 같이 Task 클래스 내부의 goMBM() 은 아무 일도 하지 않게 하려 합니다. Task 라는 추상개념으로 goMBM() 을 실행하는 것이 목적이기 때문에 invoker 는 살려두려 합니다. 


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
/**
 * 업무 처리
 *
 * Created by Doohyun on 2017. 4. 17..
 */
public abstract class Task {
 
    /**
     * 일을 한다는 약속
     *
     * <pre>
     *     이 메소드를 실행시키면 일을 하는 것.
     * </pre>
     */
    public abstract void runTask();
 
    /**
     * MBM 을 수행하는 메소드
     * 
     * <pre>
     *     추상적인 곳에서 추상클래스가 아닌 아무 일도 하지 않는 메소드를 hooker 라고 합니다.
     * </pre>
     */
    public void goMBM() {
    }
 
    /**    
     * 면접관을 담당하는 메소드
     * 
     * <pre>
     *     추상적인 곳에서 추상클래스가 아닌 아무 일도 하지 않는 메소드를 hooker 라고 합니다.
     * </pre>
     * 
     */
    public void goInterview() {
    }
}
cs


필수적인 runTask() 만이 추상의 구현을 하위 개념에게 강제하였고, 나머지 메소드 (goMBM(), goInterview()) 는 hooker 로써, 구현의 여부를 선택하게 하였습니다. 


goMBM() 메소드는 현재 요구사항으로 보았을 때, 일단 Marketing 에 구현될 필요가 있어 보입니다. goMBM() 에서 Marketing 이 해야할 일을 구체개념으로 빼도록 하겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * 마케팅 업무
 * Created by Doohyun on 2017. 4. 17..
 */
public class Marketing extends Task{
 
    @Override
    public void runTask() {
        System.out.println("마케팅 일을 하는 중...");
    }
 
    @Override
    public void goMBM() {
        System.out.println("마케팅은 MBM 을 떠난다.");
    }
}
cs


주목할 점은 goInterview() 를 오버라이딩 하지 않고 있으며, 즉 선택적으로 행위를 추가할 수 있음을 알 수 있습니다.



2. 가위바위보의 승패를 출력하는 메소드 제작


두 상태에 대한 승패여부를 if 나 자료구조 없, 나타내는 방법을 생각해보라는 주제였습니다. 아래 소스는 초기에 주어진 요구사항으로, 각 케이스에 대한 모든 분기가 되어있음을 알 수 있습니다.


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
/**
  * 심플한 가위,바위,보 결과
  * 
  * @param me
  * @param enemy
  * @return
  */
public static String 가위바위보_결과 (final String me, final String enemy) {
    if (me.equals("가위")) {
        switch (enemy) {
        case "가위":
            return "무승부";
        case "바위":
            return "패";
        case "보":
            return "승";
        }
    } else if (me.equals("바위")) {
        switch (enemy) {
        case "가위":
             return "승";
        case "바위":
            return "무승부";
        case "보":
            return "패";
        }
    } else if (me.equals("보")) {
        switch (enemy) {
        case "가위":
            return "패";
        case "바위":
            return "승";
        case "보":
            return "무승부";
        }
    }
 
    throw new RuntimeException("입력 오류");
}
 
cs


참여자 전원 모두 과제를 잘 해왔으며, 제출자의 코드를 기반으로 포스팅을 하고자합니다.


리팩토링을 생각해볼 부분은 분기처리이며, 각 분기처리에 대해서 책임소지가 있는 클래스로 기능을 이관(SRP)하는 작업이 필요할 것 같습니다.


책임 분배를 위해 아래와 같은 구체화 클래스를 만들려고 합니다. 

서로의 매치를 if 없이 관계를 주기 위하여, 오버로딩 개념을 사용하였습니다.


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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/**
 * 클래스 간의 서로의 사용개념을 처리하기 위한 오버로딩 인터페이스
 */
public interface StrategyVisitor {
    String visit(가위 가위);
    String visit(바위 가위);
    String visit(보 가위);
}
 
// 구체화된 가위 클래스
public class 가위 implements StrategyVisitor{
 
    @Override
    public String visit(가위 enemy) {
        return "무승부";
    }
 
    @Override
    public String visit(바위 enemy) {
        return "패";
    }
 
    @Override
    public String visit(보 enemy) {
        return "승";
    }
 
    @Override
    public String toString() {
        return "가위";
    }
}
 
// 구체화된 바위 클래스
public class 바위 implements StrategyVisitor {
 
    @Override
    public String visit(가위 enemy) {
        return "승";
    }
 
    @Override
    public String visit(바위 enemy) {
        return "무승부";
    }
 
    @Override
    public String visit(보 enemy) {
        return "패";
    }
 
    @Override
    public String toString() {
        return "바위";
    }
}
 
// 구체화된 보 클래스
public class 보 implements StrategyVisitor {
 
    @Override
    public String visit(가위 enemy) {
        return "패";
    }
 
    @Override
    public String visit(바위 enemy) {
        return "승";
    }
 
    @Override
    public String visit(보 enemy) {
        return "무승부";
    }
 
    @Override
    public String toString() {
        return "보";
    }
}
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
26
27
28
29
30
31
32
33
34
35
36
List<StrategyVisitor> me = Arrays.asList(new 가위(), new 바위(), new 보());
 
// 가위에 대한 응용처리
{
    가위 a = new 가위();
 
    System.out.println("============================");
    System.out.println("게입 시작합니다! \n");
    for (StrategyVisitor game1 : me) {
        System.out.println("me:" + game1.getClass().getSimpleName() + "  enemy:" + a.getClass().getSimpleName());
        System.out.println("result:" + game1.visit(a) + "\n");
    }
}
 
// 바위에 대한 응용처리
{
    바위 b = new 바위();
    System.out.println("============================");
    System.out.println("게입 시작합니다! \n");
    for (StrategyVisitor game1 : me) {
        System.out.println("me:" + game1.getClass().getSimpleName() + "  enemy:" + b.getClass().getSimpleName());
        System.out.println("result:" + game1.visit(b) + "\n");
    }
}
 
// 보에 대한 응용처리
{
    보 c = new 보();
            
    System.out.println("============================");
    System.out.println("게입 시작합니다! \n");
    for (StrategyVisitor game1 : me) {
        System.out.println("me:" + game1.getClass().getSimpleName() + "  enemy:" + c.getClass().getSimpleName());
        System.out.println("result:" + game1.visit(c) + "\n");
    }
}
cs


요구사항처럼 if 없이 관계는 처리가 되었습니다. 

하지만 제출자의 입장에서는 위와 같이 세 가지 케이스에 대하여 모든 경우를 비지니스 로직으로 만들고 싶지는 않았고, 자료구조를 두 개 선언하여 2중 for 문으로 해결하고 싶었다고 합니다.


하지만 아래와 같은 사유로 실패했습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
List<StrategyVisitor> me = Arrays.asList(new 가위(), new 바위(), new 보());
List<StrategyVisitor> enemy = Arrays.asList(new 가위(), new 바위(), new 보());
        
for (StrategyVisitor a : me) {
    System.out.println("============================");
    System.out.println("게입 시작합니다! \n");
    
    for (StrategyVisitor b : enemy) {
        System.out.println("me:" + a.getClass().getSimpleName() + "  enemy:" + b.getClass().getSimpleName());
        
        // 오버로딩 전략으로 가려고 했지만, enemy 리스는 추상적인 StrategyVisitor!!!!
        System.out.println("result:" + a.visit(b) + "\n");
    }
}
cs


두 자료구조의 for-loop 을 방문하면서, 로직을 처리하기 위하여 고전적인 방문자 패턴을 응용해보려 합니다.


3. 방문자패턴을 이용한 3차 리팩토링


오버로드를 수행하는 StrategyVisitor 를 일종의 방문자라 생각한다면, 방문할 Element 만 구현해주면 그만입니다.


방문할 Element 객체를 아래와 같이 정의합니다.


1
2
3
4
5
6
7
8
/**
 * 방문 대상 Element
 *
 * Created by Doohyun on 2017. 4. 27..
 */
public interface Strategy {
    String accept(StrategyVisitor strategyVisitor);
}
cs


방문대상인 Strategy 는 기존 StrategyVisitor 를 매개변수로 받는 메소드를 정의했습니다. 즉 방문자가 해당 Element 를 방문했을 때, 해야할 일을 구현체에 작성해주면 됩니다. 


이를위해 저는 기존 만들어진 가위,바위,보를 재활용하고자 합니다. 아래와 같이 말이죠.


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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
 * Element 와 Visitor 를 한 곳에 구현.
 * 구체화된 가위 클래스
 */
public class 가위 implements StrategyVisitor, Strategy{
 
    @Override
    public String visit(가위 enemy) {
        return "무승부";
    }
 
    @Override
    public String visit(바위 enemy) {
        return "패";
    }
 
    @Override
    public String visit(보 enemy) {
        return "승";
    }
 
    // 방문자가 본인을 다녀갔을 때의 처리를 수행합니다.
    @Override
    public String accept(StrategyVisitor strategyVisitor) {
       return strategyVisitor.visit(this);
    }
 
    @Override
    public String toString() {
        return "가위";
    }
}
 
// 구체화된 바위 클래스
public class 바위 implements StrategyVisitor, Strategy {
 
    @Override
    public String visit(가위 enemy) {
        return "승";
    }
 
    @Override
    public String visit(바위 enemy) {
        return "무승부";
    }
 
    @Override
    public String visit(보 enemy) {
        return "패";
    }
 
    @Override
    public String accept(StrategyVisitor strategyVisitor) {
        return strategyVisitor.visit(this);
    }
 
    @Override
    public String toString() {
        return "바위";
    }
}
 
// 구체화된 보 클래스
public class 보 implements StrategyVisitor, Strategy {
 
    @Override
    public String visit(가위 enemy) {
        return "패";
    }
 
    @Override
    public String visit(바위 enemy) {
        return "승";
    }
 
    @Override
    public String visit(보 enemy) {
        return "무승부";
    }
 
    @Override
    public String accept(StrategyVisitor strategyVisitor) {
        return strategyVisitor.visit(this);
    }
 
    @Override
    public String toString() {
        return "보";
   }
}
cs


accept 메소드를 모두 구현했으니 아래와 같이 클라이언트 코드를 작성해볼 수 있을 것 같습니다. 앞써, 제작된 코드와는 달리 다형화의 선택조건을 구현체 내부에서 구현하고 있기 때문에 아래와 같은 구현이 가능합니다.


1
2
3
4
5
6
7
8
9
10
11
12
List<StrategyVisitor> me = Arrays.asList(new 가위(), new 바위(), new 보());
List<Strategy> enemy = Arrays.asList(new 가위(), new 바위(), new 보());
 
for (StrategyVisitor visitor : me) {
    System.out.println("============================");
    System.out.println("게입 시작합니다! \n");
 
    for(Strategy strategy : enemy){
        System.out.println(String.format("me : %s,  enemy: %s", visitor, strategy));
        System.out.println(String.format("result : %s\n", strategy.accept(visitor)));
    }
}
cs


나름 신경을 써 볼 만한 문제 였던 것 같습니다. 다들 한 번이라도 신경을 써서 문제를 해결해줘서 고맙고, 다시 이 글을 봐주셔서 감사합니다. [꾸벅]


다음 주, Study 도 파이팅!!! 

반응형
Posted by N'

3. 실무 활용 패턴 (상).pdf


이번주 학습 주제는 실무 활용 패턴입니다.

 

패턴은 다양하기 때문에 한번에 많은 내용을 진행하지 않고 부분적으로 진행할 예정입니다.


프린트는 해오면 최고, 용지도 아껴주면 최고.



참고자료 

- 한국기술교육대학교 - 객체지향개발론및실습 (김상진 교수님)

- [Head First] Design Pattern




반응형
Posted by N'

지난 스터디에서 과제에 대한 내용을 포스팅하지 않았기 때문에 어떤 것을 해야하는 지에 대한 내용이 명확하지 않았던 것 같습니다. (경험이 짧은 그룹장이라 죄송합니다. ㅜㅡㅜ)


그렇기 때문에 해야할 요구사항에 대한 정리내용을 작성하고자 합니다.


1. 기존 구현된 Task 클래스에 goMBM() 이라는 메소드를 추가.


앞서, 스터디에서 다뤘던 내용 중 Task 에 요구사항을 추가하고 싶은 경우가 생겼습니다. 

Task 클래스 내부에 goMBM 이란 항목을 추가하고 싶으며, 해당 항목은 Marketing 만 가고 싶습니다. 물론 추 후에는 Development 도 MBM 에 가야할 수도 있습니다.


일단은 아래와 같이 임시로 코드를 만들었지만, 우리 MIDAS 의 개발자들은 똑똑하니 알아서 잘 리팩토링 해줄 수 있을 것이라 생각합니다.


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
/**
 * 업무 처리
 *
 * Created by Doohyun on 2017. 4. 17..
 */
public abstract class Task {
 
    /**
     * 일을 한다는 약속
     *
     * <pre>
     *     이 메소드를 실행시키면 일을 하는 것.
     * </pre>
     */
    public abstract void runTask();
 
    /**
     * MBM 을 수행하는 메소드
     * 
     * <pre>
     *     구체적인 것이 추상적인 상태로 올라옴.
     * </pre>
     */
    public void goMBM() {
        if (this instanceof Marketing) {
            // 마켓팅 main.
        } else if (this instanceof Devlopment) {
            // 일단 여지는 있어보임.
        }
    }
 
    /**
     * 면접관을 담당하는 메소드
     * 
     * <pre>
     *     면접관을 수행하는 메소드도 추가되길 바람.
     *     물론 이 업무 역시, 특정 직군 (마켓팅,개발) 만 할 수도 있음.
     * </pre>
     * 
     */
    public void goInterview() {
    }
}
 
cs


2. 가위바위보의 승패를 출력하는 메소드 제작


스터디 내부에서 했을 때는 요구사항을 의도했던 것과 달리, 문제 제시를 제대로 못했던 것 같습니다. input & output 과는 별도로 아래의 요구사항을 만족하면 됩니다.


가위바위보의 두 상태에 따른 승&패 여부를 확인할 수 있는 코드를 작성하고 싶습니다. 그렇기 때문에 첫 코드를 아래와 같이 작성하고자 하였습니다.  


[아마 첫 요구사항은 입력에 대한 이기는 결과를 출력해달라고 했을 것 같은데, 의도 했던 것은 사실 아래와 같았습니다. (미안, 기획을 잘해야돼 ㅜㅡㅜ)]


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
/**
  * 심플한 가위,바위,보 결과
  * 
  * @param me
  * @param enemy
  * @return
  */
public static String 가위바위보_결과 (final String me, final String enemy) {
    if (me.equals("가위")) {
        switch (enemy) {
        case "가위":
            return "무승부";
        case "바위":
            return "패";
        case "보":
            return "승";
        }
    } else if (me.equals("바위")) {
        switch (enemy) {
        case "가위":
             return "승";
        case "바위":
            return "무승부";
        case "보":
            return "패";
        }
    } else if (me.equals("보")) {
        switch (enemy) {
        case "가위":
            return "패";
        case "바위":
            return "승";
        case "보":
            return "무승부";
        }
    }
 
    throw new RuntimeException("입력 오류");
}
cs


다음부터 과제를 준비한다면, 이번 포스팅처럼 구체적으로 작성해보는 것으로 하겠습니다. 기획서 없이 개발하라고 시킨 것 같아 미안하군요. ㅜㅡㅜ 


그럼 오늘도 좋은 일만 가득하길 바래요. :-)





반응형
Posted by N'