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

이주오·2021년 8월 30일
0

도서

목록 보기
9/15

이번 주제 키워드

  • 람다 표현식으로 코드 리팩터링 하기
  • 람다 표현식이 객체지향 설계 패턴에 미치는 영향
  • 람다 표현식 테스팅
  • 람다 표현식과 스트림 API 사용 코드 디버깅

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

  • 람다 표현식은 익명 클래스보다 코드를 좀 더 간결하게 만든다.
  • 그뿐만 아니라 동작 파라미터화의 형식을 지원하므로 람다 표현식을 이용한 코드는 더 큰 유연성을 갖출 수 있다.
  • 람도 표현식을 이용하여 코드를 리택터링 해보자!!

코드 가독성 개선

일반적으로 코드 가독성이 좋다는 것은 '어떤 코드를 다른 사람이 보았을 때 쉽게 이해할 수 있음'을 의미한다.

여러가지 방법들이 있지만 세 가지 리팩터링 예제를 알아보자

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

    • 하지만 모든 익명 클래스를 람다 표현식으로 변환할 수 있는 것은 아니다.
    • 뒤에서 확인
  2. 람다 표현식을 메서드 참조로 리팩터링하기

    • 메서드 참조의 메서드명으로 코드의 의도를 명확하게 알릴 수 있다.
  3. 명령형 데이터 처리를 스트림으로 리팩터링하기

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

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

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

Runnable r2 = () -> System.out.println("hello");
  • 하지만 모든 익명 클래스를 람다 표현식으로 변환할 수 있는 것은 아니다.
  • 익명 클래스에서 this는 익명 클래스 자신을 가리키지만 람다에서 this는 람다를 감싸는 클래스를 가리킨다.
  • 익명 클래스는 감싸고 있는 클래스의 변수를 가릴 수 있다.
    • shadow variable
int a = 10;
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        int a = 2;
        System.out.println(a);
    }
};

Runnable r2 = () -> {
    int a = 2; // error
    System.out.println(a);
};
  • 익명 클래스는 인스턴스화할 때 명시적으로 형식이 정해지는 반면 람다의 형식은 콘텍스트에 따라 달라진다.

interface Task {
    void execute();
}
public static void doSomething(Runnable r) { r.run(); }
public static void doSomething(Task t) { t.execute(); }
  • 익명 클래스와 달리 람다 표현식으로 인수를 전달하면 Runnable과 Task 모두 대상 형식이 될 수 있으므로 문제가 생긴다.
  • 명시적 형변환을 이용해서 제거할 수 있다.

코드 유연성 개선

  • 람다 표현식을 이용하면 동작 파라미터화(behavior parameterzation)을 쉽게 구현할 수있다.
  • 따라서 변화하는 요구사항에 대응할 수 있는 코드 구현 가능

함수형 인터페이스 적용

  • 람다 표현식을 사용하기 위해 함수형 인터페이스 적용해야 한다.
    • 조건부 연기 실행실행 어라운드 패턴을 살펴보자


조건부 연기 실행

  • 코드 내부에 제어 흐름문이 복잡하게 얽힌 코드를 볼 수 있다.
  • 만약 클라이언트 코드에서 객체 상태를 자주 확인하거나,객체의 일부 메서드를 호출하는 상황이라면 내부적으로 객체의 상태를 확인한 다음에 메서드를 호출(람다나 메서드 참조를 인수로 사용)하도록 새로운 메서드를 구현하는 것이 좋음
  • 코드 가독성이 좋아질 뿐아니라 캡슐화도 강화됨 (객체 상태가 클라이언트 코드로 노출되지 않음)

실행 어라운드

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

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

디자인 패턴에 람다 표현식이 더해지면 색다른 기능을 발휘할 수 있다.

전략 (Strategy)

  • 전략 패턴은 한 유형의 알고리즘을 보유한 상태에서 런타임에 적절한 알고리즘을 선택하는 기법이다.
  • 전략을 구현하는 새로운 클래스를 람다 표현식을 통해 직접 전달할 수 있다.

기존 패턴

@FunctionalInterface
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 validationStrategy;

    public Validator(ValidationStrategy validationStrategy) {
        this.validationStrategy = validationStrategy;
    }
    
    public boolean validate(String s) {
        return validationStrategy.execute(s);
    }
}

Validator numericValidator = new Validator(new isNumeric());
numericValidator.validate("aaa");

Validator lowerCaseValidator = new Validator(new IsAllLowerCase());
lowerCaseValidator.validate("bbbb");

람다 사용

Validator lowerCaseValidator2 = new Validator((String s) -> s.matches("[a-z]+"));
lowerCaseValidator2.validate("bbbb");

Validator numericValidator2 = new Validator((String s) -> s.matches("\d+"));
numericValidator2.validate("1234");

템플릿 메서드 (template method)

  • 알고리즘의 개요를 제시한 다음에 알고리즘의 일부를 고칠 수 있는 유연함을 제공해야 할 때 템플릿 메서드 디자인 패턴을 사용한다.
  • 추상 메서드로 원하는 동작을 구현하는 곳을 람다 표현식을 통해 전달할 수 있다.

기존

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

  abstract void makeCustomerHappay(Customer c);
}

람다 사용

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

new OnlineBankingLambda().processCustomer(1337, (Customer c) -> print("hello" + c.getName()));
  • 이전에 정의한 makeCustomerHappy의 메서드 시그니처와 일치하도록 Consumer 형식을 갖는 두 번째 인수를 메서드에 추가


옵저버 (observer)

  • 어떤 이벤트가 발생했을 때 한 객체(subject)가 다른 객체 리스트(observer)에 자동으로 알림을 보내야 하는 상황에서 사용하는 패턴이다.
  • 자세히 보고 싶다면? 여기


기존

interface Observer {
  void notify(String tweet);
}

public class NyTimes implements NotiObserver {
    @Override
    public void notify(String tweet) {
        if (tweet != null && tweet.contains("monety")) {
            System.out.println("Breaking news in NY ! " + tweet);
        }
    }
}

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

public class LeMonde implements NotiObserver {
    @Override
    public void notify(String tweet) {
        if (tweet != null && tweet.contains("wine")) {
            System.out.println("Today cheese, wine and news! " + tweet);
        }
    }
}

public interface NotiSubject {
    void registerObserver(NotiObserver o);
    void notifyObservers(String tweet);
}

public class Feed implements NotiSubject {
    private final List<NotiObserver> observers = new ArrayList<>();
    @Override
    public void registerObserver(NotiObserver o) {
        observers.add(o);
    }

    @Override
    public void notifyObservers(String tweet) {
        observers.forEach(o -> o.notify(tweet));
    }
}

Feed f = new Feed();
f.registerObserver(new NyTimes());
f.registerObserver(new LeMonde());
f.registerObserver(new Guardian());
f.notifyObservers("The Queen ...")
  • Observer 인터페이스는 새로운 트윗이 있을 때 subject가 호출할 수 있도록 notify라고 하는 하나의 메서드를 제공한다.
  • Observer 인테페이스를 구현하는 클래스를 만드는 대신 람다 표현식을 직접 전달해서 실행할 동작을 지정할 수 있다.

람다

Feed feed = new Feed();
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 new from London .." + tweet);
    }
});
  • 하지만 옵저버상태를 가지며, 여러 메서드를 정의하는 등 복잡하다면 람다 표현식보다 기존의 클래스 구현방식을 고수하는 것이 바람직할 수 있다.

의무 체인 (chain-of-responsibility)

  • 작업 처리 객체의 체인(동작 체인 등)을 만들 때는 의무 체인 패턴을 사용한다.
  • 한 객체가 어떤 작업을 처리한 다음에 다른 객체로 결과를 전달하고, 다른 객체도 해야 할 작업을 처리한 다음에 또 다른 객체로 전달하는 식이다.
  • 일반적으로 다음으로 처리할 객체 정보를 유지하는 필드를 포함하는 작업 처리 추상 클래스로 의무 체인 패턴을 구성한다.
    • 작업 처리 객체가 자신의 작업을 끝냈으면 다음 작업 처리 객체로 결과를 전달한다.

작업 처리 객체 예제 코드

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);
}
  • UML를 자세히 살펴보면 템플릿 메서드 패턴이 사용되었음을 알 수 있다.
  • handle 메서드는 일부 작업을 어떻게 처리할지 전체적으로 서술한다.
  • ProcessingObject 클래스를 상속받아 handleWork 메서드를 구현하여 다양한 종류의 작업 처리 객체를 만들수 있다.

기존

public class HandleTextProcessing extends ProcessingObject<String> {
    @Override
    protected String hadleWork(String input) {
        return "From Raoul, Mario and Alan : " + input;
    }
}

public class SpellCheckProcessing extends ProcessingObject<String> {
    @Override
    protected String hadleWork(String input) {
        return input.replaceAll("labda", "lambda");
    }
}

ProcessingObject<String> p1 = new HandleTextProcessing();
ProcessingObject<String> p2 = new SpellCheckProcessing();
p1.setSuccessor(p2);
p1.handle("Aren't ladbas really sexy?");

람다

UnaryOperator<String> headerProcessing = (String text) -> "From Raoul, Mario and Alan : " + text;
UnaryOperator<String> spellCheckProcessing = (String text) -> text.replaceAll("labda", "lambda");
Function<String, String> pipeline = headerProcessing.andThen(spellCheckProcessing);
pipeline.apply("Aren't ladbas really sexy?");
  • 이러한 패턴은 함수 체인과 비슷하다.
  • 람다 표현식을 조합하는 방식으로는 기본적으로 compose, andThen이 있다.
  • andThen메서드로 이들 함수를 조합해 체인을 만들어 보자.

팩토리 (factory)

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

기존

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("...");
        }
    }
}

Product p = ProductFactory.createProduct("loan");
  • createProduct 메서드는 생산된 상품을 설정하는 로직을 포함할 수 도 있지만
  • 주요 목적은 생성자와 설정을 외부로 노출하지 않음으로써 클라이언트가 단순하게 상품을 create 할 수 있다는 점

람다

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);
  ...
}
  • 하지만 팩토리 메서드 createProduct가 상품 생성자로 여러 인수로 전달하는 상황에서는 인수 개수에 맞게 특별한 함수형 인터페이스를 만들어야 한다.
  • 그러면 Map 시그니처가 복잡해진다.

람다 테스팅

  • 람다에 대해서도 단위 테스팅(unit testing)이 작성되어야만 한다.
  • 하지만 람다는 익명이므로 테스트 코드 이름을 호출할 수 없다.

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

  • 람다의 동작을 테스트 하기 위해 람다를 필드에 저장해서 테스트할 수 있다.

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

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

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

  • 복잡한 로직이 포함된 람다를 구현하게 된다면 로직을 분리 하거나 메서드 레퍼런스를 활용하도록 하자.
  • 그러면 일반 메서드를 테스트하듯이 람다 표현식을 테스트할 수 있다.

고차원 함수 테스팅

고차원 함수란 함수를 인수로 받거나 다른 함수를 반환하는 메서드 이다.

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

디버깅

람다 표현식과 스트림은 기존의 디버깅 기법을 무력화한다. 디버깅 방법을 살펴보자!!

스택 트레이스 확인

  • 람다 표현식은 이름이 없기 때문에 조금 복잡한 스택 트레이스가 생성된다.
  • 그렇기에 람다 표현식과 관련된 스택 트레이스는 이해하기 어려울 수 있다.
  • 이는 미래의 자바 컴파일러가 개선해야 할 부분이다.

정보 로깅

  • forEach를 통해 스트림 결과를 출력하거나 로깅할 수 있다. 하지만 forEach는 스트림을 소비하는 연산이다.
  • 스트림 파이프라인에 적용된 각각의 연산의 결과를 확인할 수 있다면 대신 peek라는 스트림 연산을 활용할 수 있다.
  • peek는 스트림의 각 요소를 소비한것 처럼 동작을 실행하지만, 실제로 스트림을 소비하지않고 자신이 확인한 요소를 파이프라인의 다음 연산으로 그대로 전달한다.

참고출처

profile
동료들이 같이 일하고 싶어하는 백엔드 개발자가 되고자 합니다!

0개의 댓글