본문 바로가기

통신

suspendCancellableCoroutine 이란

suspendCancellableCoroutine

게시글을 이해하기 위해서 필요한 지식들

1. 코루틴에 대한 기본적인 개념

2. 코틀린

3. 안드로이드

4. Firebase Firestore

5. Firebase Storage

 

 CancellationException 을 제공해주는 코루틴이다. 외부와 통신을 했을때 콜백의 작업이 다 끝나고 났을때 해당 결과값을 사용하고 싶을때 쓰는 기술이다. 

 

안드로이드에선 통신에 대한 결과 처리를 콜백에서 처리하게끔 구현하는 기술들이 많다. 이번 게시글에서 소개할 코드에서 쓰는 Firestore도 그렇고 Retrofit또한 그러하다. 콜백에서 요청에 대한 응답값을 받아왔오면 우리는 해당 응답값을 바탕으로 다음 작업을 진행하기를 원한다. 하지만 오늘 소개할 susepndCancellableCoroutine 을 모르면 외부로부터 응답값을 받아오기도 전에 코드가 먼저 실행되버려서 NPE를 만나게 될 것이다. ㅠ

 

예시 1

게시글을 작성하고 나면 사용자가 작성한 게시글이 게시판에 새로 추가되서 보여져야 할 것이다.  이때 작성한 게시글의 결과를 받아와야지 게시판에 추가해서 보여줄 수 있다.

예시 2

게시글의 좋아요나 싫어요를 누르고 나면 해당 게시글의 최신 좋아요, 싫어요 개수도 불러와서 표현해야 할 것이다.

 

예시 3

사용자가 로그인을 시도하면 사용자가 입력한 아이디와 비밀번호 값을 기준으로 회원정보를 확인하고 그에 맞는 응답 결과를 보내준뒤에  사용자는 받은 응답값을 기준으로 어떤 처리를 해줄 것이다.

 

이 기술을 공부하게된 계기는 바로 예시2의 상황을 내가 직접 구현하다가 뜻대로 동작하지 않았기 때문이다.

문제가 발생했던 코드는 아래와 같다.

    override suspend fun getDailyBoard(documentId: String): DailyBoard =
        withContext(Dispatchers.IO) {
            var dailyBoard: DailyBoard
                dailyBoard = DailyBoard(
                    Uri.parse("nothing"), "nothing", "nothing",
                    emptyList(), 0, 0, "nothing", "nothing",0
                )
            try {
                val document =
                    fireStoreRef.collection("dailyBoard").document(documentId).get().await()
                val dailyBoardCollection = DailyboardCollection(
                    Util.parsingFireStoreDocument(document, "boardContents"),
                    Util.parsingFireStoreDocument(document, "disLike").toInt(),
                    Util.parsingFireStoreDocument(document, "like").toInt(),
                    Util.parsingFireStoreDocument(document, "writerUID"),
                    Util.parsingFireStoreDocument(document, "userFavourability"),
                )

                val userNicknameSnapshot =
                    fireStoreRef.collection("MZUsers").document(dailyBoardCollection.writerUID)
                        .get().await()
                val writerProfileUri =
                    storage.reference.child("user_profile_image/" + dailyBoardCollection.writerUID + ".jpg").downloadUrl.await()
                val boardImagesSnapshot =
                    storage.reference.child("board/${document.id}").listAll().await()
                val boardImages = boardImagesSnapshot.items.map {
                    it.downloadUrl.await()
                }

                val boardUID = document.id
                val userNickName = userNicknameSnapshot.get("nickName").toString()
                val boardContents = dailyBoardCollection.boardContents
                val like = dailyBoardCollection.like
                val disLike = dailyBoardCollection.disLike
                val userFavourability = dailyBoardCollection.favourability


                dailyBoard = DailyBoard(
                    writerProfileUri,
                    userNickName,
                    boardContents,
                    boardImages,
                    disLike,
                    like,
                    boardUID,
                    userFavourability
                )


            } catch (e: Exception) {
                Logger.v(e.message.toString())
                dailyBoard = DailyBoard(
                    Uri.parse("nothing"), "nothing", "nothing",
                    emptyList(), 0, 0, "nothing", "nothing"
                )
            }

            return@withContext dailyBoard
        }

 

코드가 동작하면 처음에 설정한 dailyBoard 값을 getDailyBoard메소드가 리턴한다.

코드를 수정하면서 로직이 많이 바뀔일이 있어서 변한 부분이 많았는데 susepndCancellableCoroutin 영역을 사용해서 아래와 같이 바꾸면 된다.

코드가 많이 바뀌어서 이해하는데 어려움이 있을 수 있는데 아래쪽에서 자세히 설명하겠다.

    override suspend fun getDailyBoard(documentId: String): DailyBoard = suspendCancellableCoroutine { continuation ->
        fireStoreRef.collection("dailyBoard").document(documentId).get().addOnSuccessListener { result ->

                val dailyBoardCollection = DailyboardCollection(
                    Util.parsingFireStoreDocument(result, "boardContents"),
                    Util.parsingFireStoreDocument(result, "disLike").toInt(),
                    Util.parsingFireStoreDocument(result, "like").toInt(),
                    Util.parsingFireStoreDocument(result, "writerUID"),
                    Util.parsingFireStoreDocument(result, "userFavourability"),
                    Util.parsingDailyBoardFiles(result, "fileURL"),
                    Util.parsingFireStoreDocument(result, "viewType").toInt()
                )

                try {
                    runBlocking {
                        val userInfoSnapshot =
                            fireStoreRef.collection("MZUsers")
                                .document(dailyBoardCollection.writerUID).get().await()

                        val files = dailyBoardCollection.files.map {
                            Uri.parse(it)
                        }

                        val boardUID = result.id
                        val userProfile = Uri.parse(userInfoSnapshot.get("profileURL").toString())
                        val userNickName = userInfoSnapshot.get("nickName").toString()
                        val boardContents = dailyBoardCollection.boardContents
                        val like = dailyBoardCollection.like
                        val disLike = dailyBoardCollection.disLike
                        val userFavourability = dailyBoardCollection.favourability
                        val viewType = dailyBoardCollection.viewType

                        val dailyBoard = DailyBoard(
                            userProfile,
                            userNickName,
                            boardContents,
                            files,
                            disLike,
                            like,
                            boardUID,
                            userFavourability,
                            viewType
                        )

                        continuation.resume(dailyBoard, null)
                    }
                } catch (e: Exception) {
                    Logger.v(e.message.toString())
                }

        }
    }

 

우선 코루틴 스코프를 susepndCancellableCorotuine 으로 설정했다. 이번 게시글의 핵심인 콜백에서 응답을 처리해주기 위함이다. 그리고 runBlocking을 아래쪽에서 사용했는데 이는 게시글을 작성한 사용자의 정보를 따로 갖고오기 위함이다. await() 함수를 사용해서 필요한 값을 먼저 갖고온 뒤에 그 값을 바탕으로 다시 새로운 값을 요청한다.

만약 await() 함수가 존재하지 않는다면 userInfoSnapshot 변수는 null이 될 것이고 아래쪽에서 userProfile 변수도 null이 되버릴 것이다. 그리고 최종적으로 필요한 dailyBoard 객체또한 null이 되버린다.

 

완성된 dailyBoard를 사용해서 coroutine.resume(리턴하고 싶은값, null) 메소드를 쓰면 원하는 값을 리턴해서 뷰나 뷰모델에서 사용하는게 가능해진다.

 

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

 

참고자료

https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/suspend-cancellable-coroutine.html

 

 

 

 

'통신' 카테고리의 다른 글

동기(Synchronous), 비동기 (Asynchronous), blocking, non blocking  (2) 2023.11.18