[모던 자바 인 액션] Chapter 9. 리팩터링, 테스팅, 디버깅

SunYerim·2024년 2월 25일
0

언어

목록 보기
9/11

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

람다 표현식을 이용한 코드는 다양한 요구사항 변화에 대응할 수 있도록 동작을 파라미터한다.

9.1.1 코드 가독성 개선

코드 가독성이 좋다는 것은 어떤 코드를 다른 사람도 쉽게 이해할 수 있음을 의미한다.

  • 익명 클래스를 람다 표현식으로 리팩터링하기
  • 람다 표현식을 메서드 참조로 리팩터링하기
  • 명령형 데이터 처리를 스트림으로 리팩터링하기

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

하나의 추상 메서드를 구현하는 익명 클래스는 람다 표현식으로 리팩터링할 수 있다.

// 익명 클래스를 사용한 이전 코드
Runnable r1 = new Runnable() {
	public void run() {
		System.out.println("Hello");
	}
};

// 람다 표현식을 사용한 코드
Runnable r2 = () -> System.out.println("Hello");
  • 익명 클래스에서 사용한 this와 super는 람다 표현식에서 다른 의미를 갖는다.
    • 익명에서 this는 익명 클래스 자신을 가리킴.
    • 람다에서 this는 람다를 감싸는 클래스를 가리킴.
  • 익명 클래스는 감싸고 있는 클래스를 변수를 가릴 수 있다.
  • 익명 클래스를 람다 표현식으로 바꾸면 콘텍스트 오버로딩에 따른 모호함이 초래될 수 있다.
    • 익명 클래스 : 인스턴스화할 때 명시적으로 형식이 정해짐.
    • 람다: 콘텍스트에 따라 달라짐.

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

람다 표현식 대신 메서드 참조를 이용하면 가독성을 높일 수 있다.

Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = 
	menu.stream().collect(groupingBy(Dish::getCaloricLevel));

// 람다 표현식을 메서드로 추출 후 dish 클래스에 getCaloricLevel 메서드 추가
public class Dish {
	...
	public CaloricLevel getCaloricLevel() {
		...
	}
}

Collectors API를 사용하면 코드의 의도가 더 명확해짐 (최댓값이나 합계 계산할 때 람다 표현식과 저수준 리듀싱 연산을 조합하는 것보다)

int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
// summingInt -> 컬렉터

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

스트림 API는 데이터 처리 파이프라인의 의도를 더 명확하게 보여준다.

List<String> dishNames = new ArrayList<>();
for (Dish dish: menu) {
	if (dish.getCalories() > 300) {
		dishNames.add(dish.getName());
	}
}

// 스트림 API 사용
menu.parallelStream()
		.filter(d -> d.getCalories() > 300)
		.map(Dish::getName)
		.collect(toList());

9.1.5 코드 유연성 개선

람다 표현식을 이용하면 동작 파라미터화를 쉽게 구현할 수 있다. 즉, 람다를 전달해서 다양한 동작을 표현할 수 있다.

함수형 인터페이스 적용

람다 표현식을 이용하려면 함수 표현식이 필요하다.

조건부 연기실행과 실행 어라운드 패턴으로 람다 표현식 리팩터링을 살펴보자.

조건부 연기 실행

(예제) 특정 조건에서만 메시지가 생성될 수 있도록 메시지 생성 과정을 연기할 수 있어야되는데, 이는 람다를 이용하면 해결할 수 있다.

logger 문제(logger 상태 노출)를 해결할 수 있도록 Supplier를 인수로 갖는 오버로드된 log 메서드를 제공했다.

이로써 logger의 수준이 적절하게 설정되어 있을 때만 인수로 넘겨진 람다를 내부적으로 실행한다.

코드의 가독성이 좋아질 뿐만 아니라 캡슐화도 강화된다는 장점이 있다.

실행 어라운드

매번 같은 준비, 종료 과정을 반복적으로 수행하는 코드가 있따면 이를 람다로 변환할 수 있다.


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

다양한 패턴을 유형별로 정리한 것이 디자인 패턴이다. 디자인 패턴은 공통적인 소프트웨어 문제를 설계할 때 재사용할 수 있는, 검증된 청사진을 제공한다.

디자인 패턴에 람다를 이용하면 이전에 디자인 패턴으로 해결하던 문제를 더 쉽고 간단하게 해결할 수 있다.

다섯가지 패턴을 살펴본다.

  • 전략
  • 템플릿 메서드
  • 옵저버
  • 의무 체인
  • 팩토리

9.2.1 전략

한 유형의 알고리즘을 보유한 상태에서 런타임에 적절한 알고리즘을 선택했는지 선택하는 기법이다.

다양한 기준을 갖는 입력값을 검증하거나, 다양한 파싱 방법을 사용하거나, 입력 형식을 설정하는 등 다양한 시나리오에 전략 패턴을 활용할 수 있다.

전략 패턴은 세 부분으로 구성된다.

  • 알고리즘을 나타내는 인터페이스
  • 다양한 알고리즘을 나타내는 한 개 이상의 인터페이스 구현
  • 전략 객체를 사용하는 한 개 이상의 클라이언트

람다 표현식 사용

Validator numericValidator =
	new Validator((String s) -> s.matches("[a-z]+"));
boolean b1 = numericValidator.vallidate("aaaa");
Validator lowerCaseValidator =
	new Validator((String s) -> s.matches("\\d+"));
boolean b2 = lowerCaseValidator.validate("bbbb");

람다 표현식은 코드 조각을 캡슐화한다. 즉, 람다 표현식으로 전략 디자인 패턴을 대신할 수 있다.

9.2.2 탬플릿 메서드

알고리즘의 개요를 제시한 다음에 알고리즘의 일부를 고칠 수 있는 유연함을 제공해야 할 때 템플릿 메서드 디자인 패턴을 사용한다.

즉, 해당 알고리즘을 사용하고 싶은데 그대로는 안 되고 조금 고쳐야 하는 상황에 적합하다.

온라인 뱅킹 애플리케이션 동작을 정의하는 추상 클래스이다.

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

processCustomer 메서드는 온라인 뱅킹 알고리즘이 해야 할 일을 보여준다.

람다 표현식 사용

람다나 메서드 참조로 알고리즘에 추가할 다양한 컴포넌트를 구현할 수 있다.

makeCustomerHappy의 메서드 시그니처와 일치하도록 Customer 형식을 갖는 두 번째 인수를 processCustomer에 추가한다.

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

onlineBanking클래스를 상속받지 않고 직접 람다 표현식을 전달해 다양한 동작을 추가할 수 있다.

new OnlineBankingLambda().processCustomer(1337, (Customer c) ->
	System.out.println("Hello " + c.getName());

9.2.3 옵저버

어떤 이벤트가 발생했을 대 한 객체가 다른 객체 리스트에 자동으로 알림을 보내야 하는 상황에서 옵저버 디자인 패턴을 사용한다. GUI 애플리케이션에서 옵저버 패턴이 자주 등장한다.

버튼 같은 GUI 컴포넌트에 옵저버를 설정할 수 있다. 그리고 사용자가 버튼을 클릭하면 옵저버에 알림이 전달되고 정해진 동작이 수행된다.

9.2.4 의무 체인

작업 처리 객체의 체인을 만들 때는 의무 체인 패턴을 사용한다. 한 객체가 어떤 작업을 처리한 다음에 다른 객체로 결과를 전달하고, 다른 객체도 해야 할 작업을 처리한 다음에 또 다른 객체로 전달하는 식이다.

일반적으로 다음으로 처리할 객체 정보를 유지하는 필드를 포함하는 작업 처리 추상 클래스로 의무 체인 패턴을 구성한다. 작업 처리 객체가 자신의 작업을 끝냈으면 다음 작업 처리 객체로 결과를 전달한다.

9.2.5 팩토리

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


9.3 람다 테스팅

단위 테스팅

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

람다는 익명이므로 테스트 코드 이름을 호출할 수 없다.

따라서 필요하다면 람다를 필드에 저장해서 재사용할 수 있으며 람다의 로직을 테스트할 수 있다.

람다 표현식은 함수형 인터페이스의 인스턴스를 생성한다.

@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 람다를 사용하는 메서드의 동작에 집중하라

람다의 목표는 정해진 동작을 다른 메서드에서 사용할 수 있도록 하나의 조각으로 캡슐화하는 것이다.

람다 표현식을 사용하는 메서드의 동작을 테스트함으로써 람다를 공개하지 않으면서도 람다 표현식을 검증할 수 있다.

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

복잡한 람다 표현식을 메서드 참조로 바꾸어 테스트할 수 있다. 그러면 일반 메서드를 테스트하듯이 람다 표현식을 테스트할 수 있다.

9.3.4 고차원 함수 테스팅

함수를 인수로 받거나 다른 함수로 반환하는 메서드는 좀 더 사용하기 어렵다. 메서드가 람다를 인수로 받는다면 다른 람다로 메서드의 동작을 테스트할 수 있다.

테스트할 메서드가 다른 함수를 반환한다면? → 함수형 인터페이스의 인스턴스로 간주하고 함수의 동작을 테스트할 수 있다.


9.4 디버깅

코드를 디버깅할 때 다음 두 가지를 가장 먼저 확인해야 한다.

  • 스택 트레이스
  • 로깅

9.4.1 스택 트레이스 확인

예를 들어 예외 발생으로 프로그램 실행이 중단되었다면 관련 정보를 살펴봐야 하는데, 이를 스택 프레임에서 얻을 수 있다.

람다 표현식은 이름이 없으므로 컴파일러가 람다를 참조하는 이름을 만들어낸다. 클래스에 여러 람다 표현식이 있다면? 골치아픈 일이 벌어진다!

메서드 참조를 사용해도 스택 트레이스에는 메서드명이 나타나지 않는다.

메서드 참조를 사용하는 클래스와 같은 곳에 선언되어 있는 메서드를 참조할 때는 메서드 참조 이름이 스택 트레이스에 나타난다.

람다 표현식과 관련된 스택 트레이스는 이해하기 어렵다!

9.4.2 정보 로깅

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

forEach를 호출하는 순간 전체 스트림이 소비된다.

스트림 파이프라인에 적용된 각각의 연산이 어떤 결과를 도출하는지 알고싶다면 peek이라는 스트림 연산을 활용하면 된다.

peek은 스트림의 각 요소를 소비한 것처럼 동작을 실행한다.

List<Integer> result = 
	numbers.stream()
			 .peek(x -> system.out.println("from stream: " + x)); // 이런 식으로 코드 작성하면 된다.
			 .map(x -> x + 17)
			 .peek
			 .filter(x -> x % 2 == 0)
			 .peek
			 .limit(3)
			 .peek
			 .forEach(System.out.println);
profile
내 안에 있는 힘을 믿어라.

0개의 댓글