메뉴 리뉴얼

알고리즘/프로그래머스 2021. 5. 29. 18:09 Posted by 아는 개발자

문제 푸는 키 포인트는 두개다.

 

1. 손님들마다 주문한 단품 메뉴 목록에서 가능한 조합을 목록으로 만든다.

 

손님이 최대 주문 할 수 있는 단품 메뉴의 개수가 10개다. 그러면 10개로 만들 수 있는 조합의 개수는 2^10 - 1개. 알고리즘 시험 문제에서 풀 수 있을 만큼의 범위다. 단품의 개수가 2개 이상인 조합만 고려대상이지만 이건 일단 무시하도록 하자.  코딩으로 조합 목록을 만드는 방법은 다양하게 있을텐데 내 경우에는 비트마스크를 사용했다. 만약 손님이 5개를 주문 했다면 총 31가지 조합이 만들어지므로, 1 ~ 31 까지 For 루프를 돌고 각 회차별로 비트를 확인 해서 단품 목록을 포함 시킬 것인지 말 것인지를 결정한다. 

 

for (int i = 0; i < orders.size(); i++) {

    int totalCombnation = (1 << (orders[i].length())) - 1;
    for (int bit = 1; bit <= totalCombnation; bit++) {
        vector<char> orderSet;
        string combi = "";
    
        for (int position = 0; (1 << position) <= bit; position++) {
            if (bit & (1 << position)) {
                orderSet.push_back((char) orders[i][position]);
            }
        }

        sort(orderSet.begin(), orderSet.end());

        for (int i = 0; i < orderSet.size(); i++) {
            combi += orderSet[i];
        }

 

totalCombination은 현재 주문에서 가능한 조합이 가능한 총 개수다. bit는 1에서부터 totalCombination 까지 순회하면서 단품 목록에서 조합이 가능한 쌍의 집합을 의미한다. 조합에 포함할지 말지는 position 변수가 있는 For 문에서 결정한다. 포함 유무는 orderSet 에 담고 오름차순으로 정리하고 문자열로 조합을 만들었다. 

 

2. 주문 조합 별로 노출 횟수 관리하기

 

모든 주문들의 조합을 순회하면서 노출된 횟수를 저장해야한다. 간단한 방법은 모든 주문 조합 별로 인덱싱이 가능한 int 배열을 만든 다음에 각각을 순회하면서 노출 횟수를 카운트 하는 것이다. 그런데 주문개수가 10개인 경우 알파벳 형태로 가능한 조합의 총 개수는 (26)*(25)*...(17) 이므로 배열에 둘 수 있는 구조가 아니다. 이럴때는 해시 자료구조를 쓴다. 문제에서 주어진 조건으로 가능한 모든 주문의 조합은 10 * 1024개이므로 해시 구조의 최적화를 이용한다면 메모리 범위 내에서 모든 자료구조를 담을 수 있다. 

 

map<string, int> combinations;
map<string, int>::iterator it = combinations.find(combi);

if (it == combinations.end()) {
    combinations.insert(make_pair(combi, 1));
} else {
    it->second++;
}

key와 value가 string, int인 해시 함수를 이용해서 주문 조합의 노출 횟수를 관리했다. 현재 주문 조합이 해시함수에 없으면 새로운 조합을 추가했고 있다면 노출 횟수를 +1 시켜주는 함수다. 이렇게 관리한 자료구조로 노출이 가장 많은 주문 조합을 고를 수 있다.

 

https://github.com/kwony/algorithm/blob/main/Programmers/72411.cpp

728x90

'알고리즘 > 프로그래머스' 카테고리의 다른 글

메뉴 리뉴얼  (0) 2021.05.29

Kotlin - Coroutine

개발/안드로이드 2021. 5. 21. 20:00 Posted by 아는 개발자

Coroutine을 공부할 때 당장 실행되는 코드를 짜려고 launch, async 함수부터 먼저 써보게 되는데(과거의 나) 이것보단 Coroutine을 이루는 구조가 무엇인지를 먼저 공부하고 유틸리티 함수를 사용하면 훨씬 이해하기가 쉽다.  Coroutine을 이루는 구조는 크게 CoroutineScope과 CoroutineContext다. 아래 그림으로 보면 CoroutineScope이 CoroutineContext를 포함하는 관계다.

 

 

1. CoroutineScope

 

CoroutineScope은 Coroutine이 활동할 수 있는 범위를 말한다. 예를 들어 Coroutine이 ViewModel의 생성주기 내에서만 동작하게 할 수 있고 Activity Lifecyle 생명주기를 따라서 동작하게 할 수 있는데 CoroutineScope은 Coroutine의 활동범위를 말한다. 이 속성을 잘 이용하면 Component의 생성주기에 맞춰 Coroutine 작업을 자동으로 취소할 수 있어서 유용하다. Kotlin에서는 안드로이드에서 사용할 수 있도록 몇가지 CoroutineScope을 미리 만들어뒀다. 

 

  • GlobalScope: 앱 프로세스의 생명주기를 따라감. 
  • MainScope: UI 관련 작업을 처리하는 용도.
  • ViewmodelScope: ViewModel의 생성주기를 따라감.
  • LifecycleScope: Activity, Fragment의 생명주기를 따라감. 생명주기별로 콜백이 다르다.
MainScope().launch {}

GlobalScope.launch {}

 

CoroutineScope 인터페이스를 구현해서 커스텀한 CoroutineScope을 만들수도 있긴 한데 공식 문서에서 이 방법은 추천하진 않고 있다.

 

2. CoroutineContext 

 

CoroutineContext는 Coroutine을 이루는 정보다. Coroutine 이름, Job, Dispatcher, ExceptionHandler 가 이에 해당한다. Dispatcher는 Coroutine이 실행될 쓰레드 풀을 의미한다. 대표적으로 Main, IO 쓰레드 풀이 있어서 UI 작업의 경우에는 Main, 디스크 작업에는 IO 쓰레드를 사용하도록 지정 할 수 있다. Executors 라이브러리를 이용해 커스텀으로 만든 쓰레드 풀에도 지정이 가능하다. 아래 코드는 우선순위가 높은 쓰레드 풀에서 동작하는 CoroutineScope을 만든 예다.

 

val customExecutor: Executor = Executors.newCachedThreadPool { r ->
    Thread(r, "CustomThread").apply {
        priority = Thread.MIN_PRIORITY
    }
}
val customDispatcher = object : CoroutineDispatcher() {
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        customExecutor.execute(block)
    }
}

CoroutineScope(customDispatcher).launch {

}

 

ExceptionHandler는 Coroutine 내의 코드 실행중 발생하는 Exception을 처리할 수 있는 Handler다. 현재 Scope 별로 Exception Handler를 다르게 둘 수 있기 때문에 이것도 잘 써먹으면 유용하다. 

 

val handler = CoroutineExceptionHandler { context, th->
    println("$context ${th.toString()} ")
}

GlobalScope.launch(handler) {
    val async1 = async(){ 1 }
}

 

3. Utility 함수 

 

3.1 launch

 

launch 함수는 CoroutineScope내에서 실행되며, 현재 쓰레드를 막지 않고(blocking) 동작할 수 있는 새로운 Coroutine Job을 생성한다. 병렬로 수행되기 때문에 여러가지 작업을 동시에 수행할 때 쓰면 좋다. 아래 코드는 GlobalScope 내에서 두개의 Coroutine Job을 생성한 코드다. 앞에 코드에 300ms 의 딜레이를 줬다. 그 결과 scope2가 먼저 프린트 되고, scope1은 그 이후에 프린트 된다.

 

GlobalScope.launch {
    launch {
        delay(300)
        println("scope1")
    }
    launch {
        println("scope2")
    }
}

2021-05-21 16:14:17.677 I/System.out: scope2
2021-05-21 16:14:17.978 I/System.out: scope1

 

Job 내부 함수인 join() 은 동시성을 제어할 수 있는 함수다. 현재 Coroutine의 실행이 종료되지 않을 때까지 다음 코드를 실행하지 않는다. 순서를 관리할 때는 이 함수를 쓰면 된다. 그리고 cancel() 처럼 취소할 수 있는 함수도 있다. 이건 실제로 사용하다 보면 어떻게 써야하는지 감이 온다.

 

3.2 async 

 

async 함수는 launch 와 거의 동일하고 결과 값을 받을 수 있다는 점이 추가 됐다. 아래 코드의 두 async Job은 각각 1, 2를 리턴하는 CoroutineScope이다. async 내부 await() 함수는 여기서 실행된 결과 값을 받아오게 된다. 여기서 주의깊게 볼 부분은 각각에 delay를 300ms, 100ms 씩 줬는데도 start 로그로부터 결과 값까지 걸린 시간은 둘의 합인 400ms가 아니라 가장 긴 delay인 300ms라는 점이다. 두 Job을 병렬로 처리했기 때문에 가장 delay를 오래 잡는 Job의 시간만큼 소요된다.

 

GlobalScope.launch {
    println("start")
    val a = async {
        delay(300)
        1
    }
    val b = async {
        delay(100)
        2
    }

    println("a + b = ${a.await() + b.await()}")
}

2021-05-21 16:22:19.885 I/System.out: start
2021-05-21 16:22:20.190 I/System.out: a + b = 3

 

3.3 withContext 

 

동일한 CoroutineScope 내에서도 종종 Coroutine Context를 바꿔야 할 일이 생긴다. 예를 들면 I/O 작업을 수행 중에도 중간중간 화면 UI를 업데이트 해야하는 경우 Dispatcher를 바꿔 코드에 적용하는 쓰레드를 변경해야한다. 이럴때 쓰면 유용한 함수가 withContext다. 아래 코드는 I/O 스레드 풀에서 "abc", "def"라는 문자열을 받아오고 두 문자를 합해서 text라는 변수를 만들었다. UI 에 적용하려면 withContext를 이용해 임시로 Main함수로 바꿔주어 UI 컴포넌트에 접근 할 수 있다. 참고로 withContext는 내부적으로 async{}.await()로 구현돼 있어 내부 코드가 모두 실행된 다음에 다음 코드로 넘어가게된다. 

 

CoroutineScope(Dispatchers.IO).launch {
    val a = async {
        "abc"
    }
    val b = async {
        "def"
    }
    
    val text = a.await() + b.await()
    
    withContext(Dispatchers.Main) {
        textView.text = text
    }
}
728x90

'개발 > 안드로이드' 카테고리의 다른 글

Serializable 과 Parcelable  (0) 2021.06.19
kotlin lateinit, lazy by  (0) 2021.06.05
Kotlin - Coroutine  (0) 2021.05.21
Android 10 스토리지 정책 대처하기  (0) 2021.05.18
다음 페이지가 살짝 보이는 ViewPager2 만들기  (0) 2021.05.13
움직이는 TextView  (0) 2021.05.11

클린 아키텍처

기술/아키텍처 2021. 5. 20. 19:51 Posted by 아는 개발자

 

로버트 마틴의 클린 코드에선 코드를 깔끔하게 잘 짜는 방법을 배웠다면 클린 아키텍처에서는 소프트웨어를 더 잘 만드는 방법을 배운 것 같다. 책의 표현을 빌리자면 클린 코드에서는 좋은 벽돌을 구분하는 방법을 배웠다면 클린 아키텍처에서는 좋은 벽돌로 건물을 짓는 방법을 배운 느낌이랄까. 책에선 저자가 경험한 내용을 바탕으로 전달하려는 교훈이 많다. 저수준, 고수준, 프레임워크는 세부사항일 뿐이다 등등.. 그런데 나의 소프트웨어 깊이가 부족해 공감하기 어려운 부분도 있었고 이해되지 않던 부분도 있어서 모두 소화하진 못했다. 그래도 연차가 늘어나고 더 큰 규모의 소프트웨어를 경험하다보면 이 책에서 내가 캐치하지 못했던 새로운 면이 보일 것 같아 기대된다. 2-3년 후에 다시 이 책을 읽어봐야 겠다.

 

많은 전달 내용 중 내 머릿속을 관통하는 소프트웨어 원칙은 이 그림으로 표현 할 수 있다.

 

 

다이어그램 상에선 컴포넌트A 가 컴포넌트B를 가리키고 있는 그림인데 소프트웨어상에선 컴포넌트A 가 컴포넌트B 에 의존한다는 의미의 그림이다. 이 의존 관계는 소프트웨어 상에서 생길 수 있는 가장 중요한 관계고 이 관계를 어떻게 정의하느냐에 따라서 소프트웨어의 아키텍처가 결정된다. 컴포넌트A는 저수준으로, 컴포넌트B는 고수준으로 둬야한다고 그림상에선 표현 했는데 여기서 말하는 수준은 어떤 컴포넌트가 우월한지를 결정하는 기준이 아니라 얼마만큼 변동성이 크냐를 기준으로 결정한다.

 

위 그림처럼 의존관계가 성립되려면 고수준인 컴포넌트B는 수정할 일이 적어야 한다. 그래야 컴포넌트A에 미치는 영향을 최소화 할 수 있기 때문이다. 위 그림처럼 결정되는 대표적인 예가 애플리케이션에서 String, Math 같은 자바 고유 라이브러리 클래스를 사용하는 경우다. 자바 버전에 따라서 클래스가 변경될 소지가 있지만 그래도 우리가 개발하는 클래스보다 변경될 소지는 적다. 이런 경우 의존 관계는 적절한 것으로 볼 수 있다.

 

그래도 가끔은 이런 의존 관계를 성립하기 힘든 경우도 있다. 둘다 변경의 소지가 크지만 두 클래스를 연결해야할 때가 있다. 이럴때 사용하는 방식이 의존성 역전원칙이다. 좀더 고수준으로 보이는 클래스에 특정 인터페이스를 만들고 이것과 상속 관계로 만든다. 그리고 저수준 클래스를 인터페이스에 의존하는 관계로 만든다. 인터페이스는 변경될 소지가 적기 때문에 이 의존 관계도 적절한 관계로 볼 수 있다.

 

 

물론 매번 이렇게 코딩할 수는 없다. 개발하다보면 새로운 함수도 추가해야돼 인터페이스도 손될 일이 많아지니까. 모든 원칙을 지키다 보면 오버 엔지니어링이돼 개발 프로세스가 느려지는 부작용도 생길 수 있다. 항상 모든 원칙을 지키기는 어려울 것 같다. 하지만 원칙을 알고 생략하는 것과 모르고 넘어가는 것은 차이가 크다. 앞으로 일하면서 어떤 원칙을 넘기면서 개발하고 있는지 되새겨봐야 할 것 같다.

728x90

'기술 > 아키텍처' 카테고리의 다른 글

오브젝트 리뷰 - 1  (0) 2021.07.11
응집도(Cohesion)와 결합도(Coupling)  (0) 2021.07.10
클린 아키텍처  (0) 2021.05.20
DIP(Dependency Inversion Principle)  (0) 2021.05.09
ISP (Interface Segregation Principle)  (0) 2021.05.09
LSP (Liskov Substitution Principle)  (0) 2021.05.09

Android 10 스토리지 정책 대처하기

개발/안드로이드 2021. 5. 18. 20:40 Posted by 아는 개발자

targetSdkVersion 을 30으로 올리면 파일 절대 경로를 사용해서 접근 할 수 없기 때문에 개발자들은 지금부터 슬슬 절대 경로를 사용해서 접근하는 코드를 변경해야한다. 이번 포스트에서는 안드로이드 새로운 스토리지 정책을 적용한 과정을 다뤄본다.

 

1. 절대 경로 대신 Uri 를 사용하도록 변경

 

기존에는 ContentResolver 클래스를 이용해 파일을 읽어올 때 DATA 칼럼을 이용해서 파일의 절대 경로를 읽어올 수 있었다. 그런데 DATA 컬럼은 Android 10부터 Deprecated가 됐고, targetSdkVersion 30으로 올리면 DATA 칼럼으로 얻을 수 있는 절대 경로로 파일이 접근이 되지 않는다. 

 

private suspend fun loadVideoContent(): Cursor? = coroutineScope {
    val where = MediaStore.Video.VideoColumns.SIZE + " > " + 0
    val sortOrder = MediaStore.Files.FileColumns.DATE_ADDED + " DESC"
    val projections = listOf(
        MediaStore.Video.Media._ID,
        MediaStore.Video.Media.DATA, // Deprecated됨
        MediaStore.Video.Media.DISPLAY_NAME
    ).toTypedArray()

    return@coroutineScope requireActivity().contentResolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, projections, where, null, sortOrder)
}

 

 

이제는 우리에게 익숙한 절대 경로 대신 Uri를 이용한 상대 경로를 사용해야한다. Uri는 content:// 로 시작하는 문자열인데, _ID 칼럼에서 얻어온 값과 ContentUri 클래스를 이용해서 얻어올 수 있다. 이 값도 파일을 찾는 경로로 사용되며 현재 Glide, MediaMetadataRetriever, Exoplayer처럼 유명한 안드로이드 라이브러리들은 Uri를 통해서도 파일을 불러올 수 있게끔 업데이트가 된 상태라 호환성은 크게 걱정하지 않아도 된다. 절대경로와 다른점은 실제 파일의 경로를 보여주지 않아 플랫폼 보안적인 요소가 강화된다. 반대로 개발자의 피로도는 악화되고.

 

val uriCol = cursor.getColumnIndex(MediaStore.Video.Media._ID)

do {
    mediaItems.add(
        MediaItem(
            ContentUris.withAppendedId(
                MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
                cursor.getLong(uriCol)
            ),

 

2. File 클래스를 선언해야하는 경우 

 

문제는 File 클래스를 선언해야하는 경우다. 오래된 오픈소스거나 Uri를 고려하지 않은 모듈인 경우엔 절대 경로가 필요한 File 클래스를 사용해야하는 경우가 종종 있다. 그런데 앞서 언급했듯이 Uri는 상대 경로다. Uri를 통해 File 클래스로 바꿀수 있긴 한데 이건 file 스키마를 가진 Uri인 경우에만 그렇다. Uri 클래스에 Kotlin에 확장 코드로 toFile() 함수가 있긴 한데 ContentUris로 얻어온 Uri 클래스에 쓰면 요런 에러가 뜬다.

 

 

이럴 때는 절대 경로를 읽을 수 있는 형태로 꼼수가 필요하다. Android 10부터 스토리지를 절대 경로로 접근하는 것은 안되지만 앱 전용 캐시 영역은 여전히 절대 경로로 접근 할 수 있다. 스토리지에 있는 파일을 캐시로 복사하면 복사한 파일의 절대 경로로 파일 클래스를 선언해줄 수 있다. copy 작업이 딜레이도 있고 불필요하게 캐시영역 써야해 완벽한 방법은 아니다. 하지만 라이브러리에서 Uri를 지원하기 전까지는 써먹을 수 있을 것 같다. 더 좋은 방법이 있다면 공유해주시면 좋겠다. 나는 이것 말고는 딱히 방법을 못찾겠다...

 

val dir = File(context.cacheDir.path + File.separator + effectFolderName)
val filePath = context.cacheDir.path + File.separator + effectFolderName + File.separator + filename
val file = File(filePath)
val inputStream = getApplication<App>().contentResolver.openInputStream(uri)

try {
    FileUtils.copyToFile(inputStream, it) // org.apache.commons.io 를 사용
} catch (e: IOException ) {
    e.printStackTrace()
}

 

3. 미디어 파일을 추가하는 경우

 

앱에서 이미지나 동영상을 다운받는 경우 예전에는 Environment.getExternalStorageDirectory().path 코드를 이용해서 직접 원하는 경로에 파일을 생성해서 추가할 수 있었으나 Android 10 부터는 ContentResolver를 이용해 Uri로 파일을 추가해야한다. 아래 코드는 이미지 파일을 저장소에 추가하는 코드다. ContentValues 값을 설정해 임의의 이미지 파일을 만든 후 insert 함수에서 생성된 Uri 변수로 FileOutputStream을 만들고 I/O 라이브러리를 이용해 기존 파일과 복사하는 작업이다. 관계형 데이터베이스에 새로운 행을 추가하고 값을 업데이트한다고 보면 쉬울 것 같다. 실제로 ContentResolver는 관계형 데이터베이스 쿼리랑 상당부분 흡사하다.

 

val collection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)

val contentvalues = ContentValues().also {
    it.put(MediaStore.MediaColumns.RELATIVE_PATH, "Images")
    it.put(MediaStore.MediaColumns.DISPLAY_NAME, name)
    it.put(MediaStore.MediaColumns.IS_PENDING, true)
}

val uri = context.contentResolver.insert(collection, contentvalues)
val fos = context.contentResolver.openOutputStream(uri!!, "w")

try {
    FileUtils.copyFile(sourceFile, fos)
} catch (e: IOException) {
    return false
}
values.put(MediaStore.MediaColumns.IS_PENDING, false)
context.contentResolver.update(uri, values, null, null)

 

728x90

아래 그림처럼 ViewPager 형태인데 다음 페이지가 살짝 보이는 UI를 만드는 경우가 종종 있다. 이번 포스트에서는 ViewPager2를 이용해 이 화면을 만드는 방법을 다뤄보려고 한다.

 

val currentVisibleItemPx = DimensionUtils.dp2px(requireContext(), 40f).toInt()

margin_pager.addItemDecoration(object: RecyclerView.ItemDecoration() {
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        outRect.right = currentVisibleItemPx
        outRect.left = currentVisibleItemPx
    }
})

ViewPager에서 inflate 된 페이지는 부모의 width를 따라가게 되므로 우선 각 페이지가 전체 영역을 잡지 않게 여백을 만들어둔다. 현재 보여진 위치로부터 양 옆에 margin을 추가한다. 그러면 아래 그림과 같은 상태가 된다.

 

 

val nextVisibleItemPx = DimensionUtils.dp2px(requireContext(), 20f).toInt()
val pageTranslationX = nextVisibleItemPx + currentVisibleItemPx

margin_pager.offscreenPageLimit = 1

margin_pager.setPageTransformer { page, position ->
    page.translationX = -pageTranslationX * ( position)
}

 

다음은 이전 페이지와 다음 페이지에 이동 효과를 줘야한다. 먼저 첫번째는 offscreenPageLimit 값을 설정는데 이 속성은 ViewPager2가 스크린에 현재 페이지로부터 얼만큼 떨어져 있는 페이지를 미리 생성 할 것인지 설정하는 함수다. offscreenPageLimit 값이 1이고, 5번 페이지가 현재 위치라면 ViewPager2는 4, 6번 페이지도 미리 생성 해둔다.

 

그 다음은 setPageTransformer 함수를 사용하는 것이다. 이 콜백은 현재 포커싱된 page 뷰 객체를 받을 수 있고 각 page 별로 포커싱 된 페이지로부터 얼마만큼 떨어져 있는지 비율 정보를 position으로 받을 수 있다. 아래 그림에선 현재 페이지가 1번에 포커싱돼 있어서 이전 페이지인 0번 페이지는 -1f만큼 떨어져 있게 되고, 다음 페이지인 2번 페이지는 1f만큼 떨어져 있게 된다. 

 

 

이 정보 값을 이용하면 0번과 2번 페이지를 필요한 만큼 이동시킬 수 있다. translationX 값을 변경하면 0번과 2번 페이지가 움직여서 미리보기 형태로 볼 수 있게 된다.

 

728x90