프로그래밍/Java

모던 자바 인 액션 스터디 - 동작 파라미터, 람다 표현식

Jay22 2019. 11. 16. 14:26
반응형

 

2장

동작 파라미터화 코드 전달하기

어떤 상황에서든지 요구사항은 바뀔 수 있습니다. 이에 따라 자바에서는 동작 파라미터라는 것을 이용하여 자주 바뀌는 요구사항에 효과적으로 대응할 수 있습니다.

다음과 같이 동작을 추상화해서 변화하는 요구사항에 대응할 수 있는 코드를 구현하는 방법을 살펴볼까요.

public static void main(String[] args) {
    List<Apple> inventory = Arrays.asList(
            new Apple(80, FilteringApples.Color.GREEN),
            new Apple(155, FilteringApples.Color.GREEN),
            new Apple(120, FilteringApples.Color.RED));
    prettyPrintApple(inventory, new AppleWeightFormatter());
}

public interface AppleFormatter {
    void accept(Apple apple);
}

public static class AppleWeightFormatter implements AppleFormatter {
    @Override
    public void accept(Apple apple) {
        System.out.println(apple.getWeight() + "g 입니다.");
    }
}

public static void prettyPrintApple(List<Apple> inventory, AppleFormatter appleFormatter) {
    for (Apple apple : inventory) {
        appleFormatter.accept(apple);
    }
}

AppleFormatter라는 인터페이스를 정의를 하고 그 안에 하려는 동작(accept)를 정의합니다. 그렇게 되면 저 구현체만 갈아끼워주게 되면 원하는 방식으로 동작하게 될 것입니다.

하지만 이 방법도 조건이 많아지면 매번 클래스를 정의해야 하는 문제가 있습니다. 익명 클래스 라는 것을 사용해서 개선해보겠습니다.

익명 클래스

익명 클래스는 말 그대로 이름이 없는 클래스 입니다. 익명 클래스를 이용한다는 것은 무엇 일까요?

익명 클래스를 이용하면 클래스 선언과 동시에 초기화를 할 수 있습니다. 다시 말해 위의 예제처럼 필요한 부분을 따로 클래스로 만들어서 넣는게 아니라 즉석에서 필요한 부분을 구현하는 것입니다.

public static void main(String[] args) {
    List<Apple> inventory = Arrays.asList(
            new Apple(80, FilteringApples.Color.GREEN),
            new Apple(155, FilteringApples.Color.GREEN),
            new Apple(120, FilteringApples.Color.RED));
    prettyPrintApple(inventory, apple -> apple.getWeight() + "g 입니다");
}

public interface AppleFormatter {
    String accept(Apple apple);
}

public static void prettyPrintApple(List<Apple> inventory, AppleFormatter appleFormatter) {
    for (Apple apple : inventory) {
        System.out.println(appleFormatter.accept(apple));
    }
}

위의 예제와 다른점이 보이시나요?

prettyPrintApple 메서드에서 익명 클래스를 전달하여 따로 클래스를 만들지 않고 바로 실행하는 모습입니다. 훨씬 간단해졌죠.

3장. 람다 표현식

람다 표현식은 메서드로 전달하는 익명 함수를 단순화한 것입니다.

람다의 기본 문법은 다음과 같습니다.

표현식 스타일

(parameters) -> expression

블록 스타일

(parameters) -> {statements;}

() -> "Jay"
() -> {return "Jay";}

위의 예시는 다음과 같습니다. 첫 번째 것은 단순 표현식 (문자열) 이기 때문에 가능합니다. 두 번째는 표현식이기 때문에 중괄호로 감싸야 합니다.

함수형 인터페이스

함수형 인터페이스란 하나의 추상메서드를 지정하는 인터페이스를 말합니다. 1장에서 살펴본 Predicate 가 바로 그것입니다.

public interface Predicate<T> {
    boolean test(T t);
}

참고로 인터페이스 안에 디폴트 메서드가 존재하더라도 추상 메서드가 오직 한 개라면 함수형 인터페이스라고 할 수 있습니다.

함수형 인터페이스는 전체 표현식을 함수형 인터페이스의 인스턴스로 취급하여 다룰 수 있습니다.

Runnable 인터페이스를 살펴볼까요?

public static void main(String[] args) {
    Runnable r1 = new Runnable() {
        @Override
        public void run() {
            System.out.println("hello1");
        }
    };
    process(r1);

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

    process(() -> System.out.println("hello3"));
}

public static void process(Runnable r) {
    r.run();
}

r1은 익명클래스를 사용하였고 r2 는 람다를 사용하였습니다. 마지막은 인자 자체에 넣어버린 경우입니다.

다음과 같은 코드가 가능한지 살펴볼까요.

execute(() -> {});
public void execute(Runnable r) {
    r.run();
}

Runnable 인터페이스의 추상 메서드는 인자와 반환값이 없는 run 이라는 메서드 입니다. 그렇기 때문에 () →{} 이렇게 인자와 반환값이 없는 경우가 들어갈 수 있다는 것이죠.

실행 어라운드 패턴

실행 어라운드 패턴이란 로직 앞 뒤로 준비/마무리 로직이 들어가는 경우 입니다. 데이터베이스 작업 같은 경우 커넥션을 열고 로직을 수행하고 자원을 닫는데 이것이 실행 어라운드 패턴이라고 할 수 있습니다.

다음과 같은 코드에서 로직을 람다를 이용하여 개선해보겠습니다.

public String processFile() throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
        return br.readLine();
    }
}

이 코드는 동작이 정해져있습니다. br.readLine()을 호출하는 방식으로 고정이 되어 있는데 다른 방식이 필요할 경우 메서드를 하나 더 만들어야 합니다.

그렇게 때문에 변하는 '동작'을 파라미터화 해야 합니다. 메서드이름을 2로 진행해보겠습니다.

public static String processFile2(Process p) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
        return p.process(br);
    }
}

@FunctionalInterface
interface Process {
    String process(BufferedReader br) throws IOException;
}

FunctionalInterface 의 정의로 BufferedReader를 받아서 String 을 반환하는 인터페이스를 만들었습니다. 그렇기 때문에 여러 동작을 정의를 할 수가 있겠죠.

String result = processFile2((BufferedReader br) -> br.readLine());

이런식으로 한줄 읽기를 정의할 수 있습니다. 다른 동작을 정의를 하려면 파라미터를 다르게 주면 됩니다.

메서드 레퍼런스 방식으로 코드를 더 줄이면 다음과 같습니다.

String result = processFile2(BufferedReader::readLine);

전체 코드는 https://github.com/tech-book-study/modern-java-in-action/tree/master/src/main/java/com/techbookstudy/chap2 에 있습니다.

 

tech-book-study/modern-java-in-action

Study for Modern Java In Action 📕. Contribute to tech-book-study/modern-java-in-action development by creating an account on GitHub.

github.com

Consumer

Consumer 인터페이스는 T 객체를 받아서 void 를 반환하는 accept라는 추상메서드를 정의합니다. 인자를 받고 반환형이 없는 메서드인 것입니다.

public static void main(String[] args) {
    List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
    arrayForEach(integers, integer -> System.out.println("print ... " + integer));
}

public static <T> void arrayForEach(List<T> array, Consumer<T> consumer) {
    for (T t : array) {
        consumer.accept(t);
    }
}

Function

Function<T,R> 인터페이스는 T를 인수로 받아 R객체를 반환하는 메서드 apply를 정의합니다.

예를 들어, String 리스트를 인수로 받아 각 String 의 길이를 포함하는 Integer 리스트로 변환하는 map 메서드를 정의해보겠습니다.

List<String> strings = Arrays.asList("hello", "hi", "jay");
List<Integer> integerLists = map(strings, String::length);

public static <T, R> List<R> map(List<T> array, Function<T, R> function) {
    List<R> list = new ArrayList<>();
    for (T t : array) {
        R apply = function.apply(t);
        list.add(apply);
    }
    return list;
}

String::length는 s → s.length() 와 같습니다. String (T) 을 받아서 Integer (R) 로 반환시키는 Function 입니다.

형식 검사, 형식 추론

내용이 길어 간단히 축약을 해보자면 형식 추론은 코드를 좀 더 단순화 시키는 것입니다. 즉 자바 컴파일러는 람다 표현식이 사용된 컨텍스트를 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론합니다. 즉 대상형식을 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론합니다.

Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

상단의 코드는 형식으로 추론하지 않은 것이고 하단의 코드는 형식을 추론하여 a1, a2 만 썼습니다.

이런건 언제 써야할까요? 상황에 따라 명시적으로 형식을 포함하는 것이 좋을 때도 있고 배제하는 것이 가독성을 향상시킬 때도 있습니다. 규칙은 없으므로 가독성을 향상시킬 수 있는 방식으로 선택하면 되겠습니다.

반응형