동작 파라미터화(behavior parameterization)
동작 파라미터화란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블럭을 의미한다.
동작 파라미터화를 사용하면
- 자주 바뀌는 요구사항에 효과적으로 대응할 수 있다.
예를들어 정수 리스트에서 특정 정수만 필터링하는 기능을 구현하고 싶다고 한다면
일반 메서드를 사용
2로 나누어 떨어지는 값들을 필터링하는 기능을 일반 메서드로 만든다면 아래와 같다.
public static List<Integer> filterDivideByTwo(List<Integer> list) {
List<Integer> result = new ArrayList<>();
for(int x : list) {
if(x % 2 == 0) {
result.add(x);
}
}
return result;
}
하지만 만약 2뿐만 아니라 4, 7 등 다양한 값들로 나누어떨어지는 필터를 만들고 싶다면 다음과 같이 할 수 있다
public static List<Integer> filterDivideByN(List<Integer> list, int n) {
List<Integer> result = new ArrayList<>();
for(int x : list) {
if(x % n == 0) {
result.add(x);
}
}
return result;
}
메서드에서 n을 파라미터로 받아 원하는 값을 넣을 수 있다.
한 개의 값 뿐만 아니라 여러 값들로 나누어 떨어지는 값을 필터링 하고싶다면
public static List<Integer> filterDivideByNM(List<Integer> list, int n, int m) {
List<Integer> result = new ArrayList<>();
for(int x : list) {
if(x % n == 0 && x % m == 0) {
result.add(x);
}
}
return result;
}
이런 식으로 함수를 다시 작성해 주어야 한다.
- 중복되는 코드가 너무 많고, 모든 함수를 작성해주어야 한다는 단점이 있다.
프레디케이트(Predicate)를 사용
참 또는 거짓을 반환하는 함수를 프레디케이트라고 한다. 우선 선택 조건을 결정하는 인터페이스를 정의하자.
public interface NumberPredicate {
boolean test(int n);
}
그 후에는 여러 버전의 NumberPredicate를 정의할 수 있다.
public class DivideByTwoPredicate implements NumberPredicate{
public boolean test(int n) {
return n % 2 == 0;
}
}
public class DivideByThreePredicate implements NumberPredicate{
public boolean test(int n) {
return n % 3 == 0;
}
}
numberFilter 메서드는 아래와 같이 NumberPredicate 인터페이스를 받아서 내부적으로 다양한 동작을 수행할 수 있다.
이렇게 하면 numberFilter 메서드 내부에서 컬렉션을 반복하는 로직과 컬렉션의 각 요소에 적용할 동작을 분리할 수 있다는 점에서 소프트웨어 엔지니어링적으로 큰 이득을 얻는다.
public static List<Integer> numberFilter(List<Integer> list, NumberPredicate p) {
List<Integer> result = new ArrayList<>();
for(int x : list) {
if(p.test(x)) {
result.add(x);
}
}
return result;
}
실제 사용은 이런식으로 한다.
public static void main(String[] args) {
List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));
List<Integer> ret = numberFilter(list, new DivideByTwoPredicate());
List<Integer> ret = numberFilter(list, new DivideByFourPredicate());
}
위 예제에서 numberFilter 메서드는 NumberPredicate 의 객체에 따라 다르게 동작하는데, 이를 전략 디자인 패턴이라고 한다.
전략 디자인 패턴은 각 알고리즘을 캡슐화 하는 알고리즘 패밀리를 정의해둔 다음에 런타임 알고리즘을 선택하는 기법이라고 한다.
- 프레디케이트를 구현하는 클래스들을 만들어야 한다는 단점이 있다.
익명클래스를 사용
이전 예제에서 각 프레디케이트를 구현하는 여러 클래스를 정의한 다음 인스턴스화 해야 하는 번거로움이 있었다. 따라서 익명 클래스를 사용해서 더 간결한 코드를 짤 수 있다.
익명 클래스란 자바의 지역 클래스와 비슷한 개념인데, 익명 클래스를 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있다.
public static void main(String[] args) {
List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));
List<Integer> ret = numberFilter(list, new NumberPredicate() {
@Override
public boolean test(int n) {
return n % 2 == 0 || n % 3 == 0;
}
});
}
// ret = {2, 3, 4}
- 익명 클래스도 충분히 편리하지만 여전히 많은 공간을 차지한다
람다 표현식을 사용
자바8의 람다 표현식을 사용하면 위 예제를 다음과 같이 간단하게 재구현할 수 있다.
public static void main(String[] args) {
List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));
List<Integer> ret = numberFilter(list, n -> n % 2 == 0 || n % 3 == 0);
}
// ret = {2, 3, 4}
[모던 자바 인 액션] https://www.yes24.com/Product/Goods/77125987