728x90
반응형





본 글은 모던자바인액션 책을 참고하여 작성한 글입니다.
👉🏻이전글에 이어 본 글에서는 람다표현식이 함수형 인터페이스에서 어떻게 활용되는지 알아보고,
람다표현식의 참조 기법과 디폴트 메서드에 대해 알아보겠습니다.

1. 표준 함수형 인터페이스에서의 람다표현식 사용

표준 함수형 인터페이스

Java8부터는 표준 함수형 인터페이스를 제공합니다.
매번 함수형 인터페이스를 정의하여 사용하는 번거로운 작업을 최소화시키기 위해서죠.
아래 사진은 대표적인 표준 함수형 인터페이스를 나타냅니다.




대표 case

위 사진 속 함수형 인터페이스 중 세 가지 정도만 알아보겠습니다.

Predicate

Predicate 인터페이스는 test라는 추상메서드를 활용하여 제네릭 형식 T객체를 인수로 받아 boolean을 반환하는 함수형 인터페이스입니다.
주로 특정 조건에 관한 판단기준을 정의할 때 활용됩니다.

  • Predicate 예제
@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}
public <T> List<T> filter(List<T> list, Predicate<T> p) {
    List<T> results = new ArrayList<>();
    for (T t : list) {
        if (p.test(t)) {
            results.add(t);
        }
    }
    return results;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);



Consumer

Consumer 인터페이스는 accept 라는 추상메서드를 활용하여 제네릭 형식 T객체를 인수로 받아 void를 반환하는 함수형 인터페이스입니다.
특정 동작을 인수로 받아서 실행시키고 싶을 때 주로 활용됩니다.

  • Consumer 예제
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

public <T> void forEach(List<T> list, Consumer<T> c) {
    for (T t : list) {
        c.accept(t);
    }
}
forEach( Arrays.asList(1, 2, 3, 4, 5), (Interger i) -> System.out.println(i) );



Function

Function 인터페이스는 apply 라는 추상메서드를 활용하여 제네릭 형식 T객체를 인수로 받아 제네릭 형식 R객체를 반환하는 함수형 인터페이스입니다.
특정 동작을 인수로 받아서 실행시킨 후에 반환해야 하는 값이 있을 때 주로 활용됩니다.

  • Function 예제
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

public <T, R> List<R> map(List<T> list, Function<T, R> f) {
    List<R> result = new ArrayList<>();
    for (T t : list) {
        result.add(f.apply(t));
    }
    return result;
}

// [7, 2, 6]
List<Integer> l = map(
    Arrays.asList("lambdas", "in", "action"),
    (String s) -> s.length()
);




이 밖에도 ComparatorSupplier와 같은 함수형 인터페이스들이 있고,
Function 인터페이스에서 두 개의 인수를 받고싶다면, BiFunction 인터페이스를 활용할 수 있는 것처럼
다양한 표준 함수형 인터페이스들이 있습니다.


기본형 특화 모델

함수형 인터페이스는 제네릭 형식을 활용하고 있어서 기본형 타입을 사용하는 데 제약이 있습니다.
그래서 기본형 타입도 활용할 수 있는 기본형 특화 함수형 인터페이스가 있습니다.
해당 인터페이스는 제네릭 형식이 아닌 타입을 명시하고 있어서 기본형 타입을 활용할 수가 있습니다.

  • 오토박싱 예제
List<Integer> list = new ArrayList<>();
for (int i = 300; i < 400; i++) {
    list.add(i);
}




물론 위 코드와 같이 기본형 타입을 제네릭 활용 함수형 인터페이스의 추상 메서드의 인수로 줘도 실행이 됩니다.
왜냐하면 기본형 타입이 참조형 타입으로 변환되는 오토박싱기능이 제공되기 때문입니다.

  • 박싱 : 기본형을 참조형으로 변환하는 기능
  • 언박싱 : 참조형을 기본형으로 변환하는 기능
  • 오토박싱 : 박싱과 언박싱이 자동으로 이루어지는 기능




하지만 이러한 변환 과정은 메모리 공간을 추가로 소비하고, 기본형 타입을 가져오기 위해 메모리를 탐색하는 추가적인 과정이 생기기 때문에
비용이 소모됩니다. 그렇기 때문에 비용이 소모되는 오토박싱동작을 피할 수 있도록 기본형 특화 함수형 인터페이스가 제공됩니다.

  • 제네릭 활용 함수형 인터페이스
public interface Predicate<T> {
    boolean test(T t);
}
Predicate<Integer> oddNumbers = (Integer i) -> i % 2 != 0;
oddNumbers.test(100);



  • 기본형 특화 함수형 인터페이스
public interface IntPredicate {
    boolean test(int i);
}
IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000);



함수형 인터페이스 람다 매핑 예제

  • 람다 예제 : (List<String> list) -> list.isEmpty(), 함수형 인터페이스 : Predicate<List<String>>
  • 람다 예제 : () -> new Apple(10), 함수형 인터페이스 : Supplier<Apple>
  • 람다 예제 : (Apple a) -> System.out.println(a.getWeight()), 함수형 인터페이스 : Consumer<Apple>
  • 람다 예제 : (String s) -> s.length(), 함수형 인터페이스 : Function<String, Integer> or ToIntFunction<String>
  • 람다 예제 : (int a, int b) -> a * b, 함수형 인터페이스 : IntBinaryOperator
  • 람다 예제 : (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()), 함수형 인터페이스 : Comparator<Apple> or BiFunction<Apple, Apple, Integer> or ToIntBiFunction<Apple, Apple>



2. 람다 표현식 컴파일 과정

람다 표현식의 컴파일 과정을 통해 람다 표현식이 어떻게 함수형 인터페이스에 매핑이 되는지 알 수 있습니다.

  • 대상 형식 : 어떤 컨텍스트(ex. 람다 표현식이 파라미터로 입력되는 메서드 등)에서 기대되는 람다 표현식의 형식



형식 검사

람다 표현식은 형식 검사라는 컴파일 과정을 거칩니다.
특정 메서드에 람다표현식을 입력했을 때, 해당 메서드에서 파라미터로 선언한 함수형 인터페이스와 람다표현식이 호환되는지 확인하는 과정이라고 볼 수 있습니다. 예제를 보며 형식 검사가 어떻게 이루어지는지 확인해보겠습니다.



  • 예제
List<Apple> heavierThan150g = filter(inventory, (Apple apple) -> apple.getWeight() > 150);



  • 형식 검사 과정
  1. filter 메서드의 선언을 확인합니다.
  2. filter 메서드는 두 번째 파라미터로 Predicate<Apple> 형식(대상 형식)을 기대합니다.
  3. Predicate<Apple>은 test라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스입니다.
  4. test 메서드는 Apple을 받아 boolean을 반환하는 함수 디스크립터를 묘사합니다.
  5. filter 메서드로 전달된 인수는 이와 같은 요구사항을 만족해야 합니다.




컴파일러가 위 형식 검사 과정 중 예외를 던지지 않는다면, 형식 검사를 마치고 다음 단계로 넘어가게 됩니다.

형식 추론

형식 추론을 통해 우리는 람다표현식에서의 코드를 더욱 단순화할 수 있습니다.
형식 검사 과정을 통해 우리는 파라미터로 입력된 람다표현식과 관련된 함수형 인터페이스를 추론할 수 있고,
해당 함수디스크립터를 알 수 있으므로 컴파일러가 람다의 시그니처도 추론할 수 있습니다.

즉, 람다 표현식에서 파라미터 형식을 명시하지 않아도 컴파일러가 해당 형식을 추론하여 컴파일을 진행할 수 있습니다.

Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

위 코드의 두 표현은 같은 표현입니다.

제약

람다 표현식을 사용하는 데 있어서 변수 사용에 제약이 있습니다.

지역변수의 제약

  • 자유변수 : 람다의 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수
  • 람다캡처링 : 람다에서 자유변수를 활용하는 것

람다에서는 인스턴스 변수와 정적 변수를 자유롭게 캡처하여 사용할 수 있지만, 해당 변수는 명시적으로 final로 선언되어야 하거나 실직적으로 final처럼 사용되어야 합니다. 아래 예제는 컴파일 과정에서 에러를 반환하는 코드입니다.

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;

portNumber 자유변수의 람다 캡처링 및 활용 후, 해당 변수에 값을 새로 할당하고 있습니다. final처럼 사용해야하는 규칙을 위반했으므로 에러를 반환하게 됩니다.

제약이 필요한 이유

내부적으로 인스턴스 변수와 지역 변수가 있는데, 인스턴스 변수는 힙에 저장되는 반면 지역 변수는 스택에 위치합니다.
그리고 이 때, 람다에서 지역 변수에 직접 접근할 수 있다고 가정한다면(원래는 접근에 제약을 걸어둠), 람다가 다른 스레드에서 실행되고 지역 변수를 선언한 스레드가 사라졌을 때 람다를 실행하는 다른 스레드에서 지역 변수에 다시 접근하려고 할 때 에러가 발생할 여지가 있습니다.




그렇기 때문에 람다에서 지역변수 직접 접근에 제약을 걸었고, 람다에서 지역 변수를 사용해야 한다면 람다캡처링 과정을 통해 해당 지역 변수의 복사본을 사용하게 됩니다. 즉, 해당 지역변수의 값은 람다에서 활용하기 때문에 바뀌면 안되고, final로 선언하거나 final처럼 값이 한 번만 할당될 수 있도록 사용해야 하는 것입니다.



3. 메서드 참조

메서드 참조란?

메서드 참조는 기존에 정의된 메서드를 람다처럼 전달할 수 있는 기법입니다.
메서드명 앞에 구분자 ::를 붙이는 방식으로 활용할 수 있습니다.
Apple::getWeight
위 메서드 참조는 람다 표현식 (Apple a) -> a.getWeight()를 축약한 표현입니다.



  • 기존코드
inventory.sort(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
  • 메서드 참조 활용 (java.util.)
inventory.sort(comparing(Apple::getWeight));

이렇게 메서드 참조를 활용하면 코드의 가독성을 높일 수 있습니다.



메서드 참조 생성 case

메서드 참조를 활용하는 case는 대표적으로 아래 3가지 case가 있습니다.

정적 메서드 참조

정적 메서드 참조는 말그대로 정적메서드를 참조하는 case입니다.
Integer의 정적메서드인 parseInt는 아래와 같이 표현할 수 있습니다.
Integer::parseInt

다양한 형식의 인스턴스 메서드 참조

정적 메서드가 아닌 특정 형식의 인스턴스 메서드를 참조하는 case입니다.
String의 인스턴스 메서드인 length는 아래와 같이 표현할 수 있습니다.
String::length

  • 활용 예제
(String s) -> s.toUpperCase();
String::toUpperCase




위 람다표현식을 아래 메서드 참조 표현으로 바꿔서 활용할 수 있습니다.

기존 객체의 인스턴스 메서드 참조

정적 메서드가 아니고 람다 파라미터도 아닌 지역 변수의 인스턴스 메서드를 참조하는 case입니다.
Transaction을 할당받은 람다의 외부객체 expensiveTransaction의 인스턴스 메서드인 getValue는 아래와 같이 표현할 수 있습니다.
expensiveTransaction::getValue

  • 활용 예제
() -> expensiveTransaction.getValue();
expensiveTransaction::getValue

위 람다표현식을 아래 메서드 참조 표현으로 바꿔서 활용할 수 있습니다.

메서드 참조 예제

메서드 참조의 예시를 더 알아보겠습니다.
아래 예제 코드에서 sort 메서드는 인수로 Comparator를 기대하고 있고, 함수 디스크립터로 (T, T) -> int를 갖습니다.

List<String> str = Arrays.asList("a", "b", "A", "B");
str.sort((s1, s2) -> s1.compareToIgnoreCase(s2));




아래와 같이 메서드 참조 표현을 활용하면 위 코드와 같이 Comparator를 새로 구현하지 않고 기존 메서드를 참조시킬 수 있습니다.

List<String> str = Arrays.asList("a", "b", "A", "B");
str.sort(String::compareToIgnoreCase);

String의 인스턴스 메서드 compareToIgnoreCase는 시그니처로 (T, T) -> int를 가지기 때문에 인수로 기대하는 Comparator와 호환됩니다.

4. 생성자 참조

정적 메서드 참조를 만드는 것처럼 new 키워드를 활용하여 생성자 참조를 만들 수 있습니다.
ClassName::new

아래 예제는 () -> T 시그니처를 갖는 Supplier를 활용한 생성자 참조 예제입니다.

  • 기존 코드
Supplier<Apple> c1 = () -> new Apple();
Apple a1 = c1.get();
  • 생성자 참조 활용
Supplier<Apple> c1 = Apple::new; // Apple() 생성자 참조
Apple a1 = c1.get();




아래 예제는 (T) -> T 시그니처를 갖는 Function을 활용한 생성자 참조 예제입니다.

  • 기존 코드
Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
Apple a2 = c2.apply(110);
  • 생성자 참조 활용
Function<Integer, Apple> c2 = Apple::new; // Apple(Integer weight) 생성자 참조
Apple a2 = c2.apply(110);




최종 예제로 아래와 같이 사용할 수 있습니다.

List<Integer> weights = Arrays.asList(7, 3, 4, 10);
List<Apple> apples = map(weights, Apple::new);
public List<Apple> map(List<Integer> list, Function<Integer, Apple> f) {
    List<Apple> result = new ArrayList<>();
    for (Integer i : list) {
        result.add(f.apply(i));
    }
    return result;
}



5. 람다/메서드참조 활용 예제

사과 리스트를 다양한 정렬 기법으로 정렬하는 예제로 람다활용과 메서드참조를 최종적으로 정리해보겠습니다.

코드 전달

정렬 메서드 sort에 정렬기법을 아래 코드와 같이 전달합니다.
동작 파라미터화된 sort메서드는 파라미터로 Comparator를 기대하고 있기 때문에 Comparator를 재정의한 AppleComparator를 인수로 전달할 수 있습니다.

public class AppleComparator implements Comparator<Apple> {
    public int compare(Apple a1, Apple a2) {
        return a1.getWeight().compareTo(a2.getWeight());
    }
}
inventory.sort(new AppleComparator());



익명 클래스 사용

코드를 구현하지 않고 직접 파라미터에 구현코드를 전달하는 방식인 익명 클래스 활용 방식을 이용할 수 있습니다.

inventory.sort(new Comparator<Apple>() {
    public int compare(Apple a1, Apple a2) {
        return a1.getWeight().compareTo(a2.getWeight());
    }
});



람다 표현식 사용

지금까지 우리는 람다표현식이 어떻게 함수형 인터페이스에 매핑되는지 알아봤습니다.

람다 표현식을 활용하여 익명 클래스를 사용한 장황한 코드를 간단하게 정리할 수 있습니다.
람다표현식은 함수형 인터페이스를 기대하는 어느 메서드에서나 사용될 수 있습니다.

inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));




자바 컴파일러는 사용된 콘텍스트를 통해 람다의 파라미터 형식도 추론할 수 있습니다. 그리고 아래와 같이 더 간단하게 정리할 수 있습니다.

inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));




ComparatorFunction을 인수로 받아 Comparator 객체로 반환하는 comparing이라는 정적 메서드(디폴트 메서드라고도 불림)를 포함합니다. 디폴트 메서드는 아래에서 짚고 넘어가겠습니다.

Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());
위 표현을 활용하여 아래와 같이 코드를 더 간소화 시킬 수 있습니다.


import static java.util.Comparator.comparing;
inventory.sort(comparing(apple -> apple.getWeight()));



메서드 참조 사용

메서드 참조를 사용하면 람다 표현식을 더 깔끔하게 전달할 수 있습니다.
(apple) -> apple.getWeight(); 람다 표현식은 Apple::getWeight로 표현할 수 있습니다.

inventory.sort(comparing(Apple::getWeight));



6. 람다 표현식 조합을 위한 디폴트 메서드

함수형 인터페이스의 정적메서드(디폴트 메서드)를 활용하면 여러개의 람다 표현식을 조합하여 복잡한 람다 표현식을 만들 수 있습니다.
해당 메서드의 예제로 위에서 언급한 Comparator.comparing메서드가 있습니다.

디폴트 메서드란?

디폴트 메서드는 인터페이스를 구현한 클래스에서 구현하지 않아도 되는 메서드를 말합니다.
디폴트 메서드는 아래 코드와 같이 활용됩니다. 아래 코드는 실제 Comparator 선언문의 일부를 가져온 코드입니다.

@FunctionalInterface
public interface Comparator<T> {

    // 유일 추상 메서드
    int compare(T o1, T o2);

    // 최상위 Object 클래스의 추상메서드 (유일 추상 메서드의 함수형 인터페이스 규칙을 위반하지 않음)
    boolean equals(Object obj);

    // 디폴트 메서드
    default Comparator<T> reversed() {
        return Collections.reverseOrder(this);
    }

}




디폴트 메서드는 선언문 앞에 java8에서 새로 추가된 default 키워드가 붙은 메서드입니다.

등장배경

java8 등장이전에는 인터페이스에 메서드가 추가될 때마다 해당 인터페이스를 구현한 클래스에서 해당 메서드를 모두 구현해야 하는 불편함이 있었습니다.
이러한 불편함을 보완하기 위해 java8에서는 구현 클래스에서 구현하지 않아도 되는 디폴트 메서드를 만들어 인터페이스를 쉽게 바꿀 수 있도록 하였습니다.

하지만 java의 클래스는 여러 인터페이스를 구현할 수 있고, 여러 인터페이스에 다중 디폴트 메서드가 있다면 다중 상속을 어느정도 허용한다는 의미라고 볼 수도 있습니다. 원래는 인터페이스에서 메서드를 구현한 일이 없기 때문에 어떤 메서드가 어떤 인터페이스의 메서드인지 알 필요가 없어서 다중상속 문제는 java에서 논외의 문제였는데, 추상메서드가 등장하고 인터페이스에서 메서드가 구현될 수 있게되어 다중상속의 대표적인 문제인 다이아몬드 상속 문제가 발생할 수 있습니다. 해당 문제를 보완하는 방법은 다음 글에서 다뤄보도록 하겠습니다.

  • 다이아몬드 상속 문제 : 동일한 메서드 시그니처(함수명 동일, 파라미터 동일, 반환 타입 동일)의 메서드를 가지는 여러 인터페이스를 상속 받은 클래스에서 어떤 인터페이스의 메서드를 실행해야할 지 결정하지 못하는 문제

람다 표현식 조합 예제

  • Comparator.comparing

comparing 메서드를 활용하면 Comparator 함수형 인터페이스를 람다 표현식으로 일일히 작성하는 것이 아닌
비교할 키를 추출하는 Fucntion을 인수로 받아서 Comparator를 반환할 수 있습니다.

Comparator<Apple> c1 = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
Comparator<Apple> c2 = Comparator.comparing(Apple::getWeight);



  • Comparator.reverse, Comparator.thenComparing

reverse 메서드를 활용하면, 주어진 비교자의 순서를 뒤 바꾸는 역정렬을 수행할 수 있고
thenComparing 메서드를 활용하면, 두 번째 비교자를 추가할 수 있습니다.

아래 코드는 첫 번째 비교자인 무게를 기준으로 사과를 내림차순하고, 동일 무게의 사과일 때는 원산지(Country)순으로 오름차순 정렬하는 코드입니다.

inventory.sort(comparing(Apple::getWeight)
            .reverse()
            .thenComparing(Apple::getCountry));



  • Predicate.negate, Predicate.and, Predicate.or

negate 메서드를 활용하면, 기존 Predicate 객체의 결과를 반전시키는 결과를 반환합니다.
and 메서드를 활용하면, and 조건으로 여러 Predicate를 조합할 수 있고,
or 메서드를 활용하면, or 조건으로 여러 Predicate를 조합할 수 있습니다.


Predicate<Apple> notRedApple = redApple.negate(); // redApple 프레디케이트의 결과를 반전 시킴

Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150); // 빨갛고 무게가 150 초과하는 사과

Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(apple -> apple.getWeight() > 150)
                                                    .or(apple -> GREEN.equals(apple.getColor())); // 빨갛고 무게가 150을 초과하거나 초록색인 사과



Reference

반응형
728x90
반응형

본 글은 모던자바인액션 책을 참고하여 작성한 글입니다.

1. 람다 표현식

(Apple apple) -> RED.equals(apple.getColor())
👉🏻이전글에서 언급한 것처럼
람다표현식은 특정 동작을 파라미터로 전달할 때, 코드의 복잡성을 간소화하기 위해 Java8에서 도입된 기술입니다.

정의

람다 표현식은 메서드로 전달할 수 있는 익명함수를 단순화한 것이라고 볼 수 있습니다.

// 인터페이스 선언
public interface ApplePredicate {
    boolean test (Apple apple);
}

// 전달받을 메서드 선언
public static List<Apple> filterApple(List<Apple> inventory, ApplePredicate p) {
    List<Apple> result = new ArrayList<>()
    for (Apple apple : inventory) {
        if ( p.test(apple) ) {
            result.add(apple);
        }
    }
    return result;
}

// 람다 표현식을 파라미터로 전달
List<Apple> result = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));



특징

  • 익명 : 보통의 메서드와는 달리 이름이 없어서 가독성에 관한 걱정거리를 줄일 수 있습니다.
  • 함수 : 특정 클래스에 종속되지 않는다는 점에서 함수라고 부를 수 있습니다. 하지만 파라미터 및 반환형식은 맞춰줘야 합니다.
  • 전달 : 메서드의 인수로 전달하거나 변수로 저장할 수 있습니다.
  • 간결성 : 자질구레한 코드를 구현할 필요가 없습니다. 가장 핵심이라고 할 수 있는 특징입니다.

람다 문법

(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

  • (Apple a1, Apple a2) : 파라미터 리스트
  • -> : 화살표, 파라미터 리스트와 바디를 구분
  • a1.getWeight().compareTo(a2.getWeight()); : 람다 바디, 실제 구현 및 반환이 이루어지는 부분

스타일

  • 표현식 스타일 : (parameters) -> expression
  • 블록 스타일 : (parameters) -> { statements; }

활용

👉🏻이전글과 위 예시코드에서 언급한 것처럼
람다표현식을 함수형 인터페이스와 관련 메서드를 직접 선언한 후, 선언된 메서드에 전달하는 방식으로도 활용할 수 있지만,
Java8의 java.util.function패키지에서 선언된 함수형 인터페이스(ex. Comparator)를 사용할 때에도 활용될 수 있습니다.

그리고 해당 패키지의 함수형 인터페이스는 Java8의 다양한 라이브러리(Collection의 List)에서 사용되고 있는데,
먼저 함수형 인터페이스가 뭔지에 대해서 짚고 넘어가겠습니다.

2. 함수형 인터페이스



정의

함수형 인터페이스는 추상 메서드가 오직 하나인 인터페이스입니다.

public interface Predicate<T> {
    boolean test(T t);
}




함수형 인터페이스에는 아래와 같이 추상 메서드 이외에 디폴트 메서드가 선언될 수 있는데,
디폴트 메서드에 대해서는 (다음글)에서 짚고 넘어가겠습니다.

@FunctionalInterface
public interface Comparator<T> {

    // 추상 메서드 선언
    int compare(T o1, T o2);

    // 추상 메서드2 선언 (equals는 최상위 클래스인 Object의 메서드이므로 추상 메서드 Count에서 제외된다.)
    boolean equals(Object obj);

    // 디폴트 메서드 선언
    default Comparator<T> reversed() {
        return Collections.reverseOrder(this);
    }

}




함수형 인터페이스@FunctionalInterface 어노테이션을 태깅할 수 있는데,
해당 어노테이션이 태깅된 인터페이스가 컴파일 과정에서 함수형 인터페이스라고 판단되지 않는 경우,
에러를 반환합니다.

Why? 함수형 인터페이스

함수형 인터페이스는 동작 파라미터화 기능을 사용할 때 사용될 수 있습니다.

👉🏻이전글에서 언급했던 것처럼
특정 동작을 메서드의 파라미터로 전달할 때(동작 파라미터화 기능), 익명클래스를 전달하는 방식이 있고 람다표현식을 전달하는 방식이 있습니다.
그 중 람다표현식을 전달하는 방식은 사실 함수형 인터페이스를 활용한다고도 볼 수 있는데, 이에 대해서는 람다 표현식의 컴파일 과정을 보면 알 수 있습니다. 해당 부분은 (다음글)에서 짚고 넘어가겠습니다.

3. 람다의 활용



시그니처와 함수 디스크립터



메서드 시그니처란?

Java에서 메서드 시그니처는 메서드의 이름과 파라미터의 순서 및 타입, 갯수를 의미합니다.
아래 예시처럼 메서드 시그니처를 표현할 수 있습니다.

  • 메서드
public int Multiply(int a, int b) {
    return a * b;
}



  • 시그니처 : Multiply(int a, int b)

람다표현식 시그니처

람다표현식 또한 위와 같은 시그니처를 가질 수 있습니다. 람다표현식의 시그니처는 아래와 같이 표현됩니다.
(Apple, Apple) -> int

위의 시그니처는 두 개의 Apple을 인수로 받아 int를 반환하는 람다표현식을 가리키는 시그니처입니다.
이러한 시그니처 표현을 살펴보는 이유는 람다표현식과 함수형 인터페이스의 시그니처가 일치하는지 판단할 수 있어야하기 때문입니다.

함수 디스크립터란?

함수 디스크립터는 위에서 언급했던 함수형 인터페이스의 시그니처입니다.
앞으로 함수형 인터페이스의 시그니처는 함수 디스크립터로 표현할 것입니다.

예시

  • Ex1)
// 함수형 인터페이스 파라미터를 입력으로 받는 메서드 선언
public void execute(Runnable r) {
    r.run();
}

// 람다표현식 입력
execute(() -> {});




위 함수형 인터페이스 파라미터의 추상메서드 run의 시그니처, 함수 디스크립터는 () -> void이고,
입력한 람다표현식의 시그니처도 () -> void로 일치하므로 유효한 문법입니다.

  • Ex2)
// 함수형 인터페이스를 반환하는 메서드 선언
public Callable<String> fetch() {
    return () -> "Tricky example"; // 람다표현식 반환
}




위 메서드에서 반환되는 함수형 인터페이스 Callable의 추상메서드 call의 함수 디스크립터는 () -> String이고,
return 문에서 반환한 람다표현식의 시그니처도 () -> String으로 일치하므로 유효한 문법입니다.

실행 어라운드 패턴

람다표현식을 실질적으로 활용하는 패턴 중 대표적인 패턴으로 실행 어라운드 패턴이 있습니다.
실행 어라운드 패턴은 실제 처리되는 작업 이외에 해당 작업을 진행하기 위한 준비단계, 작업이 끝난 후의 마무리 단계를 공통 모듈로 사전 구현한 후에
실제 진행되는 작업만 파라미터로 입력받아 실행시키는 패턴입니다.

예를 들면 파일 처리 작업이 있는데, 해당 작업은 파일이라는 자원을 열고 처리한 후에 자원을 닫는 순서로 이루어집니다.
여기서 자원을 열고 닫는 과정은 공통적으로 반복되는 작업이고, 이 작업들로 실제 처리하는 작업을 둘러싸는 형태의 패턴을 구성할 수 있습니다.
그리고 실제 처리하는 작업은 동작 파라미터화 기능을 통해 패턴의 파라미터로 입력하여 처리할 수 있습니다.

  • 실행 어라운드 패턴 예제
// 함수형 인터페이스 선언
public interface BufferedReaderProcessor {
    String process(BufferedReader b) throws IOException;
}

// 실행 어라운드 패턴 적용 메서드 선언
public String processFile(BufferedReaderProcessor p) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
        return p.process(br);
    }
}

// 파일 처리 람다함수 입력
String oneLine = processFile((BufferedReader br) -> br.readLine()); // 한 줄 반환
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine()); // 두 줄 반환



이어서

다음 글에서 람다표현식이 함수형 인터페이스에 어떻게 활용되는지 좀 더 자세히 알아보고,
메서드/생성자 참조 기법과 디폴트 메서드에 대해 알아보겠습니다.

반응형
728x90
반응형

본 글은 "MSA 이해와 구현"이라는 사내 강의 수강 후,
여러 MSA 관련 글을 참고하여 작성한 글입니다.

1. MSA란?

MSA는 Micro Services Architecture의 약자로 이름에서 유추할 수 있듯이 하나의 서비스를 Micro 단위로 쪼개서 관리하는 아키텍처를 의미합니다.

커머스 서비스를 예로 들면, 커머스라는 도메인 아래 하나의 서비스로 개발되고 운영되고 있었던 모놀리스(Monolith)형태가 아닌
주문, 정상, 회원, 상품 등 커머스 도메인 아래 여러 서비스로 추가 분리 및 개발하여 분리한 각 서비스를 각각의 서버에 배포하여 관리하는 MicroServices형태가 있을 수 있습니다.

위 사진은 가트너에서 제시한 표준 Micro Services Architecture 입니다.
MSA로의 구현 및 전환을 시도하는 많은 기업들에서 위 사진에서 제시된 표준 아키텍처를 따르고 있다고 합니다.
해당 아키텍처에 대해서는 다음글에서 다뤄보도록 하겠습니다.

2. Why? MSA

한눈에 보기에도 복잡한 이 MSA를 저희가 왜 써야할까요?
MSA를 쓰는 궁극적인 이유는 Loose Coupling, 느슨한 결합 특성때문이라고 볼 수 있습니다.
즉, 하나의 통합된 형태로 관리되던 서비스(ex. 커머스)를 여러 나눌 수 있는 최소한의 단위인 micro 단위의 서비스(ex. 주문, 정산, 회원 등)로 나눔으로써 각 서비스 간의 영향을 최소화할 수 있고(낮은 의존성), 이로 인해 모놀리스의 High Cohesion, 높은 응집력 특성으로 야기되는 다양한 문제(ex. 기능 단위 장애의 전체 확산 등)를 보완할 수 있게됩니다.

MSA의 이러한 Loose Coupling 특성은 다양한 기대효과를 낳을 수 있습니다.

장점

장애의 확산 차단

특정 Micro 서비스에서 장애가 발생하면, 전체 서비스로 확산되지 않습니다.

기존 모놀리스 구조에서는 특정 기능의 장애가 발생하면 전체 서비스가 마비되는 일이 흔했는데,
MSA에서는 Micro단위로 서비스를 세분화하여 각 기능을 독립시켰기 때문에
하나의 Micro 서비스가 타 서비스에 영향을 주지 않아 장애의 전체 확산을 막을 수 있습니다.

각 서비스의 개별 배포

각 Micro 서비스별로 개별 배포가 가능합니다.

기존 모놀리스 구조에서는 특정 기능에 대한 변경사항이 적용되려면, 서비스가 하나의 서버로 관리되었기 때문에 전체 서비스 배포가 불가피하였습니다.
MSA에서는 이러한 서비스를 여러 Micro 서비스로 나누어 별도의 서버로 각각 배포하는 구조이기 때문에
각 Micro 서비스별로 개별 배포가 가능합니다.

신속한 기능 수정 및 요구사항 반영

긴급하게 특정 기능에 대한 요구사항이 발생했을 때, 신속하게 반영할 수 있습니다.

위에서 언급했던 것처럼 MSA는 Micro 서비스 단위로 개별 서버에 배포되기 때문에, 서버의 크기가 비교적 작습니다.
이로 인해 배포 시간이 줄어들고 기능 수정이 신속하게 진행될 수 있습니다.

용이한 유지보수

용이한 유지보수가 가능합니다.

특정 서비스에 대한 장애나 이상징후 발생 시, 해당하는 서비스만 점검하면되기 때문에 대처할 영역이 좁아지므로
유지보수가 용이합니다.

Loose Coupling과는 별개로 다양한 기술 수용 등과 같은 장점이 존재합니다.

하지만 MSA의 Loose Coupling, 느슨한 결합 특성으로 인한 단점도 존재합니다.

단점

높은 복잡성

MSA를 활용하면 복잡성이 커집니다.

기존 모놀리스 기반 시스템은 하나의 서비스를 하나의 UI, 하나의 서버, 하나의 DB로 개발하고 관리해왔기 때문에,
타 모듈간 연동도 간단한 함수 호출로 이루어졌고, 데이터 동기화에 관한 부분도 단일 트랜잭션 처리로 간단하게 관리될 수 있었습니다.

하지만 MSA로 구현이 되는 서비스는 Micro 단위의 서비스마다 별개의 서버와 DB로 관리되고 있고,
이로 인해 타 모듈간 연동에 Rest나 gRPC 등과 같은 기술이 활용되어야 하고
데이터 동기화에 있어서도 분산 트랜잭션 환경의 메세지큐 기술이 도입되어 활용되어야하기 때문에,
개발 및 운영에 대한 복잡성이 커지게 되었습니다.

통신 비용 증가

MSA를 활용하면 통신비용 또한 증가됩니다.

여기서 말하는 통신비용은 latency에 따른 통신효율을 의미합니다.
위에서 언급한 것처럼 모놀리스 방식에서는 단순 메소드/함수 호출로 끝나는 연동이
MSA에서는 Rest나 gRPC와 같은 통신 기술이 활용되어야 합니다.
이로 인해 서비스 성능상의 비효율이 발생할 수 있습니다.

데이터 동기화의 어려움

MSA 기반 환경에서는 데이터 동기화를 위한 다양한 기술이 도입되어야 합니다.

서비스의 데이터가 하나의 DB로 관리되는 것이 아닌 Micro 단위의 서비스로 분리되어 각각 별도의 DB로 관리되는 형태이기 때문에,
데이터 일관성 또한 MSA환경에서 주요하게 다뤄야할 이슈라고 볼 수 있습니다.
이를 위한 기술도입 사례로 kafka 메세지큐 기술 도입 사례가 있는데,
특정 Micro 서비스에서 데이터 변경이 발생하여 kafka 메세지 큐에 이벤트를 발행하면,
kafka의 해당 이벤트를 구독하고 있는 Micro 서비스들이 이에 맞게 데이터 변경을 진행하는 방식이 있을 수 있습니다.

분산 트랜잭션 처리 필요

먼저 분산 트랜잭션 환경이란?
2개 이상의 네트워크 시스템간 트랜잭션 환경이라고 보면 됩니다.
모놀리스 환경에서 데이터베이스의 특성 ACID(원자성, 일관성, 고립성, 지속성)를 보장하기 위해 단일 서버/DB내 트랜잭션 처리를 하는 것처럼
MSA 환경의 여러 서버/DB 간에 ACID를 보장하기 위해 단일 트랜잭션이 처리되는 것처럼 여러 기술과 패턴이 도입되어 트랜잭션이 처리되어야 합니다.

이를 위한 대표적인 패턴으로 Saga패턴이 있습니다.

Monolith vs MSA

위와 같은 단점들이 있기 때문에 무조건적으로 모놀리스 방식이 아닌 MSA가 활용되어야 한다고 말할 수 없습니다.
MSA를 활용해야 하는지에 대해서는 구현해야 하는 도메인이나 비즈니스 환경, 사용기술, 해당 서비스를 관리해야하는 조직 등 특성에 따라 선택되어야 합니다.


  1. 모놀리스
  • 단일 코드베이스
  • 쉬운 디버깅
  • 쉬운 배포
  • 상대적으로 좋은 성능
  • 시스템 업데이트 시, 전체 시스템 배포 필요
  • 코드 재사용의 어려움 (다른 시스템 모듈에서의 재사용)
  • 변경 필요사항에 대한 유연한 대처 어려움
  • 다양한 기술 사용에 제약
  • 스케일링이 어려움
  1. MSA
  • 업데이트가 필요한 모듈만 배포 가능
  • 코드 재사용이 쉬움 (다른 시스템에서의 코드 재사용)
  • 변경 필요사항에 대한 유연한 대처 가능
  • 특정 서비스의 장애로부터 독립 가능
  • 다양한 기술 적용 가능
  • 스케일링이 쉬움
  • 개발난이도 증가
  • 상대적으로 복잡한 배포
  • 네트워크 지연으로 인해 상대적으로 떨어지는 성능
  • 데이터 부정합에 취약

3. Cloud Native

MSA와 함께 많이 언급되는 개념인 Cloud Native에 대해 간략히 짚고 넘어가겠습니다.

Cloud Native란? 클라우드 환경에 맞게 서비스를 구축하여 클라우드의 이점을 최대한 활용한 환경이라고 보면 됩니다.
CNCF(Cloud Native Computing Foundation)에서 정의한 내용에서는 Cloud Native를 클라우드 네이티브 기술은 조직이 퍼블릭, 프라이빗, 그리고 하이브리드 클라우드와 같은 현대적이고 동적인 환경에서 확장 가능한 애플리케이션을 개발하고 실행할 수 있게 해준다. 컨테이너, 서비스 메쉬, 마이크로서비스, 불변(Immutable) 인프라, 그리고 선언형(Declarative) API가 이러한 접근 방식의 예시들이다. 이 기술은 회복성, 관리 편의성, 가시성을 갖춘 느슨하게 결합된 시스템을 가능하게 한다. 견고한 자동화 기능을 함께 사용하면 엔지니어는 영향이 큰 변경을 최소한의 노력으로 자주, 예측 가능하게 수행할 수 있다. 라고 정의하고 있습니다.
쉽게 말해서 Cloud 환경의 컨테이너, 서비스메쉬 등과 같은 기술들을 활용하면 관리편의성, 느슨하게 결합된 시스템 등과 같은 효과를 볼 수 있는 것이라고 보면 됩니다.

Cloud Native는 MSA로 구현되는 서비스에 적용되어야 하는 환경이라서 해당 개념을 더 자세하게 알고가면 좋을 것 같습니다.

아래 나열한 기술들은 Cloud Native의 4가지 핵심기술입니다.

  1. CI/CD : Continuous Integration and Continuous Delivery(또는 Deployment), 각자가 작업한 코드를 하나의 결과물로 통합하여 테스트 빌드 지속적 수행(CI), 빌드된 결과물을 최종 배포하여 최종 사용자가 이용가능하도록 지속적 수행(CD), 이러한 과정을 지속적 자동 수행
  2. DevOps : Development and Operation, 소프트웨어의 개발과 운영의 스피드와 품질을 향상하기 위한 새로운 조직 문화이자 접근방식. 개발계획 수립부터 비즈니스 타당성 검토, 개발, Build, Release, 서버 쪽에 Deploy , Operation, Monitoring까지 일련의 과정을 뫼비우스 띠처럼 반복
  3. MicroServices : 하나의 서비스를 Micro 단위로 쪼개서 관리하는 아키텍처
  4. Containers : 애플리케이션 구동을 위해 필요한 라이브러리나 구성파일들을 이미지라는 패키지 형태로 묶어서 구동시키는 환경. 해당 기술을 활용하면 서비스별 독립적인 구동이 가능하고, 효율적인 배포 및 Autoscale 가능

각 기술에 대한 설명을 간단하게 커멘트 남겼지만,
각 기술 하나하나 다룰내용이 많은 기술들입니다. 해당 기술들은 앞으로 작성될 글에서 더 자세하게 다뤄보겠습니다.

이어서

본 글에서 다양한 키워드가 소개되었습니다.
MSA, 모놀리스, Rest, gRPC, kafka, Saga, Cloud Native...
해당 키워드는 이어지는 글에서 MSA에 대해 상세히 다뤄보며 더 알아보겠습니다.

Reference

반응형

'개발 > MSA' 카테고리의 다른 글

[MSA] 헥사고날 아키텍처와 MSA 전환  (0) 2024.11.03
DDD와 Event Storming  (3) 2024.11.02
[k8s] kubernetes 클러스터  (1) 2024.04.06
[k8s] kubernetes란?  (0) 2024.02.29
[MSA] MSA (Microservices Architecture) 표준 아키텍처  (0) 2024.02.15
728x90
반응형

본 글은 모던자바인액션 책을 참고하여 작성한 글입니다.

동작 파라미터화란?

👉🏻Java8 등장배경 및 새로운 기능 이전글에서 언급했던 것처럼
동작 파라미터화는 Java8에서 새롭게 등장한 기능입니다.

동작 파라미터화는 말그대로 특정 동작을 파라미터로 입력할 수 있는 기능인데, 이 기능을 활용하면 특정 동작을 정의한 코드를 다른 메서드의 인수로써 넘겨줄 수 있습니다.

why? 왜 사용해야 하는가?

이전글에서 해당 동작 파라미터화 기능이 Java8의 핵심기능인 스트림 API의 토대가 되었다고 언급했었습니다.
스트림 API를 활용하기 위해 사용한다고도 말할 수 있지만, 좀 더 근본적인 사용이유는
"자주 바뀌는 요구사항에 효과적으로 대응"할 수 있기 때문입니다.

자세한건 예시를 보면서 알아보겠습니다.

예시

요구사항 1. 녹색사과 필터링

농장 재고목록 Application에서 활용되는 사과 리스트중 녹색사과만 필터링하는 기능을 추가한다는 요구사항이 있다고 가정해보겠습니다.
해당 요구사항의 기능은 아래 코드로 구현할 수 있습니다.

public static List<Apple> filterGreenApples(List<Apple> inventory) {
    List<Apple> result = new ArrayList<>()
    for (Apple apple : inventory) {
        if ( GREEN.equals(apple.getColor()) ) {
            result.add(apple);
        }
    }
    return result;
}



요구사항 2. 녹색/빨간사과 필터링

농부가 변심하여 빨간 사과도 필터링하고 싶다고 요구사항을 변경하였습니다.
위 코드를 Copy/Paste하여 if문의 조건만 RED로 바꿔 메서드를 추가할 수 있겠지만,
추후에 다양한 색으로 요구사항이 또 들어올 경우에 유연한 대처가 불가능해질 수 있습니다.
또한, 동일한 기능의 코드가 반복될 수 있죠.

이를 보완하기 위해 아래 코드처럼 색이라는 변수를 받을 수 있도록 메서드에 색 파라미터를 추가해줍니다.

public static List<Apple> filterAppleByColor(List<Apple> inventory, Color color) {
    List<Apple> result = new ArrayList<>()
    for (Apple apple : inventory) {
        if ( apple.getColor().equals(color) ) {
            result.add(apple);
        }
    }
    return result;
}



그리고 아래 코드를 호출하여 요구사항을 만족시킬 수 있습니다.

List<Apple> greenApples = filterAppleByColor(inventory, GREEN);
List<Apple> redApples = filterAppleByColor(inventory, RED);



요구사항 3. 색 이외에 무게 기준으로 필터링 가능하도록 기능 추가

농부가 또 변심하여 색 이외에도 가벼운 사과와 무거운 사과로 구분할 수 있도록 해달라고 할 수 있습니다.

public static List<Apple> filterAppleByWeight(List<Apple> inventory, int weight) {
    List<Apple> result = new ArrayList<>()
    for (Apple apple : inventory) {
        if ( apple.getWeight() > weight ) {
            result.add(apple);
        }
    }
    return result;
}



위 코드로도 무게 기준으로 필터링할 수 있도록 기능을 구현할 수 있지만,
해당 무게 필터링 코드는 색 필터링 코드와 대부분 중복됩니다.
이는 소프트웨어 공학의 DRY (Don't Repeat Yourself), 같은 것을 반복하지 말자는 원칙을 어기는 것입니다.

public static List<Apple> filterApple(List<Apple> inventory, Color color, int weight, boolean flag) {
    List<Apple> result = new ArrayList<>()
    for (Apple apple : inventory) {
        if ((flag && apple.getColor().equals(color))
            || (!flag && apple.getWeight() > weight)) {
            result.add(apple);   
        }
    }
    return result;
}



위 코드로 flag에 따라 필터링 대상을 결정하고,
필터링 대상에 맞게 필터링을 수행할 수 있지만, 요구사항 추가에 유연하게 대응할 수 없고,
flag를 다양화해서 어찌어찌 대응한다해도 코드의 복잡성이 굉장히 크게 증가할 것입니다.

이를 대체하는 방안으로 동작 파라미터화기능이 사용될 수 있습니다.

동작 파라미터화 기능의 활용

예시 보완

위 예시에서 동작 파라미터화 기능을 사용하기 위해 아래와 같은 사전준비 작업이 필요합니다.

  1. 필터링 메서드의 참/거짓을 반환하는 인터페이스(프레디케이트) 정의
  2. 참/거짓을 반환하는 인터페이스(프레디케이트)의 구현체 정의 (ex. 150이상 무게 사과 판단, 초록사과 판단 등)
  3. 필터링 메서드의 동작 파라미터화


아래는 프레디케이트 정의 코드입니다.

public interface ApplePredicate {
    boolean test (Apple apple);
}



아래는 프레디케이트의 구현체 코드입니다.

// 150이상 무게 사과 판단
public class AppleHeavyWeightPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
}

// 초록사과 판단
public class AppleGreenColorPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return GREEN.equals(apple.getColor());
    }
}



위 처럼 프레디케이트 인터페이스를 정의하고 구현체를 작성하는 패턴을 전략 디자인 패턴이라고 합니다.
전략 디자인 패턴은 전략(알고리즘)을 캡슐화하는 알고리즘 패밀리(프레디케이트 인터페이스)를 정의해둔 다음에 런타임에 상황에 따라 알고리즘을 선택하는 기법입니다. (ApplePredicate : 알고리즘 패밀리, AppleHeavyWeightPredicate/AppleGreenColorPredicate : 전략)

해당 패턴을 활용한다면 filter 메서드에서 파라미터로 받은 동작에 맞게 필터링을 수행할 수 있습니다.
아래 코드는 전략 디자인 패턴 기반의 동작을 파라미터로 받을 수 있도록 수정한 filter 메서드입니다.

public static List<Apple> filterApple(List<Apple> inventory, ApplePredicate p) {
    List<Apple> result = new ArrayList<>()
    for (Apple apple : inventory) {
        if ( p.test(apple) ) {
            result.add(apple);
        }
    }
    return result;
}



위 코드처럼 프레디케이트(인터페이스)를 파라미터로 지정한 다음에 다양한 구현체를 파라미터로 받아 다양하게 필터링을 수행할 수 있도록 하였습니다.
즉, 우리가 전달한 ApplePredicate 객체에 의해 filterApples 메서드의 동작이 결정됩니다.

익명클래스

위와 같은 방법도 충분히 좋지만, 동작을 파라미터화하기 위해 인터페이스를 정의하고 여러 클래스를 정의하는 과정이 상당히 번거롭게 느껴집니다.
이를 간소화하기 위해 익명클래스를 활용하는 방법이 있습니다.

익명클래스를 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있어 별도로 여러 클래스를 정의하는 과정이 필요하지 않습니다.
아래 코드는 익명클래스를활용하기 이전 filter메소드를 실행하는 코드입니다.

List<Apple> heavyApples = filterApples(inventory, new AppleHeavyWeightPredicate());
List<Apple> greenApples = filterApples(inventory, new AppleGreenColorPredicate());



위 코드에서는 사전에 정의된 클래스를 파라미터로 입력하여 실행하고 있습니다.
아래 코드는 익명클래스를 활용하여 filte메소드를 실행하는 코드입니다.

List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
    public boolean test(Apple a) {
        return RED.equals(a.getColor());
    }
});



익명클래스를 활용하여 별도 클래스를 선언하고 작성하는 과정을 간소화하였지만,
여전히 코드는 불필요하게 공간을 많이 차지하고 있고, 프로그래머가 여전히 익숙하게 느껴질 수 있는 형태의 코드가 아닙니다.

이러한 코드의 장황함은 코드를 구현하고 유지보수하는 데 많은 시간을 소요하게끔 할 수 있습니다.

람다 표현식

위의 익명클래스의 단점을 보완하기 위해 람다 표현식이 사용될 수 있습니다.

List<Apple> result = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));



위 코드를 통해 익명클래스에서의 복잡성을 많이 줄일 수 있습니다.
람다표현식의 자세한 부분은 👉🏻다음글에서 다룰 것입니다.

리스트 형식의 추상화

아래 코드처럼 프레디케이트에 제너릭을 활용한다면,
사과 뿐만 아니라 바나나, 오렌지 등으로 대상까지 다양화할 수 있습니다.

public interface Predicate<T> {
    boolean test(T t);
}

public static <T> List<T> filter(List<T> list, Predicate<T> p) {
    List<T> result = new ArrayList<>();
    for (T e : list) {
        if (p.test(e)) {
            result.add(e);
        }
    }
    return result;
}



실전 예제

실전에서 사용하고 있는 예제로 Comparator, Runnable 등이 있고,
간단히 다뤄보겠습니다.

Comparator로 정렬하기

Java8에서 Collection에는 sort 메서드가 포함되어 있습니다.
해당 sort메서드의 정렬방식을 Comparator를 활용하여 정의할 수 있는데요.

아래 코드처럼 Comparator를 활용하여 정렬 기준을 정의한 후에
sort 메서드에 파라미터로 입력하면 됩니다.

// java.util.Comparator
public interface Comparator<T> {
    int compare(T o1, T o2);
}
  • 익명클래스
inventory.sort(new Comparator<Apple>() {
    public int compare(Apple a1, Apple a2) {
        return a1.getWeight().compareTo(a2.getWeight());
    }
});
  • 람다표현식
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));



Runnable로 코드 블록 실행하기

자바 스레드를 이용하면 코드를 병렬로 실행할 수 있습니다.
Runnable은 스레드에 실행할 동작을 전달할 때, 사용되는 인터페이스입니다.

// java.lang.Runnable
public interface Runnable {
    void run();
}
  • 익명 클래스
Thread t = new Thread(new Runnable() {
    public void run() {
        System.out.println("Hello World!");
    }
});
  • 람다 표현식
Thread t = new Thread(() -> System.out.println("Hello World!"));



이어서

람다표현식에 대해 상세히 알아보겠습니다.

반응형

+ Recent posts