1.1 역사의 흐름은 무엇인가?

자바 1.0에서는 스레드와 락, 심지어 메모리 모델까지 지원했다.

아마 단일 코어 환경에서 최선의 퍼포먼스를 뽑아내기 위한 선택이였던 것으로 추측

하지만 상당히 저수준의 기능을 온전히 활용하기는 어려웠다.

자바 5에서는 스레드풀, 병렬 실행 컬렉션 등 아주 강력한 도구를 도입했다.

자바 7에서는 병렬 실행에 도움을 줄 수 있는 포크/조인 프레임워크를 제공했지만 여전히 어려움

  • 스레드풀
  • 자바의 스레드풀(Thread Pool)은 작업을 처리하기 위해 미리 생성된 스레드들의 집합입니다. 이를 통해 새로운 스레드를 생성하는 오버헤드를 줄이고, 리소스를 효율적으로 관리하며, 애플리케이션의 성능을 향상시킬 수 있습니다. 스레드풀을 사용하면 일정 수의 스레드가 재사용되며, 작업 큐에 있는 작업을 순차적으로 처리합니다. 대표적으로 Executors 클래스를 통해 다양한 종류의 스레드풀을 쉽게 생성할 수 있습니다.
  • 병렬 실행 컬렉션
    1. Fork/Join Framework: 자바 7에서 도입된 프레임워크로, 작업을 재귀적으로 작은 단위로 나누고, 이를 병렬로 처리하는 방식입니다. ForkJoinPool 클래스가 이를 지원합니다.
    2. Parallel Streams: 자바 8에서 도입된 스트림 API의 일환으로, stream().parallel() 메서드를 사용하여 스트림을 병렬로 처리할 수 있습니다. 내부적으로는 Fork/Join Framework를 사용합니다.
    3. Concurrent Collections: 자바의 java.util.concurrent 패키지에서 제공하는 여러 동시성 컬렉션이 있습니다. 예를 들어, ConcurrentHashMap, CopyOnWriteArrayList 등이 있습니다. 이들은 멀티스레드 환경에서 안전하게 사용될 수 있도록 설계되었습니다.
    이러한 병렬 실행 컬렉션을 사용하면 멀티코어 시스템에서 성능을 극대화할 수 있습니다. 다만, 병렬 처리가 항상 성능 향상을 보장하지는 않으므로 적절한 상황에서 사용하는 것이 중요합니다.
  • 병렬 실행 컬렉션(Parallel Execution Collections)은 자바에서 병렬 처리를 지원하는 컬렉션으로, 여러 스레드를 사용하여 컬렉션의 요소들을 동시에 처리할 수 있도록 합니다. 이를 통해 대용량 데이터의 처리를 보다 빠르게 수행할 수 있습니다. 주요 병렬 실행 컬렉션은 다음과 같습니다:

자바 8은 간결한 코드, 멀티코어 프로세서의 쉬운 활용이라는 두 가지 요구사항을 기반으로 한다.

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

자바 8은 병렬 연산을 지원하는 스트림이라는 새로운 API를 제공한다.

즉. 스트림을 이용하면 에러를 자주 일으키며 멀티코어 CPU를 이용하는 것보다 훨씬 비싼 키워드 synchronized를 사용하지 않아도 된다.

코드를 전달하는 기법, 디폴트 메서드 또한 존재함

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

인수를 이용해서 다른 동작을 하도록 하나의 메서드로 합치는 것

익명 클래스를 사용하는것보다 더 간단하고 명료한지 확인할 수 있다.

자바 8 기법은 함수형 프로그래밍에서 위력을 발휘한다.

코드를 전달하거나 조합해서 자바의 강렬한 프로그래밍 도구로 활용할 수 있다.

1.2 왜 아직도 자바는 변화하는가?

우리는 시공을 초월하는 완벽한 언어를 원하지만 현실적으로 그런 언어는 존재하지 않으며 모든 언어가 장단점을 갖고 있다.

예를 들어 C, C++은 안정성이 부족하지만 작은 런타임 풋프린트 덕분에 운영체제와 다양한 임베디드 시스템에서 사용함.

하지만 불안전해서 C#이 C, C++을 압도함

이토록 특정 분야에서 장점을 가진 언어는 다른 경쟁 언어를 도태시킨다. 자바가 어떻게 그러한 성공을 거둘 수 있었는지 살펴보자.

1.2.1 프로그래밍 언어 생태계에서 자바의 위치

자바는 출발이 좋았다.

잘 설계된 객체지향 언어로 시작해 스레드와 락을 이용한 소소한 동시성도 지원했다.

코드를 JVM 바이트 코드로 컴파일하는 특징때문에 인터넷 애플릿 프로그램의 주요 언어가 되었다.

  • 자바는 어떻게 대중적인 프로그래밍 언어로 성장했는가?자바 모델과 자바 코드 애플릿을 안전하게 실행할 수 있었던 초기 브라우저 덕분에 자바가 대학으로 깊숙이 자리잡을 수 있었고, 졸업생들이 자바를 업계에서 활용하기 시작했다. 처음에는 C/C++에 비해 (애플리케이션을 실행하는 데) 추가적으로 드는 비용(시간) 때문에 자바에 대한 반감이 있었다. 하지만 하드웨어가 발전하면서 프로그래머의 시간이 더욱 중요한 요소로 부각되었다. 마이크로소프트의 C#은 자바 형식의 객체지향 모델의 힘을 깊이 있게 검증해주었다.
  • 객체지향은 1990년대에 두 가지 이유로 각광받았다. 하나는 캡슐화 덕분에 C에 비해 소프트웨어 엔지니어링적인 문제가 훨씬 적다는 점이고, 다른 하나는 객체지향의 정신적인 모델 덕분에 윈도우95 및 그 이후의 WIMP 프로그래밍 모델에 쉽게 대응할 수 있다는 사실이다. '이것은 모든 것은 객체다'로 요약할 수 있다. 마우스를 클릭하면 핸들러로 메시지가 전송된다 (Mouse 객체의 Clicked 메서드가 호출됨). 일단 만들면 모든 곳에서 실행할 수 있었다.

프로그래머는 멀티코어 컴퓨터나 컴퓨팅 클러스터를 이용해서 빅 데이터를 효과적으로 처리할 필요성이 커졌다.

즉 병렬 프로세싱을 활용해야하는데 자바는 이게 안됐음.

이를 보완하기 위해서 자바 8은 여러가지 기능이 추가되었고 병렬성을 활용하는 코드, 간결한 코드를 구현할 수 있게 도와줄것이다.

1.2.2 스트림 처리

스트림이란 한번에 한개씩 만들어지는 연속적인 데이터 항목들의 모임이다.

연속된 데이터 요소들의 추상화, 지연 연산, 한번만 사용 등 배열과는 차이점이 있다.

기존 데이터를 처리하던 방식은 한번에 한 항목을 처리했지만 이제 자바 8에서는 고수준으로 추상화, 일련의 스트림으로 만들어서 처리할 수 있다.

여러 CPU 코어에 쉽게 할당할 수 있다는 부가적인 이득을 얻을 수 있다.

쓰레드라는 복잡한 작업을 사용하지 않으면서도 공짜 병렬성을 얻을 수 있다.

1.2.3 동작 파라미터화로 메서드 코드 전달하기

자바 8에 추가된 두 번째 프로그램 개념은 코드 일부를 API로 전달하는 기능이다.

메서드를 다른 메서드의 인수로 넘겨주는 기능을 제공한다. (pintOS때 compare 함수 전달하던거 생각남)

이러한 기능을 이론적으로 동작 파라미터화 라고 부른다.

1.2.4 병렬성과 공유 가변 데이터

스트림 메서드는 병렬성을 얻는 대신 전달하는 코드의 동작 방식을 조금 바꿔야한다.

스트림 메서드로 전달하는 코드는 다른 코드와 동시에 실행하더라도 안전하게 실행될 수 있어야한다.

보통 다른 코드와 동시에 실행하더라도 안전하게 실행할 수 있는 코드를 만들려면 공유된 가변 데이터에 접근하지 않아야한다.

이러한 함수를 순수(pure) 함수, 부작용 없는(side effect free)함수, 상태없는(stateless) 함수 라고 부른다.

결국 순차 스트림이 아닌 병렬 스트림을 사용하면 멀티 쓰레드 환경에서 데이터를 병렬적으로 처리하기 때문에 다음과 같이 코드를 작성해야하는듯

하지만 자바 8 스트림을 이용하면 기존의 자바 스레드 API보다 쉽게 병렬성을 활용할 수 있다.

공유되지 않은 가변 데이터, 메서드, 함수 코드를 다른 메서드로 전달하는 두 가지 기능은 함수형 프로그래밍 패러다임의 핵심적인 사항이다.

반면 명령형 프로그래밍 패러다임에서는 일련의 가변 상태로 프로그램을 정의한다.

1.2.5 자바가 진화해야 하는 이유

이 절의 내용을 한 줄로 요약하면 "언어는 하드웨어와 프로그래머 기대의 변화에 부응하는 방향으로 변화해야 한다"는 것이다.

1.3 자바 함수

프로그래밍 언어에서 함수라는 용어는 메서드, 특히 정적 메서드와 같은 의미로 사용된다.

자바의 함수는 이에 더해 수학적인 함수처럼 사용되며 부작용을 일으키지 않는 함수를 의미한다.

프로그래밍 언어의 핵심은 값을 바꾸는 것이다. 역사적으로 그리고 전통적으로 프로그래밍 언어에서는 이 값을 일급값이라고 부른다.

하지만 프로그램을 실행하는 동안 이러한 모든 구조체를 자유롭게 전달할 수는 없다.

1.3.1 메서드와 람다를 일급 시민으로

자바 8의 설계자들은 메서드를 값으로 취급할 수 있게. 그리하여 프로그래머들이 더 쉽게 프로그램을 구현할 수 있는 환경이 제공되도록 자바 8을 설계하기로 결정했다.

첫번째로 메서드 참조 라는 새로운 자바 8의 기능을 소개한다.

		File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
			public boolean accept(File pathname) {
				return pathname.isHidden();
			}
		});

위 코드는 단순 숨겨진 파일을 반환하는 코드인데 메서드 참조를 사용하지 않는다면 다음과 같이 긴 코드를 작성해야 한다.

		File[] hiddenFiles2 = new File(".").listFiles(File::isHidden);

하지만 메서드 참조를 사용하면 listFiles에 직접 isHidden 함수를 전달할 수 있다.

여기서 메서드라 호칭하지 않고 함수라는 용어를 사용한 이유는 다음과 같다.

  • 함수라 칭한 이유자바는 객체 지향 프로그래밍(Object-Oriented Programming, OOP) 언어로서, 메서드는 클래스에 속한 기능을 의미합니다. 하지만 자바 8에서는 함수형 프로그래밍(Functional Programming, FP)의 개념이 도입되었습니다. 함수형 프로그래밍에서는 메서드를 일급 객체(First-Class Citizen)로 다루며, 이를 함수(function)라고 부릅니다.
    1. 함수형 인터페이스: 자바 8에서는 람다 표현식과 메서드 참조를 사용하여 함수형 인터페이스(Functional Interface)를 구현할 수 있습니다. 함수형 인터페이스는 단 하나의 추상 메서드만 가지며, 이는 함수형 프로그래밍의 핵심 개념입니다.
    2. 일급 객체: 함수형 프로그래밍에서는 함수를 일급 객체로 다룹니다. 이는 함수를 변수에 할당하거나, 다른 함수의 인자로 전달하거나, 함수의 반환값으로 사용할 수 있음을 의미합니다. 메서드 참조를 통해 자바에서도 메서드를 일급 객체처럼 사용할 수 있게 됩니다.
    3. 간결성 및 가독성: 메서드 참조를 사용하면 코드가 더 간결하고 읽기 쉬워집니다. 함수형 프로그래밍에서는 이러한 간결성을 중요하게 여깁니다.
  • 메서드 참조는 이러한 함수형 프로그래밍의 개념을 따르는 기능입니다. 따라서 메서드 참조를 사용할 때 메서드를 함수처럼 취급합니다. 이는 다음과 같은 이유에서입니다:
  • 메서드' 대신 '함수'라는 용어를 사용하는 이유

간추리면 함수를 일급 객체로 다루기 때문에 함수를 변수에 할당하거나, 다른 함수의 인자로 전달하거나 함수의 반환값으로 사용할 수 있기 때문임

위 예제를 보다싶이 더이상 메서드가 이급값이 아닌 일급값이라는 점이 함수형 프로그램의 특징

람다 : 익명 함수

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

예를 들어 (int x) -> x + 1 . 즉 x라는 인수로 호출하면 x+1을 반환한다는 동작을 수행하도록 코드를 구현할 수 있다.

물론 직접 메서드를 정의할 수도 있지만, 이용할 수 있는 편리한 클래스나 메서드가 없을 때 새로운 람다 문법을 이용하면 더 간결하게 코드를 구현할 수 있다.

1.3.2 코드 넘겨주기 : 예제

다음 코드는 비슷한 로직을 수행하는 코드다.

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

하지만 이런식으로 단순 복붙을 통해 코드를 작성하면 후에 버그가 발생했을때 복붙한 코드를 모두 고쳐야 한다.

하지만 자바 8에서는 코드를 인수로 넘겨줄 수 있다.

	void test() {
		List<Apple> inventory = new ArrayList<>();
		for (int i = 50; i <= 200; i++) {
			inventory.add(new Apple(GREEN, i));
		}
		List<Apple> apples = filterApples(inventory, ApplePredicates::isGreenApple);
		List<Apple> apples1 = filterApples(inventory, ApplePredicates::isHeavyApple);
	}

내가 원하는 작업이 다음과 같다고 해보자

우선 Apple Class는 다음과 같이 짰다.

	@Getter
	class Apple{
		Color color;
		int weight;

		public Apple(Color color, int weight) {
			this.color = color;
			this.weight = weight;
		}
	}

그리고 사과를 선별하기 위한 Predicates class

	static class ApplePredicates {

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

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

	}

예제에서는 Apple Class 안에 해당 함수를 작성해둔것같은데 만약 Apple이 Entity라면 계층의 역전이 일어나는것같아 그냥 따로 뺐다.

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

여기서 Predicate Class를 import해 사용했는데 Predicate의 역할은 다음과 같다.

  • Predicate
  • Predicate 인터페이스는 조건을 테스트하는 함수형 인터페이스로, 입력값이 주어진 조건을 만족하는지 평가하는 역할을 합니다. 이를 통해 필터링 로직을 정의하고, 람다 표현식이나 메서드 참조를 사용하여 간결하고 가독성 높은 코드를 작성할 수 있습니다. 예제 코드에서는 Apple 객체를 필터링하는 조건을 정의하는 데 사용되어, 다양한 조건에 따라 Apple 리스트를 필터링하는 기능을 제공합니다.

우선 보이는 가장 큰 장점으로는 당장은 코드의 양이 늘어나보여도 하나의 함수 + 여러게의 조건을 추가할 수 있어 확장성적인 측면에서 뛰어날 것 같으며 중복되는 코드를 확 줄여줄 것 같다.

또 하나는 QueryDsl에서 Paging 처리를 할때 여러 조건을 달아주는 예제가 생각나는 것 같다.

아예 filter package를 따로 빼서 관리하는것도 괜찮을지도?

1.3.3 메서드 전달에서 람다로

메서드를 값으로 전달하는 것은 분명 유용한 기능이다. 하지만 isHeavyApple, isGreenApple 처럼 한 두번만 사용할 메서드를 매번 정의하는 것은 귀찮은 일이다.

이를 람다(익명함수) 를 사용해 해결할 수 있음

		List<Apple> lambdaApples = filterApples(inventory, (Apple a) -> GREEN.equals(a.getColor()));
		List<Apple> lambdaApples1 = filterApples(inventory, (Apple a) -> a.getWeight() > 150);
		List<Apple> lambdaApples2 = filterApples(inventory, (Apple a) -> a.getWeight() < 80 || RED.equals(a.getColor()));

수퍼 응용버전

즉 한번만 사용할 메서드는 따로 정의를 구현할 필요가 없다!!!!!!!!!

하지만 람다가 몇 줄 이상으로 길어진다면 익명 람다보다는 코드가 수행하는 일을 잘 설명하는 이름을 가진 메서드를 정의하고 메서드 참조를 활용하는 것이 바람직하다. → 반복이 자주되고 조건이 여러개면 메서드 참조로 빼는게 좋다

멀티코어 CPU의 병렬성을 응용하기 위해 filter와 비슷한 동작을 수행하는 연산 집합을 포함하는 새로운 스트림 API를 제공한다.

1.4 스트림

거의 모든 자바 애플리케이션을 컬렉션을 만들고 활용한다. 하지만 컬렉션으로 모든 문제가 해결되는것은 아니다. (for문은 하나에 하나만 연산. 즉 병렬성을 활용하지 못함)

		List<Transaction> transactions = new ArrayList<>();
		for (int i = 500; i < 2000; i++) {
			transactions.add(new Transaction(Currency.KOREA, i));
		}

		Map<Currency, List<Transaction>> transactionByCurrencies =
			new HashMap<>(); // 그룹회된 트랜잭션을 더할 map 생성
		for (Transaction transaction : transactions) {
			if (transaction.getPrice() > 1000) {
				Currency currency = transaction.getCurrency();
				List<Transaction> transactionForCurrency = transactionByCurrencies.get(currency);
				if (transactionForCurrency == null) {
					transactionForCurrency = new ArrayList<>();
					transactionByCurrencies.put(currency, transactionForCurrency);
				}
				transactionForCurrency.add(transaction);
			}
		}

코드만 봐도 어질어질 하다.

하지만 Stream API를 사용하면 다음처럼 문제를 해결할 수 있다.

		Map<Currency, List<Transaction>> transactionByCurrencies =
			new HashMap<>(); // 그룹회된 트랜잭션을 더할 map 생성

		transactions.stream()
			.filter((Transaction t) -> t.getPrice() > 1000)
			.collect(Collectors.groupingBy(Transaction::getCurrency));

레전드 ㅋㅋ

우선은 스트림 API를 이용하면 컬렉션 API와는 상당히 다른 방식으로 데이터를 처리할 수 있다는 사실만 기억하자.

컬렉션에서는 반복 과정을 직접 처리해야했다.

이런식으로 for-each 루프를 통해 모든 요소를 반복하면서 작업을 수행하는것을 외부 반복이라고 한다.

반면 스트림 API를 이용하면 루프를 신경 쓸 필요가 없다.

스트림 APU에서는 라이브러리 내부에서 모든 데이터가 처리된다.

이와 같은 반복은 내부 반복이라고 한다.

그리고 멀티 코어 환경을 잘 활용해 병렬로 작업을 처리하면 단일 CPU 환경에 비해 몇배는 더 빠르게 작업을 처리할 수 있다.

1.4.1 멀티스레딩은 어렵다

구현해봐서 안다

자바 8은 스트림 API로 컬렉션을 처리하면서 발생하는 모호함과 반복적인 코드 문제 그리고 멀티코어 활용 어려움 이라는 두가지 문제를 모두 해결했다.

자주 반복되는 패턴으로 주어진 조건을 필터링 하거나, 데이터를 추출 하거나, 데이터를 그룹화 하는 등의 기능이 있다.

또한 이러한 동작들을 쉽게 병렬화 할 수 있다는 점도 변화의 동기가 되었다.

여러 CPU CORE에서 자료구조에 접근할 때 한 CPU는 자료의 앞부분을 처리하고, 다른 CPU는 리스트의 뒷부분을 처리하도록 요청할 수 있다.

이 과정을 포킹단계 라고 한다.

각각 작업을 마친 후 마지막으로 하나의 CPU가 두 결과를 정리한다.

컬렉션을 필터링 할 수 있는 가장 빠른 방법은 컬렉션을 스트림으로 바꾸고 병렬로 처리한 다음에, 리스트로 다시 복원하는것.

  	//순차처리	
		List<Apple> heavyApples = inventory.stream()
			.filter((Apple a) -> a.getWeight() > 150)
			.toList();
    //병렬처리
		List<Apple> heavyApples2 = inventory.parallelStream()
			.filter((Apple a) -> a.getWeight() > 150)
			.toList();

1.5 디폴트 메서드와 자바 모듈

요즘은 외부 라이브러리를 import 해와서 시스템을 구축하는 경향이 있다.

이와 관련해 지금까지 자바에서는 특별한 구조가 아닌 평범한 자바 패키지 집합을 포함하는 JAR 파일을 제공하는것이 전부였다.

게다가 이러한 패키지의 인터페이스를 바꿔야 하는 상황에서는 인터페이스를 구현하는 모든 클래스의 구현을 바꿔야 했으므로 여간 고통스러운 작업이 아니었다. (인터페이스가 변경되면 구현체 전부 변경해야하는 고통)

자바 8에서는 인터페이스를 쉽게 바꿀 수 있도록 디폴트 메서드를 지원한다. (JPA interface에서 가끔 썼음)

하지만 프로그래머가 직접 디폴트 메서드를 구현하는 상황은 흔치 않다.

		List<Apple> heavyApples = inventory.stream()
			.filter((Apple a) -> a.getWeight() > 150)
			.toList();

		List<Apple> heavyApples2 = inventory.parallelStream()
			.filter((Apple a) -> a.getWeight() > 150)
			.toList();

자바 8 이전에는 Stream이나 parallelStream 메서드를 지원하지 않아 컴파일 할 수 없었다.

자바 8은 구현 클래스에서 구현하지 않아도 되는 메서드를 인터페이스에 추가할 수 있는 기능을 제공한다.

메서드 본문은 클래스 구현이 아니라 인터페이스의 일부로 포함된다.

디폴트 메서드를 이용하면 기존의 코드를 건드리지 않고도 원래의 인터페이스 설계를 자유롭게 확장할 수 있따.

예를 들어 자바 8에서는 List에 직접 sort 메서드를 호출할 수 있다. 이는 자바 8의 List 인터페이스에 다음과 같은 디폴트 메서드 정의가 추가되었기 때문이다.

진짜넹

따라서 자바 8 이전에는 List를 구현하는 모든 클래스가 sort를 구현해야 했지만 자바 8부터는 디폴트 sort를 구현하지 않아도 된다.

1.6 함수형 프로그래밍에서 가져온 다른 유용한 아이디어

자바에 포함된 함수형 프로그래밍의 핵심적인 두 아이디어는 메소드와 람다를 일급값으로 사용하는것, 가변 공유 상태가 없는 병렬 싫앵을 이용해서 효율적이고 안전하게 함수나 메서드를 호출하는 방법이 있다.

자바 8에서는 NullPointer 예외를 피할 수 있도록 Optional 클래스를 제공 (JPA 하면서 많이 봤음).

컨테이너 객체이며 값이 없는 상황을 어떻게 처리할지 명시적으로 구현하는 메서드를 포함하고 있다.

11장에서 자세히 설명할 예정

또한 구조적 패턴 매칭 기법도 있다.

자바에서는 if, then, else나 switch문을 이용했을것.

자바 8은 패턴 매칭을 완벽하게 지원하지 않는다.

왜 자바의 switch문에는 문자열과 기본값만 이용할 수 있는걸까?

18장과 19장에서 자세하게 알아볼 예정임