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


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


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


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



서버쪽 프로그램을 작성할 때 우리는 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'