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

이주오·2021년 8월 9일
0

도서

목록 보기
2/15

동작 파라미터화


소비자의 요구사항은 항상 바뀐다. 이런 변화하는 요구사항에 대해 효과적으로 대응하기 위해서 동작 파라미터화(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에서 구현되기 때문에 인터페이스의 추상 메서드 개수에도 포함되지 않는다.

참고 문헌 및 출처


profile
동료들이 같이 일하고 싶어하는 백엔드 개발자가 되고자 합니다!

0개의 댓글