FeGwan-Training / FeGwan

0 stars 0 forks source link

Chapter9 리팩터링, 테스팅, 디버깅 #23

Closed MyeoungDev closed 1 year ago

MyeoungDev commented 1 year ago

Discussed in https://github.com/FeGwan-Training/FeGwan/discussions/22

Originally posted by **MyeoungDev** June 13, 2023 # 📕 Modern Java In Action Chapter_9 # 9.0.0 리팩터링, 테스팅, 디버깅 이번 장에서 공부할 내용 - 람다 표현식을 이용해 가독성과 유연성을 높이기 위한 리팩터링 방법 - 람다 표현식을 이용해 객체지향의 다양한 디자인패턴을 어떻게 하면 간소화하는지에 대한 방법 - 람다 표현식과 스트림 API를 사용하는 코드를 테스트하고 디버깅하는 방법. # 9.1 가독성과 유연성을 개선한는 리팩터링 코드 가독성이란 무엇일까? → 다른 사람도 쉽게 이해할 수 있음을 의미 → 가독성을 개선한다는 것은 우리가 구현한 코드를 다른 사람이 쉽게 이해하고 유지보수할 수 있게 만드는 것을 의미 → 코드 가독성을 높이려면 코드의 문서화, 컨벤션 규칙을 준수하는 등의 노력이 필요. 람다 표현식을 이용한 가독성을 높이는 방법은 크게 3가지가 존재. - **익명 클래스를 람다 표현식으로 리팩터링** - **람다 표현식을 메서드 참조로 리팩터링** - **명령형 데이터 처리를 스트림으로 리팩터링** ## 9.1.2 익명 클래스를 람다 표현식으로 리팩터링하기 **하나의 추상 메서드를 구현하는 익명 클래스**는 람다 표현식으로 리팩터링할 수 있다. ```java /* 익명 클래스를 사용한 방법 -> 코드가 장황하고 에러를 일으키기 쉽다고 한다. */ Runnable r1 = new Runable() { public void run() { System.out.println("Hello"); } }; /* 람다 표현식을 이용한 방법 -> 더 간결하고 흐름이 보이는 코드 작성 가능. */ Runnable r2 = () -> System.out.println("Hello"); ``` 하지만, 모든 익명 클래스를 람다 표현식으로 변환할 수 있는 것은 아니다. - 익명 클래스에 서사용한 `this` 와 `super` 는 람다 표현식에서 달느 의미를 갖는다. - 익명 클래스에서 `this` 는 익명 클래스 자신을 가르키지만, 람다에서 `this` 는 람다를 감싸는 클래스를 가르킨다. - 익명 클래스는 감싸고 있는 클래스의 변수를 가릴 수 있다. (섀도 변수 : Shadow variable) - 람다 표현식으로는 변수를 가릴 수 없다. 코드로 보면 이해하기 더 편하다 ```java /* 람다 표현식 컴파일 에러가 발생하는 코드 */ int a = 10; Runnable r1 = () -> { int a = 2; // 변수의 재할당으로 인한 에러 System.out.println(a); }; /* 익명 클래스 정상적인 코드 */ int a = 10; Runnable r2 = new Runnable() { @Override public void run() { int a = 2; System.out.println(a); } }; ``` - 익명클래스를 람다 표현식으로 바꾸면 콘텍스트 오버로딩에 따른 모호함이 초래 될 수 있다. - 익명 클래스는 인스턴스호할 때 명시적으로 형식이 정해지는 반면 람다의 형식은 콘텍스트에 따라 달리지기 때문이다. 이 또한 코드를 보면 이해하기 편하다. - 따라서, 이 경우에는 명시적 형변환을 이용해서 모호함을 제거해야 한다. ```java /* Task interface */ interface Task { public void execute(); } public static void doSomething(Runnable r){ r.run(); } public static void doSomething(Task a){ a.execute(); } /* doSomething 익명 클래스 호출 */ doSomething(new Task() { public void execute() { System.out.println("Danger danger!!"); } }); /* doSomething 람다표현식 호출 */ doSomething(() -> System.out.println("Danger danger!!")); // 람다 표현식으로 메서드를 호출하면 Runnable 과 Task 모두 대상 형식이 될 수 있으므로 문제가 생긴다. /* 명시적 형변환을 이용한 모호함 제거 */ doSomething((Task)() -> System.out.println("Danger danger!!")) ``` ## 9.1.3 람다 표현식을 메서드 참조로 리팩터링 람다 표현식은 쉽게 전달할 수 있는 짧은 코드이다. 하지만, 람다 표현식 대신 메서드 참조를 이용하면 가독성을 높이며, 메서드명으로 코드의 의도를 명확하게 알릴 수 있다. ```java /* 칼로리 수준으로 요리를 그룹화하는 코드 */ Map> 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; })); /* 람다 표현식을 별도의 메서드로 추출한 다음에 groupingBy에 인수로 전달한 코드 */ Map> dishesByCaloricLevel = menu.stream().collect(groupingBy(Dish::getCaloricLevel)); /* 해당 메서드를 클래스에 추가 */ public class Dish { ... public CaloricLevel getCaloricLevel() { if (dish.getCalories() <= 400) return CaloricLevel.DIET; else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL; else return CaloricLevel.FAT; } } ``` 또한, `comparing` 과 `maxBy` 같은 정적 헬퍼 메서드를 활용하는 것이 좋다. 그리고 최대값이나 합계를 계산할 때 람다 표현식과 저수준 리듀싱 연산을 조합하는 것보다 `Collectors API` 를 사용하면 코드의 의도가 더 명확해진다. ```java /* comparing 메서드를 이용한 메서드 참조 방법으로 리팩토링 */ inventory.sort( (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) ); inventory.sort(comparing(Apple::getWeight)); /* 저수준 리듀싱 연산을 Collectors API를 이용한 리팩토링 */ int totalCalories = menu.stream().map(Dish::getCalories) .reduce(0, (c1, c2) -> c1 + c2); int totalCalories = menu.stream().collect(summingInt(Dish::getCalories)); ``` ## 9.1.4 명령형 데이터 처리를 스트림으로 리팩터링 이론적으로는 반복자를 이용한 기존의 모든 컬렉션 처리 코드를 스트림 API로 바꿔야 한다. 스트림 API는 데이터 처리 파이프라인의 의도를 더 명확하게 보여준다. 또한, 스트림은 쇼트서킷과 지연 연산이라는 최적화와 멀티코어 아키텍처를 활용할 수 있는 훌륭한 방법이다. ```java /* 필터링과 추출로 엉킨 반복문 */ List dishNames = new ArrayList<>(); for(Dish dish : menu) { if (dish.getCarlories() > 300) { dishNames.add(dish.getName()); } } /* 스트림 API를 이용한 리팩토링 */ menu.parallelStream() .filter(d -> d.getCalories() > 300) .map(Dish::getName) .collect(toList()); ``` ## 9.1.5 코드 유연성 개선 **함수형 인터페이스**를 적절히 사용하여 다양한 **람다**를 전달해서 변화하는 요구사항에 대응할 수 있는 **유연한 코드를 구현**할 수 있다. - 조건부 연기 실행 - 코드의 가독성이 좋아지고 캡슐화가 강화된다. ```java if (logger.isLoggable(Log.FINER)) { logger.finer("Problem: " + generateDiagnostic()); } // logger의 상태가 isLoggable 이라는 메서드에 의 해 클라이언트 코드로 노출된다. // 메세지를 로깅할 때마다 logger 객체의 상태를 매번 확인해야 한다. /* 메세지를 로깅하기 전에 logger 객체가 적절한 수준으로 설정되었는지 내부적으로 확인하는 log 메서드를 사용 */ logger.log(Level.FINER, "Problem: " + generateDiagnostic()); // 불필요한 if문 제거, logger 상태 노출 X /* 람다를 이용하여 특정 수준에서만 메세지가 생성될 수 있도록 조건부 연기 실행 코드 */ logger.log(Level.FINER, () -> "Problem: " + generateDiagnostic()); // Supplier를 인수로 갖는 오버로드된 log 메서드 활용. public void log(Level level, Supplier msgSupplier) { if (!isLoggable(level)) { return; } LogRecord lr = new LogRecord(level, msgSupplier.get()); doLog(lr); } ``` - 실행 어라운드패턴 적용 - 매번 같은 준비, 종료 과정을 반복적으로 수행하는 코드가 있다면 이를 람다로 변환할 수 있다. ```java /* Functional Interface 선언 */ public interface BufferedReaderProcessor { String process(BufferedReader b) throws IOException; } /* Functional Interface 를 인수로 받는 메서드 */ public static String processFile(BufferedReaderProcessor p) throws IOException { try(BufferedReader br = new BufferedReader(new FileReader("filepath"))) { return p.process(br); } } String oneLine = processFile((BufferedReader b) -> b.readLine()); String twoLine = processFile((BufferedReader b) -> b.readLine() + b.readLine()); ``` # 9.2 람다로 객체지향 디자인 패턴 리팩터링하기 다양한 패턴을 유형별로 정리한 것이 디자인 패턴 (Design Pattern) 이다. 디자인 패턴은 공통적인 소프트웨어 문제를 설계할 때 재사용할 수 있는, 검증된 청사진을 제공한다. 람다를 이용하면 이전에 디자인패턴으로 해결하던 문제를 더 쉽고 간단하게 해결할 수 있다. 또한 람다 표현식으로 기존의 많은 객체지향 디자인 패턴을 제거하거나 간결하게 재구현할 수 있다. ## 9.2.1 전략 패턴(Strategy Pattern) 전략 패턴은 **한 유형의 알고리즘을 보유한 상태에서** **런타임에** **적절한 알고리즘을 선택하는 기법**이다. ![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/a0cf0129-182c-498e-973f-5753e4430bbf) - 알고리즘을 나타내는 `Strategy interface` - 다양한 알고리즘을 나타내는 한 개 이상의 인터페이스 구현 클래스 - 전략 객체를 사용하는 한 개 이상의 클라이언트 ```java /* 함수형 인터페이스 */ public interface ValidationStrategy { boolean execute(String s); } public class IsAllLowerCase implements ValidationStrategy { @Override public boolean execute(String s) { return s.matches("[a-z]+"); } } public class IsNumeric implements ValidationStrategy { @Override public boolean execute(String s) { return s.matches("\\d+"); } } public class Validator { private final ValidationStrategy strategy; public Validator(ValidationStrategy strategy) { this.strategy = strategy; } public boolean validate(String s) { return strategy.execute(s); } } public class Main { public static void main(String[] args) { /* 전력패턴을 이용해 알고리즘이 구현된 클래스의 객체를 주입받아 사용하는 방식*/ Validator numericValidator = new Validator(new IsNumeric()); boolean b1 = numericValidator.validate("aaaa"); Validator lowerCaseValidator = new Validator(new IsAllLowerCase()); boolean b2 = lowerCaseValidator.validate("bbbb"); /* 람다 표현식을 이용하여 람다를 직접 전달하여 전략패턴을 구현한 방식 */ Validator numericValidatorLambda = new Validator((String s) -> s.matches("[a-z]+")); boolean b3 = numericValidatorLambda.validate("aaaa"); Validator lowerCaseValidatorLambda = new Validator((String s) -> s.matches("\\d+")); boolean b4 = lowerCaseValidatorLambda.validate("bbbb"); } } ``` - 람다 표현식을 이용하면 전략 패턴에서 발생하는 자잘한 코드를 제거할 수 있다. - 람다 표현식은 코드 조각을 캡슐화 한다. - 다양한 전략을 구현하는 새로운 클래스를 구현할 필요 없이 람다 표현식을 직접 전달하면 코드가 간견해진다. ## 9.2.2 템플릿 메서드 패턴(Template Method Pattern) 템플릿 메서드 패턴은 **알고리즘의 개요를 제시한 다음**에 **알고리즘의 일부를 고칠 수 있는 유연함을 제공해야 할 때** **사용한다.** ![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/1cfbda67-0495-4435-9c92-3aaf3a2a632d) ```java /* * 간단한 온라인 뱅킹 애플리케이션을 구현한다고 가정 * 은행마다 다양한 온라인 뱅킹 애플리케이션을 사용하며 동작 방법도 다르다. */ abstract class OnlineBanking { public void processCustomer(int id) { Customer c = Database.getCustomerWithId(id); makeCustomerHappy(c); } // 추상 클래스를 상속받은 각 역할에 맡는 클래스를 생성해야 한다. abstract void makeCustomerhappy(Customer c); } /* 람다를 이용하여 작성한 Template Method Pattern */ public class OnlineBankingLambda { public void processCustomer(int id, Consumer makeCustomerHappy) { Customer c = Database.getCustomerWithId(id); makeCustomerHappy.accept(c); } } new OnlineBankingLambda().processCustomer(1337, (Customer c) -> System.out.println("Hello " + c.getName()); ``` - **추상 클래스를 상속받지 않고** 직접 **람다 표현식을 전달해서 다양한 동작을 추가**할 수 있다. ## 9.2.3 옵저버 패턴 (Observer Pattern) 어떤 **이벤트가 발생했을 때** 한 객체(**주제 Subject**라 불리는)가 다른 객체 리스트(**옵저버 Observe**r라 불리는)에 자동으로 **알림을 보내야 하는 상황**에서 **옵저버 디자인 패턴을 사용한다.** ![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/5566f80d-fc43-4fbd-9fb3-1499a0922262) ```java /* 특정 키워드가 들어있는 문자열을 받아들여 트윗을 보내는 Observer Pattern */ public interface Observer { void notify(String tweet); } public class NYTimes implements Observer { @Override public void notify(String tweet) { if (tweet != null && tweet.contains("money")) { System.out.println("Breaking news in NY! " + tweet); } } } public class LeMonde implements Observer { @Override public void notify(String tweet) { if (tweet != null && tweet.contains("wine")) { System.out.println("Today cheese, wine and news! " + tweet); } } } public class Guardian implements Observer { @Override public void notify(String tweet) { if (tweet != null && tweet.contains("queen")) { System.out.println("Yet more news from London... " + tweet); } } } /* Subject 주제 interface */ public interface Subject { void registerObserver(Observer observer); void notifyObservers(String tweet); } public class Feed implements Subject { private final List observers = new ArrayList<>(); @Override public void registerObserver(Observer observer) { this.observers.add(observer); } @Override public void notifyObservers(String tweet) { this.observers.forEach(o -> o.notify(tweet)); } } public class Main { public static void main(String[] args) { Feed feed = new Feed(); feed.registerObserver(new NYTimes()); feed.registerObserver(new Guardian()); feed.registerObserver(new LeMonde()); feed.notifyObservers("The queen said her favourite book is Modern Java in Action!"); } } ``` 위의 코드처럼 각 상황에 맞는 `Observer` 인터페이스를 구현하는 클래스를 구현해야 한다. 람다를 이용하면 아래의 코드처럼 각 클래스를 구현하지 않고도 사용이 가능하다. ```java feed.registerObserver((String tweet) -> { if(tweet != null && tweet.contains("money")){ System.out.println("Breaking news in NY! " + tweet); } }); feed.registerObserver((String tweet) -> { if(tweet != null && tweet.contains("queen")){ System.out.println("Yet more news from London... " + tweet); } }); ``` 단, `Observer` 클래스가 **상태를 가지며**, **여러 메서드를 정의**하는 등 **복잡하게 구현**되어 있다면 **기존의 구현 방식을 사용하는 것이 바람직 하다.** ## 9.2.4 의무 체인 패턴 (C**hain-Of-Responsibility Pattern)** 작업 처리 **객체의 동작 체인** 등을 **만들 때**는 **의무 체인패턴을 사용**한다. 한 객체가 어떤 **작업을 처리한 다음**에 **다른 객체로 결과를 전달**하고, **다른 객체**도 해야 할 **작업을 처리**한 다음에 또 **다른 객체로 전달**하는 방식이다. ![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/cb3dd7a5-2ff8-4117-a22e-85c489fa9345) ```java public abstract class ProcessingObject { protected ProcessingObject successor; public void setSuccessor(ProcessingObject 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); } ``` `handle` 메서드는 일부 작업을 어떻게 처리해야 할지 전체적으로 기술한다. `ProcessingObject` 클래스를 상속받아 `handleWork` 메서드를 구현하여 다양한 종류의 작업 처리 객체를 만들 수 있다. ```java public class HeaderTextProcessing extends ProcessingObject { @Override protected String handleWork(String input) { return "From Raoul, Mario and Alan: " + input; } } public class SpellCheckerProcessing extends ProcessingObject { @Override protected String handleWork(String input) { // 오타를 변경하는 로직 return input.replaceAll("labda", "lambda"); } } public class Main { public static void main(String[] args) { ProcessingObject p1 = new HeaderTextProcessing(); ProcessingObject p2 = new SpellCheckerProcessing(); p1.setSuccessor(p2); String result = p1.handle("Aren't labdas really sexy?!!"); // 오타를 수정한 Aren't lambdas really sexy?!! 를 출력하게 된다. System.out.println(result); /* 람다식으로 handleWork을 구현한 클래스를 구현하지 않고 작성한 코드 */ UnaryOperator headerProcessing = (String text) -> "From Raoul, Mario and Alan: " + text; UnaryOperator spellCheckerProcessing = (String text) -> text.replaceAll("labda", "lambda"); // andThen() 메서드를 이용해 함수를 조합해서 체인을 만들 수 있다. Function pipeline = headerProcessing.andThen(spellCheckerProcessing); String lambdaResult = pipeline.apply("Aren't labdas really sexy?!!"); } } ``` ## 9.2.5 팩토리 메서드 패턴 (Factory Method Pattern) **인스턴스화 로직**을 **클라이언트에 노출하지 않고** **객체를 만들 때** **팩토리 메서드 패턴을 사용**한다. ```java public class ProductFactory { final static Map> map = new HashMap<>(); static { map.put("loan", Loan::new); map.put("stock", Stock::new); map.put("bond", Bond::new); } /** * 일반 Factory Method Pattern * * @param name 파라미터를 통해 넘겨받을 객체를 생성하기 위한 이름 * @return Product */ 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); } } /** * Lambda 를 이용한 Factory Method Pattern * * @param name 파라미터를 통해 넘겨받을 객체를 생성하기 위한 이름 * @return Product */ public static Product createProductLambda(String name) { Supplier p = map.get(name); if (p != null) { return p.get(); } throw new IllegalArgumentException("No Such Product " + name); } } public class Main { public static void main(String[] args) { Product loan = ProductFactory.createProduct("loan"); Product loanLambda = ProductFactory.createProductLambda("loan"); } } ``` **Factory Method Pattern** 에서 가장 중요한 점은 **생성자와 설정을 외부로 노출하지 않음**으로써 **단순하게 객체를 생성**할 수 있다는 것이다. 따라서, 람다와 static map을 통해 `switch case` 문을 이용하지 않고 깔끔하게 정리된 코드를 작성할 수 있다. 그러나, 생성자에 여러가지 파라미터를 전달해야하는 상황에서는 `Supplier` 함수형 인터페이스로 해결할 수 없다. 그 경우에는 각 생성자의 파라미터에 맞는 함수형 인터페이스를 직접 만들어서 적용해야 한다. ```java /** * 생성자가 여러 인자를 매게변수로 받을 경우를 위한 함수형 인터페이스 * * @author : 강명관 * @since : 1.0 **/ public interface TriFunction { R apply(T t, U u, V v); } ``` # 9.3 람다 테스팅 개발의 최종 목표는 **제대로 동작하는 코드를 구현**하는 것이 목표이다. 그렇기 위해서 **단위 테스트(Unit Testing)** 은 중요하다. 람다 표현식을 단위 테스트 하기 위한 방법으로는 4가지가 있다. - 보이는 람다 표현식의 동작 테스팅 - 람다는 익명함수 이므로 테스트 코드 이름을 호출할 수 없다. - 따라서 필요하다면 람다를 필드에 저장해서 재사용할 수 있으며 람다의 로직을 테스트 할 수 있다. ```java public class Point { public final static Comparator compareByXAndThenY = comparing(Point::getX).thenComparing(Point::getY); ... } @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); } ``` - 람다를 사용하는 메서드의 동작에 집중하라 - 람다의 목표는 정해진 동작을 다른 메서드에서 사용할 수 있도록 하나의 조각으로 캡슐화하는 것이다. - 세부 구현을 포함하는 람다 표현식을 공개하지 말아야 한다. ```java public static List moveAllPointsRightBy(List points, int x) { return points.stream() .map(p -> new Point(p.getX() + x, p.getY())) .collect(toList()); } @Test public void testMoveAllPointsRightBy() throws Exception { List points = Arrays.asList(new Point(5, 5), new Point(10, 5)); List expectedPoints = Arrays.asList(new Point(15, 5), new Point(20, 5)); assertEquals(expectedPoints, newPoints); } ``` - 복잡한 람다를 개별 메서드로 분할하기 - 복잡한 람다의 경우 메서드 참조로 바꿔는 것이다. - 복잡한 람다를 새로운 일반 메서드로 선언하여 메서드 참조의 경우 일반 메서드 테스트 하듯이 가능하다. - 고차원 함수 테스팅 - 함수를 인수로 받거나 다른 함수를 반환하는 메서드(고차원 함수 higher-order function)는 좀 더 사용하기 어렵다. - 메서드가 람다를 인수로 받는다면 다른 람다로 메서드의 동작을 테스트할 수 있다. ```java @Test public void testFilter() throws Exception { List numbers = Arrays.asList(1, 2, 3, 4); List even = filter(numbers, i -> i % 2 == 0); List smallerThanThree = filter(numbers, i -> i < 3); assertEquals(Arrays.asList(2, 4), even); assertEquals(Arrays.asList(1, 2), smallerThanThree); } ``` # 9.4 디버깅 문제가 발생한 코드를 디버깅할 때 개발자는 **스택 트레이스**와 **로깅** 두 가지를 가장 먼저 확인해야 한다. 하지만, 람다 표현식과 스트림은 기존의 디버깅 기법을 무력화 한다. ## 9.4.1 스택 트레이스 확인 예외가 발생한 정보는 **스택 프레임(Stack Frame)**에서 정보를 얻을 수 있다. 프로그램이 메서드를 호출할 때마다 프로그램에서의 호출 위치, 호출할 때의 인수값, 호출된 메서드의 지역 변수 등을 포함한 호출 정보가 생성되며 이들 정보는 스택 프레임에 저장된다. 따라서, 프로그램이 멈췄다면 스택 프레임에서 프레임별로 보여주는 **스택 트레이스(Stack Trace)**를 얻을 수 있다. ```java /* 일부러 에러가 나게 작성한 코드 */ public class Debugging { public static void main(String[] args) { List points = Arrays.asList(new Point(12, 2), null); points.stream().map(p -> p.getX()).forEach(System.out::println); } /* 발생한 에러 */ Exception in thread "main" java.lang.NullPointerException at Debugging.lambda$main$0(Debugging.java:6) // 이상한 $ 표시가 있다. at Debugging$$Lambda$5/284720968.apply(Unknown Source) at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline .java:193) at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators .java:948) ... ``` $ 표시같은 이상한 문자는 람다 표현식 내부에서 에러가 발생했음을 가르킨다. 람다 표현식은 익명 함수로써 이름이 없으므로 컴파일러가 람다를 참조하는 이름을 만들어내는 것이다. 뭔가 반전이 있을 것 같았지만, 이는 미래의 자바 컴파일러가 개선해야 할 부분이라고 나와 있다. 찾아본 결과 아직 개선되지 않은 것같다. ## 9.4.2 정보 로깅 스트림의 파이프라인 연산을 디버깅하기에는 어려움이 따른다. 따라서, 파이프라인 연산 중간에 어떤 결과를 도출하는지 확인하기 위헤 `peek` 메서드를 활용 할 수 있다. `peek` 메서드는 스트림의 각 요소를 소비한 것처럼 동작을 실행하지만, `forEach` 처럼 실제로 스트림의 요소를 소비하지는 않는다. 즉, `peek` 은 자신이 확인한 요소를 파이프라인의 다음 연산으로 그래도 전달한다. ![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/84aa0a2e-1fe7-4960-a0a8-c84b52d1388a) ```java List 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 ```