우리는 hello world를 지나 여러 코드를 치면서 완성해볼 것이다. 하지만 코딩을 하는 본질적 목표는 내가 무엇인가를 효율적으로 만들거나 아니면 누군가의 요구사항을 효율적으로 만들기 위함임을 나는 생각한다. 그래서 2장의 동작 파라미터화는 누구보다 큰 충격을 주었다.
만약 우리는 어느 한 농부의 요구사항을 듣고 코딩을 해서 농부의 문제를 해결해준다고 생각해보자. 농부는 우리에게 녹색, 빨간색 이렇게 두 종류의 사과를 구분할 수 있게 해달라고 부탁을 받았다.
//사과 클래스
class Apple {
enum Color{
GREEN, RED
}
public Color color;
public int weight;
public Apple(Color color, int weight){
this.color = color;
this.weight = weight;
}
}
Java
복사
첫 시도
public class Main {
public static List<Apple> Apples = Arrays.asList(new Apple(Apple.Color.GREEN),new Apple(Apple.Color.RED));
public static void main(String[] args) {
List<Apple> result = new ArrayList<>();
for (Apple apple : Apples){
if (apple.color.equals(Apple.Color.GREEN)){
result.add(apple);
}
}
result.stream().forEach(apple -> System.out.println(apple.color));
}
}
Java
복사
그런데 뭔가 농부의 마음이 변했다. 이번에는 초록뿐만 아니라 원하는 색이 있을때 마다 분류 하고 싶다고 한다.
두 번째 시도
public class Main {
public static List<Apple> Apples = Arrays.asList(new Apple(Apple.Color.GREEN),new Apple(Apple.Color.RED));
public static void main(String[] args) {
appleColorFilter(Apples,Apple.Color.GREEN).stream().forEach(apple -> System.out.println(apple.color));
}
public static List<Apple> appleColorFilter(List<Apple> apples, Apple.Color color){
List<Apple> result = new ArrayList<>();
for (Apple apple : Apples){
if (apple.color.equals(color)){
result.add(apple);
}
}
return result;
}
}
Java
복사
기존과는 다르게 메서드화 시키고 Color 값을 파라미터화 시켰다. 덕분에 농부의 요구사항에 적절히 대처할 수 있었다. 근데 농부의 변심은 끝이 없었다. 이번에는 사과의 무게도 분류를 하고 싶다고 한다. 150g 이상인 것만...
세 번째 시도
public static List<Apple> appleWeightFilter(List<Apple> apples, int weight){
List<Apple> result = new ArrayList<>();
for (Apple apple : Apples){
if (apples.weigth >= weight){
result.add(apple);
}
}
return result;
}
Java
복사
또 무게를 변심할까봐 이러한 메서드로 제작했다. 원하는 무게를 입력하고 원하는 무게 만큼이 필터링된다. 근데 여기서 문제가 발생했다. 두가지의 중복코드 appleColorFilter와 appleWeightFilter 는 근본적으로 같은 일을 하는 코드다. 그래서 중복코드가 발생했다. 이를 어찌 해결하면 좋은가?
네 번째 시도
public static List<Apple> appleFilter(List<Apple> apples, Apple.Color color, int weight){
List<Apple> result = new ArrayList<>();
for (Apple apple : Apples){
if (apple.color.equals(color) && apple.weight >= weight){
result.add(apple);
}
}
return result;
}
Java
복사
이렇듯 하나의 메서드로 묶어버렸다. 근데 여기서도 문제가 발생한다. 내가 만약 color 만 필터링하고 싶다면? appleFilter(apples, Apple.Color.Green, 0) 이렇듯 의미없는 0이 들어 가버린다.
이 예제를 통해서 묻고 싶다. 과연 지금까지 이런 실수를 범하진 않았는가? 그리고 동작 파라미터가 왜 필요한 것인지도 이 예제를 통해서 알 수 있다면 더 좋을 것 같다.
동작 파라미터화
모두 동일한 조건에서 실행된다. 하지만 이번엔 마지막 네 번째 시도에서의 실수를 없애보려고 한다.
interface ApplePredicate{
public boolean filter(Apple apple);
}
/*컬러 필터*/
class AppleColorFilter implements ApplePredicate{
@Override
public boolean filter(Apple apple) {
return apple.color.equals(Apple.Color.GREEN);
}
}
/*무게 필터*/
class AppleWeightFilter implements ApplePredicate{
@Override
public boolean filter(Apple apple) {
return apple.weight >= 150;
}
}
Java
복사
public static List<Apple> appleFilter(List<Apple> apples,
ApplePredicate p){
List<Apple> result = new ArrayList<>();
for (Apple apple : Apples){
if (p.filter(apple)){
result.add(apple);
}
}
return result;
}
Java
복사
public static void main(String[] args) {
appleFilter(Apples,new AppleColorFilter())
.stream().forEach(apple -> System.out.println(apple.color));
appleFilter(Apples,new AppleWeightFilter())
.stream().forEach(apple -> System.out.println(apple.color));
}
Java
복사
여기서 눈여겨 볼점은 아까의 문제 사항이 파라미터화를 통해서 해결되었다. 또한 좀 더 명시적인 코딩이 된 것을 확인 할 수 있다. 이러한 방법을 통하여 객체 자신이 동작이 되고 그 동작을 파라미터화 시킴으로써 좀 더 간결한 코드를 작성할 수 있다는 걸 알 수 있다.
이처럼 각 항목에 적용할 동작을 분리할 수 있는다는 것은 동작 파라미터화의 강점이다.
하지만 이러한 파라미터화에 대한 문제도 있기 마련이다. 내 생각으로는 익명객체를 활용한 방법과 그렇게 큰 차이가 생기지 않는다고 본다. 간결함을 제외한다면.. 그래서 우리가 마지막에 활용한 시도를 익명 객체로 활용해보려고 한다.
익명 객체를 이용한 파라미터화
interface ApplePredicate{
public boolean filter(Apple apple);
}
Java
복사
public static List<Apple> appleFilter(List<Apple> apples, ApplePredicate p){
List<Apple> result = new ArrayList<>();
for (Apple apple : Apples){
if (p.filter(apple)){
result.add(apple);
}
}
return result;
}
Java
복사
public static void main(String[] args) {
appleFilter(Apples, new ApplePredicate() { // 익명 객체 활용
@Override
public boolean filter(Apple apple) {
return apple.color.equals(Apple.Color.GREEN);
}
}).stream().forEach(apple -> System.out.println(apple.color));
appleFilter(Apples,new AppleWeightFilter()) // 상속 활용
.stream().forEach(apple -> System.out.println(apple.color));
}
Java
복사
여기서 내가 내린 결론은 결국 구현체는 만들어야 한다는 것이다. 이는 상당한 불편함을 초래할 수 있다. 또한 많은 익명 객체는 코드 가독성도 떨어뜨린다. 예를 들어 컬러필터가 한번만 사용된다면 내가 힘을 들여서 구현 클래스를 제작하는 것은 비효율적이다. 그렇다면 자바 8버전 이전에는 익명 객체를 활용하는 선택지 말고는 없었을 것이다. 이것은 가독성을 매우 떨어뜨리고 유지보수도 힘들게 할 것이다.
이를 해결한 람다
public static void main(String[] args) {
appleFilter(Apples, new ApplePredicate() {
@Override
public boolean filter(Apple apple) {
return apple.color.equals(Apple.Color.GREEN);
}
}).stream().forEach(apple -> System.out.println(apple.color));
appleFilter(Apples,new AppleWeightFilter())
.stream().forEach(apple -> System.out.println(apple.color));
appleFilter(Apples,(Apple apple) -> Apple.Color.GREEN.equals(apple.color));
}
Java
복사
놀랍지 않은가? 가독성 뿐만 아니라 놀라울 정도로 짧아지고 익명 객체 또한 사용할 필요가 없다. 이렇게 복잡성 문제를 해결할 수 있다.
또한 추상화를 통하여 문제를 해결 할 수 있다. 간단히 말해 사과 뿐 아니라 바나나, 두리안 등 다양한 과일을 동일한 필터를 거쳐야 한다면 그에 맞는 메서드를 전부 만들어야 할 것이다. 하지만 추상화 과정을 통하여 이를 해결할 수 있다.
추상화를 통한 문제해결
public static <T> List<T> filter(List<T> list, ApplePredicate<T> p){
List<T> result = new ArrayList<>();
for (T item : list){
if (p.filter(item)){
result.add(item);
}
}
return result;
}
Java
복사
interface ApplePredicate<T>{
public boolean filter(T item);
}
Java
복사