Modern Java in Action - Ch.9

참고: 책 - Modern Java in Action

책 Modern Java in Action을 읽고 정리합니다. 이번 포스트에서는 Ch 9.1 ~ Ch 9.4의 내용을 읽고 정리합니다.

Ch 9. 리팩터링, 테스팅, 디버깅
9.1 가독성과 유연성을 개선하는 리팩터링
- 9.1.1 코드 가독성 개선
- 9.1.2 익명 클래스를 람다 표현식으로 리팩터링하기
- 9.1.3 람다 표현식을 메서드 참조로 리팩터링
- 9.1.4 명령형 데이터 처리를 스트림으로 리팩터링하기
- 9.1.5 코드 유연성 개선
9.2 람다로 객체지향 디자인 패턴 리팩터링하기
- 9.2.1 전략
- 9.2.2 템플릿 메서드
- 9.2.3 옵저버
- 9.2.4 의무 체인
- 9.2.5 팩토리
9.3 람다 테스팅
- 9.3.1 보이는 람다 표현식의 동작 테스팅
- 9.3.2 람다를 사용하는 메서드의 동작에 집중하라
- 9.3.3 복잡한 람다를 개별 메서드로 분할하기
- 9.3.4 고차원 함수 테스팅
9.4 디버깅
- 9.4.1 스택 트레이스 확인
- 9.4.2 정보 로깅

기존 코드를 이용해서 새로운 프로젝트를 시작하는 상황을 가정할 때, 람다 표현식을 이용해 가독성과 유연성을 높이려면 기존 코드를 어떻게 리팩터링해야 하는지 설명합니다. 또한 람다 표현식으로 객체지향 디자인 패턴을 어떻게 간소화할 수 있는지 알아보고 람다 표현식과 스트림 API를 사용하는 코드를 테스트하고 디버깅하는 방법을 설명하도록 하겠습니다.


9.1 가독성과 유연성을 개선하는 리팩터링

람다 표현식은 익명 클래스보다 코드를 좀 더 간결하게 만들며 동작 파라미터화의 형식을 지원하므로 다양한 요구사항 변화에 대응할 수 있도록 동작을 파라미터화합니다.



9.1.1 코드 가독성 개선

일반적으로 코드 가독성이 좋다는 것은 ‘어떤 코드를 다른 사람도 쉽게 이해할 수 있음’을 의미합니다. 즉, 코드 가독성을 개선한다는 것은 우리가 구현한 코드를 다른 사람이 쉽게 이해하고 유지보수할 수 있게 만드는 것을 의미합니다. 코드 가독성을 높이려면 코드의 문서화를 잘하고, 표준 코딩 규칙을 준수하는 등의 노력을 기울여야 합니다.

자바 8의 새 기능을 이용해 코드의 가독성을 높일 수 있고 코드를 간결하고 이해하기 쉽게 만들 수 있습니다. 또한 메서드 참조 스트림 API를 이용해 코드의 의도를 명확하게 보여줄 수 있습니다.



9.1.2 익명 클래스를 람다 표현식으로 리팩터링하기

하나의 추상 메서드를 구현하는 익명 클래스는 람다 표현식으로 리팩터링할 수 있습니다. 익명 클래스는 코드를 장황하게 만들고 쉽게 에러를 일으킬 수 있어 람다 표현식을 이용해서 간결하고, 가독성이 좋은 코드를 구현할 수 있습니다.

ex. Runnable 객체를 만드는 익명 클래스와 이에 대응하는 람다 표현식을 비교하는 예제 입니다.

Runnable r1 = new Runnable() {
  @Override
  public void run() {
    System.out.println("Hello");
  }
};

Runnable r2 = () -> System.out.println("Hello");

하지만 모든 익명 클래스를 람다 표현식으로 변환할 수 있는 것은 아닙니다.

1) 익명 클래스에서 사용한 this와 super는 람다 표현식에서 다른 의미를 갖습니다.

ex. Shadow Variable 예제입니다.

int a = 10;
Runnable r1 = () -> {
    int a = 2; // 컴파일 에러
    System.out.println(a);
};
    
Runnable r2 = new Runnable() {
  @Override
  public void run() {
    int a = 2; // 정상 동작
    System.out.println(a);
  }
};

2) 익명 클래스는 감싸고 있는 클래스의 변수를 가릴 수 있지만 (섀도 변수 shadow variable) 람다 표현식으로는 변수를 가릴 수 없습니다.

3) 익명 클래스를 람다 표현식으로 바꾸면 콘텍스트 오버로딩에 따른 모호함이 초래될 수 있습니다.



9.1.3 람다 표현식을 메서드 참조로 리팩터링하기

람다 표현식은 쉽게 전달할 수 있는 짧은 코드입니다. 하지만 람다 표현식 대신 메서드 참조를 이용하면 가독성을 높일 수 있는데 메서드명으로 코드의 의도를 명확하게 알릴 수 있기 때문입니다.

ex. 칼로리 수준으로 요리를 그룹화하는 예제입니다.

Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = 
        menu.stream()
            .collect(
                groupingBy(dish -> {
                  if (dish.getCalories() <= 400) return CaloricLevel.DIET;
                  else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
                  else return CaloricLevel.FAT;
}));
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = 
        menu.stream().collect(groupingBy(Dish::getCaloricLevel));

public class Dish {
  public CaloricLevel getCaloricLevel() {
    if (this.getCalories() <= 400) return CaloricLevel.DIET;
    else if (this.getCalories() <= 700) return CaloricLevel.NORMAL;
    else return CaloricLevel.FAT;
  }
}
//before
inventory.sort( 
        (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())); 

//after - comparing 활용
inventory.sort(comparing(Apple::getWeight)); // 코드가 문제 자체를 설명

ex. 저수준 리듀싱 연산을 조합한 예제

int totalCalories = menu.stream().map(Dish::getCalories) 
        .reduce(0, (c1, c2) -> c1 + c2);

ex. 컬렉터 summingInt를 사용하는 예제입니다.(자신이 어떤 동작을 수행하는지 메서드 이름으로 설명함)

int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));


9.1.4 명령형 데이터 처리를 스트림으로 리팩터링하기

이론적으로는 반복자를 이용한 기존의 모든 컬렉션 처리 코드를 스트림 API로 바꿔야 하는데 스트림 API는 데이터 처리 파이프라인의 의도를 더 명확하게 보여주기 때문입니다. 스트림은 short-circuiting(모든 스트림의 요소를 처리하지 않고도 원하는 결과를 찾았으면 즉시 반환)과 laziness(여러 중간 연산을 연결해 질의를 할 수 있으며, 단말 연산을 스트림 파이프라인에 실행하기 전까지 아무 연산을 수행하지 않음)라는 강력한 최적화뿐 아니라 멀티코어 아키텍처를 활용할 수 있는 지름길을 제공합니다.

ex. 필터리과 추출로 엉킨 명령형 코드를 개선하는 예제 입니다.

//Before
List<String> dishNames = new ArrayList(); 
for (Dish dish: menu) { 
  if (dish.getCalories() > 300) {
    dishNames.add(dish.getName());
  }   
}
//After
menu.parallelStream()
        .filter(d -> d.getCalories()> 300)
        .map(Dish::getName)
        .collect(toList());



9.1.5 코드 유연성 개선

람다 표현식을 이용하면 동작 파라미터화 behaviour parameterization를 쉽게 구현할 수 있어 다양한 람다를 전달해서 다양한 동작을 표현할 수 있습니다. 따라서 변화하는 요구사항에 대응할 수 있는 코드를 구현할 수 있습니다(예를 들어 프레디케이트로 다양한 필터링 기능을 구현하거나 비교자로 다양한 비교 기능을 만들 수 있습니다).

함수형 인터페이스 적용

먼저 람다 표현식을 이용하려면 함수형 인터페이스가 필요합니다. 따라서 함수형 인터페이스를 코드에 추가해야 합니다.

조건부 연기 실행

실제 작업을 처리하는 코드 내부에 제어 흐름문이 복잡하게 얽힌 코드를 흔히 볼 수 있습니다. 흔히 보안 검사나 로깅 관련 코드가 이처럼 사용됩니다.

ex. 다음은 내장 자바 Logger 클래스를 사용하는 예제입니다.

if (logger.isLoggable(Log.FINER)){ 
    logger.finer("Problem: " + generateDiagnostic()); 
}

위 코드의 문제는?

logger.log(Level. FINER, "Problem: " + generateDiagnostic());
public void log(Level level, Supplier<String> msgSupplier)

...

logger.log(Level.FINER, ()-> "Problem: " + generateDiagnostic());
public void log (Level level, Supplier<String> msgSupplier){
  if(logger.isLoggable(level)) {
    log(level,msgSupplier.get());-람다 실행
  }
}

실행 어라운드

매번 같은 준비, 종료 과정을 반복적으로 수행하는 코드가 있다면 이를 람다로 변환할 수 있는데 준비, 종료 과정을 처리하는 로직을 재사용함으로써 코드 중복을 줄일 수 있습니다.

ex. 파일을 열고 닫을 때 같은 로직을 사용했지만 람다를 이용해서 다양한 방식으로 파일을 처리할 수 있도록 파라미터화되는 예제입니다.

String oneLine = processFile((BufferedReader b) -> b.readLine()); //람다 전달 
String twolines = processFile((BufferedReader b) -> b.readLine() + b.readLine()); //다른 람다 전달 

public static String processFile (Buffered Reader Processor p) throws IOException {
    try(BufferedReader br = new BufferedReader(new FileReader("ModernJavaInAction/chap/data.txt") {
        return p.process(br); //인수로 전달된 BufferedReaderProcessor를 실행
    }  
}

public interface Buffered Reader Processor {
    String process (BufferedReader b) throws IOException;  //IOException을 던질 수 있는 람다의 함수형 인터페이스 
}




9.2 람다로 객체지향 디자인 패턴 리팩터링하기

다양한 패턴을 유형별로 정리한 것이 디자인 패턴 입니다. 디자인 패턴은 공통적인 소프트웨어 문제를 설계할 때 재사용할 수 있는 검증된 청사진을 제공하는데 디자인 패턴에 람다 표현식이 더해지면 색다른 기능을 발휘할 수 있습니다. 즉, 람다를 이용하면 이전에 디자인 패턴으로 해결하던 문제를 더 쉽고 간단하게 해결할 수 있습니다. 또한 람다 표현식으로 기존의 많은 객체지향 디자인 패턴을 제거하거나 간결하게 재구현할 수 있습니다.



9.2.1 전략

전략 패턴은 한 유형의 알고리즘을 보유한 상태에서 런타임에 적절한 알고리즘을 선택하는 기법입니다. 다양한 기준을 갖는 입력값을 검증하거나, 다양한 파싱 방법을 사용하거나, 입력 형식을 설정하는 등 다양한 시나리오에 전략 패턴을 활용할 수 있습니다.

image

ex. 오직 소문자 또는 숫자로 이루어져야 하는 등 텍스트 입력이 다양한 조건에 맞게 포맷되어 있는지 검증하는 예제입니다.

public interface ValidationStrategy {
  boolean execute(String s);
}
public class IsAllLowerCase implements ValidationStrategy {
  public boolean execute(String s) {
    return s.matches("[a-z]+");
  }
}

public class IsNumeric implements ValidationStrategy {
  public boolean execute(String s) {
    return s.matches("\\d+");
  }
}
public class Validator {
  private final ValidationStrategy strategy;

  public Validator(ValidationStrategy v) {
    this.strategy = v;
  }

  public boolean validate(String s) {
    return strategy.execute(s);
  }
}

Validator numericValidator = new Validator(new IsNumeric()); 
boolean b1 = numericValidator.validate("aaaa"); //false 반환 
Validator lowerCaseValidator = new Validator(new IsAllLowerCase ());
boolean b2 = lowerCaseValidator.validate("bbbb"); //true 반환


람다 표현식 사용

ValidationStrategy는 함수형 인터페이스 Predicate<String>과 같은 함수 디스크립터를 갖고 있습니다. 다양한 전략을 구현하는 새로운 클래스를 구현할 필요없이 람다 표현식을 직접 전달하면 코드가 간결해집니다.

Validator numericValidator = new Validator((String s) -> s.matches("[a-z]+")); 
boolean b1 = numericValidator.validate("aaaa"); //람다를 직접 전달
Validator lowerCaseValidator = new Validator((String s) -> s.matches("\\d+")); 
boolean b2 = lowerCaseValidator.validate("bbbb"); //람다를 직접 전달


9.2.2 템플릿 메서드

알고리즘의 개요를 제시한 다음에 알고리즘의 일부를 고칠 수 있는 유연함을 제공해야 할 때 템플릿 메서드 디자인 패턴을 사용합니다. 템플릿 메서드는 ‘이 알고리즘을 사용하고 싶은데 그대로는 안 되고 조금 고쳐야 하는 상황’에 적합합니다.

ex. 간단한 온라인 뱅킹 애플리케이션 예제입니다.

  • 사용자가 고객 ID를 애플리케이션에 입력하면 은행 데이터베이스에서 고객 정보를 가져오고 고객이 원하는 서비스를 제공할 수 있습니다.
  • 예를 들어 고객 계좌에 보너스를 입금한다고 가정하고, 은행마다 다양한 온라인 뱅킹 애플리케이션을 사용하며 동작 방법도 다릅니다.
// 온라인 뱅킹 애플리케이션의 동작을 정의하는 추상 클래스
abstract class OnlineBanking {

  public void processCustomer(int id) {
    Customer c = Database.getCustomerWithId(id);
    makeCustomerHappy(c);
  }

  abstract void makeCustomerHappy(Customer c);

  // 더미 Customer 클래스
  static private class Customer {}

  // 더미 Database 클래스
  static private class Database {

    static Customer getCustomerWithId(int id) {
      return new Customer();
    }

  }

}

람다 표현식 사용

public void processCustomer(int id, Consumer<Customer> makeCustomerHappy) {
  Customer c = Database.getCustomerWithId(id);
  makeCustomerHappy.accept(c);
}
new OnlineBankingLambda().processCustomer(1337, (Customer c) -> 
        System.out.println("Hello " + c.getName());



9.2.3 옵저버

image

ex. 옵저버 패턴으로 트위터 같은 커스터마이즈된 알림 시스템을 설계하는 예제입니다.

interface Observer {
    void notify(String tweet);
}
class NYTimes implements Observer {
  public void notify(String tweet) {
    if (tweet != null && tweet.contains("money")) {
      System.out.println("Breaking news in NY! " + tweet);
    }
  }
}

class Guardian implements Observer { 
    public void notify(String tweet) { 
        if (tweet != null && tweet.contains("queen")) {
          System.out.println("Yet more news from London..." + tweet);
        }
    }
}

class LeMonde implements Observer {
  public void notify(String tweet) {
    if (tweet != null && tweet.contains("wine")) {
      System.out.println("Today cheese, wine and news! " + tweet);
    }
  }
}
interface Subject {
  void registerObserver(Observer o);

  void notifyObservers(String tweet);
}
class Feed implements Subject {
  private final List<Observer> observers = new ArrayList();

  public void registerObserver(Observer o) {
    this.observers.add(o);
  }

  public void notifyObservers(String tweet) {
    observers.forEach(o -> o.notify(tweet));
  }
}
Feed f = new Feed(); 
f.registerObserver(new NYTimes());
f.registerObserver(new Guardian());
f.registerObserver(new LeMonde());
f.notifyObservers("The queen said her favourite book is Modern Java in Action!");

람다 표현식 사용하기

f.registerObserver((String tweet) -> {
    if(tweet!=null&&tweet.contains("money")) {
        System.out.println("Breaking news in NY! "+ tweet);
    }
}); 

f.registerObserver((String tweet) -> {
    if(tweet != null && tweet.contains("queen")) { 
        System.out.println("Yet more news from London... " + tweet); 
    }
});



9.2.4 의무 체인

ex. 작업 처리 예제 코드입니다.

public abstract class ProcessingObject<T> { 
    protected ProcessingObject<T> successor; 
    public void setSuccessor(ProcessingObject<T> successor) {
        this.successor = successor;
    }
    public T handle(T input) { 
        T r = handlework(input); 
        if (successor != null) { 
            return successor.handle(r); 
        } 
        return r;
    }
    abstract protected T handleWork(T input);
}

image

ex. 텍스트를 처리하는 예제입니다.

public class HeaderTextProcessing extends ProcessingObject<String> { 
    public String handleWork(String text) {
        return "From Raoul, Mario and Alan: " + text;
    } 
}

public class SpellCheckerProcessing extends ProcessingObject<String> {
    public String handleWork(String text) {
        return text.replaceAll("labda", "lambda"); 
    }
}
ProcessingObject<String> p1 = new HeaderTextProcessing();
ProcessingObject<String> p2 = new SpellCheckerProcessing(); 
p1.setSuccessor(p2); //두 작업 처리 객체를 연결
String result = p1.handle("Aren't labdas really sexy?!!"); 
System.out.println(result); //From Raoul, Mario and Alan: Aren't lambdas really sexy?!! 출력

람다 표현식 사용

UnaryOperator<String> headerProcessing = (String text) -> "From Raoul, Mario and Alan: " + text; //첫 번째 작업 처리 객체
UnaryOperator<String> spellCheckerProcessing = (String text) -> text.replaceAll("labda", "lambda"); //두 번째 작업 처리 객체
Function<String, String> pipeline = headerProcessing.andThen(spellCheckerProcessing); //동작 체인으로 두 함수를 조합
String result = pipeline.apply("Aren't labdas really sexy?!!");

9.2.5 팩토리

인스턴스화 로직을 클라이언트에 노출하지 않고 객체를 만들 때 팩토리 자인 패턴을 사용합니다.

ex. 은행에서 취급하는 대출 채권, 주식 등 다양한 상품을 만드는 상황을 가정한 예제입니ㅏㄷ.

public class ProductFactory {
    public static Product createProduct(String name) {
        switch (name) {
          case "loan":
            return new Loan();
          case "stock":
            return new Stock();
          case "bond":
            return new Bond();
          default:
            throw new RuntimeException(" No such product " + name);
        }
    }
}

람다 표현식 사용

ex. Loan 생성자를 사용하는 코드입니다.(생성자도 메서드 참조처럼 접근 가능)

Supplier<Product> loanSupplier = Loan::new; 
Loan loan = loanSupplier.get();
final static Map<String, Supplier<Product>> map = new HashMap<>();
static {
    map.put("loan", Loan::new);
    map.put("stock", Stock::new);
    map.put("bond", Bond::new);
}
public static Product createProduct(String name) { 
    Supplier<Product> p = map.get(name); 
    if(p != null) return p.get(); 
    throw new IllegalArgumentException("No such product " + name); 
}

ex. 세 인수(Integer 둘, 문자열 하나)를 받는 상품의 생성자가 있다고 가정합니다.

public interface TriFunction<T, U, V, R> {
  R apply(T t, U u, V v);
}
Map<String, TriFunctionInteger<Integer, String, Product>> map = new HashMap<>();




9.3 람다 테스팅

일반적으로 좋은 소프트웨어 공학자라면 프로그램이 의도대로 동작하는지 확인할 수 있는 단위 테스팅을 진행합니다. 우리는 소스 코드의 일부가 예상된 결과를 도출할 것이라 단언하는 테스트 케이스를 구현합니다.

ex. 그래픽 애플리케이션의 일부인 Point 클래스가 있다고 가정합니다.

public class Point {
    private final int x; 
    private final int y;
    private Point(int x, int y) { 
        this.x = x; this.y = y; 
    } 
    public int getX() { 
        return x; 
    } 
    public int getY() { 
        return y; 
    } 
    public Point moveRightBy(int x) {
      return new Point(this.x + x, this.y);
    }
}
@Test 
public void test MoveRightBy() throws Exception { 
    Point p1 = new Point(5, 5); 
    Point p2 = p1.moveRightBy(10); 
    assertEquals(15, p2.getX()); 
    assertEquals(5, p2.getY());
}


9.3.1 보이는 람다 표현식의 동작 테스팅

ex. Point 클래스에 compareByXAndThenY라는 정적 필드를 추가했다고 가정합니다. (compareByXAndThenY를 이용하면 메서드 참조로 생성한 Comparator 객체에 접근할 수 있습니다.)

public class Point {
  public final static Comparator<Point> compareByXAndThenY
          = comparing(Point::getX).thenComparing(Point::getY);
  ...
}

ex. 다음은 Comparator 객체 compareByXAndThenY에 다양한 인수로 compare 메서드를 호출하면서 예상대로 동작하는지 테스트하는 코드입니다.

@Test 
public void testComparingTwoPoints() throws Exception {
    Point p1 = new Point(10, 15);
    Point p2 = new Point(10, 20); 
    int result = Point.compareByXAndThenY.compare(p1,p2); 
    assertTrue(result < 0);
}

9.3.2 람다를 사용하는 메서드의 동작에 집중하라

public static List<Point> moveAllPointsRightBy(List<Point> points, int x) {
    return points.stream() 
                  .map(p -> new Point(p.getX() + x, p.getY())) 
                  .collect(tolist());
}
@Test 
public void testMoveAllPointsRightBy() throws Exception { 
    List<Point> points = Arrays.asList(new Point(5, 5),new Point(10, 5)); 
    List<Point> expectedPoints = Arrays.asList(new Point(15, 5), new Point(20, 5)); 
    List<Point> newPoints = Point.moveAllPointsRightBy(points, 10);
    assertEquals(expectedPoints, newPoints);
}


9.3.3 복잡한 람다를 개별 메서드로 분할하기

테스트 코드에서 람다 표현식을 참조할 수 없는데, 그렇다면 복잡한 람다 표현식을 어떻게 테스트할 지를 알아보겠습니다. 한 가지 해결책은 8.1.3절에서 설명한 것처럼 람다 표현식을 메서드 참조로 바꾸는 것입니다.(새로운 일반 메서드 선언) 그러면 일반 메서드를 테스트하듯이 람다 표현식을 테스트할 수 있습니다.



9.3.4 고차원 함수 테스팅

함수를 인수로 받거나 다른 함수를 반환하는 메서드(고차원 함수 higher-order functions)는 좀 더 사용하기 어렵습니다. 메서드가 람다를 인수로 받는다면 다른 람다로 메서드의 동작을 테스트할 수 있습니다.

ex. 예를 들어 다양한 프레디케이트로 filter 메서드를 테스트할 수 있습니다.

@Test 
public void testFilter() throws Exception { 
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4); 
    List<Integer> even = filter(numbers, i -> i % 2 == 0); 
    List<Integer> smallerThanThree = filter(numbers, i -> i < 3); 
    assertEquals(Arrays.asList(2, 4), even); 
    assertEquals(Arrays.asList(1, 2), smallerThanThree); 
}

테스트해야 할 메서드가 다른 함수를 반환한다면 어떻게 해야 할까요? 이때는 Comparator에서 살펴봤던 것처럼 함수형 인터페이스의 인스턴스로 간주하고 함수의 동작을 테스트할 수 있습니다.




9.4 디버깅

문제가 발생한 코드를 디버깅할 때 개발자는 다음 두 가지를 가장 먼저 확인해야 합니다.

9.4.1 스택 트레이스 확인

예를 들어 예외 발생으로 프로그램 실행이 갑자기 중단되었다면 먼저 어디에서 멈췄고 어떻게 멈추게 되었는지 살펴봐야 합니다. 바로 스택 프레임에서 이 정보를 얻을 수 있는데, 프로그램이 메서드를 호출할 때마다

따라서 프로그램이 멈췄다면 프로그램이 어떻게 멈추게 되었는지 프레임별로 보여주는 스택 트레이스, 즉, 문제가 발생한 지점에 이르게 된 메서드 호출 리스트를 얻을 수 있습니다. 메서드 호출 리스트를 통해 문제가 어떻게 발생했는지 이해할 수 있습니다.

람다와 스택 트레이스

유감스럽게도 람다 표현식은 이름이 없기 때문에 조금 복잡한 스택 트레이스가 생성됩니다.

ex. 다음은 고의적으로 문제를 일으키도록 구현한 간단한 코드입니다.

import java.util.*; 

public class Debugging { 
    public static void main(String[] args) { 
        List<Point> points = Arrays.asList(new Point(12, 2), null); 
        points.stream().map(p -> p.getX()).forEach(System.out::println); 
    } 
}

프로그램을 실행하면 다음과 같은 스택 트레이스가 출력됩니다.

image

ex. 메서드 참조를 사용하는 클래스와 같은 곳에 선언되어 있는 메서드를 참조할 때는 메서드 참조 이름이 스택 트레이스에 나타나는 것을 확인할 수 있는 예제입니다.


import java.util.Arrays;
import java.util.List;

public class Debugging {

  public static void main(String[] args) {
    List<Integer> numbers = Arrays.asList(1, 2, 3);
    numbers.stream().map(Debugging::divideByZero).forEach(System.out::println);
  }

  public static int divideByZero(int n) {
    return n / 0;
  }
}

image



9.4.2 정보 로깅

스트림의 파이프라인 연산을 디버깅한다고 가정합니다.

ex. 다음처럼 forEach로 스트림 결과를 출력하거나 로깅할 수 있습니다.

List<Integer> numbers = Arrays.asList(2, 3, 4, 5);
numbers.stream() 
        .map(x -> x + 17) 
        .filter(x -> x % 2 == 0) 
        .limit(3) 
        .forEach(System.out::println);
//결과
20
22

image

List<Integer> result = numbers.stream()
        .peek(x -> System.out.println("from stream: " + x))
        .map(x -> x + 17)
        .peek(x -> System.out.println("after map: " + x))
        .filter(x -> x % 2 == 0)
        .peek(x -> System.out.println("after filter: " + x)) 
        .limit(3) 
        .peek(x -> System.out.println("after limit: " + x))
        .collect(toList());
//결과
from stream: 2
after map: 19
from stream: 3
after map: 20
after filter: 20
after limit: 20
from stream: 4
after map: 21
from stream: 5
after map: 22 
after filter: 22 
after limit: 22