우아한테크코스 5기 프리코스

우아한테크코스 프리코스 3주차 후기 & 공부한 부분

BE_성하 2022. 11. 16. 14:41
반응형

최종 3주차 미션 코드

우선 나의 최종 로또 게임 코드 링크는 다음과 같다.

https://github.com/KSH-beginner/wooteco5th-java-lotto/tree/KSH-beginner

 

GitHub - KSH-beginner/wooteco5th-java-lotto: 로또 미션을 진행하는 저장소

로또 미션을 진행하는 저장소. Contribute to KSH-beginner/wooteco5th-java-lotto development by creating an account on GitHub.

github.com


3주차 진행 후기

이번 3주차는 처음에 시간을 많이 써서 일요일 전에 완성하고,

너무 힘을 많이 쓴 나머지 그 후에 리팩토링을 거의 진행하지 않아서 

끝나고 나니 내 코드인데도 오랜만에 본 것처럼 어색하게 느껴졌고,

특정 부분의 예외 처리를 놓쳐서 아쉬웠다.

다음 주차는 지치지 않고 끝까지 리팩토링하면서 진행해야겠다고 다짐했다.

 

이번 주차에서는 ENUM과 try-catch문에 대해 더 깊게 알게 된 것 같다.


3주차 진행 전 피드백

이번 3주차도 마찬가지로 공통 피드백 문서를 본 후에

내가 무엇을 지키지 못했는 가를 자체 판단한 후에 설계를 시작했다.

 

2주차 공통 피드백 중 내가 지키지 못한 것들

1. 기능 목록을 재검토하자.

1. 기능 목록을 클래스 설계와 구현, 함수 설계와 구현과 같이 너무 상세하게 작성하지 않기
2. 정상적인 상황과 함께 예외적인 상황도 기능 목록에 추가하기(처음부터 찾기 힘들다면 구현하면서 찾기)

👉 2번은 잘 지켰지만, 1번을 잘 지키지 못 했다.

클래스 설계/구현, 함수 설계/구현 같이 상세하게 작성하면 안되는 이유가,

클래스 이름, 함수(메소드) 시그니처와 반환값은 언제든지 변경될 수 있기 때문이다.

 

예를 들어 RandomUtil이라는 클래스가 있고, RandomUtil에 사용한 함수를 기능 목록에 작성했을 때,

해당 함수에 대한 것이 변경되면 기능 목록도 수정해야 하는 것이다.

따라서, 기능 목록에는 클래스별 기능, 클래스별 메소드에 대한 것을 적지 않고, 추상적인 기능 위주로 적자!

 

2. 기능 목록을 처음부터 완성하지 말고, 업데이트해가자!

👉 2주차 설계 시에 처음 설계할 때 기능 목록을 완성하고, 그 기능을 기반으로 구현을 하려고 했었다.

그러다보니 처음부터 기능 목록을 완벽하게 정리해야겠다는 부담을 가졌고, 시간도 더 오래 걸렸던 것 같다.

이 부분이 공통 피드백인 만큼 3주차에서는 기능이 생각 안나면 넘어가고 구현 시에 업데이트하자!

 

3. 테스트를 작성하는 이유를 내 경험을 토대로 작성해보기

 - 테스트를 작성하는 이유

- 1.  기능을 구현하면서, 이 기능이 잘 작동하는지,

내가 짠 프로덕션 코드가 오류 없이 잘 동작하는지를

알아보기 위해 테스트가 필요하다.

예를 들면, 테스트 코드가 없을 때는 두 수를 더하는 기능을 만들고 이 기능이 잘 동작하는지 알아보기 위해선

그 기능을 호출하고 콘솔에 찍히도록 애플리케이션을 실행해서 콘솔에 두 수를 더한 값이 나오는지 봐야 했다.

이러한 과정이 반복되게 되면 결국 생산성이 떨어지고, 기능이 잘 동작하는지 알아보는 코드도

결국 1회용이라서 삭제되므로 이후에 그 기능이 동작하는지 알려면 또 콘솔에 찍는 코드를 짜야한다.

 

- 2. 협업 시 다른 개발자가 테스트 코드를 보고 이 기능이 잘 작동하는지 알 수 있다.

해당 기능을 만든 개발자는 테스트 코드 없이 콘솔에 찍는 코드를 짜서 기능 동작이 잘 되는지 확인했다고 해도,

결국 커밋-푸쉬 시에는 그 코드를 지우기 때문에 다른 개발자들은 해당 기능이 잘 작동하는지 알 수 없다.

 

 

💡 이렇게 기능 목록에 대한 피드백을 바탕으로 3주차 기능 목록 설계를 시작했고,

테스트의 필요성에 대해 이해하게 되었다.


기능 목록 설계 부분은 2주차 설계 방식과 거의 똑같았어서 생략하고, 

미션을 진행하면서 공부한 부분을 정리해보려고 한다.

 

공부한 부분

1. 중요한 로직이 있는 클래스(Model, Controller, Service)에서 검증을 하도록 하자.

View에서는 사용자의 입력 값을 전달만 하는 역할이므로, 잘못된 값이 들어와도 전달해야한다고 이해했다.

즉, View 단에서 검증하지 않고 Model이나 Controller, Service에서 잘못된 값을 검증하도록 한다.

또한, 단순 랜덤 수를 생성하는 객체에서도 생성하는 기능만 수행하도록 하고,

그 생성한 수에 대해서는 그 랜덤 수를 사용하는 Model, Service, Controller쪽에서 검증하도록 하자.

 

2. stream().count()

로또 번호가 중복되는 지 검증하는 메소드 구현 시 기존에는 다음과 같이 구현했다.

private boolean isNumbersDuplicate(List<Integer> numbers) {
    int numbersDistinctSize = numbers.stream()
            .distinct()
            .collect(Collectors.toList())
            .size();
    return numbersDistinctSize != CORRECT_NUMBERS_SIZE;
}

이렇게 구현했었는데, IDE에서 리팩토링을 추천해줘서 실행했더니, count()로 리팩토링이 되었다.

private boolean isNumbersDuplicate(List<Integer> numbers) {
    long numbersDistinctSize = numbers.stream()
            .distinct().count();
    return numbersDistinctSize != CORRECT_NUMBERS_SIZE;
}

원래는 distinct한 것을 collect(Collectors.toList())로 새로운 리스트로 만든 후 그 리스트의 사이즈를

받는 것으로 구현했었는데, 새로운 리스트를 만들 필요 없이 distinct()한 상태에서 count()를 하는 것이

더 간단한 코드였다.

Stream의 count()를 사용해본 적이 없어서 존재 자체를 몰랐었는데, IDE 리팩토링 기능을 통해 알게 되었다.

앞으로는 노란 줄이 뜨면 꼭 ⌥+Enter로 IDE의 리팩토링 기능을 사용해봐야겠다.

3. Collection 자료 구조를 Getter로 외부에 반환할 때는 Collections.unmodifiedXXX 를 사용

EX) List를 Getter로 외부에 반환할 때

List<Integer> numbers;

public List<Integer> getNumbers() {
    return numbers;
} (x)

public List<Integer> getNumbers() {
    return Collections.unmodifiableList(numbers);
} (o)

👉 처음 방식처럼 그냥 리스트를 리턴하게 되면 외부 사용자가 리스트의 값을 변경할 수도 있다.

.getNumbers().remove(1);

👉 이렇게 리스트를 받은 후 remove하게 되면 List의 1번 인덱스가 삭제되게 된다.

따라서, Collections.unmodifiableList()로 값을 수정 못하고 반환받게만 하도록 해야한다.

Collections.unmodifiableList()을 사용하면, 리스트 요소를 바꾸려고 할 때 컴파일 오류가 발생한다.

UnsupportedOperationException 예외가 발생하게 된다.

 

4. if문 ENUM으로 줄이기

public int decideWinningRank(List<Integer> lottoWinningNumbers, int bonusNumber) {
    List<Integer> purchaseLottoNumbers = lotto.getNumbers();
    int winningRank = 0;
    long purchaseNumbersMatchWinningNumbersCount = getPurchaseNumbersMatchWinningNumbersCount(purchaseLottoNumbers, lottoWinningNumbers);
    if (purchaseNumbersMatchWinningNumbersCount == 3) {
        winningRank = 5;
    }
    if (purchaseNumbersMatchWinningNumbersCount == 4) {
        winningRank = 4;
    }
    if (purchaseNumbersMatchWinningNumbersCount == 5) {
        if (isPurchaseNumbersMatchBonusNumber(purchaseLottoNumbers, bonusNumber)) {
            winningRank = 2;
        }
        if (!isPurchaseNumbersMatchBonusNumber(purchaseLottoNumbers, bonusNumber)) {
            winningRank = 3;
        }
    }
    if (purchaseNumbersMatchWinningNumbersCount == 6) {
        winningRank = 1;
    }
    return winningRank;
}

👉 로또 당첨 번호와 보너스 번호를 파라미터로 받아서 일치하는 횟수를 비교하여

등수를 산정하는 기능을 if문으로 구현했다.

이렇게 if 문으로 조건 하나하나를 작성하니, 미션 요구사항인

함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다. 를 지키기가 힘들었다.

또한 운 좋게 등수가 적어서 if문의 길이가 작아져서 15라인을 넘지 않았더라도, 등수가 얼마든지

추후에 늘어날 수 있기 때문에 좋은 설계가 아니라고 생각했다.

그때 마침, 이번 주차에 추가된 요구사항이 떠올랐다! Java Enum을 적용한다.

이전에 ENUM을 많이 사용해보지 않아서 익숙하진 않았지만, 등수는 충분히 ENUM으로 구현 가능해보였다.

 

구현 전에 이전에 대충 알았던 ENUM을 공부해보고 새롭게 알게 된 점은,

ENUM에도 로직이 들어갈 수 있다!는 점이었다.

이전에 ENUM을 사용할 때는 단순하게 여러 나열된 변수를 정리해주는 클래스로 알고 있었다.

그래서 단순하게 상수로 변수들을 정리하고, 더 나아가면 해당 상수들이 가지는 필드를 정의하는 정도까지로만

ENUM을 사용했었다.

하지만, 참고한 우아한형제들 기술블로그의 ENUM 활용기를 살펴보니,

ENUM안에서 ENUM의 로직을 처리하는 메소드를 사용하고 있었다.

 

따라서, 등수를 ENUM으로 만들고, 등수를 정하는 로직도 ENUM에서 구현했다.

public enum LottoWinningRank {
    NO_RANK(List.of(0L, 1L, 2L), "0"),
    FIRST(List.of(6L), "2,000,000,000"),
    SECOND(List.of(5L), "30,000,000"),
    THIRD(List.of(5L), "1,500,000"),
    FOURTH(List.of(4L), "50,000"),
    FIFTH(List.of(3L), "5,000");
    
    ...
    
    public static LottoWinningRank decideLottoWinningRank(long purchaseNumbersMatchWinningNumbersCount,
                                                          boolean isPurchaseNumbersMatchBonusNumber) {
        List<LottoWinningRank> lottoWinningRanks = Arrays.stream(LottoWinningRank.values())
                .filter(lottoWinningRank ->
                        lottoWinningRank.getPurchaseNumbersMatchWinningNumbersCounts()
                                .contains(purchaseNumbersMatchWinningNumbersCount))
                .collect(Collectors.toList());
        // 2등, 3등에 대해서만 보너스 번호 일치/불일치 판단
        if (lottoWinningRanks.contains(SECOND) && lottoWinningRanks.contains(THIRD)) {
            decideLottoWinningRankSecondOrThird(isPurchaseNumbersMatchBonusNumber, lottoWinningRanks);
        }
        return lottoWinningRanks.get(LOTTO_WINNING_RANK_INDEX);
    }
}

 이렇게 ENUM을 사용하고, ENUM 안에서 ENUM의 변수를 사용하는 로직을 구현하니,

불필요한 if문이 줄게 되었고, 로직을 구현하는 Service단의 코드도 줄게 되었다.

앞으로 나오는 4주차 미션이나, 추후 프로그램 개발 시에도 ENUM을 적재적소에 잘 적용해야겠다.

 

4. TDD를 고집할 필요 없다.

결국 미션의 최종 결과는 프로그램을 구현하는 것이다.

TDD는 그 최종 결과로 가는 길에 사용하는 도구일 뿐이다.

TDD라는 도구가 어떨 때는 매우 유용해서 지름길로 이끌 수도 있지만,

자칫 TDD를 고집하게 되면 지름길이 아니라 유턴을 할 수도 있다.

 

이번 미션의 나의 경우에는 TDD로 진행을 하다가, ENUM을 사용해서 구현하는

로또 당첨 등수를 구하는 기능에서 TDD의 첫 과정인 실패하는 테스트 케이스를 짰다가,

이 코드조차 어떻게 짜야할지 막막해서 먼저 프로덕션 코드 구현을 진행했었다.

만약, 프로덕션 코드 없이 테스트 코드 먼저 짰다면 훨씬 시간이 더 오래 걸렸을 것이다.

 

5. try-catch문 사용

예외 처리 코드를 작성하고, 이번 3주차 미션의 예외 테스트를 돌렸는데 실패했다.

해당 예외 테스트 코드는 다음과 같다.

@Test
void 예외_테스트() {
    assertSimpleTest(() -> {
        runException("1000j");
        assertThat(output()).contains(ERROR_MESSAGE);
    });
}

👉 main()의 args에 “1000j”가 입력되고, 콘솔에 출력된 로그에 ERROR_MESSAGE([ERROR])가

포함되어 있으면 테스트가 성공한다.

 

내가 습관적으로 짰던 예외 처리 코드는 다음과 같다.

// * LottoGameController
public void inputLottoPurchaseAmount() {
    ...
    lottoIssueCount = lottoGameService.getLottoIssueCount(lottoPurchaseAmount);
    ...
}

// * LottoGameService
public int getLottoIssueCount(String lottoPurchaseAmount) throws IllegalArgumentException {
    validateLottoIssueCount(lottoPurchaseAmount);
        return Integer.parseInt(lottoPurchaseAmount) / A_LOTTO_PRICE;
}

public void validateLottoIssueCount(String lottoPurchaseAmount) {
    if (isLottoPurchaseAmountNotDigit(lottoPurchaseAmount)) {
        throw new IllegalArgumentException(ERROR_MESSAGE_PREFIX + LOTTO_PURCHASE_AMOUNT_NOT_DIGIT_EXCEPTION_MESSAGE);
    }
    ...
}

👉 LottoGameController에서 LottoGameService의 getLottoIssueCount()를 호출할 때,

파라미터로 lottoPurchaseAmount를 넘기고, 해당 파라미터를 validateLottoIssueCount()로

검증한 후에 로직을 진행한다.

이때, validateLottoIssueCount()에서 조건에 맞지 않는 값이면 예외를 발생시키는데,

이 부분에서 테스트가 실패했다.

 

단순하게 처음 생각해봤을 때는, 테스트 코드에서 assertThrownBy()로 예외를 테스트하는 것이 아니라,

assertThat()으로 예외 시 발생하는 출력 메시지를 테스트하는 것이라 실패했다고 판단했다.

미션의 요구사항도 자세히 읽어보니 에러 메세지를 “출력”하라고 되어 있었다.

사용자가 잘못된 값을 입력할 경우 IllegalArgumentException를 발생시키고, 
"[ERROR]"로 시작하는 에러 메시지를 출력 후 종료한다.

 

따라서, 처음에는 단순히 예외 발생 전에 에러 메세지를 출력하고 예외를 발생시키면 될 것 이라고 예상했다.

// * LottoGameService
public int getLottoIssueCount(String lottoPurchaseAmount) throws IllegalArgumentException {
    validateLottoIssueCount(lottoPurchaseAmount);
		return Integer.parseInt(lottoPurchaseAmount) / A_LOTTO_PRICE;
}

public void validateLottoIssueCount(String lottoPurchaseAmount) {
    if (isLottoPurchaseAmountNotDigit(lottoPurchaseAmount)) {
				System.out.println(ERROR_MESSAGE_PREFIX + LOTTO_PURCHASE_AMOUNT_NOT_DIGIT_EXCEPTION_MESSAGE);
        throw new IllegalArgumentException(ERROR_MESSAGE_PREFIX + LOTTO_PURCHASE_AMOUNT_NOT_DIGIT_EXCEPTION_MESSAGE);
    }
    ...
}

👉 이렇게 throw new 전에 에러 메세지 출력 코드를 추가했는데, 똑같이 테스트가 실패했다.

 

구입금액을 입력해주세요.
[ERROR] 로또 구입 금액은 숫자여야합니다.

java.lang.IllegalArgumentException: [ERROR] 로또 구입 금액은 숫자여야합니다.

콘솔에 로그가 이렇게 찍혔다.

의문이 들었다. 분명히 콘솔에 [ERROR]가 포함된 에러 메세지를 출력하고

IllegalArgumentException이 발생했는데, 왜 테스트는 실패하는 것일까?

 

이유를 곰곰이 생각해보니 테스트 코드는 assertThrownBy()로 예외 발생 시가 아니라,

assertThat()으로 예외를 발생하지 않았을 때 테스트가 성공하는 코드였다.

따라서, 예외를 발생시키고 try-catch를 사용하여 catch로 예외를 잡아서 처리를 해주면 된다고 생각했다.

// * LottoGameService
public int getLottoIssueCount(String lottoPurchaseAmount) {
		try {
                validateLottoIssueCount(lottoPurchaseAmount);
		} catch (IlleagalArgumentException e) {
                System.out.println(e.getMessage());
		}
    return Integer.parseInt(lottoPurchaseAmount) / A_LOTTO_PRICE;
}

public void validateLottoIssueCount(String lottoPurchaseAmount) {
    if (isLottoPurchaseAmountNotDigit(lottoPurchaseAmount)) {
                System.out.println(ERROR_MESSAGE_PREFIX + LOTTO_PURCHASE_AMOUNT_NOT_DIGIT_EXCEPTION_MESSAGE);
        throw new IllegalArgumentException(ERROR_MESSAGE_PREFIX + LOTTO_PURCHASE_AMOUNT_NOT_DIGIT_EXCEPTION_MESSAGE);
    }
    ...
}

하지만, 이렇게 하니 NumberFormatException이 발생했다.

찾아보니 맞지 않는 형식으로 사칙연산 시에 발생하는 예외였는데,

Integer.parseInt(lottoPurchaseAmount) / A_LOTTO_PRICE 부분에서 발생한 것 같았다.

try-catch문을 사용하면, 사용한 예외가 발생하여 catch문이 실행돼도

바로 프로그램이 종료되는 것이 아니라 catch문 이후의 코드까지 실행 후에 종료되기 때문에

Integer.parseInt(lottoPurchaseAmount) / A_LOTTO_PRICE를 실행하여 예외가 발생한 것이다.

 

따라서, try-catch문을 LottoGameService에서 사용하는 것이 아니라,

LottoGameService.getLottoIssueCount()를 호출하는

LottoGameController에서 사용하여 해결하려고 했다.

// * LottoGameController
public void inputLottoPurchaseAmount() {
    try {
        lottoPurchaseAmount = inputView.inputLottoPurchaseAmount();
        lottoIssueCount = lottoGameService.getLottoIssueCount(lottoPurchaseAmount);
    } catch (IllegalArgumentException e) {
        System.out.println(e.getMessage());
    }
}

이렇게 하면, Service에서는 연산 전에 검증을 거칠 때 IllegalArgumentException이 발생해서

연산 과정을 수행하지 않고 예외를 가지고 호출했던 Controller로 간다.

이때 Controller의 try-catch문에서 예외 처리를 하기 때문에 에러 메세지가 출력되고

프로그램이 종료되어 정상적으로 테스트 코드가 작동했다.

하지만, 에러 메시지가 출력되고 즉시 종료되는 것이 아니라 모든 로또 게임 단계를 거친 후에 종료됐다.

👉 이러한 이유는 try-catch문을 로또 구매 단계 메소드에만 적용했기 때문이었다.

로또 구매 단계에서 try-catch문으로 예외 메세지를 출력한 후에,

다른 단계들은 정상적으로 실행되기 때문에 그 즉시 종료되지 않았던 것이었다.

 

모든 단계들에 각각 try-catch문을 적용하면 결국 한 단계가 실행된 후에

그 후 단계를 실행하면서 예외를 처리하기 때문에 즉시 종료되지 않는 것은 똑같았다.

따라서, 모든 단계들에 한번에 try-catch문을 적용해서 해결하면 됐다.

모든 단계를 실행하는 메소드인 playGame()에 try-catch를 적용했다.

public void playGame() {
    try {
        inputLottoPurchaseAmount();
        printPurchaseLottoCount();
        printIssuedLottoNumbers();
        setWinningNumberAndBonusNumber();
        decidePurchaseLottosRank();
        printGameResult();
    } catch (IllegalArgumentException e) {
        System.out.println(e.getMessage());
    }
}

👉 try안의 어느 한 단계에서 예외가 발생하면 try의 코드는 건너뛰고 catch문으로 가기 때문에,

에러 메세지를 출력 후 실행될 단계가 없어서 프로그램이 종료된다.

 

 

💡 테스트 코드 트러블 슈팅을 통해 잘 사용하지 않았던 try-catch문과 예외 처리에 대해 지식을 쌓게 되었다.

  1. try-catch문이 있으면 예외 처리를 해서 그 즉시 프로그램이 종료되지 않는다.
  2. 메소드에서 예외가 발생했을 시 해당 메소드를 호출한 곳으로 예외를 가져가서 그 곳에서 예외 발생

다음과 같은 미션 요구 사항과 예외 테스트 코드에서 아래의 생각 때문에 트러블 슈팅이 힘들었다.

사용자가 잘못된 값을 입력할 경우 IllegalArgumentException를 발생시키고, 
"[ERROR]"로 시작하는 에러 메시지를 출력 후 종료한다.
1. 테스트 코드를 보니, 예외가 발생해서 프로그램이 종료되면 안 되네?
-> 예외로 프로그램 종료를 막기 위해 try-catch문 사용

2. try-catch문을 사용해서 예외로 프로그램이 종료되는 것을 막았는데, 그 이후 코드가 계속 실행되네?
-> 어떻게 즉시 종료 시키지???

👉 이렇게 try-catch를 사용하고, catch문을 빠져나왔을 때 즉시 종료하는 방법이 없을까 계속 고민했다.

계속 “즉시 종료”라는 것에 꽂혀서 되돌이표만 반복했었다.

결국 해결 방법은 즉시 종료가 아니었다.

try-catch문 이후에 실행할 코드가 없게 해서 자연스럽게 프로그램이 종료되게 하는 것이 해결책이었다.

게임을 실행하는 기능에 try-catch를 넣어서 어떤 단계든 예외가 발생하면

catch문 이후에 실행되는 코드가 없어서 종료가 되게 했다.

이렇게 하니, 프로그램 실행 면에서나 코드 가독성 면에서나 효과적이라고 생각했다.

이러한 방법은 생각하지 못했었는데, 다음에도 유용하게 사용해볼 것 같다!!


다음 주차에 적용해볼 부분

1.  기능 목록 설계 시 Controller(실행 단계) 기준으로 분류하여 설계해보기

## 기능 목록

### [로또 구입 금액 입력 단계]
 - ...

### [로또 구매 개수 출력 단계]
 - ...

### [로또 번호 발행 단계]
 - ...

...

👉 애플리케이션이 컨트롤러를 실행시켜 게임을 진행하기 때문에,

컨트롤러의 메소드 단계별로 기능을 정리하는 것이 게임 진행 순서와 맞고, 가독성이 좋아보였다.

이번 주차에서는 처음 설계 때 다른 식으로 기능을 작성했다가 이렇게 고쳤는데,

다음 주차부터는 기능 목록 설계할 때부터 컨트롤러 기준으로 분류해서 작성해봐야겠다.

 

2.  커밋 수를 줄이기

프리코스를 진행하면서 주차별로 커밋 수가 항상 100개가 넘었었는데,

다른 크루분들의 커밋 수는 적은 것을 보고 줄여봐야겠다고 생각했다.

그래서 생각한 것이, 이전에는 기능 목록에 적힌 핵심 기능 말고도 메소드 하나 하나를 만들 때마다 커밋했었다.

이러한 이유가 커밋 수가 늘어나는 이유라고 생각하여

커뮤니티에 메소드 하나 하나를 다 커밋하는지 물어보고, 핵심 메소드(기능)만 커밋한다는 것을 알았다.

다음 주차부터는 기능 목록에 적힌 핵심 기능들만 커밋하도록 해서 커밋 수를 줄여봐야겠다.


Reference

ENUM

https://techblog.woowahan.com/2527/

 

Java Enum 활용기 | 우아한형제들 기술블로그

{{item.name}} 안녕하세요? 우아한 형제들에서 결제/정산 시스템을 개발하고 있는 이동욱입니다. 이번 사내 블로그 포스팅 주제로 저는 Java Enum 활용 경험을 선택하였습니다. 이전에 개인 블로그에 E

techblog.woowahan.com

자바 예외 처리

https://catsbi.oopy.io/92cfa202-b357-4d47-8de2-b9b3968dfb2e

 

예외처리(exception handling)

목차

catsbi.oopy.io

 

반응형