스프링으로 시작하는 리액티브 프로그래밍 | 황정식 - 교보문고
스프링으로 시작하는 리액티브 프로그래밍 | *리액티브 프로그래밍의 기본기를 확실하게 다진다*리액티브 프로그래밍은 적은 컴퓨팅 파워로 대량의 요청 트래픽을 효과적으로 처리할 수 있는
product.kyobobook.co.kr
함수형 인터페이스(Functional Interface)
리액티브 프로그래밍을 잘 사용하기 위해서 기본적으로 함수형 프로그래밍 기법을 알아야 한다.
✅ 함수형 인터페이스
단 하나의 추상 메서드만 정의되어 있는 인터페이스.
함수를 일급 시민으로 취급하여, 함수 자체를 파라미터로 전달할 수 있다.
ex) Comparator
public class Example4_1 { public static void main(String[] args) { List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies; Collections.sort(cryptoCurrencies, new Comparator<CryptoCurrency>() { @Override public int compare(CryptoCurrency cc1, CryptoCurrency cc2) { return cc1.getUnit().name().compareTo(cc2.getUnit().name()); } }); for(CryptoCurrency cryptoCurrency : cryptoCurrencies) System.out.println("암호 화폐명: " + cryptoCurrency.getName() + ", 가격: " + cryptoCurrency.getUnit()); } }
-> sort 메서드 안에 Comparator 인터페이스를 익명 구현 객체로 전달한다.
Comparator 인터페이스의 내부 모습은 다음과 같다.
@FunctionalInterface public interface Comparator<T> { int compare(T o1, T o2); ... }
하나의 추상 메서드만 정의되어 있고, @FunctionalInterface라는 애노테이션으로 함수형 인터페이스임을 명확하게 했다.
사실 익명 구현 객체, 즉 객체를 파라미터로 전달하는 방식은 이미 Java에서 많이 쓰이고,
익명 구현 객체를 전달하는 방식은 코드가 길어 지저분하게 보이기도 한다.
-> 그래서 Java 8부터 람다 표현식을 통해 익명 구현 객체를 전달하던 방식을 조금 더 함수형 프로그래밍에 맞게 표현할 수 있게 됐다.
람다 표현식(Lambda Expression)
✅ 람다 표현식
람다 표현식은 Java에서 함수를 값으로 취급하기 위해 생겨난 간결한 형태의 표현식이다.
람다 표현식은 함수형 인터페이스를 구현한 클래스의 메서드 구현을 단순화한 표현식이다.
(람다 파라미터) -> { 람다 몸체 }
람다 파라미터: 함수형 인터페이스에 정의된 추상 메서드의 파라미터.
람다 몸체: 추상 메서드에서 구현되는 메서드 몸체.
앞서 봤던 Comparaotr의 예시를 람다 표현식으로 표현해본다.
public class Example4_4 { public static void main(String[] args) { List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies; Collections.sort(cryptoCurrencies, (cc1, cc2) -> cc1.getUnit().name().compareTo(cc2.getUnit().name())); for(CryptoCurrency cryptoCurrency : cryptoCurrencies) System.out.println("암호 화폐명: " + cryptoCurrency.getName() + ", 가격: " + cryptoCurrency.getUnit()); } }
-> 훨씬 간결해진 것을 볼 수 있다.
람다 파라미터인 cc1, cc2는 파라미터 타입인 CryptoCurrency가 생략된 표현인데, 람다 표현식에서는 메서드 파라미터의 타입이 내부적으로 추론되어 생략이 가능해진다.
여기서 혼동하지 않아야 할 것은, 람다 표현식도 결국 함수형 인터페이스를 구현한 클래스의 인스턴스를 람다 표현식으로 작성해서 전달한다는 것이다.
✅ 람다 캡처링
람다 표현식 외부에서 정의된 변수(자유 변수)를 사용하는 것.
public class Example4_5 { public static void main(String[] args) { List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies; String korBTC = "비트코인"; // korBTC = "빗코인"; cryptoCurrencies.stream() .filter(cc -> cc.getUnit() == CurrencyUnit.BTC) .map(cc -> cc.getName() + "(" + korBTC + ")" ) .forEach(cc -> System.out.println(cc)); } }
map() 메서드의 파라미터인 람다 표현식에서 외부에서 정의된 korBTC 변수를 사용하고 있다.
주의할 점으로, 만약 예제의 주석을 해제하면 오류가 발생하는데, 그 이유는 람다 표현식에서 사용되는 자유 변수는 final 또는 final과 같은 효력을 지녀야 하기 때문이다.
메서드 레퍼런스(Method Reference)
✅ 메서드 레퍼런스
람다 표현식을 더 간결하게 작성할 수 있는 방법.
✅ ClassName::static method
public class Example4_6 { public static void main(String[] args) { List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies; cryptoCurrencies.stream() .map(cc -> cc.getName()) // .map(name -> StringUtils.upperCase(name)) .map(StringUtils::upperCase) .forEach(name -> System.out.println(name)); } }
주석 처리된 라인과 그 아래 라인은 같은 표현식이다.
✅ ClassName::intance method
public class Example4_7 { public static void main(String[] args) { List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies; cryptoCurrencies.stream() .map(cc -> cc.getName()) // .map(name -> name.toUpperCase()) .map(String::toUpperCase) .forEach(name -> System.out.println(name)); } }
주석 처리된 라인과 그 아래 라인은 같은 표현식이다.
✅ object::intance method
public class Example4_8 { public static void main(String[] args) { List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies; int amount = 2; PaymentCalculator calculator = new PaymentCalculator(); cryptoCurrencies.stream() .filter(cc -> cc.getUnit() == CurrencyUnit.BTC) .map(cc -> new ImmutablePair(cc.getPrice(), amount)) // .map(pair -> calculator.getTotalPayment(pair)) .map(calculator::getTotalPayment) .forEach(System.out::println); } }
주석 처리된 라인과 그 아래 라인은 같은 표현식이다.
✅ ClassName::new
public class Example4_9 { public static void main(String[] args) { List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies; int amount = 2; Optional<PaymentCalculator> optional = cryptoCurrencies.stream() .filter(cc -> cc.getUnit() == CurrencyUnit.BTC) .map(cc -> new ImmutablePair(cc.getPrice(), amount)) // .map(pair -> new PaymentCalculator(pair)) .map(PaymentCalculator::new) .findFirst(); System.out.println(optional.get().getTotalPayment()); } }
생성자도 메서드 레퍼런스로 사용할 수 있다.
주석 처리된 라인과 그 아래 라인은 같은 표현식이다.
함수 디스크립터(Function Descriptor)
✅ 함수 디스크립터
일반화된 람다 표현식을 통해서 함수형 인터페이스가 어떤 파라미터를 가지고, 어떤 값을 리턴하는지 설명해 주는 역할.
함수형 인터페이스 | 함수 디스크럽터 |
Predicate<T> | T -> boolean |
Consumer<T> | T -> void |
Function<T, R> | T -> R |
Supplier<T> | () -> T |
BiPredicate<L, R> | (L, R) -> boolean |
BiConsumer<T, U> | (T, U) -> void |
BiFunction<T, U, R> | (T, U) -> R |
✅ Predicate
추상 메서드가 하나의 파라미터를 가지고, 리턴 값으로 boolean 타입을 반환하는 함수형 인터페이스.
@FunctionalInterface public interface Predicate<T> { boolean test(T t); }
Predicate를 가장 흔하게 볼 수 있는 코드는 filter 메서드다.
filter 메서드는 파라미터로 Predicate를 가지며 test 메서드의 리턴 값이 true인 데이터만 필터링하게 된다.
다음은 filter 메서드를 직접 코드로 구현한 예제다.
public class Example4_11 { public static void main(String[] args) { List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies; List<CryptoCurrency> result = filter(cryptoCurrencies, cc -> cc.getPrice() > 500_000); for (CryptoCurrency cc : result) { System.out.println(cc.getName()); } } private static List<CryptoCurrency> filter(List<CryptoCurrency> cryptoCurrencies, Predicate<CryptoCurrency> p){ List<CryptoCurrency> result = new ArrayList<>(); for (CryptoCurrency cc : cryptoCurrencies) { if (p.test(cc)) { result.add(cc); } } return result; } }
✅ Consumer
추상 메서드가 하나의 파라미터를 가지고, 리턴 값이 없는 함수형 인터페이스.
@FunctionalInterface public interface Consumer<T> { void accept(T t); }
Consumer의 대표적인 예로, 특정 작업을 수행한 후, 결과 값을 리턴할 필요가 없는 배치 처리를 들 수 있다.
public class Example4_13 { public static void main(String[] args) { List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies; addBookmark(cryptoCurrencies, cc -> saveBookmark(cc)); } private static void addBookmark(List<CryptoCurrency> cryptoCurrencies, Consumer<CryptoCurrency> consumer) { for (CryptoCurrency cc : cryptoCurrencies) { consumer.accept(cc); } } private static void saveBookmark(CryptoCurrency cryptoCurrency) { System.out.println("# Save " + cryptoCurrency.getUnit()); } }
addBookmark() 메서드의 파리미터로 리턴 값이 없는 saveBookmark가 추상 메서드인 Consumer 인터페이스를 전달한다.
✅ Function
추상 메서드가 하나의 T 타입 파라미터를 가지고, 리턴 값으로 R 타입을 반환하는 함수형 인터페이스.
@FunctionalInterface public interface Function<T, R> { R apply(T t); }
어떤 input에 대해 어떤 처리 과정을 거친 후 어떤 output을 반환하는 전형적인 함수 역할을 하기 때문에, Function이라는 이름을 가진다.
public class Example4_15 { public static void main(String[] args) { List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies; int totalPayment = calculatePayment(cryptoCurrencies, cc -> cc.getPrice() * 2); System.out.println("# 구매 비용: " + totalPayment); } private static int calculatePayment(List<CryptoCurrency> cryptoCurrencies, Function<CryptoCurrency, Integer> f) { int totalPayment = 0; for (CryptoCurrency cc : cryptoCurrencies) { totalPayment += f.apply(cc); } Supplier s = () -> ""; return totalPayment; } }
✅ Supplier
추상 메서드가 파라미터를 갖기 않고, 리턴 값을 반환하는 함수형 인터페이스.
@FunctionalInterface public interface Supplier<T> { T get(); }
Supplier 인터페이스는 어떤 값이 필요할 때 데이터를 제공하는 용도로 사용할 수 있다.
public class Example4_17 { public static void main(String[] args) { String mnemonic = createMnemonic(); System.out.println(mnemonic); } private static String createMnemonic() { return Stream .generate(() -> getMnemonic()) .limit(12) .collect(Collectors.joining(" ")); } private static String getMnemonic() { List<String> mnemonic = Arrays.asList( "alpha", "bravo", "charlie", "delta", "echo", "foxtrot", "golf", "hotel", "india", "juliet", "kilo", "lima", "mike", "november", "oscar", "papa", "quebec", "romeo", "sierra", "tango", "uniform", "victor", "whiskey", "xray", "yankee", "zulu" ); Collections.shuffle(mnemonic); return mnemonic.get(0); } }
Stream.generate(Supplier).limit(int)는 limit 만큼의 데이터를 생성한다.
✅ Bixxxxx
BiPredicate, BiConsumer, BiFunction과 같이 Bi로 시작하는 함수형 인터페이스는 추상 메서드에 전달해야 할 파라미터가 하나 더 추가되어, 두개의 파리미터를 갖는 함수형 인터페이스다.
-> 기본 함수형 인터페이스의 확장형이라고 보면 된다.
참고자료
https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html
'book > 스프링으로 시작하는 리액티브 프로그래밍' 카테고리의 다른 글
[리액티브 프로그래밍] 마블 다이어그램(Marble Diagram) (0) | 2024.03.11 |
---|---|
[리액티브 프로그래밍] Reactor 개요 (0) | 2024.03.11 |
[리액티브 프로그래밍] Blocking I/O와 Non-Blocking I/O (2) | 2024.02.26 |
[리액티브 프로그래밍] 리액티브 스트림즈(Reactive Streams) (0) | 2024.02.19 |
[리액티브 프로그래밍] 리액티브 시스템과 리액티브 프로그래밍 (0) | 2024.02.19 |