프로그램을 개발하면서 변화하는 요구사항에 대처해야한다.
유동적으로 변화하는 요구사항에 어떻게 대처해야할까?
엔지니어링적인 비용이 가장 최소화되면서 쉽게 구현이 가능하고 유지보수가 쉬워야한다.
동작 파라미터화를 이용하면 변화하는 요구사항에 효과적으로 대응할 수 있다.
아직은 어떻게 실행할 것인지 결정하지 않은 코드 블럭을 말한다. 코드 블록의 실행이 나중으로 미뤄진다.
예를 들어 메소드의 인수로 코드 블럭을 전달할 수 있다. 코드 블럭에 따라 메소드의 동작이 파라미터화되므로 다양한 동작을 수행할 수 있다.
그러나 동작 파라미터화를 사용하게 되면 쓸데없는 코드가 늘어나게 된다. 이것은 익명 객체와 람다식을 사용하여 보완한다.
연필 클래스 생성
public class Pencil {
private COLOR color;
private int length;
public Pencil(COLOR color, int length){
this.color = color;
this.length = length;
}
public COLOR getColor(){
return color;
}
public int getLength(){
return length;
}
}
COLOR는 enum으로 {GREEN, YELLOW, RED} 값을 가지고 있습니다.
노란색 연필만 필터링하는 기능을 추가해주세요.
public static List<Pencil> filterYellowPencil(List<Pencil> inventory){
List<Pencil> result = new ArrayList<Pencil>();
for(Pencil pencil : inventory){
if(COLOR.YELLOW.equals(pencil.getColor())){
result.add(pencil);
}
}
return result;
}
인자로 연필 List 객체를 전달받아 for문을 통해 객체의 color 필드가 YELLOW일 경우 결과 반환 리스트에 해당 객체를 추가하는 코드이다.
public static void main(String[] args) {
List<Pencil> pencils = Arrays.asList(new Pencil(COLOR.YELLOW, 20),
new Pencil(COLOR.GREEN, 30),
new Pencil(COLOR.YELLOW, 30));
List<Pencil> result = filterYellowPencil(pencils);
for(Pencil pencil : result){
System.out.println(pencil.getColor() + ", " + pencil.getLength());
}
}
실행 결과
YELLOW, 20
YELLOW, 30
사용자가 요구한 노란색 연필만 필터링됐다.
이 때, 사용자의 요구사항이 추가된다고 가정하자.
초록색 연필만 필터링하는 기능을 추가해주세요.
사용자 요구사항 1과 색상만 다른 요구사항이 추가되었다.
이 때 코드를 복사 붙여넣기해서 색상만 바꾸는 방법도 있지만 이 방법은 적절하지 못하다.
중복되는 코드가 많고 전혀 유연하지 못하기 때문이다.
그렇다면 중복되는 코드를 추상화해보자.
변하는 값인 색상을 파라미터로 받는 메소드를 작성한다면 조금 더 유연하게 동작할 것이다.
public static List<Pencil> filterPencilsByColor(List<Pencil> inventory, COLOR color){
List<Pencil> result = new ArrayList<>();
for(Pencil pencil : inventory){
if(color == pencil.getColor()){
result.add(pencil);
}
}
return result;
}
List<Pencil> result = filterPencilsByColor(pencils, COLOR.YELLOW);
List<Pencil> result = filterPencilsByColor(pencils, COLOR.GREEN);
이제 특정 색상이 아닌 사용자가 입력한 색상에 따라 필터링 기능을 제공한다.
길이가 25cm 이상인 연필만 필터링해주세요.
사용자가 색상이 아닌 길이라는 조건을 내걸었다. 이럴 때는 어떻게 해야할까?
이때까지 작성한 코드에서 인자 값과 if문 조건만 바꿔주면 된다.
그러나 이 경우, 대부분의 코드가 중복되므로 적절한 대응이 아니다.
또한 추후 유지보수할 때 중복된 코드에 대해 수정 사항이 생기면 전체를 수정해야하기 때문에 비용과 노력이 많이 든다.
그렇다면 모든 속성을 인자로 받아 필터링하는 건 어떤가?
public static List<Pencil> filterPencils(List<Pencil> inventory, COLOR color, int length, boolean flag){
List<Pencil> result = new ArrayList<>();
for(Pencil pencil : inventory){
if((flag && color.equals(pencil.getColor())) || (!flag && length < pencil.getLength())){
result.add(pencil);
}
}
return result;
}
List<Pencil> result = filterPencils(pencils, COLOR.YELLOW, 0, true);
List<Pencil> result = filterPencils(pencils, null, 25, false);
문제점이 많다.
우선 flag가 무엇을 하는 인자인지 한 번에 인지할 수 없다.
또한 색상과 길이가 아닌 또 다른 필터링 기준이 나타났을 때 유연하게 대응하지 못하기 때문에 부적절하다.
그렇다면 어떤 코드를 작성해야 다양한 요구사항에 대비하면서 직관적인 코드를 작성할 수 있을까?
그 답은 동작 파라미터화이다.
지금까지 작성한 필터링 코드의 조건은 모두 is~?형이다.
즉 속성에 대해 참 또는 거짓으로 결과를 반환받아 필터링할 수 있다.
여기서 활용할 것은 프리디케이트, 참 또는 거짓을 반환하는 함수이다.
선택 조건을 결정하는 인터페이스 역할을 수행한다.
public interface PencilPredicate {
boolean test(Pencil pencil);
}
public static class PencilYellowColorPredicate implements PencilPredicate{
@Override
public boolean test(Pencil pencil) {
return COLOR.YELLOW.equals(pencil.getColor());
}
}
public static class PencilLengthPredicate implements PencilPredicate{
@Override
public boolean test(Pencil pencil) {
return pencil.getLength() > 20;
}
}
PencilPredicate를 상속받는 클래스들은 각 클래스에서 사용될 test()메소드를 구현해야한다.
위에서는 노란색 연필을 필터링하는 클래스와 연필 길이가 20cm를 초과하는 필터링을 구현했다.
이제 값을 인수로 넘기는 것이 아닌 interface 객체를 넘긴다.
public static List<Pencil> filterPenclis(List<Pencil> inventory, PencilPredicate p){
List<Pencil> result = new ArrayList<>();
for(Pencil pencil : inventory){
if(p.test(pencil)){
result.add(pencil);
}
}
return result;
}
List<Pencil> result = filterPenclis(pencils, new PencilYellowColorPredicate());
List<Pencil> result = filterPenclis(pencils, new PencilLengthPredicate());
객체에 따라 다양한 로직이 수행이 되며 이전보다 더 유연하고 가독성이 좋고 사용자가 사용하기 편리한 코드가 만들어졌다.
전달한 객체에 따라 동작이 결정된다. 즉, 동작을 파라미터화한 것이다.
동작의 파라미터화 장점
- 컬렉션 탐색 로직과 각 항목에 적용할 동작을 분리할 수 있다.
- 한 메소드가 다른 동작을 수행하도록 재활용할 수 있다. 유연한 api를 만들 때 동작 파라미터화가 중요한 역할을 한다.
하지만 사용하기 복잡한 기능이나 개념을 사용하고 싶은 사람은 없다.
위 코드를 작성하면서 기능 하나가 추가될 때마다 (필터 조건이 추가될때마다) class를 생성하고 인스턴스화하여 사용하는 것이 맞나..? 하는 의문이 들었다.
이 방법은 상당히 번거로우며 시간을 낭비한다.
이러한 문제는 익명 클래스 또는 람다식을 사용해서 코드를 조금 더 간결하여 해결할 수 있다.
동작 파라미터화를 작성할 때 인터페이스를 상속받는 클래스를 작성하고 그 안에 메소드를 구현하였다.
그러나 익명 클래스를 사용한다면 클래스를 작성하고 호출부에서 해당 클래스를 인스턴스화하는 일이 간소화된다.
List<Pencil> result = filterPenclis(pencils, new PencilPredicate(){
public boolean test(Pencil pencil){
return COLOR.YELLOW.equals(pencil.getColor());
}
});
람다식을 이용하면 익명 클래스로 작성한 코드보다 훨씬 간결한 코드를 작성할 수 있다.
List<Pencil> result = filterPenclis(pencils, (Pencil pencil) -> COLOR.YELLOW.equals(pencil.getColor()));
public interface Predicate<T>{
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p){
List<T> result = new ArrayList<>();
for(T item: list){
if(p.test(item)) result.add(item);
}
return result;
}
이제 다양한 타입의 리스트에서 필터 메소드를 사용할 수 있게 되었다.