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

3. 실무 활용 패턴 (상).pdf


이번주 학습 주제는 실무 활용 패턴입니다.

 

패턴은 다양하기 때문에 한번에 많은 내용을 진행하지 않고 부분적으로 진행할 예정입니다.


프린트는 해오면 최고, 용지도 아껴주면 최고.



참고자료 

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

- [Head First] Design Pattern




반응형
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'

JAVA8 IN ACTION 의 포스팅은 끝났습니다.


이번 포스팅은 지식공유 보다는, 추 후 제가 함수형을 공부하기 위한 용어 및 키워드를 정리합니다.


- 일급함수 


인수로 전달하거나, 결과로 반환하거나, 자료구조에 저장할 수 있는 함수를 말함. 

즉 람다나 메소드 레퍼런스 등 값으로 가질 수 있는 함수를 일급함수라고 정의


- 고차원 함수


여러 함수들을 묶어, 새 기능을 만드는 것을 말함 (comparing, andThen, compose).

즉 람다 조합을 말함



- 커링


함수를 모듈화하는 기법, 함수를 완성시키지 않고 인수를 받아 계속 다른 함수를 만드는 기법이라고 할 수 있다. 


예제


f(x, y, z) = x + y - z 를 다음과 같이 여러 형태로 변형 가능


g(x, y) = f(x, y, 2);

z(x) = f(x, 1, 3);


JAVA8 IN ACTION 에서는 변환요소, 기준치 조정 등을 위해 유용하다고 정리하고 있음


EX. swift 책에서는 커리함수를 파라미터를 받는 함수를 반환하는 함수라 기술.

아래는 커리함수에 대한 예제 (swift).


1
2
3
4
5
6
7
8
9
10
11
func Test () -> (Int-> String {
    return {
        (intValue : Int-> String in
            return String(intValue) + " 함수 커링 샘플 제작"
    }
}
 
print(Test()(5))
 
// PRINT RESULT
// 5 함수 커링 샘플 제작
cs



- 패턴매칭


if-then-else 등을 피하기 위해, 각 분기의 블록을 일급함수화 하여 입력받고, 분기 패턴에 따라 일치하는 일급함수를 실행(언랩)하는 것으로 보입니다.


조건부 연기를 생각하면 쉬울 듯합니다. (JAVA 에서는 아직 지원 안합니다.)



- 영속자료구조


자료구조의 형태는 함수형을 이용해 제작하고, 자료구조 내부를 바꾸지 말라는 원칙. 

의도치 않은 사이드 이펙트를 발생시키고 싶지 않음 -> 참조투명성 원칙.


- 동시성 문제 해결


함수형을 사용하면, 동시성 문제를 해결할 수 있음 (참조투명성 원칙에 의해..)


아래 포스팅 참고.



작년 이 맘 때부터 시작했던 포스팅 했던 JAVA8 의 포스팅이 끝나서 보람이 있네요. :-)

물론 JAVA8 in Action 의 모든 내용을 전문적으로 알고 기억하는 것도 아니지만 나름 뿌듯합니다.


좋은 소식은 현재 직장에서 JDK 버전을 8 로 올려준다는 것입니다. (미래를 내다본 것은 아닌지..)


하지만 안드로이드 개발을 위해 RxJava 및 오픈소스를 계속해야할 것으로 보이고, 곧 새 포스팅할 주제인 IOS-swift 도 보면서 다양한 함수형을 지원하는 언어 활용에 익숙해져 봐야할 것 같습니다.


이상 올 해 목표 중 하나를 끝냅니다.


자바 8 인 액션
국내도서
저자 : 라울-게이브리얼 우르마(RAOUL-GABRIEL URMA),마리오 푸스코(MARIO FUSCO),앨런 마이크로프트(ALAN MYCROFT) / 우정은역
출판 : 한빛미디어 2015.04.01
상세보기



반응형
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'

자바에서 시간을 다루기 위한 클래스가 존재합니다.


JAVA8 IN ACTION 에 따르면,


자바 1.0 에서부터 Date 클래스를 지원했지만 0부터 시작한 달, 애매한 offset, 자체적인 시간대의 부재 등 부실한 설계가 있었고, 자바 1.1 에서는 호환성을 깨트리지 않으면서 조금 더 유용하게 사용할 수 있게 설계한 Calendar 가 등장했습니다.


Calendar 에서 Date 의 개선사항이 존재했었지만 0부터 시작하는 달 등 애매한 내용은 아직 남아있었고, 제일 큰 문제는 Date 와 Calendar 가 동시에 존재함으로써 자바개발자들에게 혼란을 야기시켰다고 합니다.


아래는 기존 시간 관련 클래스를 사용하기 위한 방법입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Date 클래스 사용
Date date = new Date();
{
    System.out.println(date.getTime());
            
    // 다른 getXXX 는 Deprecated 되었습니다.
}
 
// Calendar 사용.
Calendar calendar = Calendar.getInstance();
{
    calendar.get(Calendar.YEAR);    // 년도.
    calendar.get(Calendar.MONTH);   // 0 부터 시작하는 애매한 달
}
cs


JAVA8 부터는 오라클에서 새로운 시간 관리 API 를 내놓았으며, 이는 JAVA8 의 다른 API (Stream, Optional 등)와 같이 Builder 패턴을 사용하여 사용성을 높였다는 것에 초점을 맞춰볼 수 있습니다.


이번 포스팅에서는 핵심이 되는 부분을 중점으로 다뤄볼 생각이며, 나머지는 메소드명이나 API 문서를 보면 쉽게 사용할 수 있을 것이라 생각합니다.


1. LocalDate, LocalTime, LocalDateTime


종종 우리는 기존 Calendar 나 Date 에서 날짜로부터 시간(시,분,초)을 제거하여 데이터처리를 하는 경우가 빈번하였었습니다. 그러나 JAVA API 에서는 날짜와 시간을 분리하여 관리할 수 있으며, 혹은 원할 경우 합쳐서 사용할 수도 있습니다.


아래는 새 자바 시간 관리 클래스들의 사용법입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 2017년 1월 31일 표현
LocalDate localDate = LocalDate.of(2017131);
 
// 1~12 로 관리됨에 주목. 객체인 Month 로 관리되는 것에 한번 더 주목
{
    // 달 출력
    Month month = localDate.getMonth();
    System.out.println(month.getValue());
}
 
// 10시 5분 40초 표현
LocalTime time = LocalTime.of(10540);
 
// 날짜와 시간의 조합
{
    LocalDateTime dateTime = LocalDateTime.of(localDate, time);
    System.out.println(dateTime.format(DateTimeFormatter.ISO_DATE_TIME));
}
 
// Print Result
// 1
// 2017-01-31T10:05:40
cs


개념적인 날짜와 시간을 객체로 분리하였으며, 이를 조립할 수 있다는 것이 매우 인상적입니다. 또한 기존 시간 클래스들과 달리 달 출력이 1부터 시작된다는 것이 마음에 드는군요. ㅡㅡ^



2. 날짜 조정


날짜에 대해서 특정정보를 출력하는 것과 더불어 날짜 데이터를 조정할 수 있습니다. 

예를들어 "3일 전 혹은 후의 날짜는?" 과 같이 특정 날짜개념을 연산할 수 있고, 입력 또한 할 수 있습니다.


날짜를 입력을 하기 위해서는 withXXX 메소드를 연산을 하기 위해서는 plusXXX, minusXXX 등의 메소드를 사용할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 2017년 1월 31일 표현
LocalDate localDate = LocalDate.of(2017131);
 
// 2016 년 1월 5일로 세팅.
LocalDate withDate =  localDate.withYear(2016).withDayOfMonth(5);
System.out.println(withDate.format(DateTimeFormatter.ISO_DATE));
 
// 2017 년 1월 31일의 2일 뒤는?
LocalDate plusDays =  localDate.plusDays(2);
System.out.println(plusDays.format(DateTimeFormatter.ISO_DATE));
 
// 2017 년 1월 31일의 3일 전은?
LocalDate minusDays =  localDate.minusDays(2);
System.out.println(minusDays.format(DateTimeFormatter.ISO_DATE));
 
// PRINT RESULT
// 2016-01-05
// 2017-02-02
// 2017-01-29
cs


앞서 수행했던 간단한 연산 이외에도 특정 전략에 맞춘 날짜 조정도 가능합니다. 


예를들어 "현재 달의 첫 번째 날짜는?", "현재 달의 마지막 날짜는?", "다음주 수요일은" 과 같은 질의처럼 말이죠.


with 메소드를 통해 전략적인 날짜조정이 가능합니다. 

전략 인터페이스인 TempralAdjusters 를 작성해주면 되죠. ㅡㅡ^


1
2
3
4
@FunctionalInterface
public interface TemporalAdjuster {
    Temporal adjustInfo(Temporal temporal);
}
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
 TemporalAdjuster 형오리의_근무일_전략 = temporal -> {
 
     // week 객체 생성.
    DayOfWeek week = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
 
    // 형오리의 쉬는 날의 전날
    Collection<DayOfWeek> hollyDays = Arrays.asList(DayOfWeek.WEDNESDAY, DayOfWeek.SATURDAY);
 
    return temporal.plus(hollyDays.contains(week) ? 2 : 1, ChronoUnit.DAYS);
};
 
IntStream.range(010).boxed().reduce(LocalDate.of(201736), (date, num) -> {
        LocalDate nextDay = date.with(형오리의_근무일_전략);
 
        Optional.of(nextDay).
                        map(localDate -> localDate.format(DateTimeFormatter.ofPattern("yyyy. MM. dd (E)").withLocale(Locale.KOREA))).
                        ifPresent(System.out::println);
 
        return nextDay;
 
    }, (d1, d2) -> d1);
 
// PRINT RESULT
// 2017. 03. 07 (화)
// 2017. 03. 08 (수)
// 2017. 03. 10 (금)
// 2017. 03. 11 (토)
// 2017. 03. 13 (월)
// 2017. 03. 14 (화)
// 2017. 03. 15 (수)
// 2017. 03. 17 (금)
// 2017. 03. 18 (토)
// 2017. 03. 20 (월)
cs


하지만 모든 전략을 구현해야하는 것은 아닙니다. 이미 JAVA8 에서는 많이 사용할 법한 전략을 팩토리 메소드로 제공하며, TemporalAdjusters 클래스에서 확인해 볼 수 있습니다. (예를들면, 현재 달의 마지막 날짜라던지.. 등 ㅡㅡ^)



3. Duration, Period


각 객체의 시간 및 날짜의 간격을 알고 싶은 경우가 존재하며, 이를 위해 JAVA8 에서는 Duration, Period 클래스를 제공합니다.


Duration 은 시간단위로 간격을 표현하며, 앞서 언급한 LocalTime, LocalDateTime, Instant(기계를 위한 시간객체) 를 이용할 수 있습니다. (LocalDate 는 시간단위로 표현할 수 없기 때문에 Duration 을 사용할 수 없습니다.)


Period 는 날짜단위의 간격을 표현하며, LocalDate 를 사용할 수 있습니다. 뒤의 나올 내용인 시간대 적용 시, 섬머타임을 바탕으로 간격을 표현하려면 Period 를 사용해야합니다.


또한 of 메소드를 사용하여, 기타의 임의 간격을 표현할 수 있습니다.


1
2
3
4
5
6
7
8
// 10:00 ~ 23:20 간격
Duration duration = Duration.between(LocalTime.of(1000), LocalTime.of(2320));
 
// 2016.1.5 ~ 2018.1.20 간격
Period period = Period.between(LocalDate.of(201615), LocalDate.of(2018120));
 
// 5일 간격
Period fiveDays = Period.ofDays(5);
cs



4. 시간의 파싱과 포맷팅


기존 SimpleDateFormat 과 같이 문자열에서 날짜를 parse 나 날짜 객체를 format 하여 출력하는 기능 역시 수행할 수 있습니다. 


새 API 의 등장과 함께 DateFormatter 라는 팩토리 클래스에서 포맷팅 객체를 생산할 수 있으며, 많이 사용할 법한 ISO_LOCAL_DATE (yyyy-MM-dd) 같은 패턴은 바로 사용할 수 있습니다.


1
2
3
4
5
6
LocalDate date = LocalDate.parse("2017-04-07", DateTimeFormatter.ISO_LOCAL_DATE);
 
Optional.of(date).map(d -> d.format(DateTimeFormatter.ofPattern("yyyy. MM. dd"))).ifPresent(System.out::println);
 
// PRINT RESULT
// 2017. 04. 07
cs



5. 특정 시간대 적용


만드는 제품이 세계적이라면, 특정 지역의 시간대를 사용하고 싶을 경우가 있습니다. 


기존에는 TimeZone 클래스가 그 역할을 했으며, 새 API 로는 ZoneId 클래스가 등장했습니다.

ZoneId 를 사용하면, 시간대 변경 시 썸머타임과 같은 복잡한 문제에 대해 고려하지 않아도 됩니다.


사용법은 지역ID 를 통해 객체를 생성할 수 있으며, LocalDate 군 클래스들을 ZonedDate 군 클래스로 랩핑할 수 있습니다. (즉 로컬 시간을 특정 시간대로 변환한다는 이야기이며, 아래 사진은 Local-Zoned 간의 관계를 나타낸 사진입니다. )


[출처 : JAVA8 IN ACTION]



예제는 아래와 같이 사용할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
ZoneId zoneId = ZoneId.of("Europe/Jersey");
 
 
LocalDateTime date = LocalDateTime.now();
ZonedDateTime zonedDateTime = date.atZone(zoneId);
 
System.out.println(date.format(DateTimeFormatter.ISO_DATE_TIME));
System.out.println(zonedDateTime.format(DateTimeFormatter.ISO_DATE_TIME));
System.out.println(zonedDateTime.format(DateTimeFormatter.ISO_INSTANT));
 
// Print Result
// 2017-04-05T00:47:21.346
// 2017-04-05T00:47:21.346+01:00[Europe/Jersey]
// 2017-04-04T23:47:21.346Z
cs

 

새로운 JAVA 날짜&시간 API 는 하위 호환성을 위해 Calendar, Date 를 fromXX 등으로 지원합니다. 즉 이전 작성된 코드들을 크게 변경하지 않아도 됩니다.


이번 포스팅을 하기 전에 Android 에서 사용할 Date, Calendar 를 이용한 빌더를 만든적이 있었는데요. (JAVA8 지원이 안되기 때문에....) 



어떤면에서는 쓰기 익숙한면도 있지만, JAVA8 의 API 의 설계에 비해 많이 약소함을 느끼고 있습니다. (그들은 많은 시간 고민해서 배포한 API 겠죠. TimeBuilder 역시 조금 더 손을 봐야겠다는 생각이 듭니다.  ㅡㅡ^)


이번 포스팅을 하며 각 API 의 내부를 들여다 보게되니, 더 노력해야겠다는 생각이 드는 초보 프로그래머였습니다.


자바 8 인 액션
국내도서
저자 : 라울-게이브리얼 우르마(RAOUL-GABRIEL URMA),마리오 푸스코(MARIO FUSCO),앨런 마이크로프트(ALAN MYCROFT) / 우정은역
출판 : 한빛미디어 2015.04.01
상세보기







반응형
Posted by N'

해당 포스팅에서 언급된 내용은 Ndroid 에서 제공합니다.

https://github.com/skaengus2012/Ndroid


Java 에서 시간 관련 클래스 (Calendar, Date) 들은 꽤 많이 사용됩니다. 


하지만 언제나 사용할 때마다 헷갈리는 부분이 존재하며, 간단한 기능도 직관적이지 않은 패턴을 사용해야합니다. (예를들면, Calendar 에서 달을 출력하려면? 현재 년도에 특정 값을 더하고 싶다면?)


1
2
3
4
5
Calendar calendar = Calendar.getInstance();
        
calendar.get(Calendar.MONTH); // 출력되는 달은 0-11 입니다. (1-12 가 안나온다는 것을 기억해야하죠.
        
calendar.add(Calendar.YEAR, 2); // 년도를 더하고 싶습니다. 덧셈을 하고 싶다면, Calendar 의 상수를 이용해야하죠.
cs


제 생각에 직관적이지 못하다고 생각하는 이유는 메소드명을 보고 행위를 하는 것이 아닌, 상수를 넣고 처리를 해야하기 때문입니다. 물론 Calendar 클래스에 익숙하다면 능숙하게 사용하겠지만, 날짜 연산에 대해 조금만 복잡하게 계산을 한다고 하면 저 상수를 이용한 연산을 줄줄이 입력해야할 수도 있습니다. 


Ndroid 프로젝트에서는 조금 더 이를 편하게 사용하고 싶었습니다. 


Calendar 의 사용목적이 결국 날짜 데이터를 핸들링하는 것이고, 일련의 연산과정 중 최종으로 핸들링된 날짜만 알고 싶습니다. 이를 마치 질의를 하는 것과 같은 선언형식으로 할 수 있다면 매우 코드가 아름다워지지 않을까라는 기대가 있었습니다.


Ndroid 의 TimeBuilder 모듈은 위와 같은 요구사항을 담도록 하였습니다. 


예를 들어 이러한 요구사항이 있다고 합시다.


- String 으로 된 시간을 파싱할 것.

- 파싱한 날짜에 년도를 1 덧셈

- 파싱한 날짜에 달을 5 덧셈.

- 파싱한 날짜에 일을 1 뺄셈

- 시,분,초는 생략할 것.

- 날짜는 영어권으로 보여줄 것.


기존 자바 코드로 작성을 한다면, 이 정도 되겠네요.


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
// simple java code.
 String dateString = "2017-3-26 16:40";
 
try {
    // parse.
    Date formatData = new SimpleDateFormat("yyyy-MM-dd hh:mm").parse(dateString);
 
    // calculating
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(formatData);
    calendar.add(Calendar.YEAR, 1);
    calendar.add(Calendar.MONTH, 5);
    calendar.add(Calendar.DAY_OF_MONTH, -1);
 
    // to yyMMdd
    calendar.set(Calendar.HOUR_OF_DAY, 0);
    calendar.set(Calendar.MINUTE, 0);
    calendar.set(Calendar.SECOND, 0);
    calendar.set(Calendar.MILLISECOND, 0);
 
    System.out.println(new SimpleDateFormat("yyyy.MM.dd (hh,mm,ss a)", Locale.ENGLISH).format(calendar.getTime()));
 
catch (ParseException e) {
    e.printStackTrace();
}
cs


간단하지는 않네요. 사용하려면 상수의 기능부터 알아야하고 (DAY_OF_MONTH, HOUR_OF_DAY 등은 한 번 찾아봐야겠죠 .ㅡㅡ^), SimpleDateFormat 클래스의 기능도 알아야합니다.


위의 기능을 TimeBuilder 를 통해 작업해보도록 하겠습니다.


1
2
3
4
5
6
7
8
TimeBuilder.Create(dateString, "yyyy-MM-dd hh:mm").
                addYear(1).
                addMonth(5).
                addDay(-1).
                setLocale(Locale.ENGLISH).
                to_yyMMdd().
                getStringFormat("yyyy.MM.dd (hh,mm,ss a)").
                subscribe(System.out::println);
cs


Builder 클래스에서 제공하는 메소드를 사용함으로써, 상수들을 알 필요가 없어졌으며 파싱 및 포맷을 위해서 사용할 SimpleDateFormat 역시 몰라도 됩니다.


파이프라인 메소드 중 getStringFormat 의 출력 타입은 RxJava 의 Maybe 입니다. 

포맷의 형식이 잘못되어 포맷에 실패할 수도 있으며, 이런 결과를 이전 MaybeUtil 에서 제공하던 기능들과 같이 값이 있거나 없거나의 문제로 보았습니다.


MaybeUtil 의 기능은 아래에서 참고 



TimeBuilder 의 인스턴스를 만드는 방법은 아래와 같이 다양합니다.


1
2
3
4
5
6
7
8
9
10
11
 // non-param : current time.
TimeBuilder currentTimeBuilder = TimeBuilder.Create();
 
// param : Calendar.
TimeBuilder calendarBuilder = TimeBuilder.Create(TimeUtil.GetCalendar());
 
// param : date
TimeBuilder dateBuilder = TimeBuilder.Create(new Date());
 
// param : string, format
TimeBuilder stringBuilder = TimeBuilder.Create("2017-3-26""yyyy-MM-dd");
cs


보다 자세한 내용은 아래 url 에서 확인하실 수 있습니다.



이 기능을 제작하게 된 첫 번째 배경은 JAVA8 에서 Date 관련 API 의 개편이 있던 것으로 알았고 (아직 공부해보지는 않았습니다. 아마 다음 올릴 포스팅이 해당 부분일 것입니다.), 안드로이드 진영에서는 아직 JAVA8 을 온전히 지원하지 않기 때문에 추가하고자 하는 배경이 되었습니다.


JAVA8 의 기능을 보고 TimeBuilder 의 기능이 더 추가될 여지가 있길 바랍니다. :-)


반응형
Posted by N'