배경
우아한 테크코스 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;
}
참고 자료
☕ 함수형 인터페이스 표준 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
'개발 > Spring Boot, Java' 카테고리의 다른 글
일급 컬렉션, 왜 Collection만 특별취급 하나요? (0) | 2025.01.05 |
---|---|
[번역] JUnit5 Parameterized Test 가이드 (0) | 2024.10.27 |