요깨비's LAB

[Modern Java] Chapter 2. Function, The Transformer 본문

Java/모던 자바

[Modern Java] Chapter 2. Function, The Transformer

요깨비 2019. 12. 23. 20:57

본 기록은 케빈님의 강의와 "한빛미디어 - 모던 자바 인 액션"을 정리한 내용들입니다.


1. Functional Interface

Functional Interface에 대해서 공부하겠습니다. Functional Interface는 abstract method가 하나만 존재하는 것입니다.
Functional Interface가 왜 중요하냐? 이걸 사용하는 코드는 익명 클래스로 메서드로 감싸서 함수를 보낼 필요가 없이
람다 표현식으로 대체할 수 있습니다.

1-1. Function Interface

Function 인터페이스의 내부 코드입니다. 

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

	... 생략
}


apply를 보시면 T타입을 받아서 연산을 수행하고 결과를 R타입으로 반환하는 코드입니다.
(같은 타입으로 리턴할 수도 있습니다.) -> identity 메서드
* identity 메서드의 예를 하나 보겠습니다.

// not identity
public String identityMethod(String value) {
	return "value is " + value; // type은 갖지만 value가 바껴버림 그래서 identity가 아님
}

// identity
public String identityMethod(String value) {
	return value;
}

이 코드를 보면 "왜 굳이 값을 받아서 이걸 그대로 return하는 메서드가 필요하냐"는 의문이 생기는데 이 부분은 아래에
추가적으로 설명을 통해 알려드리겠습니다.

우선 Function 메서드를 이용하여 코드를 하나 짜보게습니다. 

Function<String, Integer> toInt = new Function<String, Integer>() {

	@Override
	public Integer apply(String t) {
		// TODO Auto-generated method stub
		return Integer.valueOf(t);
	}
};
		
final Integer number = toInt.apply("100");

이거를 앞으로 우리가 계속 사용할 람다 표현식으로 바꿔보도록 하겠습니다.

Function<String, Integer> toInt = (value) -> Integer.valueOf(value);
		
final Integer number = toInt.apply("100");

이게 가능한 이유는 @Function 인터페이스를 구현하여 단 한개의 메소드만 가질 수 있도록 내부에서 구현되었기 때문입니다. 똑같이 identity 함수를 하나 만들어 보겠습니다.

//		final Function<Integer, Integer> identity = Function.identity();
		final Function<Integer, Integer> identity = (value) -> value;


1-2. Consumer Interface

이번에는 Consumer에 대해서 알아보겠습니다. 내부 구현된 코드입니다.

@FunctionalInterface
public interface Consumer<T> {
	void accept(T t);
    ... 생략
}

Consumer 또한 구현해야할 함수가 accept 하나 입니다. 이거는 어떨때 사용할까요? 출력할때 주로 사용하죠 아래의 코드를 보겠습니다.

Consumer<String> print = new Consumer<String>() {

	@Override
	public void accept(String value) {
		// TODO Auto-generated method stub
		System.out.println(value);
	}
};
		
print.accept("Hello World");

// Lambda Expression
Consumer<String> print = value -> System.out.println(value);
		
print.accept("Hello World");

Function으로도 출력 할 수 있지 않을까? 예를 하나 들어보죠.

// Error! Cannot return Void
final Function<String, Void> printByFunction = value -> System.out.println(value);

Function은 기본적으로 Return 값을 가지고 있어야하는데 Void는 타입은 있지만 리턴 없기 때문에 에러가 납니다.
이것이 바로 Consumer를 사용하는 이유입니다.

1-3. Predicate Interface

Predicate 인터페이스의 내부 코드입니다.

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

	.. default 메서드는 생략
}

결과값을 보시면 boolean인게 보이시죠? Predicate 같은 경우에는 어떤 결과값이 주어진 조건에 만족을 하는지 확인용으로
주로 사용합니다. 아래의 코드를 보겠습니다.

Predicate<Integer> isPositive = value -> value > 0;
		
System.out.println(isPositive.test(1)); // true
System.out.println(isPositive.test(0)); // false
System.out.println(isPositive.test(-1));// false

주어진 값이 0보다 같거나 작으면 false를 반환하고 있습니다. 이것을 어떻게 활용할 수 있는지 예제를 하나 만들겠습니다.

Predicate<Integer> isPositive = value -> value > 0;

List<Integer> numbers = Arrays.asList(-5,-4,-3,-2,-1,0,1,2,3,4,5);
List<Integer> positiveNumbers = new ArrayList<Integer>();
		
for(Integer num : numbers) {
	if(isPositive.test(num)) {
		positiveNumbers.add(num);
	}
}
		
System.out.println(positiveNumbers); // [1,2,3,4,5]

여기에서 이번에는 0보다 작은수를 추가적으로 찾고 싶으면 어떻게 할까요? 네. 위 코드 바로 아래줄에 해당 코드를 
넣어주면 됩니다.

Predicate<Integer> isNegative = value -> value < 0;

List<Integer> negativeNumbers = new ArrayList<Integer>();
		
for(Integer num : numbers) {
	if(isNegative.test(num)) {
		negativeNumbers.add(num);
	}
}
		
System.out.println(negativeNumbers); // [-5,-4,-3,-2,-1]

그런데 이렇게 짜면 너무 반복되는 코드가 많아지고 지저분해집니다.
if(isNegative.test(num) 이 부분을 제외하고는 나머지는 공통되는 코드들이기 때문에 이것을 리팩토링 하겠습니다.

Predicate<Integer> isPositive = value -> value > 0;
System.out.println(filter(numbers,isPositive));

System.out.println(filter(numbers, value-> value < 0));

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

공통부분은 함수를 통해 하나로 통합하고, 바뀌는 부분만을 Function인터페이스를 통해 구현해주어 재사용성이 높고
유지보수하기 쉬운 코드로 바뀌었습니다. 만약 자주쓴다면 isPositive에 담아서 이름을 통해 명시하는 방법과
한번만 사용하는 것이라면 람다 표현식만을 간단하게 제공하는 방식 두가지를 모두 사용해서 표현하였습니다.
 

1-4 Supplier Interface

Supplier 인터페이스의 내부 구현 코드입니다.

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

Supplier를 이용한 간단한 예제입니다.

Supplier<String> helloSupplier = () -> "Hello";
System.out.println(helloSupplier.get() + " World"); // Hello World

왜 그냥 Hello World라고 쓰면되지 이렇게 굳이 하는 이유가 있나?라는 의문이 생깁니다. 아래의 예제를 보겠습니다.

printIfValidIndex(1, String.valueOf(1));

private static void printIfValidIndex(int number, String value) {
	if(number >= 0) {
		System.out.println("The value is " + value + ".");
	}else {
		System.out.println("Invalid");
	}
}

이 코드를 보면 0보다 큰 값일 때 제대로 된 값을 출력해줍니다. 메서드의 파라미터에서 value는 number >= 0일 때만 사용이 됩니다.
String 객체는 생성에 비용이 크지 않으니 신경을 쓸 필요가 없지만 만약에 String이 아닌 어떤 메서드의 결과값을 받는다고 가정해보죠.

	long start = System.currentTimeMillis();
	printIfValidIndex(1, getValueExpensiveValue());
	printIfValidIndex(-1, getValueExpensiveValue());
	System.out.println((System.currentTimeMillis() - start) / 1000 + "초 걸렸습니다.");
	// 6초 걸렸습니다.
    
 	// 이 값을 얻기까지 엄청난 연산이 필요하다는 것을 의미
	private static String getVeryExpensiveValue() {
		// 이 연산을 수행하기까지 3초나 걸린다고 가정하기 위해 넣은 코드
		try {
			TimeUnit.SECONDS.sleep(3);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		return "Yoggaebi";
	}

이 코드를 보면 getValueExpensiveValue의 결과값이 필요한 경우는 1일 경우에만 필요합니다.(조건이 0보다 같거나 클때 이므로)
-1일 때는 굳이 getValueExpensiveValue()함수를 실행하여 반환값을 얻을 필요가 없죠. 그렇지만 사용이 되어 불필요한 3초가 추가적으로
낭비되고 있습니다. 이것을 일반 함수가 아닌 Functional Interface를 한번 사용해보죠.

	long start = System.currentTimeMillis();
//	자바 8 이전이었다면... 복잡하고.. 쓰기 귀찮고..
//	printIfValidIndex(1, new Supplier<String>() {
//
//	@Override
//	public String get() {
//		// TODO Auto-generated method stub
//		return getVeryExpensiveValue();
//		}
//	});

	printIfValidIndex(1, () -> getVeryExpensiveValue());
	printIfValidIndex(-1, () -> getVeryExpensiveValue());
	System.out.println((System.currentTimeMillis() - start) / 1000 + "초 걸렸습니다.");

	// 이 값을 얻기까지 엄청난 연산이 필요하다는 것을 의미
	private static String getVeryExpensiveValue() {
		// 이 연산을 수행하기까지 3초나 걸린다고 가정하기 위해 넣은 코드
		try {
			TimeUnit.SECONDS.sleep(3);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		return "Yoggaebi";
	}

	// private static void printIfValidIndex(int number, String value) {
	private static void printIfValidIndex(int number, Supplier<String> valueSupplier) {
		if (number >= 0) {
			System.out.println("The value is " + valueSupplier.get() + ".");
		} else {
			System.out.println("Invalid");
		}
	}


이 코드의 결과는 몇 초가 나올까요? 3초가 나옵니다. ??? 왜 3초가 나올까요?? 아래의 추가 설명을 하겠지만 함수의 결과값이 아닌 람다 표현식을 파라미터로 전달할 경우 Lazy Evaluation이 진행됩니다!!(함수형 프로그래밍을 쓰는 이유 중 하나!)

-> 기존에 Supplier를 사용하지 않는 경우는 printIfValidIndex(1, getValueExpensiveValue()); 호출과 동시에 파라미터에
getValueExpensiveValue()라는 함수가 있는 것을 확인하여 해당 메서드의 결과값을 파라미터로 넣기 위해 메서드 실행전에 
getValueExpensiveValue()가 먼저 실행합니다. 그래서 조건식의 여부와 관계없이 비싼 연산을 수행하는 것이죠.

하지만 Supplier를 통해 함수형 인터페이스를 전달하는 경우에는 자바가 인정하는 파라미터 타입과 값으로 넣었기 때문에
printIfValidIndex(...)가 먼저 수행되고 그다음 조건문에 충족할 경우 함수형 인터페이스의 구현 함수를 호출하기 때문에
불필요한 비싼 연산과 메모리 낭비를 하지 않아도 됩니다.

Lazy Evaluation은 Stream API와 람다식을 모두 이해한 뒤에 설명 드려야 확실하게 이해가 될 것이기 때문에
일단은 간단한 예제를 통해 맛을 보고 다음에 제가 Stream API를 학습하고 머리에 정리가 되었을때
따로 포스팅 할 예정입니다. 아주 깊고, 상세하고, 온갖 구글링을 통해 필요한 내용들만을 추려서!!

해당 포스팅들에도 추가적으로 책을 읽으면서 부족한 내용들을 계속 보충할 것입니다.

Comments