모던 자바 인 액션 1장
책 [모던 자바 인 액션]을 보며 나름대로 정리하여 포스팅하였습니다.
1.1 자바 8에서의 변화
자바는 버전을 거쳐오며 많은 변화가 일어났지만 자바 8만큼 획기적이거나 생산성이 바뀌었던 변화는 없었다. 예시코드를 보자.
Collection.sort(inventory, new Comparator<Apple>() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(aw.getWeight());
}
});
이를 자바 8을 이용하면 자연어에 더 가깝게 간단한 방식으로 코드를 구현할 수 있다.
inventory.sort(comparing(Apple::getWeight));
자바 8은 간결한 코드, 멀티코어 프로세서의 쉬운 활용이라는 두 가지 요구사항을 기반으로 한다.
자바 8에서 제공하는 새로운 기술
- 스트림 API
- 메서드에 코드를 전달하는 기법
- 인터페이스의 디폴트 메서드
자바 8은 데이터베이스 질의 언어에서 표현식을 처리하는 것처럼 병렬 연산을 지원하는 스트림이라는 새로운 API를 제공한다. 스트림을 이용하면 에러를 자주 일으키며 멀티코어CPU를 이용하는 것보다 비용이 훨씬 비싼 키워드인 synchronized를 사용하지 않아도 된다.
메서드에 코드를 전달하는 기법(메서드 참조와 람다)을 이용하면 새롭고 간결한 방식으로 동작 파라미터화를 구현할 수 있다. 예를 들어 약간만 다른 두 메서드가 있다고 가정하자. 이 두 메서드를 그대로 유지하는 것보다는 인수를 이용해서 다른 동작을 하도록 하나의 메서드로 합치는 것이 바람직할 수 있다. (프로그램이 짧고 간결)
1.2 자바 8 설계의 밑바탕을 이루는 세 가지
첫 번째 프로그래밍 개념은 스트림 처리다.
스트림이란 한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임이다. 이론적으로 프로그램은 입력 스트림에서 데이터를 한 개씩 읽어 들이며 마차가지로 출력 스트림으로 데이터를 한 개씩 기록한다. 즉, 어떤 프로그램의 출력 스트림은 다른 프로그램의 입력 스트림이 될 수 있다.
ex) 유닉스의 cat 명령은 두 파일을 연결해서 스트림을 생성하며, tr은 스트림의 문자를 번여가고, sort는 스트림의 행을 정렬하며, tail -3은 스트림의 마지막 3개 행을 제공한다. 다음 코드처럼 유닉스 명령행에서는 파이프(|)를 이용해서 명령을 연결할 수 있다.
cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3
이 예제는 파일의 단어를 소문자로 바꾼 다음에 사전순으로 단어를 정렬했을 때 가장 마지막에 위치한 세 단어를 출력하는 프로그램이다. 유닉스에서는 여러 명령(cat,tr,sort,tail)을 병렬로 실행한다.
자바 8에는 java.util.stream 패키지에 스트림 API가 추가되었다. 스트림 패키지에 정의된 Stream
결론적으로 스트림 파이프라인을 이용해서 입력 부분을 여러 CPU 코어에 쉽게 할당할 수 있으며 스레드라는 복잡한 작업을 사용하지 않으며 공짜로 병렬성을 얻을 수 있는 것이다.
두 번째 프로그램 개념은 코드 일부를 API로 전달하는 기능이다.
유닉스 예제에서 sort 명령에 파라미터를 추가하고 싶은 사용자도 있을 것이다. sort에 파라미터를 제공해서 역순 정렬 등 다양한 정렬을 수행할 수는 있지만 어쨋든 sort의 동작은 미리 정해져 있다.
예를 들어 2013KR0001, 2022US0002, … 등의 형식을 갖는 송장 ID가 있다고 가정해보자. 연도, 국가코드, 고객 ID로 이루어진 저 송장에서 어떤걸 기준으로 정렬할지는 이미 정해진 동작이 있는 sort에 우리가 지정하는 순서대로 자료를 정리하도록 따로 코드를 제공해야 한다.
우선은 두 송장ID를 비교하는 compareUsingCustomId 메서드를 구현할 수 있다. 그런데 자바 8 이전의 자바에서는 메서드를 다른 메서드로 전달할 방법이 없다. 물론 Comparator 객체를 만들어서 sort에 넘겨주는 방법도 있지만 너무 복잡하며 기존 동작을 단순하게 재활용한다는 측면에서도 맞지 않다.
자바 8에서는 메서드(우리 코드)를 다른 메서드의 인수로 넘겨주는 기능을 제공한다. 이러한 기능을 이론적으로 동작 파라미터화 라고 부른다.
동작 파라미터화가 왜 중요할까? compareUsingCustomerId를 이용해 sort의 동작을 파라미터화 했던 것처럼 스트림 API는 연산의 동작을 파라미터화할 수 있는 코드를 전달한다는 사상에 기초하기 때문이다.
세 번째 프로그래밍의 개념은 ‘병렬성을 공짜로 얻을 수 있다’는 말에서 시작된다.
그럼 공짜를 얻는 대신 무엇을 포기해야 할까? 스트림 메서들 전달하는 코드의 동작 방식을 조금 바꿔야 한다.스트림 메서드로 전달하는 코드는 다른 코드와 동시에 실행하더라도 안전하게 실행될 수 있어야 한다. 보통 다른 코드와 동시에 실행하더라도 안전하게 실행할 수 있는 코드를 만드려면 공유된 가변 데이터에 접근하지 않아야 한다.이는 18장과 19절에서 자세하게 알아보자.
요약
언어는 하드웨어나 프로그래머 기대의 변화에 부응하는 방향으로 변화해야한다. 자바는 계속 새로운 기능을 추가하면서 인기 언어의 자리를 유지하고 있다. 자바8의 새로운 기능덕분에 함수형 프로그래밍과 객체지향형 프로그래밍 두가지 패러다임의 장점을 모두 잡았다. 이제 자바8에 추가된 새로운 개념을 하나씩 자세히 살펴보자.
1.3 자바함수
1.3.1 메서드와 람다를 일급 시민으로
이 책에서는 프로그래밍 언어의 핵심은 값을 바꾸는 것이라며 값 자체를 일급 시민으로 표현하고 있다.(구조체같은 값의 구조는 이급시민으로 표현함) 1.3.1절의 제목을 이해할 수 있을 것이다.
첫번째로 메서드 참조라는 새로운 자바 8의 기능을 소개한다. 디렉터리에서 모든 숨겨진 파일을 필터링한다고 가정하자. 우선 주어진 파일이 숨겨져 있는지 여부를 알려주는 메서드를 구현해야 한다. 다행히 File 클래스는 이미 isHidden 메서드를 제공한다. isHidden은 File 클래스를 인수로 받아 boolean을 리턴하는 함수이다. 다음예제처럼 FileFilter 객체 내부에 위치한 isHidden의 결과를 File, listFiles메서드로 전달하는 방법으로 숨겨진 파일을 필터링할 수 있다.
File[] hiddenFiles = new File(".").listFiles(new FileFilter(){
public boolean accept(File file){
return file.isHidden();
}
});
위 코드는 각 행이 무슨 작업을 하는지 투명하지 않다. File클래스에는 이미 isHidden이란 메서드가 있는데 왜 굳이 FileFilter로 isHidden을 복잡하게 감싼다음에 FileFilter를 인스턴스화 해야할까?
이제 자바8에서는 다음 코드처럼 구현할 수 있다.
File[] hiddenFiles = new File(".").listFiles(File::isHidden);
- 메서드참조 :: (‘이 메서드를 값으로 사용하라’는 의미)
isHidden이라는 함수는 준비되어있으므로 메서드 참조를 이용해서 listFiles에 직접 전달한다.
1.3.2 코드 넘겨주기 : 예제
Apple 클래스와 getColor 메서드가 있고, Apples 리스트를 포함하는 변수 inventory가 있다고 가정하자. 이때 모든 녹색 사과를 선택해서 리스트를 반환하는 프로그램을 구현하려고 한다. 이처럼 특정 항목을 선택해서 반환하는 동작을 필터라고 한다.
- 자바 8 이전의 코드
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> filterGreenApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<>();
for(Apple apple:inventory){
if(apple.getWeight()>150){
result.add(apple)'
}
} return result;
}
이 예제에서 두 메서드는 코드 한줄만 다르다. 자바 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> 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);
코드의 동작은 2장과 3장에서 설명한다. 여기서 핵심은 자바 8에서는 메서드를 전달할 수 있다는 사실이다.
프레디케이트(predicate)란? 인수로 값을받아 true나 false를 반환하는 함수를 프레디케이트라고 한다. 나중에 설명하겠지만 자바 8에서도 Function<Apple, Boolean> 같이 코드를 구현할 수 있지만 Predicate
을 사용하는 것이 더 표준적인 방식이다(또한 boolean을 Boolean으로 변환하는 과정이 없으므로 더 효율적이기도 하다.)
1.3.3 메서드 전달에서 람다로
메서드를 값으로 전달하는건 분명 유용한 기능이긴 하나 한번만 사용할 메서드를 매번 정의하는 것은 귀찮은 일이다. 자바 8에서는 다음처럼 새로운 개념을 이용해서 구현할 수 있다.
filterApples(inventory, (Apple a)-> GREEN.equals(a.getColor()) );
filterApples(inventory, (Apple a)-> a.getWeight() > 150 );
filterApples(inventory, (Apple a)-> a.getWeight()<80 || RED.equals(a.getColor()) );
즉, 한 번만 사용할 메서드는 따로 정의를 구현할 필요가 없다. 위 코드는 우리가 넘겨주려는 코드를 애써 찾을 필요가 없을 정도로 더 짧고 간결하다.
하지만 람다가 몇 줄 이상으로 길어진다면 익명 람다보다는 코드가 수행하는 일을 잘 설명하는 이름을 가진 메서드를 정의하고 메서드 참조를 활용하는 것이 바람직하다. 코드의 명확성이 우선시되어야 한다.
1.4 스트림
스트림을 이용하면 한눈에 알아보기 어려운 긴 자바 코드를 깔끔하게 표현할 수 있다. 이 장에서는 스트림 API를 사용하면 컬렉션 API와는 상당히 다른방식으로 데이터를 처리할 수 있다는 사실만 기억하자.
1.4.1 멀티스레딩은 어렵다
이전 자바 버전에서 제공하는 스레드 API로 멀티스레딩 코드를 구현해서 병렬성을 이용하는 것은 쉽지 않다. 두 스레드에서 공유된 변수에 접근하여 변경을 시도하면 예상과 다른값이 저장이 될 수 있다.
자바8은 스트림 API로 ‘컬렉션을 처리하면서 발생하는 모호함과 반복적인 코드 문제’ 그리고 ‘멀티코어 활용 어려움’이라는 두 가지 문제를 모두 해결했다.
자주 반복되는 패턴으로 주어진 조건에 따라 데이터를 필터링하거나 데이터를 추출하거나 데이터를 그룹화하는 등의 기능이 있다. 또한 이러한 동작들을 쉽게 병렬화할 수 있다는 점도 변화의 동기가 되었다.
1.5 디폴트 메서드와 자바 모듈
자바 8에서는 default 키워드로 인터페이스 내부에서 디폴트 메서드를 선언할 수 있다. 디폴트 메소드가 어떤 개념인지 간략하게 살펴보자.
자바 8에서는 List에 직접 sort 메서드를 호출할 수 있다. 이는 자바8의 List 인터페이스에 다음과 같은 디폴트 메서드 정의가 추가되었기 때문이다.
- 하나의 클래스에서 다중 인터페이스 상속
- 각 인터페이스에 디폴트 메소드가 전부 존재한다면?
- => 이는 다중상속?? (추후 다이아몬드 상속 문제로 이어진다)
- 해결방법은 9장에서 등장한다고 한다.