1. 모던 자바
다음과 같은 레거시 자바 버전을 기준으로, 사과의 무게 순 sorting하는 로직을 작성
Collections.sort(inventory, new Comparator<Apple>() {
public int compare(Apple 1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight())
}
});
모던 자바 기준으로 작성하면 다음과 같이 간결하게 작성 가능
inventory.sort(comparing(Apple::getWeight));
대체적으로 7 -> 8 시점이 큰 변화가 일어난 시점으로 간주됨. 상기 코드 2뭉치도 7이하 vs 7이상의 코드.
대표적으로 모던 자바에서 이루어진 가장 큰 변화는 다음과 같음
- 메소드 참조(method reference)
- 스트림 API
- 인터페이스의 디폴트 메서드
이는 그냥 생각없이 추가 된 기능이 아니라, 최신의 프로그래밍 언어에서 보편적으로 요구하는 요구사항을 반영한 결과임.
- 메소드 참조 –> 간결한 코드 작성
- 스트림 API –> 멀티코어 프로세서의 쉬운 활용
- 디폴트 메서드 –> 간결한 코드 작성 (+코드의 재사용,확장성이 용이해짐)
코드 뿐만 아니라 언어 자체에도 지속적으로 시대의 흐름에 부응해야 한다는 자바의 철학이 담겨 있음.
- 레거시 자바에서부터, 잘 설계된 ‘객체지향’기반 언어로 출발
- 시대의 흐름이 변경하여, 멀티코어 프로세싱이나 클러스터링, 빅데이터 등의 데이터 처리를 하는데에 있어서 언어로써의 변화 필요
- 기존 레거시에서도 지원은 가능하나, 사용성의 편리함과는 다른 문제….
2. 스트림 처리
모던 자바! 하면 생각나는 가장 대표적인 기능 넘버원
- 유닉스 기반 OS에서 쓰는 쉘의 파이프라인(|) 을 생각하면 됨.
- 파이프라인의 이전 출력이 다음 입력으로 넘어감
ps -ef | grep java | awk '{print $1}' | xargs kill -9
- java.util.stream 패키지로 추가 됨
- 한번에 한 항목을 처리하는 형식의 기존 레거시 코드에서, 고 수준으로 추상화시켜 일련의 스트림으로 한번에 처리 가능
- 거래 리스트들을 입력받아, 가치가 1000 이상인 건들만 취합하는 필터링 후, 국가별 통화로 그룹핑 하는 코드가 필요한 경우
레거시 자바로 이를 구현해보자(읽어서 해석하려 하지 말자….)
Map<Currency, List<Transaction>> transactionByCurrencies = new HashMap<>();
List<Transaction> transactions = getTransactions();
for (Transaction transaction : transactions) {
if (transaction.getPrice() > 1000) {
Currency currency = transaction.getCurrency();
List<Transaction> transactionsForCurrency =
transactionByCurrencies.get(currency);
if (transactionsForCurrency == null) {
transactionsForCurrency = new ArrayList();
transactionByCurrencies.put(currency, transactionsForCurrency);
}
transactionsForCurrency.add(transaction);
}
}
불-편
특히 신나게 개발하다보면 흔히 빼먹을 수도 있는 null체크는…. 놓치는 순간 장애의 포인트가 될 수도 있음. 극혐.
스트림을 아낌없이 퍼부어 이 코드를 모던 자바 스타일로 구현해 본다.
List<Transaction> transactions = getTransactions();
Map<Currency, List<Transaction>> transactionByCurrencies =
transactions.stream()
.filter((Transaction t) -> t.getPrice() > 1000)
.collect(groupingBy(Transaction::getCurrency));
편-안
필터링을 하는 동작 하나, 그룹화 취합 하나, 이렇게 두 가지의 동작을 하나의 스트림으로 처리
2.1. 멀티 스레딩
멀티 스레딩이 어려운 이유
- 대부분의 자원들이 공유 자원들이기 때문에
- mutual exclusive 하지 않은 리소스들을 관리 잘 해야 함
- critical section 관리도 필요(세마포어, lock 등등)
위에 적어놓은 것들을 구현자가 직접 다 고려해서 버그 안나게 코드를 잘~ 짜야 한다…….. <– 이게 문제임. 잘 못짜면….?
근데, 스트림을 잘 활용하면 위에서 걱정하는 부분을 고려하지 않고도 ‘멀티 스레드(Thread)’ 로 동작시킬 수도 있음.(스트림 API에 이미 적절히 잘 구현되어 있으니까.) <– 멀티스레드를 잘 못짜도 어차피 멀티스레딩을 스트림 API 자체에서 수행하니 스트림API에 버그가 있지 않는 한 문제 음슴.
병렬 스트림이 그럼 어떻게 작업을 분리하는가?
자체적으로 스트림 API를 이용하여 리스트를 단위별로 쪼개(포킹. forking), 각각의 CPU 코어에 작업을 할당하여 자동적으로 멀티코어 프로세서를 제대로 활용하게끔 처리.

List<Apple> heavyApples = inventory.stream()
.filter((Apple a) -> a.getWeight() > 150)
.collect(toList());
위의 코드의 stream() 을 parallelStream() 으로 바꾸기만 해도 병렬처리로 동작함.
List<Apple> heavyApples = inventory.parallelStream()
.filter((Apple a) -> a.getWeight() > 150)
.collect(toList());
3. 메소드 참조
동작(메소드) 자체를 파라미터화 시켜서, ‘값’ 이 아닌 ‘코드’ 를 또 다른 메소드에 전달하는 것이 핵심
- 왜 해야 함?
- 뭐…. 써보면 안다. 편하니까… 이게 좋은데 대체 어떻게 설명해야 할지 모르겠네…
‘프로그래밍’의 모든 동작들을 잘 생각해보면, 어쨌든 ‘값을 바꾸는 행위’를 작성하는 것임.
하지만 이 ‘값’이란 건, 레거시 자바에서는 말 그대로 값을 표현하는 변수, 혹은 객체(의 레퍼런스)만을 뜻하지, 메소드 혹은 클래스 그 자체를 값으로 간주하여 다른 메소드에 전달하는 방법이 없음.
책에서는 변수,객체를 일급(First class)값, 메소드, 클래스를 이급 값으로 구분하여 설명하였는데, 어쨌든 이급 값으로 간주되는 메소드를 일급 값처럼 사용하면 프로그래밍에서 매우 유용하게 사용될 수 있음.
사실 배우는 입장에서 ‘해보니까 그렇다고 하더라’ 라고까지 설명듣고, 거기서 그치면 공감을 하지 못한다.(경험하지 못해서 공부를 하는건데, ‘그렇다고 한다’ 라고 말하면 반응은 둘 중 하나. ‘무슨소리야?’ 혹은 ‘딱히 안 불편한데.’ ‘익숙한 지금이 훨 좋은 것 같은데.’ ) <– 이게 과거의 나. 근데 이 소리 X소리였음.ㅇㅇ
이럴땐 예제를 한번 보면 된다. 파일시스템 안에 있는 파일들 중에, 숨김처리 되어있는 파일의 리스트를 얻고 싶은 코드를 작성하고자 한다.
레거시 자바는 다음과 같이 작성됨
File[] hiddenFiles = new File(".").listFiles( new FileFilter() {
public boolean accept(File file) {
return file.isHidden();
}
});
파일의 리스팅을 해 주는 listFiles() 메소드를 호출하는데, 그 인자값으로…. 이미 File클래스 안에 구현되어 있는 isHidden() 을 ‘한번 감싸서’ 굳이 FileFilter 객체를 만들어 던져줘야 한다.
레거시 자바에서는 이급 값(메소드,클래스) 그 자체를 함수의 파라미터로 던져주는 기능이 없기 때문에, 굳이 클래스를 통해 객체를 한번 더 생성해서 일급 값(변수,객체)을 넘겨주는 것임.
이제 모던 자바의 코드를 보자. 앞서 말했듯 모던 자바는 메소드 그 자체를 일급 값으로 간주하여, 메소드를 특정 메소드의 파라미터로 활용할 수가 있다고 하였다.
file[] hiddenFiles = new File(".").listFiles(File::isHidden);
편-안
굳이 쓸데없는 FileFilter 객체를 생성하지 않고, isHidden() 메소드 그 자체를 값으로 간주하여 넘겨버리니, 코드의 가독성도 늘어나고 손가락도 덜 아픔

3.1. 메소드 참조 응용(??)
이번엔, 기준만 다르고 행위 자체는 같은 행위를 여러번 하는 예를 한번 들어본다.
- 사과를 필터링하고 싶음.
- 녹색 사과
레거시 자바를 통해서 이 ‘필터링’ 행위를 구현하고자 하면, 다음과 같이 작성될 수 있다.
public static List<Apple> filterGreenApple(List<Apple> inventory) {
List<Apple> result = new ArrayList<>();
for (Apple apple: inventory) {
if (GREEN.equals(apple.getColor())) {
result.add(apple);
}
}
return result;
}
개발자 A가 이렇게 잘 짜놨다. 그리고 시간이 지나 퇴사하고, 개발자 B가 입사를 했는데 요구조건이 들어온다.
- 사과를 필터링하고 싶음.
- 150그램이 넘는 사과
예제가 간단해서 그렇지, 실무에서 저런 요구사항 비일비재하고, 또 요구사항 발생하면 보통 새로 짜거나, 어찌어찌 운 좋아서 기존 코드를 보면, 그걸 손보지 않(못하)고 그냥 단순 복붙해서 내 구미에 맞는 부분만 수정해서 배포함…. 특히 이 경향은 기존 코드가 규모가 크고 엉망인 상태라면 더더욱 심해짐(악순환)
public static List<Apple> filterHeavyApple(List<Apple> inventory) {
List<Apple> result = new ArrayList<>();
for (Apple apple: inventory) {
if (apple.getWeight() > 150) {
result.add(apple);
}
}
return result;
}
개발자 B도 퇴사하고 C가 입사를 했다. 이번엔 벌레 안먹은 사과를 필러링해달라는 요구가 들어온다……이후 벌어질 상황은 설명 생략..
상기 두개의 코드 블럭은 잘 보면, if문에서 사용하는 ‘동작’ 하나만 다르다. 메소드 참조를 배웠으니, 이젠 이 ‘동작’을 변수처럼(1급) 메소드(2급 –> 1급)로 정의한 다음, 다른 메소드의 파라미터로 넘길 수 있다는 생각을 저절로 해야 할 것이다.
public static boolean isGreenApple(Apple apple) {
return GREEN.equals(apple.getColor());
}
public static boolean isHeavyApple(Apple apple) {
return apple.getWeight() > 150;
}
public 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);
}
}
}
filterApples(inventory, Apple::isGreenApple);
filterApples(inventory, Apple::isHeavyApple);
편-안
그런데 메소드 정의 자체가 귀찮을 때엔?
3.2. 람다
람다를 사용하면 된다.
특정 메소드가 어플리케이션에서 한두번정도만 사용되는 경우에는 그 메소드를 정의하는 비용보다는, 그냥 호출 시점에서 작성하고 버리는 편이 더 효율적일수도.
filterApples(inventory, (Apple a) -> GREEN.equals(a.getColor()));
filterApples(inventory, (Apple a) -> a.getWeight() > 150);
특별히 정해진 법칙은 없으나, 일종의 암묵적 룰로는….
- 람다식이 3줄 이상 넘어간다면 그냥 메소드 정의를 할 지에 대해 고민해봐야 한다
- 혹은 로직 자체가 문제가 있는 것일수도
- 같은 람다식이 3번 이상 호출된다면(사실 3번도 많다. 두번 이상.) 메소드 정의를 할 지에 대해 고민해봐야 한다
4. 디폴트 메소드
자바 8서부터는 인터페이스를 쉽게 바꿀 수 있도록 디폴트 메소드를 지원한다
이게 뭘 뜻하는 것일까?
예를 들어 (뻘짓같지만)레거시 자바에서 List 객체에 stream api를 먹이고 싶다고 치자.
List는 Collection을 구현한 클래스이므로 Collection 에 stream() 이라는 인터페이스 메소드를 생성하게 된다.
- 이 순간부터 헬파티
- Collection을 상속받아 구현하는 모~~~~~~든 클래스에 stream()을 구현해 주어야 한다.
- 구현도 구현이지만 구현한 코드 테스트는..? 터지는 버그는….?
- Collection을 상속받아 구현하는 모~~~~~~든 클래스에 stream()을 구현해 주어야 한다.

그래서 자바 8부터는, 인터페이스에 디폴트 메소드 기능을 지원해 준다.
default void sort(Comparator<? super E> c) {
Collections.sort(this,c);
}
인터페이스에서 디폴트 메소드를 정의해 놓으면, 이를 구현하는 클래스는 그 클래스에서 특별히 구현을 하지 않는 이상, 인터페이스의 메소드를 상속받아 사용한다.