[JAVA] 리팩터링, 테스팅, 디버깅

Jae-Baek Song·2023년 3월 2일

모던자바인액션

목록 보기
9/11
post-thumbnail

저자: 라울-게이브리얼 우르마 , 마리오 푸스코 , 앨런 마이크로프트
도서명: 모던 자바 인 액션
출판사: 한빛미디어


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

  • 람다는 함수형 인터페이스에서만 쓰인다.
  • 추상 클래스의 인스턴스를 만들 때 람다를 쓸 수 없으니, 익명 클래스를 써야한다.
  • 추상 메서드가 여러 개인 인터페이스의 인스턴스를 만들 때도 익명 클래스를 쓸 수 있다.
@FunctionalInterface
interface CustomInterface<T> {
    // abstract method 오직 하나
    T myCall();

    // default method 는 존재해도 상관없음
    default void printDefault() {
        System.out.println("Hello Default");
    }

    // static method 는 존재해도 상관없음
    static void printStatic() {
        System.out.println("Hello Static");
    }
}

public class DefaultStaticExampleClass implements CustomInterface {}
public class Main {
   static void main(String args[]) {
      // Call interface static method on Interface
      CustomInterface.printStatic();
      
      // Call default method on Class
      DefaultStaticExampleClass defaultStaticExampleClass = new DefaultStaticExampleClass();
      defaultStaticExampleClass.printDefault();
   }
}
default 메서드static 메서드
인스턴스 생성 후 호출할 수 있습니다.static 메서드는 클래스가 아닌 인터페이스 클래스에서만 호출할 수 있습니다.
클래스를 구현할 때 default 메서드를 재정의할 수 있습니다.인터페이스 및 구현 클래스는 서로를 재정의하지 않고 동일한 이름의 static 메서드를 가질 수 있습니다.
모든 구현 클래스에서 공통 기능을 제공하는 데 사용할 수 있습니다.유틸리티 방식으로 사용할 수 있습니다.
	public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor)
    {
        Objects.requireNonNull(keyExtractor);
        return (Comparator<T> & Serializable)
            (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
    }
    
    Comparator<Person> byLastName = Comparator.comparing(Person::getLastName);

https://www.tutorialspoint.com/difference-between-default-and-static-interface-method-in-java-8

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
    boolean equals(Object obj);
    ...
}

Comparator@FunctionalInterface 추상 메서드 2개가 가능한 이유 ?

https://stackoverflow.com/questions/51966837/equalsobject-and-functional-interfaces


	public interface Task { // Runnable	() -> void 과 같은 디스크립터
        public void execute();
    }

    private static void doSomething(Runnable r) {
        r.run();
    }

    private static void doSomething(Task a) {
        a.execute();
    }

    doSomething(new Task() {    //  익명 클래스 : 아무런 문제 없음
        @Override
        public void execute() {
            System.out.println("Danger danger!!");
        }
    });

    /* Ambiguous method call. Both doSomething (Runnable) in Main and doSomething (Task) in Main match
       아래와 같이 람다 표현식을 구현하면 위 두 함수 중 어느것을 구현한 것인지 알 수 없게 된다. 
    */
    doSomething(() -> System.out.println("Danger danger!!"));

    //  명시적 형변환 (Task)를 이용해 모호함을 제거할 수 있다.
    doSomething((Task) () -> System.out.println("Danger danger!!"));
  • 익명 클래스를 람다 표현식으로 바꾸면 콘텍스트 오버로딩에 따른 모호함이 초래 될 수 있다.
    시그니처가 같은경우 모호하다.

int a = 10;
Runnable r1 = () -> {
    int a =2; // Variable 'a' is already defined in the scope
    System.out.println(a);
};

Runnable r2 = new Runnable() {
    @Override
    public void run() {
    	System.out.println(a); //10
        int a =2;
        System.out.println(a); //2
    }
};

r2.run();
System.out.println(a); //10
  • 익명 클래스에서 this는 익명클래스 자신을 가리키지만 람다에서 this는 람다를 감싸는 클래스를 가리킨다.
  • 람다식과 익명클래스 모두 클로저(Closure)

클로저는 내부함수가 외부함수의 지역변수에 접근할 수 있는 것을 뜻한다.
특이한 것은 외부 함수가 종료되더라도 내부함수에서 참조하는 외부함수의 context는 유지 된다는 것이다.

public class closure {
    @Test
    void closure() {
        final var supplier = outerMethod();
        System.out.println(supplier.get());
    }
 
    private Supplier<String> outerMethod() {
        final String str = "outer method local variable";
        return () -> str;
    }
}

lambda capturing

이는 람다 (또는 로컬 클래스, 익명 클래스)가 자유 변수를 참조할 때 직접 그 변수를 참조하는 것이 아니라 자유 변수를 자신의 stack에 복사하여 참조하기 때문이다. 이를 lambda capturing(또는 variable capture) 라고 한다.

https://velog.io/@sdb016/Variable-Capture


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

매개변수의 수가 다른 경우
메서드 참조는 매개변수의 수와 타입이 일치하는 메서드를 참조하는 것이므로, 매개변수의 수가 다른 경우에는 메서드 참조를 사용할 수 없습니다.


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

명령형 프로그래밍

var string = 'THis is the midday show with Cheryl Waters';
var urlFriendly = "";

for(var i=0; i<string.length; i++){
    if(string[i] === " "){
        urlFriendly += "-";
    }else{
        urlFriendly += string[i];
    }
}

console.log(urlFriendly);

이 예제는 문자열의 모든 문자를 루프를 돌면서 공백을 만날 때마다 그 공백을 -로 바꿉니다.
이런 구조의 프로그래밍은 우리가 원하는 것을 달성하는 방법에만 신경을 씁니다.

선언적 프로그래밍

const string = 'This is the midday show with Cheryl Waters';
const urlFriendly = string.replace(/ /g, '-');

console.log(urlFriendly);

여기서 string.replace와 정규식을 사용해서 모든 공백을 하이픈으로 변경합니다.
string.replace를 사용하면 모든 공백이 하이픈으로 변경되어야 한다는 사실을 기술할 수 있습니다.
모든 공백을 하이픈으로 변경하는 자세한 방법은 replace 함수 안에 들어가고, 구체적 절차 대신 replace(치환)라는 함수를 사용해 추상적인 개념을 표현합니다. 선언적 프로그래밍의 코드 구문은 어떤 일이 발생해야 하는지 기술하고, 실제로 그 작업을 처리하는 방법은 추상화로 아랫단에 감추어집니다.


조건부 연기 실행

람다식이 실제로 실행되는 시점은 해당 람다식이 전달된 메서드에서 호출될 때입니다.
이렇게 람다식이 작성되는 시점과 실행되는 시점이 다르기 때문에, 람다식이 생성되는 시점에는 해당 코드 블록이 실행되지 않고, 호출될 때에만 실행됩니다.
이를 통해 불필요한 계산을 줄이고, 성능을 개선할 수 있습니다.

https://m.blog.naver.com/zzang9ha/222087025042


실행 어라운드

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

전략 패턴

알고리즘을 정의하고, 각각의 알고리즘을 구현한 클래스를 생성하여 전략 객체를 생성합니다. 이후 의존성 주입(DI)을 통해서 런타임에 알고리즘을 변경할 수 있도록 전략 객체를 교체합니다.

전략 패턴에 함수형 인터페이스를 사용할경우 다양한 전략을 람다 표현식으로 직접 전달 하는 방식으로 구현가능하다.

템플릿 메소드

상위 클래스에서 알고리즘의 일부를 구현하고, 하위 클래스에서 나머지 일부를 구현하는 추상 메서드를 정의합니다. 하위 클래스에서 구현된 메서드는 상위 클래스에서 정의된 알고리즘의 일부가 되어 전체 알고리즘을 완성합니다. 의존 역전 법칙(DIP)

옵저버 패턴

객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴

의무 체인

객체들의 집합에서 각 객체가 특정 작업을 수행한 후에 다음 객체가 작업을 계속할 수 있도록 연결된 체인 형태의 구조를 갖는 패턴입니다.

  @FunctionalInterface
  public interface Function<T, R> {
    R apply(T t);

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
      return t -> t;
    }
  }

팩토리

//람다 사용 전
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("No such product " + name);
        }
    }
}
//람다 사용 후
Supplier<Product> loanSupplier = Loan::new; 
Loan loan = loanSupplier.get();

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 class Point {
        public final static Comparator<Point> compareByXAndTehnY =
                comparing(Point::get).thenComparing(Point::getY);
}

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

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

고차원 함수 테스팅

다른 함수를 인자로 받는 함수는 Array.map, Array.filter, Array.reduce와 같은 함수들이 존재합니다.

함수형 인터페이스의 인스턴스로 간주하고 함수의 동작을 테스트 할 수 있다.

스택 트레이스 확인

람다 표현식은 이름이 없기 때문에 조금 복잡한 스택 트레이스가 생성된다.
메서드 참조를 사용하는 클래스와 같은 곳에 선언되어 있는 메서드를 참조할 때는 메서드 참조 이름이 스택 트레이스에 나타난다.

정보 로깅

forEach를 호출하는 순간 전체 스트림이 소비된다.
peek은 자신이 확인한 요소를 파이프라인의 다음 연산으로 그대로 전달한다.

0개의 댓글