들어가며
Java 8에서 도입된 람다(Lambda)는 현대 자바 개발에서 빼놓을 수 없는 핵심 기능입니다. 하지만 "왜 람다가 필요한가?"라는 질문에 명확히 답하기는 쉽지 않습니다. 이번 글에서는 실제 코드 예제를 통해 람다의 필요성을 이해하고, 기본 문법부터 활용법까지 단계별로 알아보겠습니다.
❓ 람다가 필요한 이유
문제 상황 1: 중복 코드 제거
다음 코드를 살펴보겠습니다.
public class Main {
public static void helloJava() {
System.out.println("프로그램 시작");
System.out.println("Hello Java");
System.out.println("프로그램 종료");
}
public static void helloSpring() {
System.out.println("프로그램 시작");
System.out.println("Hello Spring");
System.out.println("프로그램 종료");
}
public static void main(String[] args) {
helloJava();
helloSpring();
}
}
========================
실행 결과
========================
프로그램 시작
Hello Java
프로그램 종료
프로그램 시작
Hello Spring
프로그램 종료
"프로그램 시작"과 "프로그램 종료" 부분이 중복됩니다. 이를 어떻게 해결할 수 있을까요?
해결책: 값 매개변수화(Value Parameterization)
public class Main {
public static void hello(String str) {
System.out.println("프로그램 시작"); // 변하지 않는 부분
System.out.println(str); // 변하는 부분
System.out.println("프로그램 종료"); // 변하지 않는 부분
}
public static void main(String[] args) {
hello("hello Java");
hello("hello Spring");
}
}
변하지 않는 부분은 그대로 두고, 변하는 부분을 매개변수로 외부에서 전달받는 방식입니다.
- 변하지 않는 부분 : 프로그램 시작, 프로그램 종료
- 변하는 부분 : str 값
문제 상황 2: 코드 조각 전달하기
이번엔 더 복잡한 상황입니다.
import java.util.Random;
public class Main {
public static void helloDice() {
long startNs = System.nanoTime();
//========코드 조각 시작========
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
//========코드 조각 종료========
long endNs = System.nanoTime();
System.out.println("실행 시간: " + (endNs - startNs) + "ns");
}
public static void helloSum() {
long startNs = System.nanoTime();
//========코드 조각 시작========
for (int i = 1; i <= 3; i++) {
System.out.println("i = " + i);
}
//========코드 조각 종료========
long endNs = System.nanoTime();
System.out.println("실행 시간: " + (endNs - startNs) + "ns");
}
public static void main(String[] args) {
helloDice();
helloSum();
}
}
=======================
실행결과
=======================
주사위 = 2
실행 시간: 2882959ns
i = 1
i = 2
i = 3
실행 시간: 191083ns
실행 시간을 측정하는 코드가 중복됩니다. 하지만 이번에는 단순한 값이 아니라 하나의 실행 단위(코드 조각)를 전달해야 합니다.
해결책: 동작 매개변수화(Behavior Parameterization)
import java.util.Random;
// 정적 중첩 클래스 사용
public class Main {
public static void hello(Procedure procedure) {
long startNs = System.nanoTime();
//========코드 조각 시작========
procedure.run();
//========코드 조각 종료========
long endNs = System.nanoTime();
System.out.println("실행 시간: " + (endNs - startNs) + "ns");
}
static class Dice implements Procedure {
@Override
public void run() {
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
}
}
static class Sum implements Procedure {
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println("i = " + i);
}
}
}
public static void main(String[] args) {
Procedure dice = new Dice();
Procedure sum = new Sum();
hello(dice);
hello(sum);
}
}
다음과 같이, 인터페이스를 통한 전달에 있습니다. 인터페이스에 대한 구현클래스를 만들어 오버라이딩 한다면, 원활하게 공통화가 가능합니다. 즉, 동작 자체를 매개변수로 전달할 수 있습니다.
이와 같이, 단순한 값의 전달을 넘어, 하나의 코드 조각(행동) 자체를 외부에서 전달받도록 하는 것이 동작 매개변수화(Behavior Parameterization) 라고 표현합니다.
하지만 해당 방식으로 동작 매개변수화를 하려면, 클래스를 정의하고 해당 클래스를 인스턴스로 만들어서 전달해야 합니다. 이러한 구현 클래스는 특별한 특징이 있는것도 아니고, 단순히 추상 메서드를 구현하기 위한 껍데기 클래스를 생성해야 하는 의미를 내포합니다.
이러한 불편함을 해결하기 위해 익명 클래스 방식을 채택할 수 있습니다.
익명 클래스로 개선
꼭 클래스를 만들 필요없이, 익명클래스를 통해 문제를 해결 할 수 있습니다.
물론 변수에 대해서도 선언 할 필요 없이, 아래와 같이 직접 매개변수로 넣을수도 있습니다.
// 익명 클래스 사용
public class Main {
public static void hello(Procedure procedure) {
long startNs = System.nanoTime();
//코드 조각 시작
procedure.run();
//코드 조각 종료
long endNs = System.nanoTime();
System.out.println("실행 시간: " + (endNs - startNs) + "ns");
}
public static void main(String[] args) {
// dice
hello(new Procedure() {
@Override
public void run() {
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
}
});
// sum
hello(new Procedure() {
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println("i = " + i);
}
}
});
}
}
익명 클래스를 사용하면 별도의 클래스 정의 없이 간결해지지만, 여전히 보일러플레이트 코드가 많습니다.
이러한 익명 클래스의 방식을 더욱 극대화 시킨 방법이 람다의 탄생 배경입니다.
람다로 최종 개선
public class Main {
public static void hello(Procedure procedure) {
long startNs = System.nanoTime();
procedure.run(); // 코드 실행
long endNs = System.nanoTime();
System.out.println("실행 시간: " + (endNs - startNs) + "ns");
}
public static void main(String[] args) {
// dice (주사위)
hello(() -> {
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
});
// sum (1~3 출력)
hello(() -> {
for (int i = 1; i <= 3; i++) {
System.out.println("i = " + i);
}
});
}
}
람다를 사용하면 핵심 로직만 간결하게 표현할 수 있습니다.
✅ 람다의 기본
람다란?
람다는 익명 함수입니다. 이는 이름 없이 함수를 간결하게 표현 할 수 있음을 의미합니다.
기본적인 자바의 시그니처와는 다소 차이가 존재합니다.
기본 메서드 시그니처
반환타입 메서드명(매개변수){
본문
}
int add(int x) {
return x + 1;
}
람다 시그니처
(매개변수) -> {본문}
(int x) -> {return x + 1;}
✅ 람다 문법 규칙
람다 규칙은 차이점이 꽤 존재하여, 많은 이들이 꽤나 헷갈려 하는 문법이라고 생각합니다.
하지만, 많은 분들이 말하듯, 꼭 반드시 외울 필요는 없고 쓰다보면 자연스럽게 터득 될 것이라
이게 가능하구나~ 이건 불가능 하구나~ 정도로 넘어가면서 사용하면 될 듯 합니다.
람다의 문법 규칙에는 여러가지가 있겠지만, 쉽게 아래와 같은 분류로 나눌 수 있을 것 같습니다.
- 본문 내용이 한줄 / 여러 줄
- 매개변수의 갯수 (0, 1, 2개 이상)
- return 값의 유무
1. 단일 표현식의 중괄호와 return 생략
단일 표현식이란, 본문 내부에 return 한 문장으로 끝낼 수 있는 한 줄의 본문을 의미합니다.
단일 표현식의 경우, 본문의 중괄호"{ }"도 생략 가능하며, return 도 생략 가능합니다,
// 예시 인터페이스
public interface MyFunction{
int apply(int a, int b);
}
public class LambdaSimple {
public static void main(String[] args) {
// 기본
MyFunction function1 = (int a, int b) -> {
return a + b;
};
System.out.println("function1: " + function1.apply(1, 2));
// 단일 표현식인 경우 중괄호와 리턴 생략 가능
MyFunction function2 = (int a, int b) -> a + b;
System.out.println("function2: " + function2.apply(1, 2));
// 단일 표현식이 아닐 경우 중괄호와 리턴 모두 필수
MyFunction function3 = (int a, int b) -> {
System.out.println("람다 실행");
return a + b;
};
System.out.println("function3: " + function3.apply(1, 2));
}
}
//실행결과
function1: 3
function2: 3
람다 실행
function3: 3
public class LambdaSimple2 {
public static void main(String[] args) {
// 매개변수, 반환 값이 없는 경우
Procedure procedure1 = () -> {
System.out.println("hello! lambda");
};
procedure1.run();
// 단일 표현식은 중괄호 생략 가능
Procedure procedure2 = () -> System.out.println("hello! lambda");
procedure2.run();
}
}
2. 타입 추론
람다는 자연스럽게 매개변수 타입에 대해서 추론이 가능하기에, 매개변수 타입은 모두 생략 가능합니다.
public class LambdaSimple3 {
public static void main(String[] args) {
// 타입 생략 전
MyFunction function1 = (int a, int b) -> a + b;
// MyFunction 타입을 통해 타입 추론 가능, 람다는 타입 생략 가능
MyFunction function2 = (a, b) -> a + b;
int result = function2.apply(1, 2);
System.out.println("result = " + result);
}
}
3. 괄호 생략
만일 람다식의 매개변수가 1개일 경우, 매개변수의 괄호"( )" 는 생략이 가능합니다.
public class LambdaSimple4 {
public static void main(String[] args) {
MyCall call1 = (int value) -> value * 2; // 기본
MyCall call2 = (value) -> value * 2; // 타입 추론
MyCall call3 = value -> value * 2; // 매개변수 1개, () 생략 가능
System.out.println("call3 = " + call3.call(10));
}
interface MyCall {
int call(int value);
}
}
4. 매개변수가 없는 경우
3번과 달리, 만일 매개변수가 없는 경우의 람다식일 경우엔 "반드시"
매개변수에 괄호"( )"를 붙여줘야 합니다.
// 매개변수 없으면 빈 괄호 필수
Procedure procedure1 = () -> {
System.out.println("hello! lambda");
};
// 단일 표현식이면 중괄호 생략
Procedure procedure2 = () -> System.out.println("hello! lambda");
✅ 함수형 인터페이스 (@FunctionalInterface)
눈치가 빠른 분들은 이미 눈치를 채셨겠지만, 람다를 활용하고자 할 때 만든 인터페이스에는 한 가지 특징을 가지고 있었습니다.
바로, 람다를 쓰기 위해서는 정확히 하나의 추상 메서드를 가지는 인터페이스만 할당이 가능하다는 점 입니다.
다음 포스팅에서는 람다를 극대화 시킬 수 있는 함수형 인터페이스에 대해서 알아보겠습니다.
정리
람다의 핵심 개념
- 람다는 자바 8에서 도입된 익명 함수로, 간결하게 함수를 표현합니다
- 함수형 인터페이스는 단일 추상 메서드(SAM)만 포함하는 인터페이스입니다.
- 람다를 사용하기 위해서는 SAM 인터페이스에만 할당이 가능합니다.
람다 문법 요약
// 기본 형태
(매개변수) -> {본문}
// 생략 가능한 요소
(int x) -> {return x + 1;} // 기본
(x) -> {return x + 1;} // 타입 추론
(x) -> x + 1 // 단일 표현식 (return, 중괄호 생략)
x -> x + 1 // 매개변수 1개 (괄호 생략)
람다의 장점
- ✅ 코드 간결성: 익명 클래스 대비 보일러플레이트 코드 제거
- ✅ 가독성 향상: 핵심 로직에 집중 가능
- ✅ 함수형 프로그래밍: 동작 매개변수화를 통한 유연한 설계
- ✅ Stream API 활용: 컬렉션 처리의 강력한 도구
주의사항
람다는 익명 클래스를 간소화한 도구이지만, 내부적으로는 여전히 인스턴스가 생성됩니다. 따라서 메모리와 성능을 고려한 사용이 필요합니다.
마치며
람다는 단순히 문법을 줄이는 것을 넘어, 동작 매개변수화라는 강력한 설계 패턴을 가능하게 합니다. 변하는 부분과 변하지 않는 부분을 분리하고, 코드 조각 자체를 전달할 수 있게 되면서 자바는 더욱 유연하고 표현력 있는 언어가 되었습니다.
람다를 잘 활용하면 더 깔끔하고 유지보수하기 좋은 코드를 작성할 수 있습니다. 실제 프로젝트에서 적극적으로 활용해보시길 추천합니다!