[Android/Flutter 교육] 특강 6일차

MSU·2024년 3월 8일

Android-Flutter

목록 보기
51/85
post-thumbnail

Coroutine

코루틴
동시에 작업을 하거나 오류가 발생할 가능성이 높은 코드를 처리하는데 사용한다.
쓰레드와 유사하지만 쓰레드의 단점을 보완하기 위해 만들어졌다.
안드로이드에서는 쓰레드보다 코루틴 사용을 권장하고 있다.
파이어베이스나 룸데이터베이스도 코루틴으로 사용함

  1. 쓰래드 보다 메모리 사용량이 적어 작업의 처리가 더 빨리 끝난다.
  2. 비동기적 처리(동시에 여러 작업을 수행)를 위해 사용하지만 동기적(순차 처리)으로 운영하기가 쉽다.
  3. 중간에 중단하기기 쉽다.
  4. 다른 루틴에서 발생시킨 데이터를 가져오는게 매우 쉽다.

suspend fun

코루틴으로 운영할 코드를 가진 메서드(함수)는 os에서 코드의 흐름을 관리해야 하기 때문에 그냥 fun이 아닌 suspend fun으로 정의해줘야 한다.

    suspend fun working1(){
        for(a1 in 0..10){
            // 500ms 쉬었다 간다(코루틴에서만 사용 가능하다)
            delay(500)
            val now = System.currentTimeMillis()
            activityMainBinding.textView.text = "working1 : $now"
        }
    }

CoroutineScope

  • CoroutineScope : 발생시키는 코루틴의 용도를 지정한다.
  • Main : 안드로이드의 MainThread가 처리해준다, 화면에 관련된 작업이 가능하다.
  • IO : 데이터 입출력 용. 별도의 쓰래가 발생한다. 네트워크 처리나 데이터 베이스, 파일처리 등에서 사용한다.
  • launch : 코루틴 가동
                CoroutineScope(Dispatchers.Main).launch {
                    // 코루틴 가동
                    // launch로 가동시키면 각각이 독립적으로 동시에 실행되는 효과를 얻을 수 있다.
                    launch {
                        working1()
                    }
                    // working1이 동작 시작하고 바로 working2가 동작 시작된다.
                    launch {
                        working2()
                    }
                    // working2이 동작 시작하고 바로 working3이 동작 시작된다.
                    launch {
                        working3()
                    }
                }
  • join : launch로 실행시킨 코루틴이 끝날 때 까지 메인 루틴의 코드를 일시 정지시킨다. 동기 처리를 하고자 할 때 사용한다.
  • async : 코루틴을 발생시킨다. 코루틴이 호출하는 함수가 반환하는 값이 있을 때 사용한다.
  • await : async로 가동시킨 코루틴이 호출하는 함수가 반환하는 값을 받을 수 있다. 값을 반환할 때 까지 메인 루틴이 대기상태가 되기 때문에 await을 호출하는 시점을 잘 잡아주는 것이 매우 중요하다.

예시

코루틴 가동(비동기)

코루틴으로 운영할 메서드를 suspend fun으로 정의해준다.

    // 코루틴으로 운영할 코드를 가지고 있는 메서드
    // suspend fun : 함수 내부의 코드를 중단하거나 일시정지하는 등의 관리가 가능한 함수
    // 코루틴으로 운영할 코드를 가지고 있는 메서드(함수)는 os에서 코드의 흐름을 관리해야 하기 때문에
    // suspend fun 으로 정의해 주는 것이 좋다.
    suspend fun working1(){
        for(a1 in 0..10){
            // 500ms 쉬었다 간다(코루틴에서만 사용 가능하다)
            delay(500)
            val now = System.currentTimeMillis()
            activityMainBinding.textView.text = "working1 : $now"
        }
    }

    suspend fun working2(){
        for(a1 in 0..10){
            delay(500)
            val now = System.currentTimeMillis()
            activityMainBinding.textView2.text = "working2 : $now"
        }
    }

    suspend fun working3(){
        for(a1 in 0..10){
            delay(500)
            val now = System.currentTimeMillis()
            activityMainBinding.textView3.text = "working3 : $now"
        }
    }

정의한 suspend 메서드들을 CoroutineScope에서 호출해주면 된다.
여러 메서드들을 각각 독립적으로 실행시키려면 launch로 감싸주면 된다.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(activityMainBinding.root)

        activityMainBinding.apply {

            button.setOnClickListener {
                // 여러 코루틴을 관리할 수 있는 객체
                // CoroutineScope : 발생시키는 코루틴의 용도를 지정한다.
                // Main : 안드로이드의 MainThread가 처리해준다, 화면에 관련된 작업이 가능하다.
                // IO : 데이터 입출력 용. 별도의 쓰래가 발생한다. 네트워크 처리나 데이터 베이스, 파일처리 등에서 사용한다.

                // launch : 코루틴 가동
                CoroutineScope(Dispatchers.Main).launch {
                    // 코루틴 가동
                    // launch로 가동시키면 각각이 독립적으로 동시에 실행되는 효과를 얻을 수 있다.
                    launch {
                        working1()
                    }
                    // working1이 동작 시작하고 바로 working2가 동작 시작된다.
                    launch {
                        working2()
                    }
                    // working2이 동작 시작하고 바로 working3이 동작 시작된다.
                    launch {
                        working3()
                    }
                }
            }
        }
    }

각각의 working1, working2, working3 메서드가 비동기적으로 동작한다.

코루틴 가동(동기)

launch 스코프는 코루틴의 흐름을 제어할 수 있는 job을 반환한다.

반환받는 job을 변수에 담아주고 join메서드를 호출하면 해당 코루틴이 끝날때 까지 대기하여 작업을 동기적으로(순서대로) 처리할 수 있다.

            button2.setOnClickListener {
                // 코루틴으로 운영하는 작업을 동기적(순서대로) 처리할 수 있다.
                CoroutineScope(Dispatchers.Main).launch {
                    // 코루틴 가동
                    // launch는 코루틴의 흐름을 제어할 수 있는 job을 반환한다.
                    val job1 = launch {
                        working1()
                    }

                    // 첫 번째 코루틴이 끝날때 까지 대기한다.
                    job1.join()

                    val job2 = launch {
                        working2()
                    }

                    // 두 번째 코루틴이 끝날때 까지 대기한다.
                    job2.join()

                    launch {
                        working3()
                    }
                }
            }

첫번째 작업이 끝나고 나서야 두번째 작업이 실행되고 두번째 작업이 끝나고 나서야 세번째 작업이 실행된다.

코루틴 개별 중지

실행되는 코루틴을 관리하는 객체를 변수에 담아주면 해당 객체를 개별적으로 중단시킬 수 있다.

// MainActivity.kt

    lateinit var w1:Job
    lateinit var w2:Job
    lateinit var w3:Job


            button3.setOnClickListener {
                // 실행되는 코루틴을 관리하는 객체를 변수에 담아준다.
                CoroutineScope(Dispatchers.Main).launch {
                    // 코루틴 가동
                    // 코루틴을 관리하는 객체를 변수에 담아준다.
                    w1 = launch {
                        working1()
                    }
                    w2 = launch {
                        working2()
                    }
                    w3 = launch {
                        working3()
                    }
                }
            }
            
            button4.setOnClickListener {
                // w1과 w3만 중단시킨다.
                w1.cancel()
                w3.cancel()
            }

working1working3만 멈추고 working2는 값이 바뀌는 게 진행되는 것을 확인할 수 있다.

코루틴 전체 중지

개별 코루틴 중지와 마찬가지로 CoroutineScope를 담을 변수를 준비하면 된다.

// MainActivity.kt

lateinit var mainWorking:Job

            button3.setOnClickListener {
                // 실행되는 코루틴을 관리하는 객체를 변수에 담아준다.
                mainWorking = CoroutineScope(Dispatchers.Main).launch {
                    // 코루틴 가동
                    // 코루틴을 관리하는 객체를 변수에 담아준다.
                    launch {
                        working1()
                    }
                    launch {
                        working2()
                    }
                    launch {
                        working3()
                    }
                }
            }
            
            button4.setOnClickListener {
                // w1과 w3만 중단시킨다.
                // w1.cancel()
                // w3.cancel()
                
                // 모든 코루틴의 수행을 중단시킨다.
                mainWorking.cancel()
            }

모든 코루틴 수행이 멈춘 것을 확인할 수 있다.

코루틴으로부터 값 받아오기(비동기)

코루틴이 호출하는 함수가 반환하는 값이 있을 때 async와 await을 사용하면
함수가 반환하는 값을 받을 수 있다.

// MainActivity.kt

            button5.setOnClickListener {
                CoroutineScope(Dispatchers.Main).launch {
                    // 코루틴 가동
                    // 코루틴으로 운영하는 함수에서 반환하는 값을 받으려면
                    // launch가 아닌 async로 가동해야 한다.
                    // launch와 마찬가지로 async도 비동기로 가동한다.

                    val job1 = async {
                        working4()
                    }
                    val job2 = async {
                        working5()
                    }
                    val job3 = async {
                        working6()
                    }

                    // await : 코루틴으로 운영하는 함수가 반환하는 값을 받아올 수 있다.
                    // 코루틴이 관리하는 코드가 수행이 완료되면 return 부분은 수행하지 않고 대기 상태가 된다.
                    // 이 때 await을 호출하면 return 부분이 수행되어 값을 반환하게 된다.
                    textView.text = "job1 : ${job1.await()}"
                    textView2.text = "job2 : ${job2.await()}"
                    textView3.text = "job3 : ${job3.await()}"
                }
            }

비동기 수행이 끝나고 받아온 값을 출력한다.

코루틴으로부터 값 받아오기(동기)

await구문을 각각의 async구문 중간에 두면 앞선 async 코루틴의 작업이 끝날 때 까지 대기하고 있다가 값을 받은 후 그 다음 async 코루틴 작업을 진행한다.
비동기 코드를 동기적(순차적)으로 실행하게 만들 수 있다.

// MainActivity.kt

            button6.setOnClickListener {
                CoroutineScope(Dispatchers.Main).launch {
                    // 코루틴 가동
                    val job1 = async {
                        working4()
                    }
                    // 여기서 await을 호출한다.
                    // await을 호출하면 코루틴의 작업이 끝날 때 까지 대기하고 있다가
                    // 값을 반환하면 그 값을 받은 다음 다음으로 이어나간다.
                    textView.text = "job1 : ${job1.await()}"

                    val job2 = async {
                        working5()
                    }
                    textView2.text = "job2 : ${job2.await()}"

                    val job3 = async {
                        working6()
                    }
                    textView3.text = "job3 : ${job3.await()}"

                }
            }

Room

SQLite 데이터베이스 사용시 프로그래밍을 보다 간단하게 할 수 있도록 제공되는 라이브러리(별개의 데이터베이스가 아님)
구글에서 정한 규격대로 프로그래밍을 하게 되면 SQLite 데이터 베이스를 사용하는 코드를 자동으로 만들어준다.

코루틴 사용을 강제한다.

사용법

레이아웃

build.gradle 셋팅

  • plugins에 kotlin("kapt") 추가
  • dependencies에 3줄 추가
    implementation("androidx.room:room-runtime:2.6.1")
    annotationProcessor("androidx.room:room-compiler:2.6.1")
    kapt("androidx.room:room-compiler:2.6.1")
// build.gradle.kts


plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    kotlin("kapt")
}

dependencies {

    implementation("androidx.core:core-ktx:1.12.0")
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.11.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")

    implementation("androidx.room:room-runtime:2.6.1")
    annotationProcessor("androidx.room:room-compiler:2.6.1")
    kapt("androidx.room:room-compiler:2.6.1")
}

Entity 작성

Entity 는 데이터 베이스에 저장된 데이터를 담거나 저장할 데이터를 담을 모델에 해당한다.
RoomDatabase는 Entiry에 작성한 내용을 기반으로 테이블을 생성해준다.

// TestModel.kt

import androidx.room.Entity
import androidx.room.PrimaryKey

// tableName : 생성될 테이블의 이름
@Entity(tableName = "TestTable")
// 주 생성자에 정의한 프로퍼티들이 컬럼으로 생성된다.
data class TestModel(
    // idx컬럼
    // autoGenerate에 true를 넣어주면 데이터를 저장할때 마다 1씩 증가되는 값으로 채워준다.
    @PrimaryKey(autoGenerate = true)
    var testIdx:Int = 0,

    var testData1:String = "",
    var testData2:Double = 0.0
)

SQLite사용시 DBHelper클래스 작성할때 오버라이딩한 onCreate와 onUpgrade메서드도 Entity작성시 자동으로 만들어진다.

Dao 작성

Dao는 Database Access Object의 약자.
데이터베이스에 접속해서 데이터를 읽고 쓰는 작업을 수행한다.
Dao는 인터페이스로 생성한다.

// TestDao.kt

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update

@Dao
interface TestDao {

    // 데이터 저장
    // 매개변수로 들어오는 모델 중에 primary key로 지정된 프로퍼티는 1부터 1씩 증가되는 값으로 저장되고
    // 그 외에는 프로퍼티에 들어있는 값이 저장된다.
    // 예) insert into TestTable(testData1, testData2) values(?, ?)
    @Insert
    fun insertData(testModel: TestModel)

    // 데이터 수정
    // 매개변수로 들어오는 모델 중에 primary key로 지정된 프로퍼티를 조건절로 하고
    // 그 외에는 프로퍼티에 들어있는 값으로 수정된다.
    // 예) update TestTable testData1 = ?, testData2 = ? where testIdx = ?
    @Update
    fun updateData(testModel: TestModel)

    // 데이터 삭제
    // 매개변수로 들어오는 모델의 프로퍼티 중에 primary key로 지정된 프로퍼티를 조건절로 하는 쿼리문이 만들어진다.
    // 예) delete from TestTable where testidx = ?
    @Delete
    fun deleteData(testModel: TestModel)

    // 만약 자동으로 만들어지는 쿼리문이 아닌 다른 쿼리문을 쓰겠다면
    // Query라는 어노테이션을 사용한다.
    // 데이터를 가져오는 것도 Query라는 어노테이션을 사용한다.
    @Query("select testIdx, testData1, testData2 from TestTable")
    fun selectDataAll() : List<TestModel>

    // 행 하나의 데이터를 가져온다.
    // 쿼리문에 값에 해당하는 부분은 매개변수의 이름을 지정한다.
    // :매개변수이름
    @Query("select testIdx, testData1, testData2 from TestTable where testIdx = :idx")
    fun selectDataOne(idx:Int) : TestModel
}

DataBase 클래스 작성

// TestDatabase.kt

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

// entities : 엔티티들을 지정한다. 지정한 엔티티 하나당 하나의 테이블이 생성된다.
@Database(entities = [TestModel::class], version = 1)
// 추상클래스로 만들어야 한다.
abstract class TestDatabase : RoomDatabase() {
    // dao를 지정한다.
    abstract fun testDao() : TestDao

    companion object {
        // 데이터베이스 객체를 담을 변수
        var testDatabase:TestDatabase? = null

        // RoomDatabase는 코루틴을 이용하게 된다.
        // 즉 비동기적으로 동작할 수 있도록 되어있다.
        // 비동기적 작업 시 데이터 베이스 접속을 여러군데서 하면 문제가 발생될 수 있으므로
        // 데이터베이스 접속을 동기적으로 할 수 있도록 해야 한다.
        @Synchronized
        fun getInstance(context:Context) : TestDatabase? {
            if(testDatabase == null){
                synchronized(TestDatabase::class){
                    // 데이터베이스를 생성하고 Model들의 구조와 동일한 테이블을 생성한다.
                    // test.db : 생성될 sqlite database 파일
                    testDatabase = Room.databaseBuilder(context.applicationContext, TestDatabase::class.java, "test.db").build()
                }
            }
            return testDatabase
        }

    }
}

이후에는 entities에 추가만 해주면 된다.

데이터 저장

// MainActivity.kt


        activityMainBinding.apply {

            button.setOnClickListener {
                // 데이터 베이스 접속
                val testDatabase = TestDatabase.getInstance(this@MainActivity)
                // 저장할 데이터를 담는다.
                // primary key로 지정된 프로퍼티는 1부터 1씩 증가되는 값이 자동으로 부여되기 때문에
                // 이 프로퍼티는 제외한다.
                val testModel1 = TestModel(testData1 = "문자열1", testData2 = 11.11)
                // 저장한다.
                CoroutineScope(Dispatchers.Main).launch {
                    async(Dispatchers.IO) {
                        testDatabase?.testDao()?.insertData(testModel1)
                    }
                }

                val testModel2 = TestModel(testData1 = "문자열2", testData2 = 22.22)
                CoroutineScope(Dispatchers.Main).launch {
                    async(Dispatchers.IO) {
                        testDatabase?.testDao()?.insertData(testModel2)
                    }
                }
                
                activityMainBinding.textView.text = "저장완료"
            }
        }

데이터 불러오기(전체)

// MainActivity.kt


        activityMainBinding.apply {

            button2.setOnClickListener {
                // 데이터 베이스 접속
                val testDatabase = TestDatabase.getInstance(this@MainActivity)
                CoroutineScope(Dispatchers.Main).launch {
                    // 데이터를 받아온다.
                    val job1 = async(Dispatchers.IO) {
                        testDatabase?.testDao()?.selectDataAll()
                    }
                    val dataList = job1.await() as List<TestModel>

                    textView.text = ""

                    dataList.forEach {
                        textView.append("testIdx : ${it.testIdx}\n")
                        textView.append("testData1 : ${it.testData1}\n")
                        textView.append("testData2 : ${it.testData2}\n\n")
                    }
                }
            }

        }

데이터 불러오기(하나)

// MainActivity.kt


        activityMainBinding.apply {

            button3.setOnClickListener {
                // 데이터 베이스 접속
                val testDatabase = TestDatabase.getInstance(this@MainActivity)
                CoroutineScope(Dispatchers.Main).launch {
                    // 데이터를 받아온다.
                    val job1 = async(Dispatchers.IO) {
                        testDatabase?.testDao()?.selectDataOne(1)
                    }
                    val testModel = job1.await() as TestModel

                    textView.text = ""

                    textView.append("testIdx : ${testModel.testIdx}\n")
                    textView.append("testData1 : ${testModel.testData1}\n")
                    textView.append("testData2 : ${testModel.testData2}\n\n")
                }
            }

        }

데이터 수정

// MainActivity.kt


        activityMainBinding.apply {

            button4.setOnClickListener {
                // 데이터 베이스 접속
                val testDatabase = TestDatabase.getInstance(this@MainActivity)
                // 데이터를 준비한다.
                // primary key인 testIdx가 조건절이 된다.
                val testModel = TestModel(testIdx = 1, testData1 = "새로운 문자열", testData2 = 55.55)
                // 수정한다.
                CoroutineScope(Dispatchers.Main).launch {
                    async(Dispatchers.IO){
                        testDatabase?.testDao()?.updateData(testModel)
                    }
                    
                    textView.text = "수정 완료"
                }
            }

        }

데이터 삭제

// MainActivity.kt


        activityMainBinding.apply {

            button5.setOnClickListener {
                // 데이터 베이스 접속
                val testDatabase = TestDatabase.getInstance(this@MainActivity)
                // 데이터를 준비한다.
                // primary key인 testIdx가 조건절이 된다.
                val testModel = TestModel(testIdx = 1)
                // 삭제한다.
                CoroutineScope(Dispatchers.Main).launch {
                    async(Dispatchers.IO){
                        testDatabase?.testDao()?.deleteData(testModel)
                    }

                    textView.text = "삭제 완료"
                }
            }

        }

profile
안드로이드공부

0개의 댓글