모던 자바 인 액션 3장

개요

이전 장에서는 ‘동작 파라미터화’ 에 대해 배웠다.

값(value)가 들어가야 하는 위치인 메소드 아규먼트에 ‘행위’ 자체가 들어가는 것이 바로 ‘동작 파라미터화’ 이다.

‘람다 표현식’에 대해 이전 2장에서도 잠깐 언급을 했는데, 이번 3장은 그 람다 표현식에 대해 예제를 통해 조금 깊게 살펴보는 시간을 갖도록 한다.

1. 람다 표현식

동작 파라미터를 이용해서 동작(혹은 메소드) 자체가 파라미터화 되며 조금 더 유연하고 재사용 할 수 있는 코드를 만들 수 있었다.

하지만, 그래도 해결되지 않는 문제가 있는데

‘코드가 깔끔하지 못하다’

(물론 람다 표현식도 살짝 맛보았지만) 사과를 필터링하는 filter 메소드를 재사용할 수 있는 코드로 만들기 위해서, 외부에서 클래스(Predicate 클래스)를 만들고, 그 클래스를 구현한 Predicate 메소드까지 만들게 되었다. filter 메소드만 보면 코드가 간결하고 깔끔하지만, 사실 filter 외부에서 너무 많은 코드를 작성하게 되는 오버헤드가 발생.

그래서 그 오버헤드마저 줄일 수 있도록,

일종의 ‘익명 함수’처럼 메소드를 정의와 동시에 다른 메소드의 파라미터로 전달시킬 수 있는 기법

이 ‘람다 표현식’ 이라 할 수 있곘다.

사실 이번 3장은 모던 자바에 대한 개념적 정리보다는, 실무에 곧바로 편히 사용하기 쉽게끔 ‘수많은 예제’ 위주로 작성이 되어 있어서… 학습의 의미보다는 그냥 습득? 에 대한 의미로 접근하는게 좋을 것 같다.

2. 그래서 람다란 무엇인가

책 본문 ‘3.1 람다란 무엇인가?’에서는 다음과 같이 정의한다.

  • 메소드로 전달할 수 있는 익명 함수를 단순화 한 것.

그리고 람다는 다음과 같은 특성을 가진다

  • 익명성
    • ‘이름 뭐짓지’ 에 대한 고민거리가 줄어든다
  • 함수
    • 메소드라고 하지 않는다. 어떤 클래스에 종속되어 있지 않기 때문에 ‘기능’의 측면이 강한 ‘함수’ 라는 이름을 가짐
  • 전달
    • 2급 값(메소드)을 1급 값(변수)으로 간주하는 그 모던 자바의 기능을 람다에도 적용 가능
  • 간결성
    • 간결하다.

그 외에 책으로는 ‘람다’라는 용어는 미적분학 학계에서 개발한 시스템에서 유래했다……고 하지만 난 읽지 않았다.

또한 람다를 사용하였을 때 ‘코드가 줄어드는’ 효과나, 표현식에 대한 설명도….. 읽지 않았다.

3. 함수형 인터페이스

함수형 인터페이스에서 람다라는 것이 대표적으로 사용 가능하다.

무슨 말인지는… 계속 읽어보면 이해가 될 것이다.

  • 함수형 인터페이스
    • 인터페이스가 구현하는 추상화된 메소드가 1개 있는 인터페이스

[expand title=”함수형 인터페이스를 모른다면” swaptitle=”닫기”]

함수형 인터페이스가 뭔지 잘 모르겠다면, 그냥 개발하면서, 공부하면서 한번쯤은 사용했던 Runnable 인터페이스 생각하면 이해가 쉽다.

보통 Runnable은 멀티스레드로 로직을 수행시키고 싶을 때 사용하는 인터페이스인데, 크게 두 범위로 동작을 쪼개볼 수 있다.

  • 내가 수행시키고 싶은 동작
    • (예를 들어)사과를 필터링
  • 컴퓨터(?)에게 위임시키고 싶은 동작
    • 스레드를 여러개 쪼개, 알아서 쪼갠 스레드별로 ‘어떤 동작’을 수행

보통 Runnable 인터페이스를 구현하는 클래스를 만들고, 그 클래스 타입의 오브젝트를 만들면, 반드시 구현해 주어야 하는게 run()이라는 메소드이다.

‘컴퓨터에게 위임시키고 싶은 동작’ 은 이미 Runnable 에 구현되어 있고, 나는 내가 수행시키고 싶은 동작을

함수(기능)형 인터페이스인 Runnable을 구현(implement)하여, ‘내가 수행시키고 싶은 동작(함수!기능!!)’ 만 정의하고 나머지는 상위 클래스인 인터페이스에게 위임

하는 것이다. 그 때, 정의된 Runnable 를 ‘함수형 인터페이스’ 라고 하는 것이다.

[/expand]

Runnable 인터페이스의 예제를 들어보자.

Runnable 인터페이스를 구현하게 되면, 개발자가 반드시 재정의(Override) 해야 하는 메소드가 있다.

run()

근데 문제는….

이 run 메소드를 구현하려고 다음과 같은 귀찮은 잡다구리한 코드들도 같이 포함이 되어야 한다.

귀찮다!

직접 익명 클래스를 일일히 생성해서 Runnable 인터페이스를 구현해야 한다.

하지만 람다를 사용하면?

안귀찮다!

기존에 2급 값이었던 ‘메소드’ 를 1급 값처럼 파라미터화 하는 언어차원에서의 기능추가 하나만으로

‘억지로 2급 값인 ‘메소드’를 1급값인 객체화(익명 클래스 구현)를 할 필요가 없다 이말이다.

그런데 사실 개념적으로는, 람다 자체가 함수형 인터페이스를 implement하는 것이다.

그럼 이제….. 단락 서두에 말했던 ‘함수형 인터페이스에서 람다를 사용 가능하다’ 가 무슨 말인지 이해가 될 것이다.

4. 실행 어라운드 패턴

jdbc로 DB의 값을 꺼내거나, 가공하는 것을 많이 해 보았다.

  • 자원을 열고
  • 하고싶은 일을 하고
  • 자원을 닫는다

이를 ‘순환 패턴’이라고 한다.

이건 실행 어라운드 패턴

위 소스코드처럼 processFile 메소드는 파일을 열어서(자원을 열고), 하고싶은 일을 하고(한줄 읽고), 파일을 닫는(자원을 닫는) 코드로 이루어져 있다.

근데, 이 실행 어라운드 패턴의 코드는

  • 한번 실행할때마다 한 줄만 읽음

의 단점을 가지고 있다. 두줄을 읽고 싶으면 두번 호출해야 하고(첫줄이 두번 나오겠지만 무시하자..), 특정 단어나 패턴을 찾고 싶다면 processFile 메소드 자체를 수정해야 하는 문제점이 있다.

그 때 processFile 자체에 동작 파라미터를 추가 & 람다화(?) 하여, 실행 어라운드 패턴의 boiler plate를 뺀 ‘한줄 읽는 작업 & 두줄 읽는 작업’ 을 간단하게 정의할 수 있다.

BufferedReaderProcessor 라는 함수형 인터페이스를 만들고, process()에서 ‘무슨 동작을 수행할 지’ 에 대해 caller에게 위임시켜서, processFile이 동작 파라미터를 받아 그 동작을 수행하는 일련의 과정이 잘~ 정리되어있다.

5. 대표적인 함수형 인터페이스를 소개하지

함수형 인터페이스 : 오직 하나의 추상 메소드를 갖고 있는 인터페이스

함수 디스크립터 : 함수형 인터페이스의 추상 메소드 시그니처(파라미터 타입+리턴타입)

모던 자바에서는 대표적으로 Predicate, Consumer, Function 라는 인터페이스를 제공해 준다.

5-1. Predicate

  • 메소드 시그니처
    • in : 오브젝트인 제네릭 타입 ‘T’
    • out : boolean

오브젝트인 제네릭 타입 ‘T’를 파라미터로 받아, boolean 으로 리턴 해 주는 인터페이스

Predicate

listOfStrings라는 문자열 리스트 중에서, nonEmptyStringPredicate 라는 동작이 true인 결과만 nonEmpty에 담기는 샘플 코드이다.

5-2. Consumer

  • 메소드 시그니처
    • in : 오브젝트인 제네릭 타입 ‘T’
    • out : void
Consumer. 말 그대로 파라미터를 흡수하고 암것도 안주네 ㅠㅠ

1,2,3,4,5 숫자 배열을 받아서, void를 리턴하니 메소드 그 자체적으로 무언가(출력)를 하는 샘플 코드이다.

5-3. Function

  • 메소드 시그니처
    • in : 제네릭 타입 ‘T’
    • out : 또다른 제네릭 타입 ‘R’
Function

Function 함수형 인터페이스를 이용하여,

List<String> 타입을 파라미터로 받아서, 또 다른(같아도 됨. 짜는놈 마음) 타입인 List<Integer> 로 리턴해 주는 샘플 코드이다.

기타 더 많은 함수형 인터페이스가 있으니, 한번 슥 보고 필요한 인터페이스가 있을 때 이를 멋있게 구현해 보자.

6. 타입 추론

레거시 자바 중, 가장 편했던(?) 것 중 하나가 ‘컴파일러에 의한 자동 타입 추론’이었다.

  • 다이아몬드 오퍼레이터
    • List<String> a = new ArrayList<>();

람다를 사용하는 곳에서도, 모던 자바는 매우 편하게 자동으로 타입을 추론해 주어, 개발자의 피로도를 줄이게 해 주곤 한다.

apple의 타입이 뭔데!?
  1. filter의 메소드 타입이 뭐지?
  2. List<Apple>, Predicate<Apple> 이겠지?
  3. 그리고 람다식의 리턴 타입이 boolean이네?
  4. 그럼 Apple타입을 받고, boolean을 리턴하는 Predicate 이군! 그럼 apple 변수의 타입은 Apple 타입일거임 ㅇㅇ

이런 식으로 추론한다

단, 이는 ‘타입을 생략해 적을 수 있는 방법이 있다’ 이지, 이게 무조건 좋다고 볼 순 없음. 가독성에 따라 가장 맞다고 생각하는 방법으로 구현해야 할 것이다.

7. 메소드 참조

너줄너줄 람다람다
깔끔깔끔 메소드 참조

사실 이건 ‘문법’에 대한 설명이고, 문법은 ‘외우는’ 영역이라 원리에 대해 그렇게 설명할 것이 별로 없음….. ‘그냥 이렇게 쓸 수 있으니까 외워둬!’

외워라 외워.

이후 람다 & 메소드 참조 활용하기, 역정렬, (+적분) 같은 경우는 과감히 생략

귀찮아서..

8. 헉헉헉헉….마치며…

  • 람다 표현식은 익명함수의 일종이다.
    • 사실 그냥 js의 익명 함수라고 생각해도 된다. 될까? (사실 난 그냥 ‘익명함수임’ 이라고 이해하고 있었음…)
  • 람다 표현식으로 익명클래스를 생략하여, 코드를 간결하게 만들 수 있다
    • 가독성 좋아지잖아!
  • 함수형 인터페이스 : ‘하나’의 추상 메소드만을 정의하는 인터페이스다. 두개는 안됨.
    • 함수형 인터페이스를 기대하는 곳에서만 람다를 사용할 수 있다.
    • 왜?
      • 람다로 호출할 땐 메소드의 이름이 생략된다. 인터페이스가 추상메소드를 2개 이상 가져버릴 경우에
        컴파일러의 입장을 들어보면, ‘대체 뭘 호출하라고’ 라고 할 것이다.
      • 심지어 2개의 추상메소드가 파라미터 타입, 리턴타입마저 같아버리면 그땐 컴파일러는 커녕 ‘사람도 구분 못하는’
        지경에 다다른다. 그래서 함수형 인터페이스를 기대하는 곳에서만 람다 사용 가능함.
  • 람다를 이용하여 함수형 인터페이스의 추상 메소드를 즉석으로 제공할 수 있으며, ‘람다 표현식 전체가 함수형 인터페이스의 인스턴스로 취급된다’
    • 앞에서 한 말이랑 비슷한데, 람다 표현식 자체가 ‘익명 클래스가 생략된’ 코드의 형식이기 때문이다.
    • ‘너줄너줄 쓰기 귀찮아서’ 람다 표현식이 ‘언어 차원에서’ 지원된 것이지, 개념적으로는 파라미터 패싱이 익명 클래스로 패싱되는 것이랑 동일하기 때문.
  • java.util.function 패키지는 우리가 자주 사용하는 ‘참/거짓’ , ‘행위’ , ‘객체소멸’, ‘객체생성’ 과 같은 자주 사용하는 다양한 함수형 인터페이스를 제공한다
    • 책에서는 다루었지만 난 다루지 않은 primitive data type을 다루는 함수형 인터페이스도 제공해 줌 ㅇㅇ
  • 실행 어라운드 패턴을 람다와 함꼐 이용하면, ‘DB커넥션을 얻고(자원을 얻음)’, ‘DB커넥션을 풀어주는(자원을 뱉뱉)’ 는 동작을 비 반복적으로 구현하며, 유연성과 재사용성까지 얻을 수 있다.
  • 메소드 참조는 문법을 외우자.
    • 못외우겠으면 IntelliJ를 사자
      • 노란줄 찍 그어주며 ‘이거 바꾸면 더좋음’ 이라고 추천해준다.(이클립스도 해주나?)

Leave a Comment