람다 테스팅
일반적으로 좋은 소프트웨어 공학자라면 프로그램이 의도대로 동작하는지 확인할 수 있는 단위 테스팅(unit testing)을 진행한다. 우리는 소스 코드의 일부가 예상된 결과를 도출할 것이라 단언하는 테스트 케이스를 구현한다.
보이는 람다 표현식의 동작 테스팅
일반적인 메서드는 이름이 존재하기 때문에 단위 테스트를 문제없이 진행할 수 있지만, 람다는 익명이므로 테스트 코드 이름을 호출할 수 없다. 따라서 필요하다면 람다를 필드에 저장해 테스트 할 수 있다.
public class OrderProduct {
private String name;
private int count;
private int price;
....
}
public static class Order {
public static final ToIntFunction<Order> getTotalPrice =
(Order o) -> o.getProducts().stream().mapToInt(p -> p.getPrice() * p.getCount()).sum();
private List<OrderProduct> products;
...
}
@Test
void test() {
Order order = new Order(List.of(
new OrderProduct("TV", 1, 300_000),
new OrderProduct("공책", 3, 1_000),
new OrderProduct("컴퓨터", 1, 1_000_000)
));
int totalPrice = Order.getTotalPrice.applyAsInt(order);
Assertions.assertEquals(totalPrice, 1_303_000);
}
람다 표현식은 함수형 인터페이스의 인스턴스를 생성한다는 사실을 기억하자.
람다를 사용하는 메서드의 동작에 집중하라
람다의 목표는 정해진 동작을 다른 메서드에서 사용할 수 있도록 하나의 조각으로 캡슐화하는 것이다.
그러려면 세부 구현을 포함하는 람다 표현식을 공개하지 말아야 한다.
람다 표현식을 사용하는 메서드의 동작을 테스트함으로써 람다를 공개하지 않으면서도 람다 표현식을 검증할 수 있다.
public static class Order {
...
public static Order limitProductPrice(Order order, int maxPrice) {
List<OrderProduct> newProducts = order.getProducts().stream()
.filter(p -> p.getPrice() <= maxPrice).collect(Collectors.toList());
return new Order(newProducts);
}
}
@Test
void test() {
Order order = new Order(List.of(
new OrderProduct("TV", 1, 300_000),
new OrderProduct("공책", 3, 1_000),
new OrderProduct("컴퓨터", 1, 1_000_000)
));
Order newOrder = Order.limitProductPrice(order, 990_000);
int totalPrice = Order.getTotalPrice.applyAsInt(newOrder);
Assertions.assertEquals(totalPrice, 303_000);
}
람다 표현식을 사용하는 메서드의 동작을 테스트함으로써 람다를 공개하지 않고 검증할 수 있다.
고차원 함수 테스팅
함수를 인수로 받거나 다른 함수를 반환하는 메서드(고차원 함수)는 좀 더 사용하기 어렵다.
메서드가 람다를 인수로 받는다면 다른 람다로 메서드의 동작을 테스트 할 수 있다.
public static class Order {
...
public static List<OrderProduct> productsFilter(Function<List<OrderProduct>, List<OrderProduct>> function, List<OrderProduct> products) {
return function.apply(products);
}
}
@Test
void test() {
Order order = new Order(List.of(
new OrderProduct("TV", 1, 300_000),
new OrderProduct("공책", 3, 1_000),
new OrderProduct("컴퓨터", 1, 1_000_000)
));
List<OrderProduct> cheapProducts = Order.productsFilter(
products -> products.stream().filter(p -> p.getPrice() <= 300_000).collect(Collectors.toList()),
order.getProducts()
);
List<OrderProduct> expensiveProducts = Order.productsFilter(
products -> products.stream().filter(p -> p.getPrice() >= 500_000).collect(Collectors.toList()),
order.getProducts()
);
...
}
테스트해야 할 메서드가 다른 함수를 반환한다면 함수형 인터페이스의 인스턴스로 간주하고 함수의 동작을 테스트 할 수 있다.
코드를 테스트하면서 람다 표현식에 어떤 문제가 있음을 발견하게 될 것이다. 그래서 디버깅이 필요하다.
디버깅
문제가 발생한 코드를 디버깅할 때 개발자는 스택 트레이스와 로깅을 가장 먼저 확인해야 한다.
하지만 람다 표현식과 스트림은 기존의 디버깅 기법을 무력화 한다. 그렇다면 어떻게 디버깅을 해야할까?
스택 트레이스 확인
예를 들어 예외 발생으로 프로그램 실행이 갑자기 중단되었다면 먼저 어디에서 멈췄고 어떻게 멈추게 되었는지 스택 프레임(stack frame)을 살펴보아야 한다.
프로그램이 메서드를 호출할 때마다 프로그램에서 호출 위치, 호출할 떄의 인수값, 호출된 메서드의 지역 변수 등을 포함한 호출 정보가 생성되며 이들 정보는 스택 프레임에 저장된다.
람다 표현식은 이름이 없기 때문에 조금 복잡한 스택 트레이스가 생성된다.
아래 고의로 람다 표현식을 사용해 NullPointerException을 발생시켜 보겠다.
@Test
void exceptionTest() {
List<Order> orderList = List.of(new Order("000001"), null);
orderList.stream().map(Order::getOrderNumber).forEach(System.out::println);
}
ImmutableCollections$List12와 같은 이상한 문자는 람다 표현식 내부에서 에러가 발생했음을 가리킨다.
람다 표현식은 이름이 없으므로 컴파일러가 람다를 참조하는 이름을 만들어 낸 것이다.
해당 람다 표현식을 사용한 메서드(com.패키지 경로.클래스.함수) 정보는 스택 트레이스에 표기가 되므로 아직까지는 해당 메서드를 찾아 수정해주는 방법 밖에 없다..
정보 로깅
스트림의 파이프라인 연산을 디버깅한다고 가정해보자.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
.map(x -> x + 1)
.filter(x -> x % 2 == 0)
.limit(2)
.forEach(System.out::println); // 2, 4
forEach를 호출하는 순간 전체 스트림이 소비되어 각각의 연산(map, filter, limit)이 어떤 결과를 도출하는지 확인할 수 없게 된다. 이럴때 peek이라는 스트림 연산을 활용할 수 있다.
peek는 스트림의 각 요소를 소비한 것처럼 동작을 실행한다.
하지만 forEach 처럼 실제로 스트림의 요소를 소비하지는 않는다.
peek는 자신이 확인한 요소를 파이프라인의 다음 연산으로 그대로 전달한다.
peek를 사용해 연산 중간마다 값을 확인해보자.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
.peek(x -> System.out.println("number : " + x)) // number : 1
.map(x -> x + 1)
.peek(x -> System.out.println("map : " + x)) // map : 2
.filter(x -> x % 2 == 0)
.peek(x -> System.out.println("filter : " + x)) // filter : 2
.limit(2)
.peek(x -> System.out.println("limit : " + x)) // limit : 2
.collect(Collectors.toList());
'Java' 카테고리의 다른 글
[모던 자바] Optional 클래스란? (0) | 2022.03.16 |
---|---|
[모던 자바] DSL(도메인 전용 언어)이란? (2) | 2022.03.15 |
[모던 자바] 디자인 패턴 소개 및 람다를 이용해 리팩터링 하기 (0) | 2022.03.11 |
[모던 자바] 가동석과 유연성을 개선하는 리팩터링 (0) | 2022.03.11 |
[모던 자바] 컬렉션 팩토리(Collection Factory) 소개 및 사용법 (0) | 2022.03.10 |