중복되는 코드 중에서 중복을 제거하기 어려운 로직을 제거하기 위해 알아보던 중
AOP 처럼 공통적인 기능을 한 곳에서 관리하고, 핵심 기능만을 분리할 수 없을까에 대해 고민해보다가
함수형 인터페이스에 대해 알게 되어 대표적인 함수형 인터페이스와 사용 방법에 대해 정리해보자 한다.
함수형 인터페이스란?
자바 8부터 도입된 개념으로, 이름 그대로 "함수"를 위한 인터페이스다.
즉, 이 인터페이스를 구현한 클래스는 하나의 함수 처럼 동작하게 된다.
이 함수처럼 동작한다는 것은 입력을 받아서 출력을 내보는데
예를 들어, Function<T, R> 이라는 함수형 인터페이스는 T라는 타입을 입력 받아 R이라는 타입의 출력한다.
그렇다면 함수형 인터페이스를 구현한 클래스는 T라는 타입의 입력을 받아 R이라는 타입을 반환하게 되는 것이다.
public class StringToIntConverter implements Function<String, Integer> {
@Override
public int apply(String s) {
return Integer.parseInt(s);
}
}
이 뿐 아니라 함수형 인터페이스를 메서드의 매개변수로 사용하면 특정 동작을 인자로 전달할 수 있다.
대표적인 예로 List 컬렉션의 sort 메서드가 Comparator 함수형 인터페이스를 매개변수로 받아 정렬을 수행한다.
// 인터페이스 함수를 재정의해 사용
List<String> names = Arrays.asList("John", "Jane", "Tom", "Alice");
names.sort(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
// 람다 표현식
List<String> names = Arrays.asList("John", "Jane", "Tom", "Alice");
names.sort((o1, o2) -> o1.compareTo(o2));
함수형 인터페이스 종류
함수형 인터페이스는 크게 5가지로 Consumer, Supplier, Function, Operator, Predicate가 있다.
함수형 인터페이스의 특징으로는 각각 한개의 추상 메서드만을 갖고 있다는 특징이 있다.
또 이 인터페이스들은 여러 인터페이스들로 나뉘는데 이 큰 종류의 인터페이스 중 4가지에 대해서만 알아보고자 한다.
함수형 인터페이스 이름 | 설명 | 추상 메서드 |
Consumer<T> | 객체 T를 받아 소비한다. | void accept(T t) |
Supplier<T> | 객체T를 반환한다. | T get() |
Function<T, R> | 객체T를 객체 R로 매핑한다. | R apply(T t) |
Predicate<T> | 객체T를 받아 boolean을 반환한다. | boolean test(T t) |
이 포스팅에서는 내가 이번에 적용한 메서드 파라미터에 함수형 인터페이스를 넘겨 사용하는 방법에 대해 설명하려 한다.
Consumer<T>
한 개의 입력을 받아서 결과를 반환하지 않는 함수를 정의한다.
void accept(T t) 메서드를 가지며, 이 메서드는 매개변수 T를 받아서 아무런 결과도 반환하지 않는다.
주로 입력값을 이용한 연산이나 출력 등의 동작에 사용된다.
// Consumer를 파라미터로 받는 메서드 정의
public static void printList(Consumer<String> consumer, List<String> list) {
for (String item : list) {
consumer.accept(item);
}
}
// 메서드 호출
printList(System.out::println, Arrays.asList("Apple", "Banana", "Cherry"));
// 출력:
// Apple
// Banana
// Cherry
Supplier<T>
입력 없이 결과를 반환하는 함수를 정의한다.
T get() 메서드를 가지며, 이 메서드는 아무런 매개변수를 받지 않고 결과 T를 반환한다.
주로 입력 받는 방식을 경정해 파라미터 없이 특정 결과를 생성하는데 사용된다.
아래 예제에서는 print() 메소드가 Supplier<String>를 매개변수로 받는다.
이 Supplier는 자신의 추상메서드 get()을 호출하고 있다.
그리고 이 메서드를 호출 시에 람다식을 이용해 문자열을 출력하고 있다.
// Supplier를 파라미터로 받는 메서드 정의
public static void print(Supplier<String> supplier) {
System.out.println(supplier.get());
}
// 메서드 호출
print(() -> "안녕하세요!");
// 출력: 안녕하세요!
Function<T, R>
한 개의 입력을 받아서 결과를 매핑하여 반환하는 함수를 정의한다.
R apply(T t) 메서드를 가지며, 이 메서드는 매개변수 T를 받아서 R타입의 결과를 반환한다.
주로 T 타입의 객체를 받아 다른 형태R로 변환하는데 사용된다.
아래 예제에서는 print() 메소드가 Function<Integer, String>를 매개변수로 받는다.
이 Function은 자신의 추상 메서드 apply()에 정수 값을 받는다.
그리고 이 메서드를 호출할 때 생성자 참조 방식을 이용해서 정수 값을 문자열로 변환하고 변환된 문자열을 출력하고 있다.
// Function 파라미터로 받는 메서드 정의
public static void print(Function<Integer, String> function, int value) {
System.out.println(function.apply(value));
}
// 메서드 호출
print(Object::toString, 5);
// 출력: 5
Predicate<T>
한 개의 입력을 받아서 boolean 결과를 반환하는 함수를 정의한다.
boolean test(T t) 메서드를 가지며, 이 메서드는 매개변수 T를 받아서 boolean 결과를 반환한다.
주로 객체를 조건에 따른 필터링이 필요할 때 사용한다.
아래에서는 print() 메소드가 Predicate<Integer>를 매개변수로 받는다.
이 Predicate는 자신의 추상 메서드 test()에 정수 값을 받아서 이 값이 특정 조건을 만족하는지를 판단한다.
그리고 이 메서드를 호출할 때 람다식을 이용해 조건을 만족하면 value값이 출력된다.
// Predicate를 파라미터로 받는 메서드 정의
public static void print(Predicate<Integer> predicate, int value) {
if (predicate.test(value)) {
System.out.println(value);
}
}
// 메서드 호출
print(x -> x > 5, 10);
// 출력: 10
함수형 인터페이스 활용
지금까지 함수형 인터페이스들의 사용법에 대해 간단하게 알아보았다.
이번 목차에서는 이 함수형 인터페이스를 활용해 코드의 유연성과 재사용성을 높이는 방법에 대해 설명하려한다.
필자가 이 함수형 인터페이스에 대해 알아보게 된 계기는 다음과 같다.
public class InputHandler {
...
public static String receiveValidatedName() {
while (true) {
try {
String input = InputView.inputName();
Vaildator.validateName(input);
return input;
} catch (IllegalArgumentException exception) {
OutputView.printErrorMessageFor(exception);
}
}
}
public static int receiveValidatedAge() {
while (true) {
try {
String input = InputView.inputAge();
Vaildator.validateAge(input);
return Integer.parseInt(input);
} catch (IllegalArgumentException exception) {
OutputView.printErrorMessageFor(exception);
}
}
}
}
위 코드를 보면 두 메서드는 거의 비슷한 구조를 갖고 있다.
while문과 try ~ catch문은 반복되어 작성되어 있고, try ~ catch 내부의 핵심 로직만 코드가 다르다.
이 핵심 로직이 try ~ catch 내부 블럭안에 감싸져 있을 뿐 아니라 검증 방식과 반환 값의 타입이 달라 메서드로 추출해 중복을 제거하기도 어렵다.
AOP 처럼 공통적인 기능을 한 곳에서 관리하고, 핵심 기능만을 분리할 수 없을까 ?
바로 여기서 함수형 인터페이스를 사용해 함수로 추상화하여 재사용가능한 코드를 만들 수 있었다.
public class InputHandler {
...
// function.identity()는 입력받은 값을 그대로 반환하는 함수를 반환
public static String receiveValidatedName() {
return receiveValidatedInput(InputView::inputName, Validator::validateName, Function.identity());
}
public static int receiveValidatedAge() {
return receiveValidatedInput(InputView::inputAge, Validator::validateAge, Integer::parseInt);
}
private static <T> T receiveValidatedInput(Supplier<String> inputSupplier, Consumer<String> validator, Function<String, T> resultConverter) {
while (true) {
try {
String input = inputSupplier.get();
validator.accept(input);
return resultConverter.apply(input);
} catch (IllegalArgumentException exception) {
OutputView.printErrorMessageFor(exception);
}
}
}
}
receiveValidatedInput() 메서드 작동 방식
- Supplier<String> inputSupplier
입력을 받는 방식을 결정한다.
이 함수를 호출하면 사용자로부터 입력을 받아서 입력 결과를 문자열로 반환한다. - Consumer<String> validator
입력값을 이용해 검증하는 방식을 결정한다.
이 함수를 호출하면 검증 클래스(Validator)를 통해 입력한 문자열이 유효한지 검사한다.
만약 유효하지 않다면 검증 클래스의 메서드validateName(), validateAge()에 정의 된 예외를 발생시킨다. - Function<String, T> resultConverter
반환 값의 변환 방식을 결정한다.
이 함수를 호출하면 입력된 문자열을 생성자 참조 방식으로 적절한 타입의 객체로 변환하여 반환한다.
receiveValidatedName() 메서드 작동 방식
receiveValidatedInput() 메서드를 호출하면서, 각각의 매개변수로 함수를 전달하고 있다.
- 이름을 입력받는 방식(InputView::inputName)
- 이름을 검증하는 방식(Validator::validateName)
- 그리고 문자열을 그대로 반환하는 방식(Function.identity())을 전달한다.
receiveValidatedAge() 메서드 작동 방식
receiveValidatedInput() 메서드를 호출하면서, 각각의 매개변수로 함수를 전달하고 있다.
- 나이를 입력받는 방식(InputView::inputAge)
- 나이를 검증하는 방식(Validator::validateAge)
- 그리고 문자열을 정수로 변환하는 방식(Integer::parseInt)을 전달한다.
이렇게 함수형 인터페이스를 활용하면 receiveValidatedInput 메서드는 전달된 함수에 따라서 다르게 동작한다.
따라서 receiveValidatedName()와 receiveValidatedAge()는 중복된 코드 없이 각각 이름과 나이를 입력받는 동작을 수행할 수 있게 된다.
'◼ JAVA' 카테고리의 다른 글
[Java] 개행 문자 사용시 주의점 (OS별 개행문자 통일하는 법) (1) | 2024.04.11 |
---|---|
[Java] 함수 파라미터에 final 키워드를 꼭 붙여야 할까? (0) | 2024.04.11 |
체크 예외(Exception)와 언체크 예외(RuntimeException) (0) | 2023.04.25 |
JSP(JavaServer Pages)와 Servlet(서블릿)이란? (0) | 2023.03.08 |
[Java/자바] java.time 패키지 (날짜와 시간 다루기) (0) | 2023.01.17 |