본문 바로가기
Programing/Java

[자바8 람다의 힘] 4장 람다 표현식을 이용한 설계

by Tomining 2016. 2. 10.
이 글은 "자바 8 람다의 힘" 책을 정리한 글입니다.

Chapter 4. 람다 표현식을 이용한 설계

앞에서 다른 내용들만 봐도 람다를 이용하여 코드를 좀 더 간결하고 읽기 쉽게 작성할 수 있다는 것을 알았다.
이 장에서는 람다 표현식을 사용하여 여러가지 패턴이나 설계들을 구현하는 방법에 대해서 알아보자.

1. 전략패턴(Strategy Pattern)

클래스를 생성하는 이유 중 하나는 코드를 재사용하기 위함이다. 좋은 의도이긴 하나 항상 그런 것은 아니다.

자산의 총합을 구하는 예제를 통해 클래스 내의 코드 재사용성을 향상시켜 보자.
먼저 Asset 클래스를 생성한다.

public class Asset {
public enum AssetType { BOND, STOCK };
private final AssetType type;
private final int value;

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

public AssetType getAssetType() {
return type;
}

public int getValue() {
return value;
}
}

간단하게 자산종류와 자산가치 라는 두 변수만 가지는 Asset 클래스를 생성했다.
이젠 자산의 총합을 개산해보자.

public class AssetUtil {
public static void main(String[] args) {
final List<Asset> assets = Arrays.asList(
new Asset(AssetType.BOND, 1000),
new Asset(AssetType.BOND, 2000),
new Asset(AssetType.STOCK, 3000),
new Asset(AssetType.STOCK, 4000)
);

System.
out.println("Total of all assets : " + totalAssetValues(assets));
}

public static int totalAssetValues(final List<Asset> assets) {
return assets.stream()
.mapToInt(Asset::getValue)
.sum();
}
}
Total of all assets : 10000

간단하게 Stream을 생성하여 합계를 계산하였다. 결과도 잘 나오는 것을 확인할 수 있다.
결과에서도 보듯이 동작에는 아무런 문제가 없다. 다만 여기에는 구조상 3가지 문제가 얽혀있다.

* iteration 을 어떻게하고
* 어떤 값들에 대한 합계를 계산하며
* 그 합계를 어떻게 구하는가

이렇게 뒤엉켜 있는 로직은 재사용성이 떨어진다.

만약 Bond(채권) 자산에 대해서만 합계를 구한다고 하면...

public class AssetUtil {
public static void main(String[] args) {
final List<Asset> assets = Arrays.asList(
new Asset(AssetType.BOND, 1000),
new Asset(AssetType.BOND, 2000),
new Asset(AssetType.STOCK, 3000),
new Asset(AssetType.STOCK, 4000)
);

System.
out.println("Total of bonds : " + totalBondsValues(assets));
}

public static int totalBondsValues(final List<Asset> assets) {
return assets.stream()
.filter(
asset -> asset.getAssetType() == AssetType.BOND)
.mapToInt(Asset::getValue)
.sum();
}
}
Total of bonds : 3000

Stock(주식) 자산의 총합을 구한다면 또 다시 totalBondsValues() 를 복사하여 filter에 있는 부분을 변경해줘야 한다.

public class AssetUtil {
public static void main(String[] args) {
final List<Asset> assets = Arrays.asList(
new Asset(AssetType.BOND, 1000),
new Asset(AssetType.BOND, 2000),
new Asset(AssetType.STOCK, 3000),
new Asset(AssetType.STOCK, 4000)
);

System.
out.println("Total of stocks : " + totalBondsValues(assets));
}

public static int totalBondsValues(final List<Asset> assets) {
return assets.stream()
.filter(
asset -> asset.getAssetType() == AssetType.STOCK)
.mapToInt(Asset::getValue)
.sum();
}
}
Total of stocks : 7000

총 자산, 채권, 주식 각각의 자산의 총합을 계산하는데, 유사한 코드가 너무 많이 생산되었다.

이를 해결하기 위해 전략 패턴이라는 것을 적용해보자.
(디자인 패턴의 종류로 자세한 내용은 디자인 패턴 책을 찾아보길 바란다.)

public class AssetUtil {
public static void main(String[] args) {
final List<Asset> assets = Arrays.asList(
new Asset(AssetType.BOND, 1000),
new Asset(AssetType.BOND, 2000),
new Asset(AssetType.STOCK, 3000),
new Asset(AssetType.STOCK, 4000)
);
System.
out.println("Total of bonds : " + totalAssetValues(assets, asset -> asset.getAssetType() == AssetType.BOND));
System.
out.println("Total of stocks : " + totalAssetValues(assets, asset -> asset.getAssetType() == AssetType.STOCK));
System.
out.println("Total of assets : " + totalAssetValues(assets, asset -> true));
}

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

totalAssetValues() 구조를 살펴보면,
filter() 를 통해서 합계 계산 대상을 정하고
mapToInt() 를 통해서 합계 대상 value를 추출하고
sum() 을 통해서 합산을 하게 됩니다.

즉, 합계 대상 객체 추출 -> 합계 value 추출 -> 합계 수행 순서로 진행됩니다.




2. Delegate

람다 표현식과 전략패턴을 활용하여 문제를 분리했다.
문제 분리를 클래스 레벨에서도 분리가 가능한데, 이 때 재사용 측면에서 보면 Delegation(위임)을 이용하면 더 좋다.

Delegation(위임) 을 람다표현식을 통해 구현해보자.

CalculateNAV 클래스를 사용하여 웹 서비스에서 받은 데이터를 사용하여 재무 연산을 해보자.

public class CalculateNAV {
private Function<String, BigDecimal> priceFinder;

public CalculateNAV(final Function<String, BigDecimal> priceFinder) {
this.priceFinder = priceFinder;
}

public BigDecimal computeStockWorth(final String ticker, final int shares) {
return priceFinder.apply(ticker).multiply(BigDecimal.valueOf(shares));
}
}

computeStockWorth() 메서드에서 priceFinder에  대한 주식시세표(ticker)를 요청하고 주식의 시세에 따라 기치를 결정한다.
이제 priceFinder가 필요하다.

예제에서는 Yahoo 주식시세표를 가져와 계산해 볼 예정이나, 먼저 Stub을 통해 TC를 만들어보자.

public class CalculateNAVTest {
@Test
public void computeStockWorth() {
final CalculateNAV calculateNAV = new CalculateNAV(ticker -> new BigDecimal("6.01"));
BigDecimal
expected = new BigDecimal("6010.00");

Assert.assertEquals(0,
calculateNAV.computeStockWorth("GOOG", 1000).compareTo(expected), 0.001
);
}
}

Stub 을 작성해서 주식시세표에 따른 가치를 계산해 보았다.
실제로 Yahoo주식시세표를 가져와서 계산해보자.

public class YahooFinance {
public static BigDecimal getPrice(final String ticker) {
try {
final URL url =
new URL("http://ichart.finance.yahoo.com/table.csv?s=" + ticker);

final BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()));
final String data = reader.lines().skip(1).findFirst().get();
final String[] dataItems = data.split(",");
return new BigDecimal(dataItems[dataItems.length - 1]);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@Test
public void computeYahooStockWorth() {
final CalculateNAV calculateNAV = new CalculateNAV(YahooFinance::getPrice);
System.
out.println(String.format("100 shares of Google worth : $%.2f",
calculateNAV.computeStockWorth("GOOG", 100)));
}
100 shares of Google worth : $54161.00

현재 구글의 한주 당 주식가치는 541.61$ 인 듯하다. 100주의 가치가 54161$로 계산되었다.



3. Decorate

앞에서 설명한 Delegation 은 뛰어난 기능이다. 여기에 Delegation 을 chain 으로 묶어서 Behavior 를 추가할 수 있다면 더 유용하게 사용할 수 있을 것이다.

다음 예제에서 람다표현식을 사용하여 Delegate 를 조합해보자.
(Decorator 패턴을 어떻게 사용하는지 알아보기 위한 예제이다.)

Camera 라는 클래스를 만들고 capture() 를 통해  Color 를 처리하는 필터를 만들어보자.

public class Camera {
private Function<Color, Color> filter;

public Color capture(final Color inputColor) {
final Color processedColor = filter.apply(inputColor);

return processedColor;
}
}

위와 같이 Camera 는 Function 타입의 filter 변수를 갖고 있고, 이는 Color 를 받아서 처리하고 결과로 Color 를 반환한다. 이는 하나의 필터만 사용하지만 다양한 필터를 적용할 수 있도록 수정해보자.
유연성을 얻기 위해 Java8에서 등장하는 Default Method 를 사용할 것이다.

쉽게 이야기하면 Interface 에 구현부가 있는 것인데, 추상 Method 에 덧붙여서 인터페이스는 구현 부분이 있는 Method 를 갖게되며, Default 로 마크된다. 이 Method 는 해당 인터페이스를 구현한 클래스에 자동으로 추가된다.

public class Camera {
private Function<Color, Color> filter;

public Color capture(final Color inputColor) {
final Color processedColor = filter.apply(inputColor);

return processedColor;
}

public void setFilters(final Function<Color, Color> ... filters) {
filter = Stream.of(filters)
.reduce((
filter, next) -> filter.compose(next))
.orElse(
color -> color);
}

public Camera() {
setFilters();
}
}

setFilters() 를 이용하여 각 필터를 iteration 하고 compose 를 통해서 chain 으로 연결한다.
만약 filter 가 없다면 Optional Empty 를 반환한다.

4. Default Method



5. 예외 처리



'Programing > Java' 카테고리의 다른 글

json parser 정리  (0) 2016.04.04
Java DBCP Datasource 설정  (0) 2016.02.23
Java Static 과 Spring Singleton 의 차이  (0) 2016.01.20
slf4j 란?  (0) 2016.01.20
PowerMockito 를 이용하여 private method Mock 처리하기  (0) 2015.12.31