티스토리 뷰

728x90

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

코드 가독성 개선

일반적으로 코드 가독성이 좋다는 것은 어떤 코드를 다른 사람도 쉽게 이해할 수 있음을 의미한다. 즉, 코드 가독성을 개선한다는 것은 우리가 구현한 코드를 다른 사람이 쉽게 이해하고 유지보수할 수 있게 만드는 것을 의미한다. 코드 가독성을 높이려면 코드의 문섷화를 잘하고, 표준 코딩 규칙을 준수하는 등의 노력을 기울여야 한다.

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

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

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

Runnable r2 = () -> System.out.println("Hello");
  1. 익명 클래스에서 사용한 this와 super는 람다 표현식에서 다른 의미를 갖는다.
    1. 익명 클래스에서 this는 익명클래스 자신을 가리키지만 람다에서 this는 람다를 감싸는 클래스를 가리킨다.
  2. 익명 클래스는 감싸고 있는 클래스의 변수를 가릴 수 있다.
  3. 익명 클래스는 인스턴스화할 때 명시적으로 형식이 정해지는 반면 람다의 형식은 컨텍스트에 따라 달라진다.

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

람다 표현식 대신 메서드 참조를 이용하면 가독성을 높일 수 있다. 메서드 참조의 메서드명으로 코드의 의도를 명확하게 알릴 수 있기 때문이다.

inventory.sort((Apple a1, Apple a2) -> a1.getWeight() - a2.getWeight()); //비교 구현에 신경써야 한다.
inventory.sort(comparing(Apple::getWeight)); //코드가 문제 자체를 설명한다.

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

스트림 API는 데이터 처리 파이프라인의 의도를 더 명확하게 보여준다. 스트림은 쇼트서킷과 게으름이라는 강력한 최적화뿐 아니라 멀티코어 아키텍처를 활용할 수 있는 지름길을 제공한다.

코드 유연성 개선

동작 파라미터화를 쉽게 구현. 즉, 다양한 람다를 전달해서 다양한 동작을 표현할 수 있다. 따라서 변화하는 요구사항에 대응할 수 있는 코드를 구현할 수 있다.

  1. 조건부 연기 실행
    1. Supplier 사용
  2. 실행 어라운드
    1. 매번 같은 준비, 종료 과정을 반복적으로 수행하는 코드가 있다면 이를 람다로변환할 수 있다. 준비, 종료 과정을 처리하는 로직을 재사용함으로써 코드 중복을 줄인다.

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

디자인 패턴에 람다 표현식이 더해지면 색다른 기능을 발휘할 수 있다. 즉, 람다를 이용하면 이전에 디자인 패턴으로 해결하던 문제를 더 쉽고 간단하게 해결할 수 있다. 또한 람다 표현식으로 기존의 많은 객체지향 디자인 패턴을 제거하거나 간결하게 재구현할 수 있다.

전략 패턴

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

소문자 또는 숫자로 이루어져야 하는 등 텍스트 입력이 다양한 조건에 맞게 포맷되어 있는지 검증

소문자인지 체크하는 전략과 숫자로만 이루어져있는지에 대한 전략이 있다.

interface ValidationStrategy {
  boolean execute(String s);
}

static private class IsAllLowerCase implements ValidationStrategy {
  @Override
  public boolean execute(String s) {
    return s.matches("[a-z]+");
  }
}

static private class IsNumeric implements ValidationStrategy {
  @Override
  public boolean execute(String s) {
    return s.matches("\\d+");
  }
}

static private class Validator {

  private final ValidationStrategy strategy;

  public Validator(ValidationStrategy v) {
    strategy = v;
  }

  public boolean validate(String s) {
    return strategy.execute(s);
  }

}

람다를 사용하면 다양한 전략을 구현하는 새로운 클래스를 구현할 필요 없이 람다 표현식을 직접 전달할 수 있다. 코드가 한결 더 간결해진다.

public static void main(String[] args) {
    // old school
    Validator v1 = new Validator(new IsNumeric());
    System.out.println(v1.validate("aaaa"));
    Validator v2 = new Validator(new IsAllLowerCase());
    System.out.println(v2.validate("bbbb"));

    // with lambdas
    Validator v3 = new Validator((String s) -> s.matches("\\d+"));
    System.out.println(v3.validate("aaaa"));
    Validator v4 = new Validator((String s) -> s.matches("[a-z]+"));
    System.out.println(v4.validate("bbbb"));
}

람다 표현식을 이용하면 전략 디자인 패턴에서 발생하는 자잘한 코드를 제거할 수 있다. 람다 표현식은 코드 조각(또는 전략)을 캡슐화 한다.

템플릿 메서드

템플릿 메서드는 이 알고리즘을 사용하고 싶은데 그대로는 안 되고 조금 고쳐야 하는 상황에 적합하다.

예) 은행마다 다양한 온라인 뱅킹 애플리케이션을 사용하며 동작 방법도 다르다. 온라인 뱅킹 애플리케이션의 동작을 정의하는 추상 클래스.

abstract class OnlineBanking {

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

}

processCustomer 메서드는 온라인 뱅킹 알고리즘이 해야 할 일을 보여준다. 각각의 지점은 OnlineBanking 클래스를 상속받아 makeCustomerHappy 메서드가 원하는 동작을 수행하도록 구현할 수 있다. 이럴 때 상속을 받지 않고 직접 람다 표현식을 전달해서 다양한 동작을 추가할 수 있다.

public class OnlineBankingLambda {

    public static void main(String[] args) {
        new OnlineBankingLambda().processCustomer(1337, (Customer c) -> System.out.println("Hello!"));
    }

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

옵저버

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

옵저버 인터페이스는 새로운 트윗이 있을 때 주제가 호출할 수 있도록 notify라고 하는 하나의 메서드를 제공한다.

interface Observer {
    void notify(String tweet);
}

트윗에 포함된 다양한 키워드에 다른 동작을 수행할 수 있는 여러 옵저버를 정의할 수 있다.

static private class NYTimes implements Observer {

    @Override
    public void inform(String tweet) {
        if (tweet != null && tweet.contains("money")) {
            System.out.println("Breaking news in NY!" + tweet);
        }
    }

}

주제도 구현하는데 주제는 registerObserver 메서드로 새로운 옵저버를 등록한 다음에 notifyObservers 메서드로 트윗의 옵저버에 이를 알린다.

interface Subject {
    void registerObserver(Observer o);

    void notifyObservers(String tweet);
}

static private class Feed implements Subject {

    private final List<Observer> observers = new ArrayList<>();

    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }

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

구현. Feed는 트윗을 받았을 때 알림을 보낼 옵저버 리스트를 유지한다.

옵저버 패턴에서의 람다 표현식 사용은 Observer 인터페이스를 구현하는 모든 클래스는 하나의 메서드 notify를 구현했다. 즉, 트윗이 도착했을 때 어떤 동작을 수행할 것인지 감싸는 코드를 구현한 것인데 람다는 불필요한 감싸는 코드 제거 전문가이다. 세 개의 옵저버를 명시적으로 인스턴스화 하지 않고 람다 표현식을 직접 전달해서 실행할 동작을 지정할 수 있다.

Feed feedLambda = new Feed();

feedLambda.registerObserver((String tweet) -> {
    if (tweet != null && tweet.contains("money")) {
        System.out.println("Breaking news in NY! " + tweet);
    }
});

하지만 옵저버가 상태를 가지며, 여러 메서드를 정의하는 등 복잡하다면 람다 표현식보다 기존의 클래스 구현방식을 고수하는 것이 바람직할 수 있다.

의무 체인

작업 처리 객체의 체인(동작 체인 등)을 만들 때는 의무 체인 패턴을 사용한다. 한 객체가 어떤 작업을 처리한 다음에 다른 객체로 결과를 전달하고, 다른 객체도 해야 할 작업을 처리한 다음에 또 다른 객체로 전달하는 식이다.

private static 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);

}

ProcessingObject 클래스를 상속받아 handleWork 메서드를 구현하여 다양한 종류의 작업 처리 객체를 만들 수 있다.

private static class HeaderTextProcessing extends ProcessingObject<String> {

    @Override
    public String handleWork(String text) {
        return "From Raoul, Mario and Alan: " + text;
    }
}

private static class SpellCheckerProcessing extends ProcessingObject<String> {

    @Override
    public String handleWork(String text) {
        return text.replaceAll("labda", "lambda");
    }
}

public static void main(String[] args) {
    ProcessingObject<String> p1 = new HeaderTextProcessing();
    ProcessingObject<String> p2 = new SpellCheckerProcessing();
    p1.setSuccessor(p2);
    String result1 = p1.handle("Aren't labdas really sexy?!!");
    System.out.println(result1);
}

이 패턴은 함수 체인(함수 조합)과 비슷한데 작업 처리 객체를 Function<String, String> 더 정확히 표현하자면 UnaryOperator 형식의 인스턴스로 표현할 수 있다. andThen 메서드로 이들 함수를 조합해서 체인을 만들 수 있다.

UnaryOperator<String> headerProcessing = (String text) -> "From Raoul, Mario and Alan: " + text;
UnaryOperator<String> spellCheckerProcessing = (String text) -> text.replaceAll("labda", "lambda");
Function<String, String> pipeline = headerProcessing.andThen(spellCheckerProcessing);
String result2 = pipeline.apply("Aren't labdas really sexy?!!");
System.out.println(result2);

팩토리

인스턴스화 로직을 클라이언트에게 노출하지 않고 객체를 만들 때 팩토리 디자인 패턴을 사용한다. 상품명을 생성자로 연결하는 Map을 만들어서 코드를 재구현할 수 있다.

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

static {
    map.put("loan", Loan::new);
    map.put("stock", Stock::new);
    map.put("bond", Bond::new);
}

이제 Map을 이용해서 팩토리 디자인 패턴에서 했던 것 처럼 다양한 상품을 인스턴스화 할 수 있다.

public static Product createProductLambda(String name) {
    Supplier<Product> p = map.get(name);
    if (p != null) {
        return p.get();
    }
    throw new RuntimeException("No such product " + name);
}

람다 테스팅

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

람다는 익명(결국 익명 함수) 이므로 테스트 코드 이름을 호출할 수 없다.

람다 표현식은 함수형 인터페이스의 인스턴스를 생성한다는 사실을 기억하자. 따라서 생성된 인스턴스의 동작으로 람다표현식을 테스트할 수 있다.

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

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

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

복잡한 람다 표현식은 테스트 코드에서 람다 표현식을 참조할 수 없는데 이럴 때는 람다 표현식을 메서드 참조로 바꾸는 것이다. 그러면 일반 메서드를 테스트 하듯이 람다 표현식을 테스트할 수 있다.

디버깅

문제가 발생한 코드 디버깅시 가장 먼저 확인해야할 두 가지

  • 스택 트레이스
  • 로깅

스택 트레이스 확인

프로그램이 멈췄다면 프로그램이 어떻게 멈추게 되었는지 프레임별로 보여주는 스택 트레이스를 얻을 수 있다. 유감스럽게도 람다 표현식은 이름이 없기 때문에 조금 복잡한 스택 트레이스가 생성된다. 람다 표현식과 관련한 스택 트레이스는 이해하기 어려울 수 있다는 점을 염두에 두자. 이는 미래의 자바 컴파일러가 개선해야 할 부분이다.

정보 로깅

스트림의 파이프라인 연산을 디버깅 한다고 할 때 forEach로 스트림의 결과를 출력하거나 로깅할 수 있다. 그러나 안타깝게도 forEach를 출력하는 순간 전체 스트림이 소비되므로 스트림의 각각의 연산이 어떤 결과를 도출하는지 알기가 쉽지 않다.

이럴 때 peek라는 스트림 연산을 활용할 수 있다. peek는 스트림의 각 요소를 소비한 것처럼 동작을 실행한다. peek는 자신이 확인한 요소를 파이프라인의 다음 연산으로 그대로 전달한다.

public static void main(String[] args) {
    List<Integer> result = Stream.of(2, 3, 4, 5)
            .peek(x -> System.out.println("taking from stream: " + x)) //소스에서 처음 소비한 요소를 출력
            .map(x -> x + 17)
            .peek(x -> System.out.println("after map: " + x)) //map 동작 실행 결과를 출력 
            .filter(x -> x % 2 == 0)
            .peek(x -> System.out.println("after filter: " + x)) // filter 동작 후 선택된 숫자를 출력
            .limit(3)
            .peek(x -> System.out.println("after limit: " + x)) // limit 동작 후 선택된 숫자를 출력
            .collect(toList());
}
728x90
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함