Java 공식 문서를 번역하며 학습한 글입니다.
✅ 람다 표현식
익명 클래스의 한 가지 문제점은, 인터페이스에 하나의 메서드만 포함되어 있는 등 구현이 매우 간단한 경우에 익명 클래스의 구문이 다소 복잡하고 이해하기 어려울 수 있다는 것입니다. 이러한 경우에는 주로 다른 메서드에 기능을 인자로 전달하려고 할 때(예: 버튼을 클릭했을 때 어떤 동작을 수행해야 하는지) 람다 표현식이 사용됩니다. 람다 표현식을 사용하면 기능을 메서드 인자로 취급하거나 코드를 데이터로 취급할 수 있습니다.
Java에서는 초기 버전에서는 일급 객체를 직접적으로 지원하지 않았습니다. 그러나 Java 8에서 람다식과 익명 클래스가 도입되면서 일급 객체의 개념을 구현할 수 있게 되었습니다. 람다식은 익명 함수를 표현하는 간단한 문법으로, 변수에 할당하고 함수의 인자로 전달하며, 함수의 리턴값으로 사용할 수 있습니다. 이를 통해 Java에서도 일급 객체의 요건을 충족하는 함수형 프로그래밍 스타일을 구현할 수 있습니다. 따라서 람다식과 익명 클래스를 사용하여 Java에서 일급 객체와 비슷한 기능을 달성할 수 있습니다.
✅ Approach Lambda
람다를 왜 사용하고, 어떻게 사용하는지에 대해서 단계별로 접근해보겠습니다. 익명 클래스와 람다 syntax에 대해서 기본적인 이해가 필요합니다.
public class Person {
public enum Sex {
MALE, FEMALE
}
String name;
LocalDate birthday;
Sex gender;
String emailAddress;
public int getAge() {
// ...
}
public void printPerson() {
// ...
}
}
소셜 네트워킹 애플리케이션의 회원은 List<Person> 인스턴스에 저장되어 있다고 가정합니다.
이 섹션은 이 사용 사례에 대한 초기 접근 방식부터 시작하여 로컬 및 익명 클래스를 사용한 개선 방법을 소개하고, 마지막으로 람다 표현식을 사용한 효율적이고 간결한 방법을 제시합니다. 이 섹션에서 설명하는 코드 조각은 RosterTest 예제에서 찾을 수 있습니다.
🔹Create Methods That Search for Members That Match One Characteristic
- 하나의 특성과 일치하는 멤버를 검색하는 메서드 만들기
여러 개의 메서드들을 만드는 방법으로 학습해 보겠습니다. 성별이나 나이와 같은 단일의 특징에 맞는 회원을 검색하는 여러 개의 메서드들을 만들어 보겠습니다. 다음 메서드는 특정 나이보다 나이가 많은 회원을 출력합니다.
public static void printPersonsOlderThan(List<Person> roster, int age) { for (Person p : roster) { if (p.getAge() >= age) { p.printPerson(); } } }
Note: A List is an ordered Collection. A collection is an object that groups multiple elements into a single unit. Collections are used to store, retrieve, manipulate, and communicate aggregate data. For more information about collections, see the Collections trail.
매개변수로 받은 age를 기준으로 List 안에 있는 Person 객체의 요소들에서 age를 추출해 비교합니다. 기준 나이보다 많으면 이름과 나이를 출력하는 간단한 로직입니다.
이 접근 방식은 애플리케이션을 취약하게 만들 수 있으며(데이터 형식과 같은 업데이트 도입으로 인해 작동하지 않을 가능성이 있음), 애플리케이션을 업그레이드하고 Person 클래스의 구조를 변경하는 경우에는 많은 API를 다시 작성해야 합니다.(강한 결합) 예를 들어, Person 클래스의 구조를 변경하여 다른 멤버 변수를 포함한다고 가정해 보겠습니다. 아마도 클래스는 다른 데이터 형식이나 알고리즘을 사용하여 나이를 기록하고 측정할 것입니다. 이러한 변경을 수용하기 위해 많은 API를 다시 작성해야 할 것입니다. 게다가, 이 접근 방식은 불필요하게 제한적입니다.
특정 나이보다 어린 회원을 출력하려는 경우는 어떻게 처리할까요? 이러한 문제를 해결하고 개선하기 위해 다음으로 설명할 로컬 및 익명 클래스를 사용하는 방법을 소개하겠습니다.
🔹Create More Generalized Search Methods
- 보다 일반화된 검색 메소드
다음 메소드는 이전에 작성한 printPersonsOlderThan보다 더 일반적인 메소드입니다. 지정된 연령 범위 내의 구성원을 인쇄합니다.
public static void printPersonsWithinAgeRange( List<Person> roster, int low, int high) { for (Person p : roster) { if (low <= p.getAge() && p.getAge() < high) { p.printPerson(); } } }
만약 특정 성별의 회원이나 특정 성별과 나이 범위의 조합을 출력하려는 경우는 어떻게 처리할까요? 또한, Person 클래스를 변경하여 관계 상태나 지리적 위치와 같은 다른 속성을 추가하기로 결정한 경우는 어떨까요? printPersonsOlderThan보다는 이 방법이 더 범용적이지만, 모든 가능한 검색 쿼리에 대해 별도의 메서드를 생성하려는 시도는 여전히 취약한 코드를 만들 수 있습니다.( 👉여기서 말하는 취약한 코드란, 결합력이 강해 재사용이 어려운 코드를 말합니다.) 대신 검색할 기준을 지정하는 코드를 별도의 클래스로 분리할 수 있습니다.
🔹Specify Search Criteria Code in a Local Class
- 검색 기준을 로컬 클래스로 분리하기
public static void printPersons( List<Person> roster, CheckPerson tester) { for (Person p : roster) { if (tester.test(p)) { p.printPerson(); } } }
이 메서드는 roster 파라미터에 포함된 각 Person 인스턴스가 CheckPerson 매개변수 tester에 지정된 검색 기준을 만족하는지 확인하기 위해 tester.test 메서드를 호출합니다. 만약 tester.test 메서드가 true 값을 반환하면 해당 Person 인스턴스에서 printPersons 메서드가 호출됩니다.
검색 기준을 지정하기 위해 CheckPerson 인터페이스와 이를 상속한CheckPersonEligibleForSelectiveSevice를 구현합니다.
interface CheckPerson { boolean test(Person p); }
class CheckPersonEligibleForSelectiveService implements CheckPerson { public boolean test(Person p) { return p.gender == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25; } }
CheckPersonEligibleForSelectiveSevice 클래스는 CheckPerson 인터페이스를 구현하여 test 메서드에 대한 구체적인 내용을 작성합니다. 이 메서드는 Selective Service에 적격한 회원을 필터링합니다. 즉, Person 매개변수가 남성이며 18세에서 25세 사이인 경우 true 값을 반환합니다.
이를 사용하면 다음과 같이 작성됩니다.
printPersons( roster, new CheckPersonEligibleForSelectiveService());
이 접근 방식은 이전 방식보다는 취약성이 줄었지만, 여전히 추가적인 코드가 필요합니다. 즉, 응용 프로그램에서 수행할 각 검색에 대해 새 인터페이스와 로컬 클래스가 필요합니다. CheckPersonEligibleForSelectiveService가 인터페이스를 구현하기 때문에, 각 검색에 대해 새로운 클래스를 선언하는 필요 없이 익명 클래스를 사용할 수 있습니다.
👉 검색 조건이 늘어날 수 수록 CheckPerson 인터페이스를 구현한 구체 클래스가 늘어나게 될텐데 이렇게 여러 클래스를 만드는 반복적인 작업을 줄이고 익명 클래스를 통해서 각 조건을 부여할 수 있도록 만들 수 있다는 말입니다.
🔹Specify Search Criteria Code in an Anonymous Class
- 익명 클래스에서 검색 조건 지정
printPersons( roster, new CheckPerson() { public boolean test(Person p) { return p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25; } } );
각 검색에 대해 새로운 클래스를 생성할 필요가 없기 때문에 필요한 코드 양을 줄일 수 있습니다. 그러나 CheckPerson 인터페이스에는 하나의 메서드만 있는데도 익명 클래스의 구문은 번잡하다는 단점이 있습니다. 이 경우, 다음 섹션에서 설명하는대로 익명 클래스 대신 람다 표현식을 사용할 수 있습니다. Lambda 표현식은 코드를 더 간결하게 만들어 줍니다.
🔹Specify Search Criteria Code with a Lambda Expression
CheckPerson 인터페이스는 함수형 인터페이스입니다. 함수형 인터페이스(functional interface)는 하나의 abstract method을 포함하는 인터페이스를 말합니다. (함수형 인터페이스는 하나 이상의 default method나 static method를 포함할 수도 있습니다.) 함수형 인터페이스는 하나의 추상 메서드만을 가지기 때문에, 해당 메서드의 이름을 구현할 때 생략할 수 있습니다. 이를 위해 익명 클래스 표현식 대신 람다 표현식을 사용합니다.
- 람다식으로 검색 조건 지정
- 익명 클래스로 작성한 메서드
printPersons( roster, new CheckPerson() { public boolean test(Person p) { return p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25; } } );
- 람다식으로 변경한 메서드
printPersons( roster, (Person p) -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25 );
추상메서드가 하나인 functional interface를 구현할 때 람다식을 사용할 수 있습니다. 하나의 메서드를 구현하기 때문에 메서드의 이름을 생략할 수 있습니다. 화살표 ( → ) 는 람다표현식임을 의미하며 ( ) 파라미터를 정의하고 화살표 ( → ) 를 사용하여 funtional interface의 추상 메소드의 구현부를 작성합니다.
이렇게 구현한 람다식은 first class object의 기능과 비슷한 역할을 할 수 있는 것입니다. 이제 printPersons() 메소드를 호출하는 Client를 살펴보겠습니다.
- 익명 클래스로 작성한 메서드
- 람다식 호출
public static void printPersons( List<Person> roster, CheckPerson tester) { for (Person p : roster) { if (tester.test(p)) { p.printPerson(); } } }
검색 기준을 로컬 클래스로 나누었던 과정의 코드와 호출부는 달라진게 없습니다. 다만 검색 조건에 따라 구체 클래스를 직접 작성하지 않아도 되는 간편함이 있습니다. 이는 메소드의 오버로딩을 통해서 구현부를 달리하여 다양한 검색 조건을 구현할 수 있게 됩니다.
🔹Use Standard Functional Interfaces with Lambda Expressions
- 표준 기능의 람다 표현식 사용
interface CheckPerson { boolean test(Person p); }
이전에 작성한 Functional Interface는 매우 간단한 인터페이스입니다. 하나의 추상 메서드만을 포함하고 있기 때문에 함수형 인터페이스입니다. 이 메서드는 하나의 매개변수를 받고 boolean 값을 반환합니다. 메서드가 너무 간단하여 애플리케이션에서 정의할 가치가 없을 수도 있습니다. 그 결과로 JDK에서는 여러 표준 함수형 인터페이스를 정의하였으며, 이는
java.util.function
패키지에서 찾을 수 있습니다.예를 들어, Predicate<T> 인터페이스를 CheckPerson 대신 사용할 수 있습니다.
interface Predicate<T> { boolean test(T t); }
Predicate<T> 인터페이스는 제네릭 인터페이스의 한 예입니다. 제네릭 타입(제네릭 인터페이스와 같은)은 각 꺾쇠 괄호(<>) 내에 하나 이상의 타입 파라미터를 지정합니다. 이 인터페이스는 하나의 타입 파라미터인 T만을 포함하고 있습니다. 실제 타입 아규먼트와 함께 제네릭 타입을 선언하거나 인스턴스화할 때, 파라미터화된 타입을 갖게 됩니다. 예를 들어, 파라미터화된 타입 Predicate<Person>은 다음과 같습니다.
interface Predicate<Person> { boolean test(Person t); }
👉 이를 Person에서 구현하면 다음과 같습니다. 구현부는 기존에 CheckPerson Functional Interface를 사용한 것과 동일합니다. 하지만 JDK가 기본적으로 제공해주는 Functional Interface를 사용한다는 것이 차이점입니다.
public static void printPersonsWithPredicate( List<Person> roster, Predicate<Person> tester) { for (Person p : roster) { if (tester.test(p)) { p.printPerson(); } } }
매개변수화된 타입은
CheckPerson.boolean test(Person p)
와 동일한 반환 타입과 매개변수를 가진 메서드를 포함하고 있습니다. 따라서 다음 메서드에서처럼Predicate<T>
를 CheckPerson 대신 사용할 수 있습니다. 이를 어떻게 사용하는지 호출부를 살펴보겠습니다.printPersonsWithPredicate( roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25 );
결과적으로, 다음 메서드 호출은 Selective Service에 적격한 회원을 얻기 위해 로컬 클래스에 검색 기준 코드를 지정한 방식(Approach 3)에서 printPersons를 호출한 것과 동일합니다
🔹Use Lambda Expressions Throughout Your Application
- 어플리케이션 전체의 람다 표현식 사용
public static void printPersonsWithPredicate( List<Person> roster, Predicate<Person> tester) { for (Person p : roster) { if (tester.test(p)) { p.printPerson(); } } }
이 메서드는 파라미터 roster에 포함된 각 Person 인스턴스가 파라미터 tester에서 지정한 조건을 만족하는지 확인합니다. 만약 Person 인스턴스가 tester가 지정한 조건을 만족한다면, 해당 Person 인스턴스에서 printPerson 메서드가 호출됩니다.
printPerson() 메서드를 호출하는 대신, tester가 지정한 조건을 만족하는 Person 인스턴스에 대해 수행할 다른 작업을 지정할 수 있습니다. printPerson()과 유사한 파라미터 하나(Person 타입의 객체)를 받아서 void를 반환하는 람다 표현식을 원한다고 가정해 보겠습니다. 람다 표현식을 사용하려면 함수형 인터페이스를 구현해야 합니다. 이 경우, 하나의 Person 타입 아규먼트를 받고 void를 반환하는 추상 메서드를 가진 함수형 인터페이스가 필요합니다. 이러한 특성을 갖는 Consumer<T> 인터페이스는 void accept(T t) 메서드를 포함하고 있습니다. 다음 메서드는 p.printPerson() 호출을 accept 메서드를 호출하는 Consumer<Person>의 인스턴스로 대체합니다.
👉 printPerson() 메서드는 파라미터를 하나만 받고, return 값이 void인 Functional Interface입니다. JDK에서 기본적으로 제공해주는 파라미터가 하나이고, return 값이 void인 Consumer<T>를 사용할 수 있음을 의미합니다.
- Consumer<Person> 사용
public static void processPersons( List<Person> roster, Predicate<Person> tester, Consumer<Person> block) { for (Person p : roster) { if (tester.test(p)) { block.accept(p); } } }
결과적으로, 다음 메서드 호출은 Selective Service에 적격한 회원을 얻기 위해 로컬 클래스에 검색 기준 코드를 지정한 방식(Approach 3)에서
printPersons
를 호출한 것과 동일합니다. 회원을 출력하기 위해 사용된 람다 표현식을 강조했습니다.이를 사용한 호출부를 살펴보겠습니다.
processPersons( roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25, p -> p.printPerson() );
processPersons() 메소드를 호출하는 곳을 보겠습니다. Person 클래스가 정의한 processPersons() 메소드는 List<Person> 타입의 List와, Predicate<Person>, Consumer<Person> 타입의 함수형 인터페이스를 매개변수로 받고 있습니다. 함수형 인터페이스의 추상메소드를 람다식으로 구현하여 메소드를 호출할 때 인수로 전달함으로써, 함수형 인터페이스의 추상메소드를 호출할 때 이를 구현한 람다식을 사용할 수 있게 되는 것입니다.
- Function<T,R> 함수형 인터페이스 사용
만약 회원의 프로필을 출력하는 것 이상의 작업을 수행하고 싶다면 어떻게 해야 할까요? 예를 들어, 회원 프로필을 유효성 검사하거나 연락처 정보를 검색하려는 경우가 있을 수 있습니다. 이 경우, 값을 반환하는 추상 메서드를 포함하는 함수형 인터페이스가 필요합니다. Function<T,R> 인터페이스는 T 타입의 인자를 받고 R 타입의 값을 반환하는 R apply(T t) 메서드를 포함하고 있습니다. 다음 메서드는 mapper 매개변수로 지정된 데이터를 검색하고, 그 후에 block 매개변수로 지정된 동작을 수행합니다
public static void processPersonsWithFunction( List<Person> roster, Predicate<Person> tester, Function<Person, String> mapper, Consumer<String> block) { for (Person p : roster) { if (tester.test(p)) { String data = mapper.apply(p); block.accept(data); } } }
다음 메서드는 roster에 포함된 각 적격 회원의 이메일 주소를 검색한 후 출력하는 역할을 합니다.
processPersonsWithFunction( roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25, p -> p.getEmailAddress(), email -> System.out.println(email) );
Interface Function<T, R> R apply(T t)
Funtction Interface는 위와 같이 정의가 되어있습니다. 람다식을 통해 이를 구현하면 아래와 같이 구현될 것입니다.
Interface Function<Person, String> String apply(Person p) { return p.getEmailAddress(); }
🔹Use Generics More Extensively
- 제네릭 메소드로 변경하기
public static <X, Y> void processElements( Iterable<X> source, Predicate<X> tester, Function <X, Y> mapper, Consumer<Y> block) { for (X p : source) { if (tester.test(p)) { Y data = mapper.apply(p); block.accept(data); } } }
- 호출
processElements( roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25, p -> p.getEmailAddress(), email -> System.out.println(email) );
메소드 호출부에서 제네릭을 생략했습니다. 이는 제네릭의 타입 추론 기능 덕분에 가능한 것입니다. 컴파일러는 파라미터화된 제네릭 인수를 보고 유추할 수 있습니다.
➡️ 이 메서드 호출은 다음 동작을 수행합니다.
- 컬렉션 source에서 객체의 소스를 얻습니다. 이 예제에서는 컬렉션 roster에서 Person 객체의 소스를 얻습니다. roster는 List 타입의 컬렉션이지만, Iterable 타입의 객체입니다.
- Predicate 객체 tester와 일치하는 객체를 필터링합니다. 이 예제에서 Predicate 객체는 Selective Service에 적격한 회원을 지정하는 람다 표현식입니다.
- Function 객체 mapper에 의해 각 필터링된 객체를 값으로 매핑합니다. 이 예제에서 Function 객체는 회원의 이메일 주소를 반환하는 람다 표현식입니다.
- Consumer 객체 block에 지정된 동작을 각 매핑된 객체에 대해 수행합니다. 이 예제에서 Consumer 객체는 Function 객체가 반환하는 문자열인 이메일 주소를 출력하는 람다 표현식입니다.
🏷️이미지 출처 및 참고한 사이트
Uploaded by N2T