개발/Spring Boot, Java

[Java] 함수형 인터페이스(Functional Interface)를 활용해 중복 코드 줄이기

mixxeo(믹서) 2024. 11. 24. 23:12

배경

 

우아한 테크코스 7기 프리코스의 3주차, 4주차 과제에 다음과 같은 요구사항이 있었다.

사용자가 잘못된 값을 입력할 경우 예외를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다.

 

처음 구현한 코드는 사용자 입력을 받는 컨트롤러의 메서드마다 `try-catch` 구문 처리를 하고,

예외가 발생했을 때 에러 메시지를 출력한 뒤 재귀호출을 하는 형태였다.

public class LottoController {
    // ...
    
    private PurchasePrice requestPurchasePrice() {
        try {
            outputView.displayPurchasePriceRequest();
            return new PurchasePrice(inputView.getInteger());
        } catch (IllegalArgumentException e) {
            outputView.displayErrorMessage(e.getMessage());
            return requestPurchasePrice();
        }
    }
    
    private Lotto requestMainNumbers() {
        try {
            outputView.displayMainNumbersRequest();
            String mainNumbers = inputView.getString();
            return lottoService.createMainNumbers(mainNumbers);
        } catch (IllegalArgumentException e) {
            outputView.displayErrorMessage(e.getMessage());
            return requestMainNumbers();
        }
    }
    
    private BonusNumber requestBonusNumber(Lotto mainNumbers) {
        try {
            outputView.displayBonusNumberRequest();
            int bonusNumber = inputView.getInteger();
            return BonusNumber.of(bonusNumber, mainNumbers);
        } catch (IllegalArgumentException e) {
            outputView.displayErrorMessage(e.getMessage());
            return requestBonusNumber(mainNumbers);
        }
    }
    
    // ...
}

 

메서드를 3개만 작성했는데도, 코드가 장황해졌다.

일종의 템플릿 같은 형태의 공통 메서드가 필요하다고 생각하고, 가능한지는 모르겠지만(ㅎㅎ) 중복 코드를 추출해봤다.

    private ??? requestWithRetry(??? request) {
        try {
            return request();
        } catch (IllegalArgumentException e) {
            outputView.displayErrorMessage(e.getMessage());
            return requestWithRetry(request);
        }
    }

 

각 메서드의 `try` 구문내 로직을 하나의 메서드로 묶으면, 위와 같은 메서드를  활용할 수 있을 것 같았다!

 

 


해결 과정

 

`try` 구문내의 로직들은 아래와 같이 각각의 메서드로 작성할 수 있다.

    private PurchasePrice requestPurchasePrice() {
        outputView.displayPurchasePriceRequest();
        return new PurchasePrice(inputView.getInteger());
    }
    
    private Lotto requestMainNumbers() {
        outputView.displayMainNumbersRequest();
        String mainNumbers = inputView.getString();
        return lottoService.createMainNumbers(mainNumbers);
    }

    private BonusNumber requestBonusNumber(Lotto mainNumbers) {
        outputView.displayBonusNumberRequest();
        int bonusNumber = inputView.getInteger();
        return BonusNumber.of(bonusNumber, mainNumbers);
    }

 

 

3개의 메서드는 서로 다른 타입의 객체를 반환하는 형태이기 때문에, 추출한 공통 메서드를 제네릭 메서드로 정의할 수 있다.

제네릭 메서드
- 제네릭 메서드란, 메서드 선언부에 제네릭 타입 파라미터(<T>)가 선언된 메서드를 말한다.
- 제네릭 메서드는 직접 메서드에 제네릭 타입 파라미터(<T>)를 설정함으로써 타입을 동적으로 타입을 받아와, 클래스의 타입과는 독립적으로 운용 가능한 메서드라고 이해하면 된다.

출처

 

    private <T> T requestWithRetry(??? request) {
        try {
            return request();
        } catch (IllegalArgumentException e) {
            outputView.displayErrorMessage(e.getMessage());
            return requestWithRetry(request);
        }
    }

 

 

이제 추출한 3개의 메서드를 공통 메서드에게 전달할 수 있도록 하는, 공통메서드의 파라미터 타입을 정해야한다.

 

잠시 타입스크립트의 콜백 함수를 떠올려봤다. 타입스크립트는 함수의 파라미터로 또 다른 함수를 전달할 수 있다.

이 개념을 콜백 함수라고 하고 화살표 함수를 이용해 구현할 수 있다.

const hello = function (callName: (name: string) => void) {
    console.log("hello");
    callName("mixxeo");
}

const callName = function (name: string) => {
    console.log(name);
}

hello(callName);

 

 

다시 자바에서 함수를 매개변수로 넘기는 방법을 알아보자.

자바에서 인터페이스(Interface)는 타입의 역할을 할 수 있다. 따라서, 자바에서 함수를 매개변수로 넘길 때는 함수의 타입을 정의하기 위해 함수형 인터페이스(Functional Interface)를 사용해야한다.

기본적으로 자바에서 메서드를 정의하기 위해서는 클래스를 정의해야 한다. 그런데, 이를 간결하게 표현할 수 있도록 도와주는 개념이 java8에서 등장한 람다 표현식(Lambda expression)이다. 그리고 람다 표현식을 사용하기 위한 수단이 함수형 인터페이스이다.

 

 

위의 타입스크립트 코드를 자바로 변환하면 다음과 같이 작성할 수 있다.

public class Main {
    public static void main(String[] args) {
        NameHandler callName = name -> System.out.println(name);
        hello(callName);
    }

    public static void hello(NameHandler callName) {
        System.out.println("hello");
        callName.handle("mixxeo");
    }
}

 

여기서 간결하게 표현된 `name -> System.out.println(name)` 이 람다식은 아래의 인터페이스 `NameHandler`를 구현(implement)한 익명 객체를 간결하게 표현한 것이다.

@FunctionalInterface
interface NameHandler {
    void handle(String name);
}

// 람다식을 풀어서 작성하면
NameHandler callName = new NameHandler() {
    @Override
    public void handler(String name) {
        System.out.println(name);
    }
};

 

정리하자면, `NameHandler`와 같이 추상(abstract) 메서드를 1개만 가지고있는 인터페이스를 함수형 인터페이스라고 하며

람다식으로 함수형 인터페이스를 간결하게 구현할 수 있다. 그리고 이 람다식을 메서드의 매개변수로 함수를 넘길 때 매개변수의 타입으로 사용할 수 있다! `NameHandler`처럼 함수형 인터페이스를 개발자가 직접 정의할 수도 있지만, 자바에서는 Consumer, Supplier, Function, Operator, Predicate 등 함수형 인터페이스 표준 API를 제공한다.

 

나는 객체 T를 반환하는 함수를 매개변수의 타입으로 지정해야하기 때문에, Supplier를 사용하면 처음 코드를 다음과 같이 작성할 수 있다.

public class LottoController {
    // ...
    
    public void run() {
        // ...
        PurchasePrice purchasePrice = requestWithRetry(this::requestPurchasePrice);
        Lotto mainNumbers = requestWithRetry(this::requestMainNumbers);
        BonusNumber bonusNumber = requestWithRetry(() -> requestBonusNumber(mainNumbers));
        // ...
    }
    
    private PurchasePrice requestPurchasePrice() {
        outputView.displayPurchasePriceRequest();
        return new PurchasePrice(inputView.getInteger());
    }
    
    private Lotto requestMainNumbers() {
        outputView.displayMainNumbersRequest();
        String mainNumbers = inputView.getString();
        return lottoService.createMainNumbers(mainNumbers);
    }

    private BonusNumber requestBonusNumber(Lotto mainNumbers) {
        outputView.displayBonusNumberRequest();
        int bonusNumber = inputView.getInteger();
        return BonusNumber.of(bonusNumber, mainNumbers);
    }
    
    private <T> T requestWithRetry(Supplier<T> request) {
        try {
            return request.get();
        } catch (IllegalArgumentException e) {
            outputView.displayErrorMessage(e.getMessage());
            return requestWithRetry(request);
        }
    }
    
    // ...
}

 

`run` 메서드에서 각 메서드의 호출부를 보면, 각 메서드에 재실행 로직이 적용된다는 것을 파악할 수 있고

각 메서드들도 `try-catch` 구문이 제거되어 로직을 파악하기 쉬운 형태가 됐다! 

 

이렇게만 작성할 수도 있지만 요구 사항에 따라 `request-` 메서드들은 `IllegalArgumentException` 예외를 던진다는 것을 강조하기 위해 별도의 함수형 인터페이스 `SupplierWithException`을 추가로 정의했다.

    private <T> T requestWithRetry(SupplierWithException<T> request) {
        try {
            return request.get();
        } catch (IllegalArgumentException e) {
            outputView.displayErrorMessage(e.getMessage());
            return requestWithRetry(request);
        }
    }

    @FunctionalInterface
    private interface SupplierWithException<T> {
        T get() throws IllegalArgumentException;
    }

 


참고 자료

 

https://inpa.tistory.com/entry/%E2%98%95-%ED%95%A8%EC%88%98%ED%98%95-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-API

 

☕ 함수형 인터페이스 표준 API 총정리

함수형 인터페이스 표준 API 함수형 인터페이스(functional interface)는 추상메서드가 1개만 정의된 인터페이스를 통칭하여 일컫는다. 이 인터페이스 형태의 목적은 자바에서 람다 표현식(Lambda Expressi

inpa.tistory.com

https://inpa.tistory.com/entry/%E2%98%95-Lambda-Expression

 

☕ 람다 표현식(Lambda Expression) 완벽 정리

람다 표현식 (Lambda Expression) 람다 표현식(lambda expression)이란 함수형 프로그래밍을 구성하기 위한 함수식이며, 간단히 말해 자바의 메소드를 간결한 함수 식으로 표현한 것이다. 지금까지 자바에

inpa.tistory.com