코틀린-안드로이드

62일차)알고리즘 문제(디스크 컨트롤), 특강 정리(Repository, UDF, 싱글톤, 데이터클래스 Immutable modeling, 뷰홀더 생성시 nested, EnumClass entries, when문 사용시 defualt값 만들기), 챌린지반 6주차 강의(클린 아키텍쳐), 심화 주차 강의(SharedPreferences,Room, 사용자 위치정보, 지도앱 만들기)

songyooho 2024. 7. 31. 20:46

>알고리즘 문제

1. 디스크 컨트롤

1)문제 

하드디스크는 한 번에 하나의 작업만 수행할 수 있습니다. 디스크 컨트롤러를 구현하는 방법은 여러 가지가 있습니다. 가장 일반적인 방법은 요청이 들어온 순서대로 처리하는 것입니다.

예를들어

- 0ms 시점에 3ms가 소요되는 A작업 요청
- 1ms 시점에 9ms가 소요되는 B작업 요청
- 2ms 시점에 6ms가 소요되는 C작업 요청

와 같은 요청이 들어왔습니다. 이를 그림으로 표현하면 아래와 같습니다.

한 번에 하나의 요청만을 수행할 수 있기 때문에 각각의 작업을 요청받은 순서대로 처리하면 다음과 같이 처리 됩니다.

- A: 3ms 시점에 작업 완료 (요청에서 종료까지 : 3ms)
- B: 1ms부터 대기하다가, 3ms 시점에 작업을 시작해서 12ms 시점에 작업 완료(요청에서 종료까지 : 11ms)
- C: 2ms부터 대기하다가, 12ms 시점에 작업을 시작해서 18ms 시점에 작업 완료(요청에서 종료까지 : 16ms)

이 때 각 작업의 요청부터 종료까지 걸린 시간의 평균은 10ms(= (3 + 11 + 16) / 3)가 됩니다.

하지만 A → C → B 순서대로 처리하면

- A: 3ms 시점에 작업 완료(요청에서 종료까지 : 3ms)
- C: 2ms부터 대기하다가, 3ms 시점에 작업을 시작해서 9ms 시점에 작업 완료(요청에서 종료까지 : 7ms)
- B: 1ms부터 대기하다가, 9ms 시점에 작업을 시작해서 18ms 시점에 작업 완료(요청에서 종료까지 : 17ms)

이렇게 A → C → B의 순서로 처리하면 각 작업의 요청부터 종료까지 걸린 시간의 평균은 9ms(= (3 + 7 + 17) / 3)가 됩니다.

각 작업에 대해 [작업이 요청되는 시점, 작업의 소요시간]을 담은 2차원 배열 jobs가 매개변수로 주어질 때, 작업의 요청부터 종료까지 걸린 시간의 평균을 가장 줄이는 방법으로 처리하면 평균이 얼마가 되는지 return 하도록 solution 함수를 작성해주세요. (단, 소수점 이하의 수는 버립니다)

제한 사항

  • jobs의 길이는 1 이상 500 이하입니다.
  • jobs의 각 행은 하나의 작업에 대한 [작업이 요청되는 시점, 작업의 소요시간] 입니다.
  • 각 작업에 대해 작업이 요청되는 시간은 0 이상 1,000 이하입니다.
  • 각 작업에 대해 작업의 소요시간은 1 이상 1,000 이하입니다.
  • 하드디스크가 작업을 수행하고 있지 않을 때에는 먼저 요청이 들어온 작업부터 처리합니다.

2)솔루션

import java.util.*
class Solution {
    fun solution(jobs: Array<IntArray>): Int {
        var answer = 0
        
        val pq=PriorityQueue<IntArray>(compareBy{it[1]})
        
        jobs.sortWith(compareBy<IntArray>{it[0]}.thenBy{it[1]})
        
        pq.offer(jobs[0])
        
        var idx=1
        var time=jobs[0][0]
        
        var total=0
        
        while(pq.isNotEmpty()){
            val cur = pq.poll()
            if(cur[0]<=time) time+=cur[1]
            else time=cur[0]+cur[1]
            total+=time-cur[0]
            if(idx<jobs.size){
                while(idx<jobs.size){
                    if(jobs[idx][0]<time) pq.offer(jobs[idx++])
                    else break
                }
                if(idx<jobs.size&&pq.isEmpty()) pq.offer(jobs[idx++])    
            }
            
        }
        
        answer=total/jobs.size
        
        return answer
    }
}

-우선순위큐를 이용하여 소요시간이 적은 것 우선 나오도록 하였음

-우선순위큐에 넣기전에 먼저 jobs를 요청시간순으로 정렬하고 요청시간이 같으면 소요시간이 짧은 순으로 정렬

=>하드디스크가 수행중인 일이 없을때 동시에 요청이 들어오면 더 짧은것부터 수행하도록 하기 위함.

-우선순위큐에 넣을때는 현재시간보다 요청시간이 빠른것들을 넣고 이 과정을 지났을때 pq가 비어있으면(하드디스크가 수행중인 작업이 없는 상태) jobs에서 하나를 빼서 넣어준다. 

 

 

>특강

-Repository
1. 사용이유
-Repository는 데이터 소스가 어떤 데이터를 다루는지 알 필요 없다.
->새로운 Data를 다루는 DataSource를 추가해도 부담이 없음
-데이터 출처와 상관없이 같은 interface로 data 접근
-개발자는 repository인터패이스에 의존하기에 unit test가 쉬움
->repository만 테스트하면 데이터가 잘 오는지 확인 가능
->repository가 없으면 view나 viewModel단에서 data를 다루는 로직을 테스트해야함
=>curl, postmap활용해서 unit test가능

@데이터 흐름:UDF구조
VIew -> ViewModel ->Repository ->DataSource
=>view, viewmodel은 UI Layer

-싱글톤: lazy하게 생성할거 아니면 object클래스로 만들기

-데이터 클래스는 immutabitiy modeling으로 만들기(모든 변수는 val로)
=>데이터 변경은 .copy( 변수명 = ~)식으로 복사해 가면서 만들어줌

-어댑터에서 뷰홀더 만들때 inner class가 아니라 nested class로 만들기

-EnumClass사용시 instance찾을때: values Enum.entries사용하기 =>entries는 array로 반환하는 values와 달리 인스턴스들을 immutable한 List로 반환(array는 mutable해서 크기는 변경 불가하나 entry는 바꿀 수있음-list는 원소도 못바꿈)
=>enum.entries.find{it.~ =~}같이 사용

-when문 사용시 else에서 throw하지 말고 default값을 만들어서 처리하기

 

 

 

>챌린지반 강의

클린 아키텍쳐
-단일책임원칙(SRP)
1. 구조
-View -> ViewModel ->UseCase(UseCase그 자체이거나 Repository) -> Entities(Model)
=>화살표 방향으로 참조. 역방향은 X
=>즉, 화살표 방향으로 observe하는 방식이지 화살표 역방향으로 데이터를 보내주는 것은 아님
=>화살표 역방향으로는 존재를 몰라야함

2. 장점: 
<1>독립적으로 분리되어있으므로 각 단계를 재활용하기 쉬움
-단 View의 경우는 재활용 하기 어려우므로 비즈니스 코드를 view에서 최대한 분리해 줘야 함
=>뷰의 디자인이 바뀌든 말든 그대로 사용하도록
<2>역할이 Usecase로 나뉘기에 책임이 줄어들음:단일책임원칙

3. UseCase
<1>구조: presentation(view, viewModel) - Domain(Usecase) - Data(Repository, DataSource)
<2>사용 이유: 동일한 Repository를 사용하면서 다양한 Presentation에 사용하기 위함 -app이 크면 사용
<3>장점 presentation에 대한 테스트 코드짜기 어려움 ->Usecase에 대한 테스트 코드를 짜면 됨.
<4>사용 방식: repository에 대해 사용 방식에 따라 usecase로 쪼갬 =>viewmodel에서 필요한 usecase들을 가져다가 씀

참고:https://medium.com/@avengers14.blogger/use-case-layer-and-abstraction-in-clean-architecture-android-96534885cc6b

 

@lilysAi:동영상 자막 필요시 사용

 

 

 

>심화주차 강의

1. SharedPreference

1)Preference란

-프로그램 설정정보같은 간단한 값을 영구적으로 저장하기위해 사용

-XML에 키-값 세트로 정보 저장

2)사용

<1>객체 생성

val pref = getSharedPreferences("pref",0)

-앞의 인자는 저장될 xml파일의 이름

-뒤의 인자는 모드. 

=>0은 Context.MODE_PRIVATE로 생성된 xml파일은 호출한 앱 내에서만 읽고 쓰기 가능

=> MODE_WORLD_READABLE, MODE_WORLD_WRITEABLE 는 다른 앱애서도 가능하게 하나 deprecated됨

 

<2>쓰기

val edit = pref.edit() // 수정 모드
edit.putString("name", binding.etHello.text.toString())
edit.apply() // 저장완료

 

<3>읽기 

val pref = getSharedPreferences("pref",0)
// 1번째 인자는 키, 2번째 인자는 데이터가 존재하지 않을경우의 값
binding.etHello.setText(pref.getString("name",""))

 

<4>사용 가능한 자료형: Boolean, Float, Int, Long, String, StringSet

 

3)파일 위치: data-app-data-패키지명 내에 shared_prefs에 위치

 

 

2. Room

1)Room이란:

-SQLite를 쉽게 사용할 수있는 데이터베이스 객체 매핑 라이브러리

-Query결과를 LiveData로 받아 쉽게 UI변경 가능

 

2)주요 3요소

<1>@Database: 클래스를 데이터 베이스로 지정하는 어노테이션.

-RoomDatabase를 상속받은 클래스여야 함

-Room.databaseBuilder를 이용하여 인스턴스 생성

-DAO를 가져올 getter메소드 만들음: abstract로 만들면 자동 생성됨

-Room은 인스턴스가 하나만 있으면 되므로 싱글톤 패턴 사용

-version: 버전에 따라 테이블의 형태가 달라질 수 있으므로 버전을 명시해야함

-예시

@Database(entities = [Student::class, ClassInfo::class, Enrollment::class, Teacher::class], version = 1)
abstract class MyDatabase : RoomDatabase() {
    abstract fun getMyDao() : MyDAO

    companion object {
        private var INSTANCE: MyDatabase? = null
        private val MIGRATION_1_2 = object : Migration(1, 2) {
            override fun migrate(database: SupportSQLiteDatabase) { 생략 }
        }

        private val MIGRATION_2_3 = object : Migration(2, 3) {
            override fun migrate(database: SupportSQLiteDatabase) { 생략 }
        }
        fun getDatabase(context: Context) : MyDatabase {
            if (INSTANCE == null) {
                INSTANCE = Room.databaseBuilder(
                    context, MyDatabase::class.java, "school_database")
                    .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
                    .build()
            }
            return INSTANCE as MyDatabase
        }
    }
}

 

@Migration: 버전이 기본 데이터베이스보다 높으면 migration진행

-여러개의 migration 지정 가능

-예시

Room.databaseBuilder(...).addMigrations(MIGRATION_1_2, MIGRATION_2_3)

private val MIGRATION_1_2 = object : Migration(1, 2) {   // version 1 -> 2
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE student_table ADD COLUMN last_update INTEGER")
    }
}

private val MIGRATION_2_3 = object : Migration(2, 3) {   // version 2 -> 3
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE class_table ADD COLUMN last_update INTEGER")
    }
}

 

 

<2>@Entity: 테이블 스키마 정의 =>테이블 명과 열과 primary key값 지정

-테이블명 지정: @Entity(tableName = "~")

-기본 키 설정:

[1]일반:

@PrimaryKey
val id:Int,

[2]자동생성: 이 경우 키 값을 안 넣어도 알아서 생성함

@PrimaryKey(autoGenerate = true)
val id: Int = 0,

-주로 데이터 클래스를 사용

-예시

@Entity(tableName = "student_table")    // 테이블 이름을 student_table로 지정함
data class Student (
    @PrimaryKey 
    @ColumnInfo(name = "student_id") 
    val id: Int,
    val name: String
)

 

 

<3>DAO(Data Access Object): 쿼리 정의 및 쿼리를 위한 메소드 선언

-interface나 abstract class로 정의 되어야 함

-가능한 어노테이션으로 @Insert, @Update, @Delete, @Query 존재

=>Insert,Delete,Update는 suspend로 만들어야 함

-Transaction: 단일 DAO메소드 호출은 단일 트랜잭션으로 실행되도록 보장

-명시적으로 트랙젝션을 묶어서 하나의 트랜잭션이 되도록 할 수 있음

=>이런경우 두 작업이 모두 성공해야 데이터 베이스에 반영됨

-트랙젝션은 실패시에 롤백됨(즉, Insert에서 onConflict지정 안하면 Abort가 기본)

-트랜젝션 실패를 인지하기 위해서는 try{} catch(e:Exception)을 이용해야 

 

@ onConflict: @Insert에서 지정 가능하며 키가 중복되는 경우 처리 방식을 지정

-OnConflictStrategy.ABORT: key 충돌시 종료

-OnConflictStrategy.IGNORE: key 충돌 무시

-OnConflictStrategy.REPLACE: key 충돌시 새로운 데이터로 변경

 

@LiveData: 리턴 타입을 LiveData로 하면 Observer를 통해 업데이트 가능

@Query에서  SQL정의시 메소드 인자 사용 가능

@Query("SELECT * FROM student_table WHERE name = :sname")
suspend fun getStudentByName(sname: String): List<Student>

 

-예시

@Dao
interface MyDAO {
    @Insert(onConflict = OnConflictStrategy.REPLACE)  // INSERT, key 충돌이 나면 새 데이터로 교체
    suspend fun insertStudent(student: Student)

    @Query("SELECT * FROM student_table")
    fun getAllStudents(): LiveData<List<Student>>        // LiveData<> 사용

    @Query("SELECT * FROM student_table WHERE name = :sname")   
    suspend fun getStudentByName(sname: String): List<Student>

    @Delete
    suspend fun deleteStudent(student: Student); // primary key is used to find the student

    // ...
}

 

 

3)UI와 연결: 쿼리값이 LiveData인 경우 옵저버로 관측하여 실시간 업데이트 가능

val allStudents = myDao.getAllStudents()
allStudents.observe(this) {   // Observer::onChanged() 는 SAM 이기 때문에 lambda로 대체
    val str = StringBuilder().apply {
            for ((id, name) in it) {
                append(id)
                append("-")
                append(name)
                append("\n")
            }
        }.toString()
    binding.textStudentList.text = str
}

 

@주의점1: 위와같이 UI에 연결되어있는경우 테이블을 업데이트하는 DAO메소드를 사용하려면 UI스레드에서 바로 사용하면 안된다.

=>runBlocking{}이나 코루틴스코프 내에서 사용해야 함.

 

@주의점2: 위와같은 이유로 코루틴 스코프내에서 작업중인경우 UI를 건드리려면 다시 Main(=UI)스레드에서 작업해야함 

=>withContext(Dispatchers.Main)사용

withContext(Dispatchers.Main) {
    binding.textQueryStudent.text = ""
}

 

 

@데이터베이스 확인법

:하단 App Inspection 탭에서 Database Inspector을 누르면 확인 가능

 

 

3. 사용자 위치 얻기

 

@접근권한

- android.permission.ACCESS_COARSE_LOCATION: 와이파이나 모바일 데이터(또는 둘 다)를 사용해 기기의 위치에 접근하는 권한입니다. 도시에서 1블록 정도의 오차 수준

- android.permission.ACCESS_FINE_LOCATION: 위성, 와이파이, 모바일 데이터 등 이용할 수 있는 위치 제공자를 사용해 최대한 정확한 위치에 접근하는 권한

android.permission.ACCESS_BACKGROUND_LOCATION: 안드로이드 10(API 레벨 29) 이상에서 백그라운드 상태에서 위치에 접근하는 권한

 

1)내장 위치 매니저: LocationManager 사용

<1>LocationManager

val manager = getSystemService(LOCATION_SERVICE) as LocationManager

<2>위치제공자:GPS,Network, wifi, Passive(다른 앱에서 이용한 마지막 위치정보)

=>현재 위치제공자 목록: 

manager.allProviders

=>현재 사용가능한 위치 제공자

manager.getProviders(true)

 

<3>위치정보얻기

-getAccuracy():정확도

-getLatitude():위도

-getLongitude(): 경도

-getTime():획득 시간

[1]단일 위치

if (ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.ACCESS_FINE_LOCATION
            ) == PackageManager.PERMISSION_GRANTED
        ) {
            val location: Location? = manager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
            location?.let{
                val latitude = location.latitude
                val longitude = location.longitude
                val accuracy = location.accuracy
                val time = location.time
                Log.d("map_test", "$latitude, $location, $accuracy, $time")
            }
        }

[2]위치 계속 가져오기

val listener: LocationListener = object : LocationListener {
            override fun onLocationChanged(location: Location) {
                Log.d("map_test,","${location.latitude}, ${location.longitude}, ${location.accuracy}")
            }
        }
manager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 10_000L, 10f, listener)
manager.removeUpdates(listener)

-requestLocationUpdates(위치제공자, 위치업데이트 간격(밀리초), 위치 변경간격(미터), 위치 업데이트 받을 리스너)

 

 

2)구글 Play 서비스의 위치 라이브러리:FusedLocationProvider

<1>위치 제공자 지정시 고려사항

-전력 소비

-정확도

-API가 간단한가

-부가기능

-안드로이드 기기 호환성

=>위 사항을 기반으로 최적 알고리즘으로 위치제공자 지정

 

<2>implementation

implementation 'com.google.android.gms:play-services:12.0.1'

 

<3> FusedLocationProvider에서 핵심 클래스

[1]FusedLocationProviderClient: 위치 정보를 얻음

[2]GoogleApiClient: 위치제공자 준비등 여러 콜백 제공

val connectionCallback = object: GoogleApiClient.ConnectionCallbacks{
    override fun onConnected(p0: Bundle?) {
        // 위치 제공자를 사용할 수 있을 때
        // 위치 획득
    }

    override fun onConnectionSuspended(p0: Int) {
        // 위치 제공자를 사용할 수 없을 때
    }
}
val onConnectionFailCallback = object : GoogleApiClient.OnConnectionFailedListener{
    override fun onConnectionFailed(p0: ConnectionResult) {
        // 사용할 수 있는 위치 제공자가 없을 때
    }
}
val apiClient = GoogleApiClient.Builder(this)
    .addApi(LocationServices.API)
    .addConnectionCallbacks(connectionCallback)
    .addOnConnectionFailedListener(onConnectionFailCallback)
    .build()

 

 

<4>실행과정

[1]FusedLocationProviderClient초기화

val providerClient = LocationServices.getFusedLocationProviderClient(this)

[2]GoogleApiClient에 위치 제공자 요청

apiClient.connect()

[3]onConnected()에서 FusedLocationProviderClient의 getLastLocation()호출

override fun onConnected(p0: Bundle?) {
        if(ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) === PackageManager.PERMISSION_GRANTED){
            providerClient.lastLocation.addOnSuccessListener(
                this@MainActivity,
                object: OnSuccessListener<Location> {
                    override fun onSuccess(p0: Location?) {
                        p0?.let {
                            val latitude = p0.latitude
                            val longitude = p0.longitude
                            Log.d("map_test", "$latitude, $longitude")
                        }
                    }
                }
            )
            apiClient.disconnect()
        }
    }

 

 

4. 구글 지도앱 만들기

1)사전 설정

<1>implementation

implementation 'com.google.android.gms:play-services-maps:18.1.0'
implementation 'com.google.android.gms:play-services-location:21.0.1'

 

<2>permission

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET"/>

 

<3>구글 지도 API이용 키 등록: android manifest의 application내부에 작성

<uses-library android:name="org.apache.http.legacy" android:required="true"/>
<meta-data android:name="com.google.android.maps.v2.API_KEY"
      android:value="### 구글 지도 API 키 등록 ###"/>
<meta-data android:name="com.google.android.gms.version"
      android:value="@integer/google_play_services_version"/>

 

@구글 개발자 콘솔 (console.cloud.google.com) 에 접속해 프로젝트 생성 후 사용자 인증정보 만들면 지도 API키 발급

 

2)XML

<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/mapView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:name="com.google.android.gms.maps.SupportMapFragment"/>

 

3)지도 제어

<1>설정: 액티비티에서 OnMapReadyCallback을 상속받고 액티비티와 싱크시킨 뒤 onMapReady에서 작업을 하면됨.

class MainActivity : AppCompatActivity(), OnMapReadyCallback {

    var googleMap: GoogleMap? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        (supportFragmentManager.findFragmentById(R.id.mapView) as SupportMapFragment)!!.getMapAsync(this)

		// 지도 객체를 이용할 수 있는 상황이 될 때
    override fun onMapReady(p0: GoogleMap?) {
        googleMap = p0
    }
}

 

<2>지도의 중심을 이동 및 줌 변경: CameraPosition.Builder() =>moveCamera(CameraUpdateFactory.newCameraPosition(position))

val latLng = LatLng(37.566610, 126.978403)
        val position = CameraPosition.Builder()
            .target(latLng)
            .zoom(18f)
            .build()
        googleMap?.moveCamera(CameraUpdateFactory.newCameraPosition(position))

 

<3>마커 표시:MarkerOptions()

val markerOptions = MarkerOptions()
markerOptions.icon(BitmapDescriptorFactory.fromResource(R.drawable.ic_marker))
markerOptions.position(latLng)
markerOptions.title("서울시청")
markerOptions.snippet("Tel:01-120")

googleMap?.addMarker(markerOptions)

 

<4>위치 요청: LocationRequest

val locationRequest = LocationRequest.create().apply {
            interval = 1000
            fastestInterval = 500
            priority = LocationRequest.PRIORITY_HIGH_ACCURACY
        }

locationCallback = object : LocationCallback(){
    //1초에 한번씩 변경된 위치 정보가 onLocationResult 으로 전달된다.
    override fun onLocationResult(locationResult: LocationResult) {
        locationResult?.let{
            for (location in it.locations){
                Log.d("위치정보",  "위도: ${location.latitude} 경도: ${location.longitude}")

            }
        }
    }
}

 

<5>이벤트 처리

-GoogleMap.OnMapClickListener: 지도 클릭 이벤트

-GoogleMap.OnMapLongClickListener: 지도 롱 클릭 이벤트

-GoogleMap.OnMarkerClickListener: 마커 클릭 이벤트

-GoogleMap.OnMarkerDragListener: 마커 드래그 이벤트

-GoogleMap.OnInfoWindowClickListener: 정보 창 클릭 이벤트

-GoogleMap.OnCameraIdleListener: 지도 화면 변경 이벤트

googleMap?.setOnMapClickListener { latLng ->
            Log.d("map_test", "click : ${latLng.latitude} , ${latLng.longitude}")
        }
        
googleMap?.setOnMapLongClickListener { latLng ->
            Log.d("map_test", "long click : ${latLng.latitude} , ${latLng.longitude}")
        }
        
googleMap?.setOnCameraIdleListener {
            val position = googleMap!!.cameraPosition
            val zoom = position.zoom
            val latitude = position.target.latitude
            val longitude = position.target.longitude
            Log.d("map_test", "User change : $zoom $latitude , $longitude")
        }
        
googleMap?.setOnMarkerClickListener { marker ->
            true
        }
        
googleMap?.setOnInfoWindowClickListener { marker ->
        }

 

 

<6>예제는 학습자료 참고

https://teamsparta.notion.site/11-2-def99f594e3c4b86a3860d14a88ba0ab

 

11-2. 구글 지도앱 만들기 | Notion

지도하면 구글맵이지!

teamsparta.notion.site