본문 바로가기
Java/Java

함수형 인터페이스와 람다식 + 전략패턴

by oneny 2023. 4. 19.

 

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

 

함수형 인터페이스란?

함수형 인터페이스(Fuctional Interface)란, 하나의 추상 메서드(abstract method)만을 갖는 인터페이스를 말한다. 

그리고 람다식은 이러한 함수형 인터페이스를 기반으로 작성이 가능하다.

 

왜 생겼을까?

위에서 말한 것처럼 함수형 인터페이스를 사용하는 이유는 람다식은 함수형 인터페이스로만 접근이 가능하기 때문이다.

사진에서 위 메서드는 Comparator의 추상 메서드이다. 그리고 아래는 람다식인데 람다식의 매개변수는 연관된 인터페이스의 메서드, 즉 추상 메서드에 의해 결정된다. 따라서 보는 것과 같이 Comparator의 경우, compare 메서드를 사용하기 때문에 두 개의 인수가 있고, 람다식을 사용할 때 매개변수로 o1, o2를 받게 된다.

이렇게 인터페이스 익명 구현 객체 보다 람다식로 작성하는 것이 생산성과 가독성을 높일 수 있다는 장점이 있다.

 

자바의 대표적인 함수형 인터페이스

인터페이스 분류 추상 메서드 목적
Consumer<T> void accept(T t) 반환하는 데이터 없이 메서드 실행
BiConsumer void accept(T t, U u)
Function<T, R> R apply(T t) 1개의 인수를 받아
R 타입의 데이터 반환
BiFunction<T, U, R> R apply(T t, U u)
UnaryOperator<T> T apply(T t) 1개의 인수를 받아
인수와 같은 타입의 값을 반환
BinaryOperator<T> T apply(T t, T t)
Predicate<T> boolean test(T t) 조건이 true나 false인지 테스트
BiPredicate<T, R> boolean test(T t, U u)
Supplier<T> T get() 인수없이
해당 클래스의 객체를 생성할 때 사용

 

함수형 인터페이스 어노테이션

@FunctionalInterface
public interface GameStrategy {
  boolean movable();
}

@FunctionalInterface 어노테이션은 자바8에 추가된 어노테이션으로 의도적으로 해당 인터페이스가 하나의 추상 메서드만 가지는 함수형 인터페이스라고 명시하고 적합하지 않은 메서드를 추가하는 일을 방지할 수 있다.

따라서, 인터페이스 검증과 유지보수를 위해 붙여주는 것이 좋다.

 

람다식(Lambda Expression)

 

왜 생겼을까?

불필요한 코드를 줄이고, 가독성을 높이기 위함이다.

함수형 인터페이스를 인터페이스 익명 구현 객체로 작성한 것도 구현체를 작성해야하는 일을 줄이는 것이지만 그럼에도 불구하고 가독성 면에서는 좋지 못했다. 하지만 람다식으로 작성하면 개발자의 의도를 좀 더 명확히 드러내 가독성을 높일 수 있고, 함수를 만드는 과정없이 한 번에 처리할 수 있어 생산성이 높아진다.

 

람다식으로 표현할 수 없는 경우

interface EnhancedComparator<T> extends Comparator<T> {
  int secondLevel(T o1, T o2);
}

var comparatorMixed = new EnhancedComparator<Person>() {
  @Override
  public int secondLevel(Person o1, Person o2) {
    int result = o1.lastName().compareTo(o2.lastName());
    return ((result == 0) ? secondLevel(o1, o2) : result);
  }

  @Override
  public int compare(Person o1, Person o2) {
    return o1.firstName().compareTo(o2.firstName());
  }
};

위와 같은 경우에는 인터페이스로 작성할 수 있을까? No!!

람다식은 함수형 인터페이스가 아닌 경우에는 표현할 수 없다. 함수형 인터페이스는 하나의 메서드만 가지고 있어 람다식과 인터페이스가 1:1로 연결될 수 있다. 만약 여러  추상 메서드를 갖는다면 어떤 추상 메서드를 선택해야하는지 알 수 없기 때문에 람다식으로 표현할 수 없다.

 

함수형 인터페이스와 람다식을 활용한 전략 패턴

객체들이 할 수 있는 행위 각각에 대해 전략 클래스를 생성하고, 유사한 행위들을 캡슐화하는 인터페이스를 정의하여 객체의 행위를 동적으로 바꾸고 싶은 경우 직접 행위를 수정하지 않고 전략을 바꿔주기만 함으로써 행위를 유연하게 확장하는 방법을 말한다.

간단히 말해서 객체가 할 수 있는 행위들 각각을 전략으로 만들어 놓고, 동적으로 행위의 수정이 필요한 경우 전략을 바꾸는 것만으로 행위의 수정이 가능하도록 만든 패턴이다.

 

전략 패턴 사용 이유

자동차 경주 미션에서 '전진하는 조건은 0에서 9 사이에서 random 값을 구한 후 random 값이 4 이상일 경우이다'라고 요구사항에 명시되어 있다. 그러다 미션 중간에 돌발 미션으로 전진하는 조건을 바꾸게 되면 어떻게 될까? RacingGameStrategy의 movable() 메서드를 직접 바꿔줘야 할까? 만약 그렇게 수정하면 SOLID 원칙 중 OCP(Open-Closed Principle)에 위배된다. OCP에 의하면 기존의 movable()을 수정하지 않으면서 행위가 수정되어야 한다. 그리고 자동차 경주가 아닌 택시 경주, 자가용 경주, 버스 경주, 오토바이 경주로 시스템이 커져 확장 될 경우에 movable() 메서드를 사용함에 따라 메서드 중복 문제가 발생할 수 있다. 지금까지의 얘기를 정리하면 아래와 같다.

  • OCP(Open-Closed Principle) 위배
  • 시스템이 커져서 확장이 될 경우 메서드의 중복문제 발생

따라서 이를 해결하고자 전략 패턴을 사용하면 된다.

 

전략 패턴 구현

위 그림을 보는 것처럼 시스템이 유연하게 변경되고 확장될 수 있도록 자동차가 전진하는 조건을 GameStrategy라는 인터페이스를 통해 전략 패턴을 사용해보자. 그리고 이를 구현한 RacingGameStrategy는 '0에서 9 사이에서 random 값을 구한 후 random 값이 4 이상일 경우 true를, 아닌 경우 false를 반환'하도록 구현했다. 만약 다른 전략으로 바뀐다라고 가정했을 때를 구현한 OtherGameStrategy는 단순히 boolean 형의 랜덤값을 통해 전진하도록 구현했다.

 

이를 코드로 표현하면 다음과 같다.

@FunctionalInterface
public interface GameStrategy {
  boolean movable();
}
public class RacingGameStrategy implements GameStrategy {
  private static final int RANDOM_RANGE = 10;
  private static final int MOVABLE_CRITERIA = 4;
  private static final Random random = new Random();

  @Override
  public boolean movable() {
    return randomNumber() >= MOVABLE_CRITERIA;
  }

  private int randomNumber() {
    return random.nextInt(RANDOM_RANGE);
  }
}
public class RacingGameStrategy implements GameStrategy {

  private static final Random random = new Random();

  @Override
  public boolean movable() {
    return random.nextBoolean();
  }
}

 

GameStrategy를 사용하는 Car 클래스

  public void forward(GameStrategy racingStrategy) {
    if (racingStrategy.movable()) {
      distance++;
    }
  }

그리고 기존에 boolean 타입의 movable을 인자로 받고 있었다면 위 GameStrategy 타입으로 리팩토링하여 movable 메서드를 통해 앞으로 전진할 수 있는지 결정할 수 있도록 구성했다. 이런 식의 패턴으로 작성하면 나중에 앞으로 전진하는데 다른 전략이 사용되는 경우에 다른 GameStrategy의 구현체만 바꿔주면 되기 때문에 다형성도 챙길 수 있다. 

 

 

CarTest

 

@Test
@DisplayName("Car 객체 전진 테스트")
public void forward() {
  car.forward(() -> true);
  car.forward(() -> true);
  car.forward(() -> true);

  assertThat(car.distance()).isEqualTo(3);
}

그리고 TDD 작성할 때도 forward 인자가 GameStrategy가 아니라면 익명 구현 객체를 작성해줘야 했지만

위 코드 () -> true처럼 람다식으로 작성하는 경우 GameStrategy 함수형 인터페이스의 추상 메서드와 1대1로 연결되어 true 값을 반환하는 구현 메서드가 실행되도록 쉽게 작성할 수 있었다! :)