Tiny Star
본문 바로가기
Dev Log/Kotlin

[Kotlin] 예외 처리 패턴: 사용자 입력을 '우아하게' 다시 받는 법 (ft. 재귀 함수)

by 별터 2026. 1. 2.

개발자가 가장 먼저 배우는 것은 '기능을 구현하는 법'이지만, 시니어 레벨로 갈수록 고민하는 것은 '예외를 처리하는 법'입니다. 특히 사용자와 상호작용하는 콘솔 프로그램이나 애플리케이션에서, 사용자가 실수로 잘못된 값을 입력했다고 해서 프로그램이 Exception을 뱉으며 종료(Crash)되어 버린다면, 그건 좋은 소프트웨어라고 할 수 없습니다.

 

우아한테크코스 프리코스 과제(로또)의 요구사항 중 하나는 "[ERROR]로 시작하는 에러 메시지를 출력하고, 해당 부분부터 입력을 다시 받으세요"입니다.

 

이 요구사항을 충족하기 위해 우리는 어떤 전략을 취해야 할까요? 오늘은 try-catch와 재귀 함수를 활용한 가장 코틀린스러운 해결책을 깊이 있게 다뤄보겠습니다.


1. 기존 방식의 한계: while 루프의 늪

 

보통 "될 때까지 반복한다"라는 말을 들으면 조건 반사적으로 while문을 떠올립니다. 자바(Java) 시절부터 이어져 온 전통적인 방식이죠.

 

❌ while문을 사용한 코드 예시

fun readTryCount(): Int {
    var tryCount: Int? = null
    while (true) { // 1. 무한 루프
        try {
            print("시도할 횟수는 몇 회인가요? : ")
            val input = Console.readLine()
            tryCount = input.toInt() // 2. 여기서 에러나면 catch로 이동
            break // 3. 성공하면 루프 탈출
        } catch (e: NumberFormatException) {
            println("[ERROR] 숫자를 입력해 주세요.")
            // 4. 다시 while 처음으로 돌아감
        }
    }
    return tryCount!! // 5. nullable 타입 언래핑 필요
}

 

무엇이 문제일까?

  1. 가변 변수(var) 사용: tryCount 값을 루프 밖으로 꺼내기 위해 var를 써야 합니다. 코틀린은 불변성(val)을 지향하는데, 이를 위배하게 됩니다.

  2. 강제 언래핑(!!): 초기값을 null로 설정해야 하므로, 나중에 반환할 때 !!를 쓰거나 스마트 캐스팅을 믿어야 하는 불안함이 남습니다.

  3. 복잡한 흐름제어: breakcontinue 등이 섞이면 코드를 읽는 흐름이 위아래로 널뛰게 되어 가독성을 해칩니다.


2. 코틀린스러운 해결책: 재귀 함수(Recursion) 활용

 

코틀린은 함수형 프로그래밍의 특징을 많이 가지고 있습니다. 그중 하나가 '표현식(Expression)'입니다. 코틀린에서는 try-catch 블록 자체가 값을 반환하는 표현식입니다.

 

이 점을 이용하면, 성공하면 값을 반환하고, 실패하면 나 자신을 다시 호출하여 기회를 얻는 재귀 패턴을 만들 수 있습니다.

 

재귀 함수를 사용한 코드 예시

fun readTryCount(): Int {
    return try {
        print("시도할 횟수는 몇 회인가요? : ")
        val input = Console.readLine()
        
        // 유효성 검증 로직 (여기서 실패하면 catch로 이동)
        require(input.toIntOrNull() != null) { "[ERROR] 숫자가 아닙니다." }
        require(input.toInt() > 0) { "[ERROR] 1 이상의 숫자여야 합니다." }
        
        // 성공 시 바로 반환 (Int 타입)
        input.toInt()
    } catch (e: IllegalArgumentException) {
        println(e.message) // 에러 메시지 출력
        
        // 🚀 핵심: 에러 발생 시 함수 자신을 다시 호출
        readTryCount()
    }
}

3. 심화 분석: 왜 이 방식이 더 좋을까?

 

① 불변성(Immutability) 확보

위 코드에서 var는 단 한 번도 사용되지 않았습니다. 모든 변수는 val이며, 함수는 계산된 '값' 자체를 반환합니다. 상태 변경으로 인한 사이드 이펙트(Side Effect)를 원천 차단할 수 있습니다.

 

② 가독성 (Readability)

코드가 입력 시도 → 검증 → (성공 시) 반환 / (실패 시) 재시도 라는 논리적 흐름을 그대로 따라갑니다. breakflag 변수를 신경 쓸 필요가 없습니다.

 

③ 확장성 (Reusability)

이 패턴은 어떤 입력 로직에도 그대로 복사해서 적용할 수 있습니다.

  • 자동차 이름 입력
  • 로또 구입 금액 입력
  • 당첨 번호 입력

4. 스택 오버플로우는 어떨까?

 

재귀 함수를 쓰면 스택 오버플로우(Stack Overflow)가 발생하지 않을까?라는 궁금증이 생깁니다.

함수가 종료되지 않고 계속 스택 메모리에 쌓이기 때문입니다.

 

하지만 결론부터 말하면, '사용자 입력(Console Input) 단계에서는 걱정할 필요가 업다'입니다.

  • 스택 오버플로우가 나려면 보통 수천~수만 번 이상의 재귀 호출이 필요합니다.
  • 사용자가 콘솔에 수천 번 연속으로 틀린 값을 입력할 확률은 0에 수렴합니다.
  • 알고리즘 문제 풀이(DFS/BFS)에서는 조심해야 하지만, 이런 UI 로직에서는 가독성을 챙기는 것이 훨씬 이득입니다.

결론

 

프로그래밍에서 예외 처리는 '귀찮은 작업'이 아니라 '프로그램의 완성도'를 결정짓는 핵심입니다. 지저분한 while문 대신, 코틀린의 표현식 특징과 재귀 함수를 활용해 보세요. 코드가 훨씬 간결하고 아름다워질 것입니다.