리팩터링, 테스팅, 디버깅

김종준·2023년 2월 9일
0

모던자바

목록 보기
7/15

리팩터링, 테스팅, 디버깅

코드 가독성

익명클래스를 람다 표현식으로

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

익명 클래스는 코드를 장황하게 만들고 쉽게 에러를 일으킨다.

람다 표현식을 활용하면 간결하고, 가독성이 좋은 코드를 구현할 수 있다.

이때 모든 익명 클래스를 람다 표현식으로 변환할 수 있는 것은 아니다.

그리고 익명클래스와 람다 표현식은 아래와 같은 차이가 있게 이에 주의하여야 한다.

첫째, 익명 클래스에서 사용한 this와 super은 람다 표현식에서 다른 의미를 가진다.

익명 클래스에서 this는 익명 클래스 자신을 가리키지만 람다에서 this는 람다를 감싸는 클래스를 가리킨다.

둘재, 익명 클래스는 감싸고 있는 클래스의 변수를 가릴 수 있다.

// a = 10을 콜솔을 통해 보여주길 바란다.
int a = 10;

// 람다
Runnable r1 = () -> {
  int a = 2; // 컴파일 에러
  System.out.println(a);
}

Runnable r2 = new Runnable() {
  public void run() {
  	int a = 2; // 익명 클래스가 클래스의 변수를 가려 정상 작동
	  System.out.println(a);
  }
}

마지막으로 익명 클래스를 람다 표현식으로 바꾸면 콘텍스트 오버로딩에 따른 모호함이 초래될 수 있다.

익명 클래스는 인스턴스화할 때 명시저긍로 형식이 정해지는 반면 람다 형식은 콘텍스트에 따라 달라지기 때문이다.

하지만 이는 명시적 형변환을 이용해 모호함을 제거할 수 있다.

람다 표현식을 메서드 참조로

람다 표현식은 쉽게 전달할 수 있는 짧은 코드다.

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

메서드 참조의 메서드 명으로 코드의 의도를 명확하게 알릴 수 있기 때문이다.

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

이론적으로는 반복자를 이용한 기존의 모든 컬렉션 처리 코드를 스트림 API로 바꿔야 한다.

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

코드 유연성 개선

함수형 인터페이스

람다 표현식을 이용하려면 함수형 인터페이스가 필요하다.

조건부 연기 실행

java의 Logger를 가지고 예제를 구성합니다.

if(logger.isLoggable(Log.FINER)) {
  logger.finer("Problem: " + generateDiagnostic()); // diagnostics: 진단
}

위 코드는 다음과 같은 문제가 있다고 한다.

  • logger의 상태가 isLoggable이라는 메서드에 의해 클라이언트 코드로 노출된다.
  • 메시지를 로깅할 때마다 logger 객체의 상태를 매번 확인한다.(generateDiagnostic())

그럼 아래처럼 수정하면 어떨까?

logger.log(level.FINER, "Problem: " + generateDiagnostic());

여전히 logger 객체의 상태를 매번 확인하는 과정이 필요하다.

generateDiagnostic()을 특정조건(FINER)에서만 실행될 수 있도록 메시지 생성 과정을 연기할 수 있어야 한다.

이를 위해 Supplier를 인수로 갖는 logger.log()를 사용하면 된다.

logger.log(Level.FINER, () -> "Problem: " + generateDiagnostic());
public void log(Level level, Supplier<String> msgSupplier) {
  if(logger.isLoggable(level)) { 
    log(leve, msgSupplier.get());
  }
}

위의 코드를 보면 logger.isLoggable(level)에서 FINER 여부를 확인한 다음 msgSupplier.get()을 수행한다.

즉, FINER이 아니라면 msgSupplier.get() 이 수행되지 않는 것이다.

다른 말로 logger 객체의 상태를 매번 확인하지 않는 것이다.

실행 어라운드

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

준비, 종료 과정을 처리하는 로직을 재사용함으로써 코드 중복을 줄일 수 있다.

이전글에서 동작 파라미터화를 아래와 같이 정리하였다.

요구사항은 항상 변경될 수 있고 우리는 이를 대비하여야 한다.

이를 대비하는 방법이 인터페이스(ex ApplePredicate)를 통해 요구사항을 어떻게 수행할지 틀만 정의하고 구체적인 코드(ex AppleColorPredicate)는 파라미터로 전달받는 것이다.

이때 파라미터로 전달받는 방법은 인터페이스를 정의한 클래스, 익명 클래스, 람다 표현 식과 같은 방법이 있다.

이때 함수형 인터페이스의 경우 객체 생성 타이밍을 조절할 수 있다는 점을 염두하고 있으면 좋을 것 같다.

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

전략

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

public interface ValidationStrategy {
  boolean execute(String s);
}
public class Validator {
  private final ValidationStrategy strategy;
  public Validator(ValidationStrategy v) {
    this.strategy = v;
  }
  public boolean validate(String s) {
    return strategy.execute(s);
  }
}
Validator numericValidator = new Validator(new IsNumeric());

이전에는 위와 같이 코드를 작성하였다.

하지만 람다 표현식을 활용하면 아래와 같이 위의 코드를 활용할 수 있다.

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

람다 표현식은 코드 조각을 캡슐화한다.

즉, 람다 표현식으로 전략 디자인 패턴을 대신할 수 있다.

템플릿 메서드

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

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

위의 OnlinBanking 을 상속받아 그냥 구현한다면 makeCustomerHappy 를 고칠 수 있는 유연함을 제공할 수 없다.

하지만 아래와 같이 함수형 인테페이스를 매개변수를 추가하는 코드를 수정하면 알고리즘의 일부를 고칠 수 있는 유연함을 가질 수 있다.

abstract class OnlinBanking {
  public void processCustomer(int id, Consumer<Customer> makeCustomerHappy) {
    Customer c = Database.getCustomerWithid(id);
    makeCustomerHappy.accept(c);
  }
}

위와 같이 메서드를 추가하였다면 이제는 람다 표현식을 사용하여 메서드를 구현할 수 있다.

옵저버

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

이는 아래와 같은 인터페이스를 구현하여 구현할 수 있다.

interface Observer {
  void notify(String tweet);
}
interface Subject {
  void registerObserver(Observer o);
  void notifyObservers(String tweet);
}

람다 표현식의 경우 Observer를 파라미터로 받는 registerObserver()를 사용할 때 사용한다.

하지만 이때 옵저버가 상태를 가지면 람다 표현식보다는 기존 클래스의 구현방식을 사용하는 것이 좋을 수 있다.

의무체인

작업 처리 객체의 체인을 만들 때는 의무 체인 패턴을 사용한다.

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

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);
}

위의 코드를 보면 공개된 메서드는 handle()이다.

우선 보호된 내부의 handleWork()를 통해 1차적으로 input을 처리한다.

이후 successor이 존재한다면 그 결과를 successor의 handle()에게 넘겨준다.

만약 successor이 없다면 handleWork()을 통해 처리한 결과를 반환해 준다.

이는 Function<String, String> 더 정확히는 UnaryOperator<String> 형식의 인스턴스로도 표현할 수 있기에 람다 표현식을 사용할 수 있다.

작업처리 체인의 경우 andThen 메서드로 이들 함수를 조합해서 만들 수 있다.

팩토리

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

public class ProductFactory {
  public static Product createProduct(String name) {
    switch(name) {
      case "loan" : return new Loan();
      case "stock" : return new Stock();
        ...
    }
  }
}

위를 보면 가독성이 떨어진다.

이를 람다 표현식을 사용하여 리펙토링하면 다음과 같이 수정할 수 있다.

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);
}

어떻게 위와 같이 수정할 수 있을까?

우선 "loan"과 "stock" 같은 것이 Product라는 공통의 틀을 가지고 있어야 한다.

그리고 new를 통해 직접 객체를 생성하는 것이 아닌 Supplier와 같은 공통의 Product를 만들 수 있는 틀을 만든다.

Supplier<Product> loanSupplier = Loan::new;

이렇게 만든 틀을 Map을 통해 모아둔다.

final static Map<String, Supplier<Product>> map = new HashMap<>();

static {
  map.put("loan", loanSupplier);
  map.put("stock", stockSupplier);
  ...
}

이제 Map에서 필요한 생성틀을 꺼내서 위의 코드처럼 사용할 수 있게된 것이다.

람다 테스팅

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

따라서 생성된 인스턴스의 동작으로 람다 표현식을 태스트 할 수 있다.

람다를 테스트 하기 위해서는 람다를 사용하는 메서드의 동작에 집중해야 하는 것이다.

디버깅

문제가 발생한 코드를 디버깅할 때 개발자는 다음 두가지를 확인해야 한다.

  • 스택 트레이스
  • 로깅

스택 트레이스

예외 발생으로 프로그램 실행이 갑자기 중된되었다면 먼저 어디에서 멈췄고 어떻게 멈추게 되었는지 살펴봐야 한다.

스택 프레임에서 이 정보를 얻을 수 잇다.

프로그램이 메서드를 호출할 때마다 프로그램에서의 호출 위치, 호출할 때의 인수값, 호출된 메서드의 지역 변수 등을 포함한 호출 정보가 생성되며 이들 정보는 스택 프레임에 저장된다.

따라서 프로그램이 멈췄다면 프로그램이 어떻게 멈추게 되었는지 프레임별로 보여주는 스택트레이스를 얻을 수 있다.

하지만 람다 표현식을 사용하면 스택 트레이스가 복잡하게 생성된다.

at Debuggin.lambda$main$0(...)

이는 메서드 참조를 사용해도 마찬가지이다.

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

0개의 댓글