본문 바로가기
스터디

[모던 자바 인 액션] 1,2장. Java 8의 변화, 동작 파라미터화

by eunoo 2023. 6. 23.

1장 자바의 주요 변화

  • Java1.0 : 스레드, 락, 메모리 모델 지원
  • Java5 : 스레드 풀, 병렬 실행 컬렉션(concurrent collection) 등의 도구 도입
  • Java7 : 병렬 실행에 도움을 주는 포크/조인 프레임워크 제공
  • Java 8 : 스트림 API, 람다, 메서드 참조, 인터페이스의 디폴트 메서드
  • Java9 : 리액티브 프로그래밍(병렬 실행 기법, RxJava)
Java 8의 변화
  1. 스트림 API
    • 내부 반복으로 데이터를 처리한다.
    •  기존에는 한 번에 한 항목을 처리했지만 자바 8에서는 작업을(데이터베이스 질의처럼) 고수준으로 추상화하여 일련의 스트림으로 만들어 처리할 수 있다.
    • 입력 부분을 여러 CPU 코어에 쉽게 할당할 수 있다.
    • 스레드를 사용하지 않고 공짜로 병렬성을 얻을 수 있다.
    • 멀티쓰레드에서 리스트를 필터링할 때, 포킹 단계(forking step)으로 처리할 수 있다. 포킹 단계란 리스트의 앞부분은 한 CPU가, 리스트의 뒷 부분을 다른 CPU가 처리한 다음, 하나의 CPU가 두 결과를 합치는 방식이다.
    • 하지만 지금은 새로운 스트림 API도 컬렉션 API랑 동일하게 순차적으로 데이터 접근한다.
    •  
  2. 메서드에 코드를 전달하는 기법(메서드 참조, 람다)
    • 메서드의 파라미터에 메서드를 전달할 수 있다.
    • 메서드 참조 : 메서드가 이급값이 아닌 일급값. 더 간결한 코드 구현 가능
  3. 인터페이스의 디폴트 메서드
    • 이미 정의된 인터페이스를 쉽게 바꿀 수 있도록 디폴트 메서드 지원.
    • 기존 인터페이스를 변경하면 구현 클래스도 변경해야 한다. 구현 클래스에서 변경하지 않아도 인터페이스에 메서드를 추가할 수 있도록 디폴트 메서드가 고안되었다.
    • but 다이아몬드 상속 문제가 발생한다. 인터페이스의 다중 상속으로 인해 디폴트 메서드 충돌했을 때, 구현 클래스에서 오버라이딩을 해야 한다.
  4. Optional<T> : null 회피하는 기법

 

2장 동작 파라미터화 코드 전달하기

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

첫 번째 : 녹색 사과 필터링하기

public static List<Apple> filterGreenApples(List<Apple> inventory) {
    List<Apple> result = new ArrayList<>();
    for(Apple apple: inventory) {
        if(GREEN.equals(apple.getColor())) {
            result.add(apple);
        }
    }
    return result;
}

 

두 번째 : 색을 파라미터화

빨간 사과도 필터하고 싶다면 어떻게 해야할까? 파라미터로 Color를 받아 처리할 수 있다.

public static List<Apple> filterApples(List<Apple> inventory, Color color) {
    List<Apple> result = new ArrayList<>();
    for(Apple apple: inventory) {
        if(apple.getColor().equals(color)) {
            result.add(apple);
        }
    }
    return result;
}

 

세 번째 : 모든 속성으로 필터링

색깔말고 무게로도 필터링하고 싶다면 파라미터에 무게도 추가할 수 있다.

public static List<Apple> filterGreenApples(List<Apple> inventory, Color color,int weight, boolean flag) {
    List<Apple> result = new ArrayList<>();
    for(Apple apple: inventory) {
        if((apple.getColor().equals(color) && flag) || (apple.getWeight() > weight && !flag)) {
            result.add(apple);
        }
    }
    return result;
}
2.2 동작 파라미터

위의 코드에서 다양한 속성으로 필터링하기 위해선 계속 파라미터에 추가해야 한다는 문제가 발생한다. 이는 동작 파라미터를 사용해서 간단하게 작성할 수 있다.

우선 Predicate 인터페이스를 구현한다. Predicate 는 boolean을 반환하는 함수를 가지고 있다.

public interface ApplePredicate {
	boolean test(Apple apple);
}

public class AppleGreenColorPredicate implements ApplePredicate {
	@Override
    	public boolean test(Apple apple) {
       		 return GREEN.equals(apple.getColor());
   	 }
}

public class AppleRedAndHeavyPredicate implements ApplePredicate {
	@Override
    	public boolean test(Apple apple) {
       		 return RED.equals(apple.getColor() && apple.getWeight() > 150);
   	 }
}

이렇게 동작 파라미터를 사용하면 다양한 동작(전략)을 받아서 내부적으로 다양한 동작을 수행할 수 있다.

 

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

위에서 작성한 Predicate를 사용하여 필터 메소드를 수정해보자.

public static List<Apple> filterGreenApples(List<Apple> inventory, ApplePredicate p) {
    List<Apple> result = new ArrayList<>();
    for(Apple apple: inventory) {
        if(p.test(apple)) {
            result.add(apple);
        }
    }
    return result;
}

 

한 개의 파라미터(ApplePredicate)에 다양한 동작을 전달할 수 있다. 전달한 ApplePredicate 객체에 의해 filterApples 메서드의 동작이 결정된다. 이는 filterApples 메서드의 동작을 파라미터화한 것이다.

 

2.3 복잡한 과정 간소화

다음 코드는 동작 파라미터화를 적용시키고 실행하는 코드이다. filterApples 메서드를 호출할 때 ApplePredicate 구현 클래스를 생성한다.

// 무거운 사과 선택
public class AppleHeavyWeightPredicate implements ApplePredicate {
	public boolean test(Apple apple) {
    	return apple.getWeight > 150;
    }
}

//녹색 사과 선택
public class AppleGreenColorPredicate implements ApplePredicate {
	public boolean test(Apple apple) {
    	return GREEN.equals(apple.getColor());
    }
}

public class FilteringApples {

    public static void main(String... args) {
        List<Apple> inventory = Arrays.asList(
                                new Apple(80, Color.GREEN),
                                new Apple(155, Color.GREEN),
                                new Apple(120, Color.RED)
                                );

        List<Apple> heavyApples = filterApples(inventory, new AppleHeavyWeightPredicate());
        
        List<Apple> greenApples = filterApples(inventory, new AppleGreenPredicate());

      }
      
    public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) {
    	List<Apple> result = new ArrayList<>();
        for(Apple apple : inventory) {
          if(p.test(apple))  result.add(apple);
        }
        return result;
      
      }

}

 

다섯 번째 : 익명 클래스 사용하기

filterApples 메서드를 호출할 때, 익명 클래스를 사용하면 더 간단하게 작성할 수 있다.

public class FilteringApples {

    public static void main(String... args) {
        List<Apple> inventory = Arrays.asList(
                                new Apple(80, Color.GREEN),
                                new Apple(155, Color.GREEN),
                                new Apple(120, Color.RED)
                                );

        List<Apple> heavyApples = filterApples(inventory, new AppleHeavyWeightPredicate() {
        	@Override
            	public boolean test(Apple apple) {
            		return apple.getWeight() > 150;
            }
        
        });
        
        List<Apple> greenApples = filterApples(inventory, new AppleGreenPredicate() {
        	@Override
            	public boolean test(Apple apple) {
            		return GREEN.equals(apple.getColor());
            }
        });

      }
}

 

 

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

익명 클래스는 람다를 사용하면 더 간단히 작성할 수 있다.

public class FilteringApples {

    public static void main(String... args) {
       List<Apple> inventory = Arrays.asList(
                                new Apple(80, Color.GREEN),
                                new Apple(155, Color.GREEN),
                                new Apple(120, Color.RED)
                                );

       List<Apple> heavyApples = filterApples(inventory, (Apple apple) -> apple.getWeight() > 150);
        
       List<Apple> greenApples = filterApples(inventory, (Apple apple) -> GREEN.equals(apple.getColor()));

    }
}

 

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

 

작성해온 filterApples 메서드가 사과 말고도 바나나, 오렌지, 정수, 문자열 등의 다양한 데이터를 다룰 수 있도록 추상화하자.

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;
}

 

이제 다양한 타입을 가진 리스트에 filter 메서드를 사용할 수 있다.

List<Apple> redApples = filter(inventory, (Apple a) -> RED.equals(a.getColor()));

List<Integer> smallNumbers = filter(inventory, (Integer n) -> n < 10);

 

실제 자바 API에서 동작 파라미터화 사용

실제 자바 API에서 동작 파라미터화를 사용하는 경우를 살펴보자.

Comparator로 정렬하기

  • java.util.Comparator 인터페이스를 이용해서 sort의 동작을 파라미터화하기
public interface Comparator<T> {
    int compare(T o1, T o2);
}
inventory.sort( new Comparator<Apple>() {
        @Override
        public int compare(Apple a1, Apple a2) {
            return a1.getWeight().compareTo(a2.getWeight());
        }
    }
);

//람다로 작성
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

 

Runnable로 코드 블록 실행하기

Thread tread = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("hello");
        }
    }
);

//람다로 작성
Thread tread = new Thread(() -> System.out.println("hello"));

 

GUI 이벤트 처리하기

  • ExecutorService 인터페이스 : 태스크 제출과 실행 과정의 연관성을 끊어준다.
    • 태스크를 스레트 풀로 보내고 결과를 Future로 저장한다.
    • Callable 인터페이스를 이용해 결과를 반환하는 태스크를 만든다.
//java.util.concurrent.Callable
public interface Callble<V> {
    V call();
}
ExecutorService es = ExecutorService.newCachedThreadPool();
Future<String> threadName = es.submit(new Callable<String>() {
        @Override
        public String call() throws Exception {
            return Thread.currentThread().getName();
        }
    }
);

//람다로 작성
Future<String> threadName = es.submit(() -> Thread.currentThread().getName(););

댓글