본문 바로가기

프로그래밍/Book

[모던 자바 인 액션] 2장. 동작 파라미터화

  • 소프트웨어 엔지니어링에서 요구사항은 항상 변한다.
  • 시시각각 변하는 요구사항을 반영하면서 엔지니어링적인 비용이 가장 최소화될 수 있으면 좋다.
  • 새로 추가한 기능은 쉽게 구현할 수 있어야 하며 장기적인 관점에서 유지보수가 쉬워야 한다.

변화하는 요구사항에 대응하기

간단한 도서관리 애플리케이션의 예시로 원하는 책들을 찾는 프로그램을 개발한다고 가정하였다.
Book 클래스는 아래와 같다.

public class Book {
    private String name;    //책이름
    private String author;  //저자
    private Integer year;   //출판 연도
    private String field;   //분야

    public Book(String name, String author, Integer year, String field) {
        this.name = name;
        this.author = author;
        this.year = year;
        this.field = field;
    }
    /// getter 생략
}

첫 번째 시도: IT 분야의 책 필터링

자바8 이전에 books 리스트에서 IT 분야의 책들을 찾으려면 아래와 같이 할 수 있었다.

public class Main {
    public static void main(String[] args) {
        List<Book> books = initBooks();
        List<Book> itBooks = filterITBooks(books);
    }
    public static List<Book> filterITBooks(List<Book> inventory) {
        List<Book> result = new ArrayList<>();
        for (Book book : inventory) {
            if ("IT".equals(book.getField())) {
                result.add(book);
            }
        }
        return result;
    }

    private static List<Book> initBooks() {
        return asList(new Book("이펙티브 자바", "조슈아 블로크", 2018, "IT"),
                new Book("모던 자바 인 액션", "라울-게이브리얼 우르마", 2019, "IT"),
                new Book("이순신의 바다", "황현필", 2021, "History"),
                new Book("갈매기의 꿈", "리처드 바크", 2020, "Fiction")
        );
    }
}

요구사항이 변하여 it 분야 대신 History 분야도 필터링하고 싶으면 비슷한 메소드를 만들고 if문을 수정할 수 있지만 이후 추가되는 분야에 대한 거의 비슷한 메소드를 여럿 만들어야 한다.

거의 비슷한 코드가 반복 존재한다면 그 코드를 추상화해야 한다.

두 번째 시도: 분야를 파라미터화

책의 분야를 파라미터화하여 매번 메소드를 추가하지 않아도 되도록 하였다.

public static List<Book> filterBooksByField(List<Book> inventory, String field) {
        List<Book> result = new ArrayList<>();
        for (Book book : inventory) {
            if (book.getField().equals(field)) {
                result.add(book);
            }
        }
        return result;
    }

하지만 이 경우도 분야 이외에 출판 연도나 저자와 같은 다른 조건에 대한 요구사항이 들어올 수 있다.

세 번째 시도: 모든 분야를 파라미터화

public static List<Book> allFilterBooks(List<Book> inventory, String name, String author, int year, String field) {
        List<Book> result = new ArrayList<>();
        for (Book book : inventory) {
            if (book.getName().equals(name) || book.getAuthor().equals(author) || book.getYear() == year || book.getField().equals(field)) {
                result.add(book);
            }
        }
        return result;
    }

모든 파라미터를 추가 하였다...
하지만 이후 요구사항이 변하면 그에 대응할 수 없으며 결국 여러 중복된 필터 메서드를 만들거나 모든 것을 처리하는 필터 메서드를 구현해야한다.
다음번엔 동작 파라미터화를 통해 유연성을 얻어보자.

동작 파라미터화

책의 어떤 속성에 기초하여 boolean 값을 반환할는 방법이 있다.(책이 역사책인가?)
참 또는 거짓을 반환하는 함수를 Predicate라고 하는데 선택 조건을 결정하는 인터페이스를 정의하였다.

//인터페이스 정의
public interface BookPredicate {
    boolean test(Book book);
}

//IT 책 선택
public class ITBookPredicate implements BookPredicate {
    @Override
    public boolean test(Book book) {
        return "IT".equals(book.getField());
    }
}

//
public class Recently3YearPublishedBookPredicate implements BookPredicate {
    @Override
    public boolean test(Book book) {
        int nowYear = LocalDateTime.now().getYear();
        return nowYear - book.getYear() <= 3;
    }
}

네 번째 시도: 추상적 조건으로 필터링

public static List<Book> filterBooks(List<Book> inventory, BookPredicate predicate) {
        List<Book> result = new ArrayList<>();
        for (Book book : inventory) {
            if (predicate.test(book)) {
                result.add(book);
            }
        }
        return result;
    }

이제 predicate를 적절히 구현하여 filterBooks에 전달하면 된다.

List<Book> itBooks = filterBooks(books, new ITBookPredicate());

좀 더 유연성을 얻었으나 filterBooks에 매번 다른 동작을 추가하려면 BooksPredicate의 구현체를 만들어야 하는데 로직과 관련 없는 코드가 추가되었다.
Java에서는 클래스의 선언과 인스턴스화를 동시에 수행할 수 있도록 익명클래스라는 기법을 제공한다.

다섯 번째 시도: 익명 클래스 사용

List<Book> itBooks = filterBooks(books, new BookPredicate() {
            @Override
            public boolean test(Book book) {
                return "IT".equals(book.getField());
            }
        });

위의 예시에서 알 수 있듯 익명클래스는 여러 클래스를 선언하는 과정은 줄일 수 있으나 코드가 매우 장황하다. 이를 자바8에서 등장한 람다 표현식을 사용하여 간결하게 정리할 수 있다.

여섯 번째 시도 : 람다 표현식 사용

List<Book> itBooks = filterBooks(books, book -> "IT".equals(book.getField()));

코드가 훨씬 간단해졌다.

일곱 번째 시도: 리스트 형식으로 추상화

public interface Predicate<T> {
  boolean test(T t);
}

//형식파라미터T 등장
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
    List<T> result = new ArrayList<>();
    for (T e : list) {
        if (p.test(e)) {
            result.add(e);
        }
    }
    return result;
}

이제 리스트에 필터 메서드를 사용할 수 있다.

List<Book> itBooks = filter(books, book -> "IT".equals(book.getField()));
List<Book> books2021 = filter(books, book -> 2021 == book.getYear());
List<Book> JoshuaBook = filter(books, book -> "조슈아 블로크".equals(book.getAuthor()));

참고문서

모던 자바 인 액션 - 라울 게이브리얼 우르마, 마리오 푸스코, 앨런 마이크로소포트