danuri
오늘의 기록
danuri
전체 방문자
오늘
어제
  • 오늘의 기록 (307)
    • java (150)
      • java (33)
      • spring (63)
      • jpa (36)
      • querydsl (7)
      • intelliJ (9)
    • kotlin (8)
    • python (24)
      • python (10)
      • data analysis (13)
      • crawling (1)
    • ddd (2)
    • chatgpt (2)
    • algorithm (33)
      • theory (9)
      • problems (23)
    • http (8)
    • git (8)
    • database (5)
    • aws (12)
    • devops (10)
      • docker (6)
      • cicd (4)
    • book (44)
      • clean code (9)
      • 도메인 주도 개발 시작하기 (10)
      • 자바 최적화 (11)
      • 마이크로서비스 패턴 (0)
      • 스프링으로 시작하는 리액티브 프로그래밍 (14)
    • tistory (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

인기 글

태그

  • JPA
  • 자바 최적화
  • Security
  • S3
  • Bitmask
  • RDS
  • Database
  • Spring
  • 트랜잭션
  • gitlab
  • Jackson
  • Java
  • AWS
  • reactive
  • Kotlin
  • SWAGGER
  • 등가속도 운동
  • CICD
  • Saving Plans
  • ChatGPT
  • PostgreSQL
  • docker
  • 마이크로서비스패턴
  • 도메인 주도 설계
  • mockito
  • connection
  • DDD
  • nuribank
  • Thymeleaf
  • POSTGIS

최근 댓글

최근 글

hELLO · Designed By 정상우.
danuri

오늘의 기록

book/스프링으로 시작하는 리액티브 프로그래밍

[리액티브 프로그래밍] 리액티브 프로그래밍을 위한 사전 지식

2024. 3. 3. 19:48

스프링으로 시작하는 리액티브 프로그래밍 책 정리

 

스프링으로 시작하는 리액티브 프로그래밍 | 황정식 - 교보문고

스프링으로 시작하는 리액티브 프로그래밍 | *리액티브 프로그래밍의 기본기를 확실하게 다진다*리액티브 프로그래밍은 적은 컴퓨팅 파워로 대량의 요청 트래픽을 효과적으로 처리할 수 있는

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
    'book/스프링으로 시작하는 리액티브 프로그래밍' 카테고리의 다른 글
    • [리액티브 프로그래밍] 마블 다이어그램(Marble Diagram)
    • [리액티브 프로그래밍] Reactor 개요
    • [리액티브 프로그래밍] Blocking I/O와 Non-Blocking I/O
    • [리액티브 프로그래밍] 리액티브 스트림즈(Reactive Streams)
    danuri
    danuri
    IT 관련 정보(컴퓨터 지식, 개발)를 꾸준히 기록하는 블로그입니다.

    티스토리툴바