함수형 프로그래밍을 시작하는 첫 장에서는 


- 함수란 무엇인가?


- 다형성(Polymorphism)의 필요성과 이를 이용하기 위한 OOP 의 한계


- 이를 극복하기 위한, 기존 Java8 이전의 방법과 편리해진 람다와 메소드 레퍼런스


정도의 내용을 다루었습니다. (위의 개념에 대해 생각이 안난다면, 꼭 복습을 권장합니다.)


이에 대한 포스팅 정보는 아래에서 확인 :-)



그리고 이를 실습해보기 위한 과제가 있었고, 이번 포스팅에서는 과제에 대한 리뷰를 해보고자 합니다.


1. SwitchAndSwitch


첫 번째 숙제는 람다에 대한 튜토리얼 진행을 해보고자 했던 내용이었습니다.


요구사항은 아래의 소스를 리팩토링하는 것이었습니다.

메소드 내부에 존재하는 코드들은 일련의 공통적인 작업들이 많이 보입니다.


보통은 이런 작업에 대해서 따로 메소드 추출 등의 리팩토링을 하겠지만, 이 로직이 오직 switchAndSwitch 메소드에서만 사용될 것 같았기 때문에 다른 방법을 찾아보자는 것이었습니다.

(추 후, 다른 로직에서도 사용이 된다면 그 때 캡슐화하는 것으로...)


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
public void switchAndSwitch() {
 
    List<GenerateModel1> generateModel1List = Model1Dao.GetInstance().selectList(Arrays.asList(1,2,3));
 
    // something work1
    {
        List<Integer> memberSnList = Arrays.asList(2,3);
 
        for (GenerateModel1 model1 : generateModel1List) {
             if (memberSnList.contains(model1.getMemberSubjectSn())) {
                String name = model1.getName();
 
                switch (name) {
                case "강현지":
                    System.out.println("IF 란 사치임을 증명한 " + name);
                    break;
                
                case "유덕형":
                    System.out.println("한 수에 버그를 말살하는 " + name);
                    break;
                case "유형주":
                    System.out.println("한 메소드에 5줄 이면 충분한 " + name);
                    break;
                }
            }
        }
    }
 
    // something work2
    {
        List<String> filterNameList = Arrays.asList("강현지""유덕형");
 
        for (GenerateModel1 model1 : generateModel1List) {
            if (filterNameList.contains(name)) {
                String name = model1.getName();
            
                switch (name) {
                case "강현지":
                    System.out.println("IF 란 사치임을 증명한 " + name);
                    break;
                
                case "유덕형":
                    System.out.println("한 수에 버그를 말살하는 " + name);
                    break;
 
                case "유형주":
                    System.out.println("한 메소드에 5줄 이면 충분한 " + name);
                    break;
                }
            }
        }
    }
}
cs


우리는 일련의 동작 즉 함수를 값으로 가질 수 있다는 개념을 알았고, 이를 이용해서 굳이 메소드 추출을 안하고 이 문제를 해결할 수 있었습니다.


네, 람다를 이용해보는 것이죠.


그 전에 프로그래밍 원칙 중 중요한 원칙이 한 개 있습니다.


"변하는 부분과 변하지 않는 부분을 분리하라!"


이 법칙에 근거했을 때, 저는 해당 로직에 대해 다음과 같이 정의를 했습니다.


- 변하는 부분 : GenerateModel1 에 대한 필터 로직

- 변하지 않는 부분 : Loop 를 돌며, switch 문을 수행하는 과정.


이에 따라 저는 다음과 같이 리팩토링 할 수 있었습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public void switchAndSwitch() {
 
    List<GenerateModel1> generateModel1List = Model1Dao.GetInstance().selectList(Arrays.asList(1,2,3));
 
    /**
     *  Predicate 를 받아, 일련의 공통작업을 수행하는 소비자.
     */
    Consumer<Predicate<GenerateModel1>> switchConsumer =
        (predicate) -> {
            for (GenerateModel1 model1 : generateModel1List) {
                
                if(predicate.test(model1)) {
 
                    String name = model1.getName();
                    
                    switch (name) {
                    case "강현지":
                        System.out.println("IF 란 사치임을 증명한 " + name);
                        break;
 
                    case "유덕형":
                        System.out.println("한 수에 버그를 말살하는 " + name);
                        break;
 
                    case "유형주":
                        System.out.println("한 메소드에 5줄 이면 충분한 " + name);
                        break;
                    }
                }
            }
        };
 
    // something work1
    {
        List<Integer> memberSnList = Arrays.asList(2,3);
 
        // 프리디케이트가 정의되는 부분이 변하는 부분.            
        switchConsumer.accept(model1 -> memberSnList.contains(model1.getMemberSubjectSn()));    
    }
 
    // something work2
    {
        List<String> filterNameList = Arrays.asList("강현지""유덕형");
 
        // 프리디케이트가 정의되는 부분이 변하는 부분.            
        switchConsumer.accept(model1 -> filterNameList.contains(model1.getName()));    
    }
}
cs


람다에 대한 첫 튜토리얼로 나름 나쁘지 않았다고 생각합니다. :-)


사실 개인적으로 람다표현식 보다도 위의 규칙이 더 중요하며, 저 규칙만 잘지켜도 좋은 프로그래머가 될 수 있을 것이란 생각이 드는군요.



2. Template-Method 의 극복? 이 방법은 정말로 좋은가?


동적 파라미터화를 이용하면, 굳이 클래스를 상속받아 구현하지 않아도 다형성을 사용할 수 있음을 알았습니다.


또한 JAVA8 에서는 기본 함수형 인터페이스까지 제공해주기 때문에 동적파라미터화를 하기 위해 따로 인터페이스를 제작하지 않아도 될 것 같아 보이는데요.


기본형 함수형 인터페이스에 대한 내용은 아래 포스팅에서 참고.



이 숙제는, 정말로 이 방법이 좋은 지에 대해 다뤄보는 내용이었습니다.


요구사항은 다음과 같았습니다.


ORM 으로 제작된 클래스들의 필드는 종종 비슷한 경우가 많지만, 아쉽게도 제너레이터에 의해 제작되기 때문에 이 곳을 수정하는 것은 문제가 있어보입니다.

(즉 상속 등 클래스 관계를 지어줄 수 없으며, 이는 꽤 골치아픈 문제가 될 수 있습니다.)


즉 이러한 이유로 다형성 등의 개념을 이용할 수 없는 것처럼 보이며, 이는 비슷한 로직도 재활용이 쉽지 않음을 의미합니다.


이를 극복하기 위한 여러가지 방법을 다뤘으며, 대표적인 방법 중 한 가지는 Template-Method 패턴을 이용해보는 것이었습니다.



하지만, 굳이 한 메소드 제작을 위해서 복잡한 클래스 구조를 가질 수 있어 보이는 Template-Method 를 사용하는 것은 부담이라 생각하였습니다.

(이런 생각은 귀차니즘에서 보통 비롯하곤 합니다. ㅡㅡ^)


그러던 중, 동적 파라미터화 및 기존 제공 함수형 인터페이스가 있는 것을 배웠고 이를 이용해 Template-Method 와 비슷한 효과를 낼 수 있을 것 같았습니다.


즉, 어떠한 인터페이스나 추상클래스를 만들지 않고 Template-Method 를 흉내내는 것이 이 과제의 목적이었습니다.


아래 소스는 과제로 제출해준 한 분의 소스입니다.

(제작해주셔서 감사합니다.^^)


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
/**
 * 제너레이트 된 모델에 대한 비지니스 로직 정의.
 *
 *
 * @param <G>
 * @param addTargetMemberSnList
 * @param excludeTargetMemberSnList
 */
public <G> void createByGenerateModel(
    List<Integer> addTargetMemberSnList
    , List<Integer> excludeTargetMemberSnList
    , Function<Integer, G> toGenerateModel
    , Function<List<Integer>, List<G>> selectList
    , Function<G, Integer> toMemberSubjectSn
    , Consumer<G> insertData
    , Consumer<List<Integer>> deleteByMemberSnList) {
        
    final HashMap<Integer, G> groupByMemberSnMemberMap = new HashMap<>();
    {    
        for (Integer memberSn : addTargetMemberSnList) {
            // 일단은 MemberSn 만 넣는다고 가정.
                
            G generateModel = toGenerateModel.apply(memberSn);
            groupByMemberSnMemberMap.put(memberSn, generateModel);
        }
    }
 
    // 이미 존재하는 구성원이거나 제외대상자는 입력 대상에서 제외.
    {
        // 이미 존재하는 구성원순번 또는 제외 타겟 순번 집합.
        HashSet<Integer> excludeTargetMemberSnSet = new HashSet<>();
        {
            // 이미 존재하는 구성원 순번 목록 삽입.
            List<G> existList = selectList.apply(groupByMemberSnMemberMap.keySet().stream().collect(Collectors.toList()));
            for (G model : existList) {
                excludeTargetMemberSnSet.add(toMemberSubjectSn.apply(model));
            }
 
            // 제외 대상 파라미터도 추가.
            excludeTargetMemberSnSet.addAll(excludeTargetMemberSnList);
        }
 
        // 추가대상 그룹에서 제외 대상 집합을 삭제한다.
        groupByMemberSnMemberMap.keySet().removeAll(excludeTargetMemberSnSet);
    }
 
    // 데이터 트랜잭션
    {
        // 데이터 삽입.
        for (G model : groupByMemberSnMemberMap.values()) {
            insertData.accept(model);
        }
 
        // 제외대상 삭제.
        deleteByMemberSnList.accept(excludeTargetMemberSnList);
    }
}
 
// 메소드 사용 예
// Model1 에 대한 데이터 처리.
ModelSampleService.GetInstance().createByGenerateModel(Arrays.asList(12), Arrays.asList(3),
                    (Integer memberSn) -> {
                        GenerateModel1 generateModel1 = new GenerateModel1();
                        generateModel1.setMemberSubjectSn(memberSn);
                        return generateModel1;
                    },
                    (List<Integer> memberSnList) -> {
                        List<GenerateModel1> list = Model1Dao.GetInstance().selectList(memberSnList);
                        return list;
                    },
                    (GenerateModel1 generateModel1) -> {
                        Integer memberSn = generateModel1.getMemberSubjectSn();
                        return memberSn;
                    },
                    (GenerateModel1 generateModel1) -> {
                        Model1Dao.GetInstance().create(generateModel1);
                        return;
                    },
                    (List<Integer> targetMemberSnList) -> {
                        Model1Dao.GetInstance().deleteByMemberSnList(targetMemberSnList);
                        return;
                    }
);
cs


의도 했던 바와 같이 어떠한 [인터페이스, 추상메소드] 없이, OCP 를 지킨 코드는 나왔습니다.

추 후, GenerateModel 이 또 등장하였고, 비슷한 로직을 사용한다면 각 함수형 인터페이스를 구현해주면 됩니다.


하지만, 일단 메소드 사용성 면에서 많은 불편함을 느꼈을 것입니다.

특정 프로토콜로 묶인 추상메소드의 구현이 아니기 때문에, 각 람다를 구현할 때마다 무슨 기능을 했었는지 살펴봐야 합니다.

이는 가독성도 떨어진다고 볼 수 있을 것 같네요..

(인터페이스나 추상클래스로 구현 했다면, 보다 동적인 구현에 있어서 무슨 일을 하는지 명확했을 수 있습니다.)  


이 과제의 의도는 새로운 지식을 맹신하지 말라는 일종의 견제를 해주고 싶었고(패턴병과 유사한 함수병), 요구사항과 현재 상황에 따라 적절한 대처가 필요함을 느낄 수 있도록 하는 것이 목적이었습니다.

(요구사항이 기껏 한 두개의 함수형 인터페이스만 사용할 정도라면, 깔끔할 수 있었습니다.)



3. 계속 존재했던 동적 파라미터화


마지막 과제는 명령패턴을 이용해, undo/redo 를 구현해보고자 하였습니다.


이와 관련된 내용은 아래 포스팅을 참고.



과제 자체는 사실 이 패턴에 대한 이해도 중요했지만, 꼭 동적 파라미터화 같은 방법이 JAVA8 에서 등장한 것은 아니었다는 것에 대한 실습이었습니다.


명령패턴은 일종의 요청을 캡슐화하고 컨트롤러가 이를 관리하게 함으로써, 실제 요청자와 실행자 사이의 관계를 느슨하게 하게 하는 것을 목적으로 합니다.


대부분 이 패턴의 예제에서는 명령을 캡슐화하기 위해 인터페이스를 구현하는 구현클래스를 제작하지만, 이 과제에서는 굳이 클래스를 제작하지 않고 동적 파라미터화를 이용하여 즉시 구현하는 것을 목적으로 하였습니다.

(다양한 요청에 대해서, 재활용 안 할 구현클래스를 만드는 것은 일종의 낭비이지 않을까요?)


일단, 이 패턴을 구현하기 위한 interface 는 다음과 같이 제작했습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * 커맨드 인터페이스 정의.
 *
 * Created by Doohyun on 2017. 7. 10..
 */
public interface ICommand {
 
    /**
     * 어떤기능이든 undo 구현.
     */
    void undo();
 
    /**
     * 어떤기능이든 실행 구현.
     */
    void execute();
}
cs


이를 실행하기 위한 실행자(Receiver)와 컨트롤러(Controller) 는 다음과 같습니다.

(대부분의 내용은 앞써 언급한 포스팅에서 복사했습니다. 이 패턴이 궁금하다면 해당 링크를 참고하세용.)


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
/**
 * 명령을 관리하는 컨트롤러
 */
public class RemoteController {
     
    // 일반 명령을 위한 스택
    private Stack<ICommand> commandStack = new Stack<>();
    // UNDO 명령을 위한 스택
    private Stack<ICommand> undoStack = new Stack<>();
 
    // 명령을 추가
    public void setCommand(ICommand commandWithUndoable) {
        commandStack.push(commandWithUndoable);
    }
 
    /**
     * 일반적인 실행. (REDO 포함)
     */
    public void execute() {
        if (!commandStack.isEmpty()) {
            // [일반명령 스택]에서 가장 마지막에 담긴 명령객체를 추출 후 실행.
            ICommand command = commandStack.pop();
            command.execute();
 
            // 해당 명령을 UNDO 스택에 삽입.
            undoStack.push(command);
        }
    }
 
    /**
     * 작업 취소 (Undo)
     */
    public void undo() {
        if (!undoStack.isEmpty()) {
            // [UNDO 명령 스택]에서 가장 마지막에 담긴 명령객체를 추출 후 실행.
            ICommand command = undoStack.pop();
            command.undo();
 
            // 일반 실행 스택에 데이터 삽입.
            commandStack.push(command);
        }
    }
}
 
/**
 * 글씨를 입력하는 데모 클래스.
 *
 * Created by Doohyun on 2017. 7. 10..
 */
public class TextWatcherDemo {
 
    private StringBuilder builder = new StringBuilder("");
 
    /**
     * 텍스트 입력.
     *
     * <pre>
     *     텍스트를 입력하고, 현재 상태를 표시한다.
     * </pre>
     *
     * @param text
     */
    public void addText(String text) {
        builder.append(text);
        System.out.println(builder.toString());
    }
 
    /**
     * 텍스트 삭제.
     *
     * <pre>
     *     텍스트를 삭제하고, 현재 상태를 표시한다.
     * </pre>
     */
    public void deleteText() {
        builder.deleteCharAt(builder.length() - 1);
        System.out.println(builder.toString());
    }
}
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
37
38
39
40
41
42
43
44
45
46
47
RemoteController remoteController = new RemoteController();
TextWatcherDemo textWatcherDemo = new TextWatcherDemo();
 
        
 
// 텍스트를 입력받아, 컨트롤러에 명령을 세팅하고 실행하는 소비자 정의.
Consumer<String> InputText = (text) -> {
        // 명령 세팅.
        // 동적으로 행위를 정의. 즉 동적파라미터화
        remoteController.setCommand(
            new ICommand(){
        
                @Override                                                    
                public void undo(){
                    textWatcherDemo.deleteText();
                }
 
                @Override
                public void execute(){
                    textWatcherDemo.addText(text);
                }
            }
        );
 
        // 실행
        remoteController.execute();
};
 
// 메소드 레퍼런스
// undo 실행을 위한 함수 정의.
Runnable undo = remoteController::undo;
 
// 람다 버전의 REDO 함수.
// redo 실행을 위한 함수 정의.
Runnable redo = () -> remoteController.execute();
        
InputText.accept("안");
InputText.accept("녕");
 
undo.run();
redo.run();
 
// CONSOLE LOG
// 안
// 안녕
// 안
// 안녕
cs


이런 방식은 사실 이벤트 처리 기반 시스템을 만들다보면, 꽤 많이 해봤을 것입니다.

결국 [JAVA8 in Action] 에서 메소드를 파라미터로 넘긴다는 이야기는 이전부터 있던 개념이며, 람다나 메소드 레퍼런스는 이를 보다 쉽게 사용할 수 있는 개념이라 볼 수 있을 것 같네요.

(추 후 공부하게 될, Stream API 사용을 위해서는 이를 적극적으로 사용해야 합니다.)



이로써, 지난 주에 실습한 내용에 대한 리뷰가 끝났습니다.

첫 시간이지만, 꽤 많은 내용을 다뤘던 듯 합니다. 그에 따라 과제도 좀 많이 있었죠. ^^;


이 과제를 언급할 때, 최근에 시청한 한 프로그램의 출연자가 했던 대화 중 하나를 같이 말했습니다.


 Knowing is nothing, Doing is the best. 

(아는 것은 중요하지 않다. 하는 것이 가장 좋다.)


단순히 듣기만 하는 것이 아니라, 한번 해보는 것은 정말 중요한 듯 합니다.

(이런 행동들은 조금씩 우아한 방법을 찾아 보는 것에 도움이 되겠죠?.. 아마도...)


어쨌든 개인시간을 투자하며, 계속 지금과 같은 시간을 같이 보내준 여러분들께 감사합니다.

(맥주를 한 병 더 먹었다면 아마 더 감성글이 됐을지도...)



반응형
Posted by N'

8. 함수형 프로그래밍(오리엔테이션).pdf




두번째 주제인 FP 에 대한 오리엔테이션 자료입니다.


감사합니다. :-)

반응형
Posted by N'

지난 포스팅에서는 현재 만들어지는 어플리케이션이 대부분 네트워크 작업을 수행하며, 그에 따라 비동기 처리가 중요함을 생각할 수 있었습니다. 


JAVA 에서는 이를 위해 Future 인터페이스를 제공했으며, JAVA8 에서는 조금 더 쓰기 쉬운 CompleteableFuture 의 사용법을 알아보았습니다.



CompleteableFuture 에 대해서 조금 더 봐야할 부분은 Future 에 비해 쓰기 쉬워진 점과 더불어 비동기 처리를 조합할 수 있다는 것입니다.


예를들어, 비동기 처리를 하는 중 동기 처리를 수행하고 비동기 처리를 계속해서 진행해야할 수도 있고 각자 시작한 비동기 처리의 결과의 싱크를 맞춰 처리해야할 수도 있습니다. CompleteabeFuture 에는 이를 위한 파이프라인 메소드를 지원합니다.


오늘 포스팅은 시나리오에 따른 구현을 살펴보려 합니다.


1. 두 비동기작업(A, B) 간의 순서가 존재하며, A의 결과로 B 를 수행.


비동기 처리를 해야하는 두 가지 작업 A, B 가 있다고 가정합시다. 


그런데 상황이 조금 복잡합니다.


비동기 처리 A 를 수행하는 것은 문제가 아니지만, B 의 결과를 도출하기 위해서는 A 의 결과가 필요합니다. 즉 A 가 끝난 다음, B 가 실행되어야함을 의미합니다.


CompleteabeFuture 에서는 아래와 같은 파이프라인 작업으로 이를 쉽게 해결할 수 있습니다.


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
 Supplier<String> A = () -> {
    try {
        System.out.println("A 스레드 작업 시작");
        Thread.sleep(2000);
        System.out.println("A 스레드 작업 완료");
        return "A 실행";
    } catch (InterruptedException e){
        e.printStackTrace();
        return "실패";
    }
};
 
Function<StringString> B = (aResult) -> {
    try {
        System.out.println("B 스레드 작업 시작");
        Thread.sleep(1000);
        System.out.println("B 스레드 작업 완료");
        return aResult + " B 실행";
    } catch (InterruptedException e){
        e.printStackTrace();
        return "실패";
    }
};
 
 Future<String> result = CompletableFuture.
                supplyAsync(A).
                thenApply(aResult -> aResult + " A 성공 -> ").
                thenCompose(aSucceedResult -> CompletableFuture.supplyAsync(() -> B.apply(aSucceedResult)));
 
System.out.println(result.get());
 
// 결과
// A 스레드 작업 시작
// A 스레드 작업 완료
// B 스레드 작업 시작
// B 스레드 작업 완료
// A 실행 A 성공 -> C 실행
cs


새로운 문법이 등장했습니다.


thenApply 


A의 결과를 받아, 다른 결과를 내보는 Function 과 같은 역할을 수행합니다. 

기존 파이프라인 메소드인 map 과 동일합니다.


생각해봐야할건 앞서, A 의 결과가 끝날 때까지 thenApply 에서 블록이 걸리지 않는다는 것입니다. 즉 A 의 비동기 결과가 모두 끝나야 실행이 됩니다.


thenCompose


A로부터의 최종 결과가 끝나는 즉시, B를 실행합니다. 


함수 디스크립터를 보면, 

A 의 결과인 aSucceedResult 를 받음을 알 수 있으며 B 실행 시 이를 사용합니다.


2. 두 비동기작업(A, B) 간 순서는 없지만, A, B 결과를 합쳐야 하는 경우


이번엔 다른 케이스를 생각해보겠습니다. 


시간이 오래 걸리는 A, C 의 순서 관계는 없으며, 이 둘의 결과가 합쳐지기만 기다리면 됩니다.

비동기 처리는 동시에 보내지만, 최종 결과는 A,C 가 모두 끝날 때까지 기다려야합니다.


이 기능을 위해 우리는 CompleteabeFuture 의 파이프라인 메소드인 thenCombine 을 사용할 수 있습니다. 아래 예제에서 사용법을 확인할 수 있습니다.


가정 : C 의 작업을 A 보다 빨리도록 조정하였습니다.


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
 Supplier<String> A = () -> {
    try {
        System.out.println("A 스레드 작업 시작");
        Thread.sleep(2000);
        System.out.println("A 스레드 작업 완료");
        return "A 실행";
    } catch (InterruptedException e){
        e.printStackTrace();
        return "실패";
    }
};
 
Supplier<String> C = () -> {
    try {
        System.out.println("C 스레드 작업 시작");
        Thread.sleep(500);
        System.out.println("C 스레드 작업 완료");
        return "C 실행";
    } catch (InterruptedException e){
        e.printStackTrace();
        return "실패";
    }
};
 
Future<String> result2 = CompletableFuture.
                supplyAsync(A).
                thenApply(aResult -> aResult + " A 성공 -> ").
                thenCombine(CompletableFuture.supplyAsync(C), (a, c) -> a + c);
 
System.out.println(result.get());
 
// 결과
// A 스레드 작업 시작
// C 스레드 작업 시작
// C 스레드 작업 완료
// A 스레드 작업 완료
// A 실행 A 성공 -> C 실행
cs


역시 새로운 문법인 thenCombine 입니다.


thenCombine


파라미터로 또 다른 CompleteableFuture 를 받으며, 동시에 각 비동기 작업의 결과로 다른 결과를 도출하는 BiFunction 을 받음을 알 수 있습니다.


A,C 의 실행은 동시에 되지만, 결과는 A,C 가 모두 끝난다음에 도출되는 것을 볼 수 있습니다.

즉 비동기 프로그래밍에서 싱크 맞추기 문제가 이렇게 쉽게 처리가 됨을 알 수 있습니다. 


3. 동작을 미리 등록하고, 실행계획 세우기 


비동기로 실행하는 여러 작업이 있다는 가정하에 우리는 join 메소드를 통해 모든 작업이 완료가 되고 결과를 받아볼 수 있었습니다.


하지만 작업이 너무 많아, 실행이 너무 오래 걸리고 또한 어떤 작업은 타임아웃이 되버릴 수 있습니다. 결국 작업이 많은 게 문제네요. ㅡㅡ^


하지만 이런 것을 생각해볼 수 있습니다. 


future 에 결과로 미리 할 작업을 등록하고, 


비동기 작업이 모두 완료 해야하는지


어느 한개만 완료 해도 되는지,


등을 생각해 볼 수 있습니다. 


물론 타임아웃 시간도 등록하여, 해당 시간내에 작업이 끝났는지 혹은 타임아웃이 되었는지 알려 줄 수도 있겠죠?


일단 미리 작업을 등록하는 것 부터 살펴보죠.


future 에 대한 결과를 받아, 소비하는 Consumer 를 등록하는 CompleteableFuture 의 thenAccept 메소드를 주목합시다.


thenAccept


파라미터로 future 의 결과를 받아, 할 일을 지정하는 Consumer 를 받습니다.

즉 Future 의 결과를 받을 수 있을 때 일을 정의하는 것이 아닌, 미리 일을 정의하고 실행 계획에 따라 Consumer 를 실행합니다.


thenAccept 를 사용하여 일을 미리 저장하고, 실행 계획을 지정하는 방법에 대한 예제는 아래와 같습니다.


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
// 해야할 일에 대한 Supplier 목록 정의
List<Supplier<Integer>> supplierList = IntStream.range(050).
        mapToObj(n -> {
            // 각 Supplier 는 랜덤한 delay 작업 후, 해당 delay 시간을 출력하는 역할.
            Supplier<Integer> supplier = () -> {
                int time = new Random().nextInt(2000+ 1000;
 
                try {
                    Thread.sleep(time);
                    return time;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    return -1;
                }
            };
 
            return supplier;
        }).
        collect(Collectors.toList());
 
CompletableFuture[] completableFutures = supplierList.stream().
                map(CompletableFuture::supplyAsync).
                map(f -> f.thenAccept(System.out::println)).        // thenAccept 를 이용하여, 할 일 정의.
                toArray(size -> new CompletableFuture[size]);       // 이 후, Future 의 제네릭은 Void,
                                                                    // 배열로 출력하자.
 
// allOf 사용. 모든 Supplier 는 전부 실행. join 메소드를 통해 모든 실행이 끝나길 기다립니다.
CompletableFuture.allOf(completableFutures).join();
 
// anyOf 사용. 한 Supplier 만 실행되도 작업 마무리, get 메소드를 사용하여 timeout 지정.
CompletableFuture.anyOf(completableFutures).get(5000, TimeUnit.MILLISECONDS);
cs


Stream API 처럼 any와 all 과 같은 형식으로, anyOf, allOf 메소드를 지원합니다. 각 실행 전략에 따라 각 Future 의 결과를 적어도 한 가지만 실행할 지, 모두 실행해야하는 지를 지정합니다.


오늘 포스팅에서는 여러 비동기 처리에 따른 싱크를 맞추는 방법, 실행전략 등을 파이프라인식으로 간단하게 처리할 수 있음을 알 수 있었습니다. 


비동기처리를 하는 방식 역시 선언형으로 간단하게 제어할 수 있다는 것은 매우 흥미로운 점이며, 어플리케이션 만드는 방법은 더욱 간편해지고 우리는 비지니스 로직에 집중하기가 매우 좋아질 것이라 생각합니다.


이처럼 새로운 기술을 익힌다는 것은 매우 즐거운 일이며, 우리의 어플리케이션의 질은 더 좋아질 것입니다. :-)


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


반응형
Posted by N'

요즘 제작되는 어플리케이션은 대부분 네트워크 작업을 필요로 하는 경우가 늘어나고 있습니다.


단순 정보만 요청하고 받던 클라이언트 기반 프로그램부터 시작하여, 인터넷이 필요가 없을 것 같은 메모나 사진 촬영 앱도 공유 혹은 클라우드 처리를 지원하기 때문에 네트워크 작업이 없는 경우는 거의 없다고 생각할 수 있을 것 같습니다.


이러한 네트워크와 연결을 시도하는 프로그램을 제작해본 사람들이라면 알고 있겠지만, 네트워크 처리나 File IO 등 오래걸리는 일은 비동기 처리를 해야 합니다. 


비동기 처리는 하는 이유는 IO 작업이 일어나는 동안 메인스레드(아마도 UI 스레드)가 그동안 놀고 있는 상태(블럭 상태 - CPU 사이클이 낭비됨.)를 피하기 위해서입니다. 사용자 측면에서 어떤 정보를 읽어오는 동안 UI 가 멈춘다면, 불편하다고 충분히 느낄 수 있을 것입니다. ㅡㅡ^


아래는 비동기 처리 작업에 대한 그림입니다. 


비동기 프로세스(Async) 의 경우 Process B 가 실행되는 동안 Process A 는 계속해서 작업을 할 수 있습니다.



Java7 부터는 Future 인터페이스를 통해서, 이러한 비동기 프로세스를 수행할 수 있었습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ExecutorService executorService = Executors.newCachedThreadPool();
 
Future<Double> future = executorService.submit(()-> {
    // 편의 상 람다 사용. 자바7 에서는 사용 불가.
    Thread.sleep(2000);
 
    return 1000.0;
});
 
System.out.println("비동기 처리를 하는 동안 다른 일처리.");
 
try {
    // 타임아웃 3초로 지정.
    System.out.println("결과 : " + future.get(3000, TimeUnit.MILLISECONDS));
catch (Exception e) {
    e.printStackTrace();
}
 
// 출력 결과
// 비동기 처리를 하는 동안 다른 일처리.
// 결과 : 1000.0
cs


Future 클래스는 비동기 계산이 끝났는지 확인할 수 있는 isDone, 타임아웃 기간을 결정하고 결과를 출력하는 get 메소드 등이 있습니다.


간단히 비동기 처리는 되지만 조금 아쉽습니다. 


실무에서는 비동기 처리가 꼭 하나씩 생긴다고 볼 수 없으며, 각 비동기 처리에 대한 결과를 동기를 맞춰 또 다른 결과를 내야할 수 도 있습니다. 즉 각 Future 클래스 간 여러 의존성에 대한 관리가 힘들 수 있습니다.


JAVA8 에서는 복잡한 비동기처리를 선언형으로 이용할 수 있는 CompleteableFuture 를 제공하며, Stream API 나 Optional 같이 람다표현식과 파이프라인을 사용하여 비동기 작업을 조합할 수 있습니다.


일단 CompleteableFuture 의 간단한 예제는 아래와 같습니다. CompleteableFuture 는 기본적으로 supplyAsync, runAsync 등 팩토리 메소드를 제공하며, 쉽게 비동기 작업을 수행할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Future<Double> future = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(2000);
            } catch (Exception e){
                e.printStackTrace();
            }
 
            return 1000.0;
        });
 
System.out.println("비동기 처리를 하는 동안 다른 일처리.");
 
try {
    // 타임아웃 3초로 지정.
    System.out.println("결과 : " + future.get(3000, TimeUnit.MILLISECONDS));
catch (Exception e) {
    e.printStackTrace();
}
cs


비동기로 처리되어야 할 일이 많아지면 어떨까요? 동시에 앞의 예제처럼 Sleep 을 해야하는 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
Supplier<Double> supplier = () -> {
    try {
        Thread.sleep(2000);
    } catch (Exception e){
        e.printStackTrace();
    }
 
    return 1000.0;
};
 
List<Supplier<Double>> supplierList = Arrays.asList(supplier, supplier, supplier, supplier);
 
// 병렬 스트림을 이용. 각 태스크를 병렬로 하여 성능을 높이자.
supplierList.parallelStream().
    map(Supplier::get).
    reduce(Double::sum).
    ifPresent(System.out::println);
 
// CompletableFuture 를 이용한 비동기적으로 처리
{
    List<CompletableFuture<Double>> completableFutures = supplierList.stream().
            map(CompletableFuture::supplyAsync).
            collect(Collectors.toList());
 
    // join 메소드는 모든 비동기 동작이 끝나길 기다립니다.
    completableFutures.stream().
            map(CompletableFuture::join).
            reduce(Double::sum).
            ifPresent(System.out::println);
}
cs


두 구현 방식에 따라 결과는 큰 차이가 나지 않을 수 있습니다. 


그러나 일반 순차 Stream 을 병렬 Stream 으로 변경한 첫 번째 방법이 간단해 보입니다.


굳이 CompletableFuture 를 쓸 필요가 없어보이지만, 병렬스트림과 달리 이를 이용한 방법은 executor 를 커스터마이징 할 수 있습니다.


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
List<Supplier<Double>> supplierList = IntStream.range(0100).mapToObj(n -> supplier).collect(Collectors.toList());
 
supplierList.parallelStream().
        map(Supplier::get).
        reduce(Double::sum).
        ifPresent(System.out::println);
 
// CompletableFuture 를 이용한 비동기적으로 처리
{
 
    final Executor executor = Executors.newFixedThreadPool(Math.min(supplierList.size(), 100), r -> {
                Thread t = new Thread(r);
                // 데몬 스레드 정의
                // 일반 스레드가 실행 중일 때 자바 프로그램은 종료되지 않음 -> 어떤 이벤트를 한없이 기다리면서 종료되지 않은 일반 자바 스레드가 있으면 문제
                // 데몬 스레드는 자바 프로그램이 종료될 때 종료
                t.setDaemon(true);
                return t;
            });
 
    List<CompletableFuture<Double>> completableFutures = supplierList.stream().
            map(CompletableFuture::supplyAsync).
            collect(Collectors.toList());
 
    // join 메소드는 모든 비동기 동작이 끝나길 기다립니다.
    completableFutures.stream().
            map(CompletableFuture::join).
            reduce(Double::sum).
            ifPresent(System.out::println);
}
 
// 병렬스트림 걸린 시간 : 26066초
// CompletableFuture 사용 걸린 시간 : 2015초
cs


놀라운 결과입니다. 걸린 시간 자체가 무려 10배가 넘게 차이가 남을 알 수 있습니다.

즉 로직에 따라 Executor 를 다르게 하여, 최적화 시키는 것이 효과적일 수 있음을 알 수 있습니다.


비동기 처리의 최적화와 더불어, CompleteableFuture 는 람다표현식이나 파이프라인 메소드를 이용하여, 비동기 연산을 조합할 수 있습니다. 다음 포스팅에서는 이에 대해 다루어 보도록 하겠습니다. 



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


반응형
Posted by N'

JAVA8 부터 지원하는 기능 중 특이한 녀석 중 하나는 Optional 입니다. 


문자 그대로 선택을 내포하고 있는 개념적 모델은 비지니스 로직을 구현함에 있어서, Stream 과는 또 다른 의미로 변혁을 불러올 수 있습니다.


이 개념을 사용하면, 그동안 당했던 NullPointerException 에서 어느 정도 해결할 수 있으며 분기처리 (if 등) 을 간략하게 생략할 수 있습니다. 특히 Null 처리에 대하서 고민을 하지 않아도 된다는 것은 꽤 큰 의미를 줄 수 있습니다.


예를 들어 아래 코드를 한번 같이 볼까요?


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 class OptionalTestVo {
    
    private A a;
    
    public A getA() {
        return a;
    }
 
    public void setA(A a) {
        this.a = a;
    }
 
    public static class A {
        private B b;
 
        public B getB() {
            return b;
        }
 
        public void setB(B b) {
            this.b = b;
        }
    }
    
    public static class B {
    }
}
 
cs


임의로 만들어진 위의 VO 는 멤버 변수로 객체 A 를 가지고 있습니다. 

객체 A 는 또 객체 B 를 가지고 있군요. 이 VO 를 한번 사용한다고 가정해보겠습니다.


1
2
3
4
5
6
7
8
9
10
OptionalTestVo ov = new OptionalTestVo();
OptionalTestVo.A a = new OptionalTestVo.A();
        
if (ov != null) {
    if (ov.getA() != null) {
        if (ov.getA().getB() != null) {
            System.out.println(ov);
        }
    }
}
cs


객체 내부의 멤버 변수들 역시 객체이며, 값이 있을 수도 있고 없을 수도 있기 때문에 위와 같이 방어코드를 작성해줘야 합니다. 물론 꼼꼼하게 처리할 수 있을 수도 있지만, 비지니스 로직을 작성하는 환경이 언제나 베스트하여 모든 상태를 체크한다는 것은 꿈과 같은 일일 수 있습니다. ㅡㅡ^


값이 있을 수도 있고 없을 수도 있는 이 상태를 Optional 을 사용함으로써 명시할 수 있습니다.


Optional 의 튜토리얼은 다음과 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
String stringValue = "1";
        
// Optional 생성.
Optional<String> stringOptional = Optional.of(stringValue);
Optional<String> stringNullAbleOptional = Optional.ofNullable(stringValue);
Optional<String> emptyOptional = Optional.empty();
        
// Optional 에서 데이터 꺼내기.
// 값이 존재하지 않는 경우도 있기 때문에 위험!!
String originalData = stringOptional.get();
        
// 값이 존재하지 않을 경우 default 값 출력 요청!
String originlDefaultData = stringOptional.orElse("Default");
        
// 값이 존재하지 않을 경우 supplier 
// 일종의 조건부 실행 
String originlDefaultSupplierData = stringOptional.orElseGet(() -> "Default");
        
// 값이 존재할 경우 데이터 출력!!
stringOptional.ifPresent(System.out::println);
        
// 값의 존재 유무 확인.
stringOptional.isPresent();    
cs


Optional 로 실제 데이터 객체를 감싸고, 이를 실제 데이터 객체가 필요할 때 default 값을 고려하여 반환하거나 값이 있을 때만 실행하는 등 값이 있을 때만을 고려하여 데이터를 출력할 수 있어보입니다.


Optional 을 이용하여, 실제 데이터 VO 를 아래와 같이 정의하면 Null 상태의 걱정을 하지 않아도 됩니다.


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
public class OptionalTestVo {
    
    // 멤버 변수 자체를 Optional 로 가지는 방법!
    private Optional<A> a = Optional.empty();
    
    // 이 때 필수라고 생각되는 데이터는 Optional 을 취하지 않는 것도 방법!
    private String requiredData;
    
    public String getRequiredData() {
        return requiredData;
    }
 
    public void setRequiredData(String requiredData) {
        this.requiredData = requiredData;
    }
 
    public Optional<A> getA() {
        return a;
    }
 
    public void setA(A a) {
        this.a = Optional.ofNullable(a);
    }
 
    public static class A {
        private B b;
 
        // 멤버변수가 Optional 은 아니지만, 출력하는 getter 를 Optional 로 랩핑
        public Optional<B> getB() {
            return Optional.ofNullable(b);
        }
 
        public void setB(B b) {
            this.b = b;
        }
    }
    
    public static class B {
        @Override
        public String toString() {
            return "테스트";
        }
    }
}
cs


위와 같이 제작된 VO 에 의해, 앞서 서술한 null 검사를 수행하던 첫 번째 로직은 다음과 같이 변경될 수 있습니다.


1
2
3
4
5
6
OptionalTestVo ov = new OptionalTestVo();
OptionalTestVo.A a = new OptionalTestVo.A();
ov.setA(a);
a.setB(new OptionalTestVo.B());
        
ov.getA().flatMap(OptionalTestVo.A::getB).ifPresent(System.out::println);
cs


객체 ov 내부에 있던, Optional<A> 는 flatMap 을 통해 A 내부 Optional<B> 로 1차원 평준화할 수 있으며, 값이 존재할 때 콘솔로그를 출력하도록 하였습니다. 

flatMap 을 통한 평준화 과정은 두 Optional [ex. Optional<A>, Optional<B>] 를 합치는 과정으로 두 Optional 중 한 가지라도 빈 값이라면, 빈 Optional 상태를 출력하게 됩니다.



Optional 는 Null 체크 외에도 Stream API 의 중간연산과 같이 map, filter 등을 지원합니다. 

비지니스 로직에서 빠질 수 없는 분기처리에 대한 연산을 Optional 을 통해 선언형으로 작성할 수 있음을 의미합니다.


다음은 Optional 의 기능을 응용하여 수행한 분기처리입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
Integer weight = 70;
        
String weightToString;
        
if (weight > 50) {
    weightToString = "보통 체중";
else {
    weightToString = "마른 체중";
}
        
Optional.ofNullable(weight).
        filter(w -> (w > 50)).
        map(w -> "보통 체중").
        orElse("마른 체중");
cs


이와 같이 Optional 을 이용하면 잠재적인 Null 에 의한 오류 혹은 예외에 대해 대비하여 로직을 구성할 수 있고 비지니스 로직 역시 간략화시킬 수 있음을 볼 수 있었습니다.


이전 버전 JAVA 에 익숙하다면, Null 이 없다는 것을 상상도 할 수 없을 것입니다. 

JAVA 기본 라이브러리 역시 Optional 을 호환성 혹은 막대한 코드량에 의해 제대로 활용하지 못하고 있다고 JAVA8 in Action 에서도 언급을 하고 있습니다.


하지만 옛것의 익숙함을 조금 덜어내고, 새로운 패러다임에 익숙해진다면 여러분의 코드는 보다 아름다워질 수 있을 것입니다. :-)


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






반응형
Posted by N'

카테고리가 기존 함수형 방법론 으로 분류해서


포스팅을 하고 있지만 계속 공부하는 책인 JAVA8 in Action 을 보며, 포스팅을 하고 있습니다.


분류의 목적을 따지자면, JAVA8 에서 가장 중요하다고 생각할 만한 튜토리얼은 Lambda 와 Stream API 라고 할 수 있겠는데요. 

이와 관련된 기초 튜토리얼은 모두 끝났기 때문에 조금 더 More 하게 공부를 할 부분이기 때문에 나누었습니다.


More 한 카테고리에서의 첫 포스팅에서는 함수형 프로그래밍으로 리팩토링하기를 소개하려 합니다.

소개하는 내용은 JAVA8 in Action  리팩토링 관련 목록을 따라가지만 중요하다 싶은 부분만 소개하려 합니다.


1. Stream 을 통한 컬렉션 처리 리팩토링.


JAVA8 in Action 에 따르면,  모든 Collection 처리는 Stream API 로 바꿔야 한다고 하고 있습니다. 


Stream API 는 데이터 처리 파이프라인의 의도를 더 명확히 하여 가독성 향상에 도움을 줍니다. 

또한 쇼트서킷 처리와 Lazy 처리로 최적화 되어 있으며, 쉽게 병렬로 처리하도록 변경할 수 있습니다.


그러나 모든 Collection 처리를 Stream API 로 변경하는 것은 쉽지 않으며, 특히 break, continue, return 등 제어 흐름문에 대한 연산 역시 쉽지는 않아보입니다. 


하지만 우회하는 법은 언제나 존재합니다. 다음과 같이 말이죠.


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
// break 리팩토링.
{
    for (Integer a : numberList) {
        if (a % 10 == 0 && a > 55) {
            System.out.println(a);
            break;
        }
    }
            
    // findFirst 활용 
    numberList.stream().filter(a -> a % 10 == 0 && a > 55).findFirst().ifPresent(System.out::println);
}
 
// continue 리팩토링.
{
    int size = 0;
    for (Integer a : numberList) {
        if (a % 10 == 0 && a > 55) {
            continue;
        }
                
        ++size;
    }
            
    // 해당 조건이 아닌 것에 대한 Predicate 정의. 
    numberList.stream().filter(a -> !(a % 10 == 0 && a > 55)).collect(Collectors.counting());
}
 
// return 
 
/**
 * 리턴 for-loop
 * 
 * @param numberList
 * @return
 */
public Integer returnExampleForLoop(List<Integer> numberList) {
    for (Integer a : numberList) {
        if (a % 10 == 0 && a > 55) {
            return a;
        }
    }
    
    return null;
}
 
/**
 * 리턴 Stream API
 * 
 * @param numberList
 * @return
 */
public Optional<Integer> returnExampleStream(List<Integer> numberList) {
    return numberList.stream().filter(a -> !(a % 10 == 0 && a > 55)).findFirst();
}
cs



2. 조건부 연기


예를 들어 아래와 같은 코드가 있다고 합시다.


1
2
3
4
5
6
7
Data data = new Data();
data.setNumber(12);
        
if (object == null) {
    String message  = RollBaMessage();
    throw new MesseageException(message);
}
cs


객체의 null 체크를 하는 유효성 검사 로직입니다. 

객체가 Null 일 때, 지정된 Roll back message 를 예외 메시지로 입력합니다.


하지만, Null 을 체크하는 유효성 검사 모듈은 범용적으로 쓰이고 싶어 메소드를 만들어 재활용을  하고 싶었습니다. 

그래서 만들었죠.


1
2
3
4
5
6
7
public static void NullCheck(Object object, String message) throws MesseageException{
    if (object == null) {
        throw new MesseageException(message);
    }
}
 
NullCheck(data, RollBaMessage());
cs


각 비지니스 상황에 따라 여러 에러 메시지를 유연하게 보낼 수 있게 되었습니다. 


호스트코드에서는 단지 NullCheck 메소드만 불러주면될 것 같아 보입니다. 


그런데, 특정 메시지를 보내기 위한 새로운 메소드 RollBackDelayedMessage 를 제작했습니다.

미리 만들어둔 NullCheck 유효성 검사 모듈이 있으니 다음과 같이 사용을 할 생각입니다.


1
2
3
4
5
6
7
public static void NullCheck(Object object, String message) throws MesseageException{
    if (object == null) {
        throw new MesseageException(message);
    }
}
 
NullCheck(data, RollBackDelayedMessage());
cs


그런데 문제가 생겼습니다. RollBackDelayedMessage 는 실행시간이 10초나 걸립니다. 


게다가 이 로직은 에러 메시지를 출력하기 전에 비지니스 로직을 수행을 하는데, 그 로직은 null 일때만 해야하죠.. 

if문을 사용하여 따로 제작을 해서 중복코드를 만들어야 하는건가요?


아닙니다! 메소드 파라미터화를 시켜서 넘기면되죠. 아래와 같이 말이죠.


1
2
3
4
5
6
7
8
// 파라미터에 String 대신, String 을 출력하는 공급자를 언급
public static void NullCheck(Object object, Supplier<String> messageSupplier) throws MesseageException{
    if (object == null) {
        throw new MesseageException(messageSupplier.get());
    }
}
 
NullCheck(data, () -> RollBackDelayedMessage());
cs


이제 Null Check 유효성 검사 모듈에서 실패할 때만 RollBackDelayedMessage 을 실행할 수 있습니다. 즉 위와 같이 특정 상황에서만 실행하도록 하는 조정하는 것을 조건부 연기라고 합니다. 


3. 의무 체인


Lambda 에 대하여 포스팅할 때, 두 Lambda 식을 조합할 때 사용하는 andThen, compose 메소드를 본 적이 있습니다.



해당 메소드를 이용하면 두 함수를 엮어 새로운 기능을 만들어낼 수 있었습니다. 


정확히 말하면, 한 객체가 어떤 작업을 처리한 다음에 다른 객체로 결과를 전달하고, 다른 객체도 작업을 처리한 다음 또 다른 객체로 전달하는 등 기능들을 엮는 것이며, 이를 의무 체인이라고 합니다.


예를 들어 다음과 같이 함수형 인터페이스를 제작하고, 의무체인을 할 수 있는 여지를 남길 수 있습니다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@FunctionalInterface
interface IProcesser<T> {
    T accept(T t);
    
    default IProcesser<T> chain(IProcesser<T> t1) {
        return t -> {
            T result = accept(t);
            
            return t1.accept(result);
        };
    }
}
 
IProcesser<Integer> t1 = n -> n + 5;
IProcesser<Integer> t2 = n -> n - 2;
IProcesser<Integer> t3 = t1.chain(t2);
        
System.out.println(t3.accept(10));
 
// result : 13
cs


인터페이스에는 JAVA8 부터 사용가능한 디폴트메소드를 사용하여, 각 함수들을 체인으로 걸도록 하였습니다.



위와 같이 Lambda 를 사용하여 리팩토링할 수 있는 여지들을 보았습니다. 

리팩토링을 통하여, 우리의 코드는 보다 더 가독성이 개선되며, 유지보수에 유연해질 수 있을 것입니다. 


리팩토링 하고 싶어지지 않나요?  :-)



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


반응형
Posted by N'

계속해서 JAVA8 에서의 병렬 처리 방법에 대해 포스팅을 하고 있습니다.


JAVA8 에서 Stream API 를 사용하면, 병렬처리를 구현할 때 고려해야할 문제들을 생각하지 않아도 됐지만, 어떤 시점에 어떤식으로 주의하며 적용을 해야한다는 내용을 포스팅 했었습니다.



오늘은 병렬처리관련 마지막 포스팅으로, JAVA8 에서 새로 추가된 인터페이스인 Spliterator 를 소개하려 합니다. Spliterator 는 '분할할 수 있는 반복' 로, 기존 존재했던 반복자인 Iterator 와 비슷하지만 병렬 작업에 특화된 인터페이스입니다.


구현해야하는 인터페이스의 추상메소드 시그니처는 다음과 같습니다.


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
public interface Spliterator<T> {
    
    /**
     * <pre>
     *     해당 반복자를 순차적으로 소비(Consumer) 하면서, 
     *     탐색해야할 요소가 남아있는지에 대한 여부를 출력합니다.
     * </pre>
     * 
     * @param action
     * @return
     */
    boolean tryAdvance(Consumer<super T> action);
    
    /**
     * <pre>
     * 본인의 Spliterator 의 요소 값을 일부 분할하여, 
     * 또 다른 Spliterator 를 생성합니다. 
     * 
     * (작업의 분할과정이라 생각하면, 쉬울 것 같습니다.)
     * </pre>
     * 
     * @return
     */
    Spliterator<T> trySplit();
    
    /**
     * <pre>
     *     작업해야할 요소 정보 수를 추측한 크기를 반환합니다.
     *    해당 크기는 꼭 일치해야 하는 것은 아닙니다. 
     *
     *    (trySplit 메소드를 이용하여, 작업분할 시 참고 자료가 됩니다.)
     * </pre>
     * 
     * @return
     */
    long estimateSize();
    
    /**
     * 현재 Spliterator 의 특성정보를 표시합니다.
     * 
     * <pre>
     *     ORDER : 요소분할 시, 순서가 존재하기 때문에 유의해서 분할하도록 합니다.
     *     DISTINCT : 요쇼 간 같은 값(equals)은 없다고 보장합니다.
     *     SORTED : 탐색된 요소는 미리 정의된 정렬순서를 따릅니다. 
     *     SIZED : estimateSize 은 정확한 정보를 줄 것을 보장합니다.
     *     NONNULL : 탐색하는 모든 요소는 null 이 아님을 보장합니다.
     *     IMMUTABLE : 요소가 탐색되는 동안 데이터를 추가하거나, 삭제할 수 없습니다.
     *     CONCURRENT : 동기화 없이, 여러 스레드가 소스를 동시에 수정할 수 있습니다.
     *     SUBSIZED : 이 Spliterator 와 분할 된 모든 Spliterator 는 모두 SIZE 임을 보장합니다.
     * </pre>
     * 
     * @return
     */
    int characteristics();
}
 
cs


즉 Spliterator 의 메소드 시그니처들은 요소들의 분할을 위해 존재하고 있습니다.


해당 분할 과정은 다음과 같이 이루어 집니다.


출처 : [JAVA8 in Action]


Spliterator 들은 trySplit 메소드를 사용하여, 분할할 수  있는 단계까지 분할합니다. 

즉 내부적으로 분할할 수 있다면, 분할된 부분에 대한 정보를 가진 Spliterator 을 생성한다는 것이죠. 더이상 분할할 수 없다면 null 을 출력합니다. 


이런식으로 최종적으로 도출된 [병렬 특화 반복자]로 병렬처리를 수행합니다. 


한번 예제를 작성해보겠습니다.


1
2
3
4
5
6
7
8
List<Integer> dataSet = IntStream.range(01000).boxed().collect(Collectors.toList());
        
dataSet.parallelStream().
        reduce(Integer::sum).
        ifPresent(System.out::println);
 
// 출력
// 499500
cs


간단한 덧셈에 대한 선언입니다.  


현재는 parallelStream 을 이용해서, Stream 형태로 추출하였지만 우리는 병렬에 특화된 Spliterator 를 만들겁니다. ㅡㅡ^


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
/**
 * Integer 의 덧셈을 위한 Spliterator 
 * 
 * @author Doohyun
 *
 */
public class IntegerSumSpliterator implements Spliterator<Integer>{
    // 자르지 말아야할 최소 사이즈!
    private static final int LIMITED_SPLIT_SIZE = 500;
    
    private List<Integer> integerList;
    private Integer currentIndex = 0;
    
    public IntegerSumSpliterator(List<Integer> integerList){
        this.integerList = integerList;
    }
 
    @Override
    public boolean tryAdvance(Consumer<super Integer> action) {
        action.accept(integerList.get(currentIndex++));
        return currentIndex < integerList.size();
    }
 
    @Override
    public Spliterator<Integer> trySplit() {
        Integer currentSize = integerList.size() - currentIndex;
        
        if (currentSize <= LIMITED_SPLIT_SIZE) {
            // 잘라진 사이즈가 자르지 말아야할 최소사이즈보다 작다면 null 을 출력.
            return null;
        } else {
            // 할 일을 절반씩 잘라줍시다.
            Integer splitTargetSize = currentIndex + currentSize/2;
    
            List<Integer> subList = integerList.subList(currentIndex, splitTargetSize);
            currentIndex = splitTargetSize;
            
            return new IntegerSumSpliterator(subList);
        }
    }
 
    @Override
    public long estimateSize() {
        return integerList.size() - currentIndex;
    }
 
    /**
     * 타겟 리스트의 속
     *     - 각 요소의 유일함을 보장 [(1~1000) 까지의 리스트] : DISTINCT
     *     - 연산 중 중간에 수정될 일이 없으며 : IMMUTABLE
     *     - 여러 스레드가 동시 접근 가능 : CONCURRENT
     *     - 크기는 보장, 하위로 만들어지는 크기도 보장 : SIZED, SUBSIZED - filter 등 고려 안함.
     */
    @Override
    public int characteristics() {
        return Spliterator.DISTINCT + Spliterator.IMMUTABLE + Spliterator.CONCURRENT + Spliterator.SIZED + Spliterator.SUBSIZED;
    }
 
}
 
cs


각 인터페이스를 구현하고, StreamSupport 팩토리를 통해서 Stream 으로 변환해줍니다.


1
2
3
4
5
6
StreamSupport.stream(new IntegerSumSpliterator(dataSet), true)
            .reduce(Integer::sum)
            .ifPresent(System.out::println);
 
// 출력
// 499500
cs


위의 일반 Stream 을 사용한 것과 같은 결과가 나왔습니다. 그런데 굳이 귀찮게 Spliterator 를 만들어야 하나요? 같은 결과가 나오는 데 ㅡㅡ^


Spliterator 의 장점은 개발자의 의지병렬로 처리할 양이나 처리할 때 고려할 사항 (characteristics) 등을 변경할 수 있다는 것입니다. 성능 측정 결과를 기반으로 병렬처리 로직을 더욱 최적화 시킬 수 있을 것이라 생각합니다.



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








반응형
Posted by N'

Stream API 의 병렬스트림을 이용하여, 큰 고민 없이 병렬처리를 할 수 있음을 알 수 있었습니다.



현재 공부하고 있는 책인 [JAVA8 in Action] 에 따르면, 성능을 최적화할 때는 세 가지 황금 규칙이 있다고 합니다. 첫째도 측정, 둘째도 측정, 셋째도 측정!


병렬스트림으로 변경했을 때, 순차스트림보다 빠르다는 것을 보장할 수 있을까요? 

한번 측정을 해보겠습니다.


아래는 0~1000000 에 대한 덧셈 로직과 수행시간 결과입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
final int[] dataSet = IntStream.range(01000000).toArray();
 
// 단순한 for-loop 순차처리
{
    int sum = 0;
    for (int i = 0, size = dataSet.length; i < size; ++i) {
        sum += i;
    }
}
 
// 병렬스트림 사용.
{
    IntStream.of(dataSet).boxed().parallel().reduce(Integer::sum);
}
 
// Misure time!!
// 1.  3msecs.
// 2. 70msecs.
cs


결과는 단순한 for-loop 이 훨씬 빠르게 나왔습니다. 꼭 빠르다고만은 볼 수 없군요. ㅡㅡ^ 


병렬스트림을 사용한 예제에서는 최종연산 reduce 에서 코드를 간결하게 하기 위해(Integer::sum) 박싱(boxed) 과정을 사용하였습니다. 


알게모르게 놓치는 부분이지만 Wrapper 클래스를 사용하기 위해 하는 박싱과정은 비용이 은근히 크다고 볼 수 있습니다. 그렇기 때문에 JAVA8 에서는 원시타입을 위한 함수형인터페이스와 원시타입 전용 Stream 을 제공한다고 포스팅을 했었던 적이 있습니다.



병렬스트림의 예제를 다음과 같이 변경해보겠습니다.


1
2
3
4
5
// 병렬스트림 사용.
IntStream.of(dataSet).parallel().reduce((a, b) -> a + b);
 
// Misure time!!
// 1. 26msecs.
cs


Integer::sum 이란 이미 구현된 메소드레퍼런스 사용을 포기하고 람다로 구현하였으며, 박싱을 하지 않았습니다. 위의 예제보다 분명 시간은 빨라졌는데 단순 for-loop 보다 느립니다. 


이번에는 한 번 덧셈을 해야하는 양을 늘려보겠습니다. 현재 사용하는 IDE 인 이클립스의 힙을 늘리지 않은 상태에서 out of memory 가 나지 않는 최대량인 (0~700000000) 까지 늘려보겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
개수 : 2000000
1. For-loop (4 msecs)
2. Parallel   (43 msecs)
 
개수 : 25000000
1. For-loop (13 msecs)
2. Parallel   (60 msecs)
 
…….
 
개수 : 350000000
1. For-loop (122 msecs)
2. Parallel   (120 msecs)
 
개수 : 400000000
1. For-loop (152 msecs)
2. Parallel   (121 msecs)
 
개수 : 700000000
1. For-loop (243 msecs)
2. Parallel   (174  msecs)
cs


개수가 350000000 부터 비슷한 성능을 보였으며, 그 이후부터는 병렬스트림이 빠른 것을 알 수 있습니다.


즉 이 실험에서 알 수 있는 것은 작업의 분할, 멀티코어 간의 데이터 이동, 결과 병합 등 각 단계 과정의 비용이 꽤 크며, 순차로 하는 작업이 꽤 걸리는 작업일 때만 빛을 볼 수 있다는 것입니다.


두 가지 정도 병렬 처리 시, 주의해야할 문제에 대해서 언급을 했습니다.


- 반복자 내부 박싱에 주의할

(일반 비지니스로직 작성 중에서도 주의할 내용입니다. ㅡㅡ^)


- 병렬로 처리하고자 하는 일이 충분히 오래걸리는 작업인가

    (한 단위의 계산시간이 길다면, 성능개선의 가능성이 있습니다.)


이 외에, [JAVA8 in Action] 에서는 병렬스트림 사용 시, 고려해야할 사항을 다음과 같이 명시하고 있습니다.


1. 확신이 서지 않는다면 측정할 것! 


최적화를 위해서 해야할 세 가지 황금규칙을 생각하세요.


2. limit 나 findFirst 와 같은 순서에 의존하는 연산을 지양할 것.


병렬처리 자체가 반복자의 작업을 분할-정복 하는 알고리즘을 사용하는 데, 순서를 고려해야한다면 그만큼 작업(Thread의 task)간 동기화 시간이 길어지게 될 것입니다.


3. 적절한 자료구조를 사용할 것!


LinkedList 보다 ArrayList 가 좋습니다. 작업 분할을 위해서 LinkedList 는 모든 요소를 탐색해야하지만, ArrayList 는 index의 요소 단위로 탐색이 가능합니다.


작업 분할을 위한 자료구조 선택에 다음 사진을 참고하세요.


출처 : [JAVA8 in Action]


4. Stream 내의 파이프라인의 중간연산에 따라 작업 분할 성능에 영향이 있음!


일정한 크기의 Stream 의 경우 크기가 정확하기 때문에 적절하게 작업을 분리할 수 있지만, 중간에 filter 등 크기를 예측할 수 없게 만드는 중간연산이 있다면 병렬처리를 하는 것에 애로 사항을 꽃피울 수 있습니다


5. 병합과정의 비용도 고려할 것.


기껏 작업을 분할, 계산 열심히 해서 성능을 최적화 했지만, 병합과정에서 오래걸린다면 상쇄될 수 있습니다. 


6. 작업 간의 공유 변수 사용 금지!


예를 들어, 다음의 코드는 문제가 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A {
    Integer a = 0;
    
    void sum(int number) {
        this.a += number;
    }
}
 
A object = new A();
            
// 공유변수 접근.
IntStream.of(dataSet).boxed().parallel().forEach(object::sum);
 
// Measure time!!
// 526msecs.
cs


A 인스턴스의 멤버변수인 a 로 덧셈결과를 취합하고 있습니다. 이 때, 각 스레드는 공유 변수인 A.a 에 서로 접근하려하는 Race Condition 상황이 벌어질 수 있습니다. 


물론, 한 스레드가 한 변수에 접근가능하도록 내부적으로는 세마포어가 작동하는 것은 보장하지만 실험결과와 같이 비용이 크다는 것을 알 수 있습니다.



병렬스트림 사용 시, 주의사항을 다음과 같이 알아보았습니다. 


JAVA8 에서 멀티스레드 프로그래밍을 하기에 분명 Simple 해진 것은 맞고, 공짜로 병렬성을 얻었다고 하지만 이 세상에 완전한 공짜는 없다는 것을 깨닫고 갑니다.



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


반응형
Posted by N'

JAVA8 을 공부하는 있는 책인 [JAVA8 in Action] 에서 언급되었던 내용 중 한 가지는 "공짜로 병렬성을 얻을 수 있다"는 것이었습니다. Collection 군의 데이터를 새로 추가된 API 인 Stream 의 형태를 parallel 관련 keyword 를 사용하여, 선언형 프로그래밍을 작성하면 병렬처리가 된다는 것이었죠.


아래는 병렬처리를 수행하는 간단한 예제입니다.


1
2
3
4
5
6
7
8
9
10
// Collection 클래스 군의 parallelStream 사용.
List<Integer> boxedDataList = IntStream.of(dataSet).mapToObj(Integer::new).collect(Collectors.toList());
boxedDataList.parallelStream().reduce(Integer::sum).ifPresent(System.out::println);
        
// 순차스트림 (IntStream) 을 병렬스트림으로 변
IntStream.of(dataSet).boxed().parallel().reduce(Integer::sum).ifPresent(System.out::println);
 
// 출력 결과
// 499500
// 499500
cs


위의 예제를 보면 정말 간단하다는 것을 알 수 있습니다. (Too Simple!!)

병렬 keyword 를 제외한 중간, 최종 연산 방법이 생각이 안나면, 이 곳을 참고하세요. 



간단히 리뷰를 해보면, 


1번 선언에서는 일반적으로 Collection 에서 스트림을 구하는 것 대신에 parallelStream 키워드를 통해 병렬스트림으로 변경한 후, 처리를 하고 있습니다. 


2번 선언에서는 이미 순차스트림인 상태를 parallel 중간연산을 사용하여 병렬스트림으로 변경시켰습니다.


단순히 병렬스트림으로 변경하는 것만으로 병렬처리를 할 수 있으니, 책에서 소개한 것과 같이 공짜로 병렬성을 얻었다는 말이 뻥은 아니라는 것을 알 수 있습니다. ㅡㅡ^


즉 병렬처리를 하기 위해 생각해봐야할 고민인 

사용할 스레드 개수경쟁상태(race condition), 계산된 결과들의 동기화 등이 추상화되었습니다.


 

parallel 관련 키워드 메소드를 사용하여 순차스트림을 병렬스트림으로 변경했들이,

병렬스트림을 순차스트림으로 변경할 수도 있습니다. sequencial 키워드로 말이죠. :-) 


하지만, 이러한 중간연산(parallel 과 sequencial)을 아래와 같이 특정 중간연산의 제어를 하겠다는 목적으로 여러번 사용하는 것은 부질 없습니다. ㅡㅡ^


1
2
3
4
5
6
IntStream.of(dataSet).boxed()
            .parallel()
            .filter(n -> n % 2 == 0)
            .map(n -> n + 2)
            .sequential()
            .collect(Collectors.reducing(Integer::sum)).ifPresent(System.out::println);
cs


최종적으로 선택된 sequential 만 적용되며, 위 연산은 순차상태로 계산됩니다.


하지만, 이러한 병렬처리가 무조건 성능을 끌어다 줄까요?


이 이야기는 다음 포스팅에서...




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



반응형
Posted by N'


JAVA8 의 등장하면서 생긴 가장 큰 변화 중 하나는 Stream API 를 통한 함수형 프로그래밍 패러다임을 사용할 수 있게 된 것입니다. 비지니스 로직 중 대부분을 SQL 과 같은 질의로 처리할 수 있게 되었기 때문에 활용을 잘한다면 성능과 간결함이라는 토끼를 모두 잡을 수 있게 된 것이죠. 


지난 포스팅까지 살펴본 Stream API 는 파이프라인식으로 여러 메소드를 연결하여 원하는 결과를 질의하는 형태였는데요. 이러한 파이프라인에 해당하는 각 메소드는 데이터를 필터하거나 타입을 변경하는 중간연산(filter, skip, map 등)과 결과를 원하는 형태로 반환하는 최종연산(foreach, collect, reduce)으로 분류할 수 있을 것 같습니다.


오늘은 그 중 최종 연산을 원하는 형태로 질의할 수 있는 방법중 하나인 collect 집중적으로 알아보려 합니다. 

(중간 연산에 대한 내용은 아래에서 참고하실 수 있습니다.)




Stream API를 포스팅 했던 대부분 예제에서는 collect 를 다음과 같이 중간연산의 결과를 List 형태로 출력하는 방법을 많이 사용했었습니다.


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
class Menu {
    private Integer price;
    private String name;
    
    public Menu (String name, int price) {
        this.name = name;
        this.price = price;
    }
 
    public String getName() {
        return name;
    }
    
    public Integer getPrice() {
        return price;
    }
}
 
final List<Menu> menuList = Arrays.asList(
                new Menu("고기"1200)
                , new Menu("랍스타"7600)
                , new Menu("피자"3100));
        
List<Menu> meatMenuList = menuList.stream().
            filter(menu -> menu.getName().equals("고기")).
            collect(Collectors.toList());
cs


collect 부분의 메소드를 조금 zoom-in 해서 보면, Collector 클래스를 파라미터로 받겠금 되어있는 것을 볼 수 있는데 오늘의 메인은 바로 이 Collector 입니다. :-)




1. Collector 란?


Collector 는 Stream API 의 최종연산을 어떻게 도출할 지를 추상화시킨 인터페이스입니다. 


해당 인터페이스의 각 구현 방법에 따라 원하는 최종 결과를 수행할 수 있습니다. 원하는 최종 결과를 얻기 위해 구현해야할 항목은 다음과 같습니다.


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
public interface Collector<T, A, R> {
    /**
     * 새로운 결과 컨테이너를 만들기 위한 메소드
     * <pre>
     *         처음에는 빈 값이 들어가며, 연산 과정을 통해 채워진 후 결과를 출력합니다.
     * </pre>
     * 
     * @return
     */
    Supplier<A> supplier();
    
    /**
     * 컨테이너에 요소를 추가하는 메소드
     * <pre>
     *         공급자 (Supplier) 를 통해 출력해야할 컨테이너에 데이터를 추가합니다.  
     * </pre>
     * 
     * @return
     */
    BiConsumer<A, T> accumulator();
    
    /**
     * 병렬 처리(parallelStream) 시, 두 컨테이너를 병합합니다.
     * 
     * @return
     */
    BinaryOperator<A> combiner();
    
    /**
     * 최종 변환값을 결과 컨테이너 적용합니다.
     * 
     * <pre>
     *         누적된 결과 컨테이너와 최종형태가 같다면, 항등함수( f(x) = x ) 를 내보냅니다.
     * </pre>
     * 
     * @return
     */
    Function<A, R> finisher();
    
    /**
     * 컬렉터 연산 시, 방법에 대한 전략 목록을 정의합니다.
     * 
     * <pre>
     *         UNORDERED : 리듀싱 결과는 방문 순서나 누적 순서에 영향을 받지 않는다.
     *         CONCURRENT : 누적 컨테이너에 요소를 추가하는 accumulator 메소드를 동시에 호출할 수 있다.
     *         IDENTITY_FINISH : finisher 메소드가 항등함수를 내보내야하는 상황이라면, 해당 메소드를 생략하고 누적객체를 바로 사용한다.
     * </pre>
     * 
     * @return
     */
    Set<Characteristics> characteristics();
}
cs


구현된 Collector 는 위의 메소드를 일련의 논리적 순서를 수행하며 원하는 결과를 도출할 수 있습니다. 


즉 collect 를 사용하여 최종결과를 얻기 위해서는 위의 interface 를 사용자가 구현하거나, Collectors 클래스 내부의 toList() 와 같이 이미 구현된 클래스를 사용하는 방법이 있습니다.


Collectors 는 구현화된 전략 Collector 를 출력하는 팩토리로, Stream API 는 이 전략 Collector 를 바탕으로 최종연산을 할 수 있습니다.


Collectors 내부에 정의된 최종연산을 수행하는 방법이 크게 다음과 같은 세 가지 (Reducing, Grouping, Partitioning) 정도의 목적을 가지고 있다고 할 수 있습니다.



2. Reducing 


Stream API 와 관련된 포스팅 중 map 과 reduce  연산을 통하여 요약된 결과를 출력하는 map-reduce  패턴에 대해 언급을 한 적이 있습니다. map 과 reduce 를 통해 Collection 의 원하는 부분만을 원하는 결과로 출력할 수 있다는 것은 매우 매력적인 일이라 할 수 있습니다. (해당 내용은 아래 포스트에서 확인할 수 있습니다.)



Collectors 클래스에서도 이러한 요약연산을 할 수 있는 reducing 메소드를 제공하며, 이 외에도 많이 사용될 법한 통계용 전략 Collector 역시 볼 수 있습니다.


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
final List<Menu> menuList = Arrays.asList(
                new Menu("고기"1200)
                , new Menu("랍스타"7600)
                , new Menu("피자"3100));
                
// 총 합
menuList.stream().collect(Collectors.summingInt(Menu::getPrice));
        
// 사용자 정의 총 합
menuList.stream().collect(Collectors.reducing(0, Menu::getPrice, (a, b) -> a+b));
        
// 사용자 정의 총 합 Optional
menuList.stream().map(Menu::getPrice).collect(Collectors.reducing((a, b) -> a+b)).ifPresent(System.out::println);
        
// 연산 통계
IntSummaryStatistics statics = menuList
                                .stream()
                                .collect(Collectors.summarizingInt(Menu::getPrice));
 
System.out.println("Max : " + statics.getMax());
System.out.println("Min : " + statics.getMin());
System.out.println("sum : " + statics.getSum());
System.out.println("Average : " + statics.getAverage());
System.out.println("count : " + statics.getCount());
 
// 출력물 
// 11900
// Max : 7600
// Min : 1200
// sum : 11900
// Average : 3966.6666666666665
// count : 3
cs


Reducing 을 하는 방법은 위와 같이 열려 있으며, 비지니스 로직 구현 시 알맞은 상황따라 구현하면 될 것 같네요.



3. Grouping


SQL 로 쿼리를 만들다보면 특정 필드따라 그룹을 만들고 싶은 경우가 있으며 GROUP BY 키워드를 사용하여 다음과 같이 grouping 된 결과를 얻을 수 있습니다.


1
2
3
4
5
6
7
SELECT
    company_subject_sn
    , COUNT(member_subject_sn)
FROM
    MEMBER
GROUP BY
    company_subject_sn
cs


Collectors 의 groupingBy 메소드를 사용하면, Collection 객체에 대하여 특정조건으로 분류시키는 작업을 간단하게 만들 수 있습니다.


예를들어 아래의 조직구성원 타입의 Collection 을 회사별, 조직별로 묶는 Map 을 제작은 아래와 같습니다.


비지니스 로직을 처리하기 위한 작업 중 아래와 같이 여러 key 값을 이용하여 Map 을 구성해야할 경우가 다수 존재합니다. 알고리즘 자체는 어려운 것은 아니지만 매번 저렇게 작성을 해야한다는 것은 안타까운 일입니다.


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
/**
 * 조직 정보를 담은 객체
 * 
 * @author Doohyun
 *
 */
class OrganizationMember {
    private Integer companySubjectSn;    // 회사 순번 
    private Integer memberSubjectSn;    // 구성원 순번
    private Integer organizationSn;        // 조직 순번  
    private String name;                // 이름
    
    public OrganizationMember(Integer companySubjectSn, Integer memberSubjectSn, Integer organizationSn, String name) {
        this.companySubjectSn = companySubjectSn;
        this.memberSubjectSn = memberSubjectSn;
        this.organizationSn = organizationSn;
        this.name = name;
    }
    
    public Integer getCompanySubjectSn() {
        return companySubjectSn;
    }
    public Integer getMemberSubjectSn() {
        return memberSubjectSn;
    }
    public Integer getOrganizationSn() {
        return organizationSn;
    }
    public String getName() {
        return name;
    }
}
 
List<OrganizationMember> testList = Arrays.asList(
                new OrganizationMember(511"남두현")
                , new OrganizationMember(622"윤석진")
                , new OrganizationMember(633"성지윤")
                , new OrganizationMember(744"백선기")
                , new OrganizationMember(754"황후순")
                , new OrganizationMember(561"이현우")
                , new OrganizationMember(572"태재영"));
        
HashMap<Integer, Map<Integer, List<String>>> groupingMap = new HashMap<>();
        
for (OrganizationMember organizationMember : testList) {
    final Integer companySubjectSn = organizationMember.getCompanySubjectSn();
    final Integer orgSn = organizationMember.getOrganizationSn();
            
    if (!groupingMap.containsKey(companySubjectSn)) {
        // 회사순번으로 분류
        groupingMap.put(companySubjectSn, new HashMap<>());
    }
            
    final Map<Integer, List<String>> orgMap = groupingMap.get(companySubjectSn);
    if (!orgMap.containsKey(orgSn)) {
        // 조직순번으로 분류
        orgMap.put(orgSn, new LinkedList<>());
    }
            
    // 조직순번으로 분류
    orgMap.get(orgSn).add(organizationMember.getName());
}
        
System.out.println(groupingMap.toString());
 
// 출력
// {5={1=[남두현, 이현우], 2=[태재영]}, 6={2=[윤석진], 3=[성지윤]}, 7={4=[백선기, 황후순]}}
 
cs



Collectors 팩토리에서 제공하는 groupingBy 를 사용하면 위의 문제를 보다 쉽고 가독성있게 구현할 수 있습니다. 아래 예제는 위의 기능을 Collectors.groupingBy() 를 사용하여 간결하게 작성한 내용입니다.


1
2
3
4
5
6
7
8
9
10
11
Map<Integer, Map<Integer, List<String>>> groupinParallelMap = 
                    testList
                    .parallelStream()
                    .collect(
                        Collectors.groupingBy(OrganizationMember::getCompanySubjectSn
                        , Collectors.groupingBy(OrganizationMember::getOrganizationSn, Collectors.mapping(OrganizationMember::getName, Collectors.toList()))));
        
System.out.println(groupinParallelMap.toString());
 
// 출력
// {5={1=[남두현, 이현우], 2=[태재영]}, 6={2=[윤석진], 3=[성지윤]}, 7={4=[백선기, 황후순]}}
cs


파이프라인이 조금 복잡해보이지만,  groupingBy 에서 회사조직 순번순으로 분류작업을 하였으며Stream API map 비슷한 역할을 하는 mapping  메소드를 통해 조직객체를 이름으로 변경을 했습니다.



4. Partitioning


앞서 설명한 Collectors.groupingBy() 과 비슷하게, Stream 내의 객체들을 특정 Predicate 로 분류하여 그룹화할 수 있는 기능 또한 존재합니다. Collectors.partitioningBy() 을 통해 사용할 수 있습니다.


Predicate 로 분류작업을 수행하기 때문에 반환되는 key 의 값은 Boolean(참, 거짓) 입니다. 그러므로, 결과 Map 은 최대 2 개의 그룹이 존재한다고 봐도 될 것 같습니다.


아래 예제는 회사순번이 5인 그룹과 아닌 그룹으로 분류, 또한 세부 그룹을 조직순번으로 분류한 작업입니다.


1
2
3
4
5
6
7
8
9
10
Map<Boolean, Map<Integer, List<String>>> groupinParallelMap = 
                    testList
                        .parallelStream()
                        .collect(
                            Collectors.partitioningBy((OrganizationMember v) -> v.getCompanySubjectSn() == 5
                            , Collectors.groupingBy(OrganizationMember::getOrganizationSn
                                    , Collectors.mapping(OrganizationMember::getName, Collectors.toList()))));
 
// 출력
// {false={2=[윤석진], 3=[성지윤], 4=[백선기, 황후순]}, true={1=[남두현, 이현우], 2=[태재영]}}
cs


앞의 예제와 크게 변하지 않고 단순히 파이프라인 한 개만 변할 것을 알 수 있습니다. (이게 함수형 프로그래밍의 장점이죠!!)


또한 이런 분류작업을 병렬로 하고 있음(parallelStream) 을 알 수 있습니다.



오늘 포스팅으로 Stream 에서 진행되는 중간연산과 최종연산을 수행하는 방법을 간략하게는 훑어보게 된 것 같습니다. 이러한 결과로 컨테이너 객체를 이용한 비지니스 로직이 한 결 더 유연하고 간략하며, 최적화까지 할 수 있는 여지가 생겼습니다. 


하지만 잘 사용해야 가능한 이야기겠죠? :-)



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






반응형
Posted by N'