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


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


- 일급함수 


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

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


- 고차원 함수


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

즉 람다 조합을 말함



- 커링


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


예제


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


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

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


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


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

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


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



- 패턴매칭


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


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



- 영속자료구조


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

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


- 동시성 문제 해결


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


아래 포스팅 참고.



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

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


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


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


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


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



반응형
Posted by N'

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


JAVA8 IN ACTION 에 따르면,


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


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


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


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


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


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


1. LocalDate, LocalTime, LocalDateTime


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


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


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


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



2. 날짜 조정


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

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


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


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


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


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


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

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


1
2
3
4
@FunctionalInterface
public interface TemporalAdjuster {
    Temporal adjustInfo(Temporal temporal);
}
cs


예제를 한번 만들어 보죠. 


보통 사람들은 평일에 일을 하지만, 제 사수 형오리의 경우 목요일에 사정이 있어서 토요일에 대체 근무를 하고 있습니다. 즉 형오리의 쉬는 날은 목요일과 일요일입니다. 형오리의 다음 근무일을 구하는 전략을 구해보죠.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 TemporalAdjuster 형오리의_근무일_전략 = temporal -> {
 
     // week 객체 생성.
    DayOfWeek week = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
 
    // 형오리의 쉬는 날의 전날
    Collection<DayOfWeek> hollyDays = Arrays.asList(DayOfWeek.WEDNESDAY, DayOfWeek.SATURDAY);
 
    return temporal.plus(hollyDays.contains(week) ? 2 : 1, ChronoUnit.DAYS);
};
 
IntStream.range(010).boxed().reduce(LocalDate.of(201736), (date, num) -> {
        LocalDate nextDay = date.with(형오리의_근무일_전략);
 
        Optional.of(nextDay).
                        map(localDate -> localDate.format(DateTimeFormatter.ofPattern("yyyy. MM. dd (E)").withLocale(Locale.KOREA))).
                        ifPresent(System.out::println);
 
        return nextDay;
 
    }, (d1, d2) -> d1);
 
// PRINT RESULT
// 2017. 03. 07 (화)
// 2017. 03. 08 (수)
// 2017. 03. 10 (금)
// 2017. 03. 11 (토)
// 2017. 03. 13 (월)
// 2017. 03. 14 (화)
// 2017. 03. 15 (수)
// 2017. 03. 17 (금)
// 2017. 03. 18 (토)
// 2017. 03. 20 (월)
cs


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



3. Duration, Period


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


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


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


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


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



4. 시간의 파싱과 포맷팅


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


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


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



5. 특정 시간대 적용


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


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

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


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


[출처 : JAVA8 IN ACTION]



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


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

 

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


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



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


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


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







반응형
Posted by N'

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


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 부터 등장한 디폴트 메소드에 의해 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'

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


포스팅을 하고 있지만 계속 공부하는 책인 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'