티스토리 뷰

728x90
반응형

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


💡 코드 가독성 개선

  • 코드 가독성이란, 추상적인 표현이므로 이를 정확하게 정의하긴 어렵습니다. 일반적으로 코드 가독성이 좋다는 것은 어떤 코드를 다른 사람이 봐도 쉽게 이해할 수 있음을 의미합니다. 
  • 즉 코드 가독성을 개선한다는 것은 우리가 구현한 코드를 다른 사람이 쉽게 이해하고 유지보수할 수 있도록 만드는 것을 의미합니다.
  • 코드 가독성를 높이기 위해서는 코드의 문서화와 표준 코딩 규칙을 준수해야 합니다.

 

💡 익명 클래스를 람다로 리펙터링하기

  • 함수형 인터페이스를 구현한 익명 클래스는 람다 표현식으로 리펙터링할 수 있습니다.
  • 그렇다고 모든 익명 클래스를 람다 표현식으로 변환할 수 있는것은 아닙니다. 

 

익명 클래스를 람다 표현식으로 변환할 수 없는 경우 1)

  • 익명 클래스에서 사용한 this와 super는 람다 표현식에서 다른 의미를 가집니다. 익명 클래스에서 this는 자기자신을 가리키지만 람다에서 this는 람다를 감싸는 외부 클래스를 가리킵니다.
  • 여기서 this란 인스턴스 메서드 또는 생성자와 함께 현재 클래스의 객체에 대한 참조로 사용되는 자바의 키워드입니다.
  • 아래 예제에서의 익명 클래스의 this는 익명 클래스의 인스턴스를 지칭합니다. 하지만 람다 표현식에서 this를 사용하면 컴파일 에러가 발생하는 것을 알 수 있습니다. 이렇게 되면 람다에서 this는 사용하지 못하는 걸까요? 그렇지는 않습니다.
// 익명 클래스에서의 this
public static void main(String[] args) {
    Main main = new Main();
    main.process(10, new Consumer<Integer>() {
        @Override
        public void accept(Integer integer) {
            System.out.println(this.getClass().getName()); // modern.java.inaction.chapter09.Main$1 출력
            System.out.println(this); // anonymous class
        }

        @Override
        public String toString() {
            return "anonymous class";
        }
    });
    
    // 람다를 사용한 this는 컴파일 에러 발생
    main.process(10, i -> System.out.println(this)); // 'this' cannot be referenced from a static context
}

public void process(int i, Consumer<Integer> consumer) {
    consumer.accept(i);
}

 

but, 람다 또한 this를 사용할 수 있습니다.

  • 위의 예제의 람다 표현식에서 this 사용시 컴파일 에러가 발생한 이유는 this를 사용한 공간이 static 메서드이기 때문입니다. 
    람다 표현식은 람다식을 정의한 클래스의 인스턴스를 참조합니다. 하지만 위의 예제는 static 메서드 내부에 this가 있고, 인스턴스가 없기 때문에 문제가 발생하빈다.
  • https://stackoverflow.com/questions/24202236/lambda-this-reference-in-java >> 참고 자료
public class Main {
    public static void main(String[] args) {
        Main main = new Main();
        main.execute();
    }

    public void execute() {
        process(10, i -> {
            System.out.println(this.getClass().getName()); // modern.java.inaction.chapter09.Main
            System.out.println(this); // Main Class
        });
    }
    
    public void process(int i, Consumer<Integer> consumer) {
        consumer.accept(i);
    }

    @Override
    public String toString() {
        return "Main Class";
    }
}

 

익명 클래스를 람다 표현식으로 변환할 수 없는 경우 2)

  • 익명 클래스는 외부 클래스의 변수를 가리킬 수 있습니다. 람다 또한 외부 클래스의 변수를 가리킬 수 있지만 같은 변수명으로 새로운 값을 할당할 수는 없습니다.
public static void main(String[] args) {
        
    int a = 10; // 변수 선언

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

    Runnable runnable2 = () -> {
        int a = 200; // 컴파일 에러 발생
        System.out.println(a);
    };
}

 

익명 클래스를 람다 표현식으로 변환할 수 없는 경우 3)

  • 익명 클래스를 람다 표현식으로 변경하면 컨텍스트 오버로딩에 따른 모호함이 초래될 수 있습니다. 익명 클래스는 인스턴스화 할 때 명시적으로 구체화될 형식이 정해지는 반면 람다 표현식은 콘텍스트에 따라 달라지기 때문입니다.
  • 익명 클래스로 구현시 인스턴스화 하는 동시에 지정할 수 있지만 람다를 사용하는 경우 컴파일러가 어떤 것을 사용해야 할지 모르기 때문에 에러가 발생합니다. 그렇기 때문에 명시적으로 선언을 해주어야 합니다.
@FunctionalInterface
public interface Task {

    void execute();
}

@FunctionalInterface
public interface Runnable {
   
    public abstract void run();
}

public class Main {

    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                // something ...
            }
        };
        
        Task task = new Task() {
            @Override
            public void execute() {
                // something ...
            }
        };

//        doSomething(() -> System.out.println("Hello?")); // 컴파일 에러 발생(Task, Runnable 중 어떤 것을 사용해야할지 컴파일러가 모름)
        doSomething((Task) () -> System.out.println("Hello?"));
    }

    public static void doSomething(Runnable runnable) {
        runnable.run();
    }

    public static void doSomething(Task execute) {
        execute.execute();
    }
}

 

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

  • 람다 표현식은 충분히 전달할 수 있는 짧은 코드입니다. 하지만 메서드 참조를 사용하면 가독성을 높일 수 있고 코드의 의도를 명확이 알릴 수 있습니다.
public class Main {
    public static void main(String[] args) {

        // 메서드 참조를 사용하지 않은 예제
        Map<String, List<Dish>> 칼로리별_음식_종류 = MENU.stream()
                .collect(Collectors.groupingBy(dish -> {
                    if (dish.getCalorie() <= 400) return "DIET";
                    else if (dish.getCalorie() <= 700) return "NOMAL";
                    else return "FAT";
                }));

        // 메서드 참조를 사용한 예제
        Map<String, List<Dish>> 칼로리별_음식_종류 = MENU.stream()
                .collect(Collectors.groupingBy(Main::caloriesType));
                
        List<Integer> integerList = new ArrayList<>(List.of(3, 1, 5, 4, 2));
        integerList.sort((o1, o2) -> Integer.compare(o1, o2));
        integerList.sort(Integer::compare);
    }

    public static String caloriesType(Dish dish) {
        if (dish.getCalorie() <= 400) return "DIET";
        else if (dish.getCalorie() <= 700) return "NOMAL";
        else return "FAT";
    }
}

 

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


💡 전략 패턴

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

https://rok93.tistory.com에서 이미지 참고

@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 v;

    public Validator(ValidationStrategy v) {
        this.v = v;
    }

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

public class Main {
    public static void main(String[] args) {

        // 람다를 적용하지 않은 코드
        Validator numberValidator = new Validator(new IsNumeric());
        Validator lowerCaseValidator = new Validator(new ISAllLowerCase());
        System.out.println(numberValidator.validate("AA")); // false
        System.out.println(lowerCaseValidator.validate("aa")); // true

        // 람다를 적용한 코드
        Validator numberValidator = new Validator((String s) -> s.matches("\\d+"));
        Validator lowerCaseValidator = new Validator((String s) -> s.matches("[a-z]+"));
        System.out.println(numberValidator.validate("AA"));
        System.out.println(lowerCaseValidator.validate("aa"));
        
    }
}

 

💡 옵저버 패턴

  • 어떤 이벤트가 발생했을 때 한 객체(주체)가 다른 객체 리스트(옵저버)에 자동으로 알림을 보내야하는 상황에서 옵저버 디자인 패턴을 사용할 수 있습니다.
  • 예를들어 주식의 가격 변동에 반응하는 다수의 거래자 예제에서 옵저버 패턴을 사용할 수 있습니다.

https://rok93.tistory.com에서 이미지 참고

@FunctionalInterface
public interface Observer {

    void notify(String tweet);
}

public class NYTimes implements Observer {

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

public class LeMonde implements Observer {

    @Override
    public void notify(String tweet) {
        if (tweet != null && tweet.contains("wine")) {
            System.out.println("Today cheese, wine and news! " + tweet);
        }
    }
}

public interface Subject {

    void registerObserver(Observer observer);
    void notifyObservers(String tweet);
}

public class Feed implements Subject {

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

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

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

public class Main {

    public static void main(String[] args) {
        Feed feed = new Feed();

        // 람다 표현식을 사용하지 않은 예제
        feed.registerObserver(new NYTimes());
        feed.registerObserver(new Guardian());
        feed.notifyObservers("반갑습니다.");

        // 람다 표현식을 사용한 예제
        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("wine")) {
                System.out.println("Today cheese, wine and news! " + tweet);
            }
        });

        feed.notifyObservers("hello");
    }
}

 

디버깅


💡 스텍 트레이스 확인

  • 아래 코드는 NPE가 발생하지만 정확히 어디서 문제가 발생했는지 알려주지 않습니다. 이와 같은 상황은 람다 내부에서 에러가 발생했음을 가리키고 있습니다. 람다는 이름이 없으므로 컴파일러가 람다를 참조하는 이름을 만들어내게 됩니다.
List<Point> pointList = List.of(new Point(1, 10), null);
pointList.stream().map(Point::getX).forEach(System.out::println);

 

  • 메서드 참조를 사용해도 스택 트레이스에 메서드명이 나타나지 않지만, 메서드 참조를 사용하는 클래스와 같은 곳에 선언되어 있으면 참조 이름이 스택 트레이스에 나타납니다. (아래는 같은 클래스 내부에 있기 때문에 출력됨)
public static void main(String[] args) {
    List<Integer> integerList = List.of(1, 2, 3);
    integerList.stream()
            .map(Debugging::byZero) // 메서드 참조 사용
            .forEach(System.out::println);
}

public static int byZero(int n) {
    return n / 0;
}

 

💡 정보 로깅

  • peek는 스트림의 각 요소를 소비한 것처럼 동작합니다. 하지만 forEach처럼 실제로 스트림의 요소를 소비하지 않습니다.
  • peek는 자신이 확인한 요소를 파이프라인의 다음 연산으로 그대로 전달합니다.
  • 스트림 파이프라인에 적용된 filter, map등이 어떤 결과를 도출하는지 확인하는 경우에 좋습니다.
List<Integer> integerList = List.of(1, 2, 3);
integerList.stream()
        .peek(x -> System.out.println("x : " + x))
        .map(x -> x + 10)
        .forEach(System.out::println);

 

 

 

 

 

 

 

 

 

728x90
반응형