[Modern Java In Action] CH03. 람다

khyojun·2023년 7월 8일

Modern Java In Action

목록 보기
3/3
post-thumbnail

CH3. 람다

람다 표현식은 함수를 하나의 식으로 표현한 것인데요. 메소드의 이름을 갖다버린 익명 함수(Anonymous Function)로 볼 수 있습니다.

이전 챕터 우리가 코드를 유연하고 간결하게 만들어보기 위하여 람다를 통해 코드를 변경시켜봤다. 어떤 특징을 가지고 있을까?

  • 익명 : 다른 메서드와 달리 이름이 없다.
  • 함수 : 특정 클래스에 종속되지 않아 함수라고 부른다. 메서드처럼 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 포함
  • 전달 : 메서드 인수로 전달하거나 변수로 전달할 수 있다.
  • 간결성 : 자잘구레한 코드를 구현 하지 않아도된다.

람다는 다음과 같이 이루어져 있다.


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

람다의 표현 스타일

1. (parameter) -> expression;

2. (parameter) -> {statements;}

자 람다 끝! 은 아니다. 이제 어떻게 사용되는지 deep 하게 가보자.

이전 챕터에서 봤었던 함수형 인터페이스를 사용했다. 바로 Predicate이다. 동작 파라미터화를 위해서 사용했던 이 친구가 함수형 인터페이스다.

함수형 인터페이스 : 하나의 추상 메서드를 지정하는 인터페이스

대표적인 예시로 Comparator, Runnable 이라는 친구들이 있다.


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

public interface Runnable{
    void run();
}

많은 디폴트 메서드드들이 있더라도 추상 메서드가 오직 하나면 함수형 인터페이스라고 한다!

책에 나온 문제인데 헷갈리는 것이 하나 있다.

public interface Adder{
    int add(int a, int b);
}

public interface SmartAdder extends Adder{
    int add(double a, double b);
}

정답부터 말하자면 Adder만 함수형 인터페이스이다. SmartAdder는 2 개의 추상 메서드를 가지고 있기 때문에 아닌 것이다.

그래서 핵심은! 함수형 인터페이스는 추상 메서드가 하나여야 한다!


람다를 활용한 실행 어라운드 패턴

대표적인 람다와 동작 파라미터화의 대표적인 예제이다. 크게 3가지 단계로 나뉜다고 한다.

  • 자원을 열고
  • 처리를 하고
  • 자원을 닫는다.

흔히 알고 있는 DB가 파일을 처리하는 방식이라고 볼 수 있다.

예시로 다음과 같은 코드가 나와있다.


  public static String processFile() throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
      return br.readLine();
    }
  }

위 코드는 파일의 한 행을 읽는 코드이다.

이제부터 위와 같은 코드를 점차 바꿔가보자.

본격적으로 어떻게 구성이 되는지 알아보자.

1. 동작 파라미터화 활용

한 번에 두 행을 읽으려면 어떻게 해야 할까?


String result = processFile((BufferReader br) -> br.readLine() + br.readLine()); 

이런식으로 람다를 이용하여 동작 파라미터화를 활용하면 되지 않을까? 하는 아이디어로 시작해서 아래의 단계를 거쳐가면서 람다를 넘길 수 있도록 해볼 것이다.

2. 함수형 인터페이스를 이용해서 동작 전달

@FunctionalInterface
public interface BufferedReaderProcessor {

    String process(BufferedReader b) throws IOException;

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

이번엔 함수형 인터페이스라는 것을 통해 동작을 전달해보는 것이다.

이 과정은 BufferReader -> String과 IOException을 던질 수 있는 시그니처와 일치하는 함수형 인터페이스를 만드는 과정이다.

이렇게 만들었으니 이제 processFile 메서드의 인수로 전달이 가능해졌다.

3. 동작 실행

여기서부터 이제 위에 이해가 되지 않은것이 하나씩 풀릴 것이다.

(BufferReader -> String) 시그니처와 일치하는 람다를 전달할 수 있다. 코드를 먼저 볼까?

 public static String processFile(BufferedReaderProcessor p) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader(FILE))) {
      return p.process(br);
    }
  }

이제 이것을 활용해서 람다로 표현해볼까?

람다로 실행

String oneLine = processFile((BufferReader br) -> br.readLine());
String twoLines = processFile((BufferReader br) -> br.readLine() + br.readLine());

이제부터 이 코드를 보고 다시 한 번 위로 올라가서 아까부터 변화한 과정을 하나씩 보면 다음과 같을 거다.

이 과정들을 보면 고정적이었던 processFile이 동작 파라미터를 통해 유동적으로 바뀌었다는 것을 알 수 있었다.

즉 람다, 함수형 인터페이스를 통해 한 행만 읽었던 코드를 이제 유동적인 코드로 바꿀 수 있다는 것을 알 수 있었다.


함수형 인터페이스 사용

함수형 인터페이스의 추상 메서드 시그니처를 함수 디스크립터라고 한다.

다양한 람다 표현식을 사용하려면 공통의 함수 디스크립터를 기술하는 함수형 인터페이스 집합이 필요하다! 자바 8 라이브러리에서는 java.util.Function 패키지에 여러 가지 새로운 함수형 인터페이스를 제공한다.

대표적으로는 Predicate, Consumer, Function 인터페이스 등등이 있다.

  • Predicate : T -> boolean
  • Consumer : T -> void
  • Function<T,R> : T -> R
  • Supplier : () -> T

이런 형식으로 반환하여 주는 함수형 인터페이스이다. T : Type R: 해당 객체이다.

그런데 궁금한 점이 있다 위 대표적인 예시라고 했던 함수형 인터페이스들은 다 제네릭의 형태로 같은 것이 있는데 여기 T안에는 항상 참조형 타입이 들어가게 된다.

이것은 제네릭의 내부 구현 때문에 어쩔 수 없다고 한다. 그리고 이렇게 기본형을 참조형으로 변환하는 기능을 자바에서 제공하는데 이것을 박싱 그 반대 기능을 언 박싱이라고 한다.

그리고 프로그래머가 편리하게 코드를 구현할 수 있게 오토 박싱도 지원을 해준다고 한다.

그치만 이런 박싱 작업이 장점이 될 수 있긴 하지만 사실 단점도 분명하다.

박싱한 값은 기본형을 감싸는 래퍼이며 힙 메모리에 저장한다. 그래서 메모리를 더 소비하게 된다.

그래서 자바 8에서는 기본형을 입출력으로 사용하는 상황에서 오토박싱 동작을 피할 수 있도록 하는 함수형 인터페이스들이 존재한다고 한다.

ex) DoublePredicate, IntConsumer, LongBinaryOperator, IntFunction .....

타입의 형식들이 앞에 붙어있는 것을 통해 확인을 하며 오토 박싱을 하지 않을 수 있도록 한다.


컴파일 할 때는 람다를 어떻게 바라보는지 알아보자.

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

이 코드를 컴파일 할 때 형식 검사를 다음과 같은 단계를 거친다고 한다.

  • filter 메서드 선언 확인
  • filter 메서드는 두 번째 파라미터로 Predicate 형식을 기대한다.
  • Predicate은 test라는 한 개의 추상 메서드를 가지는 함수형 인터페이스이다.
  • Predicate라는 것의 test 메서드는 Apple 을 받아 boolean을 반환하는 함수 디스크립터를 묘사한다.
  • filter 메서드로 전달되는 인수는 위와 같은 요구사항을 만족해야 한다.

여기서 만약 예외를 던질 수 있다면 추상 메서드도 같은 예외를 던질 수 있도록 throws로 선언을 해줘야 한다고 한다!


인터페이스는 다르지만 같은 람다를 사용하는 것

이런 case도 있을 것이다. 예를 들자면

같은 형태의 람다 표현식의 시그니처가 있을 때 다른 함수형 인터페이스여도 동일하게 사용이 가능하다고 한다.


Callable<Integer> c = () -> 42;
PrivillegedAction<Integer> p = () -> 42;

위 코드는 유효한 코드이다.

위에서 말한 것처럼 동일한 람다 표현식을 사용하지만 서로 호환이 되는 것을 알 수 있다!

여기에서 말하고 싶은 것은 하나의 람다 표현식이 다양한 함수형 인터페이스에 사용할 수 있다는 것이다.


형식 추론

다이아몬드 연산자 <>

List< String > lists = new ArrayList<>() 이 코드에서 <> 가 비어있지만 사실 String이 들어간다는 것을 통해 콘텍스트에 따른 제네릭 형식을 추론할 수 있다는 것을 알 수 있다!

갑자기 위와 같이 형식 추론에 대해서 언급을 했다. 이제부터 얘기할 것이기 때문이다.


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

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

자바 컴파일러는 위와 같이 람다 표현식이 사용된 콘텍스트를 이용해서 함수형 인터페이스를 추론한다. 이 말은 즉, 어떤 형태인지 굳이 Apple을 넣지 않아도 앞의 콘텍스트를 통해서 알 수 있다는 말이다.


람다에서의 지역 변수 사용

결론부터 말하자면 람다 표현식에서 사용하는 지역 변수는 final(한 번만 할당할 수 있는)을 붙인 것과 같은 변수만 사용할 수 있다.

왜냐하면 인스턴스 변수, 지역 변수의 태생이 다르기 때문이라고 한다. 스레드가 2개인 상황을 가정해서 보면 변수 할당을 담당한 1번 스레드, 람다를 담당한 2번 스레드가 있다고 했을 때

1번이 끝났는데 갑자기 2번에서 1번의 변수에 접근하려 하는 상황이 발생하기 때문이다. 그래서 자유 지역 변수(외부 지역 변수)의 복사본을 제공한다. 그래서 복사본의 값이 바뀌면 안되기 때문에 한 번만 할당해야 한다는 제약이 생긴 것이다.


메서드 참조

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

이런 구문을 봤을 거다. 여기서 ::이 바로 메서드 참조(람다의 축약형) 이다.

뭘 축약했냐?

(Apple a) -> apple.getWeight()

이 코드가 위의 메서드 참조를 활용해 축약이 되었다.

메서드 참조는 3가지 유형이 있다.

  1. 정적 메서드 참조
  2. 다양한 형식(type)의 인스턴스 메서드 참조
  3. 기존 객체의 인스턴스 메서드 참조

생성자 참조

위 맥락이랑 비슷하다. 다음과 같은 것이다.

Function<Integer, Apple> c = (Apple a) -> new Apple();    // -> Apple::new 생성자 참조
Apple a = c.apply(110);


Function<Integer, Apple> c = Apple::new
Apple a = c.apply(110);

이런 형식처럼 사용하는 것이다.

결론적으로 람다의 표현식을 축약해서 더 분명한 코드를 만들 수 있었다.


위 얘기들 요약

  • 람다 표현식은 익명 함수의 일종이다
  • 람다 표현식으로 간결한 코드 구현이 가능하다.
  • 함수형 인터페이스는 단 하나의 추상 메서드만을 정의하는 인터페이스이다.
  • 함수형 인터페이스를 기대하는 곳에서만 람다 표현식 사용이 가능하다.
  • 람다 표현식 전체가 함수형 인터페이스의 인스턴스로 취급된다.
  • java.util.function 에서는 다양한 함수형 인터페이스를 제공한다.
  • 실행 어라운드 패턴을 람다와 같이 활용하여 유연성, 재사용성을 얻을 수 있다.
  • 람다 표현식의 기대 형식을 대상 형식이라고 부른다.
  • 메서드 참조를 이용하여 기존의 메서드 구현을 재사용하고 직접 전달할 수 있다.
profile
코드를 씹고 뜯고 맛보고 즐기는 것을 지향하는 개발자가 되고 싶습니다

0개의 댓글