자바 8 람다 및 스트림 API 검토
이번 글은 Java8 람다와 스트림 API 관련해서 가벼운 마음으로 편하게 글을 작성하였다. 사실, Java13 이 나온 상황에서 Java 8에 대한 글을 쓰는 것이 참 부끄러운 마음이지만, 아직 실무에서 Java8을 많이 사용하기 때문에 꼭 필요한 내용이라고 생각한다. 하지만, 이 글은 Java Stream 및 Lambda에 대한 기본 개념을 전부 친절하게 설명하지는 않을 예정이다. 자세히 알고 싶은 개발자에게 필자가 몇 권의 책을 추천한다.
자바 8 인 액션
자바 8 람다의 힘
필자의 이 글도 해당 책을 일부 참고는 하였지만, 지극히 필자의 개인적인 생각이라는 점을 먼저 밝힌다. 필자의 글에서 잘못된 내용, 부적합한 내용 등은 댓글로 피드백을 꼭 남겨주길 바란다.
그동안 블로그에 글을 거의 못썼는데, 반성하는 의미로 이번 주말에 집중해서
생각나는대로 미친듯이 글을 막 쓰고 있다.
짧은 시간에 작성한 글이라서 내용이 허접하더라도 이해해주길 바란다...
Java 8 람다&스트림에 대해서 알아보기 전에, 함수형 프로그래밍에 대해서 잠시 생각해보자.
함수형 프로그래밍은 자료 처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임의 하나이다. [1] 주변 개발자들과 대화해보면 함수형 프로그래밍은 객체지향 프로그래밍과 완전히 반대의 개념이라고 오해하고 있는 개발자가 꽤 많은데, 사실 함수형 프로그래밍은 객체지향 프로그래밍, 즉 OOP와 반대되는 개념이 아니다. 사실, 필자가 함수형 프로그래밍을 논할 수 있을 만큼의 실력 있는 개발자가 아니기 때문에, 이 글에서 옳고 그름을 판단하지는 않겠다.
이 글은, 함수형 프로그래밍이 무엇인지, 어떤 특징이 있는지에 대해서 자세히 설명하지는 않겠다. 필자도 함수형 프로그래밍에 대해서 공부를 하고 있다. 나중에 각 잡고 글을 다시 작성하겠다.
물론, 스칼라 같은 함수형 언어를 사용하면, 함수형 프로그래밍을 더 잘 사용할 수 있지만, 언어 자체가 함수형 프로그래밍을 완성하는 것은 아니다. 함수형 프로그래밍은 개발 패러다임일 뿐이고, 그것을 구현하는 것은 개발자의 몫이다. 자바 언어로도 충분히 함수형 프로그래밍을 할 수 있다. 물론, 스칼라처럼 완벽한 함수형 프로그래밍을 하기는 어려울 수도 있지만, 어쨌든 언어에 종속되는 개념은 아니라고 생각한다.
함수형 프로그래밍에 대해서 추후에 다시 각 잡고 글을 쓸 예정이다. (필자의 역량으로는 아직 멀었다..)
자바에서 함수형 프로그래밍을 위한 람다식에 대해서 알아본다. 참고로, 해당 내용은 주니어 수준의 내용이라서, 2년 차 이상 되는 개발자는 볼 필요가 없다. 필자는, 개발을 많이 까먹은 상황이라서 정리할 겸 다시 공부하였다.
임백준 님의 책을 보면, Java 8에 람다가 어떻게 도입되었는지에 대한 배경을 알 수 있다. 관심 있는 개발자는 한번 읽어보길 바란다.
람다식은 익명 함수를 심플하게 구현해서, 코드를 매우 간결해할 수 있다. 간단한 인터페이스를 하나 만들어보자. 한 개의 추상 메서드를 정의한 SimpleInterface 인터페이스를 선언하자.
그리고, 인터페이스를 구현하는 익명 클래스를 만들고 사용해보자. 아주 간단하다.
해당 구현은 Java 8 버전이 등장하기 전까지 주로 사용하던 방식이다. JDK 8 버전에서 람다식이 추가된 이후로 우리는, 해당 구문을 람다식으로 심플하게 구현할 수 있다.
참고로, 람다식은 매개 변수를 가진 코드 블록이지만, 런타임 환경에서는 익명 구현 객체를 생성해준다. 람다식은 세 가지 부분으로 이루어지는데 아래와 같다.
위 경우는 매개변수가 없고, 리턴 값도 없는 경우이다. 만약, 매개변수가 있는 경우에는 어떻게 될까? String 변수를 매개변수로 받고, 리턴 값이 없는 메서드를 갖고 있는 인터페이스를 정의하였다.
해당 인터페이스를 익명 클래스로 사용해보면 아래와 같다.
람다식으로 변환해보자.
하나의 매개 변수만 존재한다면, ()를 생략할 수 있다. 또한 람다식이 하나의 실행문이라면 중괄호를 생략할 수 있는데, 위 샘플이 바로 생략한 구문이다. 만약, 중괄호를 생략하지 않는다면 아래와 같을 것이다.
아주 간단하게 람다식에 대해서 알아봤다. 사실, 너무 기초적인 내용이라서, 글을 쓰는 필자도 매우 부끄럽다. 근데, 다들 잘 알고 있겠지만 위와 같은 Interface를 굳이 작성할 필요가 없다. 왜냐하면, Java 8에서 기본적으로 함수형 인터페이스를 제공해주기 때문이다.
자바에서 기본적으로 제공하는 함수형 인터페이스에 대해서 알아보자. 자바에서 제공하는 함수형 인터페이스를 사용하면 되는 상황이라면, 굳이 별도로 커스텀하게 구현할 필요가 없다. 따로 구현하지 말고, 기본으로 제공해주는 함수형 인터페이스를 사용하는 것이 좋다. 물론, 기본 인터페이스 중 적합한 게 없다면 새로 만들어서 사용하면 된다. 이펙티브 자바에서도 유사한 의견을 참고할 수 있다.
Runnable
Supplier<T>
Consumer<T>
Function<T, R>
Predicate<T>
UnaryOperator<T>
BinaryOperator<T>
BiPredicate<T, U>
BiConsumer<T, U>
BiFunction<T, U, R>
Comparator<T>
Runnable는 스레드 생성할 때 주로 사용했었는데, 매개변수도 없고, 리턴 값도 없다.
Java8 이전에, Runnable를 사용한 예시는 아래와 같다.
해당 구문을 람다식으로 변경하면,
Supplier는 인자는 받지 않고 리턴 타입만 존재한다. 매개변수를 받지 않기 때문에 get 메서드는 항상 동일한 데이터를 반환할 것이다. (내부 로직이 랜덤 함수라면 결과는 바뀌겠지만)
Supplier에서 실행한 get 메서드는 T 타입의 데이터를 반환한다. 샘플 코드는 아래와 같다.
Consumer는 T 타입의 인자는 받고, 리턴은 하지 않는다. 즉, 전달받은 매개변수를 소비하고 끝내버린다.
추가로, andThen이라는 디폴트 메서드를 제공한다.
참고로, 메서드 레퍼런스를 사용해서 좀 더 간결하게 코드를 정리할 수 있다.
메서드 레퍼런스에 대해서는 나중에 다시 각 잡고 정리하겠다.
매개변수가 있고, 리턴 값도 있는 경우이다.
매개변수로 전달받은 String을 대문자로 변환해주는 아주 간단한 기능의 함수를 작성해보자.
eddy라는 매개변수를 받았고, 리턴 값으로 EDDY라는 대문자를 리턴해준다.
Predicate 는 T 타입의 매개변수를 받고, boolean를 리턴한다. Function<T, Boolean> 와 같은 기능을 한다고 생각해도 된다.
더 많은 함수형 인터페이스가 있다. 자세한 내용은 생략한다.
위에서 소개한 인터페이스는 모두 Functional Interface 어노테이션이 선언되어있다.
함수형 인터페이스에 Functional Interface 어노테이션이 필수는 아니다. 하지만, 가능하면 붙여주는 게 좋다. Functional Interface 어노테이션은 컴파일 단계에서 메스드가 하나만 선언되어있는지 체크해준다.
지금까지 Java8에서 제공하는 함수형 인터페이스에 대해서 알아봤다. Java 8에서 함수형 프로그래밍을 하기 위해서는 기본적으로 알고 있어야 하는 내용들이다.
Stream API에 대한 내용은 생략한다. "모던 자바 인 액션" 책을 읽어보길 바란다.
http://www.yes24.com/Product/Goods/77125987
명령형 프로그래밍, 함수형 프로그래밍에 대해서 고민해보자.
간단한 샘플 코드를 작성해보자. 카페에 가서 커피를 주문할 예정이다. 필자는, 커피 메뉴 중에서 우유가 들어가 있고 가장 비싼 메뉴를 주문하고 싶다. 고전적인 명령형 프로그래밍 방식으로 작성해보자.
coffees라는 변수를 선언한다.
coffees.getMilk()를 사용해서, 커피에 우유가 들어있는지 확인한다.
우유가 들어있다면, coffees.add(coffee)를 실행해서 변수에 데이터를 추가한다.
coffees.isEmpty를 실행해서 우유가 들어있는 커피가 있는지 검사한다.
coffees.get(0)를 실행해서, 우유가 들어있는 커피 중 첫 번째(가장 저렴한) 커피를 조회한다.
혹시, 해당 프로그래밍 방식에 대해서 어떻게 생각하는가? 해당 프로그래밍 방식은 "명령형 프로그래밍"이라고 부른다. 변수를 귀찮게 선언해줘야 하고, 너무 상세하게 작성된 코드이다. 사실, 개발자의 개발 스타일에 따라서 해당 방식이 더 편할 수도 있다. 필자 역시 이 방식이 더 편하다. 하지만... 함수형 프로그래밍에서는 이렇게 작성하지 않는다.
너무 상세하게 작성했던 "명령형 프로그래밍"을,
선언형 프로그래밍 방식, 즉 함수형 프로그래밍으로 개선하였다.
if 조건으로 지저분하게 코딩되었던 구문은 Stream의 filter 메서드로 대체되었다. 또한, findFirst 메서드를 사용해서 첫 번째 데이터를 조회한다. 데이터가 있는지에 대한 검증은 Optional 이 isPresent 메서드를 사용한다. if , else 구문은 filter의 메서드로 대체 가능하며, get(0) 구문은 findFirst의 메서드로 개선한 것이다.
함수형 프로그래밍에 대해서 혹시 감이 오는가? 또다른 예시를 생각해보자. Repository 에서 데이터를 조회할 때 LIMIT 제한으로 특정 개수의 데이터만 조회하고 싶다면 어떻게 할까?
명령형 프로그래밍 방법에서는 if 문을 사용하면 된다.
너무 상세하게 작성했던 "명령형 프로그래밍"을, "함수형 프로그래밍" 방법으로 변경해보자.
stream 의 limit 를 사용해서 작성하면 깔끔하게 코드를 작성할 수 있다.
함수형 프로그래밍에 대한 "느낌"이 오는가? 필자도 아직 정확히 와닿지는 않는다. 더 공부해서 나중에 기회가 되면 "함수형 프로그래밍" 이라는 주제로 다시 글을 작성하겠다.
바로 위에서 검토했던 코드를 좀 더 보자. 함수형 프로그래밍으로, 역할에 따른 분리를 심플하게 할 수 있다. 물론, 오해가 없기를 바라며, 함수형 프로그래밍이 아니라도 역할에 따른 분리는 잘할 수 있다. 이 글은 함수형 프로그래밍을 찬양하는 글은 절대 아니다.
아무튼, 만약 filter에 사용하는 조건이 여러 가지라면 어떻게 될까?
우유가 들어있는 커피의 총합을 구하는 기능, 1600원 이하의 커피의 총합을 구하는 기능을 구현해보자. 아래와 같이 filter에 필요한 기능을 구현해서 필터링을 해주면 된다.
참고로, sum 메서드를 사용하는 것도 함수형 프로그래밍이다. 만약 명령형 프로그래밍으로 구현했다면 어떻게 될까? for 문, 반복문을 실행해서 커피 가격을 전부 계산할 것이다.
[명령형 프로그래밍] for 문으로 가격을 합치는 것 ---> [함수형 프로그래밍] sum 메서드를 실행하는 것
어떤 방식을 사용할지는 여러분의 몫이다. 필자는 함수형 프로그래밍을 선택하겠다. 즉, Stream에서 제공해주는 sum 메서드를 사용하겠다. 어쩃든, filter에 들어가는 Coffee::getMilk 와 c.getPrice() < 1600 은 각자 다른 기능이지만, 총합을 구하는 로직은 동일하다. sum으로 총합을 구하는 로직을 별도의 함수로 분리해보자.
총합을 구하는 sumByPredicate라는 메서드를 정의하였다. 해당 함수는 커피리스트와 어떤 커피의 총합을 구할지에 대한 조건, 즉 Predicate 함수를 매개변수로 전달받는다. 전달받은 Predicate 함수형 인터페이스는 filter(predicate) 에 들어가게 된다.
아래 코드와 같이 사용할 수 있다.
코드가 심플해지는 장점뿐만 아니라, 더 큰 장점이 있다. 해당 기능, 즉 총합을 구하는 메서드를 테스트하기에 적합하다. 또한, sumByPredicate 메서드는 매개변수가 변경이 없다면 항상 동일한 결과를 반환할 것이다. 함수형 프로그래밍의 조건에 매우 적합한 방식이라는 것을 알 수 있다.
자바 8의 함수형 인터페이스를 사용하는 방법에 대해서 알아보자.
개인적으로 구현한 Interface를 사용해보자.
커피 가격을 할인해주는 메서드를 구현하였다. 이때, 할인 정책을 Function으로 전달받을 예정이다. 필자가 정의한 CustomFunction 함수형 인터페이스를 넘겨준다. T 타입의 매개변수를 받고, R 타입의 값을 넘겨주는데, 필자는 Coffee라는 객체를 넘겨서, 할인된 가격인 Integer 타입의 데이터를 넘겨준다.
첫 번째 파라미터인 Coffee 타입에는 latte라는 이름의 커피 객체를 넘겨준다. 두 번째 파라미터에는 CustomFunction을 넘겨주는데 반환 값은 R 타입, 즉 Integer이다. 해당 구현은 커피의 가격에서 100원을 할인해서 데이터를 넘겨주는 함수이다.
물론, 해당 방식을 람다식을 사용하지 않고, 익명 클래스로 구현할 수도 있다. 인텔리 J에서 친절하게 람다식으로 변경할 수 있다고 알려준다.
자... 위와 같이 필자가 별도로 구현한 인터페이스를, Java에서 제공하는 함수형 인터페이스로 변경해보자.
CustomFunction을 Function으로 변경하였다.
사용 방법은 CustomFunction과 동일하다.
여러분의 선택은 무엇인가? 별도로 작성한 인터페이스를 사용할 것인가? 아니면 자바에서 제공하는 함수형 인터페이스를 사용할 것인가??
"이펙티브 자바 3판"을 읽어보면, 기본으로 제공하는 함수형 인터페이스를 사용하는 것을 권장한다.
변수 점유 관련 내용은, 매우 중요하지만, 필자가 완벽하게 이해하지 못했기 때문에 필자의 글에 자신이 없다. 혹시라도 필자의 글이 이상하다면 피드백을 꼭 해주길 바란다.
람다식은 기존 익명 클래스와 변수 점유 방식이 다르다. 반드시 인지하고 알아야 하는 내용이다.
클래스에 prefix라는 String 타입의 변수를 선언하였다. 해당 변수는 클래스 어디서든 this.prefix로 사용할 수 있다.
해당 변수를 클래스 내에서 사용하는 익명 클래스에서 사용할 수 있을까?? 익명 클래스에서 this.prefix를 사용하면 에러가 발생한다.
클래스. this. 필드명으로 접근해야 한다. 하지만, 람다식에서는 좀 다르다.
람다 식에서는 this.prefix로 변수에 접근할 수 있다.
아주 중요한 내용이지만, 해당 내용은 클로저 개념과 함께 다시 공부를 해야 하겠다. 필자가 실력이 부족하니 이 정도로 정리하고 마무리한다.
생략. 나중에 시간이 되면 작성하겠다.
필자가 함수형 프로그래밍에 대해서 제대로 모르고 있는 것 같다. 자바에서 제공하는 Stream API, 람다식을 사용한다고 해서 함수형 프로그래밍이 완성되는 것은 절대 아닐 것이다. 함수형 프로그래밍에 대한 패러다임을 이해하는 것이 중요하다. 단지, 함수형 프로그래밍 언어를 사용한다고 해서, 함수형 프로그래밍을 잘하는 것은 절대 아닐 것이다. 앞으로 공부를 많이 해야겠다. 반성하면서 이 글을 마치겠다.
[2] 라인기술블로그 https://engineering.linecorp.com/ko/blog/functional-programing-language-and-line-game-cloud/