모던 자바 인 액션 3 : 람다

minseok·2023년 4월 13일
0
post-thumbnail

시작하기 전

이번 포스팅은 허용되는 람다 표현식이 어떤 것 인지 알려주는 포스팅입니다. 이번 포스팅은 흥미가 없으며 대충 작성하였습니다.
개인적으로 느낌만 배우고 직접 사용하기를 추천합니다.




람다란 무엇인가?

메서드로 전달할 수 있는 익명 함수를 단순화 한 것
람다가 가질 수 있는 것에는 파라미터 리스트, 바디, 반환 형식, 발생할 수 있는 예외리스트가 존재합니다.

람다의 특징

익명 : 이름이 없으므로 익명이라 표현합니다.
함수 : 특정 메서드에 종속되지 않으므로 함수라고 부릅니다.
전달 : 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있습니다.
간결성 : 익명클래스처럼 지저분한 코드가 생기지 않습니다.

람다 표현식은 처음 해보면은 다소 난해할 수도 있습니다.
하지만 최신 IDE의 문법검사와 검색을 함께 이용하면 어렵지 않습니다.




람다 표현식의 특징


'문'과 '식'을 모두 표현할 수 있다.

문 (Statement) : 아무런 값을 만들지 않는다.
식 (Expression) : 값을 만들어 낸다.

또한 여러행의 문장을 포함할 수도 있습니다.


함수형 인터페이스 종류

java 8 설계자는 java.util.function 패키지에 여러 가지 함수형 인터페이스를 제공합니다.

Predicate : ( T t ) -> boolean
단일 참조 형태 인수(T)를 받아 불린을 반환합니다.

Consumer : ( T t ) -> void
단일 참조 형태 인수(T)를 받아 void를 반환합니다.

Function : ( T t ) -> R
단일 참조 형태 인수(T)를 받아 R을 반환합니다.

Generic의 한계때문에 원시 타입을 사용할 수 없는 문제가 있습니다.
이러한 문제를 해결하기 위해 원시 타입을 지원하는 함수형 인터페이스도 존재합니다.
IntFunction, IntToDoubleFunction 같이 원시 타입이 함수명에 포함되어서 제공됩니다.




형식 검사, 추론, 제약

람다식 자체에는 어떤 함수형 인터페이스를 구현하는지에 대한 정보가 포함되어 있지 않습니다.

형식 검사
람다가 사용되는 context를 이용해서 람다의 형식을 추론할 수 있습니다.
사용되는 context가 어떤 인터페이스를 사용하는가?, 타입 파라미터가 어떤 것 인가?, 어떤 추상 메서드가 있는가?에 대하여 디스크립터를 묘사한 후
전달하는 람다식이 위의 요구사항을 만족하는지 확인합니다.

void
람다의 바디에 일반 표현식이 있으면 void를 반환하는 함수 디스크립터와 호환됩니다.

형식 추론

형식을 추론하면 코드를 더 간단하게 작성할 수 있습니다.
람다 표현식이 사용된 컨텍스트를 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론합니다.

상황에 따라서 생산성을 위해 추론을 해야하는지는 개발자의 판단에 달렸습니다.
꼭 추론하는 방식을 사용했다고 해서 생산성이 올라가지는 않습니다.

지역 변수 사용

람다 표현식의 바디에서 파라미터로 넘겨준 변수가 아닌 외부의 변수를 참조할 수 있습니다.(람다 캡처링)
하지만 약간의 제약 사항이 존재합니다.
지역 변수의 경우 최소 effective final이여야 합니다.

스레드1이 종료된다면?


참조하는 경우 스레드1의 생명 주기가 종료되는 경우 문제가 생기므로 카피를 사용하는 스레드에서 카피를 해서 사용합니다.
이때 data1이 effecitve final이 아니라면 카피본과 값이 달라
문제가 생길 수 있으므로 람다에서 참조하는 자유 변수에는 제약이 생기는 것 입니다.

스레드1의 data1이 수정된다면?




추가적인 이야기 : 람다 예외처리

🌊 Stream과 함께 사용한 예시입니다.

IOException은 handling이 필요한 예외입니다.
readAllLines()를 수정하면 좋겠지만 read-only file이거나 잘못된 설계기반에서 usage가 많이 쌓였을 경우 수정하기가 쉽지않습니다.
이럴때는 람다에서 예외핸들링이 나오기 마련입니다.

public static List<String> readAllLines(Path path) throws IOException {
      return readAllLines(path, UTF_8.INSTANCE);
  } 

이전에 이렇게 되었니 저렇게 되었니 해도 일단은 기능을 만들어야해서 냅다 람다를 볶았습니다.
기능은 잘 되네요!

하지만 발견한 문제 : Lambda의 간결함이 사라짐





🐇 이제 더 개선된 방식을 제시합니다.

코드 자체는 C+V 로 긁었습니다.
주요 포인트는 아래와 같습니다.

  • 예외 핸들링 중복의 제거
  • 비즈니스 로직과 분리
  public class LambdaWrapper {
    public interface FunctionWithException<T, R, E extends Exception> {
        R apply(T t) throws E;
    }

    // Checked Exception 발생 시 별도의 로직처리가 없는 경우 사용
    static public <T, R, E extends Exception> Function<T, R> wrapper(FunctionWithException<T, R, E> fe) {
        return arg -> {
            try{
                return fe.apply(arg);
            }catch (Exception e) {
                throw new RuntimeException(e);
            }
        };
    }

}

lambda에 포함된 기능이 직접 작성한 기능이며 수정할 수 있는 상황이면 wrapper를 사용하지 않고 해당 기능에서 직접 RuntimeException을 상속한 예외를 던져도 됩니다.

위와 같은 경우가 아니며 예외 케이스에서 추가적인 로직이 없거나 회사의 규모가 작다면 도입해볼만 로직 입니다.
저의 경우에는 예외를 그대로 던져서 예외 관심사를 다루는 모듈에서 처리를 할 것 같습니다.
필자의 경우에도 스타트업 ~ 중소기업의 경험에 기반하여 작성하는 것 이므로 자신이 속한 환경을 고려하고 기능을 작성하는 것이 중요합니다.

변경된 코드

profile
즐겁게 개발하기

0개의 댓글