공지

  • 코틀린 코루틴의 정석 책 출간

    코틀린 코루틴의 정석 책 출간 소식 안녕하세요. '조세영의 Kotlin World' 기술 블로그를 운영 중인 조세영입니다. 이번에 제가 저술한 『코틀린 코루틴의 정석』, 책이 출간되었습니다. 이 책은 많은 개발자들이 어렵게 느끼는 비동기 프로그래밍을 다양한 시각적 자료와 설명을 통해 누구나 쉽게 이해할 수 있도록 쓰였습니다. 안드로이드, 스프링 등 코틀린을 사용하는 개발자들 중 코루틴을 사용한 비동기 프로그래밍을 기초부터 심화까지 제대로 배워보고 싶은 분들께 추천합니다. 책에서 다루는 내용 풍부한 시각적 자료를 통해 초보 개발자라도 코루틴을 사용한 비동기 프로그래밍을 쉽게 이해할 수 있도록 설명한다. 코루틴 이전의 멀티 스레드 프로그래밍이 어떤 한계를 가졌는지, 코루틴이 그 한계를 어떻게 극복했는지를 설..

  • Kotlin Coroutines 공식 기술 문서 한국어 번역본 배포

    Kotlin Coroutines 공식 문서 번역을 시작하며 Kotlin Coroutines는 Kotlin을 위한 강력한 비동기 솔루션이다. 안드로이드 실무에서는 한동안 높은 점유율을 자랑한 RxJava를 Coroutines가 대체하고 있으며, 새로 시작하는 프로젝트들은 모두 Coroutines를 사용하고 있다. 그 이유는 Coroutines의 성능과 간결성, 가독성에 있다. Coroutines는 기존 스레드 모델들과 다른 경량 스레드(Light Weight Thread)라는 개념을 도입하여 불필요한 Thread Blocking을 방지할 수 있도록 하였으며, 직관적인 키워드를 통해 가독성을 높였다. 이러한 장점으로 많은 개발자들이 실무에서 Coroutines를 사용하기 시작했지만, 공부를 위한 자료가 많이..

  • [Jetpack Compose] Dynamic Theme 라이브러리 사용해 Compose Theme 관리 하기

    GitHub : https://github.com/seyoungcho2/ComposeDynamicTheme 도움이 되셨다면 스타★를 눌러주세요! Dynamic Theme 이란 무엇인가? 지금까지 안드로이드에서 테마를 변경하는 것은 매우 어려운 작업이었다. 이번에 배포한 Dynamic Theme은 안드로이드의 테마 관리를 편하게 만들기 위해 개발되었다. Dynamic Theme는 안드로이드 Jetpack Compose를 위한 Material Design 기반의 테마 관리 시스템으로 단순히 테마를 적용하고 싶은 곳 최상위에 'ProvidesTheme'을 추가하여 테마를 설정하는 것을 가능하게 한다. class MainActivity : ComponentActivity() { override fun onCre..

  • 문의&제안

    기술 서적 번역, 기술 서적 리뷰 등의 문의는 아래로 부탁드립니다. 연락처이메일 : seyoungcho2@gmail.com링크드인 : https://kr.linkedin.com/in/seyoungcho

  • [Compose UIs] Compose Filled Slider 라이브러리 사용해 슬라이더 만들기

    Filled Slider 란? 이번에 Compose용 Filled Slider 라이브러리를 배포했다. Filled Slider은 볼륨 컨트롤이나 밝기 제어 등에 사용할 수 있는 채워지는 형태의 슬라이더 입니다. Filled Slider 라이브러리에서 제공하는 슬라이더는 가로 모드 세로 모드, 슬라이더 모양 설정, Continuous Discrete, 터치 민감도 설정 등 많은 기능들을 지원한다. GitHub : https://github.com/seyoungcho2/FilledSliderCompose GitHub - seyoungcho2/FilledSliderCompose: Provides Filled Slider for Jetpack Compose Provides Filled Slider for Jet..

Coroutines

  • [Kotlin Coroutines] Log 를 사용한 Coroutines 디버깅

    로그를 사용한 디버깅의 필요성 Kotlin Coroutines는 같은 Coroutine Builder(launch, async 등) 의 중괄호 내부의 코드들이 다른 스레드에서 실행될 수 있다. 예를 들어 아래 코드에서 "task1 : start" 는 메인 스레드에서 실행되지만, "task1 : end"는 DefaultExecutor 스레드에서 실행된다. import kotlinx.coroutines.* fun main() = runBlocking { val task1 = launch(Dispatchers.Unconfined) { log("task1 : start") delay(100) log("task1 : end") } val task2 = launch { log("task2 : start") dela..

  • [Kotlin Coroutines] IntelliJ 사용해 Coroutines 디버깅 하기

    디버깅을 하기 위한 준비 Kotlin Coroutines는 일시 중단 후에는 다른 스레드에서 실행될 수 있기 때문에 디버깅을 하기가 매우 어렵다. 이 글에서는 디버깅을 하기 어려운 예 중에 가장 간단한 예시를 제시할 것이다. 다음의 코드를 보자. import kotlinx.coroutines.* fun main() = runBlocking { launch(Dispatchers.Unconfined) { println("launch1 Working Thread : ${Thread.currentThread().name}") delay(100) println("launch1 Working Thread : ${Thread.currentThread().name}") } launch { println("launch2..

  • [Coroutine Flow] flatMapMerge 을 사용해 flow 변환 동시 처리하기

    flatMapMerge는 무슨 역할을 하는가? flatMapConcat과 flatMapLatest는 flow에서 발행된 데이터를 변환할 때 발행된 순서대로 순차적으로 변환한다. 반대로 flatMapMerge는 변환을 병렬로 수행한다. 대부분의 연산이 flatMapConcat이나 flatMapLatest를 이용한 순차 처리에 해당하지만 들어오는 데이터들을 동시에 수집한 후 수집한 값들이 가능한 빨리 방출 될 수 있도록 병렬로 처리되어야 할 때가 있다. 예를 들어 비용 처리를 위해 수십개의 지출 데이터를 취합하여 합치는 작업을 할 경우 굳이 순차적으로 처리하지 않고 병렬로 처리되는 것이 빠를 것이다. flatMapMerge는 이러한 병렬 연산을 지원하기 위해 만들어진 연산자이다. flatMapConcat과 ..

  • [Coroutine Flow] flatMapLatest 이용해 최신 데이터만 사용해 flow 변환하기

    flatMapLatest란? flatMapLatest는 flow를 최신데이터만을 이용해 새로운 flow로 변환할 수 있도록 도와주는 함수이다. flatMapLatest를 사용하면 flow에서 발행된 데이터를 변환하는 도중 새로운 데이터가 발행될 경우, 변환 로직을 취소하고 새로운 데이터를 사용해 변환을 수행한다. collectLatest의 경우 먼저 발행된 데이터를 처리하는 도중 새로운 데이터가 들어올 경우 이전 데이터 처리를 취소하고 새로운 데이터를 이용해 데이터를 처리하는데 flatMapLatest는 collectLatest와 동작이 매우 유사하다. flatMapLatest 동작 살펴보기 예를 들어 다음과 같은 flow가 있다고 해보자. 이 flow는 1과 5를 순차적으로 발행한다. val flow ..

  • [Coroutine Flow] flatMapConcat을 사용해 flow를 다른 flow로 변환하기

    Flow의 Flattening Operator flow는 데이터 파이프라인이다. 코드 상에서 데이터 파이프라인은 그 자체로 사용되는 경우는 거의 없으며 보통 다른 데이터 파이프라인들과 합쳐져 하나의 데이터 파이프라인을 완성한다. flow 또한 여러 flow가 합쳐져 하나의 flow로 만들어지기 위한 연산자를 제공하는데 데이터 파이프라인을 합치는(Flatten) 연산자여서 Flattening Operator(하나로 만드는 연산자)라 한다. 우리는 이번 글에서 가장 대표적인 Flattening Operator인 flatMapConcat에 대해 다뤄볼 것이다. flatMapConcat 은 무엇을 하는가? flatMapConcat은 여러 flow를 연결하는(concatenating) 연산자이다. 이름에서 알 수..

  • [Coroutine Flow] conflate를 이용해 최신 데이터 collect 하기

    collectLatest를 이용한 최신 데이터 collect의 한계점 그림1과 같이 데이터 발행 시간 사이의 간격보다 데이터를 처리하는 suspend fun이 수행하는 시간이 오래 걸릴 경우, 새로 들어온 데이터는 계속해서 소비되지 못한다. 즉 이런 상황에서 collectLatest를 쓸 경우 중간 데이터를 하나도 얻지 못하고 마지막 데이터만을 얻을 수 있다. 예를 들어 아래 그림2와 같이 데이터 발행에 0.1초가 걸리는데 데이터 소비에 1초가 걸릴 경우 하나도 소비가 안되고 마지막 데이터만이 소비된다. conflate을 이용해 최신 데이터 collect하기 이를 해결하는 방법은 간단하다. 한 번 시작된 데이터 소비는 끝날 때까지 하고 데이터 소비가 끝난 시점에서의 가장 최신 데이터를 다시 소비하는 것이..

Kotlin

  • [Kotlin] @JvmRecord 사용해 자바의 record 클래스 정의하기

    자바의 Record 클래스 Java 14부터 Record라 불리는 데이터를 저장하는 클래스가 도입됐다. 이 Record는 코틀린의 Data class와 매우 유사한 기능으로, 'record' 키워드로 선언된 클래스는 equals, hashCode, toString 함수를 자동으로 생성한다. 예를 들어 다음과 같이 생성된 record 클래스 Blog가 있다고 해보자. *아래 코드는 Java이다. public record Blog(String name, int age) { } 이 Blog 클래스는 이름(name)과 생긴 후 지난 날짜(age)를 인자로 받는다. 이제 이 record 클래스가 어떻게 동작하는지 확인하기 위해 다음과 같이 코드를 만들어보자. public class Main { public st..

  • [Kotlin] 제네릭 사용 시 Cannot check for instance of erased type: T 오류 해결 방법 : inline과 reified를 사용한 문제 해결

    Cannot check for instance of erased type: T 오류는 왜 발생할까? JVM은 제네릭 타입을 실행 시점에 지원하지 않기 때문에, 자바의 제네릭과 마찬가지로 코틀린의 제네릭 또한 컴파일 타입에 타입 지워짐(Type Erasure)이 발생한다. 이 때문에 제네릭을 사용하는 일반 함수에서는 함수 본문에서 제네릭 타입을 이용해 연산을 할 수 없다. 예를 들어 다음과 isType 함수를 살펴 보자. fun isType(value: Any): Boolean { return value is T } 이 함수는 겉보기에는 문제가 없어 보인다. 하지만, T라는 타입은 컴파일 시점에 지워지고, 실행 시점에는 T가 무슨 타입인지 알 수 없기 때문에 다음과 같은 오류가 발생한다. Cannot ch..

  • [Kotlin] inline fun 함수를 사용한 고차 함수 최적화

    함수의 매개 변수로 람다식을 받을 경우의 문제 일반적으로 함수를 호출하면 해당 함수가 서브루틴으로써 실행된다. 반면 inline fun으로 선언된 함수를 호출하면, 함수 호출을 실행하는 것이 아니라 해당 함수가 호출된 위치에 함수 내부의 코드가 삽입돼 실행된다. 예를 들어 다음과 같은 코드가 있다고 해보자. fun main(args: Array) { printWorldAfterFunction { println("Hello") } } fun printWorldAfterFunction(function: () -> Unit) { function() println("World") } 이 코드에서 printlnWorldAfterFunction 함수를 () -> Unit 타입의 람다식과 함께 실행하면, () -> ..

  • [Kotlin] operator fun 을 통한 연산자 오버로딩 한번에 정리하기

    operator fun이란 코틀린은 특정한 부호의 연산을 함수로 정의할 수 있는 연산자 오버로딩 기능을 제공한다. 예를 들어 plus 라는 함수를 operator fun으로 선언하면 + 연산과 같은 효과를 낸다. 예를 들어 다음 Vector 클래스를 살펴보자. data class Vector(val x: Float, val y: Float) { operator fun plus(vector: Vector): Vector { return Vector(this.x + vector.x, this.y + vector.y) } } 이 Vector 클래스는 operator fun plus을 선언하고 있으며, 이 함수는 두 Vector를 더할 때 x 값은 x 값끼리, y 값은 y 값끼리 더해 Vector 객체의 x 값..

  • [Kotlin] MutableMap 사용 시 java.util.ConcurrentModificationException 문제 해결법

    문제 원인 Kotlin에서 mutableMapOf 함수를 통해 생성되는 MutableMap 객체는 내부적으로 Java의 LinkedHashMap을 사용한다. 이 LinkedHashMap은 내부에서 LinkedHashIterator이란 것을 사용하는데, 이 객체는 원소를 순환할 때 LinkedHashMap이 실제로 조작된 횟수(modCount)와 순환이 시작될 때 확인된 조작된 횟수(expectedModCount)의 값을 비교해 만약 두 값이 일치하지 않으면 ConcurrentModificationException을 발생시킨다. abstract class LinkedHashIterator { ... int expectedModCount; ... final LinkedHashMap.Entry nextNode..

  • [Kotlin] UByte, UShort, UInt, ULong 사용해 부호 없는(Unsigned) 정수 다루기

    코틀린의 양의 정수 타입 일반적인 다른 언어들과 같이 코틀린에서도 부호 없는 정수(0 보다 크거나 같은 정수)형 데이터 타입을 다루기 위한 다양한 타입들이 코틀린 1.5버전부터 지원되기 시작했다. 글의 제목이 있는 UByte, UShort, UInt, ULong이 그 타입들이다. 변수를 각 타입으로 만드는 방법은 어렵지 않다. 우리가 Float을 선언할 때 접미어로 f를 붙이는 것처럼 접미어로 U를 붙여주면 된다. fun main() { val uByte: UByte = 100U // U를 붙여주어야 한다. val uShort: UShort = 10_000U // U를 붙여주어야 한다. val uInt: UInt = 1_000_000U // U를 붙여주어야 한다. val uLong: ULong = 10..

Android

  • [Android] onBackPressed 함수 Deprecated 경고 해결 방법 한 번에 정리하기: onBackPressedDispatcher으로 이전하는 방법

    오류 메세지와 원인 분석 안드로이드 API33부터 onBackPressed 함수가 Deprecated 되면서 함수를 override하면 다음과 같은 경고 메세지가 뜨고 있다. 'onBackPressed(): Unit' is deprecated. Overrides deprecated member in 'androidx.core.app.ComponentActivity 직역하면 다음과 같은 뜻이다. 'onBackPressed(): Unit'은 더 이상 사용되지 않습니다. 'androidx.core.app.ComponentActivity'에서 더 이상 사용되지 않는 메서드를 재정의합니다. 당분간은 메세지가 뜬 상태로 있겠지만, 향후 몇 버전 뒤에는 코드가 없어질 것이므로, 이 글에서는 onBackPressed를..

  • [Android] onBackPressedDispatcher 사용해 뒤로가기 동작 설정하기: 뒤로가기 두 번 눌러 종료하기 예제

    onBackPressedDispatcher이란? 안드로이드에서 애플리케이션을 개발하다보면, 종종 Activity 혹은 Fragment에서 뒤로 가기 버튼에 대한 동작을 커스텀하게 정의해야 할 상황이 발생한다. 이렇게 뒤로 가기 버튼에 대한 동작을 커스텀하게 정의해야 할 때, Activity 혹은 Fragment에서 접근할 수 있는 onBackPressedDispatcher를 사용할 수 있다. *onBackPressedDispatcher 변수는 OnBackPressedDispatcher 객체를 가리킨다. onBackPressedDispatcher 사용해 뒤로가기 동작 설정하기 onBackPressedDispatcher를 사용해 뒤로 가기 동작을 정의하기 위해서는 두가지가 필요하다. onBackPressed..

  • [Android] LifecycleEventObserver 사용해 Activity 생명주기에 대한 콜백 등록하기: onStart, onResume, onPause, onStop 등에 대한 콜백 등록하기

    일반적인 Activity 생명주기에 대한 콜백 등록 방법 일반적으로 Activity 생명주기에 대한 콜백을 등록하기 위해서는 Activity 수준에서 onStart, onResume, onPause, onStop 등의 함수들을 override 하고 그 함수에 해당 콜백을 등록해야 한다. class MainActivity : ComponentActivity() { ... override fun onStart() { super.onStart() println("onStart 콜백") } override fun onResume() { super.onResume() println("onResume 콜백") } override fun onPause() { super.onPause() println("onPause..

  • JUnit5 사용 시 No tests found for given includes 오류 수정 방법

    JUnit 의존성 설정 시 발생하는 오류와 해결 방법 JUnit5를 사용하기 위해서는 다음 의존성을 추가해야 한다. dependencies { // JUnit5 테스트 프레임워크 testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") ... } 하지만, 이 둘을 사용해 테스트를 실행하면 다음과 같은 오류가 난다. Execution failed for task ':test'. > No tests found for given includes: [SimpleTest](--tests filter) 이런 애러가 나는 이유는 테스트를 실행할..

  • 오류 수정 방법 : Caused by: org.gradle.api.GradleException: 'compileDebugJavaWithJavac' task (current target is 17) and 'compileDebugKotlin' task (current target is 1.8) jvm target compatibility should be set to the same Java version.

    애러 원인 애 애러는 현재 Kotlin 버전과, Kotlin Compile 시 JVM 타겟 버전 설정이 달라서 생기는 문제이다. Caused by: org.gradle.api.GradleException: 'compileDebugJavaWithJavac' task (current target is [Kotlin 타겟 버전]) and 'compileDebugKotlin' task (current target is [JVM 타겟 버전]) jvm target compatibility should be set to the same Java version. 애러 해결 방법 이 문제는 아주 간단히 해결 가능하다. app 수준의 build.gradle.kts 파일 혹은 build.gradle 파일에 아래와 같이 적혀..

  • 오류 수정 방법 : BUG! exception in phase 'semantic analysis' in source unit '_BuildScript_' Unsupported class file major version

    애러 개요 여러 프로젝트를 다루다 보니, 앱을 빌드할 때 아래와 같은 오류가 나는 경우가 생겼다. BUG! exception in phase 'semantic analysis' in source unit '_BuildScript_' Unsupported class file major version 64 문제 원인 현재 Gradle 버전과 Java Version이 일치하지 않을 때 오류가 발생한다. 나의 경우 JDK 버전을 20으로 쓰고 Gradle 버전을 8.0으로 써서 문제가 생겼다. JDK 버전 확인 방법 1. 프로젝트에 설정된 JDK 버전을 확인하기 위해 다음 메뉴에 진입한다. Android Studio > Preferences... 2. Build, Execution, Deployment > Bu..

Spring

  • [Spring] Accept-Language 헤더에 따라 다국어 지원하는 방법 한 번에 정리하기

    HttpServletRequest 사용해 Accept-Lanauge 헤더 처리하기 HttpServletRequest를 사용해 Aceept-Lanauge 헤더를 처리하고, Locale 객체(지역 정보를 나타내는 객체)로 변환하기 위해서는 ServletRequest의 함수로 선언된 getLocale 함수를 사용하면 된다. @GetMapping("/bloginfo/kotlinworld") fun getBlogInfo( request: HttpServletRequest ): ResponseEntity { val locale: Locale = request.locale // getLocale 실행 해 HttpServletRequest의 Accept-Lanauge 헤더 정보를 Locale 정보로 변환 ?: retu..

  • [Spring] properties 파일의 Unsupported characters for the charset 'ISO-8859-1' 경고 메세지 해결 방법

    Unsupported characters for the charset 'ISO-8859-1' 경고 메세지의 발생 원인 이 메세지는 .properties 파일의 기본 인코딩이 ISO-8859-1 로 되어 있는데, 해당 인코딩으로 지원하지 않는 문자열을 쓸 때 발생한다. 이런 경우 경고를 무시하고 애플리케이션을 실행할 경우, 해당 문자를 읽으면 '????' 로 표기된다. 예를 들어 /bloginfo/kotlinworld 경로의 응답을 처리하는 함수를 만들어보자. 이 함수는 Accept-Language 값을 읽어 Locale 객체로 변환한다. @GetMapping("/bloginfo/kotlinworld") fun getBlogInfo( @RequestHeader("Accept-Language") locale..

  • [Spring] @RequestHeader 사용해 HTTP Request의 헤더 값 처리하기

    @RequestHeader 사용해 HTTP Request의 헤더 값 처리하기 @RequestHeader을 사용하면, Http Request의 헤더에서 특정 키에 대한 값을 변수에 매핑할 수 있다. 예를 들어 헤더에서 Accep-Language 의 값을 찾아내 accepLanguage 변수에 매핑하고 싶다면 다음과 같이 사용하면 된다. *물론 실제로 Accept-Language는 복수의 언어가 올 수 있기 때문에 이렇게 처리하면 안된다. 여기서는 간단한 예시를 위해 이렇게 만들었다. @Controller class HomePageController { @GetMapping("/blog/info") fun getBlogInfo( @RequestParam(value = "blogUrl") blogUrl: St..

CI/CD

  • toml 파일을 사용해 Gradle 의존성 관리하기

    toml 파일이란 무엇인가? TOML(Tom's Obvious, Minimal Language)은 GitHub의 공동 창립자인 Tom Preston-Werner가 만든 파일 형식으로, 이름 그대로, 간결하면서도 읽기 쉬운 파일 형식이다. 일반적으로 키-값쌍으로 값들을 관리하며, 하나의 파일로 여러 프로젝트의 버전 관리를 통합하는데 사용될 수 있다. 그동안 안드로이드에서는 주로 buildSrc를 사용해 버전 관리를 해왔는데, 최근 toml이 매우 활성화되기 시작해 이번 글에서 다뤄보고자 한다. *Android Developers 에서도 공식적으로 Version Catalog로 이전 이라는 명칭으로, toml 파일을 사용하는 방법을 설명하고 있으며, Android Gradle Plugin(AGP) 버전 7...

  • [GitHub Actions] if문 사용해 Job 실패 제어하기

    Step과 Job의 차이점 Step은 무조건 순차적으로 실행되는 반면, Job은 병렬적으로 실행될 수도 있고 순서대로 실행될 수도 있다. 이 말은 Step에서 실패를 제어하기 위해 사용했던 전제인 "먼저 실행된 Step은 이후 Step 시작 전에 끝난다"가 더이상 유효하지 않다는 뜻이다. 따라서 이 전제를 맞추기 위해 추가적인 설정을 해주어야 한다. 병렬적인 Job 간의 실패 제어 일단, 병렬적인 Job A와 Job B가 있다고 해보자. B가 A의 실패를 제어하는 것은 불가능하다. 이유는 B는 A에 대한 정보가 없기 때문이다. 하나의 Job이 다른 Job에 대한 정보를 알기 위해서는 needs Context를 사용해야 하는데, 병렬적인 Job 간에는 needs에 다른 Job의 정보가 없다. 공식 문서에는..

  • [GitHub Actions] outputs Context 활용해 특정 step이 fail 되었는지 체크하기

    Step 실패 시 흐름 제어 이전 글에서 if와 failure() 을 조합해 Step 실패 시 실행되는 Step을 정의해보았다. 그렇다면 만약 여러 Step이 있고 특정 Step이 fail 되었을 때만 수행되어야 하는 Step이 있으면 어떻게 해야할까? 바로 if 문에 step failure을 체크하기 위한 코드를 추가하는 것이다. 이를 위해 다른 step의 어떤 상태 값에 접근할 수 있는지 Context를 확인해보자. 특정 step이 fail 되었는지 체크하기 위한 Context steps Context 에서는 steps..outcome 를 통해 step 실행 결과에 대한 상태 값을 제공한다. 속성 이름 Type Description steps..outcome string continue-on-erro..