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


- 함수란 무엇인가?


- 다형성(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'

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

https://github.com/skaengus2012/Ndroid


RxJava2 와 더불어 안드로이드에서 더욱 Functional Programming 을 할 수 방법이 늘어났습니다. 

점점 JAVA8 에서 언급된 내용들이 지원이 되고 있죠.


특히 RxJava 진영에서 JAVA8 의 기본 함수형 인터페이스를 제공해주기 때문에, 더욱 개발은 편해질 수 있을 것이라 생각합니다. (없었다면 람다를 인스턴스로 가지고 있기 위해, 함수형인터페이스를 만들어주었어야 할 것입니다.)


하지만 RxJava 의 함수형 인터페이스를 사용하는데 있어서 불편한 점이 두 가지가 있었습니다.


1. 함수형 인터페이스의 함수들이 모두 throws Exception


RxJava 에서 지원하는 함수형 인터페이스들은 모두 예외를 필수적으로 출력하도록 되어 있습니다. 기존 JAVA8 에서는 그렇지 않았으며,  RxJava 의 Observerable 이 더이상 NULL 을 허용하지 않겠다는 정책이 만들어짐에 따라 생긴 특성일 것이라 생각합니다.


물론 어떤면에서는 좋겠지만 이 행위는 람다를 쓸 때마다 Exception 에 대한 처리를 해야함을 말합니다. Exception 이 생길 일이 없는데 무조건 처리해야한다는 것은 안타까운 일입니다.


1
2
3
4
5
6
7
8
9
10
11
@FunctionalInterface
public interface BiFunction<T, U, R> {
    R apply(T t, U u) throws Exception
}
 
try {
      BiFunction<Integer, Integer, Integer> biFunction = (Integer a, Integer b) -> a + b;
     biFunction.apply(25);
catch (Exception e) {
    // Exception Required. ㅡㅡ^
}
cs


Ndroid 이를 위해 JAVA8 에서 지원하는 기본 함수형 인터페이스를 다시 만들었습니다. 


Ndroid 에서 지원하는 기본형들은 모두 인터페이스임을 명시하기 위해 Ixxxx 형식을 사용합니다.

(ex. IPredicate, IFunction)


상황에 따라 RxJava 의 람다와 섞어 사용할 수 있을 것이라 생각합니다.


1
2
3
4
5
6
7
8
9
@FunctionalInterface
public interface IBiFunction<T, U, R> {
    R apply(T t, U u) throws Exception
}
 
// Too simple. None Exception
IBiFunction<Integer, Integer, Integer> biFunction = (Integer a, Integer b) -> a + b;
biFunction.apply(25);
 
cs



2. 람다 조합 불가 


람다를 조합할 수 있다는 것은 매우 편리한 기능입니다. 


각종 조건들에 대해서, 더이상 논리연산자를 복잡하게 추가하지 않아도 되며, 이는 한결 유지보수에 도움이 될 수 있습니다.



그러나 JAVA8 의 디폴트, 정적 메소드들이 안드로이드 24버전부터 지원함에 따라 해당 기능을 사용할 수 없었습니다. 


RxJava 역시 해당 기능을 지원하지 않기 때문에 조합을 할 수 없는 방법이 없을까 고민하던 찰나, 해당 기능을 지원하는 Builder 를 만들기로 했습니다.


Ndroid 의 LambdaUtil 은 람다에 대한 조합을 할 수 있으며, Java8 에서 사용가능한 모든 기능을 지원합니다.


1
2
3
4
IPredicate<Integer> predicate = LambdaUtil.PredicateBuilder((Integer a) -> a >= 5).
                                    and(a -> a < 10).
                                    or(a -> a == 0).
                                    get();
cs


LambdaUtil 은 기본적으로 Ndroid 에 정의된 기본형 인터페이스들을 지원합니다. 


하지만 조합된 람다는 RxJava 의 Observable 과도 호환되야 하기 때문에, Rx 스타일의 람다 역시 지원하도록 하였습니다.


1
2
3
4
5
6
7
// a % 5 == 0 && a > 40 || a < 20
Observable.range(060).filter(
                LambdaUtil.PredicateBuilder((Integer a) -> a % 5 == 0).
                        and(a -> a > 40).
                        or(a -> a < 20).
                        getRx()).
                subscribe(System.out::println);
cs


자세한 내용은 Ndroid 의 ReadMe 에서 확인할 수 있습니다.

[https://github.com/skaengus2012/N-java/wiki/N-java-v0.2-wiki#lambda-combination]


비록 안드로이드에서 JAVA8 의 모든 기능을 사용할 수 없지만, 조금씩 그 격차는 줄어들고 있습니다. 

우리의 코드는 계속해서 자리를 잡을 것입니다. 언제나 그랬던 것 처럼 말이죠. :-)


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

지난 포스트에 이어서, 계속 Stream API에 대한 포스팅을 하고 있습니다. 


Stream API를 활용하여, 컨테이너(Collection)에서 복잡하고 다양한 조건을 질의형태로 구현할 수 있다는 것을 배웠습니다. 방어코드 없이 말이죠!! 


지난 포스팅은 이곳에서 확인! 



이러한 질의와 같은 방법은 SQL과 비슷해서 우리가 원하는 결과를 깔끔하게 구현할 수 있는데요. 오늘은 검색과 더불어 특정 데이터를 선택하는 작업인 매핑에 대해 알아보려 합니다.


계속해서 알기 쉬운 SQL에 비교하여 알아보겠습니다.


우리는 DBMS에 질의를 할 때, 언제나 테이블의 ROW 데이터 모두를 원하지 않습니다. 질의어 자체에서 출력 결과를 조정할 수 있으며, 선택을 할 수 있습니다. 예를들어 "구성원이라는 테이블에, 이름만 출력" 하고자 한다면 이렇게 사용을 하죠.


1
2
3
4
5
6
SELECT
    member.name
FROM
    member as member
WHERE
    member.age = 40
cs


JAVA8에서는 map 메소드를 사용하여 특정 필드의 결과만을 가지는 Collection을 생성할 수 있습니다. 함수형 인터페이스로 친다면 Function 의 성격을 가지고 있습니다. (T를 받아 R을 출력하고 있으니 말이죠 ㅡㅡ^) 


함수형 인터페이스의 성격이 무슨 말인지 모르신다면 해당 포스트를 참고하세요.



다시 돌아와서, map의 활용을 보도록하겠습니다.


일단 테스트VO 클래스에 대한 명시입니다.


1
2
3
4
5
6
public class MemberVo {
    private Integer sn;
    private Integer age;
    private String name;
}
 
cs


위 VO는 구성원을 가르키며, 고유넘버, 나이, 이름 정도의 멤버 변수를 가지고 있습니다. 

상단의 언급한 SQL 쿼리의 로직을 수행해보도록 하죠!


Map의 활용


1
2
3
4
5
6
7
8
9
10
List<MemberVo> memberList = Arrays.asList(
                new MemberVo(127"남두현"), new MemberVo(220"유형주"),
                new MemberVo(320"태재영"), new MemberVo(440"남궁계홍"));
        
memberList.stream()
    .filter(a -> a.getAge().intValue() == 40)
    .map(MemberVo::getName)
    .forEach(System.out::println);
 
console value : 남궁계홍
cs


지난번에 배웠던 검색 메소드인 filter를 이용하여 나이에 대한 조건을 언급하였고, map 메소드를 이용하여 이름이라는 데이터를 출력하였습니다.


보다 다양한 결과를 출력하고 싶다면, map 메소드의 파라미터안에 적절한 람다 혹은 메소드 레퍼런스를 언급해주면 됩니다.


JAVA 8 IN ACTION 에서는 이러한 map 과 더불어, flatMap을 같이 소개했습니다. flatMap의 기능은 스트림의 평면화로, 두 개의 컨테이너 객체의 스트림을 하나로 묶는 것을 말합니다. (쉽게 말해서 SQL의 JOIN과 같다고 볼 수 있습니다. ㅡㅡ^)


flatMap을 이용하여 두 테이블을 합친 로직을 구현해볼까요?


FlatMap의 활용


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
List<MemberVo> memberList = Arrays.asList(new MemberVo(127"남두현"), new MemberVo(220"유형주"),
                new MemberVo(320"태재영"), new MemberVo(440"남궁계홍"));
List<BoardDetailVo> boardList = Arrays.asList(new BoardDetailVo(1"람다게시판입니다.""람다에 관련된 클만 올려주세요."),
                new BoardDetailVo(1"람다란 무엇이니가?""함수형 프로그래밍의 꽃?"),
                new BoardDetailVo(2"물어볼 게 있습니다.""람다공부는 어떻게 하죠?"));
 
memberList.stream().flatMap(i ->
        boardList.stream()
        .filter(j -> j.getSn() == i.getSn().intValue())
        .map(j -> {
                MemberBoardResultVo result = new MemberBoardResultVo();
                result.setSn(i.getSn());
                result.setName(i.getName());
                result.setContents(j.getContens());
                result.setTitle(j.getTitle());
                return result;
        })).forEach(ret -> {
                System.out.println(String.format("[%s]%s:%s"
                    ret.getName(), 
                    ret.getTitle(), 
                    ret.getContents()));
            }
        );
 
- console result -
 
[남두현]람다게시판입니다.:람다에 관련된 클만 올려주세요.
[남두현]람다란 무엇이니가?:함수형 프로그래밍의 꽃?
[유형주]물어볼 게 있습니다.:람다공부는 어떻게 하죠?
 
cs


일단 chain된 Stream 이 살짝 복잡해 보이네요. 파이프 하나씩 한번 살펴보죠!


flatMap 내부부터 한번 살펴보면,


1. memberList의stream이 주가 되어, flatMap 내부에서 boardList의 Stream을 사용하고 있습니다.


1
memberList.stream().flatMap(i -> boardList.stream()....)
cs



2. boardList의 stream 내부 파이프라인 중 filter는 memberList 의 각 객체 중, 본인 객체의 sn이 서로 같은 것만 찾으라는 조건을 명시하고 있습니다.


1
filter(j -> j.getSn() == i.getSn().intValue())
cs


3. boardList stream의 내부 map에서는 Function 의 역할로 MemberBoardResultVo 를 출력하도록 되어 있습니다. 람다표현식을 사용하여, MemberVo와 BoardDetailVo 에서 각각 필요한 정보를 주입합니다.


1
2
3
4
5
6
7
8
map(j -> {
                MemberBoardResultVo result = new MemberBoardResultVo();
                result.setSn(i.getSn());
                result.setName(i.getName());
                result.setContents(j.getContens());
                result.setTitle(j.getTitle());
                return result;
        })
cs


4. foreach에서는 위의 flatMap으로부터 출력된 컨테이너를 순회하며, 필요한 정보를 출력하고 있습니다.


1
2
3
4
5
6
7
forEach(ret -> {
                System.out.println(String.format("[%s]%s:%s"
                    ret.getName(), 
                    ret.getTitle(), 
                    ret.getContents()));
            }
        );
cs


람다식이 조금 복잡해보이지만, 사실 기존 java8 이전의 구현 방식대로 진행한다고 하면, 2중 for-loop을 통해 보다 복잡한 로직이 될 수 있습니다. 위의 예제는 비교적 간단해보이지만, 컬렉션 3~4개를 합친다거나, 합쳐진 컬렉션에서 limit, skip, order by 등의 추가사항이 붙는다면 기존 구현된 소스를 보고 고민에 빠지는 시간이 조금 길어질것입니다.


하지만 람다식을 통해 다음과 같이 chain 메소드 한개만 출력해주면 간단히 해결이 됩니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
memberList.stream().flatMap(i ->
            boardList.stream()
            .filter(j -> j.getSn() == i.getSn().intValue())
            .map(j -> {
                    MemberBoardResultVo result = new MemberBoardResultVo();
                    result.setSn(i.getSn());
                    result.setName(i.getName());
                    result.setContents(j.getContens());
                    result.setTitle(j.getTitle());
                    result.setAge(i.getAge());
                    return result;
        }))
        .sorted((a, b)-> a.getAge().compareTo(b.getAge()))
        .limit(1)
        .forEach(ret -> {
                System.out.println(String.format("[%s]%s:%s"
                        ret.getName(), 
                        ret.getTitle(), 
                        ret.getContents()));
            }
        );
 
- console result -
[유형주]물어볼 게 있습니다.:람다공부는 어떻게 하죠?
cs


단순히, 두 객체를 머징하는 내부 람다에 age를 추가 하였고, sorted와 limit를 사용하였습니다.


즉 우리는 이번 포스팅을 통해 알 수 있었던 것은 컨테이너를 사용하는 로직에서 


단순히 체인형 stream을 추가하는 방법만으로 변화에 보다 쉽고 유연하게 대처


할 수 있음을 알게 되었습니다.



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



반응형
Posted by N'

현재 보고 있는 책인 JAVA8 IN ACTION 에서는 람다에 대해서 너무 자세히 다루고 있습니다. 

덕분에 많은 공부가 되었고, 포스팅 개수도 많이 늘어나고 있는 것 같습니다. ㅡㅡ^


오늘은 람다에 대한 마지막 포스팅은 람다를 연결해서 사용하는 방법에 대해 알아보려고 합니다. 


람다가 수학식에 대하여 추상화를 한 것이라 했을 때, 우리는 각 식을 조합하여 복잡한 식을 만들 수 있다는 것을 생각해 볼 수 있습니다. 

(잘 생각해보면, 굳이 람다가 아니더라도 if 절을 사용할 때 and 나 or 등을 사용하여 복잡한 컨디션을 만들고 있지 않나요?)


JAVA8은 람다 표현식을 더욱 적극적으로 사용할 수 있도록, 람다를 조합을 하거나 혹은 편리하게 사용하게 해줄 수 있는 메소드를 제공해줍니다.


1. Comparator 


- reverse 메소드를 이용하여 역정렬을 사용할 수 있습니다.


1
2
3
4
5
List<Apple> dataList = new ArrayList<>();
Comparator<Apple> compare = Comparator.comparing(Apple::getWeight);
dataList.sort(compare);
dataList.sort(compare.reversed());
 
cs


- thenComparing 메소드를 조건을 더 연결할 수 있습니다.


1
2
// 무게가 같다면, 컬러로 
dataList.sort(Comparator.comparing(Apple::getWeight).reversed().thenComparing(Apple::getColor));
cs



2. Predicate


- negate 메소드를 사용하여 반대 (!) 를 사용할 수 있습니다.


1
2
3
4
5
6
BiFunction<String, Integer, Apple> creator2 = Apple::new;
Apple apple2 = creator2.apply("green"100);
 
Predicate<Apple> isNotHeavy = (v) -> v.getWeight() > 150;
System.out.println(isNotHeavy.negate().test(apple2)); // true 
 
cs


- and나 or를 사용할 수 있습니다.


1
2
3
4
5
Predicate<Apple> isNotHeavy = (v) -> v.getWeight() > 150;
 
// 사과의 무게는 150 이상 200 이하 이거나, 색이 green 인 경우
isNotHeavy.and((v) -> v.getWeight() < 200).or((v) -> v.getColor().equals("green")).test(apple2);
 
cs



3. Function


우리가 보통 함수를 처음 배울 때, 이러한 표현을 배운적이 있습니다.


f(x) = x + 1

g(x) = x * 2


g(f(x)) = (x + 1) * 2


위와 같은 역할을 해줄 수 있는 메소드가 존재합니다. (andThen, compose)


1
2
3
4
5
6
Function<Integer, Integer> f = (x) -> x + 1;        // f(x)
Function<Integer, Integer> g = (x) -> x * 2;        // g(x)
        
Function<Integer, Integer> fg = f.andThen(g);     // g(f(x))
Function<Integer, Integer> gf = f.compose(g);    // f(g(x))
 
cs



람다를 이어줌으로써, 복잡한 식을 보다 쉽게 작성할 수 있지만 아무래도 복잡한 식에 대해서는 메서드 레퍼런스를 사용하는 것이 좋아보입니다.


만드는 것은 간단해보일 수 있으나, 유지보수적인 면도 봐야겠죠. ㅎㅎ



이 것으로 람다에 대한 포스팅은 끝!!!!  :-)


다음 포스팅부터는 자바8의 꽃인 Stream API에 대해 다루어 보려 합니다.



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


반응형

'개발이야기 > 함수형 프로그래밍' 카테고리의 다른 글

Stream과 Collection 차이  (0) 2016.08.04
Stream API  (0) 2016.08.03
메서드 레퍼런스  (0) 2016.08.01
람다의 실제 형식  (0) 2016.07.28
클로저와 람다  (2) 2016.07.28
Posted by N'

계속해서 람다와 관련된 포스팅을 하고 있습니다. 


람다는 익명 클래스를 어느정도 대체를 해줄 수 있는 것처럼 보이며, 간단한 처리는 대부분 람다를 이용하게 되지 않을까요? 

(예를들어 안드로이드에서 이벤트 처리 시, 익명클래스의 대체가.. ㅡㅡ^ ) 


람다와 람다를 왜 써야하는지 보려면 이 곳으로...



오늘은 람다표현식과 더불어, 간결성과 더불어, 가독성까지 챙길 수 있는 메서드 레퍼런스에 대하여 포스팅하려 합니다.


JAVA 8 IN ACTION 에 따르면, 메서드 레퍼런스는 특정 메서드만을 호출하는 람다의 축양형 이라고 볼 수 있다고 합니다. 

즉 람다를 통해 어떻게 어떤식의 메서드를 사용하라가 아닌, 메서드의 이름만을 참조하여 처리한다고 볼 수 있습니다. (그래서 레퍼런스라고 하나 봅니다... ㅎㅎ)


예를 들어, 이런 것을 볼 수 있을 것 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
class Apple {
    private int weight;
 
    public int compareTo(Apple apple){
        if (weight >= apple.getWeight()) return 0;
        else if (weight <= apple.getWeight()) return 1;
        else return -1;
    }
}
 
inventory.sort((Apple a, Apple b) -> a.compareTo(b));
inventory.sort(Apple::compareTo); // -> 메서드 레퍼런스
 
cs


저는 이 것을 보면서 처음에 이해가 안됐던 부분이 파라미터는 어떻게 넣을 것이냐라는 것이었습니다. 

람다의 경우 파라미터 영역이 따로 명시되어 있지만 메서드 레퍼런스는 (Apple::compareTo) 다음과 같이 명시가 되어있지 않습니다. 

그러나 이 것은 sort 매개변수 넣을 당시, 람다표현식에 대한 형식검사를 함을 알 수 있습니다. sort의 파라미터로 람다 표현식을 사용 시, 람다 디스크립터를 맞춰야 한다는 것은 지난 포스팅 때 언급을 했었습니다.




즉 이런한 점을 이용해, 람다 디스크립터에 맞춰 메서드 레퍼런스를 사용해주면 됨을 알게 되었습니다.


JAVA8 IN ACTION 에서는 메서드 레퍼런스를 만드는 법은 크게 세가지로 보고 있습니다.


1. 정적 메서드 레퍼런스 (정적 메서드 - static 메서드라 생각합시다.)


예를들어 Integer 클래스의 parseInt 메서드는 정적 메서드로, String 객체를 Integer 형태로 변경해줍니다. 해당 메서드는 다음과 같이 인수로써 가질 수 있습니다.


1
2
3
Function<String, Integer> parse = Integer::parseInt;
int a = parse.apply("5");
 
cs



2. 다양한 형식의 인스턴스 메서드 레퍼런스 (일반 메서드라 생각합시다.)


클래스에 정의 된 일반 메서드를 인수로 가질 수 있음을 말합니다. 아래는 List의 메소드인 size() 를 인수로 가지는 예제입니다.


1
2
3
4
5
6
7
8
9
10
List<String> strList = new ArrayList<String>() {
    {
        add("A");
        add("B");
        add("C");
    }
};
 
Function<List, Integer> size = List::size;    // 일반 메서드의 메서드 레퍼런스
System.out.println(size.apply(strList));     // 3 
cs




3. 기존 객체의 인스턴스 메서드 레퍼런스 


인스턴스 객체를 이용하여, 메서드 레퍼런스를 사용할 수 있습니다. 

예를들면 이런식으로 사용이 가능해보입니다. 


1
2
3
4
5
6
7
8
9
10
List<String> strList = new ArrayList<String>() {
    {
        add("A");
        add("B");
        add("C");
    }
};
        
Consumer<String> consume = strList::add
consume.accept("E");
cs


메소드레퍼런스를 사용하려는 대상 메소드가 다형성에 의해 가려질 때 사용할 듯 합니다.



4. 생성자 레퍼런스


new 키워드를 사용하여, 생성자의 레퍼런스를 만들 수 있습니다. 아래와 같이 작성이 가능합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Apple {
    private String color;
    private int weight;
    
    public Apple(){}
    
    public Apple(String color, Integer weight){
        this.color = color;
        this.weight = weight;
    }
}
 
Supplier<Apple> creator = Apple::new;    // 기본 생성자에 대한 메서드 레퍼런스
Apple apple = creator.get(); 
 
// color 와 weight 를 받아 Apple을 만드는 생성자의 메서드 레퍼런스
BiFunction<String, Integer, Apple> creator2 = Apple::new
Apple apple2 = creator2.apply("green"100);
 
cs


위와 같이 다양한 시그니처 생성자에 대하여 메서드 레퍼런스를 사용할 수 있습니다.



이러한 메서드 레퍼런스를 람다표현식을 직접 사용하지 않고 사용하는 이유는 복잡한 람다식을 일일이 명시하는 것보다 가독성이 좋기 때문입니다. 


즉 람다식이 복잡하다면, 차라리 메소드로 정의하여 처리하는 것이 좋아보입니다.



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




반응형

'개발이야기 > 함수형 프로그래밍' 카테고리의 다른 글

Stream API  (0) 2016.08.03
람다 조합  (0) 2016.08.02
람다의 실제 형식  (0) 2016.07.28
클로저와 람다  (2) 2016.07.28
원시타입을 위한 함수형 인터페이스  (0) 2016.07.28
Posted by N'

계속해서 람다에 대한 포스팅을 하고 있습니다. 


람다 표현식의 사용을 통해 우리는 함수형 인터페이스의 인스턴스를 만들 수 있다는 것을 알게 되었습니다.



그러나 사실 람다표현식만 보게 되면 어느 함수형 인터페이스를 구현하는지에 대한 정보는 없습니다. 

단지 함수형 인터페이스에서 구현할 함수 디스크립터와의 형식 검사를 통해 유효성을 가질 수 있으며, 또한 어떻게 보면 추론까지 가능합니다. 


오늘은 이러한 내용에 대해 포스팅을 하려 합니다. ㅡㅡ^


1. 형식 검사


일단 아래 코드를 먼저 보도록 하겠습니다.


1
2
3
4
5
6
7
@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}
 
Predicate<Apple> predicate = (Apple a) -> "green".equals(a.getColor());
 
cs


람다 표현식 (Apple a) -> "green".equals(a.getColor()); 이 Predicate<Apple>에 대입은 test 라는 한 개의 추상 메소드에 대한 함수 디스크립터에서 묘사해야할 내용과 같음을 알 수 있습니다. 


즉 추상 메소드의 시그니처가 요구하는 요구사항을 맞춰 람다를 제작해주어야 합니다.


이 때, void 의 경우 특별한 호환 규칙이 존재합니다.


1
2
3
4
5
6
public boolean checkData(Integer a){
    return a != null;
}
 
Consumer<Integer> setData = (Integer a) -> checkData(a);
setData.accept(5);
cs


위의 예제의 경우 람다 표현식 (Integer a) -> checkData(a); 의 경우 Integer를 파라미터로 받아 boolean 을 출력함을 알 수 있습니다. 


하지만 이 것은 유효한 코드입니다. 

(프로시저에서 boolean 함수를 사용한 것과 비슷하다고 생각하면 자연스럽게 넘어갈 수 있는 문제인 것 같습니다. ㅡㅡ^)


2. 형식 추론


함수형 인터페이스가 선언된 대상 형식(ex. Consumer<Apple>)에 따라, 람다 표현식의 형식 추론을 할 수 있습니다. 

대상 형식을 통해 함수 디스크립터(ex. (Integer a) -> a +2)를 알 수 있으며, 다음과 같은 코드가 가능합니다.


1
Consumer<Integer> setData = (a) -> checkData(a) ;
cs


대상형식 Consumer<Integer> 에 의해 람다 파라미터 부분에 Integer 형태가 이미 들어갈 것이라는 것을 추론할 수 있으며, 그렇기 때문에 단순히 a를 쓰는 것으로 대체가 가능합니다.


우리는 이미 이런 것을 제네릭을 사용하면서 많이 봐왔습니다. 

아래와 같이 말이죠.


1
List<Integer> dataList = new ArrayList<>();
cs



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



반응형

'개발이야기 > 함수형 프로그래밍' 카테고리의 다른 글

람다 조합  (0) 2016.08.02
메서드 레퍼런스  (0) 2016.08.01
클로저와 람다  (2) 2016.07.28
원시타입을 위한 함수형 인터페이스  (0) 2016.07.28
JAVA8 에 정의된 함수형 인터페이스  (0) 2016.07.28
Posted by N'

자바스크립트를 사용하다보면 종종 클로저를 사용할 때가 있습니다. 아래와 같이 말이죠.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Vo() {
    var data;
    
    return {
        getData : function() {
            return data;
        },
 
        setData : function(value) {
            data = value;
        }
    }
}
 
var closure = Vo();
closure.setData('Hello world');
console.log(closure.getData()); // Hello world
cs


클로저의 구조를 보게되면 함수 안에 내부함수가 존재하며, 이 내부함수는 인스턴스같이 외부로 넘겨지기도 하고 비지역변수를 자유롭게 변경이 가능합니다. 


람다나 익명클래스 역시도 저 동작과 비슷한 작업을 수행할 수 있습니다. 

함수를 인수로 넘기는 것이 가능하며, 메소드 내부에 함수를 선언할 수 있습니다. 

(아래 예제는 메소드 내부에 람다를 표현했고, 외부변수 x를 변경하였습니다.)


1
2
3
4
5
6
7
8
9
private int x = 0;
    
public void func(){
    int data = 0;
        
    IntConsumer setData = (int a) -> x = a;
    setData.accept(5);
}
 
cs


"JAVA 8 IN ACTION" 에서는 이러한 점 때문에 람다가 클로저의 정의에 부합하는가에 대해서 다루고 있습니다. 


결론부터 말하면, 서로 비슷한 일을 하고는 있지만 람다의 경우 선언된 지역변수의 데이터를 변경할 수 없습니다. 

지역변수가 람다 내부에서 사용되려면 final 한 상태이어야 합니다. 혹은 final과 같은 동작을 하거나 말이죠 ㅡㅡ^

(아래 예제는 람다 표현식에서 지역변수 data를 변경하려 하지만, 이 것은 문법 오류라고 체크하게 됩니다.)


1
2
3
4
5
6
7
public void func(){
    int data = 0;
         // 문법 오류
    IntConsumer setData = (int a) -> data = a;
    setData.accept(5);
}
 
cs


위와 같은 이유는 지역 변수 값의 경우 스택에 존재하며, 해당 메소드의 스레드 생명주기와 동일하게 종료되어야 합니다. 

이것이 보장되지 않는다면 람다 내부 블럭이 실행 시, 안전하지 않은 동작이 수행되겠죠.

(멤버 변수의 경우 힙에 있으므로, 람다에 대해 특별한 제약이 없습니다.) 



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


반응형
Posted by N'

JAVA8에서는 람다 표현식을 조금 더 편리하게 사용할 수 있도록 미리 제네릭 형태의 interface를 제작했습니다.




그러나, 기존 제네릭을 쓰는 클래스 (이를테면 Collection 그룹) 들과 마찬가지로, 

원시타입의 함수형 인터페이스는 사용할 수 없는 것처럼 보였습니다. ㅜㅜ 


오늘 포스팅은 이런 점들을 위한 원시타입을 지원하는 함수형 인터페이스에 대해 알아보려 합니다.


일단 원시타입을 지원하지 않는다고 하더라도, 이미 자바에는 원시타입을 지원하기 위한 wrapper 클래스들이 존재합니다. 


(Integer, Boolean 등) wrapper 라는 이름에서 보듯이, 이 클래스를 이용하면,


원시타입을 wrapper 로 변경하는 박싱 (boxing) 작업

wrapper 클래스를 원시타입으로 변경하는 언박싱 (unboxing) 작업


을 수행할 수 있습니다. 


보통 파라미터로 데이터를 넘기거나, 타입변환 시 자동으로 해주는 작업(오토 박싱)이지만, 이러한 변환 과정은 결국 비용이 발생하는 작업입니다. 

(인스턴스를 생성한 작업이기 때문에 결국 힙 메모리에 저장하게 되고, 원시타입을 찾을 때 역시도 메모리를 탐색해야 합니다)


자바8에서는 많이 사용되는 원시타입이 오토박싱되는 작업을 피할 수 있도록 원시타입에 특화된 함수형 interface를 제공합니다.


예를들어, int형의 Predicate 를 사용한다고하면, 다음과 같이 사용할 수 있습니다.


1
IntPredicate predicate = (int a) -> a >= 50;
cs


위와 같이 원시타입의 함수형 인터페이스를 사용하고 싶다면, 앞에 원시타입의 이름을 붙인 인터페이스를 사용하면 됩니다. 

(double 형의 Consumer 면 DoubleConsumer 겠죠. ㅡㅡ^)



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


반응형

'개발이야기 > 함수형 프로그래밍' 카테고리의 다른 글

람다의 실제 형식  (0) 2016.07.28
클로저와 람다  (2) 2016.07.28
JAVA8 에 정의된 함수형 인터페이스  (0) 2016.07.28
함수형 인터페이스  (0) 2016.07.27
람다 표현식  (0) 2016.07.27
Posted by N'

우리는 람다가 왜 쓰이는 지에 대해서 알게 되었고, 람다를 사용하기 위해서는 함수형 인터페이스를 제작해야한다는 것을 알게 되었습니다. 




하지만 대부분 함수형에서 사용할 법한 함수 디스크립터는 비슷할 것이며, 그렇기 때문에 JAVA8 라이브러리 설계자들은 java.util.function에 미리 사용할 법한 함수 인터페이스를 정의했습니다. 

(Observer 패턴을 많이 사용한다고 Observerable 을 정의한 것과 같은 맥락이겠죠. ㅡㅡ^)


오늘 포스팅은 자주 쓰일 법한 JAVA8의 함수형 인터페이스를 알아보려 합니다. (인터페이스에 대한 표기는 함수 디스크럽터만 정의합니다. 디폴트 메소드는 생략하겠습니다. ㅜㅜ)


1. Predicate

boolean 형식이 필요한 상황에서 별다른 구현 없이 람다식을 사용할 수 있습니다.


1
2
3
4
5
6
@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}
 
Predicate<Apple> predicate = (Apple a) -> "green".equals(a.getColor());
cs


2. Consumer

interface의 이름대로, 소비자입니다. 즉 프로시저로써, void 형의 함수를 만든다고 생각하면 됩니다. 


1
2
3
4
5
6
7
8
9
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}
 
People doohyun = obj.createObject(27"남두현");
 
Consumer<People> cosumer = (People people) -> System.out.println(people.toString());
cosumer.accept(doohyun);
cs


3. Function

Function 인터페이스의 경우 제네릭 형식의 T를 받아 R 객체를 반환하는 apply 라는 추상 메소드를 가지고 있습니다.


1
2
3
4
5
6
7
@FunctionalInterface
public interface Function<T, R> {
    R accept(T t);
}
 
Function<String, Integer> function = (String name) -> name.length();
Integer blogNameLength = function.apply("남두현 블로그 입니다.");
cs


4. Supplier

Consumer와 반대로 제네릭 형식의 T를 출력해주는 인터페이스입니다.  


1
2
3
4
5
6
7
8
9
10
@FunctionalInterface
public interface Supplier<T> {
    T accept();
}
 
Supplier<People> supplier = () -> {
    People ret = new People();
    ret.setName("기본 이름 지정");
    return ret;
};
cs


5. UnaryOperator

제네릭 형식의 T 타입을 파라미터로 받아 T를 return 하는 인터페이스다. 생각보다 쓸모는 없어보이네요다. 굳이 쓰자면 이런데에 쓸만하지 않을까요? (Clone 만들기)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@FunctionalInterface
public interface UnaryOperator<T> {
    T createObject(T t);
}
 
UnaryOperator<People> unaryOperator = (People people) -> {
    People clone = new People();
    clone.setAge(people.getAge());
    clone.setName(people.getName());
 
    return clone;
};
 
People doohyun = obj.createObject(27"남두현");
People cloneDoohyun = unaryOperator.apply(doohyun);
cs


6. BinaryOperator

제네릭 형식의 T 타입 두개를 받아, 조합하는 용도로 사용할 수 있다. 


1
2
3
4
5
6
7
@FunctionalInterface
public interface BinaryOperator<T> {
    T createObject(T t1, T t2);
}
 
BinaryOperator<Integer> binaryOperator = (Integer a, Integer b) -> a + b;
System.out.println(binaryOperator.apply(54));
cs


여기서 한가지 봐야할 부분은 UnaryOperator 와 BinayOperator 인터페이스는 compose 와 andThen 등의 디폴트 메소드를 가지고 있습니다. 해당 메소드를 통하여 다른 함수형 인터페이스를 만들수 있으니, 참고하세요.^^


1
2
3
4
5
6
7
UnaryOperator<String> unaryOperator = (String name) -> {
    return name;
};
Function<People, String> func = unaryOperator.compose((People people) -> people.getName());
 
People doohyun = obj.createObject(27"남두현");
System.out.println(func.apply(doohyun));
cs


Default method 로 등장장한 compose, andThen 에 대한 설명은 이 곳을 참고.



7. BiPredicate, BiConsumber, BiFunction

서로 다른 제네릭 형식 T, R 에 대한 비교를 수행할 수 있습니다. 

Bi가 붙은 인터페이스들은 서로 다른 제네릭에 대하여 수행을 합니다.


Bi 가 붙은 인터페이스는 함수디스크립터만 명시하겠습니다.


BiPredicate<L, R>   -->  (L, R)   --> boolean

BiConsumbe<T, U>   --> (T, U)   --> void

BiFunction<T, U, R>    --> (T, U)   --> R


1
2
3
4
5
6
7
@FunctionalInterface
public interface BiPredicate<T, R> {
    boolean test(T t, R r);
}
 
BiPredicate<Apple, String> biPredicate = (Apple apple, String type) -> type.equal(apple.getColor());
biPredicate.test(apple, "green");
cs



위와 같이, 이미 정의된 함수형 interface로 인해, 많은 기능을 사용할 수 있을 것 처럼 보입니다. 그러나 가만보니, 인터페이스에 붙어있는 제네릭 T에 의해 원사타입의 value 들(이를 테면 int, boolean 등)은 사용할 수 없는 것 처럼 보이네요.


그것에 대한 해결은 이 곳에서 확인 바랍니다.



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




반응형
Posted by N'