동작 파라미터화
소비자의 요구사항은 항상 바뀐다. 이런 변화하는 요구사항에 대해 효과적으로 대응하기 위해서 동작 파라미터화(behavior parameterization) 를 이용하면 좋다.
- 동작 파라미터화란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미한다.
- 코드 블록은 나중에 프로그램에서 호출한다.
- 즉, 코드 블록의 실행은 나중으로 미뤄진다.
- 따라서 메서드에 코드 블록을 인수로 전달할 수 있다면??
- 메서드의 동작이 코드 블록 파라미터에 의해 결정된다.
- 즉 동작이 파라미터화 된다
변화하는 요구사항 대응의 발전 단계
영화 관련 객체를 필터링하는 예시
자바 인 액션 책의 예제와 비슷한 예제로 해보았다.
public class Movie {
private String name;
private String director;
private Genre genre;
private int rating;
// getters and setters omitted...
}
- 영화를 평점으로 필터링하는 기능을 추가해보자!!
첫 번째 시도 : rating을 파라미터화
public static List<Movie> filterMoviesByRating(List<Apple> movies, int rating) {
List<Movie> result = new ArrayList<>();
for(Movie movie : movies) {
if(movie.getRating() > rating) {
result.add(movie);
}
}
return result;
}
- 하지만 사용자가 평점 이외에도 감독, 장르, 등등 계속해서 추가되는 필터 조건이 생긴다면 어떻게 해야 할까?
두번째 시도: 가능한 모든 속성으로 필터링
절대 사용하지 말아야 하는 방법
public static List<Movie> filterMovies(List<Apple> movies,int rating, Genre genre, boolean flag) {
List<Movie> result = new ArrayList<>();
for(Movie movie : movies) {
if((flag && movie.getRating() > rating) ||
(!flag && movie.getGenre().equals(genre)){
result.add(movie);
}
}
return result;
}
List<Movie> TopRatedMovies = filterMovies(movies, 4, null, true);
List<Movie> horrorMovies = filterMovies(movies, 4, HORROR, false);
- 메서드의 파라미터가 계속해서 늘어날 것이다.
- 코드의 가독성이 매우 떨어지고 직관적이지 못하다.
- 요구사항을 유연하게 대응할 수도 없는 구조
- 계속 필터 조건이 추가된다면?
- 중복된 필터 메서드를 여러개 만들거나
- 위 처럼 하나의 거대한 필터 메서드를 구현해야 한다.
- 요구사항의 변경이 없고 정해져있다면 혹시 괜찮을 수 있다고 생각하지만 보통은 아니기에 코드의 비용이 너무 크다.
세번째 시도: 동작 파라미터화
기존까지는
값을 파라미터화
했다면 요구사항 변화에 유연하게 대응하도록동작을 파라미터화
를 사용해보자!
선택조건을 결정하는 인터페이스 정의
public interface MoviePredicate {
boolean test(Movie movie);
}
public class TopRatedMoviePredicate implements MoviePredicate {
public boolean test(Movie movie) {
return movie.getRating() > 4;
}
}
public class HorrorMoviePredicate implements MoviePredicate {
public boolean test(Movie movie) {
return movie.getGenre().equals(HORROR);
}
}
public class MovieDirectorPredicate implements MoviePredicate {
private String director;
public MovieDirectorPredicate(String director) {
this.director = director;
}
public boolean test(Movie movie) {
return movie.getDiretor().equals(director);
}
}
- 이제 다양한 필터 조건을 대표하는 여러 MoviePredicate를 구현할 수 있다.
- 위 조건에 따라 filter 메서드가 다르게 동작할 것이라고 예상할 수 있다.
- 이를
전략 디자인패턴(strategy design patter)
이라고 부른다. - 전략 디자인 패턴은 각 알고리즘(전략이라 부르는)을 캡슐화 하는 알고리즘 패밀리를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법이다.
- 여기서는 MoviePredicate가 알고리즘 패밀리이며, 이를 구현한 클래스들이 전략이다.
public static List<Movie> filterMovies(List<Apple> movies, MoviePredicate p) {
List<Movie> result = new ArrayList<>();
for(Movie movie : movies) {
if(p.test(movie)) {
result.add(movie);
}
}
return result;
}
List<Movie> horrorMovies = filterMovies(movies, new HorrorMoviePredicate());
List<Movie> bongMovies = filterMovies(movies, new MovieDirectorPredicate("Bong"));
- filterMovies 메서드의 동작을 파라미터화했다.
- 더 유연한 코드를 얻었으며 동시에 가독성도 좋아졌을 뿐 아니라 사용하기도 쉬워 졌다.
- 즉, 전략 디자인 패턴(Strategy Design Pattern)과 동작 파라미터화를 통해서 필터 메서드에 전략(Strategy)을 전달 함으로써 더 유연한 코드를 만들었다.
- 하지만 filterMovies에 새로운 동작을 전달하려면 MoviePredicate를 구현하는 클래스를 정의하고 인스턴스화 해야한다.
- 이러한 코드들은 로직과 관련없는 코드들이며 복잡해보인다.
- 이를 개선 하기 위해서 자바는
클래스의 선언과 인스턴스화
를 동시에 수행할 수 있도록익명 클래스
를 이용하여 개선 가능하다.
네 번째 시도 : 익명 클래스 사용
클래스의 선언과 객체의 생성을 동시에 하는 이름없는 클래스(일회용)
List<Movie> TopRatedMovies = filterMovies(movies, new MoviePredicate() {
public boolean test(Movie movie) { // but 여기 까지 필요없는 코드
return movie.getRating() > 4;
}
});
List<Movie> horrorMovies = filterMovies(movies, new MoviePredicate() {
public boolean test(Movie movie) { // but 여기 까지 필요없는 코드
return HORROR.equals(movie.getGenre());
}
});
List<Movie> bongMovies = filterMovies(movies, new MoviePredicate() {
public boolean test(Movie mioive) { // but 여기 까지 필요없는 코드
return "Bong".equals(movie.getDirector())
}
});
- 하지만 아직도 코드가 길고 장황하며 코드들은 여전히 보일러 플레이트이다.
다섯 번째 시도 : 람다 표현식 사용
MoviePredicate는 함수형 인터페이스이므로 람다 표현식 사용 가능하다.
List<Movie> horrorMovies = filterMovies(movies, (Movie m) -> HORROR.equals(movie.getGenre());
List<Movie> bongMovies = filterMovies(movies, (Movie m) -> "Bong".equals(movie.getDirector());
- 마지막으로 지네릭을 이용해 참조 타입에 관계없이 추상적인 리스트의 필터 메소드를 만들 수 있다.
public interface Predicate<T> {
boolean test(T t);
}
public static 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;
}
- 람다를 사용해서 훨씬 간결해졌으며 복잡성 문제를 해결하였다!
요약
- 동작 파라미터화는 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달한다.
- 동작 파라미터화를 이용하면 의존성을 줄여 요구사항에 유연하고 유지보수가 용이한 코드작성이 가능하다.
- 코드 전달 기법을 이용해 메서드의 인수로 전달할 수 있으나 자바8 이전까지는 코드가 간결하지 못했으나 람다를 이용하여 더욱 간결하고 직관적으로 구현할 수 있게 되었다.
공부하며 궁금했던점
Comparator 인터페이스는 왜 함수형 인터페이스인가?
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
}
- equals()는 java.lang.Object에서 오버라이딩 되는 메서드들 중 하나이다.
- 따라서 인터페이스가 java.lang의 공용 메서드 중 하나를 오버라이딩되는 추상 메서드를 선언하는 경우
- 인터페이스 구현 시 java.lang에서 구현되기 때문에 인터페이스의 추상 메서드 개수에도 포함되지 않는다.
참고 문헌 및 출처
'java' 카테고리의 다른 글
[모던 자바 인 액션] 스트림 소개 (0) | 2024.07.07 |
---|---|
[모던 자바 인 액션] 람다 표현식 (0) | 2024.07.07 |
[모던 자바 인 액션] 자바 8, 9, 10, 11에서 일어난 일 (0) | 2024.07.07 |
static inner vs non-static inner class (0) | 2024.07.07 |
자바 제네릭스 (0) | 2024.07.07 |