Diary Java Technical

Modern Java in Action

0. 시작하며

최근에는 언어 스킬로 Go를 습득하고 있는데, 우연하게 제목과 같은 “Modern Java in Action” 이라는 책을 맞이하게 되었다. 주로 사용하는 언어가 Java이기도 하지만, 설마 책을 통해서 Java를 배워야 할 것들이 있을까? 라는 생각으로 사실 열어보기 까지 시간도 걸렸다.

그러나, 사실 최근 Java을 사용하면서 많이 변했네… 라는 생각을 가지고 있었지만, Spring 이라는 잘 만들어진 틀을 쓰다보면, 객체지향적인 느낌의 언어의 사고는 사라졌고, 그져 빠르게 결과만 만들어 내는 추세로 가고 있다고 생각한다.

이런 과정에서 문법적인 특성에 Java 8부터 많이 바뀐것들을 실전에서 사용해 보고 있었는데, 그 과정에서 인터넷 서핑을 통해 정식적이지 않은 화려하면서, 때론 이상하다고 느껴지던 패턴들이 이번에 맞이한 이 책에서는 정말 깔끔하게 논리적으로 정리되어 있다.

오늘 부터는 책의 내용을 이해하고, 정리하는 차원에서 조금씩 채워나가고자 한다. 2019년 8월에 초판이 발행되었고, 재판이 될때마다 보강되는 Chapter도 몇장씩이나 해주는 훌륭한 도서다. 게다가 총 페이지는 700페이지 수준에 달해서 사실 읽는 다는 거 자체에 마음의 다짐이 필요하다.

1. Java8, Java9, Java10의 변화

Java8의 변화는 20년만의 큰 변화라고들 한다. 맞다. 그러나 인지하지 못하고 있을 수도 있다. 그도 그럴것이 기존의 문법적인 부분을 그대로 사용이 가능하다. 새로운 방법을 쓰기 위해 기존 방법을 사용못하거나, 문법이 바뀌거나 하는 것들이 거의 없다.

개인적으로 Java는 1.4 (당시 Java2라고 불리던)에서 대한민국에서 수많은 개발자를 양성하던 버전이 있고, 이후에 Java1.5(Java5라고 불렀다)에서 nio 패키지와 Generic의 등장으로 다시 한번 큰 변화를 주었다고 생각한다. nio 이후에 Java는 느려. 라는 표현이 차즘 줄어들게 되었다고도 생각한다. (언어 특성적인 것보다는 코딩속에 불필요한 If, Loop 절 등 잘못된 습관속에서 구현한 것들이 저하를 유발하고, 사실 이런것들도 H/W로 밀어 붙였던 것 같다)

그러나 Java8에서는 Stream과 Lambda의 등장으로 기존 방식을 유지하면서, 사용에 불편함 없이 새로움을 느낄수 있도록 제시했다. Stream은 사실 Collection과 유사하지만, Collection을 제어할 때 사용하던 Loop(For, While) 순환을 외부에 의존해서 하던 방식에서 Stream은 SQL 사용하는 질의 언어와 같이 내부 순환으로 처리 할 수 있다. 내부 순환이라는 것은 병렬성에도 큰 효과를 발휘한다. 이런 큰 변화는 구현적인 측면에서도 Collection을 사용하면서 Loop에서 필요한 변수나 로직 그리고 데이터 가공 과정에서 중간 변수등 많은 부분에서 들어가도 노력도 다 축소된다.

Lambda는 흔히 “->” 이거로 표시하지만, 메서드 참조 “::” 와 같이 사용하여, 코드의 가독성이 월등히 높아진다. 거기다가 Functional Programming 기법이 도입되어, 함수를 인자로 전달하고 실행하는 기상천외한 일도 일어난다. 함수 호출에 인자가 함수인 것이다.

여기까지만 Java8의 변화이고, Java9에서는 불변 Collection이 제공되었다. Python의 튜플이라는 방식과 유사한데, Java에서는 그와 같은 방식으로 문법을 제공하기에는 혼란의 여지가 있어서, Collection.of 형식으로 제공하게 되었다. List.of, Map.of 등으로 제공된다. 이렇게 제공된 Collection은 불변이 되어 수정이 불가능하다. Array.asList로 기존에 사용하던 방식과 머가 다르냐 라고 할수 있겠지만, 이 방식은 add 를 제공하지 않는 List 일 뿐이고, 수정은 가능하다.

Java 10은 Java8(2014년 3월) -> Java9(2017년 9월) 이라는 3년의 기간이 아니라, 2018년 3월의 6개월만에 발표하게 되었고, 이제부터는 3월, 9월인 6개월 마다 발표하신다고 한다. Java 10에서는 형 추론 관련된 상항이 추가되었다는데, 자세히는 모르는 분야이지만, 변경사항들을 보면 주로 내부적인 처리에 변화가 많아 보인다. Cloud 개발자를 지원하기 위한 구조적인 형태로 전환된 시기가 아닌가 싶다.

2. Stream

책에도 언급되지만, Java는 출발부터 좋았다. 유용한 많은 라이브러리를 쉽게 사용할 수 있는 방식이 좋았고, 쓰레드와 락에 대한 개념도 포함되어 있는데다가, 객체지향적인 사고로 “모든것은 객체다”라는 설계적 방식에도 최고였다. 그러나 생태계의 변화 흐름에 따라 빅데이터라는 것이 생겨나고 비규칙적이고, 대용량 데이터를 처리함에 있어서는 더욱 빠른 병렬 프로세싱이 요구되고 있었다.

기존의 방식으로는 구현 코드의 양도 많고, 쓰레드 개념이 있다고 하지만 사용자가 일일이 제어하는 방식으로는 사용하기가 쉽지 않다는 것이다.

이 과정에서 등장한 것이 Stream 처리이다. Stream 의 의미는 한번에 한개씩 만들어지는 연속적인 데이터 항목들의 모임이다. 유닉스 명령어의 “|”(pipe)와 같이 출력이 다음 프로그램의 입력이 되어 명령을 연결하는 형태와 유사하다.

java.util.stream 패키지에 추가되었고, 기존과의 다르게 한번에 하나씩의 명령이 아닌, SQL 질의 처럼 고수준으로 추상하여 일련의 스트림으로 처리 가능하다는 것이다.

-- 일반 Loop
Map<Currency List<Transaction» transactions>> transactionsByCurrencies = new HashMap<>(); // 그룹화하여 결과를 저장할 Map

for (Transaction transaction : transactions) {                  // 트랜잭션 Loop
    if (transaction.getPrice() > 1000) {                        // 조건 필터
        Currency currency = transaction.getCurrency();         // 특정 값 추출
        List<Transaction> transactionsForCurrency = transactionsByCurrencies.get(currency); 

        if (transactionsForCurrency == null) {                  // 값이 없다면, 새로 생성
            transactionsForCurrency = new ArrayList<>();
            transactionsByCurrencies.put(currency, transactionsForCurrency);        // 그룹화 결과 저장
        }
    }

    transactionsForCurrency.add(transaction);                   // 값이 있다면, 결과 바로 저장
}

-- Stream
Map<Currency, List<Transaction>> transactionsByCurrencies = 
    transactions.stream()
        .filter((Transaction t) -> t.getPrice() > 1000)         // 조건 필터
        .collect(groupingBy(Transaction::getCurrency));         // 특정 값으로 그룹화

코드 예제를 보면, 위에 코드는 전형적이 외부순환 방식으로 특정 조건에 따라 데이터를 필터 하고, 특정 값을 추출한 후에 그 값을 Key로 있으면 바로 add 하고, 없다면 List를 만들어서 put 하면서 넣는 방식이다. 자료구조적으로 Key, Value(List) 형태로 많이 사용하는 그룹화 형태다.

그러나, Stream을 사용하면 마치 SQL 질의 하듯이 가능하다. stream 이라는 키워드로 시작하여, filter로 조건을 걸고 collect로 Collection을 만들어 내는데, 이때 grouping 키워드를 명시하여 필요했던 Key, Value(List) Map으로 바로 만들어 진다.

코드도 간결해 지고, 이해하기도 쉽다. 게다가 stream 이라는 내부순환에 의한 부분은 pararrelStream 이라고 변경하면 병렬수행도 가능하다.

3. Lambda

public static List<Apple> filterGreenApples(List<Apple> inventory) {
    List<Apple> result = new ArrayList<>();

    for (Apple apple : inventory) {
        if (GREEN.equals(apple.getColor())) {
            result.add(apple);
        }
    }

    return result;
}

public static List<Apple> filterHeavyApples(List<Apple> inventory) {
    List<Apple> result = new ArrayList<>();

    for (Apple apple : inventory) {
        if (apple.getWeight() > 150) {
            result.add(apple);
        }
    }

    return result;
}

일반적으로 사용하는 코드이다. inventory를 순환하면서, green 사과를 식별하는 함수가 있고, 무게 150 이상의 무거운 사과를 식별하는 2개 함수를 조건 필터 하는 함수이다. 지금까지의 Java에서 사용하는 방식이지만, 이 방법을 쓰다보면 마음 한쪽에서 무언가가 올라온다. 거의다 같은 코드 인데, 이걸 복붙해서 만들어야 하는걸까… 물론 좋은 방법이 많겠지만.. Lambda로 넘어가기 위한 좋은 떡밥인건 확실하다.

public static boolean isGreenApple(Apple apple) {
    return GREEN.equals(apple.getColor());
}

public static boolean isHeavyApple(Apple apple) {
    return apple.getWeight() > 150;
}

public interface Predicate<T> {
    boolean test(T t);
}

static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
        if (p.test(apple)) {
            result.add(apple);
        }
    }
    return result;
}


filterApples(inventory, Apple::isGreenApple);

filterApples(inventory, Apple::isHeavyApple);

isGreenApple과 isHeavyApple 이라는 조건에 일치하는 내용만 담은 함수를 만들자. 그리고 참과 거짓을 제공하는 Predicate 를 정의하고, 이제 함수를 인자로 전달할수 있는 함수를 만들자. filterApples라는 함수가 그 역활을 한다. Predicate가 인수로 함수를 받아 boolean을 반환하는 형태로 사용된다.

filterApples(inventory, Apple::isGreenApple) 과 filterApples(inventory, Apple::isHeavyApple)을 사용하여 함수를 전달하고 각 함수의 기능을 사용한다. 이과정에서 사용된 방식이 메서드 참조방식이고, 이는 Lambda로 아래와 같이 다시 바꿀수 있다.

filterApples(inventory, (Apple a) -> GREEN.eqauls(a.getColor());
filterApples(inventory, (Apple a) -> a.getWeight() > 150);

filterApples(inventory, (Apple a) -> a.getWeight() > 150 && GREEN.equals(a.getColor());
filterApples(inventory, (Apple a) -> a.getWeight() < 100 && RED.equals(a.getColor());

위와 같이 1회성으로 사용하는 메소드라면 처음에 사용한 것과 달리 선언과정 없이 직접 구현내용을 사용할 수 있다. Lambda 내용이 짧지 않고 길어진다면, 별도의 메소드 이름을 식별가능한 좋은 이름으로 정의하고 처음과 같이 메소드 참조를 사용하는 것이 더 현명하다. 어디까지는 기교가 아닌 가독성을 항상 최우선해야 한다는 의미이다.

4. 마무리

별 내용 기술하지 않았지만, 아마도 처음본 사람에게는 놀람이 작지 않을 것 같다. 2014년이면, 지금으로 부터 6년 전에 나왔다는 사실에 놀랍고, Java 가 참 많이 달라졌다는 것에 정말 놀랍다. 실전에서는 물론 이것보다 훨씬 더 한 내용으로도 잘 쓰고 있고, 효과도 많이 보고 있다. 앞으로 이 내용에 대해서는 더 추가적인 내용으로 계속 작성하도록하여, 생각도 정리하고 내용도 공유 할 수 있도록 하겠다.

댓글 남기기

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.

%d 블로거가 이것을 좋아합니다: