지난 주에는 객체지향의 개념을 재검토 및 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'

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


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


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'

지난번과 마찬가지로 오늘 스터디 했던 내용에 대한 추가설명 및 코드를 남깁니다.


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




1. 캡슐화의 역할 중 하나는 행위의 숨김


데이터와 행위를 한 단위로 관리하는 캡슐화로 인하여, 크게 두 가지의 이점을 얻을 수 있습니다.


- 접근제어 (private, public) 등으로 의도치 않은 데이터 변경을 막을 수 있습니다.

- 행위가 숨겨져 있습니다. 즉 형태만 유지된다면 다른 알고리즘을 사용할 수 있습니다. (다형성)


예를들어 아래와 같이 Duck 클래스의 행위의 형태(메소드)만 지켜진다면 내부 알고리즘은 다르게 사용할 수 있습니다. 

(노란오리와 형오리는 다르지만, 사용자인 runTest 입장에서는 뭐든지 중요하지 않습니다. Duck 인 것이 중요하죠. ㅡㅡ^)


이를 다형성이라고 하며, OOP 에서 객체를 다루는 것의 핵심 개념이라 할 수 있습니다.


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
/**
 * 추상적인 오리 클래스
 *
 * Created by Doohyun on 2017. 4. 16..
 */
public abstract class Duck {
 
    public abstract String getName();
 
    /**
     * 비행하는 행위 수행.
     */
    public abstract void fly();
 
    /**
     * 꽥꽥소리를 내는 행위 수행
     */
    public abstract void quack();
}
 
/**
 * 브로덕 클래스 (오리의 형태를 지켜주고 있음)
 * 
 * Created by Doohyun on 2017. 4. 16..
 */
public class BroDuck extends Duck {
    @Override
    public String getName() {
        return "브로덕 한국말로 형오리!";
    }
 
    @Override
    public void fly() {
        System.out.println("나는 날 수 없다!");
    }
 
    @Override
    public void quack() {
        System.out.println("오리! 꽥꽥!");
    }
}
 
/**
 * 노란 오리 정의
 * 
 * Created by Doohyun on 2017. 4. 16..
 */
public class YellowDuck extends Duck{
 
    @Override
    public String getName() {
        return "노란 오리";
    }
 
    @Override
    public void fly() {
        System.out.println("오리 날다!!! 50m");
    }
 
    @Override
    public void quack() {
        System.out.println("꽥");
    }
}
 
 
/**
 * 오리를 사용하는 함수
 * @param duck
 */
public void runTest(Duck duck) {
    System.out.println("오리 이름 : " + duck.getName());
    duck.fly();
    duck.quack();
}
 
 
//////////////////////////////////////////
 
 
// 노란 오리 테스트
runTest(new YellowDuck());
 
// 형오리 테스트
runTest(new BroDuck());
 
// PRINT RESULT 
//        
// 오리 이름 : 노란 오리
// 오리 날다!!! 50m
// 꽥
//
// 오리 이름 : 브로덕 한국말로 형오리!
// 나는 날 수 없다!
// 오리! 꽥꽥!
cs



2. SRP, OCP, DIP 리팩토링 예제


SOLID 원칙 중 세 가지가 적용된 리팩토링 예제를 보았었습니다. 


준비된 첫 코드로 리팩토링을 했었는데, 그 과정을 블로그에 리뷰 해보도록 하겠습니다.


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. 16..
 */
public class Hyunji {
    private String name = "강현지";
 
    public String getName() {
        return name;
    }
 
    // Run Work.
    public void runTask() {
        workByPlanning();
        workByMarketing();
        workByDesign();
        workByDevelopment();
    }
 
    public void workByPlanning() {
        System.out.println("기획 일을 하는 중...");
    }
 
    public void workByDesign() {
        System.out.println("디자인 일을 하는 중...");
    }
 
    public void workByDevelopment() {
        System.out.println("개발 일을 하는 중...");
    }
 
    public void workByMarketing() {
        System.out.println("마케팅 일을 하는 중...");
    }
}
 
// Client Code.
{
    Hyunji hyunji = new Hyunji();
    
    System.out.println("업무 수행자 : " + hyunji.getName());
    hyunji.runTask();
}
cs


첫번 째 기준에서 작성된 Hyunji 클래스는 기획, 디자인, 개발, 마케팅 일을 하고 있습니다. 


Client code 는 이 클래스를 이용하여 잘 사용하고 있지만, 언제나 그랬듯이 사용하고 있는 클래스는 변화가 생길 것입니다. 우리가 할 일은 최대한 변화에 유연하게 설계를 하는 것입니다.


이 클래스는 적어도 4가지 이유로 변경될 여지가 있습니다. 기획, 디자인, 개발, 마케팅 일 중 하나가 변경될 때마다 이 클래스를 고쳐야 합니다. 또는 인사업무, 총무 등 다른 업무가 추가될 수도 있죠. 


다른 시기, 다른 이유로 클래스를 변경해야한다는 것은 그리 반가운 일은 아닌 것 같습니다. 일단  먼저 살펴볼 것은 변화하는 부분과 변하지 않는 부분을 찾아보는 것입니다. 


현재 주어진 스펙을 보면, Hyunji 클래스는 종사자로써 그 자체가 변한다기보다는 업무자체가 많이 변할 소지가 있어보입니다. (종사자의 업무가 바뀌는 것인지 종사자 자체가 변화하는 것은 아닐 것 같습니다. ㅡㅡ^ ) 


"클래스 하나 당 단일 책임을 부여하자는 SRP 원칙"에 따라 업무관련 메소드를 아래와 같이 Task 클래스로 이관하고자 합니다.


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
/**
 * 강현지 객체는 현재 기획,디자인,개발,마케팅 업무를 담당하고 있다.
 *
 * Created by Doohyun on 2017. 4. 16..
 */
public class Hyunji {
    private String name = "강현지";
    private Task task = new Task();
 
    public String getName() {
        return name;
    }
 
    // Run Work.
    public void runTask() {
        System.out.println("업무 수행자 : " + hyunji.getName());
        hyunji.workByPlanning();
        hyunji.workByMarketing();
        hyunji.workByDesign();
        hyunji.workByDevelopment();
    }
}
 
/**
 * 업무 처리
 *
 * Created by Doohyun on 2017. 4. 17..
 */
public class Task {
    /**
     * 기획 일을 처리.
     */
    public void workByPlanning() {
        System.out.println("inHR 기획 일을 하는 중...");
    }
 
    /**
     * 디자인 일을 처리.
     */
    public void workByDesign() {
        System.out.println("inHR 디자인 일을 하는 중...");
    }
 
    /**
     * 개발 일을 처리.
     */
    public void workByDevelopment() {
        System.out.println("개발 일을 하는 중...");
    }
 
    /**
     * 마케팅 일을 처리.
     */
    public void workByMarketing() {
        System.out.println("마케팅 일을 하는 중...");
    }
}
 
// Client Code.
{
    Hyunji hyunji = new Hyunji();
    hyunji.runTask();
}
cs


Task 로 업무 관련 메소드를 모두 옮겼기 때문에, 업무가 변경된다고 Hyunji 가 변경될 필요는 없습니다. 현재 Hyunji 는 Task에게 의존적인 상태가 되었습니다.


그러나 아쉽게도 업무가 삭제 되거나 추가되는 것에 대하여, Hyunji 클래스는 대응을 할 수 없습니다. 업무가 삭제된다면 Hyunji 클래스의 runTask() 메소드를 수정해줘야 하고, 업무가 추가된다면 runTask() 에 등록을 해줘야합니다. 


즉 수정사항이 생길 때마다 상위 모듈 내부를 고쳐야 합니다. 


우리는 수정에는 닫혀있고, 확장에는 열려있으라는 OCP 원칙을 배웠습니다.


- 수정에 닫혀있다는 것은 내부 코드를 수정하지 않아도, 확장하거나 변화할 수 있음을 말합니다.

- 확장에 열려있다는 것은 모듈을 추가하거나 행위를 변경할 수 있다는 것입니다.


즉 현재 Task 클래스의 내부 메소드(기획,디자인,마케팅,개발)들은 확장의 대상으로 보이며, 앞으로 다른 업무(인사, 총무 등)들을 추가할 여지도 있습니다.


또한 한 가지 배운 또 다른 법칙은 상위모듈(Hyunji)이 하위모듈(Task)에 의존하지 말고, 두 모듈 모두 추상에 의존하라는 DIP(의존성 역전 법칙)이 있었습니다. 


이 원칙들에 따라, Task 자체를 조금 더 추상화하고, 구체적인 업무개념(디자인, 개발, 마케팅, 기획 등)을 서술해보고자 합니다. Hyunji 는 여전히 Task(추상)에 의존하고 구체적인 업무(하위모듈) 역시 Task(추상)에 의존할 계획입니다.


아래는 작성된 코드입니다.


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
public class Hyunji {
    private String name = "강현지";
 
    private List<Task> taskList = new LinkedList<>();
 
    public String getName() {
        return name;
    }
 
    /**
     * 일을 추가한다
     *
     * @param task
     */
    public void addTask(Task task) {
        taskList.add(task);
    }
 
    /**
     * 업무를 수행.
     *
     */
    public void runTask() {
       taskList.stream().forEach(Task::runTask);
    }
}
 
public abstract class Task {
 
    /**
     * 일을 한다는 약속
     *
     * <pre>
     *     이 메소드를 실행시키면 일을 하는 것.
     * </pre>
     */
    public abstract void runTask();
}
 
/**
 * 마케팅 업무
 *
 * Created by Doohyun on 2017. 4. 17..
 */
public class Marketing extends Task{
 
    @Override
    public void runTask() {
        System.out.println("마케팅 일을 하는 중...");
    }
}
 
/**
 * 개발업무
 * 
 * Created by Doohyun on 2017. 4. 17..
 */
public class Devlopment extends Task {
 
    @Override
    public void runTask() {
        System.out.println("개발 일을 하는 중...");
    }
}
 
 
// Client Code.
{
    Hyunji hyunji = new Hyunji();
 
    // Task(추상)을 만족하는 업무를 유동적으로 변경할 수 있다. 변경에 있어서 다른 곳의 영향은 없음.
    hyunji.addTask(new Marketing());
    hyungi.addTask(new Devlopment());
 
    hyunji.runTask();
}
cs


변화하는 부분(업무의 개수 및 종류 등..)의 추가나 삭제가 있어도 다른 영역(Hyunji, Task 등)의 수정은 없어보입니다. [OCP를 일단 만족, 다른 업무를 추가하고 싶다면 Task 를 상속받은 객체를 Hyunji 의 addTask 메소드를 통해 추가시켜주면 됩니다. ㅡㅡ^ ]



3. 리스코프 치환 법칙 예제


LIP 는 하위 개념이 기반 개념의 행위를 불법으로 만들거나 무시하면 안된다는 법칙입니다. y 가 x 의 하위개념이라 했을 때 상위 개념 x 가 f(x) 를 만족한다면, y 도 f(y) 를 만족해야함을 말합니다. 


스터디에서는 이 개념을 설명하기 위해 가장 많이 제시되는 사각형 예제를 사용하였습니다.


사각형인 Rectangle 클래스를 만들었고, 이 클래스는 width 와 height 값을 가지고 있습니다. Rectangle 클래스의 기능은 width 와 height 을 변경하는 것이며, 아래와 같이 사용가능한 유닛테스트를 작성하였습니다.


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
/**
 * 심플한 사각형을 나타내는 객체
 *
 * Created by Doohyun on 2017. 4. 16..
 */
public class Rectangle {
    private Integer width = 0;
    private Integer height = 0;
 
    public Integer getWidth() {
        return width;
    }
 
    public void setWidth(Integer width) {
        this.width = width;
    }
 
    public Integer getHeight() {
        return height;
    }
 
    public void setHeight(Integer height) {
        this.height = height;
    }
 
    /**
     * 넓이 계산
     *
     * @return
     */
    public Integer getArea() {
        return width * height;
    }
 
    @Override
    public String toString() {
        return String.format("사각형 정보 [가로 : %d, 세로 : %d, 넓이 : %d]", width, height, getArea());
    }
}
 
/**
 * 사각형 서비스
 *
 * <pre>
 *     사각형 객체를 테스트 목적으로 사용하는 서비스.
 *     인스턴스는 오직 한 개로 존재해도 무방.
 * </pre>
 *
 * Created by Doohyun on 2017. 4. 16..
 */
public class RectangleTestUnitService {
 
    private RectangleTestUnitService(){}
 
    private static class ManagerHolder {
        private static RectangleTestUnitService unique = new RectangleTestUnitService();
    }
 
    public static RectangleTestUnitService GetInstance() {
        return ManagerHolder.unique;
    }
 
    private void printResult(Rectangle rectangle) {
        System.out.println("세팅 완료 -> " + rectangle.toString());
    }
 
    /**
     * 사각형 객체와 가로 세로 높이를 세팅한 후, 넓이를 체크한다.
     *
     * @param rectangle
     * @param w
     * @param h
     */
    public void checkArea(Rectangle rectangle, Integer w, Integer h) {
        rectangle.setWidth(w);
        rectangle.setHeight(h);
 
        if ((rectangle.getHeight() * rectangle.getWidth()) != (w * h)) {
            throw new RuntimeException("[에러] 사각형의 넓이가 올바르게 계산되지 않음");
        }
 
        printResult(rectangle);
    }
 
    /**
     * 가로 값 세팅에 대하여 유효성 체크를 한다.
     *
     * @param rectangle
     * @param w
     */
    public void checkAreaOnlyWidth(Rectangle rectangle, Integer w) {
        rectangle.setWidth(w);
 
        if ((rectangle.getHeight() * rectangle.getWidth()) != (w * rectangle.getHeight())) {
            throw new RuntimeException("[에러] 사각형의 가로 계산이 올바르게 계산되지 않음");
        }
 
        printResult(rectangle);
    }
 
    /**
     * 가로 값 세팅에 대하여 유효성 체크를 한다.
     *
     * @param rectangle
     * @param h
     */
    public void checkAreaOnlyHeight(Rectangle rectangle, Integer h) {
        rectangle.setHeight(h);
 
        if ((rectangle.getHeight() * rectangle.getWidth()) != (h * rectangle.getWidth())) {
            throw new RuntimeException("[에러] 사각형의 세로 계산이 올바르게 계산되지 않음");
        }
 
        printResult(rectangle);
    }
}
 
// Client Code
{
    Rectangle rectangle = new Rectangle();
 
    RectangleTestUnitService rectangleTestUnitService = RectangleTestUnitService.GetInstance();
 
    rectangleTestUnitService.checkArea(rectangle, 54);
    rectangleTestUnitService.checkAreaOnlyWidth(rectangle, 2);
    rectangleTestUnitService.checkAreaOnlyHeight(rectangle, 9);
 
}
 
// PRINT RESULT
//
// 세팅 완료 -> 사각형 정보 [가로 : 5, 세로 : 4, 넓이 : 20]
// 세팅 완료 -> 사각형 정보 [가로 : 2, 세로 : 4, 넓이 : 8]
// 세팅 완료 -> 사각형 정보 [가로 : 2, 세로 : 9, 넓이 : 18]
 
cs


결과는 잘 나오는 것으로 보입니다. 우리는 잘 나오는 Rectangle 클래스를 이용하여, 그의 상속 개념인 Square(정사각형)을 구현하고자 합니다. 


논리적으로 "정사각형은 사각형"이니 문제 없을 것으로 보입니다. 하지만 주의할 것은 width 나 height 이 수정된다면, 언제나 가로/세로 길이를 아래와 같이 맞춰줘야한다는 것입니다.


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 class Square extends Rectangle{
 
    /**
     * 정사각형의 개념을 살리고자, 가로/세로 길이를 동기화
     * 
     * @param width
     */
    @Override
    public void setWidth(Integer width) {
        super.setWidth(width);
        super.setHeight(width);
    }
 
    /**
     * 정사각형의 개념을 살리고자, 가로/세로 길이를 동기화
     *
     * @param width
     */
    @Override
    public void setHeight(Integer width) {
        super.setWidth(width);
        super.setHeight(width);
    }
}
 
// Client Code
{
    Square square = new Square();
 
    RectangleTestUnitService rectangleTestUnitService = RectangleTestUnitService.GetInstance();
 
    rectangleTestUnitService.checkArea(square, 54);
    rectangleTestUnitService.checkAreaOnlyWidth(square, 2);
    rectangleTestUnitService.checkAreaOnlyHeight(square, 9);
 
}
 
// ERROR LOG
Exception in thread "main" java.lang.RuntimeException: [에러] 사각형의 넓이가 올바르게 계산되지 않음
 
cs


디버깅을 해보니, 가로/세로 길이의 동기화가 사각형유닛테스트 모듈의 에러 원인이었습니다.


Square 는 기존 Rectangle 의 width 혹은 height 값을 변경하기 위해 setter 메소드를 오버라이딩하였고, 그 메소드들은 Rectangle의 의도와 달리 모든 변의 값을 변경시키고 있습니다. 즉 ISP 에 위반하였고, Square 와 Rectangle 이 다름을 인지하였습니다.


그러나 Square 객체로 그동안 만든 Rectangle 의 기능들을 사용하고 싶은 리즈가 있습니다. 왜냐하면, 인간 개념에서 논리적으로 Square 는 Rectangle 이기 때문이죠. 이를 위해 저번 스터디에서 배웠던 어댑터 패턴을 이용하고자 합니다.


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
public class Square {
 
    // 정사각형은 사실 변의 길이만 알고 있으면 됨.
    private Integer length;
 
    /**
     * 정사각형을 사각형으로 어댑팅 해주는 메소드 추가.
     *
     * @return
     */
    public Rectangle toRectangle() {
        Rectangle rectangle = new Rectangle();
        rectangle.setHeight(length);
        rectangle.setWidth(length);
 
        return rectangle;
    }
 
    public void setLength(Integer length) {
        this.length = length;
    }
}
 
// Clinet Code
{
    Square square = new Square();
 
    Rectangle rectangle = square.toRectangle();
 
    RectangleTestUnitService squareTestUnitService = RectangleTestUnitService.GetInstance();
 
    squareTestUnitService.checkArea(rectangle, 54);
    squareTestUnitService.checkAreaOnlyWidth(rectangle, 2);
    squareTestUnitService.checkAreaOnlyHeight(rectangle, 9);
}
 
cs


비록 RectangleTestUnitService 의 내부 메소드들은 Rectangle 에 맞춰있기 때문에 Square 의 고유 특성을 살려 작업할 수 없습니다. (맞춘다는 것 자체가 넌센스 ㅡㅡ^) 


하지만 우리는 두 클래스의 관계를 과정을 거쳐 지금같이 명시를 함으로써, Square 인스턴스가 RectangleTestUnitService 에서 일으킬 부작용을 막았으며 혹여나 사용하고 싶다면 위와 같이 우회할 수 있습니다. 


이번 스터디 내용은 뭔가 어려운 것 같지만, 두 가지 정도로 요약해 볼 수 있을 것 같습니다.


- 변하는 부분과 변하지 않는 부분을 분리할 것.


- 추상화할 수 있는 부분을 최대한 추상화하고, 복잡한 개념은 하위 개념으로 미루자. 


이 개념들을 잘 살려서, 이번 주에 제시한 문제들을 해결할 수 있기를 바랍니다. 

(오므라이스는 먹을 수 있나요? 인증샷을 안찍었으니 마지막은 오므라이스 사진 ㅋㅋㅋ )





반응형
Posted by N'

2. OOP 개념의 재검토 (with SOLID).pdf


OOP 개념 기초부터 설계원칙 SOLID 를 스터디하고자 합니다.

먼저 읽어보면 최고, 프린트 해오면 최고 (용지 아끼자 ㅡㅡ^ [쿨럭])


참고자료 

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

- [Head First] Object-Oriented Analysis & Design

반응형
Posted by N'