Ian's Archive 🏃🏻

Profile

Ian

Ian's Archive

Developer / React, SpringBoot ...

📍 Korea
Github Profile →
Categories
All PostsAlgorithm19Book1C1CI/CD2Cloud3DB1DesignPattern9ELK4Engineering1Front3Gatsby2Git2IDE1JAVA7JPA5Java1Linux8Nginx1PHP2Python1React9Security4SpatialData1Spring26
thumbnail

Modern Java In Action 3장 정리

JAVA
2025.03.30.

Series

  • 1Modern Java In Action 1장 정리
  • 2Modern Java In Action 2장 정리
  • 3Modern Java In Action 3장 정리

2장에서는 동작 파라미터화를 이용해 더 유연하고 재사용 가능한 코드를 구현하는 방법을 학습했다.

3장에서는 더 깔끔한 코드를 작성하기 위한 람다 표현식을 배운다.

3.1 람다란 무엇인가?

람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것이라고 할 수 있다. 람다의 특징을 살펴보자

  • 익명 : 보통의 메서드와 달리 이름이 없는 익명으로 표현된다.
  • 함수 : 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부른다.
  • 전달 : 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
  • 간결성 : 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없다.

람다를 이용하면 동작 파라미터 형식의 코드를 더 쉽게 구현할 수 있으므로 코드가 간결하고 유연해진다.

복사
Comparator<Apple> byWeight = new Comparator<Apple>() {
    public int compare(Apple a1, Apple a2) {
        return a1.getWeight().compareTo(a2.getWeight());
    }
}

이 코드에 람다를 적용하면 다음과 같이 바뀐다.

복사
Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

람다는 세 부분으로 이루어진다.

  • 파라미터 리스트 : Comparator의 compare 메서드 파라미터 (Apple a1, Apple a2)
  • 화살표 : 람다의 파라미터 리스트와 바디를 구분 ->
  • 람다 바디 : 람다의 반환값에 해당하는 표현식 a1.getWeight().compareTo(a2.getWeight())

3.2 어디에 어떻게 람다를 사용하는가?

3.2.1 함수형 인터페이스

2장에서 필터 메서드를 파라미터화 하기 위해 사용했던 Predicate<T>가 바로 함수형 인터페이스이다.

함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스Comparator, Runnable등이 있다.

복사
public interface Predicate<T> {
  boolean test(T t);
}

public interface Comparator<T> {
  int compare(T o1, T o2);
}

public interface Runnable {
  void run();
}

3.2.2 함수 디스크립터

람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크럽터라고 한다. () -> void표기는 파라미터 리스트가 없으며 void를 반환하는 함수를 의미한다.

3.3 람다 활용 : 실행 어라운드 패턴

실제 자원을 처리하는 코드를 설정정리 두 과정이 둘러싸는 형태를 실행 어라운드 패턴이라고 부른다.

복사
public String processFile() throws IOException {
  try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
    return br.readLine(); // 실제 필요한 작업을 수행하는 행
  }
}

3.3.1 1단계 : 동작 파라미터화를 기억하라

위의 코드에서 한 번에 한 줄만 읽을 수 있는 문제를 어떻게 개선할 수 있을까? 람다를 이용해서 processFile의 동작을 파라미터화 해보자

복사
String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());

3.3.2 2단계 : 함수형 인터페이스를 이용해서 동작 전달

이번에는 BufferedReader -> StringIOException을 던질 수 있는 시그니처와 일치하는 함수형 인터페이스를 만들어서 동작을 전달해보자

복사
@FunctionalInterface
public interface BufferedReaderProcessor {
  String process(BufferedReader b) throws IOException;
}

public String processFile(BufferedReaderProcessor p) throws IOException {
  ...
}

3.3.3 3단계 : 동작 실행

람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있다.

복사
public String processFile(BufferedReaderProcessor p) throws IOException {
  try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
    return p.process(br);
  }
}

3.3.4 4단계 : 람다 전달

이제 람다를 이용해서 다양한 동작을 processFile 메서드로 전달할 수 있다.

복사
// 한 행을 처리하는 코드
String oneLine = processFile((BufferedReader br) -> br.readLine());

// 두 행을 처리하는 코드
String twoLine = processFile((BufferedReader br) -> br.readLine() + br.readLine());

3.4 함수형 인터페이스 사용

다양한 함수형 인터페이스들을 알아보자

3.4.1 Predicate

java.util.function.Predicate<T> 인터페이스는 test라는 추상 메서드를 정의하며 test는 제네릭 형식 T의 객체를 인수로 받아 불리언을 반환한다.

따로 정의할 필요없이 불리언 표현식이 필요한 상황에서 바로 사용할 수 있다.

3.4.2 Consumer

java.util.function.Consumer<T> 인터페이스는 제네릭 형식 T 객체를 받아서 void를 반환하는 accept라는 추상메서드를 정의한다.

T형식의 객체를 인수로 받아 어떤 동작을 수행하고 싶을 때 사용할 수 있다.

3.4.4 Function

java.util.function.Function<T, R> 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R객체를 반환하는 추상 메서드 apply를 정의한다.

입력을 출력으로 매핑하는 람다를 정의할 때 활용할 수 있다.

3.4.5 기본형 특화

위의 인터페이스들 외에도 특화된 형식의 함수형 인터페이스도 있다. 제네릭 파라미터에는 참조형(Byte, Integer, Object, List)만 사용할 수 있다.

박싱은 기본형을 참조형으로 변환하는 기능이고, 언박싱은 참조형을 기본형으로 변환하는 기능이다. 또한, 이들이 자동으로 이루어지는 오토박싱도 있다.

자바8에서는 이러한 오토방식 동작을 피할 수 있도록 IntPredicate과 같은 함수형 인터페이스를 제공한다.

복사
public interface IntPredicate {
  boolean test(int t);
}

IntPredicate evenNumbers = (int i) -> i % 2 == 0;
eventNumbers.test(1000);

Predicate<Integer> oddNumbers = (Integer i) -> i%2 != 0;
oddNumbers(1000);

3.5 형식 검사, 형식 추론, 제약

3.5.1 형식 검사

복사
List<Apple> heavierThan150g = filter(inventory, (Apple apple) -> apple.getWeight() > 150);

어떤 콘텍스트에서 기대되는 람다 표현식의 형식을 대상 형식이라고 부른다. 위의 예제에서는 다음과 같은 순서로 형식 확인 과정이 진행된다.

  1. filter메서드의 선언 확인
  2. filter메서드는 두 번째 파라미터로 Predicate<Apple>형식을 기대
  3. Predicate<Apple>은 test라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스
  4. test메서드는 Apple을 받아 boolean을 반환하는 함수 디스크립터
  5. filter메서드로 전달된 인수는 이와 같은 요구사항을 만족해야 함

3.5.2 같은 람다, 다른 함수형 인터페이스

대상 형식이라는 특징 때문에 같은 람다 표현식이라도 다른 함수형 인터페이스로 사용될 수 있다.

즉, 하나의 람다 표현식을 다양한 함수형 인터페이스에 사용할 수 있다.

복사
Comparator<Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

ToIntBiFunction<Apple, Apple> c2 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

BiFunction<Apple, Apple, Integer> c3 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

3.5.3 형식 추론

자바 컴파일러는 대상 형식을 이용해서 람다 표현식과 관련된 함수형 인터페이스와 시그니처를 추론할 수 있다.

때문에 람다 문법에서 이를 생략해서 코드를 더 단순하게 만들 수 있다.

복사
// 형식을 추론하지 않음
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

// 형식 추론
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

3.5.4 지역 변수 사용

람다식에서 지역변수는 final이 붙은 변경 불가능한 지역변수만 사용해야 한다.

3.6 메서드 참조

메서드 참조는 특정 람다 표현식을 축약한 것이라고 생각하면 된다. 메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달이 가능하다

복사
// 기존 코드
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

// 메서드 참조를 이용한 코드
inventory.sort(comparing(Apple::getWeight));

3.6.1 요약

람다 표현식 대신 명시적으로 메서드명을 참조함으로써 코드의 가독성을 높일 수 있다. 실제로 메서드를 호출하는 것은 아니므로 괄호는 필요 없다. 메소드 참조는 세가지 유형으로 구분된다.

  1. 정적 메서드 참조 : 예를 들어 Integer의 parseInt메서드는 Integer::parseInt로 표현할 수 있다.
  2. 다양한 형식의 인스턴스 메서드 참조 : 예를 들어 String의 length메서드는 String::length로 표현할 수 있다.
  3. 기존 객체의 인스턴스 메서드 참조 : 예를 들어 Transaction객체를 할당받은 expensiveTransaction 지역 변수가 있고, Transaction객체에는 getValue메서드가 있다면, 이를 expensiveTransaction::getValue라고 표현할 수 있다.

3.6.2 생성자 참조

ClassName::new 처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있다.

복사

Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get(); // Supplier의 get 메서드를 호출해서 새로운 Apple객체를 만들 수 있다.

3.7 람다, 메서드 참조 활용하기

지금까지 학습한 내용을 예제 코드에 적용해보자

3.7.1 1단계 : 코드 전달

복사
public class AppleComparator implements Comparator<Apple> {
  public int compare(Apple a1, Apple a2) {
    return a1.getWeight().compareTo(a2.getWeight());
  }
}

inventory.sort(new AppleComparator());

3.7.2 2단계 : 익명 클래스 사용

한 번만 사용하는 Comparator는 위 코드보단 익명 클래스를 이용하는 것이 좋다.

복사
inventory.sort(new Comparator<Apple>() {
  public int compare(Apple a1, Apple a2) {
    return a1.getWeight().compareTo(a2.getWeight());
  }
})

3.7.3 3단계 : 람다 표현식 사용

복사
inventory.sort((Apple a1, Apple a2) ->
                a1.getWeight().compareTo(a2.getWeight())
);

자바 컴파일러가 람다의 파라미터 형식을 추론할 수 있다고 했으므로 이 코드는 더 간결해진다.

복사
inventory.sort((a1, a2) ->
                a1.getWeight().compareTo(a2.getWeight())
);

Comparing메서드를 사용하면 더 가독성을 높일 수 있다.

복사
Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());

// 더 간소화 된 코드
inventory.sort(comparing(apple -> apple.getWeight()));

3.7.4 4단계 : 메서드 참조 활용

마지막으로 메서드 참조를 이용해 람다 표현식의 인수를 더 깔끔하게 전달해보자

복사
inventory.sort(comparing(Apple::getWeight));

3.8 람다 표현식을 조합할 수 있는 유용한 메서드

함수형 인터페이스에서는 다양한 유틸리티 메서드를 지원한다.

Comparator, Function, Predicate같은 함수형 인터페이스는 람다 표현식을 조합할 수 있도록 유틸리티 메서드를 제공하며, 간단한 여러 개의 람다 표현식을 조합해서 복잡한 람다 표현식을 만들 수 있다.

이 유틸리티 메서드는 디폴트 메서드로 제공되어 함수형 인터페이스의 정의를 해치지 않으며 여러 조합을 가능케 하는 유틸리티를 제공한다.

3.8.1 Comparator 조합

  • comparing - 비교에 사용할 Function기반의 키 지정
  • reversed - 역정렬
  • thenComparing - 동일한 조건에 대하여 추가적인 비교

3.8.2 Predicate 조합

  • and - and연산
  • or - or연산
  • negate - not연산

3.8.3 Function 조합

  • andThen - 이후에 처리할 function 추가
  • compose - 이전에 처리되어야 할 function 추가

Reference

모던 자바 인 액션

Previous Post
Modern Java In Action 2장 정리
Next Post
AssertThat 정리
Thank You for Visiting My Blog, I hope you have an amazing day 😆
© 2023 Ian, Powered By Gatsby.