클라이언트에서 위처럼 코드를 작성하면 member객체에 저장되어 있는 만료날짜를 가져와 현재 시간보다 적으면 만료되지 않았다는 것을 알 수 있다.
하지만, 이보다는 아래와 같은 방식이 역할과 책임을 잘 분리하고 유지보수하기 수월하다.
if(member.isExpired())
위처럼 설계해야 하는 이유는 두가지가 있다.
1. 만료가 되었는지 되지 않았는지는 Member 객체에 대한 책임이고, 수행해야할 역할이다.
2. 유지보수하기 수월하다.
여기서 유지보수하기가 수월하다는 이유가 와닿지 않을 수도 있다. 그렇다면, 가정해보자. 처음 설계는 밀리세컨드로 만료날짜를 확인했다. 하지만, 후에 밀리세컨드가 아닌 세컨드로 만료날짜를 확인하기로 변경되었다면 클라이언트 코드 중 어디서 만료날짜를 확인했는지부터 찾아야할 것이다. 하지만, 만료날짜를 확인하는 클라이언트 코드가 많은 경우라면? 코드를 다 찾고 수정하기 어렵다는 것을 알 수 있다.
하지만, 2번째와 같이 코드가 되어있다면 Member 클래스에서 isExpired() 메소드 안에 코드만 한번 수정해주면 끝난다. 이처럼 초반 설계는 중요하다..!
이전에 parallelStream을 설명할 때 말한 적이 있다. 상황에 따라서 병렬로 하는게 빠를 수도 있고, 더 느릴 수도 있다고 말이다. 실제로 그렇게 되는지 확인해보자.
sort()와 parallelSort() 비교
int size = 10000000;
int[] numbers = new int[size];
Random random = new Random();
IntStream.range(0, size).forEach(i -> numbers[i] = random.nextInt());
long start = System.nanoTime();
Arrays.sort(numbers);
System.out.println(" serial sorting took " + (System.nanoTime() - start));
IntStream.range(0, size).forEach(i -> numbers[i] = random.nextInt());
start = System.nanoTime();
Arrays.parallelSort(numbers);
System.out.println("parallel sorting took " + (System.nanoTime() - start));
위의 코드에서 size만 변경해보며 시간이 얼마나 걸리는지 차이를 알아보자.
size가 1,000인 경우
serial sorting took 635,363
parallel sorting took 2,320,564
size가 10,000인 경우
serial sorting took 4,114,773
parallel sorting took 5,344,624
size가 100,000인 경우
serial sorting took 39,835,655
parallel sorting took 42,495,858
size가 1,000,000인 경우
serial sorting took 967,621,466
parallel sorting took 333,204,237
알고리즘 효율성은 같다. 시간복잡도 : O(nlogN), 공간복잡도 : O(n)
size가 10,000,000인 경우
serial sorting took 968,464,268
parallel sorting took 277,913,894
size가 100,000,000인 경우
serial sorting took 10,152,806,777
parallel sorting took 2,313,726,436
size를 10단위로 늘려가며 테스트를 해보니 값이 커질 수록 병렬처리가 빠른 것을 확인해볼 수 있습니다.
그렇다면, size가 작으면 병렬이 느리고 size가 크면 병렬이 빠른 이유는 무엇일까요??
일단, 직렬 프로그래밍과 병렬 프로그래밍이 어떻게 다른지 알아야합니다.
프로세스에서 여러가지 일을 수행하기 위해선 멀티쓰레드를 사용해야 합니다. 단일쓰레드는 직렬 프로그래밍, 멀티쓰레드는 병렬 프로그래밍에서 사용한다고 보면됩니다. 여튼, 병렬 프로그래밍에서 사용하는 쓰레드는 쓰레드풀이 있지만, 쓰레드를 만들어 사용한다고하면 시간이 오래걸립니다. 이 때문에 실제로 size가 작을 때 작은 size를 계산하는 동안 쓰레드를 가져오는데 시간을 소모하기때문에 더 시간이 오래 걸리는 것 입니다.
실제로 size가 100일 때 시간 차이를 확인해 보았습니다.
public class SortTest {
public static void main(String[] args) {
int size = 100;
int[] numbers = new int[size];
Random random = new Random();
IntStream.range(0, size).forEach(i -> numbers[i] = random.nextInt());
long start = System.nanoTime();
Arrays.sort(numbers);
System.out.println(" serial sorting took " + (System.nanoTime() - start));
IntStream.range(0, size).forEach(i -> numbers[i] = random.nextInt());
start = System.nanoTime();
Arrays.parallelSort(numbers);
System.out.println("parallel sorting took " + (System.nanoTime() - start));
start = System.nanoTime();
MyThread myThread = new MyThread();
myThread.start();
System.out.println("Thread took " + (System.nanoTime() - start));
}
public static class MyThread extends Thread{
int num;
public MyThread() {
this.num = 0;
}
public MyThread(int num) {
this.num = num;
}
@Override
public void run() {
System.out.println("Thread Test");
}
}
}
출력
serial sorting 작업은 399,160입니다. 1개의 쓰레드를 생선하는데 필요한 시간은 1,075,145로 100개를 직렬로 정렬하는 것보다 더 오랜 시간을 소모하는 것을 확인할 수 있습니다. 그렇기 때문에 size에 따라서 직렬로 사용할지 병렬로 사용할지 잘 판단하여야 합니다.
기계용 시간 (machine time)과 인류용 시간 (human time)으로 나눌 수 있다.
기계용 시간은 EPOCK (1970년 1월 1일 0시 0분 0초)부터 현재까지의 타임스탬프를 표현한다.
인류용 시간은 우리가 흔히 사용하는 연,월,일,시,분,초 등을 표현한다.
타임스탬프는 Instant를 사용한다.
특정 날짜(LocalDate), 시간(LocalTime), 일시(LocalDateTime)를 사용할 수 있다.
기간을 표현할 때는 Duration(시간 기반)과 Period(날짜 기반)를 사용할 수 있다.
DateTimeFomatter를 사용해서 일시를 특정한 문자열로 formatting할 수 있다.
Date와 Time API 지금 시간을 기계 시간으로 표현하는 방법
Instant.now()
현재 UTC (GMT)를 리턴
Universal Time Coordinated == Gereenwich Mean Time
Instant now = Instant.now();
System.out.println(now); // 기준 UTC
System.out.println(now.atZone(ZoneId.of("UTC"))); // 위와 같음
System.out.println(ZoneId.systemDefault()); // 시스템 기준 시점
ZonedDateTime zonedDateTime = now.atZone(ZoneId.systemDefault()); // 시스템 기준 시점 시간
System.out.println(zonedDateTime);
GregorianCalendar와 Date 타입의 인스턴스를 Instant나 ZonedDateTime으로 변환 가능
java.util.TimeZone 에서 java.time.ZoneId로 상호 변환 가능
Date date = new Date();
Instant instant = date.toInstant();
Date newDate = Date.from(instant);
GregorianCalendar gregorianCalendar = new GregorianCalendar();
LocalDateTime now = gregorianCalendar.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
Stream이란 용어가 익숙하지 않을 수 있다. Stream이란 컨베이너 벨트에 데이터를 놓고 컨베이너 벨트를 지나가며 원하는 형식에 따라 데이터 처리를 하는 것이라고 보면 된다.
Stream
sequence of elements supporting sequential and parallel aggregate operations
데이터를 담고 있는 저장소(컬렉션)가 아니다.
Functional in nature, 스트림이 처리하는 데이터 소스를 변경하지 않는다. (기존 데이터는 그대로라고 보면 된다.)
스트림으로 처리하는 데이터는 오직 한번만 처리한다.
무제한일 수도 있다. (Short Circuit 메소드를 사용해서 제한할 수 있다.)
중개 오퍼레이션은 근본적으로 lazy하다. (lazy 함은 종료 오퍼레이션이 오기전까지, 작업을 수행하지 않는다. 라고 보면 좋다.)
손쉽게 병렬 처리 할 수 있다.
스트림 파이프라인
0 또는 다수의 중개 오퍼레이션 (intermediate operation)과 한개의 종료 오퍼레이션 (terminal operation으로 구성한다.
스트림의 데이터 소스는 오직 터미널 오퍼레이션을 실행할 때에만 처리한다.
중개 오퍼레이션
Stream을 리턴한다.
Stateless / Stateful 오퍼레이션으로 더 상세하게 구분할 수도 있다. (대부분은 Stateless지만 distinct나 sorted 처럼 이전 소스 데이터를 참조해야 하는 오퍼레이션은 Stateful 오퍼레이션이다.)
filter, map, limit, skip, sorted, ....
종료 오퍼레이션
Stream을 리턴하지 않는다. (Stream을 다른 자료구조로 변경하기 때문)
collect, allMatch, count, forEach, min, max, ....
이제 코드로 확인해보자.
package me.qazyj.java8;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class StreamTest {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("kyj");
names.add("yj");
names.add("kim");
names.add("young_jin");
Stream<String> stringStream = names.stream().map(String::toUpperCase);
names.forEach(System.out::println); // 기존 데이터는 소문자 그대로이다.
System.out.println("==============");
// 중개 오퍼레이션이 lazy함의 증거로 아무출력도 일어나지 않는다.
names.stream().map((s) -> {
System.out.println(s);
return s.toUpperCase();
});
// 중개 오퍼레이션 1개와 종료 오퍼레이션 1개로 stream 처리한 예시
// 종료 오퍼레이션이 왔기때문에 출력이 발생한다.
// 종료 오퍼레이션은 아래와 같이 Stream이 아닌 List나 Set과 같은 자료형임을 볼 수 있다.
List<String> collect = names.stream().map((s) -> {
System.out.println(s);
return s.toUpperCase();
}).collect(Collectors.toList());
// 병렬 처리 예시
List<String> collect1 = names.parallelStream().map(String::toUpperCase)
.collect(Collectors.toList());
collect1.forEach(System.out::println);
}
}
병렬 처리할 때 사용하는 parallelStream의 경우는 오히려 성능을 저하시킬 수 있다. 상황에 따라서 병렬로 하는게 빠를 수 있고, 더 느릴 수 있기 때문에 병렬을 쓸 때와 쓰지 않았을 때의 시간 차이를 보고 적절한 방법으로 사용하는걸 권장한다.
병렬처리 테스트에 관련한 자료는 아래
Stream API
스트림 API 사용 예시
걸러내기
Filter (Predicate)
예) 문자열 시작이 kim인 데이터만 새로운 스트림으로
변경하기
Map(Function) 또는 FlatMap(Function)
예) 각각의 User 인스턴스에서 String name만 새로운 스트림으로
예) List<Stream<String>>을 String의 스트림으로
생성하기
Generate(Supplier) 또는 Iterate(T seed, UnaryOperator)
예) 2씩 증가하는 무제한 숫자 스트림
예) 랜덤 Integer 무제한 스트림 (limit을 주지 않으면 무제한으로 간다.)
제한하기
limit(long) 또는 skip(long)
예) 최대 5개의 요소가 담긴 스트림을 리턴한다.
예) 앞에서 3개를 뺀 나머지 스트림을 리턴한다.
스트림에 있는 데이터가 특정 조건을 만족하는지 확인
anyMatch(), allMatch(), nonMatch()
예) k로 시작하는 문자열이 있는지 확인한다. (true 또는 false return)
예) 스트림에 있는 모든 문자열의 길이가 10보다 작은지 확인한다.
개수 세기
count()
예) k로 시작하는 문자열의 개수를 센다.
스크림을 데이터 하나로 뭉치기
reduce(identity, BiFunction), collect(), sum(), max
예) 모든 숫자 합 구하기
예) 모든 데이터를 하나의 List 혹은 Set에 옮겨 담기
이제 코드로 알아보자.
package me.qazyj.java8;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class OnlineClassTest {
public static void main(String[] args) {
List<OnlineClass> springClass = new ArrayList<>();
springClass.add(new OnlineClass(1, "spring boot", true));
springClass.add(new OnlineClass(2, "spring data jpa", true));
springClass.add(new OnlineClass(3, "spring mvc", false));
springClass.add(new OnlineClass(4, "spring core", false));
springClass.add(new OnlineClass(5, "spring batch", true));
springClass.add(new OnlineClass(6, "rest api", true));
System.out.println("spring 문자열로 시작하는 OnlineClass의 id");
springClass.stream().filter(oc -> oc.getTitle().startsWith("spring"))
.forEach(oc -> System.out.println(oc.getId()));
System.out.println();
System.out.println("close 되지 않은 OnlineClass의 id");
springClass.stream().filter(oc -> !oc.getClosed()) // Predicate.not(OnlineClass::getClosed()) 도 가능
.forEach(oc -> System.out.println(oc.getId()));
System.out.println();
System.out.println("수업 이름만 모아서 스트림 만들기");
springClass.stream().map(OnlineClass::getTitle)
.forEach(System.out::println);
System.out.println();
List<OnlineClass> javaClass = new ArrayList<>();
javaClass.add(new OnlineClass(7, "The Java, Test", true));
javaClass.add(new OnlineClass(8, "The Java, Code", true));
javaClass.add(new OnlineClass(9, "The Java, 8 to 11", false));
List<List<OnlineClass>> kyjEvents = new ArrayList<>();
kyjEvents.add(springClass);
kyjEvents.add(javaClass);
System.out.println("두 수업 목록에 들어있는 모든 OnlineClass의 id");
// flatMap이란 List 등과 같은 컬렉션의 데이터들을 풀어 놓는 것?? 이라고 보면된다. 현재는 OnlineClass 하나 하나 데이터들이 나오게 된다고 보면 된다.
kyjEvents.stream().flatMap(Collection::stream) // list -> stream 으로 flat
.forEach(oc -> System.out.println(oc.getId()));
System.out.println();
System.out.println("2씩 증가하는 무제한 스트림 중 앞에 2개 뺴고 최대 10개까지만");
Stream.iterate(0, i -> i + 2)
.skip(2)
.limit(10)
.forEach(System.out::println);
System.out.println();
System.out.println("자바 수업 중에서 Test가 들어있는 수업이 있는지 확인");
boolean test = javaClass.stream().anyMatch(oc -> oc.getTitle().contains("Test"));
System.out.println(test);
//.collect(Collectors.toList()) 대신 count()를 쓰면 개수가 나옴
System.out.println("스프링 수업 중에서 제목에 spring이 들어간 것만 모아서 List로 만들기");
List<String> spring = springClass.stream().filter(oc -> oc.getTitle().contains("spring"))
.map(OnlineClass::getTitle)
.collect(Collectors.toList());
spring.forEach(System.out::println);
System.out.println();
}
}