티스토리 뷰

반응형

Functional Interfaces from Google

최근에 다시 함수형 인터페이스 (람다와는 다른) 추상메소드 인터페이스에 관심이 많아졌습니다. 생각보다 서비스 개발을 하게 되면서 잘 안쓰게 되고 되려 메소드 방식의 내용이 익숙하고 편한지라 그런 내용들을 자주 사용하곤 하였는데요, 이번 글에서는 이제부터라도 함수형 인터페이스를 좀 더 제대로 써보고 싶은 마음에 글을 작성하게 되었습니다. 

목적 : 함수형 인터페이스가 무엇이 있는지 파악하고, 이를 실제 함수에 적용해본다.

 

1. 가장 먼저 함수형 인터페이스를 왜 사용하는지부터 알아보려 합니다. 

기본적으로 가장 많이 사용하고 있는 자바의 함수형 인터페이스는 람다 표현식입니다. 화살표 함수라고 하는 이 녀석의 기본 꼴은 아래와 같습니다. 

public interface FunctionUtil {
    public abstract void print(String str);
}

public void printLog(String print) {
    FunctionUtil function = str -> logger.info(str);
    function.print(print);
}

말그대로 화살표 모양처럼 생겼다고 하여 화살표함수입니다. 이를 해체하면 복잡한 오버라이딩 함수로 풀립니다. 

public void printLog(String text) {
    FunctionUtil func = new FunctionUtil() {
        @Override
        public void print(String str) {
            logger.info(str);
        }
    }
    func.print(text);
    func.print("Hello world"); // Hello world
}

이렇게 복잡하게 표현할 필요가 없기 때문에 인터페이스 함수라는 것으로 대체합니다. 

그리고 함수형 인터페이스는 매번 람다표현식과 메서드별로 구현해주지 않아도 공통으로 사용 가능합니다. 

 

 

2. 함수형 인터페이스 종류

많이 없습니다. 크게 5가지 입니다. 

  • Runnable : Thread 를 쓰시면 많이 보셨을 그 인터페이스
  • Supplier : 파라미터 없이 생산만 하는 인터페이스 (getter)
  • Consumer : 파라미터 넣으면 소비만 하는 인터페이스 (setter)
  • Function : 파라미터가 2개이고, 앞에 타입을 넣으면 뒤의 타입을 반환해주는 형태의 인터페이스
  • Predicate : 파라미터를 넣으면 true, false 로 리턴하는 인터페이스

 

3. 각 인터페이스의 형태

Runnable

1. Runnable 을 알아봅시다. Runnable 은 철칙이 있습니다. 

  1. 파라미터가 없다. 리턴도 없다. 
  2. 모든 것은 run() 으로 끝난다. 

쉽게 말해 실행만 시켜주는 함수형 인터페이스입니다. 즉, void 를 반환합니다. 

public static <T> void run(Runnable runnable) {
    runnable.run();
}

그래서 실행을 위해 예시를 들어보겠습니다. 

public class CommandStep {
    private Step step1;
    private Step step2;
    private StopExecution stop;

    public void command() {
        try {
            if(step1.run() || step2.run()) { // step1, step2 를 같이 실행함, 반환값 없음
                  ...
            }
        } catch(RuntimeException re) { 
            this.stop.execute(); // 중지 프로세스 실행
            return;
        }
    }
}

public void execute(CommandStep step) {
    Runnable runnable = () -> step.command(); // CommandStep.command 에 대해 실행
    runnable.run();
}

 

 


Supplier

2. Supplier 는 파라미터가 없이 Getter 만 존재합니다.

public TestDTO create() {
    Supplier<TestDTO> supplier = () -> new TestDTO(UUID.randomUUID());
    return supplier.get();
}

get() 이라는 메서드를 사용하여 파라미터가 없는 기본 생성자를 만들거나, 특정 변수값을 넣어서 사용하곤 합니다. 

아래에서 Predicate 와 보다 자세히 다뤄보겠습니다.

 


Consumer

3. Consumer 는 Runnable과는 다르게 파라미터가 있지만, 리턴이 없습니다.

Consumer 는 아래와 같은 성질이 있습니다. 

  1. accept, andThen 함수 2가지로 이루어져 있다. 
  2. andThen을 활용하여 두 개 이상의 Consumer 를 연속적으로 실행할 수 있다.

 

아래와 같은 형태로 함수를 만들어 보겠습니다.

public static <T> void ifPresent(T t, Consumer<T> func) {
    if(t != null) {
        func.accept(t);
    }
}

위의 함수는 기본적으로 첫번째 파라미터에 타입 변수를 받고, 해당 변수가 NullSafe 할 때 두번째 파라미터로 입력받은 인터페이스 함수를 실행시켜주는 조건이 됩니다. 예를 들어 이렇게 실행시킬 수 있습니다. 

@Setter
public class TestDTO {
    private Long id;
    private String testVal;

    public void setValues(Long id, String testVal) {
        TestDTO dto = new TestDTO();
        Consumer<Long> setArgument = longValue -> dto.setId(longValue);
        Consumer<String> setValue = str -> dto.setTestVal(str);
        
        doConsume1(id, setArgument);
        doConsume1(testVal, setValue);
    }
    
    public void doConsume1(T t, Consumer<T> consume) {
        ifPresent(t, consume);
    }   
}

단, 여기 위에서 doConsume1(testVal, setArgument) 의 경우는 Syntax 에러가 발생합니다. 타입이 다르기 때문입니다. 

앞에서 말씀드린 Runnable, Consumer를 사용하여 더 적극적으로 활용해보고자 합니다.

public static <T> void doFunctionOrElseRun(T t, Consumer<T> consume, Runnable runnable) {
    if(t == null) { // 첫번째 파라미터가 null 인 경우
        runnable.run(); // Runnable 을 실행한다.
    } else {
        consume.accept(t); // Consumer 를 실행한다.
    }
}

그리고 2번에서 말씀드린대로 andThen도 실행 가능합니다. 단, 파라미터의 타입은 동일해야 합니다. 

@Setter
public class TestDTO {
    private long id;
    private String testVal;
    private String name;
    
}
public void setValues(String testVal) {
    TestDTO dto = new TestDTO();
    Consumer<String> setName = name -> dto.setName(name);
    Consumer<String> setValue = str -> dto.setTestVal(str);

    setName.andThen(setValue).accept(testVal);
    setName.andThen(setValue).accept("My Name");
}

 


4. Function 은 다음과 같은 특성을 가집니다. 

  1. 2개의 타입 변수를 받아 1번째 파라미터가 입력값, 2번째 파라미터가 출력값으로 쓰인다.
    1. 예를 들어, Function<AClass, BClass> 이면 apply로 리턴받는 값은 항상 BClass 이다.
  2. apply 로 기본 실행을 시키고, compose, andThen 이라는 함수로 전후를 정할 수 있다. 
  3. identity 함수는 1번째 값을 반환시킨다. 

Function의 apply, compose

Function 은 4가지 함수를 가지고 있기에 내용이 조금 깁니다. 

Function 의 andThen, identity()

Function 함수는 무궁무진하게 사용할 수 있습니다. 기본적으로 함수 자체에서 제공하는 추상 메소드가 다양하게 존재하다보니, 취향에 맞게 조절하여 사용하는 것이 가능합니다. 저는 대표적으로 형변환을 할 때 많이 사용합니다.

public String toComma(Long value) {
    Function<Long, String> format = (val) -> new DecimalFormat("#,###").format(val);
    return format.apply(value);
}

기본적으로 위의 함수를 통해 형변환이 가능합니다. 

Function 함수는 아래와 같이 Optional과 같이 혼용해서 사용도 가능합니다. 

public static <T, R> R getNullSafe(Optional<T> t, Function<T, R> func, R defaultValue) {
    return t.isEmpty() ? defaultValue : func.apply(t.get());
}

5. Predicate 는 파라미터 타입을 받아 true, false 를 변환해주는 함수형 인터페이스 입니다. 다른 인터페이스와는 다르게 내용이 너무 길기에 그냥 코드를 가져와봤습니다.

@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);

    /**
     * Returns a composed predicate that represents a short-circuiting logical
     * AND of this predicate and another.  When evaluating the composed
     * predicate, if this predicate is {@code false}, then the {@code other}
     * predicate is not evaluated.
     *
     * <p>Any exceptions thrown during evaluation of either predicate are relayed
     * to the caller; if evaluation of this predicate throws an exception, the
     * {@code other} predicate will not be evaluated.
     *
     * @param other a predicate that will be logically-ANDed with this
     *              predicate
     * @return a composed predicate that represents the short-circuiting logical
     * AND of this predicate and the {@code other} predicate
     * @throws NullPointerException if other is null
     */
    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    /**
     * Returns a predicate that represents the logical negation of this
     * predicate.
     *
     * @return a predicate that represents the logical negation of this
     * predicate
     */
    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    /**
     * Returns a composed predicate that represents a short-circuiting logical
     * OR of this predicate and another.  When evaluating the composed
     * predicate, if this predicate is {@code true}, then the {@code other}
     * predicate is not evaluated.
     *
     * <p>Any exceptions thrown during evaluation of either predicate are relayed
     * to the caller; if evaluation of this predicate throws an exception, the
     * {@code other} predicate will not be evaluated.
     *
     * @param other a predicate that will be logically-ORed with this
     *              predicate
     * @return a composed predicate that represents the short-circuiting logical
     * OR of this predicate and the {@code other} predicate
     * @throws NullPointerException if other is null
     */
    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    /**
     * Returns a predicate that tests if two arguments are equal according
     * to {@link Objects#equals(Object, Object)}.
     *
     * @param <T> the type of arguments to the predicate
     * @param targetRef the object reference with which to compare for equality,
     *               which may be {@code null}
     * @return a predicate that tests if two arguments are equal according
     * to {@link Objects#equals(Object, Object)}
     */
    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }

    /**
     * Returns a predicate that is the negation of the supplied predicate.
     * This is accomplished by returning result of the calling
     * {@code target.negate()}.
     *
     * @param <T>     the type of arguments to the specified predicate
     * @param target  predicate to negate
     *
     * @return a predicate that negates the results of the supplied
     *         predicate
     *
     * @throws NullPointerException if target is null
     *
     * @since 11
     */
    @SuppressWarnings("unchecked")
    static <T> Predicate<T> not(Predicate<? super T> target) {
        Objects.requireNonNull(target);
        return (Predicate<T>)target.negate();
    }
}

test 는 기본 함수이며 비교에 따라 true, false 를 반환해줍니다. 아래의 코드로 쉽게 이해가 가능합니다. 

public boolean isNegative(Integer a) {
     Predicate<Integer> negative = num -> num % 2 == 1;
     return negative.test(a);
}

Predicate 는 연결해서 사용하기도 용이합니다. 2개의 Predicate 를 연결하는 함수는 and, or 입니다. and 는 연결된 내용의 두 명제가 모두 참일때 true를, or은 두 명제중 하나만 참이어도 true 를 리턴합니다. 

isNegative.test(1); == true
isNegative.and(isPositive).test(1);    == false
isNegative.and(isLessThanTen).test(7);  == true
isNegative.or(isLessThanTen).test(11);  == true

isEquals 함수는 static 메서드로 사용 가능합니다. 

Predicate<Long> isFiveBillion = Predicate.isEqual(5_000_000_000);
isFiveBillion.test(1_000_000_000); // false
isFiveBillion.test(5_000_000_000); // true

 

Predicate 는 대표적으로 QueryDSL 의 BooleanBuilder와 같이 대표적으로 많이 사용하는 함수형 인터페이스입니다. 

private Predicate today(RequestDTO search) {
    if(search.isToday()) {
        LocalDate currentDate = LocalDate.now();
        LocalDate nextDay = currentDate.plusDays(1);
        return entityClass1.updateTime.between(currentDate.atStartOfDay(), nextDay.atStartOfDay());
    }
    return null;
}

 

그리고 위에서 사용한 Supplier와 혼용하여 10번 시도하는 함수를 만들수도 있습니다. stopCondition 을 파라미터로 받아 해당 컨디션이 작동되었을 때까지 수행하는 메서드입니다.

public static <R> R exec(Supplier<R> supplier, Predicate<R> stopCondition) {
    int MAX = 10;
    int cnt = 0;
    while (true) {
        R r = supplier.get();
        if(stopCondition.test(r)) {
            return r;
        }
        ++cnt;
        if(MAX < cnt) {
            throw new RuntimeException("재시도 %d 번 진행하였으나 실패하였습니다.".formatted(MAX));
        }
    }
}

 

4. 한 화면에 요약하기

Search from Google

 

오늘은 함수형 인터페이스에 대해 정의하고, 실제로 사용하기 위해 메서드화까지 진행해보는 시간을 가졌습니다. 찾아보니 이전에 작성한 글도 존재하여 그것도 링크로 남기게 되었어요.

https://abbo.tistory.com/11

 

Lambda와 Stream(2)

Author: 주니용 아래 소스는 Java8을 기준으로 작성되었습니다. 혹시 오타가 있거나 잘못 이해한 부분이 있으면 댓글로 적어주세요 :) 글(1) 에서 기본적으로 정의를 했다면 이번에는 어떻게 구체적

abbo.tistory.com

 

앞으로는 더 단련하여 함수형 인터페이스를 더 효율적으로 쓸 수 있는 그 날이 오길..!!

반응형
댓글
공지사항