Skip to content

Instantly share code, notes, and snippets.

@ihoneymon
Last active May 15, 2018 01:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ihoneymon/f57abc352585c45e039608b3b5976be8 to your computer and use it in GitHub Desktop.
Save ihoneymon/f57abc352585c45e039608b3b5976be8 to your computer and use it in GitHub Desktop.

[java] 람다식을 이용한 설계

디자인은 어떻게 보이고 느껴지냐의 문제만은 아니다. 디자인은 어떻게 동작하냐의 문제다.

Most people make the mistake of thinking design is what it looks like. People think it’s this veneer – that the designers are handed this box and told, “Make it look good!” That’s not what we think design is. It’s not just what it looks like and feels like. Design is how it works.

— 스티브 잡스

람다 표현식을 사용해서 코드를 작성할 때 필요한 여러가지 설계 아이디어

  • 객체 사용 대신 가벼운 함수형 스타일 사용

  • 람다 표현식을 통해 로직과 함수를 쉽게 분리

  • 책임을 위임(Delegate)

  • 간결하게 데코레이터 패턴 구현(인터페이스에서 정의한 기능을 하위클래스에서 구현)

  • 평범한 인터페이스를 직관적인 인터페이스로 변화

람다를 이용한 문제분리

  • 클래스를 만든 이유는 코드를 재사용하기 위해서이다.

설계 문제 살펴보기

두 개 속성(field)를 가지는 간단한 객체 .Asset

public class Asset {
    public enum AssetType {BOND, STOCK}

    private final AssetType type;
    private final int value;

    public Asset(AssetType type, int value) {
        this.type = type;
        this.value = value;
    }

    public AssetType getType() {
        return type;
    }

    public int getValue() {
        return value;
    }
}

주어진 assets 이 가진 값을 구하는 유틸리티 .AssetUtil

public class AssetUtil {
    public static int totalAssetValue(final List<Asset> assets) {
        return assets.stream()
                .mapToInt(Asset::getValue)  // (1)
                .sum(); // (2)
    }
}
  1. mapToInt() 메서드를 사용해서 스트림으로 매핑

  2. sum() <1>에서 하나의 값으로 도출

람다 표현식을 사용해서 totalAssetValue()를 작성한 좋은 예다. 활용도가 높은 이터레이터를 사용했고 불변성도 유지했다. 그러나 메서드 설계 자체에 초점을 맞춰보면 3가지 문제가 있다.

  • 이터레이션은 어떻게 하는가?

  • 어떤 속성의 합을 계산하는가?

  • 어떻게 속성의 합을 계산하는가?

Warning

이렇게 로직이 뒤엉켜 있으면 재사용성이 떨어진다!!

…​그래? @_@)?

복잡한 문제 다루기

채권(AssetType.BOND, bond 묶여있다) 자산에 대해서만 합계를 구한다고 가정해보자.

totalAssetValue()메서드에는 필요한 내용이 다 구현되어 있다. 메서드를 복사해서 구현하면 되지 않을까?

public static int totalBondAssetsValue(final List<Asset> assets) {
    return assets.stream()
            .mapToInt(asset -> asset.getType() == Asset.AssetType.BOND ? asset.getValue() : 0)
            .sum();
}
Note

그러는 작가는 왜…​ mapToInt() 에서 로직 작성하는데? 라면서 앞을 살펴보니 아직 filter 에 대해서 설명을 안했습니다. 이제~ 설명 들어갑니다.

분리를 위한 리팩토링

이전에 작성한 3개 메서드를 살펴보면 앞에서 언급했던 3가지 문제 중 2가지 문제를 가지고 있다. 고 한다.

  • 이터레이션은 어떻게 하는가?

  • 어떤 속성의 합을 계산하는가?

  • 어떻게 속성의 합을 계산하는가?

public static int totalAssetValue(final List<Asset> assets, final Predicate<Asset> assetSelector) {
    return assets.stream()
            .filter(assetSelector)
            .mapToInt(Asset::getValue)
            .sum();
}

필터(filter) 메서드는 계산하려는 자산(Asset)만을 선택하도록 해준다. 제공된 Predicate.test() 메서드를 호출하여 어떤 것을 계산할지 결정하게 된다.

람다 표현식을 사용하여 메서드와 우리가 고려해야할 문제를 분리했다. 이것은 추가적인 클래스 생성없이 전략 퍼턴에 대한 간단한 사용예다. 이 패턴은 고차 함수 사용이 필요하며 프로그래머는 로직을 고민해야 한다.

Note

람다 표현식을 변수에 저장하고 필요할 때 재사용할 수 있다.

private Predicate<Asset> bondAssetFilter = asset -> asset.getType() == Asset.AssetType.BOND;
private Predicate<Asset> stockAssetFilter = asset -> asset.getType() == Asset.AssetType.STOCK;

@Test
public void testBondAssetFilter() {
    List<Asset> bondAssets = assets.stream().filter(bondAssetFilter).collect(Collectors.toList());

    bondAssets.forEach(asset -> assertThat(asset.getType()).isEqualTo(Asset.AssetType.BOND));
}

@Test
public void testStockAssetFilter() {
    List<Asset> bondAssets = assets.stream().filter(stockAssetFilter).collect(Collectors.toList());

    bondAssets.forEach(asset -> assertThat(asset.getType()).isEqualTo(Asset.AssetType.STOCK));
}

재사용 측면에서 보면 위임(델리게이트, Delicate)는 상속보다 더 좋은 설계다. 정말?

  • 다양한 구현을 쉽게 만들 수 있으며 동적으로 여러가지 행위(Behavior)를 추가할 수 있다.

    • 클래스 의존적인 행위가 좀 더 독립적인 다양화가 가능하다.

    • 클래스 구조를 고려하지 않아도 되어 설계 자체가 좀 더 유연해진다.

Function 인터페이스

Java 1.8에 추가된 함수형 인터페이스(Functional Interface)

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}
'함수형 인터페이스’란?

Functional, Consumer, SupplierPredicate 등등을 살펴보면 @FunctionalInterface가 선언되어 있고 추상메서드가 하나 있는 공통점을 가진다. 이러한 인터페이스를 함수형 인터페이스라고 한다. 경우에 따라 많은 정적 메서드를 가지고 있지만 크게 상관없다.

@FunctionalInterface로 인터페이스를 선언했지만 실제로 함수형 인터페이스가 아니면 '추상메서드가 여러개 있다(Multiple nonoverriding abstract methods found in interface)'고 컴파일 에러가 발생한다고 한다.

Note

람다 표현식 내부에서 처리하는 것보다 예외 처리를 어디서 해야하는지 그 위치를 정해야할 필요가 있다. 이렇게 하면 예외 처리를 다시 발생시켜 업스트림에서 처리할 수 있도록 할 수 있다.

'디폴트 메서드(Default method)'란?

자바8에서 호환성을 유지하면서 API를 바꿀 수 있도록 추가한 새로운 기능이다. 이제 인터페이스는 자신을 구현하는 클래스에서 메서드를 구현하지 않을 수 있는 새로운 메서드 시그니처를 제공한다. 그럼 디폴트 메서드는 누가 구현할까? 인터페이스를 구현하는 클래스에서 구현하지 않은 메서드는 인터페이스 자체에서 기본으로 제공한다(그래서 디폴트 메서드라고 부른다).

Note
예외, 람다, 함수형 인터페이스의 관계

함수형 인터페이스는 확인된예외(CheckedExeption)를 던지는 동작을 허용하지 않는다. 즉, 예외를 던지는 람다 표현식을 만들려면 학인된 예외를 선언하는 함수형 인터페이스를 직접 정의하거나 람다를 try-catch 블록으로 감싸야 한다.

확인된예외를 던지는 인터페이스 예
@FunctionalInterface
public interface BufferedReaderProcessor {
  String process(BufferReader b) throws BizmoneyApproveException;
}
BufferedReaderProcessor p = (BufferReader br) -> br.readLine();

기본제공되는 함수형 인터페이스를 기대하는 API를 사용하고 있기 때문에 직접 함수형 인터페이스를 만들기 어려우니 다음과 같이 사용할 것이다.

Function<BufferedReader br, String> f = (BufferedReader b) -> {
  try {
    return b.readLine();
  } catch(IOException e) {
    throw new RuntimeException(e);
  }
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment