본문 바로가기
Kotlin

[Kotlin] 실제 사례로 배우는 자료구조 선택의 기술: 로또 번호 생성기 개발 이야기

by 중곰 2025. 3. 17.

 

 

안녕하세요!

오늘은 제가 개발한 로또 번호 생성기에서 경험한 자료구조 선택과 최적화 과정에 대한 이야기를 나누고자 합니다.

단순해 보이는 로또 번호 생성이라는 작업 속에서도 효율성과 유지보수성을 고려한 다양한 기술적 결정들이 숨어있었습니다.

1. 로또 번호 생성기의 요구사항

개발을 시작하기 전, 저는 다음과 같은 명확한 요구사항을 정의했습니다:

  • 1부터 45까지의 숫자 중 중복 없이 6개를 선택해야 합니다
  • 사용자가 직접 번호를 입력할 수 있는 수동 모드가 필요합니다
  • 자동으로 랜덤 번호를 생성하는 기능이 필요합니다
  • 최종 번호는 항상 정렬된 상태로 표시되어야 합니다

이러한 요구사항들은 단순해 보이지만, 어떤 자료구조를 선택하느냐에 따라 구현 방식과 성능이 크게 달라질 수 있습니다.

2. 자료구조 선택의 여정

2.1 Set을 선택한 이유

로또 번호를 저장하기 위한 자료구조로 Set을 선택했습니다

data class LottoState(
    val numbers: Set<Int> = emptySet(),
    val manualNumbers: Set<Int> = emptySet()
)

Set을 선택한 데에는 세 가지 핵심적인 이유를 적어봅니다.

 

첫째, 중복 방지입니다.

로또 번호는 본질적으로 중복을 허용하지 않습니다.

Set은 이러한 중복 방지를 자동으로 처리해주어 별도의 중복 검사 로직을 작성할 필요가 없었습니다.

 

둘째, 검색 효율성입니다.

번호 생성 과정에서 특정 번호가 이미 선택되었는지 확인하는 contains() 연산이 자주 사용됩니다.

Set은 이러한 연산을 O(1)의 시간 복잡도로 처리할 수 있어 매우 효율적입니다.

 

셋째, 의도의 명확성입니다.

코드를 읽는 개발자는 Set이라는 자료구조를 보는 순간 "이 데이터는 중복을 허용하지 않는다"라는 의도를 즉시 파악할 수 있습니다.

이는 코드의 가독성과 유지보수성을 높이는 데 기여합니다.

2.2 List가 필요한 이유

그러나 Set만으로는 모든 요구사항을 충족시킬 수 없었습니다.

특히 UI에 표시할 때는 정렬된 상태의 번호가 필요했습니다

val sortedNumbers: List<Int>
    get() = numbers.toList().sorted()

List를 사용하게 된 이유는 아래와 같다고 생각합니다.

 

첫째, 순서 보장입니다.

Set은 순서를 보장하지 않지만, List는 요소들의 순서를 유지합니다.

이를 통해 정렬된 상태의 번호를 보여줄 수 있었습니다.

 

둘째, 인덱스 접근입니다.

UI에서는 종종 특정 위치의 요소에 접근해야 하는 경우가 있습니다.

List는 인덱스를 통한 접근이 가능하여 이러한 요구사항을 쉽게 충족시킬 수 있었습니다.

 

셋째, 정렬 기능입니다.

List는 sorted() 함수를 제공하여 번호를 오름차순으로 정렬하는 작업을 간단히 수행할 수 있었습니다.

이처럼 내부적으로는 Set을 사용하고, 외부 API로는 정렬된 List를 제공하는 방식을 통해 두 자료구조의 장점을 모두 활용할 수 있었습니다.

 

3. 무한 시퀀스와 최적화 전략

3.1 무한 시퀀스를 활용한 접근

처음에는 함수형 프로그래밍의 강력한 도구인 무한 시퀀스를 활용하여 랜덤 번호를 생성했습니다

private fun generateRandomNumbers() {
    val currentManualNumbers = _uiState.value.manualNumbers
    if (currentManualNumbers.size >= 6) return

    val remainingCount = 6 - currentManualNumbers.size
    val newNumbers = generateSequence { (1..45).random() }
        .distinct()
        .filter { it !in currentManualNumbers }
        .take(remainingCount)
        .toSet()

    _uiState.update { currentState ->
        currentState.copy(
            numbers = currentManualNumbers + newNumbers
        )
    }
}

 

이 접근 방식은 다음과 같은 장점이 있다고 생각합니다.

 

첫째, 메모리 효율성입니다.

무한 시퀀스는 실제로 필요한 만큼만 요소를 생성하기 때문에 메모리를 효율적으로 사용할 수 있었습니다.

 

둘째, 지연 평가입니다.

시퀀스는 실제로 결과가 필요할 때까지 계산을 미루는 지연 평가 방식을 사용합니다.

이는 불필요한 계산을 방지하여 성능을 향상시킬 수 있습니다.

 

셋째, 유연성입니다.

무한 시퀀스를 사용하면 필요한 만큼의 요소만 생성할 수 있어, 다양한 상황에 대응하기 쉬웠습니다.

3.2 최적화된 버전

그러나 무한 시퀀스 방식은 경우에 따라 예측하기 어려운 성능 특성을 가질 수 있습니다.

따라서 더 예측 가능한 최적화 버전을 구현했습니다:

private fun generateRandomNumbers() {
    val currentManualNumbers = _uiState.value.manualNumbers
    if (currentManualNumbers.size >= 6) return
    
    val remainingCount = 6 - currentManualNumbers.size
    val availableNumbers = (1..45).filter { it !in currentManualNumbers }
    val newNumbers = availableNumbers.shuffled().take(remainingCount).toSet()
    
    _uiState.update { currentState ->
        currentState.copy(
            numbers = currentManualNumbers + newNumbers
        )
    }
}

 

이 접근 방식의 장점은 다음과 같다고 생각합니다.

 

첫째, 예측 가능성입니다. 가용 번호 목록을 먼저 만들고 셔플링하는 방식은 실행 시간이 더 예측 가능합니다. 무한 시퀀스의 경우, 운이 나쁘면 많은 중복 번호가 생성되어 예상보다 오래 걸릴 수 있습니다.

둘째, 단순성입니다. 최적화된 버전은 코드가 더 직관적이고 이해하기 쉽습니다. 복잡한 함수형 연산자 체인 대신 명확한 단계별 접근을 사용했습니다.

셋째, 안정성입니다. 무한 시퀀스는 이론적으로 무한 루프에 빠질 가능성이 있지만, 최적화된 버전은 그런 위험이 없습니다.

4. 결론 및 배운 점

이번 로또 번호 생성기 개발 과정을 통해 저는 자료구조 선택의 중요성을 다시 한번 실감했습니다. 단순히 "동작하는 코드"를 넘어, "효율적이고 유지보수하기 좋은 코드"를 작성하기 위해서는 자료구조의 특성을 깊이 이해하고, 이를 적절히 활용하는 능력이 필요하다는 것을 배웠습니다.

특히 Set과 List의 조화로운 사용은, 각 자료구조의 장단점을 이해하고 이를 적절히 활용한 좋은 예시가 되었습니다. 내부적으로는 중복 방지와 빠른 검색이 가능한 Set을 사용하고, 외부로는 정렬과 인덱싱이 가능한 List를 제공함으로써 두 자료구조의 장점을 모두 활용할 수 있었습니다.

또한 무한 시퀀스와 최적화된 버전의 비교를 통해, 같은 결과를 내더라도 다양한 접근 방식이 가능하다는 것을 다시 한번 생각해볼 수 있었습니다. 함수형 프로그래밍의 우아함과 명령형 프로그래밍의 명확성 사이에서 적절한 균형을 찾는 과정은 매우 흥미로웠습니다.

단순한 기능 구현을 넘어, 효율성, 유지보수성, 확장성을 고려한 설계의 중요성을 항상 염두에 두어야 한다는 것을 다시 한번 깨달았습니다.

개발자로서의 성장은 이러한 작은 프로젝트에서의 고민과 결정들이 모여 이루어진다고 생각합니다.

 

글을 읽어주셔서 감사합니다.

반응형

'Kotlin' 카테고리의 다른 글

[ Kotlin ] Map & Pair  (3) 2021.12.30