모던 자바 인 액션 Chapter 1

w-beom·2021년 1월 24일
0

모던 자바 인 액션

목록 보기
1/1
post-thumbnail

자바 8, 9, 10, 11 무슨 일이 일어나고 있는가?

자연어에 가까운 코드

자바 역사를 통틀어 가장 큰 변화가 자바 8에서 일어났다. 예를 들어 다음은 사과 목록을 무게순으로 정렬하는 고전적 코드다.

Collections.sort(inventory, new Comparator<Apple>() {
    public int compare(Apple a1, Apple a2){
        return a1.getWeight().compareTo(a2.getWeight);
    }
});

자바 8을 이용하면 자연어에 더 가깝게 간단한 방식으로 코드를 구현할 수 있다.

inventory.sort(comparing(Apple::getWeight));

자바 8에서 제공하는 새로운 기술

  • 스트림 API
  • 메서드에 코드를 전달하는 기법
  • 인터페이스의 디폴트 메서드

1. 스트림 API

거의 모든 자바 애플리케이션은 Collection 을 만들고 활용한다. 하지만 Collection으로 모든 문제가 해결되는 것은 아니다.

아래의 예제 코드를 보도록 하자.

//리스트에서 고가의 거래만 필터링을 한다음 통화로 결과를 그룹화하는 코드이다.

Map<Currency, List<Transaction>> transactionsByCurrencies = new HasMap<>();
for (Transaction transaction : transactions){
    if (transaction.getPrice() > 1000){
        Currency currency = transaction.getCurrenct();
        List<Transaction> transactionsForCurrency = transactionsByCurrencies.get(currency);
        if(transactionsForCurrenc == null){
            transactionsForCurrency = new ArrayList<>();
            transactionsByCurrencies.put(currency, transactionsForCurrency);
        }
        transactionsForCurrency.add(transaction);
    }
}

스트림 API를 이용하면 다음처럴 코드를 작성할 수 있다.

Map<Currency, List<Transaction>> transactionsByCurrencies = transactions.stream()
                .filter((Transaction t) -> t.getPrice() > 1000)
                .collect(groupingBy(Transaction::getCurrency));
  • 자바 8에는 java.util.stream 패키지에 스트림 API가 추가되었다. 스트림 패키지에 정의된 Stream는 T 형식으로 구성된 일련의 항목을 의미한다.
  • 스트림 API의 핵심은 기존에는 한 번에 한 항목을 처리했지만 자바 8에서는 우리가 하려는 작업을 고수준으로 추상화해서 일련의 스트림으로 만들어 처리할 수 있다.
  • 파이프라인을 이용해서 입력 부분을 여러 CPU 코어에 쉽게 할당할 수 있다는 부가적인 이득도 얻을 수 있다.
  • 스레드라는 복잡한 작업을 사용하지 않으면서도 공짜로 병렬성을 얻을 수 있다.

2. 메서드에 코드를 전달하는 기법

  • 코드 일부를 API로 전달하는 기능이다.
  • 메서드에 코드를 전달하는 기법을 이용하면 새롭고 간결한 방식으로 동작 파라미터화를 구현할 수 있다.

3. 인터페이스의 디폴트 메서드

😮 디폴트 메서드의 탄생배경

//inventory에서 Apple의 무게가 150이 넘는 Apple객체만 필터링하는 예제
List<Apple> heavyApples1 = inventory.stream()
        .filter((Apple a) -> a.getWeight() > 150)
        .collect(toList());

자바 8 이전에는 List<T>stream 메서드를 지원하지 않는다는 것이 문제다.

따라서 위 예제는 컴파일 할 수 없는 코드다. 가장 간단한 해결책은 직접 인터페이스를 만들어 Collection인터페이스에 stream메서드를 추가하고 ArrayList 클래스에서 메서드를 구현하는 것이다.

하지만 이 방법은 사용자에게 너무 큰 고통을 안겨준다. 이미 CollectionAPI의 인터페이스를 구현하는 많은 Collection프레임워크가 존재한다. 인터페이스에 새로운 메서드를 추가한다면 인터페이스를 구현하는 모든 클래스는 새로 추가된 메서드를 구현해야 한다.

🙄여기서 우리는 딜레마에 빠진다. 어떻게 기존의 구현을 고치지 않고도 이미 공개된 인터페이스를 변경할 수 있을까?

자바 8에서는 구현 클래스에서 구현하지 않아도 되는 메서드를 인터페이스에 추가할 수 있는 기능을 제공한다. 메서드 본문은 클래스 구현이 아니라 인터페이스의 일부로 포함된다. 그래서 이를 디폴트 메서드 default method라 부른다.

아래의 코드는 List인터페이스에 추가된 default method이다.

default void sort(Comparator<? super E> c) {
    Object[] a = this.toArray();
    Arrays.sort(a, (Comparator) c);
    ListIterator<E> i = this.listIterator();
    for (Object e : a) {
        i.next();
        i.set((E) e);
    }
}

일급 객체(First Class Citizen)란?

일급 객체는 아래 3개의 조건을 충족하는 객체를 1급객체라고 정의할 수 있다.

  1. 변수와 데이터에 할당 할 수 있어야 한다.
  2. 객체의 인자로 넘길 수 있어야 한다.
  3. 객체의 리턴값으로 리턴 할 수 있어야 한다.

🙄 JAVA에서 메서드는 일급 객체인지 알아보자

// name을 가져오는 메서드가 하나 존재한다.
public String getName(){
  //TODO
}

// 조건1. 변수와 데이터에 할당 할 수 있어야 한다.
var name = getName;// Error # 변수에 메서드를 할당할 수 없다.

// 조건2. 객체의 인자로 넘길 수 있어야한다.
findByName(getName); // Error # 메서드의 인자로 메서드를 넘길 수 없다.

// 조건3. 객체의 리턴값으로 리턴 할 수 있어야 한다.
public doSomething() {
   return getName; //Error #return 값으로 메서드를 넘길 수 없다
}

//결론 JAVA에서 메소드는 일급객체가 아니다.

만약 런타임 시점에 메서드를 전달할 수 있다면, 즉 메서드를 일급 객체로 만들면 프로그래밍에 유용하게 활용할 수 있다. 따라서 자바 8 설계자들은 이급 객체를 일급 객체로 바꿀수 있는 기능을 추가했다.

메서드와 람다를 일급객체로

메서드를 일급객체로 사용하면 프로그래머가 활용할 수 있는 도구가 다양해지면서 프로그래밍이 수월해진다는 사실을 이미 실험을 통해 확인했다.
그래서 자바 8의 설계자들은 메서드를 값으로 취급할 수 있게, 프로그래머들이 더 쉽게 프로그램을 구현할 수 있는 환경이 제공되도록 자바8을 설계하기로 결정했다.

메서드를 일급객체로 사용하기 위한 새로운 기능

  1. 메소드 참조 (method reference)

    디렉터리에서 모든 숨겨진 파일을 필터링한다고 가정하자.
    우선 주어진 파일이 숨겨져 있는지 여부를 알려주는 메서드를 구현해야 한다.
    다행히 File클래스는 이미 isHidden 메서드를 제공한다.

    File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
        @Override
        public boolean accept(File file) {
            return file.isHidden(); // <- 숨겨진 파일 필터링!
        }
    });

그런데 완성한 코드가 마음에 들지 않는다. 단 세 행의 코드지만 각 행이 무슨 작업을 하는지 투명하지 않다.
File 클래스에는 이미 isHidden이라는 메서드가 있는데 왜 굳이 FileFilterisHidden을 복잡하게 감싼다음에 FileFilter를 인스턴스화 해야할까? 자바 8이 나타나기 전까지는 달리 방법이 없었기 때문이다.

자바 8에서는 아래와 같이 코드를 구현할 수 있다.
    File[] hiddenFiles = new File(".").listFiles(File::isHidden);
이미 isHidden이라는 함수는 준비되어 있으므로 자바 8의 `메서드 참조 ::`  를 이용해서 listFiles에 직접 전달할 수 있다.
  1. 람다 : 익명함수

    자바 8에서는 메서드를 일급값으로 취급할 뿐 아니라 람다를 포함하여 함수도 값으로 취급할 수 있다.

  • 코드를 넘겨주는 예제를 살펴보며 메서드 레퍼런스와 람다에 대해 좀 더 알아보자
//Apple 클래스와 getColor 메서드가 있고, Apples 리스트를 포함하는 변수 inventory가 있다.
//이때 모든 녹색 사과를 선택해서 리스트를 반환하는 프로그램을 구현하려한다.

public static List<Apple> filterGreenApples (List < Apple > inventory) {
    List<Apple> result = new ArrayList<>(); // 반환되는 result는 List로, 처음에는 비어있지만 
																						//점점 녹색사과로 채워진다.
    
    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){ // 150을 초과하는 무게를 가진 사과만 선택한다.
            result.add(apple);
        }
    }
    return result;
}

위 두개의 메서드는 if문만 다르다

만약 두 메서드가 단순히 크기를 기준으로 사과를 필터링하는 상황이었다면 인수로 (150,1000)을 넘겨주어 150그램 이상의 사과를 선택하거나 (0,80)을 넘겨주어 80그램 이하의 사과를 선택할 수 있을 것이다.

자바 8에서는 코드를 인수로 넘겨줄 수 있으므로 filter 메서드를 중복으로 구현할 필요가 없다.

앞의 코드를 다음처럼 자바 8에 맞게 구현할 수 있다.

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

위와 같이 메서드를 전달하는 것은 유용한 기능이나 isHeavyApple, isGreenApple처럼 한 두번만 사용할 메서드를 매번 정의하는 것은 귀찮은 일이다. 자바 8에서는 이 문제도 간단히 해결할 수 있다.

filterApples(inventory, (Apple a) -> GREEN.equals(a.getColor()));

//또는 다음과 같이 구현한다.
filterApples(inventory, (Apple a) -> a.getWeight() > 150);

//심지어 다음과 같이 구현할 수도 있다.
filterApple(inventory, (Apple a) -> a.getWeight() < 80 || RED.equals(a.getColor()));

즉, 한 번만 사용할 메서드는 따로 정의를 구현할 필요가 없다. 위 코드는 우리가 넘겨주려는 코드를 애써 찾을 필요가 없을 정도로 더 짧고 간결하다.

하지만 람다가 몇 줄 이상으로 길어진다면 익명 람다보다는 코드가 수행하는 일을 잘 설명하는 이름을 가진 메서드를 정의하고 메서드 참조를 활용 하는것이 바람직하다.

마치며

  • 함수는 일급값이다. 메서들르 어떻게 함수형값으로 넘겨주는지, 익명 함수(람다)를 어떻게 구현하는지 기억하자.
  • 자바 8은 프로그램을 더 효과적이고 간결하게 구현할 수 있는 새로운 개념과 기능을 제공한다.
  • 자바 8의 스트림 개념 중 일부는 컬렉션에서 가져온 것이다. 스트림과 컬렉션을 적절하게 활용하면 스트림의 인수를 병렬로 처리할 수 있으며 더 가독성이 좋은 코드를 구현할 수 있다.
profile
습득한 지식과 경험을 나누며 다른 사람들과 문제를 함께 해결해 나가는 과정에서 서로가 성장할 수 있는 기회를 만들고자 노력합니다.

0개의 댓글