동작 파라미터화

Akar·2022년 7월 18일
0

내가 기억하기 위해 작성하는 글

  • 서비스 레이어 코드를 작성하다가 동일한 List 형식에 타입의 필터 조건만 다른 중복된 함수들이 생겼다.
  • 더 나은 코드를 작성하기 위해 책을 읽다가 동작 파라미터화에 대해서 읽게 되었다.
		Market btc = new Market("KRW-BTC", 10, CoinEnum.upbit);
        Market eth = new Market("KRW-ETH", 20, CoinEnum.upbit);
        Market matic = new Market("KRW-MATIC", 50, CoinEnum.upbit);
        Market bidr = new Market("BIDRUSDT", 100, CoinEnum.binance);
        Market inch = new Market("1INCHUSDT", 5000, CoinEnum.binance);


        List<Market> markets = Arrays.asList(btc, eth, matic, bidr, inch);

5개의 마켓 정보를 가지고 있는 list가 있다고 작성하겠다


첫번째 요구사항

  1. 업비트 관련 마켓 list를 출력하라.
    public static List<Market> filterUpibts(List<Market> marketList) {
        List<Market> markets = new ArrayList<>();
        for (Market market : marketList) {
            if (market.getCoinEnum().equals(CoinEnum.upbit)) {
                markets.add(market);
            }
        }
        return markets;
    }

이번에는 바이낸스 list를 가져와야 하는 경우가 생겨서 요구사항을 더 추가 했다.

  1. 바이낸스 관련 마켓 list를 출력하라
   public static List<Market> filterBinance(List<Market> marketList) {
        List<Market> markets = new ArrayList<>();
        for (Market market : marketList) {
            if (market.getCoinEnum().equals(CoinEnum.binance)) {
                markets.add(market);
            }
        }
        return markets;
    }

나는 당연하게 함수를 하나 더 만들어서 equals 부분을 변경하기만 했다.

  • 아차! 만약 마켓이 더 추가 되어서 각 마켓에 대한 list를 출력 해야한다면 마켓의 개수만큼 함수가 늘어나게 될 것이다.

그래서 마켓에 관련된 파라메타를 하나 추가하여 위 두개의 함수를 하나로 합쳤다.


두번째 요구사항

  1. 중복되는 함수를 하나로 만들어라.
   public static List<Market> filterByMarket(List<Market> marketList, CoinEnum coinEnum) {
        List<Market> markets = new ArrayList<>();
        for (Market market : marketList) {
            if (market.getCoinEnum().equals(coinEnum)) {
                markets.add(market);
            }
        }
        return markets;
    }

이렇게 파라메타에서 마켓에 대한 정보를 받아 if문을 통해 작업이 마켓이 몇개가 추가되든
상관이 없어졌다.


세번째 요구사항

몇일이 지나고 마켓이 가지고 있는 코인의 개수를 알고 싶어졌다. 그래서 특정 개수 이상의 코인을 가지고 있는 마켓 list를 출력하는 요구사항이 추가 되었다.

  1. upbit 마켓중에 코인을 30개 이상 가지고 있는 마켓 정보를 출력하라.
public static List<Market> filterCoinQuantity(List<Market> marketList, long coinQuantity) {
        List<Market> markets = new ArrayList<>();
        for (Market market : marketList) {
            if (market.getCoinQuantity() > coinQuantity) {
                markets.add(market);
            }
        }
        return markets;
    }

작성을 하다보니 coinEnum으로 비교해서 가져오는 함수와 코인 개수를 비교해서 가져오는 함수가 if문을 제외한 부분이 동일하다.

그래서 이것도 하나로 만들어야 한다는 생각으로 flag를 생각한다.
flag가 true 이면 coinEnum으로 조회, false이면 quantity로 조회!

filterMarkets(markets, CoinEnum.upbit, 0, true)
filterMarkets(markets, CoinEnum.binance, 30, false)

public static List<Market> filterMarkets(List<Market> marketList,
CoinEnum coinEnum,long coinQuantity, boolean flag) {
        List<Market> markets = new ArrayList<>();
        for (Market market : marketList) {
            if (flag && market.getCoinEnum().equals(coinEnum) ||
            	!flag && market.getCoinQuantity() > coinQuantity) {
                markets.add(market);
            }
        }
        return markets;
    }
  • 만약 어느 개발자가 와서 위 요구사항을 안보고 코드를 봤을 때
    true, false의 의미를 알 수 있을까?

나중에 마켓에 한글이름이 추가되어 한글이름만 출력하는 list가 생겼을 때 대응하기 힘들다.


  • 이전의 요구사항을 보면
    1. coinEnum이 upbit인가?
    2. coinEnum이 binance인가?
    3. coin 개수가 coinQuantity 개수 이상인가?
    참이면 add 하였고 false이면 add를 하지 않았다.

    참 또는 거짓을 반환하는 함수를 프리디케이트 라고 한다.

  • 프리디케이트를 정의 해보자

public interface MarketPredicate {
    boolean list(Market market);
}

public class MakretUpbitPredicate implements MarketPredicate{
    @Override
    public boolean list(Market market) {
        return market.getCoinEnum().equals(CoinEnum.upbit);
    }
}

public class MarketQuantityPredicate implements MarketPredicate {
    @Override
    public boolean list(Market market) {
        return market.getCoinQuantity() > 30;
    }
}

public static List<Market> filterMarkets(List<Market> marketList, MarketPredicate marketPredicate) {
        List<Market> markets = new ArrayList<>();
        for (Market market : marketList) {
            if (marketPredicate.list(market)) {
                markets.add(market);
            }
        }
        return markets;
    }
    
    
 filterMarkets(markets, new MakretUpbitPredicate());
 filterMarkets(markets, new MarketQuantityPredicate());

MakretUpbitPredicate를 넘기면 market이 upbit인지 확인하는 작업을 하게 될 것이고

MarketQuantityPredicate를 넘기면 coinQuantity가 30이상인지 확인하는 작업을 하게 될 것이다.

이런식으로 런타임에 필요한 알고리즘을 선택해서 대응하는 기법을 전략 패턴이라고 한다.

이제 Market에 관련된 어떤 요구사항이든 Predicate를 하나 만들어서 클래스를 하나 만들어 구현하면 된다.

  • 하지만 요구사항이 늘어나면 Predicate의 개수도 늘어나고
    각 Predicate를 보면 return 부분을 제외하고는 중복된 코드이다.

더 개선 가능한 방법이 없는가?


네번째 요구사항

  1. Predicate의 중복에 대해 개선 가능한 방법이 있는가?

일단 클래스를 줄이는 방법부터 생각해보자

자바에는 익명클래스가 존재한다. 익명 클래스는 선언과 인스턴스화를 동시에 할 수 있다.

즉 따로 클래스를 만들 필요가 없다.

filterMarkets(markets, new MarketPredicate() {
            @Override
            public boolean list(Market market) {
                return market.getCoinEnum().equals(CoinEnum.binance);
            }
        });

filterMarkets(markets, new MarketPredicate() {
            @Override
            public boolean list(Market market) {
                return market.getCoinQuantity() > 20;
            }
        });

이런식으로 익명 클래스를 사용하게 된다면 별도의 클래스를 만들지 않아도 구현이 가능하다.

하지만 아직도 return 부분을 제외한 나머지 부분은 중복이다.


다섯번째 요구사항

  1. 익명 클래스의 중복에 대해 개선 가능한 방법이 있는가?

자바8의 람다를 이용하면 가능하다 라고 말할 수 있다.

filterMarkets(markets, market -> market.getCoinEnum().equals(CoinEnum.binance));

filterMarkets(markets, market -> market.getCoinQuantity() > 20);
1. filterMarkets(markets, new MarketPredicate() {
            @Override
            public boolean list(Market market) {
                return market.getCoinEnum().equals(CoinEnum.binance);
            }
        });

2. filter(markets, (Market m) -> CoinEnum.upbit.equals(m.getCoinEnum()));

1번을 2번처럼 변경 했다.

(Market m) -> CoinEnum.upbit.equals(m.getCoinEnum())

이것을 보기 편하게 변경하게 되면 아래와 같이 된다.

 public boolean list(Market m){
        return CoinEnum.upbit.equals(m.getCoinEnum());
    }

이제 filterMarkets와 함께 보자

public boolean list(Market m){
       return CoinEnum.upbit.equals(m.getCoinEnum());
}
    
public static List<Market> filterMarkets(List<Market> marketList, MarketPredicate marketPredicate) {
        List<Market> markets = new ArrayList<>();
        for (Market market : marketList) {
            if (marketPredicate.list(market)) {
                markets.add(market);
            }
        }
        return markets;
    }

이렇게 두개를 놓고 보자면

  • MarketPredicate marketPredicate에 (Market m) -> CoinEnum.upbit.equals(m.getCoinEnum())이 넘어 온다면
    marketPredicate.list(market)에서 .list(market)가 public boolean list(Market m) 함수를 호출하는 것과 같다.

  • (Market m) -> CoinEnum.upbit.equals(m.getCoinEnum())이 의미하는 것은 파라메타로는 Market m 하나가 있고 m.getCoinEnum()이 CoinEnum.upbit이면 true를 반환하고 아니면 false를 반환한다.

  • true이면 add를 하고 false이면 넘어간다.


최대한 코드 중복을 해결 하였다고 본다.
마켓에 대하여 파라메타 비교에 해당하는 list를 반환하는 기능들을 이제 중복 코드 없이 관리 할 수 있게 되었다.

  • Market 타입을 가지고 하는건 이제 알겠는데... 만약 User라는 새로운 타입이 생기고 User를 가지고 파라메타를 비교하고 해당하는 list를 반환하는 기능이 생겼을 때
public static List<User> filterUsers(List<User> users, MarketPredicate marketPredicate)

위와 같은 함수를 하나 더 만들어서 사용해야 한다.
반환값과 파라메타 List<>타입 값만 다르지 filterMarkets이랑 다를게 없다.


여섯번째 요구사항

  1. 하나의 filter 함수에서 다양한 타입을 제어할 수 있게 하자.

자바에는 제너릭이라는 것이 있다.

public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
        List<T> result = new ArrayList<>();
        for (T t : list) {
            if (predicate.list(t)) {
                result.add(t);
            }
        }
        return result;
    }

이런식으로 만들게 되면 T 로 선언되어 있으니 어떤 타입이든 사용할 수 있다. Market이 사용되든 User가 사용되는 모두 사용이 가능하다.


마무리하며

List<Market> markets_predicate = filter(markets, (Market m) -> CoinEnum.upbit.equals(m.getCoinEnum()));
List<Market> markets_stream = markets.stream().filter(market -> market.getCoinEnum().equals(CoinEnum.upbit)).collect(Collectors.toList());

첫번째꺼는 predicate를 이용해서 필터링을 한다.
두번째꺼는 stream의 filter()를 이용해서 필터링을 한다.


stream의 filter()에 대한 설명이다.

filter()안에 Predicate가 사용되고 있다.

어...그러면 custom predicate interface를 쓸게 아니라 그냥 list에서 stream을 통해 filter()를 가지고 뽑아내면 되는거 아닌가? 라는 생각을 가지게 되었다.

해당 케이스에서만 쓰는게 아니기에 확정할 수 있는건 없지만
둘다 predicate를 쓰게되니 filter()의 복잡도에 따라 케바케로 쓰게 되는 것 같다고 생각이 든다.

profile
다른 사람의 시선에 기준을 잡지 말고 할 수 있는 것에 최선을 다하자

0개의 댓글