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


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




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'

오늘 스터디 했던 내용 중 PDF 보충이 되는 내용에 대해 포스팅하고자 합니다.



1. FP 의 참조투명성을 이용한 다중 스레드 경쟁상태 제거.


참조투명성(같은 입력을 한다면, 언제나 같은 결과를 출력한다) 라는 원칙에 따라 스레드 경쟁상태를 피하는 법에 대해 살짝 맛보기를 하였습니다.


오늘 스터디에서는 경쟁상태를 피하고자 은행업무 예제를 한번 살펴보았습니다.


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
/**
 * 계좌에 대한 클래스
 */
class Account{
    
    private Integer money = 0;
 
    /**
     * 500 원 입금을 하는 메소드
     */
    public void deposit_500() {
 
        money += 500;
 
    }
 
    /**
     * 돈을 전부 출력하는 메소드
     * 
     * <pre>
     *     돈을 전부 출력한다. (돈의 금액이 음수가 되면 예외를 발생)
     * </pre>
     */
    public void withDrawAll() {        
        int withDraw = money;
 
        if (money - withDraw >= 0) {
            money -= withDraw;
        }
 
        if (money < 0) {
           throw new RuntimeException("[에러발생] 돈의 금액이 음수가 되었음!! --> 돈의 현재 금액 : " + money);
        }
    }
}
 
cs


돈의 입금은 500원씩하며, 돈의 출금은 모두 합니다. (출금 시, 돈의 금액이 음수가 되면 예외가 발생합니다.)


스레드 간의 경재상태를 만들기 위해 아래와 같은 테스트코드를 돌려보겠습니다. 


앞써, 스레드란 프로세스에서 실행흐름의 단위를 말합니다. 


아래와 같은 경우 현실세계에 대입해보면, 동시에 한명은 500원씩 입금하고 두명은 금액 모두를 출금하는 형태라고 볼 수 있습니다. ㅡㅡ^


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
Account account = new Account();
 
int count = 100000;
 
new Thread(() -> {
    // 100000 회 금액 모두를 출금하는 흐름1
    for (int i = 0; i < count; ++i) {
        account.withDrawAll();
    }
}).start();
 
new Thread(() -> {
    // 100000 회 금액 모두를 출금하는 흐름2
    for (int i = 0; i < count; ++i) {
        account.withDrawAll();
    }
}).start();
 
new Thread(() -> {
    // 100000 회 금액 500원을 입금하는 흐름3
    for (int i = 0; i < count; ++i) {
        account.deposit_500();
    }
}).start();
 
// CONSOLE LOG
java.lang.RuntimeException: [에러발생] 돈의 금액이 음수가 되었음!! --> 돈의 현재 금액 : 223000
java.lang.RuntimeException: [에러발생] 돈의 금액이 음수가 되었음!! --> 돈의 현재 금액 : 222500
cs


안타깝게도 동시성으로 각 행위를 실행했을 때, 금액이 음수가 되는 경우가 있나봅니다. 


심지어 에러로그 역시도 양수인 금액입니다. 

에러가 발생했지만 그 사이에 돈을 또 채워 넣었나 봅니다. (이러면, 디버그가 정말 어려워집니다.)


하지만 불굴의 프로그래머는 문제가 되는 지역이 withDrawAll 이라는 메소드라는 것을 알았으며, 운영체제적 지식도 있기 때문에 뮤텍스나 세마포어 등을 이용하여 동시적인 실행흐름이라도 이 곳은 꼭 한흐름만 실행되도록 을 걸었습니다. 


JAVA 개발자라면 간단한 키워드 synchronized 를 이용하여 이 문제를 해결했습니다.


아래와 같이 말이죠.  


1
2
3
4
5
6
7
8
9
10
11
public synchronized void withDrawAll() {
    int withDraw = money;
 
    if (money - withDraw >= 0) {
        money -= withDraw;
    }
 
    if (money < 0) {
        throw new RuntimeException("[에러발생] 돈의 금액이 음수가 되었음!! --> 돈의 현재 금액 : " + money);
    }
}
cs


하지만 개발자는 성능을 높이고자 병렬처리(흐름을 여러개 두어 일을 나눠처리)를 통해 효율을 높이고 싶었지만, 락이 걸려 순차처리(흐름을 한개두어 처리하는 방식, 특별히 병렬처리를 하지 않으면 순차처리방식) 를 하는만도 못하게 되었습니다.


이를 조금 더 효율적으로 하는 방법은 많겠지만, 우리는 FP 로 이 문제를 해결해보고자 합니다. 

FP 의 개념 중 하나인 순수함수로 이 문제를 해결할 수 있습니다.


1
2
3
4
5
6
7
8
9
public void withDrawAll() {
    int withDraw = money;
 
    money = Optional.of(money).filter(number -> number - withDraw >= 0).map(number -> number - withDraw).orElse(money);
 
    if (money < 0) {
        throw new RuntimeException("[에러발생] 돈의 금액이 음수가 되었음!! --> 돈의 현재 금액 : " + money);
    }
}
cs


물론 이 메소드 역시 에러가 발생하지 않습니다. 

파이프라인으로 작성된 부분은 같은 input 같은 result 를 보장합니다. (참조 투명성.)

(함수형 문법은 곧 배울테니, 생략합시다. 이정도가 있다 정도만 알고 가는 것으로.. ㅎㅎ)




2. 어댑터 패턴을 이용한 객체 변환


백-엔드 개발자 입장에서는 VO 를 쉽게 던지고 싶습니다. 


따로 다른 객체로 변형해야하는 과정은 매우 귀찮으며, 코드의 질을 떨어트립니다.


하지만 프론트-엔드 개발자의 경우 알아보기 쉬운 결과를 원하며, 이를 맞춰주기 위해서는 결국 다른 객체로 변형을 해야합니다.


결국 우리는 이런 로직을 모델에 해당하는 클래스에 정의해야합니다.


예제를 살펴봅시다.


우리는 아래의 HrSampleVo 를 사용한다고 가정합시다.


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
public class HrSampleVo {
    private Integer memberSubjectSn;        // 프론트에서 원하는 정보1
    private String name;                    // 프론트에서 원하는 정보2
    private String sampleResult;
    private Boolean maleYn;
 
    public Integer getMemberSubjectSn() {
        return memberSubjectSn;
    }
 
    public void setMemberSubjectSn(Integer memberSubjectSn) {
        this.memberSubjectSn = memberSubjectSn;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public String getSampleResult() {
        return sampleResult;
    }
 
    public void setSampleResult(String sampleResult) {
        this.sampleResult = sampleResult;
    }
 
    public Boolean getMaleYn() {
        return maleYn;
    }
 
    public void setMaleYn(Boolean maleYn) {
        this.maleYn = maleYn;
    }
}
 
cs


프론트-엔드에서 원하는 정보는 구성원주체순번과 이름입니다. 즉 해당 객체를 호환하는 다른 객체를 만들어야합니다. 아래와 같이 말이죠.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class HrFrontReturnVo {
    private Integer memberSubjectSn;        // 프론트에서 원하는 정보1
    private String name;                    // 프론트에서 원하는 정보2
 
    public Integer getMemberSubjectSn() {
        return memberSubjectSn;
    }
 
    public void setMemberSubjectSn(Integer memberSubjectSn) {
        this.memberSubjectSn = memberSubjectSn;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
}
 
cs


즉 우리는 두 객체의 호환을 맞춰 주기 위해 모델에 호환 알고리즘을 작성해야합니다.


1
2
3
4
5
HrSampleVo sampleVo = new HrSampleVo();
        
HrFrontReturnVo frontReturnVo = new HrFrontReturnVo();
frontReturnVo.setMemberSubjectSn(sampleVo.getMemberSubjectSn());
frontReturnVo.setName(sampleVo.getName());
cs


다 좋습니다. 모델 A 에서 해당 알고리즘을 작성을 하였는데, 모델B 에서도 같은 리즈가 생겼습니다. 결국 위의 알고리즘을 복사해서 또 써야겠군요....


좋지 않습니다. 복사한 알고리즘은 그 때 해결할 수 있겠지만, 변화가 생기면 다 변경해야합니다.    

우아하지 않아요. ㅡㅡ^


HrFrontReturnVo 로 변경해야하는 일은 HrSampleVo가 책임져야 할 일이지(단일 책임의 원칙),   여러 모델 클래스들이 할 일이 아닙니다.


즉 우리는 HrSampleVo 안에 아래와 같은 메소드를 추가해봅시다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class HrSampleVo {
    private Integer memberSubjectSn;        // 프론트에서 원하는 정보1
    private String name;                    // 프론트에서 원하는 정보2
    private String sampleResult;
    private Boolean maleYn;
 
    /**
     * HrFrontReturnVo 으로 변환
     * 
     * @return
     */
    public HrFrontReturnVo toHrFrontReturnVo() {
        HrFrontReturnVo newVo = new HrFrontReturnVo();
        newVo.setMemberSubjectSn(memberSubjectSn);
        newVo.setName(name);
        
        return newVo;
    }
}
 
HrSampleVo sampleVo = new HrSampleVo();
 
// 간단한 데이터 변형, 어느 모델에서든 HrSampleVo 을 사용한다면 HrFrontReturnVo 로 변경가능
HrFrontReturnVo frontReturnVo = sampleVo.toHrFrontReturnVo();
cs


위와 같은 VO 내부에 객체변형메소드를 추가하였습니다. 


즉 어느 모델에서든 변환 알고리즘을 사용할 수 있으며, 변경이 필요할 시, toHrFrontReturnVo 만 수정하면 됩니다.


하지만, ORM 객체는 변활 할 수 없는 문제가 있네요. 


ORM 객체 중 MYBATIS 의 경우 제너레이트 버튼을 한번 누르면 작성한 로직들이 모두 사라집니다. 즉 저런식으로 객체 내부에 메소드를 사용할 수 없어보이는데요. 하지만 걱정할 것은 없습니다. 패턴이란 정해진 것이 아니니까요.


제너레이트 과정을 통해 아래와 같은 MemberGenerateSimple 이 만들어졌고, 프론트로 넘기기 위한 HrFrontReturnVo 를 제작한다고 가정합시다.


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
public class MemberGenerateSimple {
    private Integer memberSubjectSn;
    private String name;
    private Integer companySubjectSn;
    private Integer age;
    private Boolean maleYn;
 
    public Integer getMemberSubjectSn() {
        return memberSubjectSn;
    }
 
    public void setMemberSubjectSn(Integer memberSubjectSn) {
        this.memberSubjectSn = memberSubjectSn;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public Integer getCompanySubjectSn() {
        return companySubjectSn;
    }
 
    public void setCompanySubjectSn(Integer companySubjectSn) {
        this.companySubjectSn = companySubjectSn;
    }
 
    public Integer getAge() {
        return age;
    }
 
    public void setAge(Integer age) {
        this.age = age;
    }
 
    public Boolean getMaleYn() {
        return maleYn;
    }
 
    public void setMaleYn(Boolean maleYn) {
        this.maleYn = maleYn;
    }
}
 
public class HrFrontReturnVo {
    private Integer memberSubjectSn;        // 프론트에서 원하는 정보1
    private String name;                    // 프론트에서 원하는 정보2
 
    // 제너레이트된 객체를 인자로 받아, 본인을 생성하는 정적 메소드 제작!
    public static HrFrontReturnVo CreateByMemberGenerateSimple(MemberGenerateSimple memberGenerateSimple) {
        HrFrontReturnVo newVo = new HrFrontReturnVo();
        newVo.memberSubjectSn = memberGenerateSimple.getMemberSubjectSn();
        newVo.name = memberGenerateSimple.getName();
 
        return newVo;
    }
}
 
MemberGenerateSimple memberGenerateSimple = new MemberGenerateSimple();
 
// 제너레이트 된 객체도 쉽게 변형가능. 객체의 생성을 숨기는 작업은 꽤 많은 편의를 줄 수 있습니다.
HrFrontReturnVo frontReturnVo = HrFrontReturnVo.CreateByMemberGenerateSimple(memberGenerateSimple);
cs


HrFrontReturnVo 를 직접 생산하지 않고, 정적메소드를 통해 특정목적에 맞게 생성하도록 하였습니다. 


만약 HrFrontReturnVo 가 오직 MemberGenerateSimple 객체를 통해서만 생성되길 원하고 남용되는 것을 막고자 한다면 아래와 같이 setter 나 생성자를 봉쇄함으로써(초기화 관련 작업 모두 제거), 다른 개발자들이 HrFrontReturnVo 사용하는 것을 막을 수 있습니다.


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
public final class HrFrontReturnVo {
    private Integer memberSubjectSn;        // 프론트에서 원하는 정보1
    private String name;                    // 프론트에서 원하는 정보2
    
    // 자체적인 객체 생성 막기
    private HrFrontReturnVo(){}
 
    // 제너레이트된 객체를 인자로 받아, 본인을 생성하는 정적 메소드 제작!
    public static HrFrontReturnVo CreateByMemberGenerateSimple(MemberGenerateSimple memberGenerateSimple) {
        HrFrontReturnVo newVo = new HrFrontReturnVo();
        newVo.memberSubjectSn = memberGenerateSimple.getMemberSubjectSn();
        newVo.name = memberGenerateSimple.getName();
 
        return newVo;
    }
 
    // 오직 getter 만 사용가능하며, 생성된 인스턴스는 오직 read 만 가능함.
    // vo 활용의 오남용 방지
    
    public Integer getMemberSubjectSn() {
        return memberSubjectSn;
    }
    
    public String getName() {
        return name;
    }
}
 
cs


이것으로 4월 10일에 pdf 와 같이 수업했던 내용의 정리를 마칩니다.

궁금한 것은 댓글이나, 찾아올 것 :-)

반응형
Posted by N'

1. 오리엔테이션.pdf


우리 스터디가 무엇을 공부할 것인지와 지향점 등 첫 모임 오리엔테이션 용 ppt 를 제작하였습니다.


다운받아서 출력해오면 최고.


첫 모임이니 노트북은 가져와도 될 것 같습니다. 



반응형
Posted by N'