9장: 람다를 이용한 리팩토링, 테스팅, 디버깅

리팩토링

  • 코드 가독성을 높이려면 코드의 문서화를 잘하고 표준 코딩 규칙을 준수하는 등의 노력이 필요하다.

  • 자바 8의 람다, 메서드 참조, 스트림을 활용하여 기존 코드의 가독성을 개선할 수 있다.

익명 클래스를 람다 표현식으로 리팩토링

  • 익명 클래스는 코드를 장황하게 만들고 쉽게 에러를 일으키기 때문에 람다를 사용도록 변경해 간결하고 가독성 좋은 코드를 구현할 수 있다.

// 익명 클래스
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello");
    }
};

// 람다
Runnable r2 = () -> System.out.println("Hello");
  • 익명 클래스와 람다에서의 this, super 지시자는 다른 의미를 갖는다.

    • 익명 클래스의 this는 자기 자신의 클래스를 의미하지만, 람다의 this는 람다를 감싸는 클래스를 의미한다.

  • 익명 클래스는 감싸고 있는 클래스의 변수를 가릴수 있지만, 람다 표현식으로는 가릴수 없다.

  • 익명 클래스는 인스턴스화 할때 명시적으로 형식이 정해지는 반면 람다의 형식은 콘텍스트에 따라 달라지기 때문에 익명 클래스를 람다 표현식으로 변경하면 콘텍스트 오버로딩에 따른 모호함이 발생한다.

  • 아래와 같이 람다식이 Task 구체 클래스와 Runnable 구체 클래스 중 어떤 것인지 알 수 없게된다. 따라서 명시적 형변환을 해주어야 해결할 수 있다.

  • 인텔리제이에서도 이 부분에 대해 오류가 있다고 감지해주며, 명시적 형변환을 하도록 제안해주고 있다.

람다 표현식을 메서드 참조로 리팩토링

  • comparing이나 maxBy , groupingBy 같은 정적 헬퍼 메서드를 메서드 참조와 조합해 사용할 수 있다.

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

  • 스트림 API는 데이터 처리 파이프라인의 의도를 명확하게 보여주고 최적화할 수 있으며 멀티코어 아키텍처를 쉽게 활용할 수 있도록 한다.

코드 유연성 개선

  • conditional deferred execution 패턴

    • 클라이언트 코드에서 객체 상태를 자주 확인하거나 객체의 일부 메서드를 호출하는 상황이라면 내부적으로 객체의 상태를 확인한 후 메서드를 호출하도록 하는 새로운 메서드를 구현하는 것이 좋다.

    • 아래는 3단계를 거쳐 리팩토링한 것으로, 조건에 맞는 상황이 되었을 때에만 메시지를 생성하도록 지연하는 conditional defferred execution 패턴을 사용했다.

  • execute around 패턴

    • 매번 같은 준비, 종료 과정을 반복하는 코드를 람다로 변환하여 재사용할 수 있다.

    • 아래는 file.txt 파일을 try-with-resource 문으로 접근하는 과정을 템플릿화 하고, 내부적으로 파일의 데이터를 어떻게 처리할지는 함수형 인터페이스인 BufferedReaderProcessor를 구현한 람다로 작성하도록 해 execute around 패턴을 구현한 예제이다.

람다와 디자인 패턴

전략 패턴

  • 전략 클래스에서는 다양하게 갈아끼울 수 있도록 인터페이스필드를 가지고 있으며, 이 인터페이스 필드에 구체적인 구현 클래스가 주입되는 패턴이다.

  • 전략 패턴에서 구현 클래스를 주입할 때 직접 구체 클래스를 정의하고 인스턴스를 생성하는 대신 람다를 사용해 간단히 주입할 수 있다.

  • 아래 예제에서는 String을 입력받아 유효성을 검사할 수 있는 ValidationStrategy 인터페이스를 Validator 전략 클래스가 필드로 가지고 있으며, 이 전략 클래스 내부에서 어떻게 유효성 검사를 할 지는 람다로 주입받도록 한다.

템플릿 메서드 패턴

  • 템플릿 메서드 패턴이란 변하는 부분을 추상 클래스의 추상 메서드를 구현하도록 하고 변하지 않는 부분은 추상 클래스의 메서드에 구현해두는 방식이다.

  • 람다를 사용할 경우 아래와 같이 추상 클래스의 추상 메서드를 두는 대신 람다식을 입력받아 수행하도록 할 수 있다.

옵저버 패턴

  • 어떤 이벤트가 발생했을 때 한 객체(Subject)가 다른 객체 리스트(Observer)에 자동으로 알림을 보내야 하는 패턴에서 옵저버 디자인 패턴을 사용한다.

  • NotiSubject 인터페이스에서 새로운 메시지를 notifyObservers 메서드를 통해 발행하면 등록된 모든 Observer로 해당 메시지가 전달되어 Observer의 notify 메서드가 호출된다.

  • 이 옵저버 패턴에서 각 Observer 구현체를 만들지 않고 람다식을 넘겨 옵저버를 등록하여 코드를 간결하게 만들 수 있다.

  • 람다 표현식으로 불필요한 코드가 제거 되는것이 바람직하다 할때만 람다식으로 사용하는 것을 권장한다.

  • 복잡한 로직이나 여러 메서드가 정의된다면 람다 표현식 보다 기존 방식을 고수하는 것이 오히려 가독성이 좋다.

의무 체인 패턴

  • 작업 처리 객체의 체인을 만들 때 사용하는 패턴으로 한 객체가 작업을 처리한 다음 다른 객체로 결과를 전달하고 다음 객체가 작업을 처리후 그 다음 객체로 전달하는 형식이다.

  • 일반적으로 아래와 같이 추상 클래스에 다음 객체를 필드로 갖고 있다가 본 클래스의 작업이 끝나면 다음 클래스의 작업을 수행해 결과를 반환한다.

  • 람다를 사용하여 UnaryOperator<String> 타입을 만들면, 구체 클래스를 생성하지 않고도 간단하게 부가 작업을 담당하는 구성 요소들을 만들어 체이닝할 수 있다.

정적 팩토리 메서드 보완

  • 인스턴스화 로직을 클라이언트에 노출하지 않고 객체를 만들 때 정적 팩토리 메서드를 사용한다.

  • 아래는 상품명을 입력받아 상품 객체를 생성해 반환하는 메서드이다.

  • 위와 같은 정적 팩토리 메서드를 람다를 사용해 아래와 같이 구현할 수 있다. (개인적으로는 이 부분은 람다 사용하지 않는 것이 더 깔끔해보인다.)

테스팅

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

  • 람다식은 함수형 인터페이스의 인스턴스를 생성하기 때문에 테스트할 때 인스턴스의 동작을 통해 테스트할 수 있다.

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

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

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

  • 아래와 같이 람다를 적용한 결과가 예상대로인지 테스트하면 된다.

복잡한 람다 개별 메서드로 분할

  • 복잡한 람다 표현식은 테스트코드에서 참조할 수 없으므로, 람다 표현식을 새로운 일반 메서드로 선언하고 메서드 참조할 수 있도록 바꾸어 테스트해야 한다.

고차원 함수 테스트

  • 함수를 입력받는 메서드의 경우 테스트 코드에서 직접 함수를 입력하여 동작을 검증할 수 있다.

  • 함수를 반환하는 메서드의 경우 반환된 함수의 동작을 테스트하여 검증해야 한다.

디버깅

  • 문제가 발생한 코드를 디버깅할 때에는 스택 트레이스와 로깅을 확인해야 한다.

  • 람다는 이름이 없기 때문에 스택 트레이스의 정보를 이해하기 어려울 수 있다.

  • 스트림 파이프라인에서 요소를 처리할 때 peek 메서드로 스트림 파이프라인의 중간 데이터를 로깅할 수 있다.

Last updated