본문 바로가기

Java

Java 람다(Lambda) 표현식, Functional Interface 에 대한 조언

https://www.baeldung.com/java-8-lambda-expressions-tips

 

Lambda Expressions and Functional Interfaces: Tips and Best Practices | Baeldung

Tips and best practices on using Java 8 lambdas and functional interfaces.

www.baeldung.com

 

1. 표준 함수형 인터페이스(Functional Interfaces) 위주로 사용하라

함수형 인터페이스는 java.util.function 패키지에 모여 있고, 거의 대부분의 람다 표현식이나 페소드 참조에 사용할 수 있다.

대표적인 함수형 인터페이스로는 아래와 같은 타입들이 있다.

  1. 파라미터 없이 어떤 타입을 반환하는 Supplier
  2. 리턴 타입 없이 파라미터를 받아 처리하는 Consumer
  3. 파라미터를 받아 테스트 결과를 true / false 로 반환하는 Predicates
  4. 파라미터를 받아 결과를 반환하는 Function

아래 예제에서 보는 바 처럼 필요한 인터페이스를 직접 정의(Foo) 해서 사용 할 수도 있지만, 대부분의 함수형 인터페이스 들은 이미 정의 되어 있으니 이것들을 이용하도록 하자

public class StandardFunctionalInterfaceDemo {

    @FunctionalInterface
    public interface Foo {
        String method(String string);
    }

    public String addViaCustomInterface(String string, Foo foo) {
        return foo.method(string);
    }

    @Test
    @DisplayName("함수형 인터페이스 Foo를 정의해서 구현하는 방법")
    public void implementCustomFunctionalInterface() {
        StandardFunctionalInterfaceDemo demo = new StandardFunctionalInterfaceDemo();
        Foo foo = parameter -> parameter + " from Custom Functional Interface";
        String result = demo.addViaCustomInterface("Message", foo);
        assertEquals("Message from Custom Functional Interface", result);
    }


    public String addViaStandardInterface(String string, Function<String, String> fn) {
        return fn.apply(string);
    }

    @Test
    @DisplayName("표준 함수형 인터페이스인 Function을 이용해서 구현한 방법")
    public void implementsStandardFunctionalInterface() {
        StandardFunctionalInterfaceDemo demo = new StandardFunctionalInterfaceDemo();
        Function<String, String> fn = parameter -> parameter + " from Standard Functional Interface";
        String result = demo.addViaStandardInterface("Message", fn);
        assertEquals("Message from Standard Functional Interface", result);
    }

}

 

 

2. @FunctionalInterface 어노테이션을 사용하라

위 예제에서 보면 Foo 인터페이스를 정의할때 @FunctionalInterfce를 사용한 것을 볼수 있다. 이 어노테이션이 없어도 Foo 인터페이스는 함수형 인터페이스로 사용이 가능하다. 하지만, 프로젝트가 커지고 담당자가 교체되는 일들을 겪다보면, 이 인터페이스를 함수형으로 사용하기위해 정의 했는지, 그냥 일반 인터페이스로 사용 가능한지 표시할 필요성을 느끼게 될 것이다.

이 경우 사용하는 어노 테이션으로 이 어노테이션이 작성된 인터페이스는 함수형 인터페이스로서의 구조가 깨지면 컴파일 에러가 발생한다.

 

3. 함수형 인터페이스에서 Default 메소드를 과용하지 마라

함수형 인터페이스에는 단일 추상 메소드만 있으면 된다. 따라서 default 메소드가 추가되어도 동작에는 문제가 없다.

함수형 인터페이스는 다른 함수형 인터페이스를 상속 할 수 있다.

다중 상속 을 받는 경우 함수형 인터페이스를 유지 하기 위해서는 양쪽 부모의 추상 메소드 이름이 서로 같아야 하며,

    @FunctionalInterface
    public interface Baz {
        String method(String string);
        default String defaultBaz() {
            return "defaultBaz";
        }
    }

    @FunctionalInterface
    public interface Bar {
        String method(String string);
        default String defaultBar() {
            return "defaultBar";
        }
    }

    @FunctionalInterface
    public interface Foo extends Bar, Baz {
//        Baz 와 Bar의 추상 메소드가 모두 "method"라는 같은 이름을 사용하기 때문에 오류가 발생하지 않는다.
    }

 default 메소드가 중복되는 경우 (양쪽 부모 인터페이스에 같은이름으로 정의 되어 있는경우) 에러가 발생한다. (이건 함수형 인터페이스가 아니어도 에러가 난다.)

이를 해결하기 위해서는 오류가 나는 메소드를 재정의 하면 된다.

    @FunctionalInterface
    public interface Baz2 {
        String method(String string);
        default String commonMethod() {
            return "common method of Baz2";
        }
    }

    @FunctionalInterface
    public interface Bar2 {
        String method(String string);
        default String commonMethod() {
            return "common method of Baz2";
        }
    }

    @FunctionalInterface
    public interface Foo2 extends Bar2, Baz2 {
//        Baz2 와 Bar2에 commonMethod 메소드가 개별로 정의 되어서 아래와 같은 오류가 발생한다.
//        Foo2 inherits unrelated defaults for commonMethod() from types Bar2 and Baz2
//        따라서 아래 처럼 문제가 되는 commonMethod를 재 정의 해 주는 것으로 해결 할 수 있다.
        default String commonMethod() {
            return "common method of Foo2";
        }
    }

무었보다 default 메소드를 많이 사용하는 것은 그렇게 좋은 구조가 아닐 수 있다. 그런 경우는 설계를 다시 해 보는것이 좋겠다.

 

4. 람다 식을 이용해서 함수형 인터페이스를 구현하라

Foo fooByIC = new Foo() {
    @Override
    public String method(String string) {
        return string + " from Foo";
    }
};

// 위와 같은 코드가 아래 처럼 간결해 진다.

Foo foo = parameter -> parameter + " from Foo";

과거부터 사용되던 Runnable, Comparator 와 같은 라이브러리 등에도 적용이 가능하다.

 

5. 함수형 인터페이스를 파라미터로 받는 메소드는 오버로딩을 피해라

아래 예제처럼 같은 함수명에 파라미터가 서로 다른경우 인자를 람다 표현식으로 넘기면 정확히 어떤타입인지 판단하기 애매해져 에러가 발생한다. (Ambiguous method call)

    public class Process {
        public String process(Callable<String> c) throws Exception {
            return c.call();
        }

        public String process(Supplier<String> s) {
            return s.get();
        }
    }

    @Test
    void testOverloading() {
        Process process = new Process();
//        아래와 같은 에러가 발생한다.
//        Ambiguous method call. Both process(Callable<String>) in Process and process (Supplier<String>) in Process match
        String result = process.process(() -> "abc");
        assertEquals("abc", result);
    }

이런 문제를 방지 하기 위해 메소드명을 분리해서 선언하는 것 이 좋다

    public class Process2 {
        public String processWithCallable(Callable<String> c) throws Exception {
            return c.call();
        }

        public String processWithSupplier(Supplier<String> s) {
            return s.get();
        }
    }

    @Test
    void testOverloading2() {
        Process2 process = new Process2();
        String result = process.processWithSupplier(() -> "abc");
        assertEquals("abc", result);
    }

 

6. 람다 표현식은 내부(Inner) 클래스와는 다르다

람다와 내부 클래스는 범위(scope)의 개념이 다르다. 내부 클래스를 생성하면 새로운 scope이 생성되기 때문에, 내부 클래스 내부에서 외부에 있는 변수와 동일한 이름의 변수를 정의하면 외부에서는 해당 변수로 접근 할 수 없게 된다. 내부 클래스 안에서 this를 사용하면 this는 내부클래스를 가르킨다.

람다의 경우에는 원래 scope에 포함되어 동작한다. 그렇기 때문에 람다 내부의 변수를 숨길 수가 없고, 람다 내부에서의 this는 람다 외부 클래스를 가르킨다.

 

7. 람다 표현식은 간결하고 명확하게 작성하라

람다는 설명문이 아니고 표현식이어야 한다. 따라서 어떤 기능을 하는지 명확하게 코드 블럭이 아닌 한 줄로 표현을 해야한다. 

7.1 람다의 본문은 코드 블록을 피하자

람다는 코드 블록이 아닌 한줄로 작성되도록 하는것이 좋다. 아래 예제를 보자

Foo foo = parameter -> { String result = "Something " + parameter; 
    //many lines of code 
    return result; 
};

// 위와 같은 식으로 작성되는 코드가 있다면,
// 아래 처럼 코드를 분리해서 람다 표현식을 간결하게 유지 할 수 있다.

Foo foo = parameter -> buildString(parameter);

private String buildString(String parameter) {
    String result = "Something " + parameter;
    //many lines of code
    return result;
}

모든 규칙이 마찬가지 이겠지만 언제나 항상 무조건 지켜야 하는 규칙은 아니다. 경우에 따라서 두세줄의 람다 코드가 훨씬 더 간결한 경우도 있다.

7.2 파라미터 타입은 생략하자

(String a, String b) -> a.toLowerCase() + b.toLowerCase();

// 위 코드를 아래와 같이 축약 할 수 있다.

(a, b) -> a.toLowerCase() + b.toLowerCase();

7.3 파라미터가 1개인 경우 괄호는 생략하자

(a) -> a.toLowerCase();

// 위 코드 대신, 아래처럼 파라미터의 괄호를 생략 할 수 있다.

a -> a.toLowerCase();

7.4 Return 문과 중괄호는 생략하자

a -> {return a.toLowerCase()};
// 대신 아래처럼...
a -> a.toLowerCase();

7.5 메소드 참조를 활용하자

간혹 람다 표현식이 이미 구현되어 있는 함수 하나를 호출하는 경우가 있다 이런경우 메소드 참조라는 아주 유용한 기능이 있으니 이를 활용 하도록 하자

a -> a.toLowerCase();
// 위와 같은 코드가 있다면 아래 메소드 참조 형태로 고쳐쓸 수 있다.
String::toLowerCase;

 

8. Effectively Final 변수를 활용해라

Effectively Final 변수란, final 키워드가 붙지는 않았지만, 값이 변경되지 않아 실제로 final 변수처럼 동작하는 변수를 말한다.

람다 코드 안에서 외부의 final 변수가 아닌 변수에 접근하려고 하면 compile 에러가 발생한다. 그렇다고 해서 꼭 모든 변수를 final 처리를 해야 한다는건 아니다. 위에서 말한 effectively final 변수를 이용하면 된다.

public class EffectivelyFinalDemo {
    public void method() {
        String localVariable = "Local";

        Consumer<String> foo = parameter -> {
//            에러 발생 - Variable used in lambda expression should be final or effectively final
            localVariable = parameter;
            return localVariable;
        };
    }
}

 

9. 객체 변수를 변경으로 부터 보호하라

람다를 이용하는 주된 이유중 하나는 병렬 컴퓨팅 이다. 따라서 람다는 쓰레드 세이프 하게 작성 되어야 한다. 위에서 살펴본 대로 effectively final 개념은 람다가 쓰레드 세이프하는데 도움이 된다. 하지만 변수가 객체(Object) 인 경우에는 예외 상황이 발생하기도 한다.

객체 자체가 변경되는것을 막을 수는 있지만 아래 예제 처럼 객체의 값이 변경되는 것은 막지 못하기 때문이다. 따라서 코드를 작성할때 항상 예상치 못한 변경을 유념 해야한다.

class ParameterType {
    private String value;

    public void append(String str) {
        this.value += str;
    }
}

public void method2() {
    ParameterType effectivelyFinal = new ParameterType();
    Consumer<ParameterType> foo = parameter -> {
    	// 변수의 값이 아닌 객체 변수의 값은 변경이 가능하다.
        parameter.append(" from lambda");
    };
}

 

 

반응형