반응형

1. 서론

이번 포스팅에서는 Chapter9의 리팩터링, 테스팅, 디버깅 에 대해 진행하도록 하겠습니다.

 

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

 

람다를 사용한 코드는 동작 파라미터화를 통해 다양한 요구사항에 대응할 수 있습니다.

 

1) 코드 가독성 개선

 

코드 가독성이란 "어떤 코드를 다른 사람도 쉽게 이해할 수 있음" 을 의미합니다.

 

람다를 사용하게되면 이 코드 가독성을 높일 수 있습니다.

 

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

 

아래는 익명클래스를 람도로 리팩터링한 예제입니다.

 

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

Runnable r2 = () -> System.out.println("Hello");

 

한눈에 봐도 코드가 간결해지면서 가독성이 올라간것을 볼 수 있습니다.

 

하지만 모든 익명 클래스를 람다로 바꿀수 있는것은 아닙니다.

 

1. 익명 클래스에서 사용한 this, super는 람다와 다릅니다.

 

익명클래스의 this는 익명클래스 자신을 가리키는 반면,

람다는 this의 경우 람다를 감사는 클래스를 가리킵니다.

 

2. 익명 클래스는 감싸고 있는 클래스의 변수를 가릴수 있지만 람다는 가릴수 없습니다.

 

아래는 예제입니다.

 

int a = 10;

Runnable r1 = new Runnable() {
    @Override
    public void run() {
        int a =2; // 정상 동작
        System.out.println(a)
    }
};

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

 

3. 익명클래스는 람다 표현식으로 바꿀시 컨텍스트 오버로딩에 따른 모호함이 발생합니다.

 

이런 경우, 명시적 형변환을 통해 모호함을 제거할 수 있습니다.

 

아래는 예제입니다.

 

interface Task {
    void execute();
}

public static void doSomething(Task a) {a.execute();};
public static void doSomething(Runnable r) {r.run();};

public static void main(String[] args) {
    doSomething((Task)() -> System.out.println("Danger Danger!!"));
}

 

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

 

람다에서 메서드 참조를 통해 더욱 가독성을 높일 수 있습니다.

 

아래는 예제입니다.

 

enum CaloricLevel {
    DIET, NORMAL, FAT
}

@Getter
@RequiredArgsConstructor
private static class Dish {
    private final String name;
    private final boolean vegetarian;
    private final int calories;
    private final Type type;
    enum Type {
        MEAT, FISH, OTHER
    }
    
    public CaloricLevel getCaloricLevel() {
        if (this.getCalories() <= 400) return CaloricLevel.DIET;
        else if (this.getCalories() <= 700) return CaloricLevel.NORMAL;
        else return CaloricLevel.FAT;
    }
}

public static void main(String[] args) {
    List<Dish> menu = Arrays.asList(
            new Dish("pork", false, 800, Dish.Type.MEAT),
            new Dish("beef", false, 700, Dish.Type.MEAT),
            new Dish("chicken", false, 400, Dish.Type.MEAT),
            new Dish("french fries", true, 530, Dish.Type.OTHER),
            new Dish("rice", true, 350, Dish.Type.OTHER),
            new Dish("season fruit", true, 120, Dish.Type.OTHER),
            new Dish("pizza", true, 550, Dish.Type.OTHER),
            new Dish("prawns", false, 300, Dish.Type.FISH),
            new Dish("salmon", false, 450, Dish.Type.FISH)
            ;
    Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = 
                    menu.stream().collect(groupingBy(Dish::getCaloricLevel));
}

 

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

 

반복자를 통한 명령형 처리를 스트림으로 리팩터링할 시 더욱 가독성이 올라갑니다.

 

아래는 예제입니다.

 

List<String> dishNames = new ArrayList<>();
for (Dish dish : menu) {
    if(dish.getCalories() > 300) {
        dishNames.add(dish.getName());
    }
}

menu.parallelStream()
        .filter(d -> d.getCalories() > 300)
        .map(Dish::getName)
        .collect(Collectors.toList());

 

 

5) 코드 유연성 개선

 

람다 표현식으로 동작 파라미터화를 쉽게 가능한것을 앞절에서 살펴봤습니다.

 

이것은 결국 코드가 유연하게 돌아간다는 것을 의미하게 됩니다.

 

대표적으로 앞에서 살펴본 실행 어라운드가 있습니다.

 

 

 

 

 

 

 

반응형

 

 

 

 

 

 

 

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

기존 자바의 유지보수와 더욱 깔끔한 코드를 생성하기 위해 디자인 패턴을 사용했었습니다.

 

람다를 이용하면 이 디자인 패턴으로 해결하던 문제를 더욱 쉽고 간단하게 해결할 수 있습니다.

 

1) 전략

 

전략(strategy) 패턴은 런타임에 적절한 알고리즘을 선택하는 기법입니다.

 

아래는 기존 디자인 패턴으로 전략패턴을 구현한 예제입니다.

 

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 v) {
        this.validationStrategy = v;
    }
    public boolean validate(String s) {
        return validationStrategy.execute(s);
    }
}

public static void main(String[] args) {
    Validator numericValidator = new Validator(new IsNumeric());
    boolean b1 = numericValidator.validate("aaaa");
    Validator lowerValidator = new Validator(new IsAllLowerCase());
    boolean b2 = lowerValidator.validate("bbbb");
}

 

위에는 구현체를 모두 정의하여 사용해야합니다.

 

하지만 람다를 사용하게 되면 아래와 같이 리팩터링이 가능합니다.

 

public interface ValidationStrategy {
    boolean execute(String s);
}

public class Validator {
    private final ValidationStrategy validationStrategy;
    public Validator(ValidationStrategy v) {
        this.validationStrategy = v;
    }

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

public static void main(String[] args) {
    Validator numericValidator = new Validator( s -> s.matches("[a-z]+"));
    boolean b1 = numericValidator.validate("aaaa");

    Validator lowerValidator = new Validator(s -> s.matches("\\d+"));
    boolean b2 = lowerValidator.validate("bbbb");
}

 

2) 템플릿 메서드

 

템플릿 메서드 패턴은 알고리즘의 일부만을 고쳐야하는 상황에 필요합니다.

 

아래는 디자인 패턴으로 템플릿 메서드 패턴을 구현한 예제입니다.

 

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

 

위 같은 경우에는 OnlineBanking의 추상클래스를 상속받은 하위 클래스가 필요합니다.

 

하지만 람다를 사용한다면 굳이 추상, 하위 클래스 구조를 만들지 않아도 됩니다.

 

예제는 아래와 같습니다.

 

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

 

3) 의무 체인

 

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

 

아래는 디자인 패턴을 통해 의무체인 패턴을 구현한 코드입니다.

 

@Setter
public abstract class ProcessingObject<T> {
    protected ProcessingObject<T> successor;

    public T handle(T input) {
        T r = handleWork(input);
        if (successor != null) {
            return successor.handle(input);
        }
        return r;
    }
    abstract protected T handleWork(T input);
}
    
public class HeaderTextProcessing extends ProcessingObject<String> {

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

public class SpellCheckProcessing extends ProcessingObject<String> {

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

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

 

위와 같은 작업 처리는 Function의 andThen 메서드를 통해 간편하게 구현이 가능해졌습니다.

 

코드는 아래와 같습니다.

 

UnaryOperator<String> headerTextProcessing = s -> "From Raoul, Mario and Alan: " + s;
UnaryOperator<String> spellCheckProcessing = s -> s.replaceAll("labda", "lambda")

Function<String, String> pipeline = headerTextProcessing.andThen(spellCheckProcessing);
String result = pipeline.apply("Aren't labdas really sexy?!!");

 

 

4. 람다 테스팅

이번엔 람다를 테스팅하는 방법에 대해 살펴보겠습니다.

 

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

 

람다는 결국 익명함수로 테스트 코드 작성 시 호출 할 수 없습니다.

 

이런 경우, 람다를 필드에 저장하여 재사용할 수 있도록 하여 람다의 로직 테스트를 수행할 수 있습니다.

 

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

 

람다의 목표는 정해진 동작을 다른 메서드에서 사용할 수 있도록 캡슐화하는 것입니다.

 

그렇기 때문에, 테스트 시 람다의 세부구현이 아닌 인풋과 람다를 통한 아웃풋으로 테스트를 진행하여 검증하면 됩니다.

 

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

 

만약 람다 표현식이 복잡하여 테스트하기가 힘든경우에는,

위에서 살펴본곳처럼 메서드 참조로 수정하여 일반 메서드를 테스트하듯이 람다표현식을 테스트할 수 있습니다.

 

5. 디버깅

디버깅 시 개발자는 아래 두가지를 먼저 확인해야 합니다.

 

  • 스택 트레이스
  • 로깅

하지만, 람다 표현식와 스트림은 디버깅하기가 좀 까다롭습니다.

 

1) 스택 트레이스 확인

 

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

 

스택트레이스는 이 스택프레임에서 정보를 가져와 보여주게됩니다.

 

하지만 람다의 경우 스택 트레이스가 기존과는 다르게 나오게 되며, 메서드 이름이 나오지 않습니다.

그 이유는, 람다 표현식은 이름이 없어 컴파일러가 이름을 자동을 생성하게 되면서 생겨나게 되기 때문입니다.

 

하지만 클래스와 같은 곳에 선언되어 있는 메서드 참조시에는 메서드 이름이 스택트레이스에 포함하게 됩니다.

 

안타깝게도 이것은 아직 fix되지 않은 점으로, 개발 시 스택트레이스의 확인에 불편함이 있을 수 있습니다.

 

2) 정보 로깅

 

스트림에서는 마지막 연산이 호출되는 순간 전체 스트림이 소비되어 로깅하기에 매우 까다롭습니다.

 

하지만, 이를 위해 peek 메서드를 제공하고 있습니다.

 

peek 메서드의 경우 실제로 스트림 요소를 소비하지 않고, 자신이 확인한 요소를 파이프라인의 다음 연산으로 그대로 전달하게 됩니다.

 

아래는 예제입니다.

 

List<Integer> numbers = List.of(1,2,3,4,5);
numbers.stream()
        .peek(System.out::println)
        .map(x -> x + 17)
        .collect(Collectors.toList());

 

6. 마무리

 

이번 포스팅에서는 Chapter9 리팩터링, 테스팅, 디버깅에 대해 진행하였습니다.

다음에는 Chapter10 람다를 이용한 도메인 전용 언어에 대해 포스팅하겠습니다.

반응형

+ Recent posts