람다 표현식에서 final 변수나 effectively final 변수를 사용해야 하는 이유
Java에서 람다 함수를 사용할 때 final 변수 또는 effectively final 변수를 사용해야 하는 이유에 대해서 알아보자.
자바에서 람다 함수를 사용할 때 'final' 변수 또는 effectively final 변수를 사용하는 이유는 자바의 클로저(closure) 구현과 관련이 있다. 클로저란 함수가 정의될 때의 스코프에 있는 변수들을 기억하고, 함수가 실행될 때 이러한 변수들에 접근할 수 있는 기능을 말한다.
일단 final 변수와 effectively final 변수의 개념부터 이해하자.
final 변수와 effectively final 변수가 무엇인가
자바에서 'effectively final' 변수는 final 변수와 유사하지만, 몇 가지 중요한 차이점이 있다. 이 개념을 이해하기 위해 상황을 설정해 보겠다.
Spring Boot를 사용하여 REST API를 개발한다고 가정해 보자. 이 API에서는 람다식이나 익명 클래스 안에서 로컬 변수를 사용하려고 한다. 자바 7 이전에는, 이러한 상황에서 해당 로컬 변수는 반드시 final로 선언되어야 했다. 하지만 자바 8부터는, 'effectively final'이라는 개념이 도입되었다.
Effectively Final이란?
변수가 선언된 후에 값이 변경되지 않으면, 자바 컴파일러는 이 변수를 'effectively final'로 간주한다. 즉, final 키워드를 명시적으로 사용하지 않았음에도 불구하고, 그 변수는 마치 final인 것처럼 취급된다.
Final과의 차이점
Final 변수: 명시적으로 final 키워드를 사용하여 선언한 변수. 이 변수의 값은 선언 후에 변경할 수 없다.
Effectively Final: final 키워드를 사용하지 않았지만, 값이 할당된 후에 변경되지 않는 변수. 자바 컴파일러는 이러한 변수를 final로 취급한다.
public void someMethod() {
int a = 10; // 이 변수는 변경되지 않으므로 effectively final
int b = 20; // 이 변수도 마찬가지로 effectively final
// 람다식 내부에서 사용
Runnable r = () -> System.out.println(a + b);
// 만약 a나 b의 값을 변경한다면, 이들은 더 이상 effectively final이 아님
// a = 30; // 주석을 해제하면 컴파일 오류 발생
}
이 예시에서 'a'와 'b'는 명시적으로 final로 선언되지 않았지만, 값이 변경되지 않으므로 effectively final로 간주된다. 이 변수들은 람다식 내에서 사용될 수 있다. 하지만 만약 이 변수들의 값을 메소드 내에서 변경한다면, 이 변수들은 더 이상 effectively final이 아니게 되고, 람다식 내에서 사용할 수 없게 된다.
이런 개념은 특히 람다식이나 익명 클래스를 사용할 때 중요하다. 이를 통해 코드의 복잡성을 줄이고, 불필요한 클래스 생성을 피할 수 있다.
람다와 'final' 변수 사용의 중요성
변수 캡처(Variable Capture)
람다 표현식에서 중요한 개념은 클로저를 통한 변수 캡처다. 람다 표현식은 자신이 정의된 메서드의 로컬 변수를 "캡처"하여 사용할 수 있다. 이를 위해 자바에서 람다 표현식에 참조되는 지역 변수들은 'final' 또는 effectively final이어야 한다. 이는 람다 표현식이 실행될 때 변수 값의 일관성을 유지하기 위함이다.
변수의 불변성(Immutability)
'final' 키워드는 변수의 재할당을 방지한다. 이는 람다 표현식이나 익명 클래스 내에서 변수가 변경되는 것을 방지함으로써, 멀티스레드 환경에서 데이터의 무결성을 유지하는 데 도움이 된다.
함수형 프로그래밍 패러다임
람다 표현식은 자바에서 함수형 프로그래밍의 일부를 구현한다. 함수형 프로그래밍에서는 불변성이 중요한 개념으로, 'final' 변수의 사용은 이러한 패러다임에 부합했다. 람다 표현식을 통해 개발자는 코드의 복잡성을 줄이고, 불필요한 클래스 생성을 피할 수 있었다.
올바른 사용 예시
이 코드에서 'base'는 'final'로 선언되었고, 람다 표현식 내부에서 이를 사용하여 값을 계산한다. 'base'가 'final'이므로 람다 표현식 내부에서 그 값이 변경되지 않음을 보장할 수 있다.
public class LambdaFinalExample {
public static void main(String[] args) {
final int base = 10; // final 변수 선언
IntFunction<String> multiply = (int factor) -> "Result: " + (base * factor);
System.out.println(multiply.apply(5)); // "Result: 50" 출력
}
}
잘못된 사용 예시
이 코드에서 'base'는 'final'이 아니며, 람다 표현식 내부에서 `base`의 값을 변경하려고 시도한다. 이는 자바에서 허용되지 않으며, 컴파일 오류를 발생시킨다. 람다 표현식 내에서 외부 지역 변수를 변경하는 것은 람다의 불변성 원칙에 어긋나며, 이는 변수 캡처의 원칙을 위반한다.
public class LambdaNonFinalExample {
public static void main(String[] args) {
int base = 10; // final이 아님
IntFunction<String> multiply = (int factor) -> {
base += 5; // 컴파일 오류 발생
return "Result: " + (base * factor);
};
System.out.println(multiply.apply(5));
}
}
헷갈릴만한 예시 코드
예시 코드 (문제 상황)
아래의 코드는 람다 표현식을 사용하여 각 User 객체의 username 필드를 업데이트하려고 한다. 그러나 이 코드는 컴파일 에러를 발생시킬 수 있다.
public void updateUsernames(List<User> users, String suffix) {
String newSuffix = suffix.toUpperCase(); // 변환된 접미사
users.forEach(user -> {
String updatedUsername = user.getUsername() + newSuffix; // 업데이트된 username
user.setUsername(updatedUsername);
});
}
문제점
여기서 newSuffix는 람다 표현식 내부에서 사용되지만, 람다 표현식 외부에서 값이 변경될 수 있으므로 effectively final로 간주되지 않을 수 있다.
문제 해결 과정
1단계: 문제 인식
먼저, 람다 표현식 내에서 사용되는 외부 변수는 final이거나 effectively final이어야 한다는 점을 인식한다. newSuffix는 람다 표현식 외부에서 정의되고, 람다 내부에서 사용되므로 이 규칙에 영향을 받는다.
2단계: 변수 수정
newSuffix 변수를 effectively final로 만들기 위해, 이 변수에 대한 모든 수정을 람다 표현식 이전에 완료하고, 람다 표현식 내부에서는 이 변수를 변경하지 않도록 한다.
3단계: 코드 수정
이제 newSuffix 변수는 람다 표현식 내에서 변경되지 않으므로, effectively final 조건을 만족한다. 코드는 다음과 같이 수정된다.
public void updateUsernames(List<User> users, String suffix) {
final String newSuffix = suffix.toUpperCase(); // 변경되지 않는 접미사
users.forEach(user -> {
String updatedUsername = user.getUsername() + newSuffix; // 업데이트된 username
user.setUsername(updatedUsername);
});
}
4단계: 최종 확인
코드 수정 후에는 이제 newSuffix가 effectively final이므로, 람다 표현식 내에서 안전하게 사용될 수 있다. 이렇게 함으로써 컴파일 에러는 발생하지 않을 것이다.
마무리
코드를 작성하다 보면 가끔 '이게 왜 안 되지?'라는 의문이 생기곤 한다. 그중 하나가 바로 '람다식에서 왜 final 변수만 사용해야 하는가'에 대한 부분이었다. 그동안은 간단한 코드로 해결이 되는 문제들은 인텔리제이의 에러 해결 추천 항목 따라 에러를 해결해 왔었다. 하지만 이러한 오류를 여러 번 경험하면서, 단순히 해결책을 적용하는 것을 넘어 그 이유를 제대로 이해하고 넘어갈 필요성을 느꼈다.
이번에 이 부분을 깊게 파고들면서, 람다식과 final 변수의 관계에 대한 높은 이해를 얻었다. 이 과정에서 깨달은 것은, 한 번 제대로 이해하고 나면 같은 에러를 만날 가능성이 적어진다는 것이다. 이처럼 개발을 하면서 궁금증이나 의문점에 부딪힐 때마다, 그냥 넘어가지 않고 멈춰서 그 문제를 해결하는 것이 중요하다고 생각한다.
'프로그래밍 언어 > JAVA' 카테고리의 다른 글
AtomicInteger를 활용한 파일 저장 순서 동기화 문제 해결 (0) | 2024.02.04 |
---|---|
[JAVA] 코드 최적화를 위한 매직 상수 사용법 (4) | 2023.12.31 |
[JAVA] 자바 인터페이스에서 public과 private 접근 제한자 활용하기 (3) | 2023.12.24 |
[JAVA] 리플렉션(Reflection)의 이해 (1) | 2023.11.08 |
[JAVA] Java와 Spring, JPA에서의 프록시 객체 이해하기 (1) | 2023.11.08 |