1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private BonusNumber getBonusNumber(WinningNumbers winningNumbers) {
try {
String bonusNumber = InputView.inputBonusNumber();
return BonusNumber.create(bonusNumber, winningNumbers);
} catch (IllegalArgumentException e) {
OutputView.printErrorMessage(e.getMessage());
return getBonusNumber(winningNumbers);
}
}
private WinningNumbers getWinningNumbers() {
try {
String winningNumbers = InputView.inputWinningNumbers();
return WinningNumbers.create(winningNumbers);
} catch (IllegalArgumentException e) {
OutputView.printErrorMessage(e.getMessage());
return getWinningNumbers();
}
}
private PurchasePrice getPurchasePrice() {
try {
String money = InputView.inputPurchasePrice();
return PurchasePrice.create(money);
} catch (IllegalArgumentException e) {
OutputView.printErrorMessage(e.getMessage());
return getPurchasePrice();
}
}
위 코드들은 입력 값을 받아 입력 값을 바탕으로한 객체를 생성해 반환하는 코드이다. 만약 객체 생성 도중 예외가 발생하면 입력 값을 다시 입력받도록 try-catch 구문을 통한 재귀가 이용되었다.
하지만 이런 재귀의 방법은 악의적으로 반복할 경우 스택오버플로우 같은 오류가 발생할 수 있으며 우선 위와 같은 구조가 계속 반복되는 코드를 제거하고 싶어졌다.
내가 사용했던 중복 코드 제거 방법들과 문제점에 대해 살펴보려 한다.
방법 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private BonusNumber getBonusNumber(WinningNumbers winningNumbers) {
return (BonusNumber) InputView.inputValue("bonusNumber",
inputBonusNum -> BonusNumber.create(inputBonusNum, winningNumbers));
}
private WinningNumbers getWinningNumbers() {
return (WinningNumbers) InputView.inputValue("winningNumbers",
WinningNumbers::create);
}
private PurchasePrice getPurchasePrice() {
return (PurchasePrice) InputView.inputValue("purchasePrice",
PurchasePrice::create);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class InputView {
private static final Map<String, String> inputMessages = new HashMap<>();
static {
inputMessages.put("purchasePrice", "구입금액을 입력해 주세요.");
inputMessages.put("winningNumbers", "\n당첨 번호를 입력해 주세요.");
inputMessages.put("bonusNumber", "\n보너스 번호를 입력해 주세요.");
} // 클래스 처음 로딩 시 실행되는 초기화
public static Object inputValue(String key, Function<String, Object> numberCreateFunction) {
while (true) {
try {
System.out.println(getMessage(key));
String input = Console.readLine();
return numberCreateFunction.apply(input);
} catch (IllegalArgumentException e) {
OutputView.printErrorMessage(e.getMessage());
}
}
}
}
입력에 필요한 출력 메시지를 매핑하기 위한 키 값과 객체를 생성하는 함수를 인자로 넘기는 형태로 중복 코드를 제거했다.
문제점
입력에 필요한 출력 메시지를 View에서 Map으로 관리하고 있다.
하지만 getWinningNumbers() 메서드 내부에서 키 값을 전달하고 있는데 여기서 키 값은 아무 String 값이나 들어갈 수 있다. 즉, 실수로 키 값을 잘못 전달할 확률이 충분히 존재한다는 것이다.
객체 생성 메서드 자체도 캐스팅이 되어 위 같은 경우 실수할 확률이 적지만 만약 WinningNumbers를 상속하여 사용하는 객체가 있다면 그 메서드는 인자값으로 넘겨질 수 있기 때문에 문제가 발생할 수 있다.
이러한 문제를 해결하기 위해 아래와 같은 방법으로 바꾸게 된다.
방법 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public enum InputValueType {
PURCHASE_PRICE("구입금액을 입력해 주세요.", PurchasePrice::create),
WINNING_NUMBERS("\n당첨 번호를 입력해 주세요.", WinningNumbers::create),
BONUS_NUMBER("\n보너스 번호를 입력해 주세요.", null);
private final String message;
private final Function<String, Object> createFunction;
/...
public Function<String, Object> getCreateFunction() {
return createFunction;
}
public Function<String, Object> getCreateFunction(WinningNumbers winningNumbers) {
return inputValue -> BonusNumber.create(inputValue, winningNumbers);
}
}
InputValueType이라는 Enum 클래스를 통해 객체 생성함수와 메시지를 관리하는 방법이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static Object inputValue(InputValueType inputValueType) {
while (true) {
try {
System.out.println(inputValueType.getMessage());
Function<String, Object> createFunction = inputValueType.getCreateFunction();
String input = Console.readLine();
return createFunction.apply(input);
} catch (IllegalArgumentException e) {
printErrorMessage(e.getMessage());
}
}
}
public static Object inputValue(InputValueType inputValueType, WinningNumbers winningNumbers) {
while (true) {
try {
System.out.println(inputValueType.getMessage());
Function<String, Object> createFunction = inputValueType.getCreateFunction(winningNumbers);
String input = Console.readLine();
return createFunction.apply(input);
} catch (IllegalArgumentException e) {
printErrorMessage(e.getMessage());
}
}
}
View에서는 위와 같이 사용하도록 바뀐다.
1
2
3
4
5
6
7
8
9
10
11
private BonusNumber getBonusNumber(WinningNumbers winningNumbers) {
return (BonusNumber) InputView.inputValue(InputValueType.BONUS_NUMBER, winningNumbers);
}
private WinningNumbers getWinningNumbers() {
return (WinningNumbers) InputView.inputValue(InputValueType.WINNING_NUMBERS);
}
private PurchasePrice getPurchasePrice() {
return (PurchasePrice) InputView.inputValue(InputValueType.PURCHASE_PRICE);
}
이제 컨트롤러에서는 입력 값에 따라 객체를 만들고, 예외가 발생했을 경우 다시 입력을 받는 로직은 한 줄로 중복이 제거된 상태로 인자값만 바꾸어 사용할 수 있게 되었다.
위에서 문제가 되었던 아무 String Key값이나 생성 메서드가 실수로 잘못 넘겨줄 가능성이 있었던 것은 해결이 되었다.
이런 면이 Enum의 장점이기도 하다.
하지만 여기에도 문제가 있다.
문제점
우선 BonusNumber는 다른 객체들과 다르게 생성에 WinningNumbers라는 인자가 하나 필요하다.
따라서 getCreateFunction() 메서드와 inputValue() 메서드는 오버로딩이 되어있다.
사실 오버로딩은 문제가 되지 않을 수 있으나, Enum 클래스를 보면 BonusNumber의 createFunction에는 null 값이 할당되어 있다. 현재 구조에서는 문제가 발생하지 않는다.
하지만 만약 BonusNumber처럼 생성에 어떤 인자가 필요한 객체가 추가로 생긴다면 그 객체에도 null 값을 할당해놓고 getCreateFunction에서 if문을 사용한다거나 하는 문제가 생기는 것이다.
위 두 방법의 공통 문제점
- 생성 함수의 반환 타입이 Object이기 때문에 타입 안정성의 문제가 있다.
- input 값을 도메인 객체에 넣어주면 String input 값을 알맞은 타입으로 변환하는 역할까지 도메인 객체가 담당하고 있다.
- 도메인이 반드시 해야하는 역할인지 고려해봐야 한다.
방법 3
1
2
3
4
5
6
7
8
9
10
private <R> R repeatToReadUntilSuccess(Supplier<R> reader) {
while (true) {
try {
return reader.get();
}
catch (IllegalArgumentException e) {
outputView.printErrorMessage(e.getMessage());
}
}
}
컨트롤러에서 위와 같은 메서드를 추가할 수 있다.
사용 방법은 아래와 같다.
1
2
3
4
private WinningNumbers getWinningNumbers() {
return repeatToReadUntilSuccess(() ->
WinningNumbers.create(InputView.inputWinningNumbers));
}
컨트롤러 내부에서 입력을 성공할 때 까지 반복한다. 라는 로직이 있는 것이 어색하지 않고, Object 타입을 반환하지 않아도 되기 때문에 명확하게 WinningNumbers와 같은 객체를 생성해낼 수 있다.
문제점
물론 이 부분도 String을 입력받은 값을 도메인 객체에서 알맞은 형태로 변환시키는 역할까지 담당하는 것은 마찬가지이다.
이 부분은 View에서 변환할 수도 있고, 그 과정에서 변환을 담당하는 클래스를 만들어줄 수 있다.
결론
나는 만약 비슷한 미션을 구현하게 된다면 방법 3을 사용해 반복되는 입력 값 처리를 해결할 것 같다.