코루틴으로 로또번호 당첨 확인하기

이번 포스팅에서는 코루틴을 사용해서 로또번호의 당첨을 확인하는 법에 대해 알아보도록 하겠습니다.

# 당첨번호 조회하기

이전에 로또번호 생성기 만들기강의에서 로또번호를 생성하는 앱을 만들어보았는데요, 여기서는 이 앱에 코루틴을 이용해서 당첨여부를 확인하는 기능을 추가하도록 하겠습니다. 코루틴에 대한 이론설명은 알기쉬운 코루틴 이론 강의를 참조하시기 바랍니다.

금주의 로또 당첨번호는 동행복권 홈페이지에서 확인할 수 있습니다. 다른 회차의 당첨번호는 홈페이지의 회차별 당첨번호 페이지에서 확인할 수 있게 되어 있습니다.

그런데 이렇게 홈페이지에 접속하지 않고도 당첨번호는 API로 조회할 수 있습니다. 다음 주소의 “회차"라고 되어 있는 부분에 확인하고 싶은 회차의 숫자를 넣으시면 됩니다.

1
https://dhlottery.co.kr/common.do?method=getLottoNumber&drwNo="회차"

예를 들면 968회차의 응답은 다음과 같은 json 형식으로 돌아옵니다. totSellamnt는 총 판매액, returnValue는 API응답 성공여부, drwNoDate는 추첨일, firstWinamnt는 1등상금, drwNo는 회차, drwtNo1~6이 당첨번호이고, bnusNo가 보너스번호입니다.

1
{"totSellamnt":95924229000,"returnValue":"success","drwNoDate":"2021-06-19","firstWinamnt":1667729683,"drwtNo6":39,"drwtNo4":14,"firstPrzwnerCo":13,"drwtNo5":24,"bnusNo":33,"firstAccumamnt":21680485879,"drwNo":968,"drwtNo2":5,"drwtNo3":12,"drwtNo1":2}

# Dependency 확인

자 그러면 drwtNo1~6, bnusNo의 값을 추출해서 나의 값과 비교하면 되겠네요. 당첨번호는 네트워크에서 백그라운드로 받아와야 하기 때문에 코루틴으로 처리하도록 하면 되겠고, 받아온 json으로부터 값을 추출하는데에는 gson 라이브러리를 사용하면 될 것 같습니다. gson의 사용법에 대해서는 SharedPreferences를 json 포맷으로 관리하기 강의에서 다루었으니 그것을 참고하시면 되겠습니다.

1
2
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'
implementation 'com.google.code.gson:gson:2.8.6'

# 네트워크로 당첨번호 받아오기

그럼 네트워크로 당첨번호를 받아오는 getLottoNumbers 함수와, 두 값을 비교하는 whatIsRank 함수를 만들도록 하겠습니다.

getLottoNumbers는 전달받은 API 주소에 조회를 요청하고 반환값을 처리합니다. 이 함수는 네트워크를 사용해야 하므로 코루틴 안에서 사용하기 위해 suspend 키워드를 붙여줍니다. API는 단순한 GET 요청이므로 소켓을 만들지 않고 간단하게 URLreadText로 값을 읽어오도록 하였습니다.

API 주소와 결합할 round에는 내 번호와 비교할 당첨번호의 회차를 넣습니다. 읽어온 값의 returnValuesuccess이면 drwtNo1~6, bnusNo 값을 추출하여 배열에 모으도록 하였습니다. 마지막에는 당첨번호를 확인할 회차를 추가하였습니다.

 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
private suspend fun getLottoNumbers(): ArrayList<Int> {
    val round = "968"
    val url = "https://dhlottery.co.kr/common.do?method=getLottoNumber&drwNo=$round"

    val lottoNumbers = ArrayList<Int>()
    try {
        val response = URL(url).readText()
        val jsonObject = JsonParser.parseString(response).asJsonObject
        val returnValue = jsonObject.get("returnValue").asString

        if (returnValue == "success") {
            for (i in 1..6) {
                val lottoNumber = jsonObject.get("drwtNo$i").asInt
                lottoNumbers.add(lottoNumber)
            }
            val bonusNumber = jsonObject.get("bnusNo").asInt
            lottoNumbers.add(bonusNumber)
            lottoNumbers.add(round.toInt())
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }

    return lottoNumbers
}

# 내 번호와 당첨번호 비교하기

다음은 내 번호와 당첨번호를 비교하는 함수를 만듭니다. 로또의 규칙은 표의 내용과 같이 되어 있습니다. 그래서 whatIsRank에서는 내 로또번호와 당첨번호를 전달받아 처음 6개까지의 숫자를 비교해 일치한 갯수를 세고, 일치한 번호의 갯수가 5개이면 추가로 보너스번호인 7번째 번호가 일치하는지 확인하도록 하면 됩니다.

순위 당첨내용
1 6개 번호 모두 일치
2 5개 번호 일치 + 나머지 1개가 보너스 번호와 일치
3 5개 번호 일치
4 4개 번호 일치
5 3개 번호 일치
 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
private suspend fun whatIsRank(lottoNumbers: ArrayList<Int>, winningNumbers: ArrayList<Int>): String {
    // https://namu.wiki/w/%EB%A1%9C%EB%98%90%206/45#s-4
    var matchCount = 0
    for (i in 0..5) {
        if (lottoNumbers.contains(winningNumbers[i])) {
            matchCount += 1
        }
    }

    return if (matchCount == 6) {
        "1등"
    } else if (matchCount == 5) {
        if (lottoNumbers.contains(winningNumbers[6])) {
            "2등"
        } else {
            "3등"
        }
    } else if (matchCount == 4) {
        "4등"
    } else if (matchCount == 3) {
        "5등"
    } else {
        "낙첨"
    }
}

xml에는 비교결과를 보여주는 텍스트뷰 하나를 추가하도록 하겠습니다.

1
2
3
4
5
6
7
8
<TextView
    android:id="@+id/tv_winning"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="20dp"
    android:gravity="center"
    android:text="당첨결과"
    android:textAppearance="@style/TextAppearance.AppCompat.Large" />

# 코루틴으로 작업 처리하기

그러면 번호생성버튼을 눌렀을 때 생성된 번호를 당첨번호와 비교하는 코루틴을 추가하겠습니다. CoroutineScope로 스코프를 정의하고, 네트워크에서 값을 가져올 것이기 때문에 DispatchersIO로 설정합니다. 그리고 작업 후에 값을 반환할 필요가 없기 때문에 빌더는 launch를 사용합니다.

작업은 getLottoNumbers에서 얻어진 결과값를 whatIsRank에 사용해야 하기 때문에 winningNumbersasync로 자식 코루틴을 만듭니다. whatIsRank에 전달할때는 Deferred 반환값을 끝까지 처리하여 ArrayList를 전해주기 위해 await를 사용합니다. 최종적으로 텍스트뷰에 표시할 텍스트를 만들때도 await를 사용해서 처리가 모두 종료될 때까지 기다리도록 합니다.

마지막으로 text를 텍스트뷰에 표시해야 하는데 이 부분은 UI를 건드리는 부분이라 메인스레드에서 작업이 이루어져야 하기 때문에 작업할 스레드를 withContext로 전환한 뒤 당첨결과를 표시하면 되겠죠.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
binding.generateButton.setOnClickListener {
    val lottoNumbers = createLottoNumbers()
    Log.d("TAG", lottoNumbers.toString())
    updateLottoBallImage(lottoNumbers)

    CoroutineScope(Dispatchers.IO + job).launch {
        val winningNumbers = async { getLottoNumbers() }
        val rank = whatIsRank(lottoNumbers, winningNumbers.await())
        val text = "${winningNumbers.await()} : $rank"

        withContext(Dispatchers.Main) {
            binding.tvWinning.text = text
        }
    }
}

# 코루틴 취소하기

액티비티가 종료되었을 때 실행중인 코루틴이 살아남는것을 방지하기 위해 취소하는 기능을 추가해 보도록 하겠습니다. 알기쉬운 코루틴 이론 강의에서 설명했던 것처럼 코루틴을 취소하기 위해서는 job에 대해 cancel을 수행하면 됩니다. 그런데 generateButton 안에서 수행된 CoroutineScope에 대해서는 반환되는 job을 오브젝트로 만들지 않았습니다.

이런 경우에는 Job 전역변수를 하나 만들어 주고 디스패처와 결합합니다. 코틀린의 코루틴은 자식 코루틴이 취소되면 연결된 부모 코루틴이 모두 취소되는 특징을 가지고 있으므로 onDestroy에서 job에 대해 cancel을 수행하면 연결시킨 코루틴을 취소할 수 있습니다.

실제 예제에서는 뷰모델을 이용하게 되므로 lifecycle 모듈과 결합하여 사용하면 좀 더 앱의 라이프사이클과 연동된 생명주기를 가지게 할 수 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class MainActivity : AppCompatActivity() {
    ...
    private val job = Job()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        binding.generateButton.setOnClickListener {
            ...
            CoroutineScope(Dispatchers.IO + job).launch {
                ...
            }
        }
    }

    override fun onDestroy() {
        job.cancel()
        super.onDestroy()
    }
}

이렇게 코루틴을 이용해서 나의 로또번호를 인터넷의 당첨결과와 비교하는 법에 대해 알아보았습니다.

Built with Hugo
Theme Stack designed by Jimmy