모던 자바 인 액션 - 6장

Daniel_Yang·2025년 5월 29일
0

Chapter 18 함수형 관점으로 생각하기

함수형 프로그래밍은 "데이터의 변환" 에 집중하여 더 안정적이고 유지보수하기 쉬운 코드를 만든다.

1. 시스템 구현과 유지보수

  • 예측 가능한 동작과 높은 유지보수성을 제공

  • 명령형 vs 선언형:
    • 명령형: "어떻게" 처리할지 명시 (루프, 조건문)
    • 선언형: "무엇을" 원하는지 표현 (스트림 API) → 가독성 ↑, 병렬화 쉬움

2. 함수형 프로그래밍이란 무엇인가?


3. 재귀와 반복

실무에서 재귀가 유용한 경우

  • 트리 구조 데이터 처리, 백트래킹 알고리즘, 분할 정복 알고리즘

꼬리 재귀 최적화

  • 누적값(acc)을 인자로 전달 → 스택 프레임 재사용 가능
// 일반 재귀 (스택 오버플로우 위험)
static long factorial(long n) {
    return n == 1 ? 1 : n * factorial(n - 1);
}

// 꼬리 재귀 (Java는 최적화 미지원, but 패턴 적용 가능)
static long factorialTail(long n, long acc) {
    return n == 1 ? acc : factorialTail(n - 1, acc * n);
}

함수형 실전 예제: 부분집합 생성

  • 문제: 리스트의 모든 부분집합 생성
  • 해결: 재귀 + 불변 데이터 구조 활용
    • subsets 메서드는 주어진 리스트의 모든 부분집합을 재귀적으로 구한다.
    • insertAll은 각 부분집합 앞에 현재 원소를 추가한 새로운 부분집합 리스트를 만든다.
    • concat은 두 리스트를 합쳐 새로운 리스트로 반환

+ 추가

  • 객체지향은 상태를 가진 객체와 그 객체의 메서드를 통해 문제를 해결.
  • 함수형은 상태를 바꾸지 않고, 순수 함수와 불변 데이터로 문제를 해결.
    • Java는 두 방식을 모두 지원하며, 스트림 API나 람다를 통해 함수형 스타일을 점점 더 많이 쓸 수 있다.

CHAPTER 19 함수형 프로그래밍 기법

자바 8의 함수형 프로그래밍은 코드의 안정성과 재사용성을 높이는 다양한 기법을 제공
불변성과 함수 조합을 통해 다음과 같은 이점

  • 동시성 문제 최소화
  • 코드 가독성 향상
  • 버그 발생 가능성 감소
  • 테스트 용이성 증가

1. 함수는 모든 곳에 존재한다

일급 함수

  • 함수를 변수에 할당하거나 인수/반환값으로 사용 가능
    Function<String, Integer> strToInt = Integer::parseInt;  // 메서드 참조 활용
  • Comparator.comparing처럼 함수를 조립하는 연산 파이프라인 구성 가능

고차원 함수

  • 함수를 인수로 받거나 반환하는 함수
  • 람다식이나 메서드 참조를 활용하여 구현
    Function<A, C> compose(Function<B, C> g, Function<A, B> f) {
    	return x -> g.apply(f.apply(x));  // 함수 조합
     }

2. 영속 자료구조

불변성 원칙

  • 기존 자료구조를 변경하지 않고 새로운 객체 생성 => 여러 곳에서 같은 자료구조를 참조해도 안전
    List<Integer> newList = new ArrayList<>(originalList);  // 방어적 복사
  • 트리 구조에서 노드 추가 시 기존 노드 재활용(효율적 메모리 관리)

3. 스트림과 게으른 평가

게으른 연산

  • 스트림 연산은 최종 결과 필요 시점에 실행
    IntStream.range(1, 10).filter(n -> n%2 == 0);  // 중간 연산만 정의
  • Supplier< T> 를 이용한 무한 수열 생성
    Supplier<Double> rand = Math::random;  // 호출 시마다 값 생성
    • Supplier< T >를 활용해 게으른 리스트를 직접 구현도 가능

4. 패턴 매칭

다중 분기 처리

  • 자바에서 람다를 활용한 패턴 매칭 흉내
  • 자바는 패턴 매칭을 직접 지원하지 않지만, 람다와 함수형 인터페이스를 이용해 비슷한 효과를 낼 수 있다
	static String patternMatch(Object obj) {
	    if (obj instanceof String s) return "String: " + s;
	    if (obj instanceof Integer i) return "Integer: " + i;
	    return "Unknown";  // 자바 16 패턴 매칭
	}

5. 기타 정보

커링

  • 여러 개의 인수를 받는 함수를, 일부 인수만 고정해서 새로운 함수를 만드는 기법
  • 다중 인수 함수를 단일 인수 함수 체인으로 변환
    // 변환 요소와 기준치만 고정해두고 변환할 값만 입력받는 함수
    DoubleUnaryOperator convertCtoF = curriedConverter(9.0/5, 32);  // 섭씨→화씨 변환

메모이제이션(캐싱)

  • 참조 투명성 활용한 결과 캐싱
  • 함수 결과를 저장해두고, 같은 인수가 들어오면 다시 계산하지 않고 저장된 값을 반환하는 기법
    Map<String, Function<Double, Double>> cache = new HashMap<>();  // 계산 결과 저장

콤비네이터(함수 조합)

  • 여러 함수를 조합해서 새로운 함수를 만드는 고차 함수 패턴

CHAPTER 20 OOP와 FP의 조화 : 자바와 스칼라 비교

프로그래밍 언어는 점점 더 "함수형"으로 진화하고 있으며, Java도 8부터 도입된 람다, 스트림, Optional 등으로 함수형 프로그래밍(FP)의 장점을 받아들이기 위한 변화를 꾀했다.
하지만 함수형 프로그래밍이 언어에 깊이 뿌리내리려면 문법 자체가 간결하고 불변성, 일급 함수, 고차 함수 등의 개념이 자연스럽게 녹아 있어야한다.
FP를 자연스럽게 지원하는 언어, 스칼라(Scala) 를 통해 자바와 비교하며 OOP와 FP의 조화를 살펴본다.

1. 스칼라 소개

  • 스칼라는 객체지향(OOP)과 함수형(FP)을 모두 지원하는 JVM 기반 언어
  • 자바처럼 클래스 기반 구조를 가지고 있지만, 함수를 일급 시민으로 대우하며 불변성과 표현식 중심 문법을 강하게 지향한다.
    - 일급 시민으로 대우 : 함수를 변수, 데이터 구조, 연산의 대상으로 자유롭게 다룰 수 있음을 의미

간단한 예:

// 1. 함수를 변수에 할당
val greet: String => String = (name: String) => s"Hello, $name!"

// 2. 함수를 인자로 전달
def printFormatted(formatFunc: String => String, name: String): Unit = {
  println(formatFunc(name))
}

// 3. 함수를 반환값으로 사용
def createGreeter(prefix: String): String => String = {
  (name: String) => s"$prefix $name!"
}

2. 함수형 스타일을 쉽게 만드는 스칼라의 기능들

문자열 보간 (String Interpolation)

  • 스칼라: s"Hello ${n} bottles"
    - 스칼라는 문자열 안에 변수를 직접 넣을 수 있어 코드가 깔끔하다.
  • 자바: "Hello " + n + " bottles"
    - 자바는 최근(JDK 21)에서야 미리보기로 유사 기능이 도입

컬렉션 – 기본이 불변

  • 스칼라:

    val list = List(1, 2, 3)  // 스칼라의 `List`, `Set`, `Map`은 기본적으로 불변
  • 자바:

    List<Integer> list = Arrays.asList(1, 2, 3);  // 변경 가능한 경우 많음

변수 선언: val vs var

  • val: 자바의 final처럼 재할당 불가
  • var: 가변 변수 (지양)
val name = "Alice"  // 불변
var count = 10      // 가변

튜플 (Tuple)

  • 자바에는 튜플이 없지만, 스칼라는 여러 값을 하나로 묶는 튜플을 지원
val book = (2018, "Modern Java in Action", "Manning")
// 자바에서는 클래스를 별도로 만들어야 하는 작업이다.

Option (자바의 Optional과 유사)

  • 스칼라의 Option은 값의 존재/부재를 안전하게 표현
val maybeName: Option[String] = Some("Alice")  // 또는 None


3. 함수형 프로그래밍 특성: 스칼라 vs 자바

일급 함수와 고차 함수

// 스칼라는 함수 자체를 변수로 다룰 수 있다.
val add = (x: Int, y: Int) => x + y


// 자바도 람다로 비슷하게 할 수 있지만, 함수 자체를 값처럼 다루는 것이 아니라 
// Function<T, R> 같은 인터페이스 구현이 기반
BiFunction<Integer, Integer, Integer> add = (x, y) -> x + y;

익명 함수와 클로저

// 스칼라는 람다(익명 함수)에서 외부의 변수를 자유롭게 참조할 수 있으며, 
// 이때 람다가 외부 환경을 "포착"하여 클로저가 된다.
val add = (x: Int) => x + externalVar
  • 자바: 외부 변수는 final 또는 사실상 final이어야 합니다.
// # scala
var externalVar = 10
val add = (x: Int) => x + externalVar  // 클로저 생성 시점의 externalVar 캡처

println(add(5))  // 15 (10 + 5)

externalVar = 20  // 외부 변수 값 변경
println(add(5))  // 25 (20 + 5) → 캡처된 변수의 최신 값 반영

// # java
int external = 10;
Function<Integer, Integer> add = x -> x + external;
external = 20;  // Error: 외부 변수 수정 불가

커링(Currying)

  • 스칼라

    • 커링(여러 인자를 받는 함수를 인자 하나씩 받는 함수의 체인으로 변환) 문법을 지원
      def add(x: Int)(y: Int): Int = x + y
       val add5 = add(5)_  // 새로운 함수 생성
  • 자바: 커링을 직접 구현하려면 복잡한 람다 체인이 필요



4. 클래스와 트레이트 (Trait)

클래스 선언

  • 스칼라: 생성자, 게터/세터 등을 자동 생성 => 매우 간결
    // 생성자와 불변 필드를 동시에 선언
    class Book(val title: String, val year: Int)
  • 자바: 모든 필드, 생성자, 메서드를 수동으로 작성해야 합니다.

트레이트: 유연한 다중 상속

  • 스칼라의 trait는 자바의 interface보다 강력하다.
    • 메서드 구현 포함 가능
    • 상태(필드)도 가질 수 있음
    • 다중 상속 가능
trait A { def hello() = println("Hello from A") }
trait B { def hello() = println("Hello from B") }

class C extends A with B  // 트레이트 다중 상속

요약

스칼라는 자바보다 간결하고 함수형 스타일을 자연스럽게 지원하며, 불변 컬렉션, 튜플, 일급 함수, 트레이트 등
다양한 기능을 제공해 객체지향과 함수형 프로그래밍의 조화를 이룬 언어다.


CHAPTER 21 결론 그리고 자바의 미래

자바 8 이후, 무슨 일이 있었을까?

1. 함수형 프로그래밍이 들어왔다 – 람다 & 메서드 참조

list.sort((a, b) -> a.compareTo(b));
list.sort(String::compareTo);
  • 함수(코드 블록)를 값처럼 다룰 수 있게 되었다.
  • 익명 클래스로 쓰던 코드를 훨씬 간결하게 바꿀 수 있다.
  • 반복되는 코드를 줄이고, 더 읽기 쉽게 만들었다.

2. 선언형 데이터 처리 – Stream API

List<String> result = list.stream()
    .filter(s -> s.startsWith("J"))
    .map(String::toUpperCase)
    .collect(Collectors.toList());
  • 데이터를 흐름(Pipeline)처럼 처리합니다.
  • filter, map, collect 등의 메서드 체이닝으로 가독성 ↑
  • 병렬 처리도 .parallelStream()으로 간단하게

3. null을 대체하는 안전한 방식 – Optional

Optional<String> name = Optional.ofNullable(user.getName());
name.ifPresent(System.out::println);
  • null 체크 지옥에서 벗어날 수 있는 구조.
  • ifPresent, orElse, map, filter 등으로 안전하고 깔끔하게 처리 가능.

4. 비동기 처리를 위한 새 도구 – CompletableFuture

CompletableFuture.supplyAsync(() -> getData())
    .thenApply(data -> process(data))
    .thenAccept(result -> display(result));
  • 비동기/병렬 처리도 이제는 복잡하지 않다!
  • thenCompose, thenCombine 등으로 여러 작업을 유연하게 조합 가능.

5. 실시간 데이터 흐름 – Flow API

  • 자바 9에서 리액티브 프로그래밍을 위한 표준 Flow 인터페이스가 도입됨.
  • 생산자-소비자 간 데이터 속도 차이를 조절하는 백프레셔(Backpressure)를 지원.
  • RxJava, Project Reactor 같은 외부 라이브러리와도 연동 가능.

6. 인터페이스도 진화했다 – 디폴트 메서드

interface MyInterface {
    default void hello() {
        System.out.println("Hello from interface!");
    }
}
  • 인터페이스도 기본 구현을 가질 수 있게 됨.
  • 기존 코드에 영향을 주지 않고 새로운 기능 추가 가능

자바 9~10: 모듈화와 간결한 코드

자바 9 – 모듈 시스템

  • module-info.java를 사용해 코드 구조를 명확히 나누고, 의존성을 명시할 수 있게 됨.
  • JDK 자체도 모듈화되어 가볍고 보안도 강화.

자바 10 – 지역 변수 타입 추론 (var)

var list = new ArrayList<String>();
  • 타입을 자동 추론해서 변수 선언을 더 간결하게 작성 가능.

그 외에도...

  • List.of(), Set.of() → 불변 컬렉션 생성
  • try-with-resources 개선
  • Stream API 기능 확장

자바의 미래는?

1. 패턴 매칭

  • ifinstanceof를 더 직관적으로 사용할 수 있게 하는 문법이 추가되고 있다.
if (obj instanceof String s) {
    System.out.println(s.toUpperCase());
}

2. 값 타입 (Value Types) – Project Valhalla

  • 객체처럼 쓰지만 메모리를 훨씬 효율적으로 사용하는 구조. 성능 향상 기대

3. 더 강력한 제네릭 – Project Amber

  • 제네릭 타입의 한계를 넘기 위한 다양한 기능이 실험되고 있다.

요약

자바는 람다, 스트림, Optional, CompletableFuture 등으로 간결하고 안전하며,
모듈화, 타입 추론 등으로 더 유연해졌고,
앞으로도 패턴 매칭, 값 타입, 제네릭 강화 등 혁신은 계속되고 있다.

0개의 댓글