들어가며
언젠가 모 기업에서 면접을 볼때 이런 질문을 받은 적이 있다.
자바 8 이전과 이후에 달라진 점에 대해서 알고 있나요?
나는 대답을 못했었던 기억이 있다.
그 기억을 더듬어 정리하는 시간을 가져보려고 한다.
각 버전마다 변경점이 있지만
자바 8 버전에서의 큰 변경점이라고 한다면
람다식(Lambda Expression)과 스트림(Stream), 옵셔널(Optional) 등을 들 수 있겠다.
- 람다식(Lambda Expression)
람다식은 자바 8에서 도입되었다.
람다식이란 파라미터를 받아서 값을 반환하는 짧은 코드 블록을 말한다.
람다식은 메소드와 유사하지만 이름이 필요하지 않고, 본문 내에서 바로 구현이 가능하다. - W3Schools
개념적으로는 무슨말인지 잘 이해가 안간다.
간단한 코드로 살펴보자
(x, y) -> x + y
// (인수리스트) -> 일련의 처리나 반환값
이런 느낌이다.
메소드 이름이 없어 흔히 익명 함수(Anonymous functions) 라고도 말한다.
우리가 흔히 아는 자바 메소드를 생각한다면 아래처럼 생각할 수 있다.
class Sample{
public int calc(int x, int y){
return x + y;
}
}
그럼 결국 람다식이란 뭘 말하는걸까?
개념적으로는 함수입니다. 현실적으로는 함수형 인터페이스의 이름없는 클래스에 의해 구현을 간결하게 기술할 수 있도록 하는 것입니다. - 자바8 람다식 해설서
함수형 인터페이스(Functional Interface) ??
자바 8 에서부터는 '하나의 추상 메소드와 선언을 포함한 인터페이스' 를 '함수형 인터페이스' 라고 말한다.
함수형 인터페이스는 단일 추상 메소드(Single Abstract Method)를 가진다.
예를 들어 아래와 같은 interface가 있다고 치자.
public interface Calc {
int plus(int x, int y);
}
이 인터페이스의 오브젝트를 작성해보자.
Calc op = new Calc();
당연히 이런식으로 작성하면 인터페이스를 직접 인스턴스화하는 것이 불가능하기 때문에 컴파일 에러가 발생한다.
인터페이스를 인스턴스화 하고싶다면
아래 코드처럼 해당 인터페이스를 구현한 클래스를 정의하는 것이 일반적이다.
public class Main {
public static void main(String[] args) {
Toy t = new Toy();
System.out.println(t.plus(1,2));
}
}
public interface Calc {
int plus(int x, int y);
}
public class Toy implements Calc {
public int plus(int x, int y){
return x + y;
}
}
하지만 익명클래스를 사용하면 간략하고 에러없이 컴파일이 가능하다.
public class Main {
public static void main(String[] args) {
Calc c = new Calc() {
public int plus(int x, int y){
return x + y;
}
};
System.out.println(c.plus(1,2));
}
}
public interface Calc {
int plus(int x, int y);
}
new Calc() 뒤에 {} 블록이, Calc interface를 정의한 익명클래스가 된다.
익명클래스는 편리하지만 깔끔한 소스라고 할 순 없다.
여기서 '람다식'은 깔끔하지 않은 익명클래스를 심플하게 쓸 수 있도록 하는 편리한 기능인 것이다.
위 코드는 람다식을 이용해서 아래처럼 기술할 수 있다.
public class Main {
public static void main(String[] args) {
Calc c = (x, y) -> x + y;
System.out.println(c.plus(1,2));
}
}
public interface Calc {
int plus(int x, int y);
}
이처럼 람다식을 쓰면
인터페이스를 인스턴스화 하기 위한 클래스 정의나
익명클래스의 깔끔하지 않은 코드를 간단히 정의할 수 있다.
이정도면 대충 람다식이 무엇인지 감이 잡힌다.
그렇다해도 '굳이 람다식을 써야 할까?' 라는 생각이 떠나질 않는다.
사실 람다식의 진가는 '스트림(Stream)'을 위한 것이라고 해도 과언이 아니다.
- 왜 스트림과 람다식을 써야할까?
기존 코드가 스트림과 람다식을 쓰면 어떻게 바뀌는지 알아보자.
import java.util.*;
public class Sample {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(5, 8, 4, 3, 9, 2, 7);
int count = 0;
for(int x : list) {
if(x % 2 == 0) {
count++;
}
}
System.out.println("짝수 갯수 = " + count);
}
}
위 코드는 for문을 이용해서 list 내의 짝수 갯수를 구하는 코드다.
해당 코드를 스트림과 람다식으로 바꾸면 아래 코드처럼 작성할 수 있다.
import java.util.*;
public class Sample {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(5, 8, 4, 3, 9, 2, 7);
final long count = list.stream()
.filter(x -> x % 2 ==0)
.count();
System.out.println("짝수 갯수 = " + count);
}
}
한눈에 보기에도 코드가 간결해졌다.
뿐만 아니라 전자의 코드는
모든 데이터에 대해서 일일이 짝수를 체크해서 변수를 증가 시키는데 비해
후자의 코드는
데이터의 집합에 짝수 체크 로직(람다식)을 대입해서 결과를 취득하고 있는 것이다.
이렇다 할지라도 for문을 사용하면 되지 굳이 스트림을 써야하나 싶은 생각이 든다.
전자의 코드와 후자의 코드의 어떤 점이 다른지 비교해보자.
1. 상태 변경이 가능한(mutable) 변수의 사용
전자의 코드에서 count변수를 증가 시키기 위해 당변히 가변(mutable)적인 변수일 수 밖에 없다.
하지만 후자의 코드에서는 스트림에서 카운터한 결과를 그대로 count 변수에 대입하고 있기 때문에 불변(immutable)한 변수로 취급할 수 있다. (그 증거로 변수가 final로 선언되어 있다.)
2. 이터레이션(Iteration)
전자의 코드는 for 문을 사용해서 데이터를 일일이 조건을 붙여 확인하고 있다.
이것을 '외부 이터레이션(external iteration, 외부 반복)'이라고 부른다.
후자의 코드는 for문이 없다. 즉 이터레이션이 은폐되어 있다.
이것을 '내부 이터레이션(internal iteration, 내부 반복)'이라고 부른다.
외부 반복은 에러발생의 가능성이 많을뿐더러 기술하는 코드의 양도 길어지게 된다.
뿐만 아니라 외부 반복과 내부 반복은 성능면에서도 차이가 나게 된다.
3. 병렬화
전자의 코드처럼 for문을 사용하여 대량의 데이터를 이터레이트하게 되면, 복수의 CPU를 탑재하고 있다고 해도, 멀티 스레드를 활용해서 효율 좋게 실행하는 것이 불가능해진다. 스케일 아웃(scale out)을 하여 CPU를 복수개 탑재하더라도 결국 사용되는 CPU는 하나뿐인 것이다.
후자의 코드는 스트림을 사용하여 병렬화가 가능하다.
내부반복과 외부반복, 스트림의 병렬화의 대한 것은 차차 포스팅해보도록 한다.
람다식 정리
이로써 우선 람다식에 대해 알아보았는데, 결국 람다식은 함수형 인터페이스의 익명 클래스에 의한 구현을 간결하게 기술할 수 있게 한 것에 불과하다.
람다식은 깊이있게 이해하고, 심플하게 사용하는 것이 베스트라고 말할 수 있다. - 자바8 람다식 해설서
해설서의 말처럼 람다식은 이런거구나 하고 이해하고 넘어가는게 좋을 것 같다.
다음으로는 스트림에 대해 알아보자.
**출처
'개발자의 삶 > Java' 카테고리의 다른 글
[Java] 메소드 참조(Method References)에 대하여 ( 이중콜론 :: ) (0) | 2023.06.05 |
---|---|
[Java] 람다식(Lambda Expression)과 스트림(Stream)에 대하여 - (2) (1) | 2023.06.01 |
[Java] 오토박싱(Autoboxing), 언박싱(unboxing) (1) | 2023.05.27 |
[Java] 원시 타입(Primitive type), 참조 타입(Reference type), 래퍼클래스(Wrapper Class) (0) | 2023.05.26 |
[JAVA] 람다식 (map, filter, reduce, collect) (0) | 2021.05.04 |