이 글은 "자바 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 |