Java는 반올림하면 20년 가까이된 개발 언어입니다. 많은 역사가 있으며 많은 사람들이 사용을 해 왔습니다. SUN에 있다가 ORACLE로 이전도 하구요. 이번장은 Java의 역사같은 것은 뒷전으로 미루겠습니다.
자바 8하면? 람다! 람다! 하시는 분들이 많으실 겁니다. 우선 이 람다, 람다식이라는것을 알아보기전에 동작 파라미터화에 대해서 집어보고 가도록 하겠습니다.
언제나 고객은 우리에게 많은것을 바라고 있습니다. 그리고 그분들의 요구는 바뀌고 또 바뀌고 그래서 유연한 코드를 만들어야 합니다.
제가 경험한 사례를 이야기 해드리겠습니다.
전 무기 딜러 였습니다. 제 무기를 찾는 고객이 어느날 '소련제 무기를 모두 찾고 싶다'고 말했습니다. 그래서 전 찾아서 목록을 쫙 정리했더니 다음날 '소련제 무기에 2kg이하의 무기를 모두 찾고 싶다'고 말했습니다. 그리고 그 다음날 또 다른 요구가 쏙쏙 나오기 시작했습니다...
고객의 요구는 시시각각 변합니다. 우린 이런 요구사항에 최소화 되는 코드를 만들어야 합니다. 동작 파라미터화(behavior parameterization)는 이런 자주 바뀌는 요구사항에 효과적으로 대응 할 수 있습니다.
동작 파라미터화는 무엇인지, 이러한 고객의 요청을 어떻게 구현할지 차근차근 예제를 통해 알아보도록 하겠습니다.
고객님이 처음에 말씀하신 소련제(RUSSIA)무기를 필터링하기 위한 메소드를 만들었습니다.
/**
* RUSSIA무기를 필터링 하는 메소드
* @param inventory
* @return
*/
public static List filterRussiaWeapons(List weaponList) {
List result = new ArrayList();
for(Weapon weapon : weaponList) {
if("RUSSIA".equals(weapon.getMaker())) {
result.add(weapon);
}
}
return result;
}
전 이 메소드가 RUSSIA제인 무기들을 필터링 하는 것을 확인하고는 저는 고객분에게 설명을 해드렸죠. 그런데 고객님께서 미국(USA)무기도 검색해 달라 하셨습니다. 저는 고민했습니다. 그리고 filterUsaWeapons()라는 메소드를 하나 만들었습니다. 이상없이 잘 검색되는 것을 확인 했습니다. 그리고 고객님께 설명을 해드렸습니다. 그랬더니? 독일제 무기를 또 검색해달라고 했습니다. 저는 고민하기 시작했습니다. 계속 filterXXXWeapons()메소드를 만들어야 할까?
이런 고민에서는 고객의 요청은 계속 추가가 될 수 있으니 maker를 파라메터로 넘겨서 처리하도록 처리하는 것이 올바를 것이라 생각하고 변경 했습니다. 이젠 maker파라메터에 따라서 유연하게 미국용, 소련(러시아)용 무기들을 필터링 할 수 있게 되었습니다. 고객분들도 좋아하겟죠?
/**
* Maker 파라메터를 받아서 관련된 무기 리스트를 반환한다.
* @param weaponList
* @return
*/
public static List filterWeaponsByMaker(List weaponList, String maker) {
List result = new ArrayList();
for(Weapon weapon : weaponList) {
if(maker.equals(weapon.getMaker())) {
result.add(weapon);
}
}
return result;
}
다음날 고객분이 중화기와 소형화기를 구분하고 싶다고 이야기 하셨습니다. 중화기는 10kg이 넘는 무기들을 중화기라 하고 5kg이하는 소형화기라고 분류 한다고 하셨습니다. 그래서 또 아래와 같이 메소드를 하나 만들었죠. 다양한 무게에 대응하기 위해서 무게 파라메터를 추가 했죠.
/**
* weight 파라메터를 받아서 특정 무게보다 큰 무기의 리스트를 반환한다.
* @param weaponList
* @return
*/
public static List filterWeaponsByWeight(List weaponList, int weight) {
List result = new ArrayList();
for(Weapon weapon : weaponList) {
if(weapon.getWeight() > weight) {
result.add(weapon);
}
}
return result;
}
현재까지 짜왓던 코드는 솔직히 좋은 해결책이 되지 못합니다. 만든 3개의 메소드를 보면 무게 비교, Maker비교를 제외한 코드는 비슷합니다. 아니 똑같죠. 이는 소프트웨어 공학의 DRY(Don't Repeat Yourself)에 위반됩니다. 만약 개선사항이 생겨서 동일한 코드를 모두 고친다면 메소드 갯수만큼 고치고 테스트 해야하는 상황이 발생합니다.
그래서 저는 고민을 했습니다. 어떻게 할까? filterWeapon이라는 메소드를 만들고 거기다 특정 플래그 값을 넣어서 처리 하게 할까? 그래서 한번 구현해 보았습니다.
/**
* returnType이 0일경우 maker관련 무기를 반환 하고, 1일 경우 무게관련 무기를 반환한다.
* @param weaponList
* @param maker
* @param weight
* @param returnType
* @return
*/
public static List filterWeapons(List weaponList, int returnType, String maker, int weight) {
List result = new ArrayList();
for(Weapon weapon : weaponList) {
if((returnType == 0 && maker.equals(weapon.getMaker())) || (returnType == 1 && weapon.getWeight() > weight)) {
result.add(weapon);
}
}
return result;
}
이젠 아래와 같이 요청해서 데이터를 받아서 처리 할 수 있게 되었습니다.
List usaWeapons = filterWeapons(weaponList, 0, "USA", 0);
List middleWeapons = filterWeapons(weaponList, 1, "", 5000);
고객이 요구하는 요구를 들어주는 코드를 만들었으며 이것을 들고 고객에게 시연을 했습니다. 시연후 고객분이 이야기를 했죠, 러시아 무기이면서 10kg이하 무기만 필터링을 하고 싶습니다. 그리고 무기의 길이랑 타입에 관해서도 필터링을 하고 싶다고 요청을 했습니다.
'알겠습니다.'라고 이야기를 한후에 곰곰히 생각해봣습니다. filterWeapons에 int returType에 추가 타입을 넣을까? 요구사항 올때마다 maker나 weight를 계속 추가 해야하나? 배열로 받아서 처리할까? 맵형태로는? 저는 많은 고민을 했습니다. 하지만 결론은 filterWeapons은 지저분한 메소드가 되어갈 뿐이며 정말 유지보수 하기 힘든 메소드가 되어가고 있었습니다.
저는 파라메터를 계속 추가하는 방법이 아닌 무언가 이런 변화하는 요구사항에 대해서 유연하게 대응할 방법이 있는지 찾기를 시작했습니다.
생각해보면 저 if()의 true/false값의 조건식만 먼가 파라메터로 주면 되지 않을까? 라는 생각을 했습니다. 찾아보니 어떤 객체의 속성에 따라서 boolean값을 반환하는 것을 predicate라고 한다는 것을 알게 되었습니다. 그래서 이런 if()에 조건을 결정하고 이것을 파라메터로 넘기는 방식을 찾아보게 되었습니다. 즉 러시아산무기 predicate, 5kg이상 무기 predicate를 만들어서 파라메터로 넘겨보자라고 생각을 했죠
우선 주체가 될 WeaponPredicate를 만들었습니다.
public interface WeaponPredicate {
boolean test (Weapon weapon);
}
그리고 WeaponPredicate를 구현하는 WeaponRussiaPredicate를 구현했습니다.
public class WeaponRussiaPredicate implements WeaponPredicate {
public boolean test(Weapon weapon) {
return weapon.getMaker().equals("RUSSIA");
}
}
그리고 WeaponPredicate를 구현하는 WeaponMiddlePredicate를 구현했습니다.
public class WeaponMiddleWeightPridicate implements WeaponPredicate {
public boolean test(Weapon weapon) {
return weapon.getWeight() > 5000;
}
}
저는 WeaponRussiaPredicate를 파라메터로 받거나 WeaponMiddleWeightPredicate를 파라메터로 받으면 이 안의 조건식에 따라 무기 리스트를 반환하면 되지 않을까라고 생각을 하게 된거죠. 그래서 예전에 filterWeapon을 이 Predicate를 받도록 수정 했습니다.
public static List filterWeapons(List weaponList, WeaponPredicate predicate) {
List result = new ArrayList();
for(Weapon weapon : weaponList) {
if(predicate.test(weapon)) {
result.add(weapon);
}
}
return result;
}
이제는 filterWeapons의 2번째 파라메터 WeaponPredicate의 구현체를 어떤 것을 넘겨주느냐에 따라서 고객님이 원하는 기능을 구현 할 수 있게 되었습니다. 이렇게 구현함으로써 실제 컬렉션(List)를 반복하는 구문과 각 요소에 비교하는 비교 로직을 분리 할 수 있게 되었습니다.
List russiaWeapons = filterWeapons(weaponList, new WeaponRussiaPredicate());
List middleWeapons = filterWeapons(weaponList, new WeaponMiddleWeightPridicate());
만약 고객님이 또다른 요구를 했을때 WeaponPredicate를 구현하는 또 하나의 구현체를 만들고 구현 후 그것을 파라메터로 넘겨서 구해오기만 하면 됩니다. 만약 컬렉션의 반복 성능 문제로 인한 코드 수정도 filterWeapons만 변경을 하면 됩니다. 아 신난다!
※ 만약에 러시아산 무기이며 무게가 10kg이상인 무기를 필터링 하고 싶으면 어떻게 해야할까요? 스스로 생각해보세요.
고민을 또 하게 되었다. 요구사항이 올때 마다 WeaponPredicate를 구현한 클래스를 계속 만들어야 하는가? 이런 고민에 빠지기 시작했습니다. 좋은 방법이 없을까?
Java는 클래스의 선언과 인스턴스화를 동시에 수행할 수 있는 익명 클래스(Anonymous class)라는 기법을 제공한다. 익명 클래스가 모든 것을 해결 해 주지는 않는다.
익명클래스사용
별도 클래스를 생성하지 않고 익명 클래스를 통해서 필요할 때마다 즉각 즉각 사용하도록 해 보았다.List middleWeapons = filterWeapons(weaponList, new WeaponPredicate() {
public boolean test(Weapon weapon) {
return weapon.getWeight() > 5000;
}
});
익명 클래스까지 사용해보았습니다. 이상없이 잘 동작도 했구요, 그런데 Weapon이 아닌 Shield, Ship등 다른 요소에 대해서는 또 다른 인터페이스를 구현해야 하고 빨간색으로 칠한 중복되는 부분이 계속 생기기 시작했습니다. 혹시 더 좋은 방법이 없나 생각을 해보았습니다.
람다표현식
Java8부터 제공하는 람다표현식을 쓰면 더 간략하고 직관적으로 알기 쉽게 구현이 가능한 것을 찾았다 그리고 써봣다.List usaWeapons = filterWeapons(weaponList, (Weapon weapon) -> "USA".equals(weapon.getMaker()));
List usaHeavyWeapons = filterWeapons(weaponList, (Weapon weapon) -> "USA".equals(weapon.getMaker()) && weapon.getWeight() > 5000);
와우 최소한으로 구현해야 할 로직만 입력해서 고객이 원하는 무기의 리스트를 필터링 할 수 있었다. Java8의 람다식이 아니면 이렇게 줄이는 것은 불가능한 일이다!
정리
- 자주 변경되는 고객의 요구사항을 처리 하기 위해서 동작을 수행하는 코드를 메서드 인수로 전달 할 수 있다.
- Java8이전에는 최대한으로는 익명 클래스를 이용해서 처리를 했지만 Java8부터는 미리 제공하는 인터페이스를 통해서 별도 인터페이스를 구현하는 수고를 없앨 수 있게 되었다.
COMMENTS