728x90

동작 파라미터화


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

참고 문헌 및 출처


728x90
728x90

자바 8, 9, 10, 11에서 일어난 일


자바 역사를 통틀어 가장 큰 변화가 자바 8에서 일어났다.
자바8은 간결한 코드, 멀티코어 프로세서의 쉬운 활용 이 두가지 요구사항을 기반으로 한다.


자바 8에서 제공하는 새로운 기술

  • 스트림 API
  • 메서드에 코드를 전달하는 방법
  • 인터페이스의 디폴트 메서드

스트림을 이용하면 에러를 자주 일으키며, 멀티코어 CPU를 이용하는 것보다 비용이 훨씬 비싼 키워드 synchronized를 사용하지 않아도 된다.

메서드에 코드를 전달하는 기법을 사용하면 동작 파라미터화(behavior parameterization)를 구현할 수 있다.

메서드에 코드를 전달(뿐만 아니라 결과를 반환하고 다른 자료구조로 전달할 수 도 있음)하는 자바 8 기법은 함수형 프로그래밍(functional-style programming)에서 위력을 발휘한다.


자바 8 설계의 밑바탕을 이루는 세가지 프로그래밍 개념


스트림 처리(stream processing)

  • 스트림이란 한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임이다.

  • 자바 8에는 java.util.stream 패키지에 스트림 API가 추가 되었다.

    • 스트림 패키지에 정의된 Stream는 T형식으로 구성된 일련의 항목을 의미한다.
    • 데이터소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들이 정의되어 있다.
  • 스트림 API의 핵심은 기존에는 한 번에 한 항목을 처리했지만, 우리가 하려는 작업을 데이터베이스 질의 처럼 고수준으로 추상화해서 일련의 스트림으로 만들어 처리할 수 있다는 것이다.

    • 또한 파이프라인을 이용해서 입력 부분을 여러 CPU 코어에 쉽게 할당할 수 있어 스레드라는 복잡한 작업을 사용하지 않으면서도 공짜로 병렬성을 얻을 수 있다.

메서드에 코드 전달하기

  • 자바 8에 추가된 두 번째 프로그램 개념은 코드 일부를 API로 전달하는 기능이다.
  • 기존에 자바는 메서드를 다른 메서드의 파라미터로 전달 할 수 없었다.
    • 물론 정렬기능을 위해서 익명함수 형태로 Comparator를 구현하는 방법도 있지만 복잡하다.
  • 자바 8에서는 메서드를 다른 메서드의 파라미터로 전달 할 수 있다.
    • 이러한 기능을 이론적으로 동작 파라미터화라고 부른다.
  • 동작 파라미터화가 중요한 이유는 스트림 API는 연산의 동작을 파라미터화할 수 있는 코드를 전달한다는 사상에 기초하기 때문이다.

병렬성과 공유 가변 데이터

  • 세 번째 프로그래밍의 개념은 병렬성을 공짜로 얻을 수 있다라는 말에서 시작된다.
  • 병렬성을 공짜로 얻기 위해서는 다른 한가지를 포기해야하는데, 스트림 메서드로 전달하는 코드의 동작 방식을 조금 바꿔야 한다. 처음에는 불편하지만 나중에는 편하게 느껴질 것이다.
  • 스트림 메서드로 전달하는 코드는 다른 코드와 동시에 실행하더라도 안전하게 실행될 수있어야 한다.

보통 다른 코드와 동시에 실행 하더라도 안전하게 실행할 수 있는 코드를 만들려면 가변 데이터(shared mutable data)에 접근하지 않아야 한다. 이러한 함수를 순수(pure) 함수, 부작용 없는 함수(side-effect-free), 상태 없는(stateless) 함수 라고 부른다.


자바 함수


일급 시민과 이급 시민

  • 프로그래밍 언어의 핵심은 값을 바꾸는 것이다.
  • 전통적으로 프로그래밍 언어에서는 이 값을 일급(first-class) 값 또는 시민(citizens) 이라고 부른다.
  • 자바 프로그래밍 언어의 다양한 구조체(메서드, 클래스 같은)가 값을 구조를 표현하는데 도움이 될 수 있다.
    • 하지만 프로그램을 실행하는 동안 이러한 모든 구조체를 자유롭게 전달할 수는 없다.
    • 이렇게 전달할 수 없는 구조체는 이급 시민이다.
  • 자바 8에서는 이급 시민을 일급 시민으로 바꿀 수 있는 기능을 추가했다.

그래서 1급 객체가 뭐지??

일급객체(First-class Object)란 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체를 가리킨다.

다음과 같은 조건을 가진다.

  • 변수에 할당(assignment)할 수 있다.
  • 다른 함수를 인자(argument)로 전달 받는다.
  • 다른 함수의 결과로서 리턴될 수 있다.

메서드와 람다를 일급 시민으로

  • 메서드 참조(method reference)

    • ::
  • 이를 통해 메서드가 이급값이 아닌 일급값이다.

  • 메소드 블럭의 메모리상 주소 값

  • 람다 : 익명함수

  • 자바 8에서는 메서드를 일급 값으로 취급할 뿐 아니라 람다(또는 익명함수 anonymous functions)를 포함하여 함수도 값으로 취급할 수 있다.

  • 람다 문법 형식으로 구현된 프로그램을 함수형 프로그래밍이라고 한다.


스트림


  • 거의 모든 자바 애플리케이션은 컬렉션을 만들고 활용하지만 컬렉션으로 모든 문제가 해결되는 것은 아니다.
  • 예를 들어 고가의 트랜잭션(transaction)(거래) 만 필터링한 다음에 통화로 결과를 그룹화 해야 한다고 가정하자. 아래와 같은 많은 기본 코드를 구현해야한다.
    Map<Curreny, List<Transaction>> transactionByCurrencies = new HashMap<>(); // 그룹화된 트랜잭션을 더할 Map 생성
    for(Transaction transaction : transactions) {
      if(transaction.getPrice() > 1000) {
        Curreny curreny = transacation.getCurrency(); // 트랜잭션의 통화를 추출
        List<Transcation> transactionsForCurrency = transactionsByCurrencies.get(currency);
        if(transactionsForCurrency == null) {
          transactionsForCurrency = new ArrayList<>();
          transactionsByCurrencies.put(currenc, transcationsForCurrency);
        }
        transactionsForCurrency.add(transacation);
      }
    }
  • 위의 예제는 중첩된 제어 흐름 문장이 많아서 코드를 한 번에 이해하기 어렵다.
  • 스트림 API를 이용하면 다음처럼 문제를 해결할 수 있다.
    import static java.util.stream.Collectors.groupingBy;
    Map<Currency, List<Transaction>> transactionsByCurrencies = transactions.stream()
                                                                  .filter((Transcations t) -> t.getPrice() > 1000); // 고가의 트랜잭션 필터링
                                                                  .collect(groupingBy(Transcation::geturrency)); // 통화로 그룹화

내부 반복과 외부 반복

  • 외부 반복(external iteration)은 for-each루프를 이용해서 각 요소를 반 작업을 수행하는 것들을 말한다.
  • 반면 내부 반복(internal iteration)은 스트림 API와 같이 루프를 신경 쓸 필요 없이, 스트림 API라는 라이브러리 내부에서 모든 데이터가 처리되는 것을 말한다.

멀티 스레딩은 어렵다

  • 자바 8은 스트림(API, java.util.stream)로 컬렉션을 처리하면서 발생하는 모호함과 반복적인 코드 문제멀티코어 활용 어려움이라는 두 가지 문제를 모두 해결했다.
  • 컬렉션은 어떻게 데이터를 저장하고 접근할지에 중점을 두는 반면 스트림은 데이터에 어떤 계산을 할 것인지 묘사하는 것에 중점을 둔다.

포킹단계(forking step)

  • 예를들어 두 CPU를 가진 환경에서 리스트를 필터링할 때 한 CPU는 앞 부분을 처리하고, 다른 CPU는 리스트의 뒷 부분을 처리하도록 요청할 수 있는데 이 과정을 포킹 단계라고 한다.
  • 각각의 cpu는 자신이 맡은 절반의 리스트를 처리하고, 마지막으로 하나의 cpu가 두 결과를 정리한다.

자바 8에서 제공하는 두 가지 요술방망이

  • 흔히 사람들은 자바의 병렬성은 어렵고 synchronized는 쉽게 에러를 일으킨다고 생각한다. 자바8은 어떤 요술방망이를 제공할까?
  • 자바 8은 두 가지 요술 방망이를 제공한다.
    • 우선 라이브러리에서 분할을 처리한다. 즉, 큰 스트림을 병렬로 처리할 수 있도록 작은 스트림으로 분할한다. 또한 filter 같은 라이브러리 메서드로 전달된 메서드가 상호작용을 하지 않는다면 가변 공유 객체를 통해 공짜로 병렬성을 누릴 수 있다.
    • 상호작용을 하지 않는다는 제약은 프로그래머 입장에서 상당히 자연스러운 일이다. 함수형 프로그래밍에서 함수란 함수를 일급값으로 사용한다라는 의미도 있지만, 부가적으로 프로그램이 실행되는 동안 컴포넌트 간에 상호작용이 일어나지 않는다라는 의미도 포함한다.

디폴트 메서드와 자바 모듈


  • 자바 9의 모듈 시스템은 모듈을 정의하는 문법을 제공하므로 이를 이용해 패키지 모음을 포함하는 모듈을 정의할 수 있다. 또한 자바 8에서는 인터페이스를 쉽게 바꿀 수 있도록 디폴트 메서드를 지원한다.
  • 디폴트 메서드는 특정 프로그램을 구현하는 데 도움을 주는 기능이 아니라 미래에 프로그램이 쉽게 변화할 수 있는 환경을 제공하는 기능이다.
  • 어떻게 기존의 구현을 고치지 않고도 이미 공개된 인터페이스를 변경할 수 있을까라는 딜레마를 디폴트 메서드가 해소시켜준다.
  • 기존에는 인터페이스에 메서드가 하나 추가되면 인터페이스를 사용하는 모든 곳에서 메서드를 추가해야하지만, 디폴트 메서드는 구현하지 않아도 되는 메서드이다.
    • 메서드 본문(bodies)은 클래스 구현이 아니라 인터페이스 일부로 포함된다.(그래서 이를 디폴트 메서드라고 한다.)

함수형 프로그래밍에서 가져온 다른 유용한 아이디어

  • 자바 8에서는 NullPointer 예외를 피할 수 있도록 도와주는 Optional<T> 클래스를 제공한다.
  • Optional는 값을 갖거나 갖지 않을 수 있는 컨테이너 객체이다.
  • 어떤 변수에 값이 없을 때 어떻게 처리할지 명시할 수 있다.

참고 문헌

728x90

'java' 카테고리의 다른 글

[모던 자바 인 액션] 람다 표현식  (0) 2024.07.07
[모던 자바 인 액션] 동작 파라미터화  (0) 2024.07.07
static inner vs non-static inner class  (0) 2024.07.07
자바 제네릭스  (0) 2024.07.07
java 버전별 차이 & 특징  (0) 2024.07.07

+ Recent posts