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

이주오·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개의 댓글

관련 채용 정보