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


이번 주에는 지난 스터디에서 실습한 전략패턴과 비슷한 구조의 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'