본문 바로가기
개발/Spring Boot, Java

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

by mixxeo(믹서) 2024. 11. 24.

배경

 

우아한 테크코스 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