코드를 작성하다 보면 비슷한 로직의 중복 코드가 생길 수 있고, 그로 하여금 관리 지점이 늘어나기 때문에 대부분 리팩터링의 대상으로 취급한다.
함수형 인터페이스를 사용하면 효과적으로 코드의 중복을 줄일 수 있고, 가독성을 높일 수 있다.
- Consumer
매개값은 있고, 반환값은 없다. 매개값을 전달받아 사용하고 아무것도 반환하지 않을 때 사용된다.
이를 소비 (Consume) 한다고 표현한다. accept 추상 메소드를 가지고 있다.
이름 | 기능 | 메소드 |
Consumer | 객체 T를 받아 소비한다. | void accept(T t) |
BiConsumer<T, U> | 객체 T와 U 두가지를 받아 소비한다. | void accept(T t, U u) |
DoubleConsumer | double 값을 받아 소비한다. | void accept(double value) |
IntConsumer | int 값을 받아 소비한다. | void accept(int value) |
LongConsumer | long 값을 받아 소비한다. | void accept(long value) |
ObjDoubleConsumer | 객체 T와 double 을 받아 소비한다. | void accept(T t, double value) |
ObjIntConsumer | 객체 T와 int 를 받아 소비한다. | void accept(T t, int value) |
대표적으로 Stream 의 forEach 메소드의 매개변수 타입이 Consumer 이다.
- Supplier
매개값은 없고, 반환값은 있다. 실행 후 호출한 곳으로 데이터를 공급 (Supply) 한다. getXXX 추상 메소드를 가지고 있다.
이름 | 기능 | 메소드 |
Supplier | T 객체를 반환한다. | T get() |
BooleanSupplier | boolean 값을 반환한다. | boolean getAsBoolean() |
DoubleSupplier | double 값을 반환한다. | double getAsDouble() |
IntSupplier | int 값을 반환한다. | int getAsInt() |
LongSupplier | long 값을 반환한다. | long getAsLong() |
- Function
매개값도 있고, 리턴값도 있다. 주로 매개값을 반환값으로 매핑할 때, 타입 변환이 목적일 때 사용한다.
이름 | 기능 | 메소드 |
Function<T, R> | 객체 T를 객체 R로 매핑한다. | R apply(T t) |
BiFunction<T, U, R> | 객체 T와 U를 객체 R로 매핑한다. | R apply(T t, U u) |
DoubleFunction | double 값을 객체 R로 매핑한다. | R apply(double value) |
IntFunction | int 값을 객체 R로 매핑한다. | R apply(int value) |
IntToDoubleFunction | int 값을 double 값으로 매핑한다. | double applyAsDouble(int value) |
IntToLongFunction | int 값을 long 값으로 매핑한다. | long applyAsLong(int value) |
LongToDoubleFunction | long 값을 double 값으로 매핑한다. | double applyAsDouble(long value) |
LongToIntFunction | long 값을 int 값으로 매핑한다. | int applyAsInt(long value) |
ToDoubleBiFunction<T, U> | 객체 T와 U를 double 값으로 매핑한다. | double applyAsDouble(T t, U u) |
ToDoubleFunction | 객체 T를 double 값으로 매핑한다. | double applyAsDouble(T t) |
ToIntBiFunction<T, U> | 객체 T와 U를 int 값으로 매핑한다. | int applyAsInt(T t, U u) |
ToIntFunction | 객체 T를 int 값으로 매핑한다. | int applyAsInt(T t) |
ToLongBiFunction<T, U> | 객체 T와 U를 long 값으로 매핑한다. | long applyAsLong(T t, U u) |
ToLongFunction | 객체 T를 long 값으로 매핑한다. | long applyAsLong(T t) |
- Operator
Function 과 마찬가지로, 매개값도 있고, 반환값도 있다.
주로 매개값을 연산 (Operation) 하여 결과를 반환할 때 사용된다.
이름 | 기능 | 메소드 |
BinaryOperator | 객체 T와 T를 연산 후 객체 T를 반환한다. | T apply(T t, T t) |
UnaryOperator | 객체 T를 연산 후 T를 반환한다. | T apply(T t) |
DoubleBinaryOperator | 두개의 double 을 연산 후 double 을 반환한다. | double applyAsDouble(double, double) |
DoubleUnaryOperator | 한개의 double 을 연산 후 double 을 반환한다. | double applysDouble(double, double) |
IntBinaryOperator | 두개의 int 를 연산 후 int 을 반환한다. | int applyAsInt(int, int) |
IntUnaryOperator | 한개의 int 를 연산 후 int 을 반환한다. | int applyAsInt(int) |
LongBinaryOperator | 두개의 long 을 연산 후 long 을 반환한다. | long applyAsLong(long, long) |
LongUnaryOperator | 한개의 long 을 연산 후 long 을 반환한다. | long applyAsLong(long) |
- Predicate
매개값은 있고, 반환 타입은 boolean 이다. 매개값을 받아 검사하고 true 혹은 false 를 반환할 때 사용된다. test 추상 메소드를 가지고 있다.
이름 | 기능 | 메소드 |
Predicate | 객체 T를 조사 후 boolean 값을 반환한다. | boolean test(T t) |
BiPredicate<T, U> | 객체 T와 U를 비교 조사 후 boolean 값을 반환한다. | boolean test(T t, U u) |
DoublePredicate | double 값을 조사 후 boolean 값을 반환한다. | boolean test(double value) |
IntPredicate | int 값을 조사 후 boolean 값을 반환한다. | boolean test(int value) |
LongPredicate | long 값을 조사 후 boolean 값을 반환한다. | boolean test(long value) |
아래는 프로젝트 중 옵저버 패턴을 구현하기 위해 사용했던 Function 인터페이스이다.
public abstract class Dispatcher {
protected final Function<DispatchMessage, DispatchMessage> pipe;
DispatcherContext dispatcherContext;
public Dispatcher(DispatcherContext dispatcherContext) {
this.dispatcherContext = dispathcerContext;
this.pipe = collectHandler();
}
public abstract Function<DispatchMessage, DispatchMessage> collectHandler();
public void dispatch(DispatchMessage message) {
pipe.apply( message );
}
}
}
@Component
public class TestDispatcher extends Dispatcher {
public TestDispatcher(DispatcherContext dispatcherContext) {
super(dispatcherContext);
}
@Override
public Function<DispatchMessage, DispatchMessage> collectHandler() {
Function<DispatchMessage, DispatchMessage>
collection = new 1Handler(dispatcherContext);
collection = collection.andThen(new 2Handler(dispatcherContext));
collection = collection.andThen(new 3Handler(dispatcherContext));
collection = collection.andThen(new 4Handler(dispatcherContext));
return collection;
}
}
추상 클래스를 상속받아, 구현체를 만들어준다.
collectHandler의 역할은 Function 인터페이스를 구현할 구현체를 지정해주는 역할이다.
@Component
public class AAAConsumer {
private final TestDispatcher dispatchers;
public AAAConsumer(TestDispatcher dispatchers) {
this.dispatchers = dispatchers;
}
@RabbitListener
public void read(final Message message) {
dispatchers.dispatch( .. );
}
}
옵저버 ( dispather ) 를 호출하는 부분이다.
RabbitMQ 리스너를 이용한 예제이다.
이제 TestDispatcher의 dispatch가 호출됐다.
추상클래스인 Dispatcher에 따라 apply 를 호출할 Function은 collectorHandler에 의해 정해지고,
구현체인 TestDispatcher에서 정의된대로 1~4 핸들러가 순차적으로 호출된다.
위의 예제에서 코드는 생략됐지만 각 핸들러는 RabbitListener에서 받은 message의 type으로 자신이 변수에 맞는 핸들러인지 구분한다.
핸들러가 구분됐다면 apply를 통해 해당 핸들러에서 구현된 로직을 수행하고, 그에 맞는 리턴 결과를 반환한다.(DispatherMessage 또한 추상클래스로 각 핸들러에 맞는 메세지 타입이 구현되어 있다. )
이렇게 옵저버 패턴을 사용한다면 후에 기능을 추가해야 할 때 Dispather를 상속받아 구현체를 만들고 , 그에 맞는 handler를 추가해주면 된다. 다른 기능에는 전혀 영향을 미치지 않는다. 가독성이 뛰어나 쉽게 기능들을 구분하고 확인할 수 있다.
추상클래스의 유연함을 위해 Function 인터페이스를 사용한 경험이였다.
프로젝트 중 특정 어플리케이션에서 사용할 라이브러리를 만들어야 하는 상황이 있었다.
A,B,C 기능을 사용할 수 있어야 하는 어플리케이션에서 C 기능을 따로 라이브러리로 만들어서 모듈을 담당한 인원에게 전달해야 했다.
public class Client {
private final Consumer< 화면캡쳐본 > a;
public RdpClient( Consumer< 화면캡쳐본 > a) {
... 생략
}
// 라이브러리 기능들
public void 마우스이동 ;
public void 키보드이동 ;
}
라이브러리는 특정 화면에서 마우스와 키보드를 이동할 수 있는 기능이 있고, 화면캡쳐본을 0.1초마다 Consumer를 통해 지속적으로 제공해준다.
private void 화면캡쳐전송() {
a.accept(화면캡쳐본);
}
라이브러리 내 화면캡쳐를 담당하는 부분에서 Client를 참조할 수 있고, Client의 Consumer를 accept 한다.
이제 어플리케이션은 해당 기능을 사용하기 위해 라이브러리를 import 하고, Client를 생성해줘야 한다.
import 라이브러리C
public class 어플리케이션 {
//생성자 등등..
public void updateScreenConsumer( 화면캡쳐본 ) {
// 화면캡쳐본으로 로직 수행
}
public static void main(String[] args){
Client client = new Client(
this::updateScreenConsumer )
client.마우스이동
client.키보드이동
.....
}
}
어플리케이션에선 이렇게 Consumer를 등록해 0.1초마다 전송되는 화면 캡쳐본을 받을 수 있게 된다.
함수형 인터페이스는 어플리케이션의 가독성과 유연성을 위해 활용 할 수 있는 곳이 매우 많다.
지속적으로 공부하고 활용해보도록 하자.
'JAVA > 자바' 카테고리의 다른 글
제네릭과 애너테이션 활용기 ( 커맨드 패턴 ) (0) | 2023.07.23 |
---|---|
[Java] 해시 알고리즘이란? (0) | 2023.06.30 |
[Java] Enum, 열거타입 (0) | 2023.06.07 |
[JAVA] SOLID 객체지향 설계 5원칙 (2) | 2022.02.26 |
[JAVA] Static과 Final (0) | 2022.02.26 |