Clean Code that Works.

로드-커팅 문제(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 되는 기분이 들었다.


신고

Comment +0

이전글에 이어 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 : 중간 작업으로 "map" 과 "flatten"을 섞은 것이다
  • 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는 이름 그림으로 설명한 것이다.


Creating your own collector.
지금까지 본 모든 collector 들은 java.util.stream.Collector 인터페이스를 구현한 것이다. 만약 자신만의 collector를 구현하고 싶으면 이 인터페이스를 상속 받아서 만들 면 된다.

결론

이 문서에서는 Stream API의 두가지 쓸만한 기능인 flatMap과 collect에 대해서 살펴봤다. 이를 사용해서 데이터 처리 작업을 간결한 코드로 할 수 있었다.
특 히 collect 메서드는 summarizing, grouping, partitioning을 쉽게 만들 수 있고 이를 서로 조합해서 "각 도시의 통화별 거래내역의 합계를 가지고 있는 두 단계 깊이의 Map 처럼 유용한 쿼리를 만들 수도 있다.

이 문서에 collector들에 대해 전부 알아보지는 않았기 때문에 다른 Collectors들(mapping(), joining(), collecting AndThen()) 같은 것들도 살펴보면 쓸만할 것이다.


신고

Comment +0

이번에는 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 이하에서의 처리 방법

List groceryTransactions = 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 에서의 처리 방법

List transactionsIds = 
    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을 가져오고 그 다음에 여러가지 기능(filtersortedmap,collect)를 체인 처럼 엮어서 데이터를 처리하는 쿼리처럼 보이게 만든다.


Figure 1

병렬 처리 코드 작성은 Java SE 8에서 매우 쉽게할 수 있다. Listing 3. 처럼 그냥 stream()을 parallelStream()으로 변경만 하면 된다. Stream API는 내부적으로 멀티코어로 동작하도록 처리한다.


Listing 3. 

List transactionsIds = 
    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 같은 처리를 지원하고 함수형 프로그래밍 같은 처리 방법도 지원한다. (filtermap,reducefindmatch, sorted 등)
게다가 Stream은 기존의 컬렉션 처리 방법과 구분되게 하는 기본적인 두가지 특징이 있다.
  • Pipelining : 많은 Stream 기능들이 Stream 자기 자신을 리턴한다. 이 방식은 처리 작업이 체인처럼 연결되어 큰 파이프라인처럼 동작 하도록 한다. 이를 통해 laziness와 short-circuiting 과 같이 최적화 작업을 할 수 있다.
  • Internal iteration : 명시적으로 반복작업을 수행해야 되는 Collection과 비교 하면 Stream 작업은 내부에서 처리된다.
아래 그림(Figure 2) 은 Listing 2에 작성되어 있는 코드 상세한 그림으로 설명한 것이다.
Figure 2

처음 접한 Stream은 list에서 거래내역에 대한 목록을 stream() 메서드를 사용해서 Stream 으로 가져온 것이다. 그 다음은 몇가지 집계 관련된 작업을 진행하는데, filter(주어진 속성에 맞게 필터링), sorted(주어진 compator를 자기고 정렬), map(정보를 추출) 메서드를 사용해서 집계 관련된 처리를 수행한다. collect()를 제외한 모든 메서드들은 stream을 리턴하기 때문에 이들을 서로 연결해서 파이프라인형태로 처리할 수 있다.

이 작업들은 collect()가 호출되기전까지 실제로 실행되지 않는다. collect()는 파이프라인에 연결된 집계 작업을 시작하고 완료되면 결과를 리턴한다(Stream 형태가 아닌, 여기서는 list). collect()에 대해서는 나중에 좀 더 자세히 살펴볼테니 지금 당장 걱정할 필요는 없다. collect() 메서드를 보면 인자값을 가지고 있는것을 볼 수 있는데 이 파라미터를 가지고 리턴 값을 결정 한다. toLIst()는 Stream을 List로 변경하여 리턴해준다.

Stream에서 사용가능한 다른 메서드들에 대해 알아보기 전에 Stream과 Collection간의 개념적으로 다른점을 확인해 보도록 하자.

Stream VS Collections
예전부터 사용했던 Collection 과 이번에 새로 추가된 Stream은 엘리먼트들을 순차적으로 처리하는것과 관련된 인터페이스를 제공한다. 차이점을 간단히 말하면 Collection은 데이터와 관련된 것이고 Stream은 데이터를 계산하는것과 관련된 내용이다.

DVD에 저장된 영화를 예를 들어 생각해보자. 이것들은 데이터들로 구성되어 있기 때문에 Collection으로 볼 수 있다(바이트나 프레임으로 구성된 집합) 같은 영화를 인터넷 스트리밍을 통해서 본다고 가정해보자. 효율적으로 사용자들에게 비디오를 보여주기 위해 사용자가 보고 있는 부분들에 대해 미리 다운로드를 하는게 효율적이다. 이렇게 다운로드를 할 범위를 결정하는 것을 Stream으로 볼 수 있다.

간단하게 말하면 Collection과 Stream의 차이점은 무언가를 계산할때 라고 볼 수 있다 Collection은 메모리에 저장되는 데이터 구조고 Stream은 이를 계산해서 새로운 데이터를 만들어 낼 수 있다.

Collection 인터페이스를 사용하기 위해서는 사용자에 의해 정의된 반복문이 필요한데(for loop, foreach 등) 이를 외부 반복이라고 부른다.

이와 다르게 Stream은 내부 반복을 사용한다(내부적으로 반복작업을 수행하고 결과값을 어딘가에 저장해둔다) 사용자는 결과값을 가지고 무엇을 할 것인지만 정의하면 된다. 아래 코드는 두가지 차이점을 보여 준다.

Listing 4. 컬렉션에서 외부 반복작업

List<String> transactionIds = new ArrayList<>(); 
for(Transaction t: transactions){
    transactionIds.add(t.getId()); 
}


Listing 5. Stream의 내부 반복작업

List<Integer> transactionIds = 
    transactions.stream()
        .map(Transaction::getId)
        .collect(toList());


Listing 4는 명시적으로 반복문을 수행하고 거래 ID를 추출하여 리스트에 추가 하고 있다. 이와 다르게 Stream은 명시적인 반복문이 없다. Listing 5처럼 질의문의 만드는데 map은 거래 ID를 추출하고 collect는 Stream을 list로 변환해서 리턴해준다.

이제 Stream이 무엇이고 이를 가지고 할 수 있는게 무엇인지 감이 슬슬 올 것이다. Stream이 제공하는 다른 기능들에 대해서 살펴보고 실제 서비스에서 사용될만한 데이터 처리 쿼리를 작성해보자.

Stream Operations : Exploiting Streams to Process Data
Stream 인터페이스는 java.util.stream.Stream에 정의되어 있고 두가지 종류로 분류할 수 있는 많은 기능을 제공한다. Figure 1에서 봤던것 처럼 아래와 같은 기능들을 볼 수 있다.
  • filtersortedmap은 파이프라인처럼 서로 연결시킬 수 있다.
  • collect는 파이프라인을 종료 시키고 결과를 리턴한다.
Stream 처리는 중간 작업(intermediate operations)라고 불리는 것을 통해 서로 연결 될 수 있다. 이들의 리턴타입은 stream이기 때문에 서로 연결될 수 있다. 이 작업은 종료 작업(terminal operations)를 통해 작업을 종료할 수 있다. 처리된 결과를 ListInteger나 void 형태로 받을 수 있다.

이를 구분해서 처리하는 것이 왜 중요한지 의문이 생길수도 있다. 중간 작업들을 Stream 파이프라인에서 종료 작업이 호출되기 전에는 절대 수행되지 않는다. 이것을 Lazy라고 한다. 
중간 작업들은 보통 병합(merged)를 통해 데이터를 처리하고 종료 작업에 이를 전달해 준다.

Listing 6.
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());

Listing 6의 코드는 주어진 숫자 목록에서 나머지가 0인 값들을 계산하는 코드이다.
아래 출력된 내용을 보면 예상과 다른 것을 볼 수 있다.

filtering 1
filtering 2
mapping 2
filtering 3
filtering 4
mapping 4

이와같은 내용으로 출력되는 원인은 limit(2)를 범위를 제한 했기 때문이다. 가끔 Stream에서 전체가 아닌 특정 부분에 대한 처리를 해야될 필요가 있다. 

지금까지 배운 Stream을 사용하는 일반적인 내용을 요약하면 3가지로 요약할 수 있다.
  • 질의를 할 데이터소스(Collection)같은게 필요하다
  • Stream 파이프라인을 형성하는 중간 작업
  • Stream 파이프라인을 실행하고 결과를 리턴하는 종료 작업
이제 Stream에서 사용할 수 있는 기능들을 살펴보자. 이에 대한 전체 목록은 java.util.stream 패키지를 참조 하라.

Filtering : Stream에는 필터 처리를 제공하는 여러가지 메서드들이 있다.
  • filter(Predicated) : 주어진 predicate(java.util.function.Predicate)와 일치하는 stream을 리턴한다.
  • distinct : 중복을 제거한 유니크 엘리먼트를 리턴한다.(stream에 포함된 엘리먼트들의 equals()구현에 따라 구분된다.
  • limit(n) : 주어진 사이즈(n)에 까지의 stream을 리턴한다.
  • skip(n) : 주어진 엘리먼트 길이 까지 제외한 stream을 리턴한다.
Finding and matching : 가장 일반적인 데이터 처리 패턴은 주어진 조건에 일치하는 것을 찾아 내는 것이다. anyMatch,allMatchnoneMatch와 같은 기능을 사용하여 필요한 것을 찾을 수 있다. 이들을 전부 predicate를 인수로 받아서 결과를 boolean형태로 리턴한다. 에를 들어 Stream에 포함된 엘리먼트들이 모두 100보다 큰지 확인하고 싶으면 아래 코드처럼 allMatch를 사용하면 된다.

Listing 7.
boolean expensive =
    transactions.stream()
        .allMatch(t -> t.getValue() > 100);

Stream은 임의의 요소를 찾는 findFirstfindAny 기능도 제공한다. 이 기능은 필터와 같이 Stream을 조작하는데 사용될 수 있다.FindFirstFindAny 값은 Optional 객체를 리턴한다.

Listing 8.
Optional<Transaction> = 
    transactions.stream()
        .filter(t -> t.getType() == Transaction.GROCERY)
        .findAny();

Optional<T> 클래스(java.util.Optional)는 값이 존재 하거나 존재하지 않을 경우에 사용할 수 있는 컨데이터다. Listing 8의 크도를 보면 Transaction 타입이 GROCERY가 아닌것이 있을 수도 있다. Optional 클래스는 엘리먼트들이 존재하는지 확인하는 여러 메서드를 가지고 있다. 예를 들어 만약 엘리먼트가 존재 한다면 ifPresent 메서드를 사용해서 특정 작업을 수행할 수 있다.

Listing 9.
transactions.stream()
    .filter(t -> t.getType() == Transaction.GROCERY)
    .findAny()
    .ifPresent(System.out::println);

Mapping : map 기능을 사용해서 인자(java.util.function.Function)로 전달받은 값들을 가지고 새로운 stream을 만들 수 있다. 

예를 들어 Stream에 포함된 엘리먼트들에 대한 특정 정보만 필요할 수도 있다. Listin 10 처럼 List에 저장된 단어의 길이들을 가지고 있는 새로운 List를 생성할 수 있다. 

Listing 10.
List<String> words = Arrays.asList("Oracle", "Java", "Magazine");
List<Integer> wordLengths = words.stream()
    .map(String::length)
    .collect(toList());

Recuding : 지금까지 본 stream의 종료작업은 boolean(allMatch 등), void(forEach), optional(findAny 등) 오브젝트들을 결과로 리턴했고 collect를 사용해서 List형태로도 리턴을 할 수 있었다.

이외에도 "가장 큰 ID를 가진 거래내역은 무엇인가", "모든 거래 내역의 합계" 처럼 Stream의 엘리먼트들에 복잡한 형태의 공식같은걸 적용하는 처리 쿼르를 만들수도 있다. 이 기능은 Stream의 reduce를 통해서 사용한데 이는 결과가 생성될때 까지 반복적으로 각 요소에 대한 (두 숫자를 추가 한다든지)동작을 허용한다.
이것을 함수형 프로그래밍에서는 고차 함수(fold operation)라고 부른다.

코드들을 보면 좀 더 쉽게 이해할 수 있을 것이다.
for 반복문을 사용해서 덧셈을 할때는 아래와 같은 코드로 작성된다.
int sum = 0;
for (int x : numbers) {
    sum += x; 
}

각각 엘리먼트드을 순회하면서 값들을 더하도록 되어 있다. 이 코드에는 두개의 파라미터가 있다고 볼 수 있는데 sum( 여기서는 0)은 값을 표현하는 것이고, 각 엘리먼트들을 더하귀 위한 것(여기서는 +)가 있다.

Stream에서 reduce 메서드를 사용한다면 엘리먼트들의 합은 Listing 11과 같은 코드로 표현된다.
reduce메서드는 두개의 인자값을 가진다. 
Listing 11.
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
  • 초기 화 값, 0
  • BinaryOperator<T>, 두개의 엘리먼트들을 더해서 새로운 값을 생성
reduce는 반복 적용 패턴을 추상화 해준다. 모든 값을 곱한다던지 최대값을 구하는 것들은 아래처럼 표현될 수 있다.
Listing12.
int product = numbers.stream().reduce(1, (a, b) -> a * b);
int product = numbers.stream().reduce(1, Integer::max);

Numeric Streams 
reduce 메서드를 사용해서 Stream에 있는 숫자형 값들에 대한 합계를 구할 수 있다. 하지만 이를 계산하는데 오브젝트를 Integer 형태로 Boxing 처리를 해야 하기 때문에 어느 정도 Cost가 든다. 아래 코드 처럼 명시적으로 sum() 메서드를 호출하는 것이 좀 더 좋아 보인다.

Listing 13.
int statement = 
    transactions.stream()
        .map(Transaction::getValue)
        .sum(); // error since Stream has no sum method

위와 같은 기능을 제공하기 위해 Stream에서는 3가지 기본형태의 Stream 인터페이스를 제공한다. IntStreamDoubleStream,LongStream 이 Stream들은 intdoublelong 형태의 Stream을 처리 하도록 되어 있다.

가장 빈번히 사용될 메서드는 아마 mapToIntmapToDoublemapToLong일 것이다 이 3가지 메서드들을 이전에 보았던 map과 동일한 동작을 하지만 이것의 리턴값은 Stream<T> 형태의 값을 준다. 예를 들어 Listing 13의 코드는 Listing 14처럼 수정할 수 있다. 

Listing 14.
int statementSum = 
    transactions.stream()
        .mapToInt(Transaction::getValue)
        .sum(); // works!

마지막으로 숫자형 Stream의 다른 활용 방법은 숫자 범위를 지정할 수 있는데 있다. 예를 들어 1에서 100사이의 값을 생성하려고 할때 range와 rangeClosed 메서드를 IntStreamDoubleSteamLongStream에서 사용할 수 있다.

이 두 메서드는 첫번째 인자와 마지막 인자사이의 값을 가져온다. range는 마지막 값을 제외하고 계산하고 rangeClosed는 이를 포함해서 계산한다. Listing 15에서 rangeClosed를 사용해서 10과 30사의 홀수를 가져오는 것을 볼 수 있다.

Listing 15.
IntStream oddNumbers = 
    IntStream.rangeClosed(10, 30)
        .filter(n -> n % 2 == 1);

Building Streams
Stream을 생성하는 몇가지 방법들이 있는데 지금까지는 컬렉션을 통해서 Stream을 생성했었고, 숫자를 사용해서 만들기도 했다. Stream은 값 목록이나 배열, 파일과 같은곳에서도 생성할 수 있으며 무한 Stream을 생성하기 위해 함수를 사용할 수도 있다.

값 목록이나 배열에서 Stream을 생성하는 것은 어렵지 않다. 스태틱 메서드인 Stream.of를 사용하거나 Arrays.Stream을 사용하면 된다.

Listing 16.
Stream<Integer> numbersFromValues = Stream.of(1, 2, 3, 4);
int[] numbers = {1, 2, 3, 4};
IntStream numbersFromArray = Arrays.stream(numbers);

파일에서 Stream을 생성할 경우 Files.Lines를 사용하면 되고, 아래 코드는 파일에서 라인수를 카운팅한다.

Listing 17.
long numberOfLines = 
    Files.lines(Paths.get(“yourFile.txt”), Charset.defaultCharset())
    .count();

Infinite streams : 지금까지 Stream이 무엇이고 어떻게 사용하는지에 대해서 이래했을 것이다. 함수형 프로그래밍에서 Stream을 생성하는 방법은 Stream.iterate와 Stream.generate가 있다. 엘리먼트들이 필요한 시점에 계산되기 때문에 이 두 함수는 지속적으로 엘리먼트들을 가져 올 수 있다. 이것이 바로 무한 Stream이다(고정된 사이즈의 Stream이 아닌)

Listing 18은 iterate를 사용해서 모든 숫자에 10을 더하는 기능이다. iterate메서드는 초기값 0부터 시작해서 계속 새로운 값을 생성한다. 

Listing 18.
Stream<Integer> numbers = Stream.iterate(0, n -> n + 10);

limit()을 사용해서 무한 Stream에서 고정된 크기의 Stream을 생성할 수 있다. Listing 19는 Stream의 사이즈를 5로 고정한 것을 보여주는 코드이다.

Listing 19.
numbers.limit(5).forEach(System.out::println); // 0, 10, 20, 30, 40

결론.
Java SE 8에서는 빠르고 정교한 데이터 처리를 위해 Stream API를 공개했다. 이 문서에서는 Stream이 제공하는 많은 기능들(filter,mapreduceiterate)을 살펴 봤고 이를 활용해서 간결한 코드로 정교한 데이터 처리 작업을 할 수 있었다. 이 방법은 Java SE 8 이전에 Collection 데이터들을 처리하는 방법과 매우 다르지만 많은 장점이 있다.
  1. Steream API는 몇가지 기능들을 활요해서 laziness 및 short-circuiting을 통해 데이터 처리를 최적화 할 수 있다.
  2. Stream은 병렬 처리를 손쉽게 지원한다.
다음 문서에서는 Stream의 filterMap이나 collect 같은 기능에 대하여 좀 더 알아볼 것이다.


신고

1

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);

만약 위 코드에서 사용하는 변수들이 null 이면 NPE가 발생된다.

이를 회피 하기 위해서 아래와 같은 코드를 작성했었다. 

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.}

NPE를 발생 시키지는 않지만 코드가 지저분하고 null 체크가 누락되기 쉽다.

Java 8
Java 8에서 제공하는 Optional 기능을 사용해서 재구현해보자

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.}

코드가 늘어나고 더 개선된 내용이 없어 보인다. 

Functional Interface를 사용해 Optional의 진짜 힘을 사용해보자!

1.// 안전하고 깔끔하다
2.Optional<String> optionalTypeDirName = optionalProject
3..map(project -> project.getApplicationType())
4..map(applicationType -> applicationType.getTypeDirName());
5.optionalTypeDirName.ifPresent(typeDirName -> System.out.println(typeDirName));

map()은 한상 Optional 객체를 리턴하기 때문에 Null은 허용되지 않고 
IfPresent()는 해당 객체에 값이 있어야 실행되고 기본값 같은것은 전혀 세팅되어 있지 않다.

위 코드를 조금 더 줄여 보면 아래와 같이 작성 할 수있다.

1.// 안전하고 여전히 깔끔하다
2.Optional<String> optionalTypeDirName2 = optionalProject
3..map(Project::getApplicationType)
4..map(ApplicationType::getTypeDirName);
5.optionalTypeDirName2.ifPresent(System.out::println);

결론
Optional을 사용한다면 null 관련된 작업 및 이를 회피하기 위한 작업을 진행 별도로 하지 않아도 된다. 
레거시 코드에서 사용되는 값들이 Null이 될 수 있다면 가능한 빨리 Optional 기능을 사용하길 바란다. 


신고

Comment +0

Java 8이 릴리즈 되면서 새로운 Date, Time API도 같이 포함되어 릴리즈 되었다.

아래 링크에 보면 기존 Java의 Date, Time의 문제점에 대해서 잘 설명되어 있다. 한번씩 읽어 보면 좋을 듯 하다.
이런 저런 문제점이 있지만, 그냥 사용하고 있는데가 많다. 나도 그렇고.
정상혁님이 정리한 Java 날짜 및 시간(http://helloworld.naver.com/helloworld/textyle/645609 )

이 내용은 이 아티클(http://www.oracle.com/technetwork/articles/java/jf14-date-time-2125367.html)을 허접하게 번역한 문서이다.

===============================================================================================================

왜 새로운 Date, Time API가 필요할까?

오랜 시간동안 Java에서 제공하는 Date, Time API는 부족한 기능 지원을 포함한 여러가지 문제점을 가지고 있었다.
예를 들어 java.util.Date와 SimpleDateFormatter는 Thread-Safe 하지 않아서 잠재적인 동시성 문제를 가지고 있다. 또한 몇몇 클래스들은 형편없이 디자인 되어있는데 java.util.Date 는 1900년도 부터 시작한다거나 월(month)는 1부터 시작 하지만 일(day)는 0부터 시작하는 등 매우 직관적이지 않도록 설계되어 있다.

이런 이슈들을 포함한 다양한 원인들로 인해 몇몇 개발자들은 Joda-Time 같은 써드파티 라이브러리를 사용 하기도 한다. 
JDK 코어에서 이런 문제점들을 해결하고 더 좋고 직관적인 API들을 제공하기 위해 새롭게 재 디자인한  Date, Time API를 Java SE 8부터 제공 하기로 했다.

Joda-Time의 저작자인 Stephen Colebourne 및 오라클의 주도 아래 JSR 310 표준에 맞춰 Java SE 8의 java.time 패키지에 새로운 API를 포함하여 릴리즈 했다.

핵심 아이디어들

새로운 API들은 아래 3가지 핵심 아이디어들을 따르고 있다.

Immutable-value classes(불변 클래스) : 기존 Java에서의 formatter가 가지고 있는 심각한 취약점 중에 하나는 Thread-safe하지 않다는 것이다. 이 문제점은 개발자들에게 formatter를 사용할 때 날짜와 관련된 내용 뿐만 아니라 동시성에 관련된 내용도 함께 생각해야 된다는 문제점이 있다. 새롭게 제공되는 API는 이러한 문제점을 회피하기 위해 핵심 클래스들을 불변 클래스로 만들고 잘 정의된 값을 제공 한다.

도메인 주도 개발 : 새로운 API 모델은 Date, Time을 사용할때 분명하게 다른 방법을 제공하도록 디자인됬다. 이 차이점은 이전 Java 버전에서는 명확하지 않게 정의 되어 있는데 예를 들어 java.util.Date 는 timeline(UNIX 시간을 기준으로 밀리세컨드)을 제공하지만 만약 toString을 호출하면 해당 timezone에 해당하는 결과를 표시함으로써 개발자들을 햇갈리게 한다. 도메인 주도 개발은 장기적으로 봤을때 명확하고 이해하기 쉽도록 해 주지만 기존 애플리케이션에 새로운 API를 제공할 때 기존 Date model과 비교해서 잘 사용할 수 있도록 해야 한다.

연대의 분리 : 새로운 API는  개발자들이 ISO-8601 표준을 사용하도록 강제 하지 않고 일본이나 태국처럼 세계 어디서나 서로 다른 시간대를 가지고 사용할 수 있도록 해준다. 이것은 표준 연대에서 작업해야되는 개발자들에게 다른 추가 부담을 주지 않고 개발에 집중 할 수 있도록 해준다.

LocalDate 와 LocalTime

새로운 API를 처음 접할때 만나게 되는것은 아마도 LocalDate와 LocalTime 클래스일 것이다. 이들은 Date와 Time에 관련된 내용을 제공하고 당신이 사용중인 달력과 시계와 동일하게 동작한다. LocalDateTime 이라는 LocalDate와 LocalTime의 복합 클래스도 존재한다.

시간대는 사람마다 다르게 적용되는데 시간대를 고정해서 사용할 필요가 없다면 Local 클래스를 사용하면 된다. 예를 들어 JavaFx 데스크탑 애플리케이션을 들 수 있는데, 이 애플리케이션에서 사용되는 시간에 일관성있는 시간을 제공할 수 있다.

Creating Objects

새로운 API의 핵심 클래스는 오브젝트를 생성하기 위해 다양한 factory 메서드를 사용한다. 오브젝트 자기 자신의 특정 요소를 가지고 오브젝트를 생성할 경우 of 메서드를 호출하면 되고 다른 타입으로 변경할 경우에는 from 메서드를 호출하면 된다. 이들은 서로 짝을 이루고 String 값을 파라미터로 받는다.

  1. LocalDateTime timePoint = LocalDateTime.now(); // 현재의 날짜와 시간
  2. LocalDate.of(2012, Month.DECEMBER, 12); // 2012-12-12 from values
  3. LocalDate.ofEpochDay(150); // 1970-05-31 middle of 1970
  4. LocalTime.of(17, 18); // 17:18 (17시 18분)the train I took home today
  5. LocalTime.parse("10:15:30"); // From a String

자바에서 사용되는 표준 Getter 방식에 따라 아래와 같이 사용할 수 있다.

  1. LocalDate theDate = timePoint.toLocalDate();
  2. Month month = timePoint.getMonth();
  3. int day = timePoint.getDayOfMonth();
  4. timePoint.getSecond();

날짜를 변경할 때 오브젝트의 값을 변경해야 되는데 신규 API는 불변 클래스이기 때문에 사용할때 새로운 오브젝트로 생성되서 값을 리턴해준다. 아래 setter 사용하는것을 참조 하라.
서로 다른 값들을 가지고 날짜를 수정하는 방법역시 제공한다.

  1. // 2010-08-10으로 세팅된 신규 Object를 리턴
  2. LocalDateTime thePast = timePoint.withDayOfMonth(10).withYear(2010);
  3. // 2010-08-10에서 3주를 더하고 또 3주를 더한 2010-09-21을 리턴
  4. LocalDateTime yetAnother = thePast.plusWeeks(3).plus(3, ChronoUnit.WEEKS);

신규 API는 adjuster(일반적인 로직을 감싸서 사용되는 코드 블록)의 개념도 가지고 있다. 하나 이상의 필드를 세팅할때 with를 사용하고 날짜나 시간을 더하거나 뺄때 plus를 사용할 수 있다. 값 객체는 조정자 처럼 동작할 수 있는데 객체 자신이 가지고 있는 값을 가지고 갱신한다. API에 정의된 내장 adjuster들도 있지만 사용자가 로직을 정의한 adjuster도 만들어서 사용할 수 있다.

  1. import static java.time.temporal.TemporalAdjusters.lastDayOfMonth;
  2. import static java.time.temporal.TemporalAdjusters.next;
  3. import static java.time.DayOfWeek.*;
  4. LocalDateTime timePoint = LocalDateTime.now()
  5. foo = timePoint.with(lastDayOfMonth()); // 2014-08-31
  6. bar = timePoint.with(previousOrSame(ChronoUnit.WEDNESDAY)); // 2014-08-27
  7. // Using value classes as adjusters, 2014-08-26
  8. timePoint.with(LocalTime.now());

Truncation(날짜 자르기)

신규 API는 날짜, 시간, 일 등에 해당하는 값 들을 잘라내서 표현할 수 있도록 truncatedTo 라는 메서드를 제공한다.

  1. // 20:39:54.073, 20시 39분 54.073초
  2. LocalTime truncatedTime = LocalTime.now();
  3. // 20:39:54
  4. truncatedTime.truncatedTo(ChronoUnit.SECONDS);
  5. // 20:38
  6. truncatedTime.truncatedTo(ChronoUnit.MINUTES);

Time Zone Classes

ZonedDateTime은 아래 예 처럼 완벽하게 기술된 날짜와 시간이다. 특정 서버의 컨텍스트에 의존하지 않고 날짜와 시간을 표현하고 싶다면 이 클래스를 사용하면 된다.

  1. ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]");

OffeSetDateTime은 data를 직렬화 하여 DB에 넣거나 만약 서버가 다른 시간대를 사용한다면 직렬화된 형태로 logging time stamp를 표현하는데 사용할 수 있다.

OffsetTime은 아래와 같은 offset을 해결해준다.

  1. ZoneOffset offset = ZoneOffset.of("+1");
  2. OffsetTime time = OffsetTime.now();
  3. // changes offset, while keeping the same point on the timeline
  4. OffsetTime sameTimeDifferentOffset = time.withOffsetSameInstant(offset);
  5. // changes the offset, and updates the point on the timeline
  6. OffsetTime changeTimeWithNewOffset = time.withOffsetSameLocal(offset);
  7. // Can also create new object with altered fields as before
  8. changeTimeWithNewOffset.withHour(3).plusSeconds(2);

기존 자바에서 사용했던 java.util.TimeZone이 있지만 JSR 310를 구현한 클래스들은 전부 immutable 이지만 기존 time zone은 mutable이기 때문에 Java SE 8에서는 사용되지 않는다.


Periods

Period를 사용해서 "3달 하고 1일 뒤"같은 표현을 사용할 수 있다. 이는 타임라인을 사용하는 개념으로 지금까지 살펴봣던 클래스들과는 약간 다르다.

  1. // 2014-08-26
  2. LocalDate oldDate = LocalDate.now();
  3. ZonedDateTime oldDateTime = ZonedDateTime.now();
  4. // 2년2개월1일
  5. Period period = Period.of(2, 2, 1);
  6. // 2년2개월1일뒤, 2016-10-27
  7. LocalDate newDate = oldDate.plus(period);
  8. // 2년2개월1일전, 2012-06-25
  9. ZonedDateTime newDateTime = oldDateTime.minus(period);


Durations

Duration은 시간의 관점에서 측정된 타임라인으로 Period와 유사하지만 서로 다른 정밀도를 보여준다.

  1. Instant firstInstant= Instant.ofEpochSecond(1294881180 ); // 2011-01-13 01:13
  2. Instant secondInstant = Instant.ofEpochSecond(1294708260); // 2011-01-11 01:11
  3. Duration between = Duration.between(firstInstant, secondInstant);
  4. // negative because firstInstant is after secondInstant (-172920)
  5. long seconds = between.getSeconds();
  6. // get absolute result in minutes (2882)
  7. long absoluteResult = between.abs().toMinutes();
  8. // two hours in seconds (7200)
  9. long twoHoursInSeconds = Duration.ofHours(2).getSeconds();

plus, minus, with 기능을 사용할 수 있고 날짜나 시간 값도 수정 할 수 있다.


The Rest of the API

다른 일반적인 상황들에서 사용할만한 클래스들도 몇가지 지원 하는데

MonthDay 클래스는 Month와 Day가 짝을 이루고 있는것으로 생일을 표현할때 유용하다.
YearMonth 클래스는 신용카드의 유효기간 처럼 월과 달로 구성된 것에 사용하는데 적합하다.

Java 8의 JDBC는 새로 추가된 타입들을 지원 하지만 JDBC API의 공개적인 변경점은 없다. 기존에 존재 했던 generic setObject, getObject로 충분히 커버 가능하다.
아래 ANSI SQL의 타입이나 vendor들이 정의한 클래스는 아래 처럼 매핑된다.

ANSI SQLJava SE 8
DATELocalDate
TIMELocalTime
TIMESTAMPLocalDateTime
TIME WITH TIMEZONEOffsetTime
TIMESTAMP WITH TIMEZONEOffsetDateTime

결론
Java SE 8은 java.time 패키지에 포함된 새로운 날짜 와 시간 API를 장착했고 이를 통해 개발자에게 기존에 비해 훨씬 더 훌륭한 안정성과 기능을 제공한다.
새로운 API는 다양한 사용사례를 포함하고 폭넓게 사용될 수 있도록 잘 구성되어 있다.


신고

Comment +0

JDK 관련해서 거의 안봤었는데.. Java 8 출시되고 나고 여러가지 좋은 기능들이 많아서 천천히 보는중.

Java 7까지 PermGen(Permanent Generation)이 있었는데 Java 8이 릴리즈 되면서 PermGen이 완전히 제거 되고 Metaspace라는 네이티브 메모리 영역으로 클래스 메타데이터들이 다 이동하게됬다.


PermGen은 무엇인가?

Permanent Generation은 힙 메모리 영역중에 하나로 자바 애플리케이션을 실행할때 클래스의 메타데이터를 저장하는 영역이다.(Java 7기준) 

자바 개발자라면 OutOfMemoryError: PermGen Space error이 발생했던것을 본적이 있을텐데 이는 Permanent Generation 영역이 꽉 찼을때 발생하고 메모리 누수가 발생했을때 생기게 된다. 메모리 누수의 가장 흔한 이유중에 하나로 메모리에 로딩된 클래스와 클래스 로더가 종료될때 이것들이 가비지 컬렉션이 되지 않을때 발생한다.


Java 7 부터 PermGen을 제거하기 위한 준비를 시작

Java 7 부터 Permanent Generation 을 제거 하기 위해 노력했고 아래와 같은 것을이 Java Heap 이나 native heap 영역으로 이동했다.

  • Symbols 는 native heap
  • Interned String(중복된 문자를 상수화) 는 Java Heap으로 
  • Class statics 는 Java Heap으로 

Java 7의 Metaspace

PermGen은 Java 8부터 Metaspace로 완벽하게 대체 되었고 Metaspace는 클래스 메타 데이터를 native 메모리에 저장하고 메모리가 부족할 경우 이를 자동으로 늘려준다. Java 8의 장정줌에 하나로 OutOfMemoryError: PermGen Space error는 더이상 볼 수 없고 JVM 옵션으로 사용했던 PermSize 와 MaxPermSize는 더이상 사용할 필요가 없다. 이 대신에 MetaspaceSize 및 MaxMetaspaceSize가 새롭게 사용되게 되었다. 이 두 값은 Metaspace의 기본 값을 변경하고 최대값을 제한 할 수 있다.


MetaspaceSize 

이 설정은 JVM이 사용하는 네이티브 메모리양을 변경하는데 사용된다. 시스템에서 기본으로 제공되는 것보다 더 많은 메모리를 사용할 것이라고 확신할 경우 이 옵션을 사용하면 된다. 

MaxMetaspaceSize

이 설정은 metaspace의 최대 메모리 양을 변경하는데 사용된다. 애플리케이션을 서버에서 동작시킬때 메모리 영역을 조절하고 싶거나 메모리 누수가 발생해서 시스템 전체의 네이티브 메모리를 사용해 버리지 않도록 하기 위해서 사용하면 된다. 만약 native 메모리가 꽉 찾는데도 애플리케이션이 메모리를 더 요구 한다면 java.lang.OutOfMemoryError: Metadata space가 발생한다.


장점

PermGen 영역이 삭제되어 heap 영역에서 사용할 수 있는 메모리가 늘어났다.
PermGen 영역을 삭제하기 위해 존재했던 여러 복잡한 코드들이 삭제 되고 PermGen영역을 스캔 하기 위해 소모되었던 시간이 감소되어 GC 성능이 향상 되었다.


참고 : http://techidiocy.com/metaspace-java8/, http://java.dzone.com/articles/java-8-permgen-metaspace, http://openjdk.java.net/jeps/122


신고

Comment +0

http://www.javacodegeeks.com/2013/02/40-java-collections-interview-questions-and-answers.html

저기 있는거 번역..
맨날 HashMap하고 ArrayList 만 쓰다 보면, 컬렉션 프레임워크가 뭔지 잊어 버릴때가...-_ -;;


======================================================================================================================

1. 자바 컬렉션 프레임워크는 무었이고, 이들을 사용하므로서 얻는 이득은 무엇인가?
  컬렉션은 대부분의 프로그래밍 언어에서 사용되고 있고 초창기 자바에서는 VectorStackHashtableArray만 제공되고 있었다. 자바 1, 2가 릴리즈 되면서 컬렉션 프레임워크들은 colletions 인터페이스를 구현하고 해당 알고리즘도 구현되었다. 자바 컬렉션들은 제너릭스의 활용과 Thread-safe한 기능들까지 제공하고 있다. 
컬렉션 프레임워크를 사용함으로써 얻을 수 있는 이점들을 아래와 같다.
  • 별도로 컬렉션 클래스를 구현하는 것보다 구현되있는것을 사용함으로써 코딩 시간을 감소 시킬 수 있다.
  • 컬렉션 프레임워크들은 잘 테스트 되고 검증되어있기때문에 코드 품질을 보장한다.
  • JDK에 포함된 컬렉션 프레임워크들을 사용하여 코드 유지보수 시간을 감소 시킬 수 있다.
  • 재사용 가능하고 상호 운용성이 보장 된다.

2. 컬렉션 프레임워크에 제너릭스가 도입되면서 생긴 장점은 무엇인가?
  자바 1.5 버전부터 제너릭스가 도입되었다. 제너릭스를 통해 컬렉션 관련 코드를 작성할때 해당 오브젝트의 타입을 지정할 수 있게되었고 이로 인해 잘못된 타입의 오브젝트를 세팅할 경우 컴파일 시점에서 이를 파악할 수 있게 되었다. 이로 인해 런타임시 발생하는 ClassCastException을 컴파일시 찾아 낼 수 있게 되었다. 그리고 제너릭스를 통해 클래스 캐스팅을 하지 않아도 되고 instansof 를 사용하지 않아도 되므로써 코드를 좀 더 깔끔하게 유지할 수 있게 되었다. 

3. 자바 컬렉션 프레임워크의 기본 인터페이스들은 무엇인가?
  • Collection 은 가장 기본이 되는 인터페이스이다. 자바는 이 인터페이스를 직접 구현한 클래스는 아무것도 제공하지 않는다.
  • Set 은 중복을 허용하지 않는 집합이다. 
  • List 는 중복을 허용하고 정렬이 가능한 컬렉션이다. 인덱스를 통해 아무런 엘리먼트나 접근할 수 있고, 길이 조정이 가능한 배열과 비슷하다고 할 수 있다.
  • Map 은 키/값을 가지고 있는 오브젝트다. 키값은 중복되어선 안되고 하나의 키 값은 하나의 값에 매핑된다.
다른 인터페이스들론 Queue, Deque, Iterator, SortedSet, SortedMap, ListIterator가 있다.

4. 왜 컬렉션은 Cloneable 과 Serializable 인터페이스를 상속받지 않았는가?
  컬렉션은 오브젝트들을 묶어서 관리하고 이를 어떻게 유지하는지는 관여하지 않는다. 예를들어 몇몇 컬렉션들은 중복 값을 허용하는 List를 사용하거나 중복 값을 허용하지 않는 Set같은것을 사용한다. 많은 컬렉션 구현체들이 clone 함수를 가지고 잇다. 하지만 이것이 모든 컬렉션이 전부다 clone 을 가지고 있어야 된다는 것을 의미 하지는 않는다. 컬력션은 추상 인터페이스고 실제 구현체에서 어떻게 사용해야 될지를 결정해야 된다.

5. 왜 Map 인터페이스는 컬렉션 인터페이스를 상속받지 않는가?
  Map 인터페이스와 이 구현체들은 컬렉션 프레임워크에 속하지만 Map은 컬렉션이 아니고 컬렉션 역시 Map이 아니다. 
만약 맵이 컬렉션 인터페이스를 상속 받았다고 치면 엘리먼트들은 어떻게 관리해야 될까? 맵은 키-값 을 가지고 있고 컬렉션 처럼 키와 값들을 검색하는 메서드들을 제공한다. 하지만 이 것은 "엘리먼트들의 그룹"이라는 컬렉션 인터페이스의 기본 개념과 맞지 않는다.

6. Iterator는 무엇인가?
  Iterator 인터페이스는 아무 컬렉션이든 반복적으로 수행하기 위한 메서드를 제공한다. iterator 메서드를 통해 컬렉션으로 부터 iterator instance를 가져올 수 있다. Iterator는 자바 컬렉션 프레임워크에서 Enumeration에 속한다. Iterator는 컬렉션을 순회하는 도중에 엘리먼트들을 삭제할 수 있다.

7. Enumeration 과 Iterator 인터페이스의 다른점은 무엇인가?
  Enumeration은 Iterator 보다 두배이상 빠르고 더 작은 메모리를 사용한다. Enumeration은 매우 간단하고 간단한 요구사항에 잘 동작되도록 최적화 되어 있다. 하지만 Iterator는 Enumeration에 비해 더 안전한데 그 이유는 Iterator가 사용될때 대상 컬렉션을 다른 쓰레드에서 접근해서 수정하는것을 막기 때문이다.
Iterator 는 자바 컬렉션 프레임워크의 Enumeration에 포함된다. iterator는 작업을 수행하면서 해당 엘리먼트를 삭제할 수 있지만 Enumeration은 불가능 하고 iterator의 메서드 이름은 기능적으로 명확하게 반복을 한다는 뜻으로 정의 되어있다.

8. 왜 다른 컬렉션 처럼 Iterator.add() 메서드는 없는가?
  이 말은 의미적으로 불명확 한데 iterator에 add를 추가 한다면 이는 반복작업의 순서를 보장하지 않는다. 하지만 ListIterator는 반복 작업을 할때 순서를 보장하기 때문에 add 기능을 제공한다.

9. 왜 Iterator는 Cursor 없이 직접적으로 이동할 수 있는 next 메서드를 제공하지 않는가?
  Iterator 인터페이스에 추가될 수는 있지만 많이 사용되지 않을 것이고, Iterator를 구현하는 클래스마다 이를 만들어 줘야 되기 때문에 제공되지 않는다. 그리고 Iterator(반복)이라는 의미와 맞지도 않는다.

10. Iterator 와 ListIterator의 차이점은 무엇인가?
  • Set과 List에 Iterator를 사용할 수 있지만 ListIterator에는 List만 가능하다.
  • Iterator 는 앞쪽으로 탐색을 하지만 ListITerator는 양방향 순회가 가능한다.
  • ListIterator는 Iterator 인터페이스를 상속받았고 추가적으로 Add, 엘리먼트 교체, 현제 index의 이전, 다음 엘리먼트 가져오기 등 많은 추가 기능을 제공한다.
11. List를 반복할 수 있는 방법은 무엇인가?
  Iterator를 사용하던가 for-each loop 를 사용하는 두가지 방법을 사용해서 List를 반복 할 수 있다.

01List<String> strList = new ArrayList<>();
02//using for-each loop
03for(String obj : strList){
04    System.out.println(obj);
05}
06//using iterator
07Iterator<String> it = strList.iterator();
08while(it.hasNext()){
09    String obj = it.next();
10    System.out.println(obj);
11}

 Iterator를 사용하는게 좀더 thread-safe한데 만약 반복 도중에 엘리먼트가 수정되려고 한다면 ConcurrentModificationException을 발생시킨다.

12. Iterator의 fail-fast에 대해 알고 있는것은 무엇인가?
  Iterator의 fail-fast 속성은 다음 엘리먼트에 접근 하려고 할 때 엘리먼트가 변한것이 있는지 확인하는 것이다. 만약 수정 사항이 발견된다면 ConcurrentModificationException를 발생시킨다. 모든 Iterator의 구현체는 ConcurrentHashMap이나 CopyOnWriteArrayList 동시성 관련된 컬렉션을 제외 하고 처럼 fail-fast를 사용하는 방법으로 디자인 되어 있다.

13. fail-fast 와 fail-safe 의 다른 점은 무엇인가?
  fail-fast를 사용하는 방식의 컬렉션들은 java.util 패키지에 들어가있고 fail-safe는 java.util.concurrent 패키지에 위치하도록 디자인되어 있다. Fail-fast Iterator는 ConcurrentModificationException 을 발생 시키고 fail-safe는 절대로 ConcurrentModificationException를 발생 시키지 않다. 

14. 컬렌션을 순회하는 도중에 ConcurrentModificationException이 발생하는것을 피할려면 어떻게 해야 되는가?
  concurrent 컬렉션을 사용하면 ConcurrentModificationException이 발생하는것을 예방 할 수 있다. ex) ArrayList 대신 CopyOnWriteArrayList를 사용

15. Iterator 인터페이스의 구현체가 없는 이유는 무엇인가?
  Iterator 인터페이스는 컬렉션들을 반복하는데 사용할 메서드들이 정의되어 있지만 실제 구현은 컬렉션의 구현체가 가지고 있다. 모든 컬렉션 클래스들은 순회를 하기 위해 내부에 Iterator를 구현한 코드를 가지고 있다. 이를 통해 iterator가 fail-fail을 사용할지 fail-safe를 사용할지 결정할 수 있도록 한다. ArrayList의 Iterator는 fail-fast이고 CopyOnWriteArrayList의 Iterator는 fail-safe이다.

16. UnsupportedOperationException 은 무엇인가?
  UnsupportedOperationException 은 사용할려는 메서드가 제공되지 않을 때 발생하는 오류이다. JDK 내에서도 넓게 사용되고 있으며 컬렉션 프레임워크에서는 모든 add 및 remove 메서드가 java.util.Collections.UnmodifiableCollection 를 던진다.

17. 자바에서 HashMap은 어떻게 동작하는가?
- HashMap 은 키-값 쌍으로 사용하도록 구현되어 있다. HashMap은 해싱 알고리즘을 사용하고 hashCode()와 equals()를 put() 과 get()을 쓸대 사용한다. 키-값 을 저장하기 위해 put 메서드를 호출 하면 HashMap은 key의 hashCode()를 호출해서 맵에 저장되어 있는 값 중에 동일한 key가 있는지 찾는다. 이 Entry는 LinkedList에 저장되어 있고 만약 존재하는 entry면 equals()메서드를 사용해서 key가 이미 존재 하는지 확인 하고 만약 존재 한다면 value값을 덮어 씌워서 새로운 키-값 으로 저장한다. 키를 가지고 get 메서드를 호출하면 hashCode()를 호출해서 array에서 값을 찾고 equals()메서드를 가지고 찾고자 하는 key와 동일한지 확인한다. 아래 이미지를 보면 명확하게 알 수있다.


HashMap에 대해 알아야할 다른 중요한 것은 capacity, load factor, threshold resizing이다. HashMap은 기본적으로 capacity 는 32, load factor는 0.75로 세팅하고 Threshold는 entry를 추가할 때 마다 capacity에 load factor를 곱한 값이 된다. 만약 map이 크기가 threshold 보다 크면 HashMap은 더 큰 capacity를 사용하도록 맵을 재 해시한다. capacity는 항상 
데이터베이스의 데이터를 캐싱하는 것 같은 많은 수의 key-value 쌍을 저장할때 알아야된다. 이것은 HashMap을 적절한 capacity 와 load factor를 사용해서 초기화 하는 좋은 방법이다.

18. hashCode()와 equals() 메서드의 중요점음 무엇인가?
  HashMap은 Key 오브젝트의 hashCode()와 eqauls()메서드를 사용해서 key-value 값을 저장할 위치를 결졍하고 HashMap에서 값을 꺼내올때도 사용한다. 만약 이 메서드들이 올바르게 구현되지 않았다면 다른 두개의 Key가 같은 hashCode() 및 eqauls() 결과를 내놓을 수 있고 이는 value 값들을 잘못된 의도하지 않은 값으로 덮어 씌울 가능성이 있다. 
equals()와 hashCode()의 구현은 아래 기본룰을 따라야 된다.
  • If o1.equals(o2), then o1.hashCode() == o2.hashCode()should always be true.
  • If o1.hashCode() == o2.hashCode is true, it doesn’t mean that o1.equals(o2) will be true.
19. 아무 클래스나 Map의 Key로 사용할 수 있는가?
 아무 클래스나 사용 가능 하지만 아래 몇몇 주의사항을 따라야 된다.
  • 만약 클래스가 equals()를 overrides 했다면 hashCode() 역시 override 해야 한다.
  • 18번에 언급된 기본 구현 규칙을 따라야 한다.
  • equals() 메서드가 사용되지 않으면 hashCode()도 사용하지 않아야 한다.
  • 가장 좋은 방법은 key 클래스를 불변(immutable)으로 만드것이다. 이렇게 하면 hashCode()값은 캐시되어 빠른 성능을 가진다. 또한 불변 클랙스는는 hashCode() 및 equals()의 값이 변하지 않기 때문에 해당 값이 변해서 생기는 문제들을 해결할 수 있다. 예를 들어 아래 HashMap의 key 로 사용될 MyKey 클래스를 살펴봐라.
01//MyKey name argument passed is used for equals() and hashCode()
02MyKey key = new MyKey('Pankaj'); //assume hashCode=1234
03myHashMap.put(key, 'Value');
04 
05// Below code will change the key hashCode() and equals()
06// but it's location is not changed.
07key.setName('Amit'); //assume new hashCode=7890
08 
09//below will return null, because HashMap will try to look for key
10//in the same index as it was stored but since key is mutated,
11//there will be no match and it will return null.
12

myHashMap.get(new MyKey('Pankaj'));


이런 이유로 인해 대부분 String 이나 Integer 값들을 HashMap의 키로 사용한다.

20. Map 인터페이스가 제공하는 다른 Collection 뷰는 무엇인가?
  Map 인터페이스는 아래 3가지 형태의 collection view 를 제공한다.
  • Set keySet(): 맵에 존재하는 Key 값들을 Set으로 보여준다. 이 set들은 맵과 연결되어 있으며 맵을 바꾸거나 set을 바꾸면 값이 수정 된다. 만약 키 Set을 사용하는중에 map이 변경 되면 Set을 반복할때 나오는 결과값은 undefined 되게 된다. Set은 엘리먼트들을 지울 수 있고 이에 대응하는 값은 맵에서 삭제 된다.(remove, Set.remove, removeAll, retaionAll, clear) add 나 addAll같은 기능은 제공하지 않는다.
  • Collection values() : 맵에 존재하는 Value 들을 컬렉션 형태로 보여준다. 이것 역시 맵과 연동되어 있으며 collection을 수정 하면 map의 값이 수정된다. 
  • Set<Map.Entry<K, V>> entrySet() : 맵의 entry 들을 Set 형태로 보여준다.
21. HashMap과 Hashtable의 차이점은 무엇인가?
  HashMap과 Hashtable은 둘다 Map 인터페이스를 구현하고 있어서 비슷해 보이지만 아래와 같은 차이점이 존재한다.
  • HashMap은 키/값에 null을 허용하는 반면 Hashtable은 이를 허용하지 않는다. 
  • Hashtable은 synchronized (synchronized) 되어 있지만 HashMap 은 그렇지 않다. 그래서 HashMap 은 단일 스레드 환경에서 더 좋은 퍼포먼스를 보여준다. 반면, Hashtable은 멀티 스레드 환경에 적합하다. 
  • LinkedHashMap 은 자바 1.4에서 HashMap의 서브클래스로 소개되었다. 그렇기 때문에 iteration 의 순서를 보장받고 싶다면,  HashMap에서 LinkedHashMap으로쉽게 변경 가능하다. 그러나 Hashtable 에서는 그럴 수 없으므로 iteration 순서를 예측할 수 없다.  
  • HashMap은 iterator 키 셋을 제공하므로 fail-fast (12 참고) 기능을 사용하나 Hashtable은 Enumeration 키를 사용하므로 이런 기능을 제공하지 못한다. 
  • Hashtable은 legacy 클래스로 취급을 받기 때문에 만약 Map에서 iteration을 하는 도중에 수정가능한 Map을 사용하고 싶다면 ConcurrentHashMap을 사용하면 된다. 
22. HashMap과 TreeMap중 무엇을 사용할지 어떻게 판단하는가?
  엘리먼트들을 추가, 삭제, 위치 변경등 작업을 하고 싶으면 HashMap이 최고의 선택이다. 하지만 만약 정렬되어 있는 key값에 따라 탐색을 하기 원한다면 TreeMap을 사용하는 것이 더 좋다. 컬렉션에 크기에 따라 다르지만 HashMap에 엘리먼트를 추가 하고 이를 TreeMap으로 변환하는게 키를 정렬해서 탐색하는 경우보다 더 빠르게 동작 한다.

23. ArrayList와 Vector간의 비슷한점과 차이점은 무엇인가?
  ArrayList와 Vector는 여러면에서 비슷하다
  • 인덱스 기반이고 내부적으로 배열로 백업 할 수 있다.
  • 엘리먼트들을 추가한 순서를 가지고 있고 이 순서를 가져 올 수도 있다.
  • iterator를 구현하였으므로 fail-fast 방식이다.
  • null 값을 가질 수 있고 인덱스 번호를 사용해 랜덤으로 접근 할 수 있다.

  아래는 ArrayList와 Vector의 차이점이다.
  • Vector는 synchronized 되어 있지만 ArrayList는 그렇지 않다. 만약 iterating 중에 엘리먼트를 수정 하고 싶다면 CopyOnWriteArrayList를 사용하면 된다.
  • ArrayList는 synchronized에 따른 간접비용이 아무것도 없기 때문에  Vector보다 빠르다. 
  • ArrayList가 좀 더 다재다능 한데 Collection Utility 클래스에서 제공하는 기능으로 synchronized를 시키거나 읽기 전용 리스트를 만들수도 있다.

24. Array와 ArrayList의 차이점은 무엇이고 언제 ArrayList를 사용해야 하는가?
  Array는  primivite 타입이나 Object 둘다 사용 가능 하지만 ArrayList는 Object만 사용 가능하다.
Array는 길이가 고정이 되있지만 ArrayList는 동적으로 변경 가능하다.
Array는 ArrayList처럼 다양한 기능을 제공하지 않는다.(addAll, removeAll, iterator 등등) 목록에 관련된 작업을 할때 ArrayList를 사용하는 것이 좋지만 가끔 Array를 사용하는것이 좋을 때가 있다
  • 리스트의 크기가 고정되어 있고 값을 저장하거나 탐색 용도로만 쓸 경우
  • primitive 타입일 경우 
  • 만약 다차원 배열을 사용할 경우 [][] 배열을 사용하는게 List<List<>>를 쓰는것보다 쉽다.

25. ArrayList와 LinkedList의 차이점은 무엇인가?
  둘다 List 인터페이스를 구현하지만 약간 다른 점이 있다.
  •  ArrayList는 인덱스 기반의 Array로 구성되어 있어서 랜덤 엑세스를 할 경우 O(1)의 속도를 가진다. LinkedList는 데이터들이 이전, 다음 노드 처럼 서로 연결된 node로 구성되어 있다. 인덱스 번호를 사용해서 엘리먼트에 접근 하더라도 내부적으로는 노드들을 순차적으로 순회하며 엘리먼트를 찾는다. LinkedList 의 속도는 O(n)으로 ArrayList 보다 느리다.
  •  엘리먼트의 추가 및 삭제는 LinkedList가 ArrayList보다 빠른데 엘리먼트를 추가 및 삭제하는 중에 array를 리사이즈 하거나 인덱스를 업데이트를 할 일이 없기 때문이다.
  •  LinkedList의 엘리먼트들은 이전, 다음 엘리먼트들에 대한 정보를 가지고 있기 때문에 LinkedList가 ArrayList보다 더 많은 메모리를 소비한다.
 
 26. 랜덤 액세스를 제공하는 컬렉션은 무엇인가?
   ArrayList, HashMap, TreeMap, Hashtable 이 자신의 엘리먼트에 대한 랜덤 엑세스를 제공한다. 

  

27. EnumSet은 무엇인가?
  java.util.EnumSet은 Enum 타입을 활용해서 Set을 구현한 클래스다. Set이 생성 될 때 Set 안의 모든 엘리먼트들은 하나의 enum 타입을 구현한 것이어야 한다. EnumSet은 synchronized되어있지 않고 null 엘리먼트도 허용하지 않는다. copyOf, of, complementOf 같은 유용한 메서드를 제공한다. 아래 포스트를 참조보길 바란다.
  
28. thread-safe 한 컬렉션 클래스들은 무엇이 있는가?
  Vector, Hashtable, Properties, stack 은 synchronized 되어있는 클래스로 thread-safe 기 때문에 multi-thread 환경에서도 정삭적으로 동작한다. Java 1.5의 Concurrent AP에 포함되어 있는 몇몇 컬렉션 클랙스들은 반본 작업을 수행하는 도중에 컬렉션을 수정할수 있는데 이는 컬렉션의 복사본을 통해 작업을 하고 있기 때문이고 이들 역시 multi-thread 환경에서 안전한다.
  
29. Concurrent 컬렉션 클래스는 무엇인가?
  Java 1.5 Concurrent 패키지는 thread-safe 하고 ireating 작업 중에 컬렉션을 수정할 수 있는 클래스들을 포함하고 있다. Iterator는 fail-fast 하도록 디자인되어있고, ConcurrentModificationException을 발생 시킨다. 가장 잘 알려진 클래스로는 CopyOnWriteArrayListConcurrentHashMapCopyOnWriteArraySet이 있다. 
이 클래스들에 대해서는 아래 포스트를 참조 하길 바란다.
30. BlockingQueue는 무엇인가?
  java.util.concurrent.BlockingQueue는 엘리먼트들을 검색하거나 삭제 할때 대기하고, 큐에 엘리먼트가 추가 될 때 저장공간이 충분해 질때까지 기다리는 기능을 제공하는 Queue 이다. BlockingQueue는 자바 컬렉션 프레임워크에서 제공하는 인터페이스중에 하나로 주로 producer-consumer 문제에 주로 사용된다. BlockingQueue를 사용하면 producser가 cosumer에게 Object를 전달할때 저장공간 부족에 따르는 여러 문제점을 걱정할 필요가 없다. Java에서는 BlockingQueue를 구현한 ArrayBlockingQueue, LinkedBlockingQueue, PriorityBlockingQueue, SynchronousQueue등을 지원 한다.
  producer-consumer 문제에 BlockingQueue를 사용한 예제는 이 포스트를 참고 하라.
  
31. Queue, Stack 간의 차이점은 무엇인가?
  Queue와 Stack은 작업을 진행하기 전에 데이터를 저장하는데 사용된다. java.util.Queue
Queue는 엘리먼트들에 접근할때 First-In-First-Out(FIFO)를 사용하지만 항상 그러는 것은 아니다. Deque 인터페이스를 사용해서 양쪽 끝에서 엘리먼트에 접근할 수 있다. 
Stack은 queue와 비슷하지만 엘리먼트를 검색할때 Last-In-First-Out(LIFO)방식을 사용한다. 
Stack은 Vector 클래스를 확장해서 사용하지만 Queue는 인터페이스일 뿐이다.

32. Collections 클래스는 무었인가?
  java.util.Collections 는 유틸리티 클래스로 static 메서드로 구성되어 있고 컬렉션들을 조작하는데 사용된다. 다형성을 활용한 알고리즘들을 가지고 컬렉션을 조작하고 정의된 컬렉션의 형태로 새로운 컬렉션을 반환하고 다른 몇가지 기능들도 지원한다. 이 클래스는 컬렉션 프레임워크의 알고리즘(이진 검색, 정렬, 섞기, 뒤집기등)을  포함하고 있다.
  
33. Comparable 인터페이와 Comparator 인터페이스는 무엇인가?
  Java는 Arrays 와 Collections에 사용되는 정렬 메서드를 사용하기 위해 Comparable 인터페이스를 제공한다. 이 인터페이스는 compareTo 메서드를 가지고 정렬을 하는데 사용한다. 이 메서드를 구현할때 리턴값으로 음수, 0, 양수를 통해 엘리먼트들을 정렬하는데 사용하는데, 만약 비교하는 오브젝트가 적거나, 똑같거나, 크거나 하는 경우에 따라 리턴한다. 
그렇지만 실제 환경에서 사용할 경우에는 서로 다른 파라미터를 가지고 정렬을 하는 경우가 있을 것이다. 예를들어 CEO의 경우 연봉에 따라 사원들을 정렬하고 싶을수도 있고 HR에서 사원들의 나이를 가지고 정렬을 할 경우가 있다. 바로 이런 상황에서 Comparator 인터페이스를 사용하면 되는데 Comparable.compareTo(Object o)는 하나의 필드만 가지고 정렬을 수행하기 때문에 정렬에 필요한 오브젝트들 선텍할 수 있다. Comparator 인터페이스는 두개의 파라미터를 가지고 있는 compare(Object o1, Object o2) 메서드를 제공하는데 이 메서드는 만약 첫번째 변수가 두번째 변수보다 작으면 음수를 리턴하고 만약 두 값이 같으면 0, 더 크면 양수를 리턴한다.

Comparable 및 Comparator 인터페이스에 대해서 더 알고 싶으면 이 포스트를 참고 하라.

34. Comparable 인터페이스과 Comparator 인터페이스의 차이점은 무엇인가?
  Comparable 및 Comparator 인터페이스는 collection 및 Array 오브젝트들을 정렬 하는데 사용한다. Comparable 인터페이스는 오브젝트를 사용하여 정렬하는 방식을 제공하고 간단한 방식으로 제공된다. 
  Comparator 인터페이스는 정렬을 위한 다른 알고리즘을 제공하는데 정렬을 할 오브젝트들 중에서 특정 값을 선택하여  정렬하는데 사용할 수 있다.
  
35. Object들의 목록을 정렬시키려면 어떻게 해야 되는가?
  Object들의 배열을 정렬해야 될때는 Arrays.sort()를 사용하면 된다. 만약 오브젝트 목록들을 정렬시키고 싶으면 Collections.sort()를 사용하면 된다. 이 두 클래스는 sort() 메서드를 오버라이드 하고 있고 Comparable을 사용한 정렬 이나 Comparator를 사용한 정렬을 사용할 수 있다. Collections는 내부적으로 Arrays 의 sorting 메서드를 사용하고 있고, list를 array로 변환하는 경우를 제외하고 동일한 성능을 보여준다.
  
36. 만약 Collections를 함수에 파라미터로 전달할 경우, 이를 수정하지 못하게 할려면 어떻게 해야 되는가?
  함수로 파라미터를 전달하기 전에 Collections.unmodifiableCollection(Collection c) 메서드를 사용해서 읽기전용 커렉션을 생성할 수 있고 만약 컬렉션을 수정할려는 시도가 생기면 UnsupportedOperationException을 발생 시킨다.
  
37. 기존 컬렉션을 가지고 동기화된 컬렉션을 만들려면 어떻게 해야 되는가?
  Collections.synchronizedCollection(Collection c)를 사용해서 동기화된(thread-safe)한 컬렉션을 만들 수 있다.
  
38. 컬렉션 프레임워크내부에서 구현된 일반 알고리즘들은 무엇인가?
  컬렉션 프레임워크들을 일반적으로 알려진 정렬 및 검색 알고리즘에 대한 구현을 제공하고 Collections 클래스들은 이 메서드들을 가지고 있다. 대부분의 알고리즘음 List에서 주로 사용되지만 모든 컬렉션에도 사용할 수 있다.(정렬, 검색, 섞기, 최소-최대 값 찾기)

39. Big-O 표기법은 무엇인가? 예를 들어 줄 수 있는가?
  Big-O 표기법은 데이터 구조에 포함된 엘리먼트들의 숫자에 따라 알로리즘의 성능을 설명해주는 표기법이다. Collection 클래는 사실 데이터 구조이기 때문에 어떤 컬렉션을 사용할지 고려할때 시간, 메모리, 성능에 대한 Big-O 표기법을 기준으로 선택할때가 많다.
  • 예1 : ArrayList get(index i)는 엘리먼트의 숫자에 영향을 받지 않고 동일한 성능을 보여주기 때문에 Big-O 표기법으료 표시하면 O(1)으로 표기 할 수잇다.
  • 예2 : 배열이나 리스트에 대한 선형 탐색은 엘리먼트를 찾는데 엘리먼트들의 숫자에 영향을 받기 때문에 O(n)으로 표시한다.

40. Java 컬렉션 프레임워크의 모범사례는 무엇인가?
  • 필요에 따라 상황에 맞는 컬렉션을 선택해야 된다. 예를 들어 사이즈가 고정되어 있으면 ArrayList보다 Array를 사용할 수 있다. 만약 맵에 삽입된 순서되로 iterate를 하고 싶으면 TreeMap을 사용하는것이 좋다. 중복을 허용하고 싶으 않으면 Set을 사용하면 된다.
  • 몇몇 컬렉션 클래스들을 초기 용량을 지정할 수 있다. 만약 저장할 엘리먼트들의 사이즈를 알 경우에 초기 용량을 지정함으로써 rehashing이나 resizing이 일어나는것을 회피할 수 있다.
  • 코드를 작성할때 구현 클래스가 아닌 인터페이스를 기반으로 작성해야 나중에 구현체를 변경할때 코드를 재작성하는 수고를 줄일수 있다. 
  • 런타임에 발생할 수 있는 ClassCastException을 회피할려면 항상 제너릭스를 사용해서 type-safety 한 상태를 유지하라
  • 맵에 키를 사용할때 JDK에서 재공하는 immutable 클래스를 사용하여 사용자 클래스에서 hashCode()와 equals() 구현할 필요가 없게 하라
  • 읽기전용 및 동기화, 빈 컬렉션등을 만들때는 자신만의 구현으로 생성하지 말고 Collections에서 제공하는 유틸리티 클래스를 사용하라. 이는 코드 재사용성을 높여주고 안정적이며 유지보수 비용을 줄여 준다.


신고

Comment +0

오랜만에 간단 번역.

이번엔 dzone.com의 TOP POST 2013: There are only 2 Roles on code. 를 번역해 보았습니다.
전체적인 내용은 객체지향 프로그래밍 및 설계의 다섯가지 기본 원칙(SOLID)중에 단일 책임 원칙(Single responsibility priciple)을 좀 더 자세히 설명한 것 같은 느낌이네요. 

중간에 TTD와 단위 테스트를 진행 하는 것보다 더 중요한 것은 한가지 역할에 집중하는 코드를 만드는 것이라 라는 내용이 나오는데, 이게 가장 중요한 내용 같습니다. 가끔 테스트 코드를 살펴보면 mock으로 도배되다 싶이 한 코드가 있는데.. 이걸 보면 이 코드를 어떻게 이해해야되고 수정해야 되는지 난감한데요. 이 포스트에 등록된 내용을 항상 마음속에 곱씹으며 개발하는 습관을 들이면 좀 더 좋은 날이 올것 같습니다. ㅎㅎ

코드를 테스트 하는 화려한 테스트 코드를 작성하는 것보다 단일 기능에 집중하는 클래스 설계를 진행 한다면, 단위 테스트에 작성되는 코드량과 시간을 감소 시킬 수 있고, 후에 유지보수도 쉽게 진행할 수 있습니다.

여기서 알고리즘은 프로그램 내에 있는 비즈니스 로직(핵심 기능)으로 생각하셔도 될 것 같습니다.

========================================================================

모든 코드는 두가지 역할로 분류될 수 있다. 알고리즘 처럼 작업을 하는 것과 작업을 조절 하는 것.

실제 환경에서 코드 베이스들의 복잡성은 한 곳에 이런 역할들을 같이 두기 때문에 발생한한다.

내가 작성했던 코드들의 90%정도가 알고리즘과 작업을 적절히 구분하지 못했던 것에 스스로 죄책감을 느끼고 있다.

일을 좀 더 명확하게 정의 하기.

왜 코드들을 알고리즘과 코디네이터로 구분을 해야 할까. 
알고리즘과 코디네이터들이 의미하는게 무엇인지 먼저 알아 보기로 하자.

우리 대부분은 common algorithms in Computer Science에 나오는 버블 소트나 이진 검색 같은것을 자주 들어서 익숙하지만 우리가 작성한 코드들이 알고리즘을 포함하여 동작하고 있다는 것을 때때로 깨닫지 못하고 있다. 

어떤 문제를 해결하거나 어떤 작업이 수행되는 명령 또는 단계들이 있고 이 단계들은 데이터를 가지고 동작하고 외부로부터 독립되어있다. (버블 소트처럼 정렬이 되는것, 데이터만 제공 하면 정렬이 됨)

우리가 작성하는 모든 코드들은 본질적으로 테스트가 가능 해야 되고 , 우리가 알고 있는 일반적인 정렬 알고리즘 처럼 잠재적으로 독립되어 동작되어야 한다. 

알고리즘들을 프로젝트로부터 제거한다면 프로그램에 남아 있는 것들은 단순히 알고리즘들을 연결하는 코드뿐일 것이다.

코드에서 알고리즘과 코디네이터를 분리하는것이 왜 중요한가

코드들을 잠재적으로 두 큰 카테고리로 분리 해야 된다는 것을 알았다. 
다음 단계는 왜 이렇게 분리해야되고 어떻게 하면 분리 할 수 있는지 확인하는 것이다.

알고리즘을 다른 코드를 조작하는 것들과 분리하면서 얻게 되는 가장 큰 이점은 알고리즘 코드가 독립적으로 동작 한다는 것이다.

알고리즘 코드를 독립적으로 관리한다면 아래 3가지 내용을 즉시 확인 할 수 있다.
1. 단위 테스트를 하기 쉬어진다.
2. 재사용이 쉬어진다.
3. 복잡도가 감소된다.

Mock 방식을 사용하지 않고 IoC 컨테이너가 드물게 사용되고 있었을때는 TTD는 정말 완전 어려웠다.
내가 처음 일을 시작할 때는 TDD를 활용해서 코드 커버리지를 100%로 맞출수 있을꺼라 생각했지만, 그때는 mock 프레임워크나 IoC 컨테이너가 존재 하지 않을 때라서 지금 생각해 보면 미친짓이었다.

만약 당신이 작성하는 코드를 TDD방식으로 하고 싶으면 알고리즘과 관련된 로직들을 분리해내야 된다.
만약 신뢰할만한 단위 테스트를 진행 하고 싶으면 클래스를 작성할때 최소한의 의존성을 가지고 있도록 해야된다.

많은 개발자들이 TDD를 어려워 하는 이유는 실제 코드를 작성하다 보면 다른 코드에 많은 의존성들이 생기기 때문이다. 이 의존성들의 문제들을 해결하기 위해 이 코드들에 대해서 가짜 버전을 만들 필요가 있어졌다.
이를 해결하기 위해 의존성이 필요한 부분을 Mock으로 대체 하는 방법이 고안되고 IoC 컨테이너를 사용하는 아키텍쳐가 인기 있게 되었다.

TDD와 유닛 테스트는 어디에서나 동작 할 수 있는 코드로 작성 되어야 하지만 TTD보다 더 중요한 점은 알고리즘 코드를 이와 관련 없는 코드로 부터 분리해내야 된다는 것이다.

더 좋은 방법!

이 문제를 해결할 수 있는 방법은 꾸준한 노력을 기울이는 것이다. 
이를 수행하기 위해 필요한 최소한의 노력은 IoC 컨테이너를 사용하고 시간이 날때마다 단위 테스트를 작성하고 조금씩 리팩토링을 진행 하도록 하는것이다. 

아래에서 간단한 예제를 볼 것이다. 
이 예제에서 가장 중요한 점은 코드들이 의존성을 제거하는 리팩토링을 진행하고 로직을 명확하게 분류해 내는것을 이해 하는 것이다.

Calculator 클래스를 살펴보자.

01.public class Calculator
02.{
03.private readonly IStorageService storageService;
04.private List<int> history = new List<int>();
05.private int sessionNumber = 1;
06.private bool newSession;
07. 
08.public Calculator(IStorageService storageService)
09.{
10.this.storageService = storageService;
11.}
12. 
13.public int Add(int firstNumber, int secondNumber)
14.{
15.if(newSession)
16.{
17.sessionNumber++;
18.newSession = false;
19.}
20. 
21.var result = firstNumber + secondNumber;
22.history.Add(result);
23. 
24.return result;
25.}
26. 
27.public List<int> GetHistory()
28.{
29.if (storageService.IsServiceOnline())
30.return storageService.GetHistorySession(sessionNumber);
31. 
32.return new List<int>();
33.}
34. 
35.public int Done()
36.{
37.if (storageService.IsServiceOnline())
38.{
39.foreach(var result in history)
40.storageService.Store(result, sessionNumber);
41.}
42.newSession = true;
43.return sessionNumber;
44.}
45.}

이 클래스는 간단한 덧셈 계산을 하고 결과를 storage 서비스를 통해 저장하는 일은 하는 것이다.
아주 복잡한 코드는 코드는 아니다. Calcalator 클래스는 storage service를 필요로 한다.

이 클래스는 로직을 추출해서 클래스를 재작성하는 작업을 통해 의존성이 제거된 클래스를 만들수 있고, 조정클래스는 로직을 전혀 가지고 있지 않은 것을 볼 수 있다.

01.public class Calculator_Mockless
02.{
03.private readonly StorageService storageService;
04.private readonly BasicCalculator basicCalculator;
05. 
06.public Calculator_Mockless()
07.{
08.this.storageService = new StorageService();
09.this.basicCalculator = new BasicCalculator();
10.}
11. 
12.public int Add(int firstNumber, int secondNumber)
13.{
14.return basicCalculator.Add(firstNumber, secondNumber);
15.}
16. 
17.public List<int> GetHistory()
18.{
19.return storageService.
20.GetHistorySession(basicCalculator.SessionNumber);
21.}
22. 
23.public void Done()
24.{
25.foreach(var result in basicCalculator.History)
26.storageService
27..Store(result, basicCalculator.SessionNumber);
28. 
29.basicCalculator.Done();
30.}
31.}
32. 
33.public class BasicCalculator
34.{
35.private bool newSession;
36. 
37.public int SessionNumber { get; private set; }
38. 
39.public IList<int> History { get; private set; }
40. 
41.public BasicCalculator()
42.{
43.History = new List<int>();
44.SessionNumber = 1;
45.}
46.public int Add(int firstNumber, int secondNumber)
47.{
48.if (newSession)
49.{
50.SessionNumber++;
51.newSession = false;
52.}
53. 
54.var result = firstNumber + secondNumber;
55.History.Add(result);
56. 
57.return result; ;
58.}
59. 
60.public void Done()
61.{
62.newSession = true;
63.History.Clear();
64.}
65.}

BasicCalculator 클래스를 보면 아무런 외부 의존성을 가지고 있지 않고 이는 단위 테스트를 쉽게 진행 할 수 있다는 것을 의미한다. 
실제 로직도 전부 포함 하고 있기 때문에 더 이해 하기가 쉽고 Calculator_Mockless 클래스는 코드들을 조정하는 역할 만 한다.

위 예는 기본적인 예제이긴 하지만 인위적이진 않다. 무슨말이냐면 의도적으로 작성한 코드는 아니고 실제 운영되고 있는 서비스에서도 볼 수 있는 그런 예이다.

마지막 조언.
목 객체를 전부 제거하거나 목 객체를 사용할 생각이 전혀 없다면 작성하는 코드들을 명확히 알고리즘과 코드 조합 부분을 구분해서 작성 해야 된다.

이것은 매우 어렵기 때문에 나 역시 이 방법이 익숙해 지도록 매번 노력하고 있다. 하지만 이 방법을 잘 수행된다면 반드시 큰 이익이 있을 것이라 생각한다. 

코드들로 부터 알고리즘을 분리해 낸다면 전체 시스템 구조를 이해하는데도 큰 도움이 된다. 


신고

Comment +0


웹 서핑중에 비밀번호 암호화 관련해서 좋은 글이 있어서 번역해 봤습니다. 

개인 프로젝트를 하던 다른 서비스 프로젝트들 하던 사용자의 비밀번호를 암호화하는것은 중요한데요. 
암호화 하기 위해서 보통 해싱 함수를 사용해서 비밀번호를 해싱하고 여기에 "소금을 친다(Adding Salt)" 라는 방법을 사용합니다.
이 내용에 대한 설명이 전반적으로 잘 되어 있네요. 

노력은 했지만 많이 부족하니... 원문 읽어 보시는 것도 강추 드립니다. 

비밀번호 해싱이란 무엇인가, 해싱값이 어떻게 해킹되나?, 소금 치기 에 대한 내용입니다.

=======================================================================

만약 웹 개발자라면 사용자 계정이 포함된 시스템을 만들어본 경험이 있을 것이다. 이 시스템에서 가장 중요한 점은 사용자의 패스워드가 어떻게 보호되고 있는가 이다. 사용자 정보를 가지고 있는 데이터베이스는 자주 해킹 당하고 만약 보호책이 없다면 반드시 비밀번호를 보호 하도록 해야 한다. 암호를 보호하는 가장 좋은 방법은 소금을 친 해싱을 사용하는 것이다.(소금을 치다 -> 패스워드를 보호하기 위해 특별한 값(소금)을 추가 하는 것) 이 페이지에서는 이 방법을 왜 수행하는지 설명할 것이다.

제대로 비밀번호 해시를 수행하는 방법에 대한 여러 상충하는 아이디어들과 오인들이 존재 한다. 아마 웹에 존재하는 잘못된 정보들 때문일 것이다. 비밀번호 해싱은 아주 간단한 것인데 많은 사람들이 오해 하고 있다. 이 페이지를 통해 해싱을 올바르게 사용하는 방법과 왜 이렇게 해야 되는지에 대해서 설명할 것이다.

 중요한 경고! 만약 자신만의 비밀번호 해싱 방법을 가지고 있다면 그렇게 하지 말아라! 그 방법은 망가지기 쉽다. 만약 암호학을 전공하고 있다고 해도 이 경고를 무시해서는 안된다. 이 경고는 모두에게 적용된다. 절대 자신만의 암호화 방법을 만들지 말라.비밀번호 저장에 관련된 문제는 이미 해결 되어있다. 


비밀번호 해싱이란 무엇인가?

해싱 알고리즘은 단방향성을 가지고 있고 고정된 길이의 "fingerprint" 값을 제공한다. 위의 예제 처럼 한글자만 변경되도 전혀 다른 해쉬 값을 생성한다. 이 방법은 비밀번호가 인코딩 되어 저장될때 디코딩할 수 없기 때문에 비빌번호를 보호하는데 아주 좋은 방법이다. 이 방법과 동시에 사용자가 입력한 패스워드가 동일한지도 검증을 해야 한다.


해쉬를 기본으로 사용하는 계정관리 시스템에서 사용하는 사용자 등록 및 인증 관련 흐름은 아래와 같다.
1. 사용자가 계정을 생성한다.
2. 사용자의 비밀번호는 해싱되어 데이터베이스에 저장된다. 원본 패스워드는 하드 디스크 어디에도 기록되지 않는다.
3. 사용자가 로그인을 시도 할 때 사용자가 입력한 패스워드의 해시값이 데이터베이스에 저장된 값과 동일 한지 비교 한다.
4. 만약 해시값이 동일하면, 사용자는 로그인에 성공하고 아니면 잘못된 값을 입력했다고 알려준다.
5. 로그인을 계속 시도 하는경우 3~4번 과정을 반복한다.

4번째 과정에서, ID가 잘못됬는지 입력한 비밀번호가 잘못되었는지는 절대로 알려주지 않는다. 항상 "사용자 ID 및 비밀번호 가 일치 하지 않습니다" 메시지를 노출 시켜야 한다. 이 방법은 암호를 모르는 상태에서 유효한 아이디를 가지고 비밀번호를 무작위로 입력할 수 있는 방법을 방어 할 수 있다.

암호를 보호하기 위해 생성된 해시 함수는 데이터 구조학 강좌에서 사용되는 해쉬 함수와 동일한 것이 아닌것을 알아야 한다. 해쉬 함수는 보안을 위해서 고안된 것이 아니라 데이터 구조학에서 해쉬 테이블을 빠르게 사용하기 위해서 만들어진 것이다. 암호화된 해쉬 함수 만이 비밀번호를 해싱 하는데 사용되어야 할 것이다. 암호화 해쉬 함수로는 SHA256, SHA512, RipeMD WHIRLPOOL 같은 것들이 있다.

암호화 해쉬 함수를 통해서 비밀번호를 관리하면 사용자들의 비밀번호는 안전할 것이라고 생각할 수 있다. 이것은 현실과는 꽤 다른데 아주 빠르게 일반 해시 암호를 찾아 낼 수 있는 방법이 있다. 하지만 이 공격 방법에 덜 영향을 받는 효과적인 몇가지 방법이 존재한다. 이러한 기술의 필요성에 대한 동기를 부여하기 위해서 웹사이트를 생각해보자. 메인 화면에서 해킹된 비밀번호 해쉬 값들을 전달해보고면 이 결과가 1초도 안되서 표시되는것을 볼 수 있다.(해킹된 비밀번호 해독 해서 입력 하는것 말하는 듯..)
확실히 비밀번호를 간단하게 해싱하는 것만으로는 보안에 대한 요구사항을 충족 시킬 수 없다.

다음 섹션에서는 해킹된 일반 해싱 비밀번호를 사용한 일반적으로 알려진 공격에 대해서 논의할 것이다.

해쉬가 해킹 되는 방법

단어 사전 입력 공격 및 무차별 대입 공격 


해쉬를 해킹하는 가장 쉬운 방법은 비밀번호를 여러가지로 예측해보고 반복해서 입력해보는 것이다. 가장 유명한 패스워드 예측 방법은 단어 사전을 통한 공격과 무차별 대입 공격이다. 

단어 사전 공격은 단어나 일반적인 비밀번호 등 비밀번호로 쓰일만한 단어들을 가지고 공격을 하는 것이다. 각각 단어들을 먼저 해싱해 놓고 해싱 되어 있는 비밀번호와 비교한다. 해쉬 값이 일치 하면 바로 그 단어가 비밀번호가 된다. 이 단어 사전 파일을 텍스트들에서 추출하여 구성되고 있기도 하고 심지어는 실제 데이터베이스에서도 추출하여 구성되어 있기도 한다. 

무차별 대입 공격은 주어진 비밀번호의 길이에 맞춰 가능한 모든 글자의 조합을 사용하는 것이다. 이 방법은 계산 비용이 비싸고 효율이 가장 좋지는 않지만 결국 비밀번호를 찾는데 성공할 것이다. 

단어 사전 공격이나 무차별 대입 공격에 대해 방어할 방법은 없다. 이 방법들이 비 효율적이긴 하지만 예방할 방법이 없다. 만약 당신의 비밀번호 해싱 시스템이 확실히 보안되어 있다면 해시를 해킹할 수 있는 방법은 단어 사전 공격이나 무차별 대입 공격을 사용하는 수 밖에 없다.

Lookup tables


룩업 테이블은 매우 빠르게 동일한 유형의 해시를 해킹하는데 매우 효과적인 방법이다. 일반적으로 비밀번호 사전에서 해쉬값들을 미리 추출해 놓고 비밀번호를 여기에서 검색 한다. 룩업 테이블의 장점은 초당 백개 정도의 비밀번호를 검색할 수 있고 해시 데이터가 수십억개가 넘더라도 사용할 수 있다.

역 룩업 테이블


먼저 공격자들은 추출한 사용자 정보를 가지고 동일한 비밀번호를 사용자끼리 그룹핑을 한다. 공격자들은 다양한 추측 비밀번호를 입력하여 검색하고 해당 추측 비밀번호와 일치하는 사용자 목록을 가지고 온다. 이 방법은 일반적으로 많은 사용자가 동일한 비밀번호를 사용하기 때문에 매우 효율적이다.

레인보우 테이블
레인보우 테이블은 시간과 메모리 사이의 선택 사항이다. 룩업 테이블과 비슷 하지만 해쉬를 해킹하는 속도 향샹을 위해 룩업 테이블보다 더 작게 구성되어 있다. 더 작기 때문에 같은 용량의 디스크에  많은 해쉬값들을 저장할 수 있어서 더 효율적으로 사용할 수 있게 한다. 레인보우 테이블은 md5 값이나 8자 까지 해킹 할 수 있다.

다음으로 룩업 테이블과 레인보우 테이블로 해쉬 해킹을 불가능하게 만들수 있는 소금치기 라고 불리우는 기술에 대해서 알아보자

Adding Salt(소금 치기)

룩업 테이블과 레인보우 테이블은 비밀번호가 해킹할 해쉬와 동일한 방법으로 해싱되어 있어야 해킹이 가능하다. 만약 두 사용자가 동일한 비밀번호를 사용한다면 이들은 동일한 해싱 비밀번호를 가지게 된다. 이는 각 해시들을 무작위 구성되게 함으로써 예방할 수 있고, 만약 같은 비빌번호가 두번 해싱된다면 이 값은 서로 동일하지 않게 된다.

소금이라고 불리는 무작위 문자열을 비밀번호를 해싱하기 전에 붙여서 해쉬 값을 무작위로 만들 수 있다. 상단의 예제를 보면 같은 비밀번호인데도 결과로 생성된 해시값은 매번 다른것을 볼 수 있다. 인증을 진행할 때 비밀번호가 동일한지 확인을 하기 위해서는 소금값이 필요 한데 이 값은 보통 사용자 계정을 저장하는 데이터베이스에 비밀번호 해쉬값과 같이 있거나 해쉬값으로 변환 되어 저장하고 있다. 

소금 값은 비밀로 관리 하지 않아도 된다. 그냥 룩업 테이블과 역 룩업 테이블, 레인보우 테이블이 효과를 볼 수 없게 해시를 무작위로 사용하면 된다. 공격자는 소금 값이 뭐가 될지 알 수 없고 룩업 테이블과 레인보우 테이블 값을 미리 생성해 놓을 수가 없다. 만약 각각 사용자마다 다른 소금 값으로 해싱되어 있다면 역방향 룩업 테이블도 동작하지 않을 것이다.

다음 섹션에서는 소금 값에 대해서 일반적으로 잘못 구현된 상황에 대해서 알아 보도록 할 것이다.

잘못된 방법 : 짧은 소금 값 & 소금 값 재사용
소금값을 잘못 사용하는 가장 흔한 경우는 같은 소금값을 여러 해시에 사용하거나 너무 짧은 소금 값을 사용하는 것이다.

소금 값 재사용
가장 흔한 실수는 각 해쉬마다 같은 소금값을 사용하는 것이다. 어느 소금값은 프로그램내에 하드 코딩 되어 있거나 랜덤으로 한번 생성해서 사용하기도 한다. 이것은 효과가 없는데 만약 두 사용자가 같은 비밀번호를 사용할 경우 그들은 여전히 같은 해쉬값을 가지게 된다. 해커들은 여전히 역 리버스 룩업 테이블을 사용해 단어 사전 공격을 시도 할 수 있다. 그들은 비밀번호를 해싱하기 전에 사용될만한 소금 값들을 추가한 후 해싱을 한다. 만약 소금 값이 유명한 제품의 이름으로 되어 있다면 룩업 테이블과 레인보우 테이블은 이 소금값을 사용해 만들어서 쉽게 비밀번호를 얻어 낼 수 있다.

사용자 계정을 새로 만들거나 비밀번호를 변경할 때는 반드시 무작위로 생성된 소금값을 사용해야 된다.

짧은 소금 값
만약 소금 값이 너무 짧으면 해커는 가능한 소금값들을 활용해서 룩업 테이블을 만들 수 있다. 예를 들어 만약 소금값이 아스키 문자 3자로 되어 있다면 소금 값으로95*95*95=857,375개의 값이 사용 가능하다. 이 값이 많아 보일 수도 있지만 각각의 룩업 테이블들이 1메가 정도의 평범한 비밀번호로 구성되어 있는 경우 837G 만으로 전체 룩업 테이블을 구성할 수 있고 요즘 1000GB 하드 디스크는 채 100달러도 하지 않는다.

같은 이유로 사용자 이름 역시 소금 값으로 사용할 수 없다. 혼자 독립적으로 운영되는 서비스의 경우는 사용자 이름이 유니크할 수 있지만 다른 서비스에서도 똑같이 자주 사용된다. 해커들은 평범한 사용자 이름을 사용해 룩업 테이블을 구성하고 이를 사용해서 사용자이름이 소금값으로 사용된 해시값을 생성한다.

해커가 사용가능한 소금값을 가지고 룩업 테이블을 생성하는 것을 불가능 하게 할려면 소금 값은 반드시 길게 만들어야 된다. 좋은 방법은 해쉬 함수를 사용해서 생성된 길이와 동일하게 만드는 것이다. 예들 들어 해시 값이 SHA256 알고리즘을 사용해서 256 비트(32 바이트)로  생성 한다면 소금 값 역시 랜덤으로 생성된 32바이트로 만들면 된다.

잘못된 방법 : 이중 해싱 및 엉뚱한 해쉬 함수
이 섹션에서는 엉뚱한 해시 알고리즘 조합 같은 잘못된 해싱 방법에 대해서 살펴본다. 다른 해싱 함수들을 조합해서 사용할 수 있으니 그 결과가 더 안전할 것이라고 생각하기 쉽다. 실제로 이를 수행함으로써 아주 작은 이득이 있다. 하지만 이 방법은 상호 운영성에 대한 문제가 발생하고 가끔 해쉬 값을 덜 안전하게 만들기도 한다. 절대로 자신만의 암호화 방식을 만들지 말고 항상 고수들에 의해 생성된 표준을 사용하도록 해라. 여러개의 해싱 함수를 사용하면 해싱 작업을 수행하는 이 느려지고 그래서 해킹하는 것도 느릴것이라고 주장 하지만 해킹 시간을 느리게 하는 더 좋은 방법이 있고 나중에 보게 될 것이다.

여기 내가 웹 포럼에서 추천하고 있는걸 본 허접한 해싱 함수가 있다.
- md5(sha1(password))
- md5(md5(salt) + md5(password))
- sha1(sha1(password))
- sha1(str_rot13(password + salt))
- md5(sha1(md5(md5(password) + sha1(password)) + md5(password)))
이중에 아무것도 사용하지 말아라.

주의 : 이 부분에 대한 논란은 이미 검증된 것이다. 나는 이 허접한 해싱 함수들이 해커들이 어떤 해싱함수들을 사용했는지 알수 없고 엉뚱한 해시 함수들을 사용해서 레인보우 테이블을 구성하는 해커들은 적은 수이고 이 방법이 해싱함수를 수행하는데 시간이 더 오래 걸릴것이기 때문에  좋은 방법이고 주장하는 메일들을 여러통 받았다. 

해커들은 알고리즘에 대해 알지 못할 경우 해시에 대한 공격을 시도 하지 않는다. 하지만 케르크호프스의 원리(키를 제외한 시스템의 다른 모든 내용이 알려지더라도 암호쳬게는 안전해야 한다)에 따르면 해커들은 소스 코드에 접근할 수 있고(특별히 무료거나 오픈 소스 소프트웨어일 경우) 비밀번호-해시 방법을 사용한 시스템이 목표가 되고, 이 알고리즘을 리버스 엔지니어링하는것은 어렵지 않다. 병렬화하기가 매우 어렵게 디자인된 알고리즘을 사용하는것이 좋다. 그리고 레인보우 테이블 문제를 해결하기위한 정확한 방법은 소금 치기 기법을 사용하는 것이다.

HMAC같이 표준화된 이상한 해시 함수를 사용한다면 괜찮다. 하지만 해시 작업을 느리게 하는것이 이유라면 key stretching 에 대한 색션을 먼저 읽어 보기 바란다.

실수로 안전하지 않은 해싱 함수를 만드는 경우와 상호 운영성에 문제가 있는 엉뚱한 해싱함수를 사용해서 얻는 작은 이득에 대해 비교해보자. 확실하게 잘 테스트된 표준 방식을 사용하는것이 가장 좋은 방법이다.

해시의 충돌

해시 함수는 고정길이의 문자열로 이루어 지도록 되어 있으므로 같은 입력에 대해서는 동일한 해쉬를 가지고 가지게 된다. 암호화 해시 함수는 이렇게 동일한 해쉬를 가지고 있는것을 찾기 어렵도록 설계 되었다. 암호학자들은 해쉬가 충돌하는 것을 찾아 낼 수 있고 최근에 MD5 해시 함수를 사용했을 때 해시 충돌을 활용한 공격이 발생하기도 했다.

해쉬 충돌은 취약한 해시 함수인 MD5를 사용할 경우에도 이를 찾아 내는데 많은 컴퓨터 리소스를 필요로 한다. 실제 환경에서는 거의 발생할 일이 없고 대부분 테스트를 하는 과정에서 우연히 발생한다. MD5와 소금값을 사용하여 해시를 하는경우 SHA256과 소금값을 사용하여 해쉬하는 것 만큼 안전하긴 하지만 가능한 SHA256, SHA512, RipeMD, WHIRLPOOL 같은 더 안전한 해시 함수를 사용하는것이 좋다.

올바른 방법 : 훌륭한 방법으로 해싱 하기

이 섹션에서는 암호 해싱에 대한 정확한 방법을 설명한다. 첫번째로 기본 해시에 대해서 알아보고 두번째로 이 기본 해시를 가지고 해킹을 어렵게 하는 방법에 대해서 알아본다

기본 : 소금값과 함께 해싱하기

이전 섹션에서 악의적인 목적을 가진 해커가 룩업 테이블과 레인보우 테이블을 사용해서 일반 해시를 해킹하는것을 살펴보았다. 이 문제를 해결하는 방안으로 소금값을 랜덤으로 생성하여 해싱할 때 같이 사용하는것을 배웠지만 소금값을 어떻게 생성하고 비밀번호에 이를 어떻게 적용할 것인가?

소금 값은 암호학적으로 안전한 난수 생성기에 의해 생성(Cryptographically Secure Pseudo-Random Number Generator, CSPRNG)되어야 한다. CSPRNG은 C언어의 rand() 함수처럼  일반 난수생성기와 매우 다르다. 이름을 통해 짐작하듯이 CSPRNG는 암호화를 사용하도록 설게되어 있고 이 것은 완벽히 예측 불가능 한것을 의미한다. 소금값은 예측가능한 것을 사용할수 없기 때문에 반드시 CSPRNG를 사용해야 된다. 아래 표에서는 유명한 프로그래밍 언어에서 제공하는 CSPRNG 목록이다.

PlatformCSPRNG
PHPmcrypt_create_iv, openssl_random_pseudo_bytes
Javajava.security.SecureRandom
Dot NET (C#, VB)System.Security.Cryptography.RNGCryptoServiceProvider
RubySecureRandom
Pythonos.urandom
PerlMath::Random::Secure
C/C++ (Windows API)CryptGenRandom
Any language on GNU/Linux or UnixRead from /dev/random or /dev/urandom

소금값은 사용자와 비밀번호 별로 유일한 값을 가져야 한다. 사용자 계정을 생성할때와 비밀번호를 변경할때마다 새로운 임의의 랜덤 소금값을 사용해서 해싱 해야 된다. 소금값은 절때 재사용 하지 말아야 되고 길게 만들어야 되기 때문에 다양한 값을 생성할 수 있다. 소금값은 해쉬 함수의 출력 값 만큼 길게 만들고 사용자 계정 테이블에 같이 저장되도록 한다.

비밀번호 저장하기
1. CSPRNG를 사용해서 임의의 소금값을 생성한다.
2. 소금값을 비밀번호 앞에 덧붙이고 SHA256 같은 표준 암호화 해시 함수를 사용해서 해시한다.
3. 소금값과 해시값을 사용자 계정 테이블에 저장한다.

비밀번호 유효성 검사
1. 사용자의 소금값과 비밀번호 해시값을 데이터베이스에서 찾는다.
2. 입력한 비밀번호에 소금값을 덧붙이고 비밀번호 해싱에 사용했던 동일한 해싱함수를 사용하여 해싱한다.
3. 입력한 비밀번호로 생성한 해싱값과 저장되어 있는 해싱값과 비교해서 일치하는지 확인하고 동일 하면 비밀번호가 정확한 비밀번호를 입력한것이고 아니면 잘못된 비밀번호를 입력한 것이다.

웹 애플리케이션에서는 항상 서버에서 해시를 해야 된다.

만약 웹 애플리케이션을 개발중이라면 해쉬를 어디서 할 것인지 고려해봐야된다. 만일 사용자의 브라우저에서 자바스크립트를 사용해 해쉬 되거나 이 해쉬된 값을 서버에 안전하게 전성되었을 경우 이를 사용해야 될까?

자바스크립트로 비밀번호를 해싱 했을때 조차도 서버에서 해시작업을 해야 된다. 사용자 브라우저에서만 해쉬를 하고 서버에서 해쉬를 하지 않을 경우를 고려해 보라. 사용자를 인증하기 위해 웹 사이트에서 생성된 해쉬를 만들고 이를 데이터베이스에 조회해서 동일한 값을 찾을 것이다. 사용자의 암호가 서버로 전송되지 않기 때문에 서버에서 해쉬작업을 하는것 보다 조금 더 안전한 것처럼 보이지만 그렇지 않다.

문제는 클라이언트 쪽에서 사용자의 비밀번호가 해쉬된다는 것이다. 모든 사용자들이 서버에 비밀번호를 확인해야 된다. 해커가 이 해쉬 값을 얻은 경우 이 값을 사용해서 사용자 인증을 진행할 수 있다. 만약 해커가 이 웹사이트의 비밀번호 해쉬가 담긴 데이터베이스를 해킹한다면 암호를 추측해서 사용할 필요도 없이 바로 모든 사용자의 계정에 접속 할 수 있다.

브라우저에서 해시를 할수 없다는 뜻은 아니지만 만약 브라우저 해쉬를 사용해야 된다면 서버 해쉬 작업도 반드시 진행해야 된다. 브라우저에서 해싱을 하는것은 좋은 아이디어이긴 하지만 구현을 위해 아래 사항을 고려해야 한다.
- 클라이언트 암호 해시는 HTTPS(SSL/TLS)를 대신할 수는 없다. 브라우저와 웹서버가 보안 통신으로 연결되어 있지 않다면 있다면 중간에서 이를 가로체 사용자의 비밀번호를 알아낼 수 있다.
- 몇몇 웹 브라우저들은 자바스크립트를 지원하지 않고 몇몇 사용자들은 브라우저에서 자바스크립트 기능을 꺼놓기도 한다. 최대한 호환성을 지원하기 위해 브라우저가 자바스크립트를 지원하는지 잘 감시 해야 되고, 클라이언트 해쉬가 동작하지 않을 경우 서버에서 해시 작업이 수행될 수 있도록 해야 된다.
- 클라이언트 쪽에서도 소금 값을 사용할수도 있다. 클라이언트 스크립트에서 서버를 통해 사용자의 소금값을 확인하는 것아 해결책 이긴 하지만 이를 사용해서는 안된다. 왜냐하면 악의적인 사용자들이 중간에서 이를 가로채 사용할 수 있기 때문이다. 
서버에서도 해싱 및 소금값을 사용한다면 사용자 이름(또는 이메일)을 사이트 정보(도메인 이름)과 함께 클라이언트 소금값으로 사용하는것은 괜찮다.

느린 해시 함수를 사용해 비밀번호를 해킹하는것을 어렵게 만들기
소금 값은 룩업 테이블이나 레인보우 테이블 처럼 해시 되어 있는 값에서 비밀번호를 찾는 방식이 통하지 않게 해준다. 하지만 단어 사전 공격이나 무차별 입력 공격같은 것은 미리 방어 하는게 불가능하다. 높은 성능의 그래픽카드(GPUs)나 직접 제작된 특별한 장비들은 1초에 수십억개의 해시를 만드는게 가능하고 이러한 공격은 여전히 유효하다. 이러한 공격들을 무용하게 만들려면 key stretching 이란 기술에 대해 알고 있어야 한다.

고성능의 GPU와 커스텀 장비를 사용한 단어 사전 공격와 무차별 대입 공격을 방어하는 방법으로 해시 함수를 느리게 하는 방법이 있다. 이 방법을 완성 하기 위해서는 위 공격들에 대해서는 해시 함수가 느리게 동작하도록 하고 실제 사용자에게는 불편함이 없는 속도로 제공해야 된다. 

Key stretching은 CPU를 많이 사용하는 특별한 해시 함수를 사용해서 구현된다. 별도로 해시 함수를 구현 할려고 하지 말고 표준 알고리즘인 PBKDF2 나 bcrypt를 사용하라. 

이 알고리즘들은 보안 요소 나 반복 횟수를 인자로 받는데 이 값들은 해쉬 함수를 어느 정도 느리게 할것인지 결정하는데 사용된다. 데스크탑 소프트웨어나 스마트폰 앱에서 어떤 변수를 사용할지에 대한 결정은 작은 벤치마킹을 한번 수행해 보는 것이다. 이 방법 대로면 사용자는 사용환경 변화를 느낄수 없고 프로그램은 가능한한 안전할 것이다.

웹 애플리케이션에서 key Stretching을 사용한다면 큰 볼륨의 인증 요청을 처리하기 위해서 컴퓨터 자원이 많이 필요할 수 있고 이 key stretching은 웹사이트를 쉽게 DoS 공격 할 수 있기 때문에 주의해야 하지만 낮은 반복 횟수를 사용한다면 key stretching을 사용하는것을 추천한다. 서버 자원을 얼마나 사용할 수 있는지 및 최대 인증 요청 횟수에  따라 반복 횟수를 결정할 수 있다. 로그인 할때마다 CAPTCHA(랜덤 문자 입력 방식)을 사용해서 Dos 위협을 해결할 수 있다. 시스템을 설계할때 반복횟수가 증가 또는 감소 될 수 잇도록 시스템을 설계한다. 

시스템 부하에 대해 걱정이 된지만 key stretching을 사용하고 싶다면 사용자의 브라우저에서 자바스크립트를 통한 key stretching을 사용하는 것을 고려할 수 도 있다. 자바스크립트 표준 암호화 라이브러리는 PBKDF2에 포함되어 있다. 반복 횟수는 모바일 장비같은 느린 환경에서도 사용할 수 있도록 충분이 낮게 설정해야 되고 사용자의 브라우저가 자바스크립트를 지원하지 않을 경우 서버에서 처리 할 수 있도록 해준다. 사용자측에서 하는 key stretchin은 서버측의 해싱을 삭제할 필요가 없다. 클라이언트가 비밀번호를 해시하는 것과 동일하게 생성된 해시를 서버에서도 해시 해야 된다.

해킹이 불가능한 해시 : 키 해시 및 하드웨어 비밀번호 해싱

비밀 키를 해시에 추가 하고 이를 알고 있는 사람만이 비밀번호가 유효한지 확인이 가능하다. 이것은 두가지 방법으로 수행될수 있는데, AES같은 암호화 모듈을 사용하여 암호화 하거나 비밀 키를 HMAC같은 키를 사용한 해싱 알고리즘에 포함하여 해시에 사용할 수 있다.

이 방법은 생각보다 쉽지 않다. 키는 해커로 부터 안전하게 보호되어야 한다. 만약 해커가 시스템에 사용할 수 있는 모든 권한을 얻어 냈을때 저장 위치에 상관 없이 키를 갈취 할 수 있다. 키는 반드시 물리적으로 분리되고 인증 시스템을 가지고 있는 외부 시스템에 저장 되거나YubiHSM같은 특별한 물리장비에 저장 되어야 한다. 
십만명 이상의 사용자가 있을 경우에만 이렇게 하는 것을 추천한다. 

만약 물리서버를 분리할수 없거나 특수 장비를 사용할 수 없는 경우에는 일반 웹 서버에서도 키 해시에 대한 이점을 사용할 수 있다. 
대부분의 데이터베이스는 SQL Injection 공격에 취약한 부분이 있는데 해커들이 이를 사용해서 local 파일 시스템에 접근하지 못하도록 한다. 만약 랜덤 키를 생성한 후 소금치는 해싱 작업을 한 후 웹에서 접근할 수 없는 파일에 저장 한다면 데이터베이스가 SQL Injection 공격하는 경우에도 괜찮다. 키 값은 소스 코드에 하드코딩 하지 말고 애플리케이션을 설치할 때 무작위로 생성 되도록 한다. 이 방법은 장비를 분리하는 것만큼 안전하지는 않지만 아무것도 하지 않는것보다는 좋다.

키 해시 방법을 사용할때 소금 값을 지울 필요는 없다. 영리한 해커들은 결국엔 키 값을 찾아 낼 것이기 때문에 해쉬 값들은 소금 값과 key stretching에 의해 보호 되고 있어야 한다.


다른 보안 조치

비밀번호 해싱은 비밀번호가 보안을 위반할때도 보호 되어야 한다. 전체 응용 시스템에 대한 보안작업을 해야 되는것은 아니지만 비밀번호 해시를 해킹 당하는 것은 가장 먼저 예방해야된다.

숙련된 개발자가 보안 관련 애플리케이션을 개발할 때도 보안사항에 대해서는 교육을 받아야 한다. 웹 애플리케이션 취약점에 대한 공부자료로는 The Open Web Application Security Project(OWASP)가 있다. 이 10개의 취약점 목록을 참고 하라. 이 리스트에 있는 모든 취약점에 대해 이해 하지 않는한 민감한 데이터를 다루는 웹 애플리케이션을 개발 할려고 하지 말아라. 모든 개발자가 보안 관련 교육을 보장 받는것은 전부 고용주의 책임이다. 

외부업체를 통한 취약점 검사를 받는것은 좋은 방법이다. 최고의 프로그래머 조차도 가끔 실수를 만들어 낼 수 있으므로 보안 전문가가 잠재적인 보안 이슈를 확인 해야 된다. 신뢰할 수 있는 기관이나 직원을 고용하여 정기적으로 코드를 리뷰 하도록 해라. 보안 검토 프로세스는 애플리케이션 개발을 시작할때 부터 계속 진행되어야 한다.

만약 웹사이트 취약점 공격에 대한것이 발견 된다면 전체 서버를 모니터링 하는것이 중요하다. 서버에 대한 공격을 감지하고 보안 침해에 대응할 직원을 최소 한명이상 고용하는 것을 추천한다. 만약 해킹에 대해 감지 하지 못한다면 해커는 악성코드를 사용자에게 감염 시킬 수 있기 때문에 취약점에 대해서 감시하고 신속하게 대응하는것은 매우 중요하다.


자주 묻는 질문
무슨 알고리즘을 사용해야 되나?
사용 해도 되는 것
- The PHP source code, Java source code, C# source code or the Ruby source code at the bottom of this page.
- OpenWall's Portable PHP password hashing framework
- Any modern well-tested cryptographic hash algorithm, such as SHA256, SHA512, RipeMD, WHIRLPOOL, SHA3, etc.
- Well-designed key stretching algorithms such as PBKDF2, bcrypt, and scrypt.
- Secure versions of crypt ($2y$, $5$, $6$)
사용하지 말아야 되는것
- Outdated hash functions like MD5 or SHA1.
- Insecure versions of crypt ($1$, $2$, $2x$, $3$).
- Any algorithm that you designed yourself. Only use technology that is in the public domain and has been well-tested by experienced cryptographers

MD5 및 SHA1에 대한 암호 공격이 없다고 하더라도 이것들은 해킹 하기가 쉽고 오래되고 비밀번호를 저장하는데 사용되지 않는 해쉬 함수이기 때문에 이것을 사용하는 것을 추천 하지 않는다. 이 규칙에 대한 예외로 PBKDF2가 있는데 내부 해시 함수를 사용하여 구현한 SHA1을 사용할 경우다. 


사용자들이 비밀번호를 잃어 버렸을때 언제 비밀번호를 초기화 할 수 있게 해야 하나?

내 개인적인 의견은 요즘 사용되는 모든 비밀번호 초기화 방법은 안전하지 않다는 것이다. 만약 암호화된 서비스를 위해 높은 수준의 보안을 적용해야 한다면 사용자가 비밀번호를 리셋할 수 없게 해라.

대다수 웹사이트들이 사용자가 비밀번호를 잃어 버렸을때 이메일 인증을 사용한다. 이 작업을 하기 위해 무작위로 생성된 일회성 토큰이 생성되고 비밀번호를 리셋하는 url에 토큰을 포함하여 사용자에게 비밀번호 초기화 이메일을 보낸다. 인증 토큰이 포함된 비밀번호 초기화 링크를 클릭 하면 새로운 패스워드 입력 화면을 표시한다. 이 일회성 토큰은 사용자 별로 별도로 생성되기 때문에 해커들이 이를 다른 사용자의 비밀번호를 리셋하는데 사용할 수 없다.

토큰은 반드시 사용하거나 생성된지 15분이 지나면 반드시 만료 처리 되도록 해야 된다. 사용자가 암호를 다시 기억해내서 로그인 하거나 다른 리셋 토큰을 요청한 경우에도 이미 생성된 것은 만료 처리를 해야 된다. 만약 토큰 만료 처리가 안된다면 사용자의 비밀번호를 해킹하는데 지속적으로 사용될 수 있다. 이메일은 일반 텍스트 프로토콜이고 웹상에는 많은 악의적인 코드들이 존재한다. 이를 통해 이메일이 노출 될 수 있으므로 토큰 만료 기능을 꼭 추가 해야 된다.

해커들이 토큰을 조작할 수 있으므로 사용자 계정 정보나 만료 시간 정보 같은것은 포함되지 않도록 하다. 토큰은 반드시 예측 불가능한 이진 BLOB 형태로 데이터베이스에 기록되도록 해야 된다.

절대 사용자에게 신규 비밀번호를 메일로 보내지 말아라. 
비밀번호를 재설정할때 새로운 소금값을 사용하고 이전에 사용했던 값은 재사용하지 말라.

만약 사용자 계정 데이터베이스가 해킹되었을땐 어떻게 해야 되나?
가장 먼저 처리해야 될 일은 시스템이 어떻게 해킹 되었고 해커가 사용한 취약점을 어떻게 패치해야될지 정하는 것이다. 만약 이런 해킹에 대한 경험이 없다는 외부 보안 담당자에게 의뢰 하는것을 강력하게 추천한다.

해킹 당한 것에 대해 감추고 아무도 이를 알아내지 않았으면 할수도 있다. 하지만 이를 감추려고 한다면 상황은 더 악화된다. 왜냐하면 사용자의 비밀번호와 개인 정보가 노출되고 있음을 사용자들에게 알리지 않음으로써 더 큰 위험요소를 만들어 내고 있을수 있기 때문이다. 가능한 빨리 사용자들에게 이 내용을 알려야된다(이 해킹 내용에 대해 정확이 인지하고 있지 않더라도). 웹 사이트 메인페이지에 이를 공지하고 상세 정보를 확인할 수 있는 링크를 걸어 놓고 모든 사용자들에게 이를 안내하는 메일을 보내도록 한다.

사용자들에게 비밀번호가 어떻게 안전하게 보관되고 있는지 설명해야되고(소금 값을 사용했기를 바라며) 비밀번호가 소금값으로 해시되어 있지만 악의적인 해커들은 단어 사전이나 무차별 공격으로 이를 해킹할 수 있다. 악성 해커들은 사용자들이 다른 웹사이트에 동일한 비멀번호를 사용했기를 기대하고 해킹한 비밀번호을 사용해서 다른 웹사이트에 로그인을 시도할 것이다. 이러한 위험성에 대해 사용자들에게 공지하고 비슷한 비밀번호를 사용하는 다른 웹사이트의 비밀번호를 변경하도록 제안한다. 사용자들이 시스템에 로그인할때 강제로 패스워드를 변경 하도록 하고 대부분의 사용자들이 이전 비밀번호를 빠르게 변경하기 위해서 이전 비밀번호와 동일하게 설정 할려고 하는데 이를 방지하는 작업도 해야 된다.

소금값과 함께 늦은 해쉬를 사용하더라도 해커들은 취약한 비밀번호들에 대해 매우 빠르게 해킹할 수 있다. 해커들이 이렇게 비밀번호를 찾아서 해킹할 가능성을 줄이기 위해서 비밀번호가 변경 되었을 때도 이를 인증하는 메일을 사용자에게 보내서 확인하도록 해야 된다. 

또한 사용자들에게 어떠한 개인정보가 저장되고 있는지 알려야 한다. 만약 신용카드 번호를 저장 하고 있다면 사용자들에게 신용카드를 재 발급 받도록 알려주고 이 카드 번호를 사용해 결제된 내용들에 대해 확인하도록 알려 줘야된다.

비밀번호 정책은 무엇이 되야 하나? 강력한 암호를 사용하도록 해야되나?
만약 서비스가 엄격한 보안 정책이 필요 하지 않다면 사용자들이 비밀번호를 설정하는데 제한을 둘 필요가 없다. 사용자들이 원하는 대로 비밀번호를 설정 할 수 있게 한다. 
만약 특별한 보안 정책이 필요 하다면 비밀번호는 최소 12자 이상을 사용하고 최소한 두 글자, 두 자리, 두 가지 특수 문자 이상을 사용하도록 한다.

사용자들에 매 6개월 이상으로 비밀번호를 강제로 변경하도록 하지 않는다. 비밀번호를 자주 바꾸도록 하면 사용자들이 이를 귀찮아해서 간단한 비밀번호를 사용할 가능성이 높아 진다. 

해커들이 데이터베이스에 접속 가능하면, 사용자의 비밀번호 해시를 그들이 생성한 해시로 바꾸고 로그인 할 수 있지 않나?
가능하다, 만약 데이터베이스에 접속할 수 있다면 해커들은 아마 서버에 있는 모든 것들에 접근 할 수 있을 것이고 따라서 그들이 필요 하지 않는한 별도로 사용자의 계정에 로그인할 필요는 없다. 암호 해시의 목적은 시스템 전체를 해킹하는것을 방어하는 것이 아니라 비밀번호 해킹이 발생하는 것을 막는 것이다.

데이터베이스의 계정을 사용자 계정을 생성할때 사용할 것과 로그인시 사용할 것을 분리해서 사용하면 로그인시 SQL Injection 공격을 사용해 비밀번호를 변경하는 것을 막을 수 있다.

Why do I have to use a special algorithm like HMAC? Why can't I just append the password to the secret key?

Hash functions like MD5, SHA1, and SHA2 use the Merkle–Damgård construction, which makes them vulnerable to what are known as length extension attacks. This means that given a hash H(X), an attacker can find the value of H(pad(X) + Y), for any other string Y, without knowing X. pad(X) is the padding function used by the hash.

This means that given a hash H(key + message), an attacker can compute H(pad(key + message) + extension), without knowing the key. If the hash was being used as a message authentication code, using the key to prevent an attacker from being able to modify the message and replace it with a different valid hash, the system has failed, since the attacker now has a valid hash of message + extension.

It is not clear how an attacker could use this attack to crack a password hash quicker. However, because of the attack, it is considered bad practice to use a plain hash function for keyed hashing. A clever cryptographer may one day come up with a clever way to use these attacks to make cracking faster, so use HMAC.


소금값을 암호 앞, 뒤 어드쪽에 붙여야 되나?
둘 중 아무거나 사용해도 상관 없다. 비밀번호 앞에 사용하는게 좀 더 일반적이긴 하다.

Why does the hashing code on this page compare the hashes in "length-constant" time?

Comparing the hashes in "length-constant" time ensures that an attacker cannot extract the hash of a password in an on-line system using a timing attack, then crack it off-line.

The standard way to check if two sequences of bytes (strings) are the same is to compare the first byte, then the second, then the third, and so on. As soon as you find a byte that isn't the same for both strings, you know they are different and can return a negative response immediately. If you make it through both strings without finding any bytes that differ, you know the strings are the same and can return a positive result. This means that comparing two strings can take a different amount of time depending on how much of the strings match.

For example, a standard comparison of the strings "xyzabc" and "abcxyz" would immediately see that the first character is different and wouldn't bother to check the rest of the string. On the other hand, when the strings "aaaaaaaaaaB" and "aaaaaaaaaaZ" are compared, the comparison algorithm scans through the block of "a" before it determins the strings are unequal.

Suppose an attacker wants to break into an on-line system that rate limits authentication attempts to one attempt per second. Also suppose the attacker knows all of the parameters to the password hash (salt, hash type, etc), except for the hash and (obviously) the password. If the attacker can get a precisise measurement of how long it takes the on-line system to compare the hash of the real password with the hash of a password the attacker provides, he can use the timing attack to extract part of the hash and crack it using an offline attack, bypassing the system's rate limiting.

First, the attacker finds 256 strings whose hashes begin with every possible byte. He sends each string to the on-line system, recording the amount of time it takes the system to respond. The string that takes the longest will be the one whose hash's first byte matches the real hash's first byte. The attacker now knows the first byte, and can continue the attack in a similar manner on the second byte, then the third, and so on. Once the attacker knows enough of the hash, he can use his own hardware to crack it, without being rate limited by the system.

It might seem like it would be impossible to run a timing attack over a network. However, it has been done, and has been shown to be practical. That's why the code on this page compares strings in a way that takes the same amount of time no matter how much of the strings match.



왜 해싱을 지루하게 생각하나?
사용자가 비밀번호를 입력하고 사이트에 로그인할때 이들은 이것이 보안 처리 되어 있을것으로 믿는다. 만약 데이터베이스가 해킹되고 사용자들의 비밀번호가 보호되지 않고 있다면 악성 해커들은 이 정보를 다른 웹사이트와 시스템에 사용할 것이다.(대부분의 사람들이 동일한 비밀번호를 사용한다) 이 문제는 단순히 해당 사이트만의 문제가 아니고 사용자들에 대한 문제이다. 시스템 담당자는 사용자들의 정보를 안전하게 관리해야될 책임이 있다.


신고

Comment +3

  • 키즈 2016.11.21 03:22 신고

    이해가 하나 안가는게 있습니다.

    salt값을 랜덤으로 하게 된다면

    암호 + salt 값의 해시, salt값을 저장하게 된다고 하셨는데요.

    그경우 다른 곳이 뚤려서 암호가 확보된 경우라면 어차피

    뚤리게 되지 않나요? salt값을 주는 의미를 잘 모르겠습니다.

    디비가 해킹되서 salt값만 노출되지 않는 경우에만 의미가 있지 않나요?

    • 어떤 방식으로든 암호가 확보된 경우라면, 소금 뿐만 아니라 어떤 보안대책도 의미가 없겠죠.
      다만 salt를 이용하는 경우는 직접적 방법 예를 들어 전수조사등의 방법을 이용해서 공격하는 경우에 대한 방어대책으로 쓰이는 것 같습니다.

      보안카드가 유출됐는데 세콤이 있으면 무슨 의미가 있죠? 라는 질문과 비슷하지 않나요?

  • 길손 2017.03.28 11:16 신고

    좋은글 감사합니다. 꾸벅..

이번에는 스프링에서 JMS를 어떻게 지원하고, 사용하는 방법에 대해서 알아보자.


위 이미지에서 보듯이 JMS API는 JDBC API 와 상당히 유사하다. 스프링에서 JDBC template을 제공 하는 것 처럼 JMS template도 제공한다.

JMS template 과 message listener container 는 스프링에서 JMS 메시징을 사용하는 핵심 컴포넌트 이다. Spring JMS template(JmsTeplate)는 동기적으로 메시지를 받거나 보내는데 사용한다. message listener container(DefaultMessageListenerContainer)는 MDP를 사용하여 비동기 메시지를 받는데 사용한다. 대부분의 자바 EE 애플리케이션 서버들과는 다르게(JBoss, WebSphere) 스프링은 자기 자신이 JMS 제공자가 되지 않는다. 이것은 외부의 JMS 제공자(ActiveMQ, JBoss Messaging, IBM WebSphereMQ)가 스프링에서 메시징을 하기 위해 필요한 것이다. JMS template 과 message listener container 의 목적은 개발자가 JMS 제공자에 대한 자세한 JMS 연결, JMS 세션 그리고 JMS message producer 와 message consumer를 만드는것에 연결정보를 필요로 하는 것을 격리한다.

고 레벨 아키텍쳐


스프링 JMS 템플릿(JmsTemplate)은 메시지를 동기적으로 주고 받기 위한 primary interface다. JNDI를 사용할 때, JmsTemplate 여러 스프링 오프젝트들이 JMS 제공자에 연결하는데 이것을을 포함하여 사용한다. JndiTemplate, JndiObjectFactoryBean, JndiDestinationResolver 와 CachingConnectionFactory(아니면 SingleConnectionFactory)
JndiTempate bean은 제공자 URL, 보안 사용자와 JMS 제공자에 연결하기 위한 보안 자격 증명의 팩토리를 초기화 하는데 사용한다. JndiObjectFactoryBean 을 경유하여 JMS 커넥션 팩토리를 정의 하는데 사용되고, JMS destination은 JndiDestinationResolver를 통해 사용된다. 
아래 그림은 Spring JNDI object와 JMS provider와 애플리케이션 사이의 관계와 협력 관계를 표시한다.

 
메시지를 비동기적으로 받기 위해서, 스프링은 message listener container(DefaultMessageListener or SimpleMessageListenerContainer)를 제공 한다. 이것은 MDP를 만들기 위해서 사용된다. 첫 눈에 보이듯이 MDP는 Java EE 명세에서 보이는 message-driven bean과 유사함을 알 수 있다. 하지만 스프링 MDP는 MDB 보다 조금 더 유연성을 제공한다. non-message-aware POJO를 통해 Spring MDP를 생성 할 수 있다. 반면에 MDS는 EJB 3 명세를 준수해서 만들어야 한다. 즉 오브젝트는 반드시 javax.jms.MessageListener interface를 구현해야 하고 onMessage메서드를 override 해야 하고 
destination type(e.g., javax.jms.Queue or javax.jms.Topic)을 포함 하는 @MessagDrieven 어노테이션이나 XML을 제공 해야 합니다. 그리고 JNDI destiname도 포함하여야 합니다.

JmsTemplate 처럼 message listener container는 JndiTemplate, JndiObjectFactoryBean, JndiDestinationResolver와 CachingConnectionFactory 사이에 JMS provider와 연결을 위해 사용되고 비동기 listener를 시작하는데 사용합니다. message-driven bean과는 달리 스프링은 MDP를 만들기 위한 3가지 다른 방법을 제공 합니다. 아래 그림은 비동기 메시지를 받기위한 오브젝트들의 관계를 표현합니다.


JmsTemplate 개요

JmsTemplate은 동기적으로 메세지를 주고 받는데 기본오브젝트이다(메시지를 받기위해 대기하는 동안 블록킹함). JMS 1.1(JmsTemplate) 과 JMS 1.0.2(JmsTemplate102) 버전이 있다. 대부분의 JMS 프로바이더와 JMS EE 서버들은 JMS 1.1을 지원 하므로 JMS1.1을 사용할 것이다. JmsTemplate 를 사용하면 개발에 들어가는 노력과 시간을 상당부분 줄여준다. JmsTemplate를 사용할 때 JMS 프로바이더와 연결할때, JMS session(Queue Session)을 만들때나 message producer(Queue Sender)를 만들거나 또는 JMS message를 만드는데 대한 걱정을 할 필요가 없다. JmsTemplate는 자동적으로 String 오브젝트, Byte[] Object, Java Object 나 java.util.Map 에 해당하는 JMS 메시지 오브젝트로 변환해준다. 또한 자신의 메시지 컨버터를 제공하여 복잡한 메시지나 기본 메시지 컨버터가 지원 하지 않는 것을 해결 할 수 있다. 
아래 코드는 스프링을 사용하여 간단한 텍스트 메시지를 전송하는 예제이다.

public class SimpleJMSSender {

public static void main(String[] args) {

try {

ApplicationContext ctx =

new ClassPathXmlApplicationContext("app-context.xml");

JmsTemplate jmsTemplate =

(JmsTemplate)ctx.getBean("jmsTemplate");

jmsTemplate.convertAndSend("This is easy!");

}

...

}

}




대충 생략

메시지 드리븐 POJOs(Message-Driven POJOs)
메시지를 비 동기 적으로 받는 다는 것은 특정 큐나 토픽에 대해 응답 개기중에 논 블록킹 프로세스를 가지고 있다는 것이다. 이 기술은 이벤트 드리븐 형태의 처리 방식이다. 메세지 리스너에서 메시지가 존재하는것을 알려준다. Message-driven beans는 비동기 리스너를 위한 Java EE 기술이다. 스프링 프레인 워크는 MDP를 사용한 비동기 리스너 역시 지원 한다.
스프링에서 비동기 메시지 리스너를 구성하기위한 3가지 방법이 있다. javax.jms.MessageListener interface, implementing Spring’s SessionAwareMessageListener를 구현 하거나 POJO 클래스를 Spring MessageListenerAdapter 클래스로 랩핑 하면 된다. 이 3가지 방법은 메시지 리스너 클래스가 어떻게 구성되어 있느야에 따라 다양하다. 아래 설명에서는 메시지 리스너 컨테이너에 대해 상세한 설명과 함께 3가지 message driven bean 기술을 배우게 된다.

The Spring Message Listener Container
Message-driven POJOs 는 메시지 리스너 컨테이너의 context에서 생성 된다. 메시지 리스너 컨테이너는 connection factory, JMS destination, JNDI destination resolver 와 message listener bean을 바인딩 한다. 스프링은 두가지 타입의 메시지 리스너컨테이너를 제공한다(DefaultMessageListenerContainer 와 SimpleMessageListenerContainer). 이러한 메시지 리스너 컨테이너 들은 비동기 리스너 스레드를 명시해 주어야 한다. 
DefaultMessageListenerContainer 만이 실행시에 동적으로 리스너 숫자를 조절 할 수 있다. 추가로 DefaultMessageListenerContainer 는 XA 트랜잭션과의 통합을 지원 하는 반면에 SimpleMessageListenerContainer는 지원하지 않는다.  로컬 트랜잭션 메니저를 사용 하고 쓰레드 및 세션, 부하조건 변화에 따라 연결 조정을 사용 하는 간단한 애플리케이션의 경우 SimpleMessageListenerContainer를 사용하면 된다. 외부 트랜잭션 매니저나 XA 트랜잭션을 사용하고, 튜닝이 필요한 메시지 애플리케이션의 경우는 DefaultMessageListenerContainer를 사용하면 된다. 

MDP Option1 : MessageListener 인터페이스 사용하기
message-driven POJO의 가장 간편한 형태는 javax.jms.MessageListener 인터페이스를 구현한 비동기 receiver를 사용하는 것이다. 이것은 EJB3 의 message0driven bean 과 비슷하다. DefaultMessageListenerContainer는 CachingConnectionFactory 와 JNDIDestinationResolver에 주입 된다.



MessageListener 인터페이스를 구현할 때, message listener 클래스에 onMessage를 반드시 오버라이드 하여야 한다. 이 방법을 사용할때에 XML 설정에서 바꿔야할 것은 없다. 아래 코드 예제를 보면, javax.jms.MessageListener를 구현하고 onMessage 메서드를 오버라이드하고 TextMessage를 받는 SimpleJMSReceiver 메시지 리스너이다.

public class SimpleJMSReceiver implements MessageListener {
public void onMessage(Message message) {
try {
if (message instanceof TextMessage) {
System.out.println(((TextMessage)message).getText());
} else {
throw new IllegalStateException("Message Type Not Supported");
}
} catch (JMSException e) {
e.printStackTrace();
}
}
}

MDP Option 2: SessionAwareMessageListener 인터페이스 사용하기

스프링 프레임 워크는 javax.jms.MessageListener를 확장한 SessionAwareMessageListener를 제공한다. javax.jms.MessageListener 처럼,
SessionAwareMessageListener는 listener클래스에서 onMessage  메서드를 오버라이드 해야 한다. 그러나 javax.jms.MessageListener는 다르게 Message 오브젝트에 추가되는게 있다. SessionAwareMessageListener는 JMS Session에 접근할수 있도록 해준다.

void onMessage(Message message, Session session) throws JMSException

아래 그림은
SessionAwareMessageListener의 사용방법이다. 설정방식의 관점에서 보면 이것은 javax.jms.Mes
sageListener를 사용할 때와 같음을 알 수 있다.


만약 비동기 메시지 리스너에서 JMS Session 오브젝트에 접근하여야 할 경우 SessionAwareMessageListener는 유용하게 사용된다. 이 방식의 일방적인 사용 방법중에 하나는 응답 메시지를 보내는 쪽에 전달해야 할 경우 이다. 다른 사용 방식은 Session에 트랜잭션을 적용할 경우 이다. 아래의 간단한 예제에 대하여 생각해보자. SimpleJMSReceiver 클래스가 보낸쪽에 메시지들 돌려주고 JMSReplyTo 헤더 속성에서 이 메시지를 처리 했을을 나타 낸다.

public class SimpleJMSReceiver implements SessionAwareMessageListener {
public void onMessage(Message message, Session session) throws JMSException {
if (message instanceof TextMessage) {
String text = ((TextMessage)message).getText();
System.out.println(text);
//send the response
MessageProducer sender =
session.createProducer(message.getJMSReplyTo());
TextMessage msg = session.createTextMessage();
msg.setJMSCorrelationID(message.getJMSMessageID());
msg.setText("Message " + message.getJMSMessageID() + " received");
sender.send(msg);
} else {
throw new IllegalStateException("Message type not supported");
}
}
}

메시지를 보낸 후에 Session 오브젝트를 스스로 clean up 할 필요가 없음을 기억하자. 스프링이 JmsTemplate에서 다 처리해 준다. 또한 SessionAwareMessageListener의 onMessage 리스너는 JMSException을 던진다.(javax.jms.MessageListener에서 하지 않는)

MDP Option 3 : MessageListenerAdapter 사용하기.
비동기 메시지 리스너를 만드는 3번째 방법은 스프링 MessageListenerAdapter 오브젝트로 POJO 오브젝트를 덮어 씌우는 방법이다. 이 방법이 다른 두가지 방법과 다른 이유는 POJO receiver 클래스가 어떤 message listener인터페이스도 구현하지 않아도 되고, javax.jms.Message 오브젝트의 어떤것도 포함하지 않아도 되서 이다. 아래 그림을 보면 POJO receiver 클랫는 스프링 MessageListenerAdapter에 주입 된다.
MessageListenerAdapter에를 사용해서 POJO receiver의 메서드를 구성하는 방법이 몇가지 있다. 기본 메시지를 핸들링 하는 메서드를 MessageListenerAdapter를 통해 사용하거나 별도의 메서드를 리스너 클래스에서 지정해서 리스너 메서드로 사용할 수 있다. 후자를 사용하는 경우, 자바 객체 타입의 메시지를 변환하는 컨버터를 사용하던지 직접 JMS message 객체를 사용하는 방식을 사용할 수 있다. 이 두가지 예제는 아래 섹션에서 살펴보도록 하자.


Default message handler method
기본적으로, MessageListenerAdaptor는 handlmessage 메서드를 JMS 메시지가 수신되고 이에 상응하는 
POJO에서 찾습니다.
아래의 목록은 자동 메시지 변환을 사용하는  handleMessage 메서드 목록이다.

//receive a converted TextMessage
public void handleMessage(String message) {...}
//receive a converted BytesMessage
public void handleMessage(byte[ ] message) {...}
//receive a converted MapMessage
public void handleMessage(Map message) {...}
//receive a converted ObjectMessage
public void handleMessage(Object message) {...}

기본 message listner handler method를 사용하기 위해 message-driven POJO(eg, SimpleJMSReceiver)를 MessageListenerAdapter 빈에다가 생성자 속성을 통해 주입해 주어야 한다.(아니면 변수의 프로퍼티를 통해)
<bean id="messageListener" class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
<constructor-arg>
<bean class="SimpleJMSReceiver"/>
</constructor-arg>
</bean>
<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="queueConnectionFactory"/>
<property name="destinationResolver" ref="destinationResolver"/>
<property name="concurrentConsumers" value="3" />
<property name="destinationName" value="queue1"/>
<property name="messageListener" ref="messageListener" />
</bean>

message-driven POJO를 정의 할 때 간단하게 사용하고자 하는 JMS message 타입에 따라 handlerMessage 메서드를 정의 할 수 있습니다. 예를 들어 아래 코드는 JMS TextMessage를 받는다.

public class SimpleJMSReceiver {
public void handleMessage(String message) {
System.out.println(message);
}
}

SimpleJMSReceiver 클래스는 JMS API에 대한 어떠한 참조도 가지고 있지 않다는 것을 확인하자. 실제로, 이 예제 안에서 message-driven POJO는 심지어 컨텍스트 안에서 메시징이 사용되는지에 대해서 알고 있는것이 아무것도 없다. 모든 메시징 인프라 스트럭쳐는 MessageListenerAdapter 나 DefaultMessageListenerContainer를 통해 전달하도록 되어 있다. 당신이 할 일은 JMS 메시지가 수신되고 이 유형에 따라 필요한 POJO handleMessage 메서드를 작성 해야 한다.

만약 수신되는 JMS 메시지 타입이 확실하지 않거나 TextMessage 나 MapMessage 중에 하나를 받을 가능성이 있다면 어떻게 해야 할까? 이전 예제에서 처럼 JMS Message Object의 instance를 확인 하거나
Message Type에 따라 즉시 처리할수 있다.

public class SimpleJMSReceiver implements MessageListener {
public void onMessage(Message message) {
try {
if (message instanceof TextMessage) {
//process message text...
} else if (message instanceof MapMessage) {
//process map message...
} else {
throw new IllegalStateException("Message Type Not Supported");
}
} catch (JMSException e) {
e.printStackTrace();
}
}
}


그러나 default handleMessage 메서드에서의 인수가 이미 받을 타입에 대해 "캐스팅" 되어 있을 경우가 있다. 스프링은 몇가지 방법으로 이것을 처리 한다. 특정 메시지 유형에 대한 default handleMessage 메서드가 정의 되지 않는 경우, MessageListenerAdapter는 NoSuchMethodException을 표시하고 JMS 메시징 타입을 찾을수 없게 된다.
따라서 handleMessage 메서드에서 각각 타입에 대해서 받기 원하는 파라미터를 명시 해야 한다. 예를 들어 TextMessage 나 MapMessage 메시지 타입을 인수로 받을 려면 HandleMessage aptjemrk String 이나 Map 파라미터를 받을 수 있도록 하면 된다.

public class SimpleJMSReceiver {
public void handleMessage(String message) {
//process String message body
}
public void handleMessage(Map message) {
//process Map message body
}
}

방금 설명된 메시지 변환 방식의 한가지 이슈는 message handler 메서드를 통해서만 메시지가 전달 되어야 한다는 점이다. 따라서 메시지 헤더 프로퍼티나 메시지 애플리케이션 프로퍼티에는 접근하거나 수정 할 수 없다. 예를 들어 발신자가 애플리케이션 프로퍼티 섹션에 추가적인 메타 정보를 기입한 후 전송하려고 하거나 메시지 헤더 프로퍼티의 JMSReplyTo, JMSMessageID 속성에 접근해야 할 경우 사용 할 수 없다. 이 예제들에서 메시지들을 자동으로 변환하는 것들에 대한 것을 DefaultMessagelistenerContainer에 알려줄 수 있다. MessageListenerAdapter 빈에  messageConverter 프로퍼티에다가 값을 null로 세팅함으로써 쉽게 사용할 수 있다. JMS 메시지 개체를 인수로 받는 방법보다 해당 자바 객체 유형을 받아서 처리 할 수 있다.

<bean id="messageListener"
class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
<constructor-arg>
<bean class="SimpleJMSReceiver"/>
</constructor-arg>
<property name="messageConverter"><null/></property>
</bean>

message conversion 기능을 중지 시키면 MessageListenerAdaptor는 기본적으로 아래의 handleMessage 메서드들 중에 하나를 찾는다.

//receive a JMS TextMessage
public void handleMessage(TextMessage message) {...}

//receive a JMS BytesMessage
public void handleMessage(BytesMessage message) {...}

//receive a JMS MapMessage
public void handleMessage(MapMessage message) {...}

//receive a JMS ObjectMessage
public void handleMessage(ObjectMessage message) {...}

//receive a JMS StreamMessage
public void handleMessage(StreamMessage message) {...}

이 방법은 JMS Message 오브젝트에 접근할 수 있는 방법을 제공해준다. 이 방법을 통해 헤더나 애플리케이션 정보를 뽑아내거나 수정 할 수 있다.

public class SimpleJMSReceiver {
public void handleMessage(TextMessage message) {
String text = message.getText();
String username = message.getStringProperty("username");
String msgId = message.getJMSMessageID();
//process text message
}
}

Custom message handler method

당연히 default handlerMessage 메소드들에  POJO 메시지 리서너를 제한할 필요가 없다. 사실 POJO 메시지 리스너들 중에  JMS Message Type이나 메시지 컨버젼 오브젝트(String, byte[], Map, or Object)둘중에 한개의 파라미터를 포함한
아무 메서드나 listener handler 메서드가 될수 있다. 자신만의 메서드를 message handler 처럼 사용할려면 반드시 MessageListenerAdapter의 defaultListenerMethod 프로퍼티에 메시지 핸들러로 사용하기 위한 메서드명을 적어 줘야 한다.
또한 MessageLIstenerAdapter는 메시지 본문을 변환하거나 message handelr message에 JMS Message type을 처리 하기 위한 것을 명시해 줘야 한다. 예를 들어,
TradeOrderManager 클래스에서 XML 거래 주문을 포함한 String 오브젝트를 처리하기 위한 createTradeOrder 메서드를MessageListenerAdapter에 구성해야 할 경우, defaultListenerMothod 프로퍼티에 createTradeOrder를 명시해 주고 SimpleMessageConverter를 사용하면 된다.

<bean id="messageListener"
class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
<constructor-arg>
<bean class="TradeOrderManager"/>
</constructor-arg>
<property name="defaultListenerMethod" value="createTradeOrder"/>
</bean>

POJO 메시지 리스너에서 String Object 파라미터를 받는 createTradeOrder메서드는 아래와 같다.

public class TradeOrderManager {
public void createTradeOrder(String xml) {
//process trade order xml message
...
}
...
}

그냥 표시된 코드를 공부할 경우, 이 POJO가 메시징과 관련된 것이 하나도 없다는 것을 볼 수 있다. 이것은 message-driven POJOs 의 예제이다. 메시징과 커뮤니케이션 로직을 POJO로 부터 분리함으로써 코드를 추상화 할 수 있고 POJO가 메시징 인프라 구조 로직 보다 비즈니스 로직에 촛점을 맞출 수 있다. 이 클래스는 메시징 컨텍스트의 안이나 밖에서 사용될 수 있고, 메시징 프레임워크 바깥에서 테스트할 수 있다.

신고

Comment +0

티스토리 툴바