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

https://github.com/skaengus2012/Ndroid


기존 JAVA 에서는 변수에 값의 존재 여부를 명확하게 표현할 수 없었기 때문에 Null 검사를 해줬어야 했으며, 그럼에도 불구하고 NullPointerException 을 발생시키지 않기가 어려웠습니다.


그러나 JAVA8 에서는 Optional 이라는 Null 을 피하고자하는 새로운 개념이 생겼으며, 이 개념을 통해 어느정도 위의 문제를 해결하거나 비지니스 로직을 더 간소화 시킬 수 있었습니다. 


자세한 내용은 아래 포스팅을 참고! 



그러나, 아쉽게도 안드로이드에서는 Optional 을 API>=24 에서 사용할 수 있습니다.

하지만 언제나 JAVA8 을 대체하는 듯한 라이브러리가 있으니, RxJava 진영에서는 Maybe 를 지원합니다. Maybe 라는 이름이 생소해보일 수 있지만, 다른 언어에서는 Optional 대신 Maybe 라고 표기하는 곳도 있습니다.


Maybe 의 튜토리얼은 아래와 같습니다.


1
2
3
4
5
6
7
8
9
10
// 빈 Maybe 생성.
Maybe<String> emptyMaybe = Maybe.empty();
// 유효 값 Maybe 생성.
Maybe<String> validMaybe = Maybe.just("Test1");
 
// 값이 유효할 때 로그 출력.
validMaybe.subscribe(System.out::println);
 
// 실제 데이터 출력.
String returnValue = validMaybe.blockingGet("Default Value");
cs


그러나 아직 RxJava2 의 버전이 높지 않아, 편의 메소드들이 많이 추가되지 않은 것으로 보입니다. 

그래서 Ndroid 프로젝트에서는 Maybe 를 조금 더 적극적으로 사용해보자는 취지로 MaybeUtil 을 제작하였습니다. 


편의의 기준은 현재 공부하고 있는 [JAVA8 in Action] 의 실무에서 많이 사용하는 예를 참고하여 작성하였습니다.


1. Null 을 포함하는 Just


Maybe 의 사용목적 자체가 값의 존재여부를 확신할 수 없기 때문에 사용한다고 볼 수 있지만, 정작 Maybe 를 생성하기 위해서는 null check 를 수행해야합니다. ㅡㅡ^


Maybe 에 있는 just 는 Null 을 포함할 수 없습니다.


MaybeUtil 에서는 이 이슈 해결을 위해 아래와 같은 튜토리얼을 제공합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
String test = "Test", nullValue = null;
 
// Null 을 포함할 수 있는 just!
MaybeUtil.JustNullable(test).subscribe(System.out::println);
 
Maybe<String> nullValueMaybe = MaybeUtil.JustNullable(nullValue);
nullValueMaybe.subscribe(System.out::println);
        
// 오직 비어있는 Maybe 일 때만 실행!
MaybeUtil.SubscribeEmpty(nullValueMaybe, () -> System.out.println("That value is null!!"));
 
// Maybe 가 유효하다면, 두번째 param, 아니라면 세번째 param 사용
MaybeUtil.Subscribe(nullValueMaybe, System.out::println, () -> System.out.println("That value is null!!"));
cs


2. Map 의 안정적 사용을 위한 Maybe 지원.


Map 에 데이터를 삽입하고, 이를 사용하기 위해서는 containsKey 메소드를 통해, Map 에 실제 데이터가 존재하는지 확인하여야 합니다. 이 문제 역시도 Maybe 의 철학과 같이 값의 존재여부 문제를 따져볼 수 있습니다.


아래의 튜토리얼을 참고하여, Map 사용에 대한 비지니스 로직을 줄여볼 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
HashMap<String, Integer> testMap = new HashMap<>();
testMap.put("key1"1);
testMap.put("key2"1);
testMap.put("key3"1);
 
// 맵에서 데이터 출력 시, Maybe 형태로 반환
ContainerUtil.JustInMap(testMap, "key1").subscribe(System.out::println);
 
// 맵에 데이터가 존재할 때 해야할 Consumer 를 정의할 수 있음.
ContainerUtil.RunMaybeInMap(testMap, "key1"System.out::println);
ContainerUtil.RunMaybeInMap(testMap, "key4"System.out::println);
cs



3. Box Object 의 parse 를 Maybe 지원.


String 클래스를 Integer 나 Double 등, Boxed Object 로 변환하는 parseXXX 메소드는 언제나 XXXFormatException 을 발생시킬 우려가 있습니다. 이 문제 역시도 String 을 각 Boxed Object 으로 변환하여 값의 존재를 따질 수 있냐는 문제로 볼 수 있습니다.


아래 튜토리얼은 안전한 변환을 지원합니다.


1
2
3
4
5
6
7
8
9
// Parse Integer
StringUtil.ParseInteger("1").subscribe(System.out::println);
StringUtil.ParseInteger("String Text").subscribe(System.out::println);
 
// Parse Boolean
StringUtil.ParseBoolean("false").subscribe(System.out::println);
 
// Parse Double
StringUtil.ParseDouble("0.0111111").subscribe(System.out::println);
cs


 2,3 번의 경우 MaybeUtil 에 모든 기능을 추가할 수 있었으나, 


MaybeUtil 의 추 후 확장성


이미 Ndroid 프로젝트에 존재하는 StringUtil 과 ContainerUtil 의 성격과 더 맞다는 점


에서 기능을 나누게 되었습니다.


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

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


Maybe 를 통해 우리는 안드로이드의 비지니스 로직을 단순화시킬 수 있을 것이라 생각합니다.

Java8 의 새 라이브러리와 RxJava 는 거의 비슷하며, 어느곳에서도 익숙한 개발자가 되는 것은 조금의 노력으로 할 수 있을 것이라 생각합니다.


여러분들의 코드가 새 패러다임을 입고, 뷰티풀해지길 바랍니다. :-)




반응형
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 부터 등장한 디폴트 메소드에 의해 interface 의 유연함은 좋아졌고, 그 말은 즉 변경에 대한 호환성이 좋아졌다는 의미로 생각할 수 있을 것 같습니다.


디폴트 메소드에 대한 이야기는 이 포스팅에서 참고! :-)



하지만 interface 에 행위를 추가함에 있어서 JAVA에서 등장하지 않았던 다중상속과 같은 기능이 생겼으며, 어떤 면에서는 편리하지만 이에 따른 충돌이 있음을 생각해 볼 수 있습니다.


아래와 같이 말이죠. 


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
/**
 * 비행을 할 수 있는 인터페이스 명시.
 *
 * @author Doohyun
 *
 */
public interface IFlyable {
    /**
     * 날 수 있는 방법 명시.
     */
    void fly();
    
    /**
     * 음식으로 변환 
     * @return
     */
    default Object toFoodObject() {
        return new FoodByFlyAble();
    }
}
 
public interface IRunable {
    /**
     *  수 있는 방법 명시.
     */
    void run();
    
    /**
     * 음식으로 변환
     * @return
     */
    default Object toFoodObject() {
        return new FoodByFRunAble();
    }
}
 
/**
 * 닭 클래스 정의
 * 
 * @author Doohyun
 *
 */
public class Chicken implements IFlyable, IRunable{
 
    @Override
    public void fly() {
        System.out.println("30 m 점프");
    }
    
    @Override
    public void run() {
        System.out.println("열심히 뛰");
    }
}
cs


Chicken 클래스가 toFoodObject() 를 사용할 때, 어떤 interface 의 디폴트 메소드를 사용해야하는 것일까요? ㅡㅡ^


물론 이러한 모호성을 없애기 위해 JAVA8 in Action 에서는 해석 규칙을 명시하였습니다.


1. 클래스에서 디폴트 메소드를 재정의한다면, 클래스가 무조건 승리!


Chicken 클래스에서 아래와 같이 toFoodObject() 을 재정의 한다면, 일단 문제는 사라집니다. 

또한 interface 는 super 클래스가 아니니, super 키워드로 toFoodObject() 을 호출하는 모호성은 없다고 볼 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Chicken implements IFlyable, IRunable{
 
    @Override
    public void fly() {
        System.out.println("30 m 점프");
    }
    
    @Override
    public void    run() {
        System.out.println("열심히 뛰");
    }
    
    /**
     * toFoodObject 을 재정의!!
     */
    @Override
    public Object toFoodObject() {
        return new FrenchStyleChiken();
    }
}
cs



2. Sub interface 가 디폴트 메소드 재정의한다면, sub interface 가 승리!


interface 간 상속구조가 존재하고, sub interface 가 부모 interface 의 디폴트 메소드를 재정의한다면, 아래와 같이 sub interface 디폴트 메소드가 실행됩니다.


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
/**
 * 비행을 할 수 있는 인터페이스 명시.
 *
 * @author Doohyun
 *
 */
 interface IFlyable {
    /**
     * 날 수 있는 방법 명시.
     */
    void fly();
    
    /**
     * 음식으로 변환 
     * @return
     */
    default Object toFoodObject() {
        return new FoodByFlyAble();
    }
}
 
 interface IRunable extends IFlyable{
    /**
     *  수 있는 방법 명시.
     */
    void run();
    
    /**
     * 음식으로 변환 
     * @return
     */
    @Override
    default Object toFoodObject() {
        return new FoodByFRunAble();
    }
}
 
/**
 * 닭 클래스 정의
 * 
 * @author Doohyun
 *
 */
 class Chicken implements IRunable{
 
    @Override
    public void fly() {
        System.out.println("30 m 점프");
    }
    
    @Override
    public void run() {
        System.out.println("열심히 뛰");
    }
}
 
System.out.println(chicken.toString());
 
// Return
// FoodByRunnable 
cs



3. 디폴트 메소드의 우선순위가 결정되지 않았다면, 명시적 선언!


1, 2 번 규칙에 의해서 여전히 디폴트 메소드의 우선순위가 결정되지 않았다면 상속대상이 되는 클래스에서 어떤 메소드를 사용할지 명시적으로 선언해야 합니다.


JAVA8 에서는 인터페이스의 디폴트 메소드를 명시적으로 선언하기 위해서,


InterfaceName.super.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
/*
 * 닭 클래스 정의
 * 
 * @author Doohyun
 *
 */
 public class Chicken implements IRunable, IFlyable{
 
    @Override
    public void fly() {
        System.out.println("30 m 점프");
    }
    
    @Override
    public void run() {
        System.out.println("열심히 뛰");
    }
    
    @Override
    public Object toFoodObject() {
        return IRunable.super.toFoodObject();
    }
}
 
cs


디폴트 메소드에 의한 복잡한 상속구조에 의한 충돌은 위 세 가지 규칙만 따르면 해결 가능성이 존재합니다. 


디폴트 메소드가 추가됨에 따라 interface 변경에 대한 호환성 뿐만 아니라, 여러 기능을 가진 모듈체(디폴트 메소드에 의한 기능)를 쉽게 붙일 수 있다는 것은 꽤 매력적일 수 있습니다. :-)


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


반응형
Posted by N'

JAVA 를 조금이라도 사용해본 프로그래머라면, interface 의 중요성을 알고 있을 것이라 생각합니다.


interface 위주의 개발은 작성자(인터페이스를 사용하여 기능 개발)의 입장에서는 인터페이스에 정의된 기능 외에는 고려하지 않아도 되며, 사용자(인터페이스로 작성된 기능 사용) 입장에서는 인터페이스가 원하는 시그니처만 충족해주면 모듈을 사용할 수 있습니다.


interface 는 모듈 개발 시 생각을 단순화시킬 수 있으며, 모듈 간의 유연성을 높일 수 있으므로 자주 애용하고 있으리라 믿어 의심치 않습니다. 

(블로그 주인장도 그러한가? ㅡㅡ^) 


라이브러리 설계자들은 이러한 interface 위주로 모듈을 설계하여 배포를 하며, 개발자들은 해당 라이브러리를 사용하여 보다 쾌적한 개발을 할 수 있습니다. 


그러나 이러한 interface 의 설계는 양날의 검과 같습니다. 

interface 의 변경 시, 이를 구현하고 있는 class 를 모두 수정해야하기 때문이죠. 

물론 interface 의 설계자와 개발자가 동일하여 해당 수정작업에 대해 커버를 할 수 있으면 다행이지만, 이미 배포를 받아 인터페이스를 해당 모듈을 사용하여 구현한 개발자들이 많다면 그것만큼 재앙도 없을 것입니다.


JAVA8 에서는 이를 위해 default, static 메소드가 등장하였습니다. 

이 개념을 사용하여 인터페이스 내부에 공통적으로 수행해야할 일을 정의할 수 있으며, 이를 통해 인터페이스의 변경 작업 시 호환성 문제를 어느정도 해결할 수 있습니다. 


아래는 디폴트 메소드를 사용한 예제입니다.


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
/**
 * 비행이 가는하다는 것을 정의한 인터페이스.
 * 
 * @author Doohyun
 *
 */
public interface IFlyAble {
    /**
     * 이륙하는 방법에 대한 구현.
     */
    void fly();
    
    /**
     * 이륙할 수 있는 여부 구현.
     * 
     * @return
     */
    Boolean flyAbleYn();
    
    /**
     * 착지를 위한 기능은 fly able 일 때 가능..
     */
    default void landing() {
        if (flyAbleYn()) {
            System.out.println("착륙을 시도합니다.");
        }
    }
}
cs


처음 interface 설계에서는 [이륙을 할 수 있는가?(flyAbleYn)], [이륙(fly)] 에 대한 설계를 하여 구현하였지만, 이륙할 수 있는 클래스군 한정 착륙 기능을 수행할 수 있어야 했습니다.


이 기능은 JAVA8 에 새로 나온 개념인 디폴트 메소드로 해결을 하였으며, 위와 같이 [착륙(landing)] 기능이 구현된 것을 볼 수 있습니다.


JAVA8 이전 버전에서는 아마 이런식의 구현이 있었을 것입니다.


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
/**
 * 비행이 가는하다는 것을 정의한 인터페이스.
 * 
 * @author Doohyun
 *
 */
public interface IFlyAble {
    /**
     * 이륙하는 방법에 대한 구현.
     */
    void fly();
    
    /**
     * 이륙할 수 있는 여부 구현.
     * 
     * @return
     */
    Boolean flyAbleYn();
}
 
/**
 * 비행이 가능한 객체에 대한 유틸 클래스
 * 
 * @author Doohyun
 *
 */
public class FlayAbleUtil {
    
    /**
     * 착지를 위한 기능은 fly able 일 때 가능..
     * 
     * <pre>
     *     default 메소드의 기능이 이러한 유틸로 제작되었을 것!
     * </pre>
     */
    public static void landing(IFlyAble flyable) {
        if (flyable.flyAbleYn()) {
            System.out.println("착륙을 시도합니다.");
        }
    }
}
cs


디폴트 메소드의 기능을 사용할 수 없으니, 공통적으로 사용가능성이 있는 모듈을 위와 같이 Util 클래스의 형태로 제작했을 것입니다.


이러한 패턴을 우리는 많이 본 적이 있습니다. 맞습니다. Collections!


1
2
3
4
5
6
7
8
9
10
11
List<Integer> numberList = IntStream.range(0100).boxed().collect(Collectors.toList());
 
/**
 * java8 이전의 정렬.
 */
Collections.sort(numberList, (a, b) -> a.compareTo(b));
 
/**
 * java8 에서 추가된 디폴트 메소
 */
numberList.sort((a, b) -> a.compareTo(b));
cs


JAVA8의 설계자는 위와 같이 유틸클래스에 있던 기능을 디폴트 메소드로 구현함으로써, 각 하위클래스들이 직관적으로 기능을 수행할 수 있도록 하였습니다. 


Collection interface 는 변경되었지만, 호환성에서는 아무런 문제가 없었습니다.


또한 JAVA8 부터 기본적으로 제공되는 함수형 인터페이스들도 람다조합 등의 기능을 동작하게 하기 위하여 default method 를 사용하고 있습니다.



default 메소드는 이외에도 선택적으로 구현해야하는 사항에 대한 stub 을 더이상 만들 필요가 없어졌습니다. 

예를들어 특정 인터페이스에서 일부 시그니처의 기능을 사용하는 때가 있고 아닐 때가 있다 가정하면 다음과 같이 선택적으로 시그니처를 구현할 수 있습니다.


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
/**
 * 비행을 할 수 있는 인터페이스 명시.
 *
 * @author Doohyun
 *
 */
public interface IFlyable {
    /**
     * 날 수 있는 방법 명시.
     */
    void fly();
    
    /**
     * 음식으로 변환
     * @return
     */
    default Object toFoodObject() {
        throw new UnsupportedOperationException("해당 제품은 먹는 것을 지원할 수 없습니다.");
    }
}
 
/**
 * 닭 클래스 정의
 * 
 * @author Doohyun
 *
 */
public class Chicken implements IFlyable{
 
    @Override
    public void fly() {
        System.out.println("30 m 점");
    }
 
    @Override
    public Object toFoodObject() {
        System.out.println("닭을 튀기자!!!");
        ..  
        return obj;
    }
}
 
/**
 * 형 오리 정의
 * 
 * <pre>
 *     형 오리는 먹을 수 없음.
 * </pre>
 * 
 * @author Doohyun
 *
 */
public class BroDuck implements IFlyable{
    @Override
    public void fly() {
        System.out.println("10cm 점프");
    }
}
cs


디폴트 메소드는 위와 같이 인터페이스의 수정에 있어서 유연성을 줄 수 있는 도구입니다. 


더이상 interface 의 수정에 있어서 호환성에 대한 문제를 잠시 잊게 해주며, 어떤 면에서보면 단순히 틀이 아닌 모듈로써의 역할까지 수행할 수 있어 보입니다.


그러나 편의적인 면과 더불에 작은 문제가 생겼습니다. 

각 interface 들간 깊은 상속관계가 구축되어 있으며, 모듈화된 디폴트 메소드에 의해 다중상속 문제가 등장하였습니다.


이에 대한 풀이를 다음 포스팅에서 하고자 합니다.




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


반응형
Posted by N'

Git 관리에 있어서 많이 보는 블로그 정리합니다.


Git 태그 관리 (http://minsone.github.io/git/git-addtion-and-modified-delete-tag)

반응형
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 의 등장으로 드디어 Java를 사용하는 어플리케이션에서, 함수형 프로그래밍을 할 수 있게 되었습니다. Lambda 를 사용할 수 있으면서, Collection 처리를 선언형으로 처리할 수 있다면 매우 매력적일 것이라 생각합니다.


그러나 아쉽게도 안드로이드에서는 안좋은 소식이 있습니다. 강력한 Stream API 를 24 버전 이 후부터 사용할 수 있습니다. 하위호환성이란 문제를 생각해야하는 입장에서 24버전을 minimum 으로 맞춰두고 개발하기란 쉽지 않을 것 같습니다.


Call requires API level 24 (current min is 15): java.util.Collection#stream


하지만 이를 커버할 수 있는 방법 중 하나는 안드로이드에서 반응형 프로그래밍을 할 수 있는 RxAndroid2(RxJava2 포함) 를 사용하는 방법이라 할 수 있을 것 같습니다. 


글 작성 기준 (2017.02.27) RxJava2 가 새로 등장하였으며, 이를 기준으로 Stream 과 호환할 수 있는 메소드 사용법에 대해 포스팅해보려 합니다. (간단한 예제는 생략합니다. ㅡㅡ^)


일단 gradle 로 다음 RxJava 라이브러리들을 다운 받습니다. 


RxAndroid2 : https://github.com/ReactiveX/RxAndroid


Rx-java-extension : https://github.com/jacek-marchwicki/rx-java-extensions



1. RxJava2 에서의 매칭


Collection 클래스 내에서 매칭을 하여 결과를 알 수 있다는 것은 꽤 유용한 방법이었습니다. 


해당 결과를 가진 데이터가 존재하는가? 혹은 모든 데이터가 해당 결과를 만족하는가? 등 우리는 Java8 에서 단순히 선언만으로 데이터를 처리할 수 있었습니다.



RxJava2 에서도 크게 다르지는 않습니다.


다음과 같이 any 와 all 을 사용할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
List<Integer[]> testT = Arrays.asList(new Integer[]{25},new Integer[]{38}, new Integer[]{3,5});
            
// 일부 매칭.
Observable.fromIterable(testT).
        map(arrays -> Arrays.asList(arrays)).
        any(list -> !list.contains(8)).blockingGet();
 
// 전체 매칭
Observable.fromIterable(testT).
        map(arrays -> Arrays.asList(arrays)).
        all(list -> !list.contains(8)).blockingGet();
cs



2. flatMap 을 통한 컬렉션 join


컬렉션 내부의 제네릭객체를 join 할 수 있는 flapMap 에 대해서 소개한 적이 있습니다. 



RxJava2 에서도 방법은 크게 다르지 않습니다. 호환성있게 지원이 됩니다. 아래와 같이 말이죠.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
List<MemberVo> memberVoList = Arrays.asList(
        new MemberVo(1"Doohyun Nam",28)
        , new MemberVo(2"Duckhyung Yuu"27));
 
List<PersonalInfoVo> personalInfoVoList = Arrays.asList(
        new PersonalInfoVo(1"My Way Functional Programming")
        , new PersonalInfoVo(2"My Way Spring FrameWork"));
 
Observable.fromIterable(memberVoList).
        flatMap(memberVo -> Observable.fromIterable(personalInfoVoList).
                                filter(personalInfoVo -> personalInfoVo.getMemberSubjectSn().equals(memberVo.getMemberSubjectSn())).map(personalInfoVo -> {
                            MemberPersonalVo memberPersonalVo = new MemberPersonalVo();
                            memberPersonalVo.setGoal(personalInfoVo.getGoal());
                            memberPersonalVo.setName(memberVo.getName());
                            return memberPersonalVo;
                        })).
        forEach(System.out::println); 
 
// Result
// Doohyun Nam : My Way Functional Programming
// Duckhyung Yuu : My Way Spring FrameWork
cs



3. Grouping 과 Partitioning 


컬렉션 데이터를 특정 조건으로 분류하고, 그 결과를 Map 으로 만들어주던 이 기능은 많은 비지니스 로직개선에 도움이 되었습니다. Collector 의 팩토리 메소드 몇개만 사용하면 쉽게 이용할 수 있었습니다.



그러나 RxJava2 에서는 이를 활용하기가 조금 까다롭습니다. Java8 의 Stream 처럼 collect 메소드를 지원하지만, 같이 사용가능한 Collectors 군의 팩토리 메소드들과 같은 유틸성 기능을 제공하지 않습니다.


대신에 toMap, toMultiMap, groupby 등의 메소드를 사용할 수 있습니다.


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
List<SubjectRelation> subjectRelationList = Arrays.asList(
                    new SubjectRelation(11001"Doohyun Nam"1)
                    , new SubjectRelation(11002"Dolkin"2)
                    , new SubjectRelation(11003"hshawng"1)
                    , new SubjectRelation(11004"spKwon"1)
                    , new SubjectRelation(21005"Other Person1"3)
                    , new SubjectRelation(21006"Other Person2"4)
            );
 
// create Map
Map<Integer, Collection<String>> mapTest = Observable.fromIterable(subjectRelationList).
           toMultimap(SubjectRelation::getCompanySubjectSn, SubjectRelation::getMemberName).
           blockingGet();
 
 
// only subscribe
Observable.fromIterable(subjectRelationList)
            .groupBy(SubjectRelation::getCompanySubjectSn).subscribe(group -> {
                  System.out.println(group.getKey());
                group.map(SubjectRelation::getMemberName).forEach(System.out::println);
            });
 
// create multi group
 Map<Integer, Map<Integer, Collection<String>>> doubleKeyMap = new HashMap<>();
            Observable.fromIterable(subjectRelationList).
                    groupBy(SubjectRelation::getCompanySubjectSn).
                    blockingSubscribe(group ->
                        doubleKeyMap.put(group.getKey(),
                                group.toMultimap(
                                        SubjectRelation::getOrganizationSubjectSn
                                        , SubjectRelation::getMemberName).blockingGet())
                    );
 
// partitioning
Map<Boolean, Collection<String>> partitioningMap = Observable.fromIterable(subjectRelationList).
             toMultimap(subjectRelation -> subjectRelation.getCompanySubjectSn().intValue() == 1
                 , SubjectRelation::getMemberName).
             blockingGet();
cs


세 번째 라인의 key 두 개를 이용하여 만든 맵의 경우 딱 맵까지 만들어주는 방법을 찾지는 못했습니다. 뭔가 우회해서 해보려고는 했으나, deadlock 현상을 보았습니다. ㅜㅡㅜ


Single 객체는 RxJava2 부터 더이상 null 을 허용하지 않겠다는 것으로 만든 객체로써, 사실 Single로 감싸게 된 맵이라도 실제 사용에는 큰 문제가 없어보입니다.


4. Math Library.


Java8 에서는 collect 나 reduce 를 이용하여 합계, 최소값, 최대값 등 통계 데이터를 구할 수 있었습니다.


RxJava2 에서는 extension 라이브러리에서 이 기능을 수행할 수 있도록 도와줍니다. 


1
2
3
4
5
// 합계
Observable.range(0100).to(MathObservable::sumInt).blockingSingle();
            
// 평균
Observable.range(0100).to(MathObservable::averageFloat).blockingSingle();
cs


5. Optional 대신 Maybe


Java8 에서는 더이상 null 을 허용하지 않겠다는 의도로 값의 존재여부를 표시할 수 있는 Optional 이란 개념이 생겼습니다. RxJava2 에서는 Maybe 를 통해서 이를 수행할 수 있습니다.


1
2
3
4
int number = 5;
 
Maybe<Integer> a = number% 5 == 0 ? Maybe.just(number) : Maybe.just(number);
a.subscribe(System.out::println);
cs


라이브러리나 플랫폼은 달라도 위에 있는 프로그래밍 패러다임의 계승은 비슷합니다.


아직 모든 기능을 알게 된 것은 아니지만, 안드로이드에서도 본격적으로 함수형 프로그래밍을 할 수 있을 것으로 보입니다.

반응형

'개발이야기 > 안드로이드' 카테고리의 다른 글

MVP 패턴을 통한 메모리 누수 관리  (0) 2016.11.24
WeakReference 테스트  (2) 2016.11.15
액티비티 개수 구하기  (0) 2016.11.15
안드로이드 DOZE 모드 만들기  (0) 2016.11.15
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'