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


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




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'