Kotlin 개발 유용한 링크
공식 문서가 굉장히 잘 되어 있다.
스프링 문서도 잘 되있음!
공식 문서 : https://kotlinlang.org/docs/reference/
Spring Kotlin 문서 : https://docs.spring.io/spring/docs/current/spring-framework-reference/languages.html#kotlin-resources
Lombok 사용 중 일경우 참고 : https://dzone.com/articles/migrating-from-lombok-to-kotlin
Lombok 으로 작성된 코드를 Kotlin 코드에서 접근할 수 없다.
Lombok은 컴파일 시 Annotation Processor 에 의해 동작하는데 Kotlin 컴파일시 Annotaion Processor 를 사용하지 않는다.
Spring one 에서 발표된 Kotlin 관련 영상 : https://www.youtube.com/results?search_query=spring+one+kotlin
Intellij 멀티 커서(멀티 라인 커서)
한 2년전에 패치 됬던가..
노트패드에도 있었던 멀티 라인 선택 기능..
가끔씩 여러라인 한번에 고칠 필요가 있는데..이때 유용.
Intellij 공식 용어는 Multicursor 인듯.
https://www.jetbrains.com/help/rider/Multicursor.html
Shift+Alt+Insert 로 활성화 후에 선택하고. 한번더 선택 하면 Multicursor 모드 해제 됨.
Spring 3.1.1 에서 @Scheduled 붙은 메서드는 Concurrent 하게 수행된다.
정말 처음 봤던 내용.
=======================================================================================================================
Spring에서는 비동기 처리 및 스케쥴링 처리를 위해서 @Async, @Scheduled 를 지원하는데요.
아래처럼 간단한 설정으로 사용 하기도 합니다.
<task:annotation-driven />
위 설정으로 세팅하고 @Scheduled를 사용하는데, 사용하는 입장에서는 당연히 비동기로 동작할 것이라 생각합니다.
(특정 시간에 동작하도록 하거나, fixedDelpay를 사용할 때)
하지만! 아무 설정을 하지 않을 경우에는 실제 작업을 수행하는 bean이 ConcurrentTaskScheduler로 설정되어 concurrent 하게 수행됩니다!
해서 비동기로 수행하고 싶으면 아래 처럼 설정을 추가해주시면 됩니다.
<bean class="org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler">
<property name="poolSize" value="10"/>
</bean>
// or
<task:scheduler id="scheduler" pool-size="10" />
그럼 비동기로 동작하는 것을 확인 할 수 있습니다.
참고로 디폴트로 ConcurrentTaskScheduler를 등록해 주는 부분은
ScheduledAnnotationBeanPostProcessor.onApplicationEvent 에서 맨 마지막에 this.registrar.afterPropertiesSet() 를 호출해주는데
여기 들어가 보면 taskScheduler 가 null 일때 ConcurrentTaskScheduler로 세팅해주는것을 확인할 수 있습니다.
public void afterPropertiesSet() {
if (this.taskScheduler == null) {
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}
if (this.triggerTasks != null) {
for (Map.Entry<Runnable, Trigger> entry : this.triggerTasks.entrySet()) {
this.scheduledFutures.add(this.taskScheduler.schedule(entry.getKey(), entry.getValue()));
}
}
if (this.cronTasks != null) {
for (Map.Entry<Runnable, String> entry : this.cronTasks.entrySet()) {
this.scheduledFutures.add(this.taskScheduler.schedule(entry.getKey(), new CronTrigger(entry.getValue())));
}
}
if (this.fixedRateTasks != null) {
for (Map.Entry<Runnable, Long> entry : this.fixedRateTasks.entrySet()) {
this.scheduledFutures.add(this.taskScheduler.scheduleAtFixedRate(entry.getKey(), entry.getValue()));
}
}
if (this.fixedDelayTasks != null) {
for (Map.Entry<Runnable, Long> entry : this.fixedDelayTasks.entrySet()) {
this.scheduledFutures.add(this.taskScheduler.scheduleWithFixedDelay(entry.getKey(), entry.getValue()));
}
}
}
감사합니다. :)
인텔리J에서 배포가 안될때..
오늘 나를 힘들게 했던것 기록..
프로젝트를 빌드해서 실행 할려고 하는데(tomcat), build artifact 하는 데서 문제가 발생!
"Error:Cannot build Artifact 'project:war exploded' because it is included into a circular dependency"
발생하는 순서는 최초 프로젝트가 한개있을 때는 잘 되다가 다른 프로젝트가 추가 됫을 경우(웹프로젝트, war 빌드) 발생했다.
연속되는 삽질을 하다가 원인을 찾았다.
Project Structure > Artifacts 로 이동 하면 output directory를 설정 할 수있는데,
각각 프로젝트들의 war exploded 위치가 동일하게 지정되있어서 이것때문에 발생한 문제 였다.
오마이 가쉬.
별거 아닌데 무한 삽질의 가능성을 내포한 문제인듯..
에러메시지를 좀 더 자세히 찍어 주면 좋겠다. ㅎㅎ
로드-커팅 문제(Cutting Stock Problem)
로드-커팅 문제(Cutting Stock Problem) : 주어진 장작을 다양한 크기로 잘라 최대한의 이익이 나도록 하는 문제.
도매로 장작을 구입해서 소매로 판매하는 회사에 근무한다고 가정해보자. 이 회사는 장작을 다양한 크기로 잘라서 최대한의 이익을 얻는다. 각각 서로 다른 길이를 갖고 있는 장작의 가격은 수시로 변경되며 따라서 주어진 크기의 장작에 대해 최대한의 이익을 구하는 프로그램을을 작성해야 한다.
단순한 재귀 호출을 사용한 해결
5인치의 장작이 있다면 그 길이에 대한 가격을 찾을 수 있다. 이 예제에서 5인치 장작의 가격은 2달러 이다, 하지만 4인치도 2달러 이기 때문에 이 경우에는 5인치를 5인치와 1인치로 자르는 방법이 더 좋다. 이 방법을 사용하면 n의 길이에 대한 수익을 구할 수 있으며, 그 방법은 해당 길이를 가능한 2n-1로 자르는 것이 가장 큰 수익을 얻을 수 있다.
주어진 길이 n에 대해서 max(no cut, cut(1, n-1), cut(2, n-2), ....)와 같이 계산할 수 있다.
이를 코드로 작성하면 아래와 같이 작성 할 수 있다.
이 메서드는 특정 길이의 수익을 찾는데 주어진 길이에 추가 하여 다른 길이의 수익을 재귀적으로 찾게 된다. 최종적으로는 그 중에 최대 수익을 구하도록 되어 있다.
final List<Integer> priceValues = Arrays.asList(1, 2, 4, 2, 2, 2, 1, 8, 9, 15);public int maxProfit(final int length) {
int profit = (length <= prices.size()) ? prices.get(length - 1) : 0;
for (int i = 1; i < length ; i++) {
final int currentProfit = maxProfit(i) + maxProfit(length - i);
if (currentProfit > profit) profit = currentProfit;
}
return profit;
}
이를 수행해 보면 length 값이 증가할 수록 복잡도가 O(2^(n-1) ) 와 같이 지수 형태로 증가 하기 때문이고, 다양한 길이에 대한 반복된 연산을 계속 수행한다.
이를 개선하기 위해 재귀 연산을 재활용하는 메모이제이션 기법을 사용한다.
메모이제이션을 사용하면 처음으로 연산하는 경우에만 연산을 실행하고, 이전에 한 번이라도 연산을 한 적이 있다면 과거의 연산 결과를 사용한다.
자바 8 람다의 힘 책에서는 이를 람다로 풀었었었는데, 이를 람다가 아닌 일반 해쉬 맵을 사용한다면..
private final Map<Integer, Integer> store = new HashMap<>();
public int maxProfit(final int length) {
int profit = (length <= prices.size()) ? prices.get(length - 1) : 0;
if (store.get(length) != null) {
return store.get(length);
} else {
for (int i = 1; i < length ; i++) {
final int currentProfit = maxProfit(i) + maxProfit(length - i);
if (currentProfit > profit) profit = currentProfit;
}
store.put(length, profit);
return profit;
}
}
위 같은 방법으로 이미 호출됬던 재귀 호출에 대한 값을 hashmap에 저장해 둠으로써 연산 속도를 엄청나게 올릴 수 있다.
기존 방법으로 할 경우 22를 값으로 줄 경우 무려 40초가 걸리는데, 맵을 사용하는 방법으로 변경하면 1초도 안걸린다.
이걸 Java 8 람다 스타일로 좀 바꿔 보면 아래 처럼 작성할 수 있다.
.computeIfAbsent는 map에 추가된 메서드로 키에 해당하는 값이 있으면 그 값을 리턴해주고 없으면 새로운 값을 할당한 다음 그것을 리턴해준다.
public int maxProfit(final int length) {
return store.computeIfAbsent(length, (key) -> {
int profit = (length <= prices.size()) ? prices.get(length - 1) : 0;
for (int i = 1; i < length; i++) {
final int currentProfit = maxProfit(i) + maxProfit(length - i);
if (currentProfit > profit) profit = currentProfit;
}
store.put(length, profit);
return profit;
}
);
}
여기서 느낄 수 있었던 것은 재귀 함수를 작성해서 문제를 해결한다. 라는 생각까지만 하지말고 이를 좀 더 머리를 써보면(여기서는 맵을 사용한 메모이제이션) 좀 더 성능이 좋은 코드를 작성할 수 있다는 것이다.
따로 알고리즘 공부를 하진 않지만 기존 알고리즘에 이런 메모이제이션 기법을 도입하는 것은 참신한 기법인듯.
프로그래밍 경험치가 +1 되는 기분이 들었다.
봄싹 2014에서 발표한 내용
Java 8 Stream API PART 2.
원본 : Part 2. Processing Data with Java SE 8 Streams
==================================================================================
다양한 기능들을 조합하여 좀 더 다양한 데이터 처리 쿼리를 만들기.
이전 문서에서 스트림을 사용하여 컬렉션 데이터 처리를 데이터베이스의 쿼리처럼 처리하는것을 살펴 보았다.
머
리를 식힐겸 스트림 API를 사용하여 거래금액이 비싼 내역들의 합을 구하는 코드인 Listing 1을 보자. 중간
작업(filter, map)을 통해 파이프라인을 세팅하고 종료 작업(reduce)을 통해 코드를 실행시키는 과정을 아래
Figure 1을 통해 볼 수 있다.
Listing 1.
int sumExpensive = transactions.stream() .filter(t -> t.getValue() > 1000) .map(Transaction::getValue) .reduce(0, Integer::sum);
Figure 1
flatMap, collect 두 기능에 대해서 알아보자.
이 두 기능은 복잡한 형태의 질의를 하는데 유용하게 쓰인다. 예를 들어 flatMap과 collect를 조합하여 Listing2 처럼 스트림에서 중복이 제거된 각 알파벳의 카운트를 가진 맵을 만들 수 있다.
이 두 기능에 대해서 더 자세히 살펴보고 설명할 예정이니 처음 보는 형태의 코드를 보고 걱정할 필요는 없다
Listing 2
import static java.util.function.Function.identity; import static java.util.stream.Collectors.*; Stream<String> words = Stream.of("Java", "Magazine", "is", "the", "best"); Map<String, Long> letterToCount = words.map(w -> w.split("")) .flatMap(Arrays::stream) .collect(groupingBy(identity(), counting()));
Listing2 의 결과를 출력해보면 Listing3과 같이 표시된다.
이제부터 flatMap과 collect가 어떻게 동작하는지 알아보자
Listing 3
[a:4, b:1, e:3, g:1, h:1, i:2, ..]
flatMap Operation
만약 파일에서 고유한 단어를 찾는다고 생각해보자. 이를 어떻게 구현할 것인가?
Files.lines()를 통해 한줄씩 읽은 다음 map() 기능을 사용해서 단어들을 잘라낸 다음에 distinct()를 사용해 중복을 제거 하면 된다. 이를 코드로 작성해보면 Listing 4처럼 될 것이다.
Listing 4
Files.lines(Paths.get("stuff.txt")) .map(line -> line.split("\\s+")) // Stream<String[]> .distinct() // Stream<String[]> .forEach(System.out::println);
하지만 이 코드는 원하는 대로 동작 하지 않는다. 이 코드를 실행시켜보면 아래와 같은 결과를 얻을 것이다.
[Ljava.lang.String;@7cca494b [Ljava.lang.String;@7ba4f24f
처음 작성한 위 코드는 스트림들을 String으로 출력하고 있다. 무슨일이 발생한걸까?
이 방식의 문제점은 람다 표현식에서 map으로 전달되는 형태가 String 배열(String[]) 이기 때문이다.
이 결과를 보면 우리가 원하는 형태는 문자들로 구성된 Stream<String>인데 map()을 사용했을때 리턴하는 스트림은 Stream<String[]>이 된다.
이는 flatMap을 사용하면 간단히 해결할 수 있다. 이 방법을 한단계씩 적용해보자.
코드를 작성하기 위해서 단어의 스트림 대신 배열 스트림이 필요하다. Arrays.stream() 메서드를 통해 배열을 스트림으로 만들 수 있다.
Listing 5
String[] arrayOfWords = {"Java", "Magazine"}; Stream<String> streamOfwords = Arrays.stream(arrayOfWords);
이처럼 배열을 스트림으로 변환하는 내용을 Listing 6 처럼 추가해보자.
이 방법도 스트림의 스트림목록 형태(Stream<Stream<String>>)로 되어 있어서 잘 동작하지 않는다.
처음엔 매 라인을 단어들의 배열로 바꾸도록 하였고, 그 다음 이 배열들을 Arrays.stream()을 사용해서 stream으로 변경했다.
Listing 6
Files.lines(Paths.get("stuff.txt")) .map(line -> line.split("\\s+")) // Stream<String[]> .map(Arrays::stream) // Stream<Stream<String>> .distinct() // Stream<Stream<String>> .forEach(System.out::println);
Listing 7 처럼 flatMap을 사용하도록 하여 잘 동작하게 할 수 있다. 이는 map(Arrays::stream)을 사용한것 처럼 각각 분리된 스트림들을 생성하고 이를 하나의 스트림으로 변경한다.
Listing 7
Files.lines(Paths.get("stuff.txt")) .map(line -> line.split("\\s+")) // Stream<String[]> .flatMap(Arrays::stream) // Stream<String> .distinct() // Stream<String> .forEach(System.out::println);
Figure 2 에서는 flatMap 메서드의 동작방식을 설명하고 있다.
Figure 2
간단하게 말하면 flatMap은 각각 다른 값을 가진 스트림들을 하나의 스트림으로 생성해 준다.
flatMap은 자주 사용되는 패턴으로 Optional이나 CompletableFuture를 사용할때 다시 보게 될 것이다.
collect Operation
이젠 colllect 메서드에 대해서 더 상세히 알아보자. 이전 문서에서 봤던 collect의 사용방법은 작업을 처리한 후 다른 스트림을 리턴하거나 다른 값(boolean, int, Optional)을 리턴하는 것이었다.
collect 메서드는 종료 작업이긴 하지만 stream을 list로 변경할 경우는 약간 다르다. 예를 들어 아래 listing 8의 코드 처럼 거래 금액이 큰 거래내역의 id 목록을 가져오는것 처럼 list를 리턴한다.
Listing 8
import static java.util.stream.Collectors.*; List<Integer> expensiveTransactionsIds = transactions.stream() .filter(t -> t.getValue() > 1000)
.map(Transaction::getId) .collect(toList());
collect 메서드에 전달된 인자의 타입은 java.util.stream.Collector다. Collector 오브젝트가 하는일은 무엇일까? 이것은 본질적으로 최종 리턴값으로 되어야할 것에 대해 설명을 한다고 보면 된다.
팩토리 메서드인 Collectors.toList()는 스트림을 리스트 형태로 변경하여 반환한다. 다른 형태의 내장 Collectors도 여러가지가 존재 한다.
스트림을 다른 컬렉션 형태로 변경하기.
toSet()을 사용하면 스트림을 Set(중복이 제거된)으로 변경 가능하다. Listing 9의 코드는 거래내역중에 도시 목록을 중복이 제거된 Set 형태로 반환한다.
Listing 9
Set<String> cities = transactions.stream() .filter(t -> t.getValue() > 1000) .map(Transaction::getCity) .collect(toSet());
위 코드를 보면 Set이 어떤 타입인지는 보장하지 않지만 toCollection() 메서드를 사용해서 타입을 지정할 수 있다.
아래 Listing 10의 코드 처럼 toCollecte에다가 HashSet을 지정할 수 있다.
Listing 10
Set<String> cities = transactions.stream() .filter(t -> t.getValue() > 1000) .map(Transaction::getCity) .collect(toCollection(HashSet::new));
위 기능은 collect를 사용해서 할 수 있는 것들중에 작은 부분일 뿐이다.
아래 예제처럼 몇가지 기능을 더 사용할 수 있다.
- 화폐에 따른 거래 내역을 그룹핑하여 합을 구하기 (Map<Currency, Integer>)
- 거래 내역을 두 개로 분리 : 비싼 내역과 그렇지 않은 내역(Map<Boolean, List<Transaction>)
- 다양한 뎁스로 그룹핑, 도시 형태로 먼저 그룹핑 하고 그 안에 비싼 내역과 그렇지 않은 내역으로 또 그룹핑(Map<String, Map<Boolean, List<Transaction>>)
계속 해서 Steam API와 collectors 들을 사용해서 다양한 질의를 생성하는것에 대해 살펴보자
가
장 먼저 볼 것은 스트림을 "summarizes" 하는 것이고(평균값 계산, 최대 및 최소값 추출) 그 다음은 간단한 그룹핑을
살펴보고 마지막으로 collectors 들을 조합하여 멀티레벨 그룹핑 같은 다양한 질의문을 만들어보자.
Summarizing
몇가지 간단한 예제들을 가지고 워밍업을 해보자.
이전 문서에서 숫자들의 목록을 가지고 최대, 최소 및 평균을 reduce 메소드를 사용해서 구해봤었다. 이것들을 미리 정의 되어 있는 collectors 들이다.
만약 숫자들의 갯수들 가져오고 싶으면 Listing 11의 코드처럼 counting()을 사용하면 된다.
Listing 11
long howManyTransactions = transactions.stream().collect(counting());
summingDouble(), summingInt(), summingLong() 같은 메서드를 사용해서 스트림에 포함된 Double, Int, long 과 같은 엘리먼트들의 합계를 구할 수 있다.
아래 Listing 12의 코드는 모든 거래 내역의 합계를 구한다.
Listing 12
int totalValue = transactions.stream()
.collect(summingInt(Transaction::getValue));
위와 비슷하게 averagingDouble(), averagingInt(), averagingLong()일 사용해서 평균값을 구할 수 있다.
maxBy(), minBy()를 사용해서 최대값과 최소값을 구할 수도 있다.
위 두 메서드는 Figure 3처럼 Comparator형태의 인자를 필요로 한다.
Figure 3
Listing 14의 코드 처럼 스태틱 메서드인 comparing() 메서드는 전달된 함수를 가지고 Comparator 객체를 생성한다. 이 함수에서 추출된 값을 사용해서 스트림의 값들을 비교하게 된다.
아래 코드에서는 가장 높은 값을 찾는 코드이다.
Listing 14
Optional<Transaction> highestTransaction = transactions.stream() .collect(maxBy(comparing(Transaction::getValue)));
reducing() 메서드 처럼 모든 엘리먼트들에 대해 정해진 작업을 반복적으로 수행하는 것도 있다.
Listing 15의 코드는 reducing() 메서드를 사용해서 합계를 구하는 코드이다.
Listing 15
int totalValue = transactions.stream().collect(reducing( 0, Transaction::getValue, Integer::sum));
recuding() 메서드는 3개의 인자를 가진다
- 초기화 값(stream이 비어있으면 이겂을 리턴) : 여기서는 0
- 스트림에서 반복적으로 수행할 함수 : 여기서는 getValue를 통해 값을 추출
- 위에서 구한 값을 조합 하는 코드 : 여기서는 값을 계속 더한다.
"이전에 reduce(), max(), min()을 통해서 위 기능을 사용했었는데 왜 이제와서 이걸 보여주지?' 라는 의문이 들 것이다.
위의 기본 기능이외에 collectors를 조합하여 더 복잡한 질의문(합계를 그룹핑 하는)을 생성하는것을 볼것인데 이 내장 collector에 대해서 알아 두는 것이 좋다.
Grouping
대부분의 데이터베이스들이 Group By 같은 명령어를 통해서 데이터 그룹핑을 지원한다.
만약 화폐에 따라 거내 내역을 그룹핑할 필요가 있을때 이를 이전 방법으로 작성한다면 아래 코드처럼 고통스러운 코드를 작성해야 될 것이다.
Listing 16
Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>(); for(Transaction transaction : transactions) { Currency currency = transaction.getCurrency(); List<Transaction> transactionsForCurrency = transactionsByCurrencies.get(currency); if (transactionsForCurrency == null) { transactionsForCurrency = new ArrayList<>(); transactionsByCurrencies.put(currency, transactionsForCurrency); } transactionsForCurrency.add(transaction); }
위 코드를 보면 먼저 거래내역을 저장할 Map을 생성한 후 거래내역에 대해 반복 작업을 수행해서 currency를 추출 한 다음 Map에 currency가 존재 하지 않으면 새로 만들어서 넣어 주는 작업을 하고 있다.
사실 currency에 따라 거래내역을 그룹핑 하는것을 원할 뿐인데 상당히 복잡한 코드로 작성되어 있다.
이를 개선하기 위해서는 groupingBy()메서드를 사용하면 되는데 이를 사용해서 간결한 코드로 위 내용을 작성할 수 있다.
Listing 17의 코드는 이전 코드와 동일한 역할을 하고 훨씬더 이해하기 쉽게 작성되어 있다.
Listing 17
Map<Currency, List<Transaction>> transactionsByCurrencies = transactions.stream().collect(groupingBy(Transaction::getCurrency));
팩토리 메서드인 groupingBy()는 그룹핑 하는데 키로 사용할 값을 추출하는 함수를 인자로 받아서 사용한다. 이를 classification function(분류함수)이라고 한다.
이 예제에서는 transaction의 currency로 그룹핑을 하기 위해서 메소드 참조(Transaction::getCurrency)를 파라미터로 전달한다.
Figure 4에서는 이를 그림으로 설명하고 있다.
Figure 4
Partitioning
groupingBy()의 특별한 케이스인 partitioningBy()라는 팩토리 메서드도 존재 한다. 이것은 predicate타입을 인자로 받으며 predicate에 만족하는 엘리먼트들을 그룹핑해서 리턴한다.
바꿔 말하면 거래내역들을 분류해서 그룹핑 할 수 있다(Map<Boolean, List<Transaction>)
예
를 들어 만약 거래내역을 두가지 그룹으로 나누고 싶다면(고가, 저가) Listing 18의 코드처럼
partitioningBy()를 사용하면 된다. 람다 표현식인 t -> t.getValue() > 1000 을 기준으로
거래 내역을 구분하게 된다.
Listing 18
Map<Boolean, List<Transaction>> partitionedTransactions = transactions.stream().collect(partitioningBy( t -> t.getValue() > 1000));
Composing collectors.
SQL을 자주 사용해봐서 익숙 하다면 GROUP BY와 함께 COUNT()와 SUM()을 사용해본 경험이 있을 것이다. 이 기능과 비슷한것을 Stream API를 통해서 할 수 있다.
groupingBy() 두번째 인자로 대상 을 넣어 주면 된다.
추
상적인 설명으로는 이해가 잘 않으니까 간단한 예제를 살펴보자. Map을 만들어서 도시별로 거래내역의 합계를 구한다고 해보자.
Listing 19의 코드 처럼 groupingBy 의 첫번째 인자로 getCity()를 줘서 키를 지정하면 도시를 키로 가지고
있고 값으로 List<Transaction>을 가지고 있는 Map을 리턴할 것이다.
Listing 19
Map<String, Integer> cityToSum = transactions.stream().collect(groupingBy( Transaction::getCity, summingInt(Transaction::getValue)));
만약 도시별 거래금액의 합계를 구하기 위해서는 summingInt라는 추가적인 컬렉터를 세팅해줘야된다. 이 코드의 실행 결과로 Map<String, Integer>를 형태로 각 도시별 거래금액의 합계를 구할 수 있다.
groupingBy(Transaction::getCity> 형태로 사용할 수도 있고 groupingBy<Stransaction::getCity, toList()) 형태로도 사용할 수 있다.
각 도시의 가장 높은 거래 금액을 가지고 있는 Map을 생성한다고 해보자.
아래 Lisinting 20의 코드처럼 이전에 봣던 maxBy 컬렉터를 사용하면 쉽게 구현할 수 있다.
Listing 20
Map<String, Optional<Transaction>> cityToHighestTransaction = transactions.stream().collect(groupingBy( Transaction::getCity, maxBy(comparing(Transaction::getValue))));
스트림 API는 다양한 기능을 제공하고 SQL처럼 동작하는 쿼리를 간결하게 작성할 수 있다.
마지막으로 조금 더 복잡한 예를 살펴보자. groupingBy()의 인자로 다른 컬렉터를 받아서 다양한 작업을 할 수 있다.
이것은 groupingBy()도 collector이기 때문이다. 멀티레벨 그룹핑을 할때 groupingBy collector를 전달함으로써 이를 구현할 수 잇다.
Listing 21의 코드는 거래 내역을 도시별로 그룹핑하고 하위 그룹은 통화별 거래내역의 평균을 가지도록 하는 코드이다.
Listing 21
Map<String, Map<Currency, Double>> cityByCurrencyToAverage = transactions.stream()
.collect(groupingBy(Transaction::getCity, groupingBy(Transaction::getCurrency, averagingInt(Transaction::getValue))));
Figure 5는 이름 그림으로 설명한 것이다.
지금까지 본 모든 collector 들은 java.util.stream.Collector 인터페이스를 구현한 것이다. 만약 자신만의 collector를 구현하고 싶으면 이 인터페이스를 상속 받아서 만들 면 된다.
결론
이 문서에서는 Stream API의 두가지 쓸만한 기능인 flatMap과 collect에 대해서 살펴봤다. 이를 사용해서 데이터 처리 작업을 간결한 코드로 할 수 있었다.
특 히 collect 메서드는 summarizing, grouping, partitioning을 쉽게 만들 수 있고 이를 서로 조합해서 "각 도시의 통화별 거래내역의 합계를 가지고 있는 두 단계 깊이의 Map 처럼 유용한 쿼리를 만들 수도 있다.
이 문서에 collector들에 대해 전부 알아보지는 않았기 때문에 다른 Collectors들(mapping(), joining(), collecting AndThen()) 같은 것들도 살펴보면 쓸만할 것이다.
자바 스트림 API
이번에는 Java SE 8에서 추가된 아주 좋은 SteamAPI에 대해서 알아 보도록 하겠습니다.
오역이 있을 수도 있으니 원본 문서도 한번 보는것을 추천 드립니다.
원본 글 : http://www.oracle.com/technetwork/articles/java/ma14-java-se-8-streams-2177646.html
오타 및 오역 지적해주시면 감사하겠습니다.
============================================================================================
복잡한 데이터 처리 질의를 표현하기 위해 Stream을 사용!.
대부분의 자바 애플리케이션들이 데이터를 만들고 처리하기 위해서 컬렉션을 사용한다. 이 컬렉션을 처리하는 로직 또한 같이 구현되어 코드에 포함되어 있고 이는 많은 프로그램의 기본(데이터를 그룹핑 하고 제어 하는 등)이 된다.
예를 들어 은행에서 고객들의 입출력 내역을 가지고 있는 컬렉션을 생성하고 이중에서 고객이 사용한 내역들에 대해 추출하는 로직을 작성할 수 있을 것이다. 이런 은행 관련 중요한 기능들을 컬렉션을 통해 작정할 수 있지만 코드가 그다지 아름답지 않고 지저분하게 보여진다.
컬렉션을 사용하는데는 두가지 단점이 있다.
1. 컬렉션에서 데이터를 처리하는 전형적인 처리 패턴은 SQL에서 데이터를 찾는것과 비슷하거나(처리 내역 중 가장 높은 값을 찾기) 데이터를 묶는것(구매 내역중에 식료품에서 쇼핑한 내역만 찾기)과 비슷하다. 대부분의 데이터베이스들은 이를 명확하게 지원하는데 만약 가장 높은 값을 가지는 것을 찾고 싶으면 "SELECT ID, MAX(VALUE) FROM TRANSACTIONS"를 사용 하면 된다.
위의 예에서 보는것 처럼 최대값을 어떻게 계산해야 하는지에 대해서 직접 구현할 필요가 없이(반복문을 사용해서 가장 높은 값을 찾는 것) 단지 원하는 값을 표현해 주기만 하면 된다. 컬렉션도 데이터베이스처럼 데이터의 집합인데 왜 비슷한 방법으로 처리할 생각을 못하고 이와 비슷한 구현을 하기 위해서 얼마나 많은 시간을 소모해서 복잡한 반복문을 작성하고 또 작성했는지 생각해보자.
2. 만약 큰 사이즈의 컬렉션은 어떻게 처리 해야 할까? 처리 속도를 향상하기 위해서는 병렬 처리를 하도록 코드를 작성해야 되는데 이는 기술적으로도 어렵고 많은 에러가 발생한다.
이러한 단점들은 Java SE 8이 출시되면서 해결할 수 있게 되었다. Stream 이라는 새로운 API가 공개되었고 이를 통해 쿼리를 작성하듯 데이터를 명시적인 방법으로 처리할 수 있게 되었다. 게다가 Stream은 멀티 스레드 관련 코드도 별도로 작성할 필요 없이 멀티코어를 지원할 수 있게 되었다. 좋아 보이지 않는가? 이 문서에서는 이런 내용들에 대하여 살펴볼 것이다.
Stream을 가지고 무엇을 할 수 있는지 알아보기 전에 Java SE 8의 Stream 사용법에 대해 간단히 살펴보자. 식료품점에서 쇼핑한 모든 거래내역중에 거래 ID를 가장 큰 비용이 들어간 순으로 정렬해서 추출하는 것에 대한 코드를 작성해보자.
Listing 1. Java SE 7 이하에서의 처리 방법
ListgroceryTransactions = new ArrayList (); for (Transaction t : groceryTransactions) { if (t.getType() == Transaction.GROCERY) { groceryTransactions.add(t); } } Collections.sort(groceryTransactions, new Comparator () { public int compare(Transaction t1, Transaction t2) { return t2.getValue().compareTo(t1.getValue()); } }); List transactionIds = new ArrayList (); for (Transaction t : groceryTransactions) { transactionIds.add(t.getId()); }
Listing 2. Java SE 8 에서의 처리 방법
ListtransactionsIds = transactions.stream() .filter(t -> t.getType() == Transaction.GROCERY) .sorted(Comparator.comparing(Transaction::getValue).reversed()) .map(Transaction::getId) .collect(Collectors.toList());
아래 그림 Fiqure 1은 Java SE 8에서 Stream이 동작하는 과정을 그린것이다.
먼저 모든 거래내역(List)로 부터 stream()메서드를 사용해서 stream을 가져오고 그 다음에 여러가지 기능(filter, sorted, map,collect)를 체인 처럼 엮어서 데이터를 처리하는 쿼리처럼 보이게 만든다.
Figure 1
병렬 처리 코드 작성은 Java SE 8에서 매우 쉽게할 수 있다. Listing 3. 처럼 그냥 stream()을 parallelStream()으로 변경만 하면 된다. Stream API는 내부적으로 멀티코어로 동작하도록 처리한다.
Listing 3.
ListtransactionsIds = transactions.parallelStream() .filter(t -> t.getType() == Transaction.GROCERY) .sorted(Comparator.comparing(Transaction::getValue).reversed()) .map(Transaction::getId) .collect(Collectors.toList());
처음보는 형태의 코드를 보고 걱정할 필요는 없다. 다음 섹션에서 이것들이 어떻게 동작하는지 살펴볼 것이다. 하지만 람다 표현식(t-> t.gerCategory() == Transaction.GROCERY)의 사용방법이나 메소드 참조(method references, Transaction::getId)에 대해서는 미리 한번 보는것이 좋다.
지금 부터 컬렉션에 저장된 데이터들을 SQL이 동작하는 것 처럼 스트림을 활용해서 처리 하는것에 대해서 살펴볼 것이다. 이 모든 동작들은 람다 표현식과 함께 간단한 파라미터로 처리 될 수 있다.
Java SE 8의 Stream에 대한 이 문서를 다 읽어보면 Stream API를 사용하여 위 예제와 같이 멋진 방식으로 사용할 수 있을 것이다.
Stream 시작 하기
작은 부분부터 시작하도록 해보자. Stream의 정의는 무엇일까? 간단하게 정의해 보면 "집계 연산을 지원하는 요소의 순서(a sequence of elements from a source that supports aggregate operations.)"라고도 할수 있는데 이에 대해 더 알아 보도록 하자.
- Sequence of element : Stream은 정의된 엘리먼트의 속성에 따라서 처리할 수 있는 인터페이스를 제공하지만 실제 엘리먼트들을 저장하지 않고 계산하는데만 쓰인다.
- Source : 스트림은 컬렉션, 배열, I/O 리소스 등에서 제공받은 데이터를 가지고 작업을 처리 한다.
- Aggreate operations : Stream은 SQL 같은 처리를 지원하고 함수형 프로그래밍 같은 처리 방법도 지원한다. (filter, map,reduce, find, match, sorted 등)
- Pipelining : 많은 Stream 기능들이 Stream 자기 자신을 리턴한다. 이 방식은 처리 작업이 체인처럼 연결되어 큰 파이프라인처럼 동작 하도록 한다. 이를 통해 laziness와 short-circuiting 과 같이 최적화 작업을 할 수 있다.
- Internal iteration : 명시적으로 반복작업을 수행해야 되는 Collection과 비교 하면 Stream 작업은 내부에서 처리된다.
List<String> transactionIds = new ArrayList<>(); for(Transaction t: transactions){ transactionIds.add(t.getId()); }
List<Integer> transactionIds = transactions.stream() .map(Transaction::getId) .collect(toList());
- filter, sorted, map은 파이프라인처럼 서로 연결시킬 수 있다.
- collect는 파이프라인을 종료 시키고 결과를 리턴한다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8); List<Integer> twoEvenSquares = numbers.stream() .filter(n -> { System.out.println("filtering " + n); return n % 2 == 0; }) .map(n -> { System.out.println("mapping " + n); return n * n; }) .limit(2) .collect(toList());
filtering 1 filtering 2 mapping 2 filtering 3 filtering 4 mapping 4
- 질의를 할 데이터소스(Collection)같은게 필요하다
- Stream 파이프라인을 형성하는 중간 작업
- Stream 파이프라인을 실행하고 결과를 리턴하는 종료 작업
- filter(Predicated) : 주어진 predicate(java.util.function.Predicate)와 일치하는 stream을 리턴한다.
- distinct : 중복을 제거한 유니크 엘리먼트를 리턴한다.(stream에 포함된 엘리먼트들의 equals()구현에 따라 구분된다.
- limit(n) : 주어진 사이즈(n)에 까지의 stream을 리턴한다.
- skip(n) : 주어진 엘리먼트 길이 까지 제외한 stream을 리턴한다.
boolean expensive = transactions.stream() .allMatch(t -> t.getValue() > 100);
Optional<Transaction> = transactions.stream() .filter(t -> t.getType() == Transaction.GROCERY) .findAny();
transactions.stream() .filter(t -> t.getType() == Transaction.GROCERY) .findAny() .ifPresent(System.out::println);
List<String> words = Arrays.asList("Oracle", "Java", "Magazine"); List<Integer> wordLengths = words.stream() .map(String::length) .collect(toList());
int sum = 0; for (int x : numbers) { sum += x; }
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
- 초기 화 값, 0
- BinaryOperator<T>, 두개의 엘리먼트들을 더해서 새로운 값을 생성
int product = numbers.stream().reduce(1, (a, b) -> a * b); int product = numbers.stream().reduce(1, Integer::max);
int statement = transactions.stream() .map(Transaction::getValue) .sum(); // error since Stream has no sum method
int statementSum = transactions.stream() .mapToInt(Transaction::getValue) .sum(); // works!
IntStream oddNumbers = IntStream.rangeClosed(10, 30) .filter(n -> n % 2 == 1);
Stream<Integer> numbersFromValues = Stream.of(1, 2, 3, 4); int[] numbers = {1, 2, 3, 4}; IntStream numbersFromArray = Arrays.stream(numbers);
long numberOfLines = Files.lines(Paths.get(“yourFile.txt”), Charset.defaultCharset()) .count();
Stream<Integer> numbers = Stream.iterate(0, n -> n + 10);
numbers.limit(5).forEach(System.out::println); // 0, 10, 20, 30, 40
- Steream API는 몇가지 기능들을 활요해서 laziness 및 short-circuiting을 통해 데이터 처리를 최적화 할 수 있다.
- Stream은 병렬 처리를 손쉽게 지원한다.
Java 8 Optional.
Java 8에서 추가된 기능인 Optional에 대해서 간단하게 소개된 글이 있어서 공유 합니다.
(http://java.dzone.com/articles/java-8-optional-avoid-null-and)
언제 어디서나 우리를 괴롭히던 Null 과 NPE를 물리칠 수 있다는 군요! +ㅅ+
참고 : java8-optional
함수형 프로그래밍 : 위키설명
Java 8 Optional - Null 및 NullPointerException 둘다 회피하고 코드를 예쁘게 짜기.
Null 과 NPE(NullPointerException), 이를 회피하는 방법에 대한 문서들을 여러방법으로 소개되고 있다. 이런 방법들 보다 Java 8에서 제공하는 Optinal 기능을 사용하면 쉽고, 안전하고, 아름다운 코드로 작성할 수 있다. 이 문서는 다른 옵션 값들이나 유틸리티코드 사용 없이 이를 구현하는 방법을 소개한다.
이전 방법
아래 코드를 살펴보자
1.
String unsafeTypeDirName = project.getApplicationType().getTypeDirName();
2.
System.out.println(unsafeTypeDirName);
01.
// 안전하지만 지저분하고 코드가 누락될 수도 있다
02.
if
(project !=
null
) {
03.
ApplicationType applicationType = project.getApplicationType();
04.
if
(applicationType !=
null
) {
05.
String typeDirName = applicationType.getTypeDirName();
06.
if
(typeDirName !=
null
) {
07.
System.out.println(typeDirName);
08.
}
09.
}
10.
}
01.
02.
Optional<Project> optionalProject = Optional.ofNullable(project);
03.
04.
// 안전하고 Java 8을 사용했지만 지저분하고 여전히 코드가 누락될 가능성이 있다
05.
if
(optionalProject.isPresent()) {
06.
ApplicationType applicationType = optionalProject.get().getApplicationType();
07.
Optional<ApplicationType> optionalApplicationType = Optional.ofNullable(applicationType);
08.
if
(optionalApplicationType.isPresent()) {
09.
String typeDirName = optionalApplicationType.get().getTypeDirName();
10.
Optional<String> optionalTypeDirName = Optional.ofNullable(typeDirName);
11.
if
(optionalTypeDirName.isPresent()) {
12.
System.out.println(optionalTypeDirName);
13.
}
14.
}
1.
// 안전하고 깔끔하다
2.
Optional<String> optionalTypeDirName = optionalProject
3.
.map(project -> project.getApplicationType())
4.
.map(applicationType -> applicationType.getTypeDirName());
5.
optionalTypeDirName.ifPresent(typeDirName -> System.out.println(typeDirName));
1.
// 안전하고 여전히 깔끔하다
2.
Optional<String> optionalTypeDirName2 = optionalProject
3.
.map(Project::getApplicationType)
4.
.map(ApplicationType::getTypeDirName);
5.
optionalTypeDirName2.ifPresent(System.out::println);
스프링 시큐리티 java config로 설정 변경 하기.
스프링 버전 : 4.0.0.RELEASE
스프링 시큐리티 버전 : 3.2.5.RELEASE
아직 root-context를 xml 설정을 사용하고 있어 시큐리티 부터 java config를 사용하기 위해 시작.
3년전만해도 스프링 시큐리티 문서 진짜 부족한게 많았는데..
이렇게 좋아 지다니.. 놀랍구나.
기존 XML 설정
<?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd"> <global-method-security pre-post-annotations="enabled" /> <http use-expressions="true" authentication-manager-ref="authManager"> <intercept-url pattern="/index.jsp" access="permitAll" /> <intercept-url pattern="/" access="permitAll" /> <intercept-url pattern="/home" access="permitAll" /> <intercept-url pattern="/favicon.ico" access="permitAll" /> <intercept-url pattern="/resources/**" access="permitAll" /> <intercept-url pattern="/publish/**" access="permitAll" /> <intercept-url pattern="/j_spring_security_logout" access="isAuthenticated()" /> <intercept-url pattern="/user/updateUser" access="isAuthenticated()" /> <intercept-url pattern="/user/info" access="isAuthenticated()" /> <intercept-url pattern="/login/**" access="isAuthenticated()" /> <intercept-url pattern="/gas/**" access="isAuthenticated()" /> <intercept-url pattern="/payment/**" access="isAuthenticated()" /> <intercept-url pattern="/etc/**" access="isAuthenticated()" /> <intercept-url pattern="/stats/**" access="isAuthenticated()" /> <intercept-url pattern="/secure/**" access="isAuthenticated()" /> <intercept-url pattern="/manage/**" access="hasRole('ROLE_ADMIN')" /> <intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')" /> <intercept-url pattern="/comment/admin/**" access="hasRole('ROLE_ADMIN')" /> <form-login login-page="/user/loginForm" authentication-success-handler-ref="loginSuccessHandler" /> <logout logout-url="/logout" logout-success-url="/home" /> </http> <authentication-manager id="authManager"> <authentication-provider user-service-ref="userDetailService"> <password-encoder ref="passwordEncoder" /> </authentication-provider> </authentication-manager> </beans:beans>
거의 기본 설정이라서 아주 심플하다.
딱히 특별한것도 없음..;
Java Config로 설정
@Configuration @EnableWebSecurity public class SecurityWebApplicationInitializer extends WebSecurityConfigurerAdapter { @Autowired private UserDetailService userDetailService; @Autowired private LoginSuccessHandler loginSuccessHandler; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/index.jsp", "/home", "/favicon.ico", "/resources/**", "/publish/**").permitAll() .antMatchers("/secure/**", "/manage/**", "/admin/**", "/comment/admin/**").hasRole("ADMIN") .anyRequest().authenticated() .and() .formLogin() .loginPage("/user/loginForm") .loginProcessingUrl("/j_spring_security_check") .usernameParameter("j_username") .passwordParameter("j_password") .successHandler(loginSuccessHandler) .permitAll() .and() .logout() .logoutUrl("/logout") .logoutSuccessUrl("/home") .and() .csrf().disable() .httpBasic(); } @Override protected UserDetailsService userDetailsService() { return userDetailService; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
위에 링크해둔 스프링 시큐리티 java config 문서에도 친절하게 설명이 잘 나와 있다.
문서 보면서 천천히 해보면 더 좋을 듯.
딱히 특별한건 없고 xml 설정에서 사용하던 authentication-manager를 사용하기 위해서 configre(AuthenticationmanagerBuilder auth)를 세팅했다.
.formLogin() 부분에 보면 processingUrl이랑 username, password 파라미터를 세팅하도록 되어 있는데 이게 원래 기본으로 설정되어 있던거였는데 안되더라..
HttpSecurity.java 의 formLogin()을 보면 FormLoginConfigurer를 사용하는데 여기서 사용하는 default 값이 "username", "password" 이 두가지로 세팅되어 있다.
UsernamePasswordAuthenticationFilter.java 에선 여전히 "j_username", "j_password"를 사용하고 있어서 위처럼 값을 세팅해줘야 사용 가능하다.
그리고 .csrf() 이 설정은 CSRF 공격을 막기위해 세팅되어있는데 .disable()을 세팅해 놓지 않으면 해당 작업을 수행하기 위한 파라미터가 없다고 에러가 발생하기 때문에 .disable()을 해주도록 한다.
csrf().disable()을 안해주면 만나는 에러
HTTP Status 403 - Expected CSRF token not found. Has your session expired?
이렇게 설정을 바꿈으로서 xml 설정보다 코드를 약간 더 줄일 수 있었고, 조금더 읽기 쉬운 코드가 됬다.