본문 바로가기
모던자바

람다식, 메서드 참조를 이용하여 예외 발생시 성공할 때 까지 반복 구현 - 함수형 인터페이스

by 임동무 2022. 12. 1.

우아한테크코스를 진행하면서

"예외사항이 발생하는 경우 예외를 던진 후 예외사항이 발생한 지점부터 다시 입력을 받아라" 라는 요구사항이 있었다.

 

그래서 처음 생각한 방법은 while 반복문으로 try ~ catch 문을 감싸,

예외가 발생하지 않는 경우 retrun 을 통해 반복문을 탈출하는 방법이었다.

아래의 코드는 해당 방법을 구현한 것이다.

    private int getAttempts() {
        outputView.askAttemptsInput();
        while (true) {
            try {
                return inputView.insertAttempts();
            } catch (IllegalArgumentException e) {
                System.out.println(e.getMessage());
            }
        }
    }

 

이 방법을 사용하여 예외처리가 나는 부분을 메서드로 추출하여 예외가 발생하는 부분마다 따로 적용을 해주면 요구사항을 만족할 수 있었다. 하지만 걸리는 부분이 한가지 있다면 바로 예외가 발생하는 부분마다 적용을 해줘야한다는 점이다.

 

예외가 발생하는 부분마다(입력받는 부분) 적용하다보니 입력 받는 모든 부분에 동일하게 반복되는 

while { try ~ catch } 가 발견되었다. 

입력받는 값이 한가지면 상관이 없겠지만 많으면 많아질수록 코드에 반복되는 부분이 많아지는 문제가 발생한다. 

반복되는 while { try~catch }

위의 사진처럼 동일한 구조가 지속적으로 반복되며 또한 indent level 을 2레벨이나 필요로 한다. 

 

다른 사람의 코드에서 보았던 재귀함수를 통한 메서드 호출도 마찬가지이다. 

재귀함수는 예외가 발생한다고 가정하면 메서드가 종료되기 전에 메서드 자기 자신을 한번 더 호출한다. 

즉, call stack 이 쌓이게 된다. 

 

메모리에는 stack 영역이 제한적이기 때문에 무한정으로 반복되는 경우 StackOverFlow 문제가 발생하게 된다. 

그렇기 때문에 재귀함수가 조금 더 직관적이여 보이고, 코드가 간결해질 순 있으나 코드가 반복되는 문제를 해결할 수 없을 뿐더러 StackOverFlow 라는 부가적인 문제까지 불러일으킬 수 있다. 

 

 

 

결국 스터디에서 다른사람의 코드를 보고 나서야 해결책을 깨달았다.

바로 예외처리가 나는 행위 자체를 Parameter 로 넘겨주는 것이다.

즉, 동작의 파라미터화가 필요하다.

 

1. 함수형 인터페이스 만들기

public interface Task<T> {
    T run();

    static <T> T reTryUntilSuccess(Task<T> task) {
        while (true) {
            try {
                return task.run();
            } catch (IllegalArgumentException e) {
                System.out.println(e.getMessage());
            }
        }
    }
}


위의 코드를 보자.

이 함수는 T run() 이라는 추상 메서드를 하나만 가지고 있는 함수형 인터페이스이다.

이 함수형 인터페이스를 구현하는 람다방식을 이용하여 동작을 파라미터화하면 while{ try~catch } 문을 반복하지 않을 수 있다. 

 

Task.reTryUntilSuccess(() -> inputView.insertCars());

구현한 방식을 보면 ()-> inputView.insertCars()

'파라미터는 없으며, inputView 의 insertCars() 라는 메서드를 실행한다 '

라는 의미이며 이 람다프로세스는 Task<T> 를 구현한다.

여기서 <T> 는 insertCars() 가 List<String> 을 반환하기 때문에 List<String> 타입이다.

 

이를 메서드 참조로 바꾸면

Task.reTryUntilSuccess(inputView::insertCars);

이렇게 간단하게 표현할 수 있다.

 

람다 혹은 메서드 참조를 이용하여 구현한 Task<T> 를 파라미터로

reTryUntilSuccess(Task<T> task) 메서드를 실행할 수 있는 것이다. 

이를 통해 반복되는 구조의 반복을 없앨 수도 있으며 코드도 꽤나 직관적으로 변경할 수 있다.

 

 

 

2. 기존의 함수형 인터페이스 사용하기

사실 주로 사용하는 함수형 인터페이스들은 이미 정의가 되어있다. 그래서 사용자가 직접 함수형 인터페이스를 만드는 경우는 거의 없다고 한다. 

 

이 외에도 다양한 함수형 인터페이스가 정의되어 있다. 

 

이중에 나는 생성된 List<String> 을 반환받아야 하므로 Supplier<T> 의 T.get() 메서드를 사용할 것이다. 

먼저 Supplier<T> supplier 를 정의하고 그 supplier 를 파라미터로 넘겨주는 방식이다.

 

    private List<String> getCarNames() {
        outputView.askCarNamesInput();
        Supplier<List<String>> supplier = inputView::insertCars;
        return retryUntilSuccess(supplier);
    }

    static <T> T retryUntilSuccess(Supplier<T> supplier) {
        while(true) {
            try {
                return supplier.get();
            } catch (IllegalArgumentException e) {
                System.out.println(e.getMessage());
            }
        }
    }

 

 

 

 

이를 좀 더 간결하게 만들면

    private List<String> getCarNames() {
        outputView.askCarNamesInput();
        return retryUntilSuccess(inputView::insertCars);
    }

    static <T> T retryUntilSuccess(Supplier<T> supplier) {
        while(true) {
            try {
                return supplier.get();
            } catch (IllegalArgumentException e) {
                System.out.println(e.getMessage());
            }
        }
    }

의 방식으로 구현할 수 있다. 

코드도 간단해졌으며 의미또한 동작 자체가 구현체가 되어 파라미터로 넘겨졌기에 getCarNames() 의 메서드가 어떤 동작을 하는지 굉장히 직관적으로 파악할 수 있는 장점이 있다.

 

 

 

 

 

출처 : https://github.com/woowacourse/java-racingcar-precourse/compare/main...viyott-lover:java-racingcar-precourse:viyott-lover

출처 : https://bcp0109.tistory.com/313

댓글