Dev/Reactive Programming

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

김세진 2024. 7. 2. 16:49
반응형

 

 

 

 

 

함수형 인터페이스(Functional Interface)

 

함수형 인터페이스란 단 하나의 추상 메서드를 갖는 인터페이스로, 함수를 일급 객체로 취급하여 함수 자체를 파라미터로 전달할 수 있는 기법이다.

 

자바에서 이같은 함수형 인터페이스라는 개념은 자바 8부터 추가되었으나, 그 이전에도 함수형 인터페이스처럼 사용하는 것이 있었는데 바로 Comparator이다.

 

예제 코드

public class Example4_1 {
	private static final List<CryptoCurrency> cryptoCurrencies = SampleData.crytoCurrencies;

	public static void main(String[] args) {
		cryptoCurrencies.sort(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());
		}

	}
}

 

CryptoCurrecy.java

더보기
@Getter
public class CryptoCurrency {
	private final String name;
	private final CurrencyUnit unit;
	private int price;

	public CryptoCurrency(String name, CurrencyUnit unit) {
		this.name = name;
		this.unit = unit;
	}

	public CryptoCurrency(String name, CurrencyUnit unit, int price) {
		this.name = name;
		this.unit = unit;
		this.price = price;
	}

	public enum CurrencyUnit {
		BTC, ETH, DOT, ADA, SOL
	}
}

 

SampleData.java

더보기
public class SampleData {
	public static final List<CryptoCurrency> cryptoCurrencies = Arrays.asList(
		new CryptoCurrency("Bitcoin", CryptoCurrency.CurrencyUnit.BTC, 40_000_000),
		new CryptoCurrency("Ethereum", CryptoCurrency.CurrencyUnit.ETH, 2_000_000),
		new CryptoCurrency("Polkadot", CryptoCurrency.CurrencyUnit.DOT, 10_000),
		new CryptoCurrency("Cardano", CryptoCurrency.CurrencyUnit.ADA, 1_500),
		new CryptoCurrency("Solana", CryptoCurrency.CurrencyUnit.SOL, 150_000)
	);
}

 

 

암호 화폐를 화폐 단위 기준으로 오름차순 정렬을 한 뒤 출력하는 예제 코드이다. 다음은 Comparator 인터페이스 내부 코드이다.

 

@FunctionalInterface
public interface Comparator<T> {
	int compare(T o1, T o2);
    
    ...
}

compare라는 단 하나의 추상 메서드만 가짐을 확인할 수 있다. @FunctionalInterface라는 어노테이션도 선언되어 있음을 확인할 수 있는데, 이는 자바 8부터 해당 인터페이스가 함수형 인터페이스임을 명시하는 목적으로 사용한다.

 

위 내용을 다시 살펴보면, 결국 sort() 의 파라미터로 Comparator 인터페이스를 익명 구현 객체의 형태로 전달하는 것이기 때문에 함수형 인터페이스를 함수형 프로그래밍 방식으로 작성했다고 보기는 어렵다.

 

따라서 익명 구현 객체를 사용할 경우 불필요하게 코드가 길어지는 것을 막고, 함수형 프로그래밍 방식에 맞게 사용할 수 있도록 자바 8부터 람다 표현식이 도입되었다.

 

 


람다 표현식(Lambda Expression)

 

람다 표현식

람다 표현식은 함수형 인터페이스를 사용(구현)할 때 익명 클래스 구현을 하는 대신, 화살표 함수(Arrow Function)를 사용해 간결하게 표현하도록 한 것이다.

 

다음은 위에서 보았던 코드에 람다 표현식을 적용한 모습이다.

 

public class Example4_4 {
	private static final List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies;
    
	public static void main(String[] args) {
		cryptoCurrencies.sort((cc1, cc2) -> cc1.getUnit().name().compareTo(cc2.getUnit().name()));

		for (CryptoCurrency cryptoCurrency : cryptoCurrencies) {
			System.out.println("암호 화폐명: " + cryptoCurrency.getName() + ", 가격: " + cryptoCurrency.getUnit());
		}

	}
}

목표와 상관 없는 불필요한 코드가 제거되고 의미가 명확해졌다. 단, 표현식만 바뀌었을 뿐 내부적으로는 함수형 인터페이스를 구현한 인스턴스를 전달하는 것임에 주의하자.

 

람다 캡쳐링(Lambda Capturing)

람다 표현식을 사용할 때, 파라미터로 전달된 변수 뿐 아니라 람다 표현식 외부에서 정의된 변수도 사용할 수 있는데, 이를 람다 캡쳐링이라고 한다. 다음은 람다 캡쳐링을 사용한 예제 코드이다.

 

public class Example4_5 {
	private static final List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies;

	public static void main(String[] args) {
		String korBTC = "비트코인";
		// kotBTC = "빗코인";

		cryptoCurrencies.stream()
			.filter(cc -> cc.getUnit() == CryptoCurrency.CurrencyUnit.BTC)
			.map(cc -> cc.getName() + "(" + korBTC + ")")
			.forEach(cc -> System.out.println(cc));

	}
}

 

스트림에서 사용된 각 함수는 람다 표현식으로 작성되었는데, map() 내부에는 외부에서 korBTC 변수를 사용하고 있는 모습을 확인할 수 있다.

 

단, 람다 캡쳐링으로 사용할 변수는 final과 같이 불변으로 사용된 것만 사용 가능하므로, 위의 주석을 해제하면 예외가 발생하게 된다.

 

 


메서드 참조(Method Reference)

 

위의 람다 표현식을 더욱 간결하게 표현할 수 있는 방법으로 클래스 및 메서드의 이름과, 구분자(::)만 선언하여 표현하는 방식이다.

 

(Car car) -> car.getCarName()Car::getCarName 과 같이 표현할 수 있다. 다음 예제 코드를 살펴보도록 하자.

 

public class Example4_6 {
	private static final List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies;

	public static void main(String[] args) {
		cryptoCurrencies.stream()
			// .map(cc -> cc.getName())
			// .map(name -> StringUtils.upperCase(name))
			// .forEach(name -> System.out.println(name));
			.map(CryptoCurrency::getName)
			.map(StringUtils::upperCase) // commons-lang3 라이브러리
			.forEach(System.out::println);
	}
}

 

주석 처리된 람다 표현식을 그 아래와 같이 메서드 참조 형태로 더 간결하게 나타낼 수 있다. 이같은 일이 가능한 이유는 람다 표현식은 각 메서드에 람다 파라미터를 전달하는데, 이를 통해 컴파일러가 내부적으로 람다 표현식의 파라미터 형식을 추론할 수 있기 때문이다. 이는 곧 그러한 경우에만 메서드 참조가 사용 가능하단 말이기도 하다.

 

람다 표현식 유형

  1. ClassName::static method 유형: 위의 예제에 해당
  2. ClassName::instance method 유형: 
  3. object::instance method 유형: 람다 표현식 외부에서 정의된 객체의 메서드를 호출할 때 사용
  4. ClassName::new 유형: 람다 표현식 내부에서 생상자 사용

 

 


함수 디스크립터(Function Descriptor)

 

함수 디스크립터란 람다 표현식을 통해 해당 함수가 어떤 파라미터를 전달받고, 어떤 타입을 리턴하는지 기술한 명세이다. 다음은 자주 사용되는 일부 함수형 인터페이스에 대한 디스크립터이다.

 

함수형 인터페이스(Functional Interface) 함수 디스크립터(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

 

위에서 T, U, L, R 은 용도에 따라 관습적으로 사용되는 제네릭 명칭이다.

  • L: Left (첫 번째 매개변수 타입)
  • R: Right (두 번째 매개변수 타입) 또는 Return (반환 타입)
  • T: Type (일반적인 타입)
  • U: Second Type (두 번째 타입)

Predicate를 예로 들면 매개변수로 T 제네릭 타입을 받아 boolean 타입을 반환한다는 의미이다.

 

정리

Predicate<T>: T 타입의 매개변수를 받아 boolean 타입을 반환하는 함수

  • 예: Predicate<String> isEmpty = str -> str.isEmpty();

Consumer<T>: T 타입의 매개변수를 받아 어떤 값을 반환하지 않는 함수 (void 반환)

  • 예: Consumer<String> print = str -> System.out.println(str);

Function<T, R>: T 타입의 매개변수를 받아 R 타입을 반환하는 함수

  • 예: Function<String, Integer> stringLength = str -> str.length();

Supplier<T>: 매개변수를 받지 않고 T 타입의 값을 반환하는 함수

  • 예: Supplier<String> supplier = () -> "Hello";

BiPredicate<L, R>: L과 R 타입의 두 매개변수를 받아 boolean 타입을 반환하는 함수

  • 예: BiPredicate<String, Integer> lengthGreaterThan = (str, len) -> str.length() > len;

BiConsumer<T, U>: T와 U 타입의 두 매개변수를 받아 어떤 값을 반환하지 않는 함수 (void 반환)

  • 예: BiConsumer<String, Integer> printLength = (str, len) -> System.out.println(str + " length: " + len);

BiFunction<T, U, R>: T와 U 타입의 두 매개변수를 받아 R 타입을 반환하는 함수

  • 예: BiFunction<String, String, Integer> concatLength = (str1, str2) -> (str1 + str2).length();

 

 

 

 

 

 

 

반응형