비동기 방식
동기 방식은 먼저 시작된 작업이 끝날 때까지 다른 작업을 시작하지 않고, 먼저 시작된 작업이 끝나야만 다음 작업을 처리하는 방식이다.
작업이 시작되고 끝나는 기준은 어떠한 것을 요청(작업 시작)하고, 그 요청에 대한 응답을 받는 것(작업 완료)이다.
비동기 방식은 동기 방식과는 달리, 먼저 시작된 작업이 완료됐는지 아닌지는 신경 쓰지 않고 다음 작업을 시작한다.
어떤 요청에 대한 응답을 기다리는 동안 다른 작업을 진행할 수 있으므로 자원을 효율적으로 사용할 수 있지만, 이를 잘 알지 못하는 상태로 활용하게 되면 내가 원하는 방식으로 프로그램이 동작하지 않을 수도 있다.
addOnSuccessListener는 비동기 방식이다. (with Firebase RTDB)
https://github.com/Capstone-Muyaho/Capstone-Frontend
GitHub - Capstone-Muyaho/Capstone-Frontend: 캡스톤용 애플리케이션 샘플
캡스톤용 애플리케이션 샘플. Contribute to Capstone-Muyaho/Capstone-Frontend development by creating an account on GitHub.
github.com
안드로이드 프로젝트에서 Firebase RTDB를 사용하여 회원 데이터 관리를 하였다.
그 어느 코드보다 Firebase RTDB를 다루면서 코틀린에 익숙해질 수 있었다..
class FriendListActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_friend_list)
val database: FirebaseDatabase = FirebaseDatabase.getInstance()
val userReference: DatabaseReference = database.getReference("users")
val db: DatabaseReference = Firebase.database.getReference("users")
var userSearchList = ArrayList<User>()
var userSearch: User? = null
var index = 0
var friendNick: String? = ""
var friendType: String? = ""
UserApiClient.instance.me { user, error ->
if(error != null) {
Log.e("TAG", "사용자 정보 요청 실패", error)
} else if (user != null) {
val id = user.id.toString()
db.child(id).child("nickname").get().addOnSuccessListener {
val myNick = it.value.toString()
db.child(id).child("friend").get().addOnSuccessListener {
val myFriend = it.value.toString()
userReference.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
// 생략
비동기 방식으로 처리되는 코드의 단적인 예시이다.
회원 정보 데이터를 불러오고 불러온 데이터를 변수(ex. myNick)에 저장한 다음, 해당 변수 사용 시 저장된 데이터가 사용되어야 한다고 생각했으나, null 값이 계속해서 출력되었다. 변수에 데이터가 저장되지 않은 것이다. 변수도 미리 선언해놓았는데 계속 null 값만 불러와서 의아했었다. 절차 지향에 너무 익숙해져 있었던 내 잘못이었다.
이전에 해당 문제와 관련해서 안드로이드 개발 커뮤니티에 질문을 올린 적이 있었는데 그때도 비동기 방식 때문이라고 답변을 받았었다.
그 당시는 동기와 비동기 방식에 대해 깊은 공부를 하지 않았던 때였고(콜백 함수도 제대로 공부하지 않았던 때다. 왜 그랬지 과거의 나?), 진짜 정말 얼마 남지 않은 프로젝트 마감 기한 때문에 위와 같은 콜백 지옥을 엿볼 수 있는 더러운 코드가 만들어졌다 😅
지금 생각해보니 Firebase RTDB 관련 말고도 카카오 로그인 API 사용 관련해서도 비동기 처리로 인한 문제가 있었다. 학부생 내 코드.. 문제가 많았구나!
원래 의도했었던 방식은 아래와 같았다.
class FriendListActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_friend_list)
// 중략
var myNick: String? = ""
var myFriend: String? = ""
UserApiClient.instance.me { user, error ->
if(error != null) {
Log.e("TAG", "사용자 정보 요청 실패", error)
} else if (user != null) {
val id = user.id.toString()
db.child(id).child("nickname").get().addOnSuccessListener {
myNick = it.value.toString()
}
db.child(id).child("friend").get().addOnSuccessListener {
myFriend = it.value.toString()
}
userReference.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
// 생략
미리 myNick과 myFriend 변수를 초기화한 뒤, addOnSuccessListener {}에서 데이터베이스에서 가져온 데이터를 String 타입으로 형변환하여 각각의 변수에 저장한다(it.value.toString()).
하지만 addOnSuccessListener 자체가 비동기 방식이기 때문에 addOnSuccessListener {} 부분이 실행되기 전에 addValueEventListener({}) 부분이 실행돼버린다. 즉, 데이터베이스에서 가져온 데이터가 저장되기 전에 변수 사용을 먼저 하기 때문에 맨 처음 변수 초기화 시 저장된 null 값만 있는 것이다.
addOnSuccessListener 자체가 비동기 방식으로 작업을 처리하는 것이니, 위 코드를 내가 원하는 방식으로 실행시키기 위해서는 addOnSuccessListener를 사용하는 대신 동기 방식으로 처리하는 코드를 작성해야 한다.
동기 방식으로 처리하기
코틀린에서 비동기로 처리되는 작업이 완료되었는지 확인을 한 뒤 다음 작업을 진행하도록 만드는 방법으로는 Coroutines 또는 Tasks를 사용하는 방법이 있다고 한다.
답변받은 두 가지 코드를 참고하자면 아래와 같다.
// Coroutines 사용
private suspend fun getTokenResult (firebaseUser: FirebaseUser) = suspendCoroutine<GetTokenResult?> { continuation ->
firebaseUser.getIdToken(true).addOnCompleteListener {
if (it.isSuccessful) {
continuation.resume(it.result)
} else {
continuation.resume(null)
}
}
Coroutines는 코틀린에서 사용할 수 있는 비동기 처리 방식이다. 코틀린에서는 async/await 방식 대신 coroutines를 사용한다.
또한 Firebase에서 데이터 값을 가져오는 것도 addOnSuccessListener가 아닌, addOnCompleteListener를 사용한다. addOnCompleteListener 사용 시 통신 완료가 된 뒤 다음 작업이 진행된다.
// Tasks의 await() 사용
object FcmToken {
@JvmStatic
fun getToken(): String? {
val task = FirebaseInstanceId.getInstance().instanceId
try {
val result = Tasks.await(task)
return result.token
} catch (e: ExecutionException) {
throw IllegalArgumentException("")
} catch (e: InterruptedException) {
throw IllegalArgumentException("")
}
}
}
...
viewModelScope.launch(Dispatchers.IO) {
val result = FcmToken.getToken(viewModelScope)
Log.e("result", result.toString())
}
Tasks.await() 방식은 실행 중인 작업이 끝날 때까지 기다리는 블로킹 동기화 방식이다.
따라서 Friebase RTDB에서 데이터 값을 불러오는 작업이 끝날 때까지 기다리게 된다.
뎁스노트 | 파이어베이스에서 읽어오는 데이터 값이 저장이 되지 않습니다.
class LogInActivity : AppCompatActivity() { var userId =
devsnote.com
비동기 방식
동기 방식은 먼저 시작된 작업이 끝날 때까지 다른 작업을 시작하지 않고, 먼저 시작된 작업이 끝나야만 다음 작업을 처리하는 방식이다.
작업이 시작되고 끝나는 기준은 어떠한 것을 요청(작업 시작)하고, 그 요청에 대한 응답을 받는 것(작업 완료)이다.
비동기 방식은 동기 방식과는 달리, 먼저 시작된 작업이 완료됐는지 아닌지는 신경 쓰지 않고 다음 작업을 시작한다.
어떤 요청에 대한 응답을 기다리는 동안 다른 작업을 진행할 수 있으므로 자원을 효율적으로 사용할 수 있지만, 이를 잘 알지 못하는 상태로 활용하게 되면 내가 원하는 방식으로 프로그램이 동작하지 않을 수도 있다.
addOnSuccessListener는 비동기 방식이다. (with Firebase RTDB)
https://github.com/Capstone-Muyaho/Capstone-Frontend
GitHub - Capstone-Muyaho/Capstone-Frontend: 캡스톤용 애플리케이션 샘플
캡스톤용 애플리케이션 샘플. Contribute to Capstone-Muyaho/Capstone-Frontend development by creating an account on GitHub.
github.com
안드로이드 프로젝트에서 Firebase RTDB를 사용하여 회원 데이터 관리를 하였다.
그 어느 코드보다 Firebase RTDB를 다루면서 코틀린에 익숙해질 수 있었다..
class FriendListActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_friend_list)
val database: FirebaseDatabase = FirebaseDatabase.getInstance()
val userReference: DatabaseReference = database.getReference("users")
val db: DatabaseReference = Firebase.database.getReference("users")
var userSearchList = ArrayList<User>()
var userSearch: User? = null
var index = 0
var friendNick: String? = ""
var friendType: String? = ""
UserApiClient.instance.me { user, error ->
if(error != null) {
Log.e("TAG", "사용자 정보 요청 실패", error)
} else if (user != null) {
val id = user.id.toString()
db.child(id).child("nickname").get().addOnSuccessListener {
val myNick = it.value.toString()
db.child(id).child("friend").get().addOnSuccessListener {
val myFriend = it.value.toString()
userReference.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
// 생략
비동기 방식으로 처리되는 코드의 단적인 예시이다.
회원 정보 데이터를 불러오고 불러온 데이터를 변수(ex. myNick)에 저장한 다음, 해당 변수 사용 시 저장된 데이터가 사용되어야 한다고 생각했으나, null 값이 계속해서 출력되었다. 변수에 데이터가 저장되지 않은 것이다. 변수도 미리 선언해놓았는데 계속 null 값만 불러와서 의아했었다. 절차 지향에 너무 익숙해져 있었던 내 잘못이었다.
이전에 해당 문제와 관련해서 안드로이드 개발 커뮤니티에 질문을 올린 적이 있었는데 그때도 비동기 방식 때문이라고 답변을 받았었다.
그 당시는 동기와 비동기 방식에 대해 깊은 공부를 하지 않았던 때였고(콜백 함수도 제대로 공부하지 않았던 때다. 왜 그랬지 과거의 나?), 진짜 정말 얼마 남지 않은 프로젝트 마감 기한 때문에 위와 같은 콜백 지옥을 엿볼 수 있는 더러운 코드가 만들어졌다 😅
지금 생각해보니 Firebase RTDB 관련 말고도 카카오 로그인 API 사용 관련해서도 비동기 처리로 인한 문제가 있었다. 학부생 내 코드.. 문제가 많았구나!
원래 의도했었던 방식은 아래와 같았다.
class FriendListActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_friend_list)
// 중략
var myNick: String? = ""
var myFriend: String? = ""
UserApiClient.instance.me { user, error ->
if(error != null) {
Log.e("TAG", "사용자 정보 요청 실패", error)
} else if (user != null) {
val id = user.id.toString()
db.child(id).child("nickname").get().addOnSuccessListener {
myNick = it.value.toString()
}
db.child(id).child("friend").get().addOnSuccessListener {
myFriend = it.value.toString()
}
userReference.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
// 생략
미리 myNick과 myFriend 변수를 초기화한 뒤, addOnSuccessListener {}에서 데이터베이스에서 가져온 데이터를 String 타입으로 형변환하여 각각의 변수에 저장한다(it.value.toString()).
하지만 addOnSuccessListener 자체가 비동기 방식이기 때문에 addOnSuccessListener {} 부분이 실행되기 전에 addValueEventListener({}) 부분이 실행돼버린다. 즉, 데이터베이스에서 가져온 데이터가 저장되기 전에 변수 사용을 먼저 하기 때문에 맨 처음 변수 초기화 시 저장된 null 값만 있는 것이다.
addOnSuccessListener 자체가 비동기 방식으로 작업을 처리하는 것이니, 위 코드를 내가 원하는 방식으로 실행시키기 위해서는 addOnSuccessListener를 사용하는 대신 동기 방식으로 처리하는 코드를 작성해야 한다.
동기 방식으로 처리하기
코틀린에서 비동기로 처리되는 작업이 완료되었는지 확인을 한 뒤 다음 작업을 진행하도록 만드는 방법으로는 Coroutines 또는 Tasks를 사용하는 방법이 있다고 한다.
답변받은 두 가지 코드를 참고하자면 아래와 같다.
// Coroutines 사용
private suspend fun getTokenResult (firebaseUser: FirebaseUser) = suspendCoroutine<GetTokenResult?> { continuation ->
firebaseUser.getIdToken(true).addOnCompleteListener {
if (it.isSuccessful) {
continuation.resume(it.result)
} else {
continuation.resume(null)
}
}
Coroutines는 코틀린에서 사용할 수 있는 비동기 처리 방식이다. 코틀린에서는 async/await 방식 대신 coroutines를 사용한다.
또한 Firebase에서 데이터 값을 가져오는 것도 addOnSuccessListener가 아닌, addOnCompleteListener를 사용한다. addOnCompleteListener 사용 시 통신 완료가 된 뒤 다음 작업이 진행된다.
// Tasks의 await() 사용
object FcmToken {
@JvmStatic
fun getToken(): String? {
val task = FirebaseInstanceId.getInstance().instanceId
try {
val result = Tasks.await(task)
return result.token
} catch (e: ExecutionException) {
throw IllegalArgumentException("")
} catch (e: InterruptedException) {
throw IllegalArgumentException("")
}
}
}
...
viewModelScope.launch(Dispatchers.IO) {
val result = FcmToken.getToken(viewModelScope)
Log.e("result", result.toString())
}
Tasks.await() 방식은 실행 중인 작업이 끝날 때까지 기다리는 블로킹 동기화 방식이다.
따라서 Friebase RTDB에서 데이터 값을 불러오는 작업이 끝날 때까지 기다리게 된다.
뎁스노트 | 파이어베이스에서 읽어오는 데이터 값이 저장이 되지 않습니다.
class LogInActivity : AppCompatActivity() { var userId =
devsnote.com