-
[모던 자바 인 액션] 자바 8 이후의 변화자바 2024. 10. 15. 14:19
팀 프로젝트, 알고리즘 문제를 해결하면서 다른 사람들의 코드와 많이 비교하면서 공부를 했다. 그 과정에서 자바에 대한 이해가 확실히 부족하다는게 느껴졌다. 그래서 이번 우아한 테크코스를 하면서 '모던 자바 인 액션' 책을 통해 기초를 잡기 위해 공부를 시작하고 있다. 특히, 자바 8부터 나온 스트림, Optional, 람다 표현식 등 내가 잘 사용하지 않았던, 사용하더라도 다른 사람의 코드를 참고하고 따라치면서 사용 방법만 알았던 것을 깊게 공부해보려한다.
처음에 공부하기 전에 어떻게 이런 개념들이 등장했고 필요성에 대해 궁금하였다. 뭐 결국엔 이전 방법보다 개발자한테 편하기 때문에 나온거라고 생각할 수 있는데 처음 사용해본 입장에서 뭐가 편한건지, 어떤 점이 도움이 되는지 느낄 수 없었기에 그 과정을 정리할 필요가 있었다. 이 책의 1장에서는 자바 8이 이런 개념들이 왜 나오게 되었는지 설명을 한다. 이제부터 그 과정을 하나씩 정리 해보려한다.
함수형 프로그래밍
이 책에서 자바 8이후의 핵심 사항은 함수형 프로그래밍에서 위력을 발휘한다는 점을 강조하였다. 하지만 나는 함수형 프로그래밍이라는 말이 와닿지 않았다. 그래서 먼저 이 개념부터 잡고 가야한다고 생각하고 따로 공부해서 정리를하려고 한다.
함수형 프로그래밍을 구글에 검색을하면 '함수를 일급 시민으로 다루며, 순수 함수와 불변성을 중시하는 프로그래밍 패러다임입니다. 이 방식은 수학에서의 함수 개념에 기반하여, 프로그래밍에서 상태 변화나 부작용을 최소합니다.' 이렇게 설명이 되어 있었는데 무슨 말인지 하나도 못알아 들었다 ㅋㅋㅋ쿠ㅜㅜㅜ.
먼저 일급 시민이 뭔지부터 알아보자!
일급 시민
일급 시민이란 프로그램 내에서 다른 객체들과 동일하게 취급될 수 있는 엔티티 즉, 직관적으로 쉽게 말하면 특정 타입의 데이터나 객체가 변수에 할당되거나 메소드의 인자로 전달되고, 함수의 반환값으로 사용할 수 있는 경우 이를 일급 시민이라고 한다. 그와 반대의 개념은 이급 시민이다. 자바 8 이전에는 함수는 이급 시민이였다. 즉, 메소드의 인자로 함수가 들어갈 순 없었다. 하지만 자바 8 이후로 부터 함수도 일급 시민으로 취급되어 함수형 인터페이스와 람다 표현식으로 가능해졌다.
public class Main{ public static void main(String[] args){ // 함수(람다 표현식)를 변수에 할당 Function<Integer, Integer> square => x -> x * x; // 함수를 다른 함수의 인자로 전달 applyFunction(5, square); // 함수를 반환값으로 사용 Function<Integer, Integer> doubleValue = getDoubleFunction(); System.out.println(doubleValue.apply(5)); } public static void applyFunction(int value, Function<Integer, Integer> func) { System.out.println(func.apply(value)); } public static Function<Integer, Integer> getDoubleFunction() { return x -> x * 2; } }
더 나아가 메서드와 람다 즉, 함수를 일급 시민으로 취급하면서 메서드 참조도 가능해졌다. 메서드 참조가 정확히 어떤 말인지 예시를 통해서 확인해보자!
// 자바 8 이전 코드 File[] hiddenFiles = new File(".").listFiles(new FileFilter() { public boolean accept(File file) { return file.isHidden(); } })l
세 행의 코드지만 각행이 무슨 작업을 하는지 투명하지 않다. File 클래스에 이미 isHidden이라는 메서드가 있어도 자바 8 전에는 굳이 FileFilter()로 감싸서 인스턴스화해야 해당 메서드를 사용할 수 있었다.
// 자바 8 이후 File[] hiddenFiles = new File(".").listFiles(File::isHidden);
자바 8의 메서드 참조(이 메서드의 값을 사용하라는 뜻)를 이용해서 listFiles에 직접 메소드를 전달할 수 있게 되었다. 코드에 대한 자세한 내용은 이후에 알아보고 현재는 함수가 일급 시민으로 취급된다는 것에 집중하자!
순수 함수와 불변성
순수 함수는 같은 입력에 대해 항상 같은 결과를 반환하며 부작용이 없는 함수를 말한다. 즉, 외부 상태를 변경하거나 함수 밖의 상태에 의존하지 않는 것을 말한다. 불변성은 상태를 변경하지 않고 불변 객체를 사용하여 데이터가 변경되면 새로운 객체를 생성해서 반환하는 것이다. 이를 통해 여러 가지 사이드 이펙트를 방지하고 여러 쓰레드 에서 안전하게 공유할 수 있게 된다.
하지만 함수에는 문제가 있다. 만약 공유 변수나 객체가 있으면 병렬성에서 문제가 발생한다. 예를 들어 X라는 변수를 함수1과 함수2가 함께 사용하고 수정할 때 동시에 수정을 가한다면 문제가 발생할 수 있다. 이를 방지하기 위해 자바 8 이전에는 synchronized를 이용해서 공유된 가변 데이터를 보호하는 규칙을 개발자가 직접 만들었다. 이는 번거러울 뿐만 아니라 시스템 성능에도 악영향을 미친다.(lock 경쟁, deadlock 등등). 즉, 불변성 상태를 유지하기 힘들었다.
하지만 자바 8 부터는 스트림을 통해 병렬적인 문제를 쉽게 해결하게 되었다. 예시 코드는 다음과 같다.
// 순차 처리 방식 코드 import static java.util.stream.Collectors.toList; List<Apple> heavyApples = inventory.stream().filter((Apple a) -> a.getWeight > 150).collect(toList()); // 병렬 처리 방식 코드 import static java.util.stream.Collectors.toList; List<Apple> heavyApples = inventory.parallelStream().filter((Apple a) -> a.getWeight > 150).collect(toList());
메서드가 서로 상호작용하지 않는다면 즉, 같은 변수를 쓰지 않는다면 병렬적으로 처리하게 끔 스트림을 작은 스트림으로 분할하여 처리한 후 결과를 합치게 된다. 현재 코드를 예를 들면 현재 코드는 사과의 무게가 150 이상인 사과를 고르고 있는데 하나의 cpu가 전부 비교하면서 하는 것이 아니라 2개의 cpu가 리스트의 절반을 각각 확인하여 150인 사과를 고르고 마지막에 합치게 된다. 만약 자바 8 이전에 하려면 개발자가 직접 쓰레드를 관리해야했다...
함수형 프로그래밍의 장점을 정리해보면 다음과 같다.
- 가독성 : 순수 함수와 불변성 덕분에 코드를 읽고 이해하기가 더 쉽다. 코드의 동작을 예측할 수 있으면, 코드가 상태 변화에 의존하지 않기 때문에 디버깅에 용이하다.
- 테스트 용의성 : 순수 함수는 같은 입력에 대해 같은 출력을 반환하기 때문에 단위 테스트를 하기 용이하다.
- 병렬 처리 : 함수형 프로그래밍에서 상태가 불변이므로 여러 쓰레드에서 동일한 데이터를 처리할 때 충동이나 부작용이 생기지 않는다.
- 재사용성 : 중복된 코드를 중리고 재사용한 함수로 추상화할 수 있다. 쉽게 얘기하면 함수를 인자로 전달할 수 있기에 중복된 코드를 방지할 수 있다.
단점은 불변성을 유지하기 위해 데이터를 변경할 때마다 새로운 객체를 생성해야 하므로, 객체 생성 비용이 증가할 수 있다. 하지만 최신 JVM 에서는 이러한 비용을 줄이기 위해 최적화가 이루어졌다고 한다.
이렇게 함수형 프로그래밍의 중요성으로 람다, 스트림 등 다양한 개념이 등장하였다. 그 외엔 뭐가 있을까?
디폴트 메서드
요즘은 외부에서 만들어진 컴포넌트를 이용해 시스템을 구축하는 경향이 있다. 자바 8 이전에는 인터페이스를 바꿔야하는 상황이 생기면 인터페이스를 구현하는 모든 클래스의 구현을 바꿔야했다. 하지만 자바 8 부터는 디폴트 메서드가 등장하면서 인터페이스의 확장이 쉬워졌다. 예시를 통해 한번 이해해보자!
// 순차 처리 방식 코드 import static java.util.stream.Collectors.toList; List<Apple> heavyApples = inventory.stream().filter((Apple a) -> a.getWeight > 150).collect(toList()); // 병렬 처리 방식 코드 import static java.util.stream.Collectors.toList; List<Apple> heavyApples = inventory.parallelStream().filter((Apple a) -> a.getWeight > 150).collect(toList());
이전 자바 8 이후의 스트림을 이용한 코드이다. 하지만 자바 8 이전 환경에서는 컴파일 오류가 무조건 나게 된다. 가장 간단한 해결책은 직접 인터페이스를 만들어서 자바 8 설계자들이 했던 것처럼 Collection 인터페이스에 stream 메서드를 추가하고 ArrayList 클래스에서 메서드를 구현하는 것이다. 하지만 이 방법은 너무 가혹하다.. 이미 컬랙션 API 인터페이스를 구현하는 많은 컬렉션 프레임워크가 존재하는데 인터페이스에 새로운 메서드를 추가한다면 이를 구현하는 모든 클래스는 새로 추가된 메서드를 구현해야한다.
그래서 이를 해결하기 위해 디폴트 메서드가 등장하였다.
default void sort(Comaprator<? super E> c) { Collections.sort(this,c); }
다음과 같이 확장하고 싶은 메서드를 디폴트 메서드를 통해 구현하면 구현 클래스에서 구현할 필요가 없어진다. 즉, 기존의 코드를 건드리지 않고 원래 인터페이스 설계를 자유롭게 확장할 수 있다.
자바 8에는 이 외에 Nullpoint 예외를 피할 수 있도록 클래스를 한번 감싸는 Optional, 패턴 매칭 활용 등 흥미로운 기법이 있다. 하지만 핵심은 결국 자바 8에서의 큰 변화는 함수형 프로그래밍이다! 이를 계속 생각하면서 자바 8부터 생겨난 새로운 개념들에 대해 차차 공부해 나갈 것이다!
'자바' 카테고리의 다른 글
[모던 자바 인 액션] 컬렉션 API (0) 2024.10.25 [모던 자바 인 액션] 스트림으로 데이터 수집 (0) 2024.10.24 [모던 자바 인 액션] 스트림 활용 (1) 2024.10.22 [모던 자바 인 액션] 스트림 (0) 2024.10.21 [모던 자바 인 액션] 동작 파라미터화와 람다 표현식 (1) 2024.10.20