티스토리 뷰

목차

    글 목록

    배경

    (지난 [Java] 함수형 프로그래밍(Functional Programming)과 람다식(Lambda Expression) (1/2) 글에 이어 작성한 내용입니다.)
    최근 회사에서 리팩토링 중에 다음과 같은 코드 중복을 함수형 인터페이스를 활용하여 개선한 적이 있다.

    public class ServiceA {
    	private final ServiceC serviceC;
        private final RepositoryA repositoryA;
        
    	public void methodA(){
        	List<ObjectA> objectAList = repositoryA.findAll();
            
            objectAList.forEach(objectA -> {
            	// objectAList의 원소별 처리 로직
                serviceC.call(objectA);
             })
    }
    
    public class ServiceB {
    	private final ServiceC serviceC;
        private final RepositoryB repositoryB;
    
       public void methodB(){
        	// ServiceA와 중복 발생
            List<ObjectB> objectBList = repositoryB.findAll();
            
            objectBList.forEach(objectB -> {
            	// objectBList 원소별 처리 로직
                serviceC.call(objectB);
             })
    }

     
    당시에는 내가 스스로 해결 방안이 떠오르지 않아 팀원에게 도움을 요청했었는데, java 8의 주요 특징으로 분명 함수형 프로그래밍(Functional Programming)과 함수형 인터페이스(Funtional Interface), 람다식(Lambda Expression)은 분명 그 개념을 알고있었음에도 이처럼 실제 상황에서 문제 해결 방안 중 하나로 떠오를 정도로 익숙하고 능숙하지는 않다는 생각이 들어 블로그에 글로 기록하며 다시 정리해보고자 이번 글을 작성하게 되었다. 또한 이번 사례처럼 내가 혼자서 해결할 수 없는 문제를 만났을때 팀원과 해결 방안을 고민하고 의견을 나누며 해결책을 찾았던 경험이 좋은 기억으로 남아있어 글로도 기록하게 되었다.

    내용

    지난 글에서는 java에서 제공하는 함수형 프로그래밍과 람다식에 대해서 작성하였다. 이번에는 지난 글에 이어 java에서 제공하는 주요 함수형 인터페이스(Function Interface)에 대해 정리하였다. java에서 제공하는 주요 함수형 인터페이스로는 Runnable, Function, Consumer, Supplier, Predicate, Operator 등이 있다.

    Runnable

    매개변수도 없고 반환형도 없는 함수형 인터페이스이다. 추상 메서드로 run() 메서드를 갖는다.

    @FunctionalInterface
    public interface Runnable {
        /**
         * When an object implementing interface {@code Runnable} is used
         * to create a thread, starting the thread causes the object's
         * {@code run} method to be called in that separately executing
         * thread.
         * <p>
         * The general contract of the method {@code run} is that it may
         * take any action whatsoever.
         *
         * @see     java.lang.Thread#run()
         */
        public abstract void run();
    }

     
    주로 java에서 Thread로 작업을 정의할때 이 Runnable 인터페이스를 구현하여 사용한다. 이 경우 아래와 같이 익명객체를 전달하는 방법이 많이 사용된다. (출처 : 이것이 자바다)

    Thread thread = new Thread(new Runnable() { // Runnable 익명 객체를 매개값으로 전달
    	@Override
    	public void run() {
    		// thread가 실행할 코드
    	}
    });

    Function

    매개변수와 반환 값이 있는 함수형 인터페이스이다. 함수(Function)의 기본적인 개념이 어떤 input 값 x에 대해 어떤 output f(x)를 만들어내는 블록이라고 생각했을때 이 x -> f(x) 개념을 그대로 가져오면 이해하기 쉬울 것 같다.

    https://ko.wikipedia.org/wiki/%ED%95%A8%EC%88%98

     
    추상 메서드로 apply(T t)를 갖고 있으며, 다음과 같이 정의되어 있다. 코드를 보면 알 수 있듯이 Function<T, R>에서 첫번째 제네릭 타입 매개변수 T가 apply() 메서드의 매개변수 타입에 대응하며 두번째 제네릭 타입 R이 반환형(return value)에 대응한다.

    @FunctionalInterface
    public interface Function<T, R> {
    
        /**
         * Applies this function to the given argument.
         *
         * @param t the function argument
         * @return the function result
         */
        R apply(T t);
    }

     
    예를 들어 다음과 같이 매개변수로 Function<String, String> function을 매개변수로 받는 functionTest()라는 함수가 있다고 가정해보면, 이 functionTest() 메서드는 String 타입으로 선언된 변수 a에 대해 어떤 처리를 할지 functionTest() 외부로부터 전달받는다. 이 때 외부로부터 전달받는 처리 방법은 매개변수로 String 타입의 변수를 받으며 처리 결과로 String을 반환해야 한다.

    public class FunctionalInterfaceTest {
    
        public static void main(String[] args) {
           String result = functionTest(lowerCase -> lowerCase.toUpperCase());
           System.out.println("result = " + result);
    
        }
    
        private static String functionTest(Function<String, String> function){
           String a = "a";
           return function.apply(a);
        }
    }

     
    예를 들어 functionTest()를 호출하는 다음 코드는 매개변수로 전달한 소문자 알파벳을 매칭되는 대문자로 변환한다.

    public static void main(String[] args) {
    	String result = functionTest(lowerCase -> lowerCase.toUpperCase());
    	System.out.println("result = " + result);
    }

     
    그 결과는 다음과 같다.

     
    두개의 매개변수를 받고 하나의 값을 반환하는 동작에 대해 추상화가 필요한 경우 BiFunction<T, U, R> 함수형 인터페이스를 활용할 수 있다. 첫번째 제네릭 타입 매개변수 T가 apply() 메서드의 첫번째 매개변수 타입에 대응하며 두번째 제네릭 타입 U가 두번째 매개변수 타입, 마지막 R이 반환형(return value)에 대응한다.

    @FunctionalInterface
    public interface BiFunction<T, U, R> {
    
        /**
         * Applies this function to the given arguments.
         *
         * @param t the first function argument
         * @param u the second function argument
         * @return the function result
         */
        R apply(T t, U u);
    }

     
    예를 들어 아래 코드에서 functionTest() 메서드는 BiFunction<Integer, Integer, String> biFunction을 매개변수로 받는데, 이 BiFunction은 두 개의 매개변수 x, y에 대해 추상화된 어떤 처리(apply())를 한 후 그 결과를 String으로 반환한다. 예시에서는 두 매개변수 x, y를 곱한 결과를 String으로 반환하는 작업을 functionTest() 메서드에 전달한다.

    public class FunctionalInterfaceTest {
    	public static void main(String[] args) {
    		String result = functionTest((x, y) -> String.valueOf(x * y));
    		System.out.println("result = " + result);
    
    	}
    
    	private static String functionTest(BiFunction<Integer, Integer, String> biFunction){
    		Integer x = 3;
    		Integer y = 5;
    		return biFunction.apply(x, y);
    	}
    }

     
    결과로 다음과 같이 15가 출력된다.

     
    이 외에도 double(primitive type)을 R로 매핑하는 DoubleFunction<R>, int를 R로 매핑하는 IntFunction<R>, int를 long로 매핑하는 IntToLongFunction 등 다양한 Function 인터페이스를 제공한다.
    Function 인터페이스에서 제공하는 자세한 종류는 출처인 함수형 인터페이스 표준 API 총정리를 참고하면 좋을 것 같다.

    Consumer

    매개변수만 있고 반환 값은 없다. 이름 그대로 매개변수를 소비(Consume)만 할 뿐 어떤 결과를 반환하지는 않는다는 의미로 이해하면 좀 더 이해가 쉬울 것 같다. Consumer는 다음과 같아 accept()라는 추상 메서드를 갖으며, 이 메서드는 매개변수로 제네릭 타입 T를 받고, 아무 값도 반환하지 않는다(void)

    @FunctionalInterface
    public interface Consumer<T> {
    
        /**
         * Performs this operation on the given argument.
         *
         * @param t the input argument
         */
        void accept(T t);
    }

     
    예를 들어 다음 코드에서 consumeTest() 메서드는 매개변수로 String 타입의 Consumer<String>을 받고, String으로 선언된 argument 변수에 대해 어떠한 처리(accept())를 수행한다.

    public class FunctionalInterfaceTest {
    	private static void consumeTest(Consumer<String> consumer){
    		String argument = "cosuming...";
    		consumer.accept(argument);
    	}
    }

     
    예제로 다음과 같이 cosumeTest() 메서드에 선언된 argument를 콘솔에 출력하기 위해서 다음과 같은 System.out.println()을 전달할 수 있다.

    public class FunctionalInterfaceTest {
    	public static void main(String[] args) {
    		consumeTest(t -> System.out.println("t = " + t));
    	}
    
    	private static void consumeTest(Consumer<String> consumer){
    		String argument = "cosuming...";
    		consumer.accept(argument);
    	}
    }

     
    그 결과는 다음과 같다.

     
    원시 타입의 매개변수(int, double, long 등)을 소비하는 경우 Consumer<Integer>와 같이  Wrapper Class를  사용하지 않고 IntConsumer, DoubleConsumer, LongConsumer를 사용할 수 있다. 이처럼 꼭 Wrapper Class를 사용해야하는 경우가 아니라면 java에서 Primitive 타입에 대해 제공하는 컨슈머를 활용하는 편이 좋을 것 같다.
     
    또한, Function 인터페이스와 같이 두 개의 변수를 consume하기 위해 BiConsumer를 활용할 수 있으며 그 구현은 다음과 같다.

    @FunctionalInterface
    public interface BiConsumer<T, U> {
    
        /**
         * Performs this operation on the given arguments.
         *
         * @param t the first input argument
         * @param u the second input argument
         */
        void accept(T t, U u);
    }

     
    BiConusmer의 사용 예시로 consumeTest() 메서드에 매개변수로 두 개의 매개변수에 대한 데이터 처리를 전달(v1과 v2를 합쳐서 ((+) 연산) 콘솔에 출력한다) 하고

    public class FunctionalInterfaceTest {
    	public static void main(String[] args) {
    		// 두 개의 매개변수(v1, v2)를 처리하는 람다식을 전달
    		consumeTest((v1, v2) -> System.out.println(v1 + v2));
    	}
    
    	private static void consumeTest(BiConsumer<String, String> consumer){
    		String argument1 = "cosum";
    		String argument2 = "ing";
    		
    		// String으로 선언된 두 개의 매개변수를 Consume 한다.
    		consumer.accept(argument1, argument2);
    	}
    }

     
     그 결과로 consum과 ing가 합쳐진 문자열이 콘솔에 출력된 것을 볼 수 있다. 
     

     
    BiConsumer로 데이터 처리를 추상화하여 전달할때, consumer 대상인 두 매개변수의 타입 중 하나가 int인 경우 다음과 같이 ObjIntConsumer를 사용할 수 있다.

    public class FunctionalInterfaceTest {
    	public static void main(String[] args) {
    		// idx만큼 star를 출력하는 함수를 Consumer 함수형 인터페이스로 전달한다.
    		consumeTest((star, idx) -> {
    			for(int i = 0; i < idx; i++) {
    				System.out.println(i + ": " + star);
    			}
    		});
    	}
    
    	private static void consumeTest(ObjIntConsumer<String> consumer){
    		String str = "*";
    		int i = 10;
    
    		// String으로 선언된 매개변수(str)와 int 타입으로 선언된 매개변수(i)에 대한 데이터 처리(consume)
    		consumer.accept(str, i);
    	}
    }

     
    결과로 다음과 같이 *이 10번 출력되는 것을 볼 수 있다.
     

     
    이 외에도 객체 T와 double (pritmitive type)을 소비하는 ObjDoubleConsumer, 객체 T와 long (pritmitive type)을 소비하는 ObjLongConsumer 등을 제공한다.

    Supplier

    매개변수 없이 반환 값만 존재한다. 공급자 라는 Supplier의 사전적 의미를 생각했을때 무언가를 반환(공급)한다는 의미로 받아들이면 쉽게 이해할 수 있을 것 같다. 추상 메서드로는 get() 메서드를 갖는데, 매개변수 없이 제네릭 타입의 반환 값만을 갖는다.

    @FunctionalInterface
    public interface Supplier<T> {
    
        /**
         * Gets a result.
         *
         * @return a result
         */
        T get();
    }

     
    사용 예시로 다음과 같이 String 변수를 반환하는 Supplier 인터페이스를 받는 supplierTest() 메서드가 있다고 가정해본다.

    private static String supplierTest(Supplier<String> supplier){
    	return supplier.get();
    }

     
    이 supplierTest() 메서드에 매개변수로 "call supplier get" 이라는 문자열을 반환하는 함수를 람다식으로 전달하면 다음과 같다.

    public class FunctionalInterfaceTest {
    	public static void main(String[] args) {
    		String result = supplierTest(() -> {
    			return "call supplier get";
    		});
    		System.out.println("result = " + result);
    	}
    
    	private static String supplierTest(Supplier<String> supplier){
    		return supplier.get();
    	}
    }

     
    System.out.println()으로 supplierTest()의 반환 값을 출력해보면 의도대로 "call supplier get" 을 반환하고 있음을 볼 수 있다.
     

    또한 Supplier 함수형 인터페이스이 종류로 제네릭 타입인 T를 반환하는 Supplier 이외에도 boolean (primitive) 타입을 반환하는 Supplier로 BooleanSupplier 인터페이스를 제공한다.

    @FunctionalInterface
    public interface BooleanSupplier {
    
        /**
         * Gets a result.
         *
         * @return a result
         */
        boolean getAsBoolean();
    }

     
    BooleanSupplier 인터페이스를 활용하는 아래 코드를 실행해보면 false를 반환하는 것을 볼 수 있다.

    public class FunctionalInterfaceTest {
    	public static void main(String[] args) {
    		boolean result = supplierTest(() -> 3 % 2 == 0); // return false
    		System.out.println("result = " + result);
    	}
    
    	private static boolean supplierTest(BooleanSupplier supplier){
    		return supplier.getAsBoolean();
    	}
    }

     
    이 외에도 원시 타입의 int를 반환하는 IntSupplier, long을 반환하는 LongSupplier, double을 반환하는 DobuleSupplier를 제공한다. 이 중 IntSupplier의 예시만 살펴보자면 다음과 같다.

    public class FunctionalInterfaceTest {
    	public static void main(String[] args) {
    		int result = supplierTest(() -> 3 - 2); // return 1
    		System.out.println("result = " + result);
    	}
    
    	private static int supplierTest(IntSupplier supplier){
    		return supplier.getAsInt();
    	}
    }

     
    실행해보면 1을 반환하는 것을 볼 수 있다. 
     

     
    IntSupplier는 다음과 같이 구현되어있다.

    @FunctionalInterface
    public interface IntSupplier {
    
        /**
         * Gets a result.
         *
         * @return a result
         */
        int getAsInt();
    }

     

    Predicate

    매개변수를 받고 true / false를 반환하는 함수형 인터페이스다. 다음과 같이 제네릭 타입 T를 매개변수로 받는 test() 추상 메서드를 갖는다.

    @FunctionalInterface
    public interface Predicate<T> {
    
        /**
         * Evaluates this predicate on the given argument.
         *
         * @param t the input argument
         * @return {@code true} if the input argument matches the predicate,
         * otherwise {@code false}
         */
        boolean test(T t);
    }

     
    String 타입의 매개변수에 대해 외부로부터 전달받은 방식으로 처리하여 true / false를 반환하는 Predicate 인터페이스를 전달받는 메서드는 다음과 같다. (predicateTest())

    private static boolean predicateTest(Predicate<String> predicate){
    	String parameter = "";
    	return predicate.test(parameter);
    }

     
    특정 문자열(parameter)이 비어있는지(isEmpty())를 확인하는 Predicate 함수형 인터페이스를 활용한 코드는 다음과 같다.

    public class FunctionalInterfaceTest {
    	public static void main(String[] args) {
    		boolean result = predicateTest(s -> s.isEmpty());
    		System.out.println("result = " + result);
    	}
    
    	private static boolean predicateTest(Predicate<String> predicate){
    		String parameter = "";
    		return predicate.test(parameter);
    	}
    }

     
    그 결과로 true를 반환하는 것을 볼 수 있다.
     

     
    만약 int, double, long 타입을 받고 true/false를 반환해야 한다면 IntPredicate, DoublePredicate, LongPredicate 인터페이스를 활용할 수 있다. 그 중 IntPredicate 인터페이스의 활용 예시는 다음과 같다. (특정 값을 2로 나눈 나머지가 0인지 검증하는 로직을 predicateTest() 메서드로 전달한다.)

    public class FunctionalInterfaceTest {
    	public static void main(String[] args) {
    		boolean result = predicateTest(x -> x % 2 == 0);
    		System.out.println("result = " + result);
    	}
    
    	private static boolean predicateTest(IntPredicate intPredicate){
    		int x = 3;
    		return intPredicate.test(x);
    	}
    }

     
    3은 홀수이므로 predicateTest()는 false를 반환한다. 
     

    만일 두 개의 매개변수에 대한 처리 결과로 true / false를 반환받아야 한다면 BiPredicate 인터페이스를 사용할 수 있다.

    @FunctionalInterface
    public interface BiPredicate<T, U> {
    
        /**
         * Evaluates this predicate on the given arguments.
         *
         * @param t the first input argument
         * @param u the second input argument
         * @return {@code true} if the input arguments match the predicate,
         * otherwise {@code false}
         */
        boolean test(T t, U u);
    }

     
    BiPredicate 인터페이스는 다음과 같이 사용할 수 있다. Integer 타입의 두 매개변수를 처리하는 Predicate 인터페이스를 매개변수로 받는 predicateTest() 메서드를 선언하고, predicateTest() 메서드에게 x % y로 나눈 나머지가 0인지 확인하는 로직을 전달한다.

    public class FunctionalInterfaceTest {
    	public static void main(String[] args) {
    		boolean result = predicateTest((x, y) -> x % y == 0);
    		System.out.println("result = " + result);
    	}
    
    	private static boolean predicateTest(BiPredicate<Integer, Integer> biPredicate){
    		int x = 3;
    		int y = 2;
    		return biPredicate.test(x, y);
    	}
    }

     
    실행 결과 false를 반환하는 것을 볼 수 있다.
     

    Operator

    매개변수를 전달받고 전달받은 매개변수와 동일한 타입을 반환한다. 하나의 매개변수를 받는 Operator 인터페이스로는 UnaryOperator가 있으며, 두 개의 매개변수를 전달받는 경우 BinaryOperator를 사용한다.
     
    사실 UnaryOperator<T> 인터페이스를 살펴보면 Function<T, T> 형태의 Function 인터페이스의 자식이라는 것을 볼 수 있다.

    @FunctionalInterface
    public interface UnaryOperator<T> extends Function<T, T> {
    
        /**
         * Returns a unary operator that always returns its input argument.
         *
         * @param <T> the type of the input and output of the operator
         * @return a unary operator that always returns its input argument
         */
        static <T> UnaryOperator<T> identity() {
            return t -> t;
        }
    }

     
    즉, (Function 인터페이스에서 소개한) 아래 예시처럼 String 타입의 매개변수를 받고 처리한뒤 String 타입을 반환하는 다음과 같은 funtionTest()는 UnaryOperator()로 대체할 수 있다.

    public class FunctionalInterfaceTest {
    
        public static void main(String[] args) {
           String result = functionTest(lowerCase -> lowerCase.toUpperCase());
           System.out.println("result = " + result);
    
        }
    
        private static String functionTest(Function<String, String> function){
           String a = "a";
           return function.apply(a);
        }
    }

     
    UnaryOperator()를 활용하여 작성한 코드는 다음과 같다.

    public class FunctionalInterfaceTest {
    	public static void main(String[] args) {
    		String result = operatorTest(lowerCase -> lowerCase.toUpperCase());
    		System.out.println("result = " + result);
    
    	}
    
    	private static String operatorTest(UnaryOperator<String> unaryOperator){
    		String parameter = "a";
    		return unaryOperator.apply(parameter);
    	}
    }

     
    Function<String, String>을 사용할때와 동일한 결과를 반환한다.

     
    두 개의 매개변수를 받고 동일한 타입의 결과를 반환하는 BinaryOperator 또한 다음과 같이 BiFunction<T, T, T>의 자식으로 구현되어 있다.

    @FunctionalInterface
    public interface BinaryOperator<T> extends BiFunction<T,T,T> {
        /**
         * Returns a {@link BinaryOperator} which returns the lesser of two elements
         * according to the specified {@code Comparator}.
         *
         * @param <T> the type of the input arguments of the comparator
         * @param comparator a {@code Comparator} for comparing the two values
         * @return a {@code BinaryOperator} which returns the lesser of its operands,
         *         according to the supplied {@code Comparator}
         * @throws NullPointerException if the argument is null
         */
        public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
            Objects.requireNonNull(comparator);
            return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
        }
    }

     
    BinaryOperator 인터페이스의 활용 예시는 다음과 같다.

    public class FunctionalInterfaceTest {
    	public static void main(String[] args) {
    		Integer result = operatorTest((x, y) -> x * y);
    		System.out.println("result = " + result);
    
    	}
    
    	private static Integer operatorTest(
        	BinaryOperator<Integer> binaryOperator // 두개의 Integer 변수를 받고 Integer 타입의 결과를 반환
            ){ 
    		Integer x = 5;
    		Integer y = 3;
    		return binaryOperator.apply(x, y);
    	}
    }

     
    실행 결과로 다음과 같이 15를 출력하는 것을 볼 수 있다.
     

     
    사실 이처럼 정수 타입의 변수을 받지만 Wrapper Class인 Integer를 사용할 이유가 없는 경우 IntBinaryOperator를 사용할 수 있다.

    public class FunctionalInterfaceTest {
    	public static void main(String[] args) {
    		Integer result = operatorTest((x, y) -> x * y);
    		System.out.println("result = " + result);
    
    	}
    
    	private static Integer operatorTest(
        	IntBinaryOperator integerIntBinaryOperator // IntBinaryOperator 활용
        ){ 
    		Integer x = 5;
    		Integer y = 3;
    		return integerIntBinaryOperator.applyAsInt(x, y);
    	}
    }

     
    BinaryOperator 인터페이스를 사용할때와 동일한 결과인 15를 반환하는것을 볼 수 있다.
     

    적용 사례

    앞 부분에서 예시로 소개한 다음 코드에서는 objectA와 objectB 객체를 매개변수로 받고 call 메서드를 호출하는 로직에서 코드 중복이 발생했었다.
     

    • 기존 코드
    public class ServiceA {
    	private final ServiceC serviceC;
        private final RepositoryA repositoryA;
        
    	public void methodA(){
        	List<ObjectA> objectAList = repositoryA.findAll();
            
            objectAList.forEach(objectA -> {
            	// objectAList의 원소별 처리 로직
                serviceC.call(objectA);
             })
    }
    
    public class ServiceB {
    	private final ServiceC serviceC;
        private final RepositoryB repositoryB;
    
       public void methodB(){
        	// ServiceA와 중복 발생
            List<ObjectB> objectBList = repositoryB.findAll();
            
            objectBList.forEach(objectB -> {
            	// objectBList 원소별 처리 로직
                serviceC.call(objectB);
             })
    }

     
    이 경우 call() 메서드를 호출한 반환 값은 따로 사용하지 않기 때문에, 앞서 소개한 함수형 인터페이스 중 Consumer를 사용하여 코드를 리팩토링하여 중복을 개선할 수 있었다.
     

    • 리팩토링 결과
    public class ServiceA {
    	private final ServiceC serviceC;
        private final RepositoryA repositoryA;
        private final LinkerService linker;
        
    	public void methodA(){
        	List<ObjectA> objectAList = repositoryA.findAll();
            linker.linking(objectAlist, objectA -> { 
            	/* objectA에 대한 처리 로직 작성 */
            });
    }
    
    public class ServiceB {
    	private final ServiceC serviceC;
        private final RepositoryB repositoryB;
        private final LinkerService linker;
    
       public void methodB(){
            List<ObjectB> objectBList = repositoryB.findAll();
            linker.linking(objectBList, objectB -> {
            	/* objectB에 대한 처리 로직 작성 */
            });
    }
    
    public class LinkerService {
    	public void linking(List<T> objectList, Consumer<T> consumer) {
        	 objectBList.forEach(objectB -> {
            	// 원소별 처리 로직
                serviceC.call(objectB);
                ... 후략
             })	
    }

     
    리팩토링 결과, objectA와 objectB에 대한 처리 로직을 LinkerService에게 위임하였다. 그리고 LinkerService 클래스에는 Consumer 인터페이스와 Consumer 인터페이스가 처리하는 타입과 동일하게 선언된 List<T>를 매개변수로 받는 linking 메서드를 선언하여 데이터 처리에 대한 추상화를 구현하였다. 이를 통해 ServiceA와 ServiceB는 각각 LinkerService 클래스의 linking 메서드로 (기존에 중복되던) 데이터 처리 방식을 전달하는 식으로 리팩토링하였다. 기존에 발생했던 objectA와 objectB에 대한 처리 로직에 대한 중복을 이와 같이 함수형 인터페이스(그 중 Consumer 인터페이스)로 개선할 수 있었다.

    유의할 점

    적절한 함수형 인터페이스의 사용은 이처럼 코드 중복을 개선하고 유연한 메서드 또는 클래스를 개발할 수 있다. 또한 가독성을 향상시키고, 스트림 API 등과 함께 사용하여 병렬처리, 멀티 쓰레드 환경에서 원자성을 확보하고 스레드 안정성을 보장하는 등 성능상 이점도 얻을 수 있다.
    하지만 함수형 프로그래밍을 통한 과도한 추상화는 오히려 이해하기 어려운 코드를 작성하고 코드 복잡도를 높일 수 있다. 따라서 (모든 개발 기술이 그렇겠지만) 무조건적으로 모든 상황에 함수형 인터페이스를 활용해서 개발하는것이 아니라 필요에 맞춰 적절한 상황과 방식으로 적용한다면 더 큰 이점을 얻을 수 있을 것 같다.

    출처 및 참고한 곳

    Comments