이번에는 처음 해 보는 문제였으나, 나름 추측했던 (?) 문제였던 로또 문제였습니다.
Enum에 대해 더 깊은 관찰과 고민을 하게 됨
1주 차 때 이펙티브 자바를 보며 공용 상수는 Enum으로 관리하는 것이 좋을 것 같다는 글을 썼었는데요, 이번 미션에서는 아예 요구사항으로 Java Enum을 적용하는 게 있었습니다! 그래서 이펙티브 자바에서 Enum에 대해 미처 알지 못했던 부분들을 읽거나, 다른 자료를 더 찾아보곤 하였습니다.
언제 Enum을 쓰고 언제 private static final을 써야 할까? (feat. 응집도)
1주 차 때 작성된 글을 보면, 객체에 private 하게 사용되는 상수는 후자 방식을 쓰고, 그렇지 않다면 Enum을 통해 관리하도록 하는 게 좋다고 했었는데요. 아쉬운 점은 응집도에 대해서 생각해보지 못했다는 것입니다. (Validator에 대해 응집도를 공부했으나 Enum에 대해서는 생각해보지 못했던 것이 이유 같습니다.)
이번 미션을 하며 내린 결론은 최대한 객체에서만 private 하게 사용되어야 상수 관리적인 측면에서도 객체의 응집도가 올라간다는 점입니다. 객체에서만 관리하고 이 값에 대해 알고 있어야만 캡슐화가 보장되는 효과도 있겠죠!
ExceptionMessage에서 참고해야 한다면 어떻게 해야 할까?
그러나 ExceptionMessage 등 예외 메시지에서 해당 객체의 상수를 알아야 할 필요가 있을 때도 있습니다. 예시로 저는 로또의 숫자 범위가 올바르지 않을 시, 아래와 같이 작성했었습니다.
public enum LottoRule {
MINIMUM_NUMBER(1),
MAXIMUM_NUMBER(45),
LOTTO_NUMBER_LENGTH(6),
...
// 객체에서 LottoRule.validateNumberValue를 호출하는 식으로 이용
public static void validateNumberValue(final int number) {
if (number > MAXIMUM_NUMBER.value || number < MINIMUM_NUMBER.value) {
throw new IllegalArgumentException(UNVALID_LOTTO_NUMBER.getMessage());
}
}
}
public class LottoMachine {
public static List<Integer> pickNumbers() {
return Randoms.pickUniqueNumbersInRange(MINIMUM_NUMBER.getValue(),
MAXIMUM_NUMBER.getValue(), LOTTO_NUMBER_LENGTH.getValue());
}
}
public enum ExceptionMessage {
UNVALID_LOTTO_NUMBER(
"로또에서 생성될 수 없는 숫자가 있습니다. 로또 번호는 " + MINIMUM_NUMBER.getValue() + " ~ " +
MAXIMUM_NUMBER.getValue() + " 사이여야 합니다."
),
...
}
원래 로또 숫자의 범위는 LottoMachine에서 관리하는 게 적합하지만, ExceptionMessage에서 사용해야 하다 보니 LottoRule enum으로 관리하고 있음을 볼 수 있습니다.
- 단순히 "값의 범위가 올바르지 않습니다"로 예외 메시지를 띄운다면 임시방편에 불과하다고 생각합니다. 뷰는 언제든지 요구사항이 바뀔 수 있고, 그렇기에 더 구체적으로 예외를 알려달라는 요청이 들어올 수도 있기 때문입니다.
- LottoMachine에서 직접 예외 메시지를 관리하면 요구사항에 따라 LottoMachine 코드를 바꿔야 할 것입니다.
- 상수를 public으로 열어두는 것은 캡슐화 관점에서 안 좋아 보입니다.
결국 이렇게 예외 메시지에서 상수에 대해 알아야 할 때가 있다면 Enum으로 만들어 관리하는 게 낫겠다는 결론을 내렸습니다.
제네릭에 대한 고민 & ChatGPT에 대한 반성
이번 미션부터는 예외가 발생했을 시 프로그램을 종료하는 게 아닌, 그 지점부터 다시 입력을 요청하는 식으로 진행해야 했습니다.
뭔가 객체를 만들려고 했을 때 예외가 터진다면 다시 생성하게끔 하면 될 것 같아, 이 내용을 코드로 작성하면 되지 않을까라고 생각했었습니다.
하지만 마땅히 적합한 방법이 떠오르지 않아 ChatGPT를 활용하여 아래와 같은 코드를 작성했었습니다.
private Investor initInvestor() {
return createInstance(Investor.class, () -> {
outputView.askInvestMoney();
String investMoneyInput = inputView.readLine();
return Investor.createDefault(investMoneyInput);
});
}
private <T> T createInstance(final Class<T> classType, final Supplier<T> creator) {
T created = null;
while (created == null) {
created = tryGetInstance(creator, created);
}
return created;
}
private <T> T tryGetInstance(final Supplier<T> creator, T created) {
try {
created = creator.get();
} catch (IllegalArgumentException exception) {
outputView.printExceptionMessage(exception.getMessage());
}
return created;
}
- tryGetInstance는 특정 클래스를 만드는 Supplier 함수로부터 객체를 받아오려고 시도하고, 이 과정에서 예외가 발생하면 예외 메시지를 출력 후 created를 반환합니다. (예외가 발생하지 않으면 Supplier 함수로부터 객체를 받아옵니다.)
- createInstance는 특정 클래스의 객체를 null로 설정한 뒤, 이 객체가 null일 때까지 tryGetInstance를 호출합니다.
- Supplier <T> creator는 initInvestor의 람다식 부분을 의미합니다. (return Investor.createDefault.. 부분)
제네릭과 Supplier를 활용하여 구현을 완료했지만, 과제를 제출했던 시점에서는 정확히 원리를 알지 못한 채 코드를 작성했습니다.
어떠한 경우에든지 코드를 작성할 때는 원리를 익힌 뒤 남에게 설명할 수 있을 때만 쓸 수 있어야 한다는 것을 다시 느꼈습니다. 대표적인 문제로는 createInstance에서 classType 인자가 필요 없습니다. initInvestor에서 Investor.class를 정의하지 않아도 람다식 내부에서 암묵적으로 클래스 타입을 나타낼 수 있기 때문입니다.
결론
Enum은 공부할 때마다 매번 고민하고 공부할 게 계속 늘어나는 것 같습니다. 특히 예외 메시지에서 상수에 대해 알아야 할 때의 대처 방법이 저 방식밖에 없을까라는 고민이 듭니다. 그럼에도 프리코스를 한 덕분에 이러한 고민들을 할 수 있던 것 같아 좋았습니다.
어느덧 마지막 미션을 개발 중에 있는데, 얼마 남지 않은 프리코스를 무사히 완료하고 마지막 후기도 이어서 작성해 보겠습니다 :)
틀린 점이 있다면 언제든지 지적해 주시면 감사하겠습니다!!
최종 제출
이곳에서 확인하실 수 있습니다.
'🚀 우아한테크코스 6기 지원 기록' 카테고리의 다른 글
[프리코스] 프리코스 4주차 후기 (크리스마스 프로모션 🎄) (1) | 2023.11.17 |
---|---|
[프리코스] 프리코스 2주차 후기 (자동차 경주 🚗) (0) | 2023.11.06 |
[프리코스] 프리코스 1주차 후기 (숫자 야구 ⚾️) (1) | 2023.10.31 |