[번역] 더나은 성능을 위한 Kotlin Idiom
원문에서 topic에 직접적으로 부합하고 필요하다고 느껴지는 내용에 대해 부분적으로 옮겼습니다. 읽다 보면 이런 류의 글은 항상 반복되는 얘기들을 하고 있는걸 알 수 있습니다. 이펙티브 코틀린이나 다른 성능 관련 아티클에서 항상 다루는 몇몇 이야기들이죠. 같은 문제에 대해 어떻게 풀어서 얘기하고 있는지를 살펴보는것도 재밌을것 같습니다.
이 글에는 다수의 오역/의역이 포함되어 있습니다. 피드백은 언제나 환영합니다.
원문: https://magdamiu.medium.com/high-performance-with-idiomatic-kotlin-d52e099d0df0
훌륭한 사용자 경험은 여러가지에 의해 결정됩니다. 그중 모바일 개발자로써 알게된 것 확실한 한가지는 성능이 저조한 앱이 사용자를 멀어지게 만든다는 것이죠. 그래서 우리 개발자들은 지속적으로 앱의 성능을 개선하는데 집중해야 합니다.
이 문서에서는 앱을 개발할때 성능이 중요한 이유와 Kotlin 으로 어떻게 하면 성능을 향상 시킬 수 있는지에 대한 내용이 포함되어 있습니다. 이에 대해 더 알아보기전에 먼저 용어에 대해 정의하겠습니다.
🧐코틀린 Idiom은?
코틀린 Idiom은 언어의기능을 효과적이고 효율적으로 사용할 수 있는 관용구를 말합니다.
🧐성능의 의미
성능은 데이터가 전송되는 속도, 안정성 및 확장석, 앱이 응답하는 속도 또는 기기의 리소스를 사용하는 방법을 포함하는 복잡한 용어입니다.
모바일 개발자로서 우리의 역할은 앱을 사용하는 유저들의 사용자 경험을 향상시키는데 집중해야합니다. 가장 좋은 시나리오는 병목현상이 나타나 사용자 경험에 영향을 미치기 전에 먼저 찾아내는 것입니다.
성능이 곧 최고의 UX 입니다.
한 연구에 따르면 40%에 달하는 사용자들은 로드하는데 3초 이상 걸리는 웹사이트를 방문하는것을 포기합니다. 그리고 이런 사용자의 79%는 다시 그 사이트를 사용하고 싶지 않다고 합니다. 이렇게 부정적인 영향을 받은 사용자의 44%는 나쁜 경험을 주변 친구들과 공유한다고 합니다. 결국 낮은 성능은 고객 만족도에 부정적인 영향을 끼치는 것에 그치지 않고 나아가 브랜드 이미지를 손상시킵니다.
반대로 우리가 몇몇 유용한 Idiom을 익혀 앱의 성능을 향상시킬 수 있다면 고객 경험과 브랜드 이미지 향상에 기여할 수도 있습니다. 가장 좋은 사용자 경험은 성능입니다.
Kotlin Idiom 으로 성능 끌어 올리기
다양한 언어 많큼 많은 프로그래밍 페러다임이 있습니다. 그 중 Kotlin은 함수형 언어, 객체지향 언어의 특성을 갖습니다. Kotlin을 사용하며 얻을 수 있는 주요 이점 중 하나는 함수형 프로그래밍 접근 방식입니다.
Kotlin은 함수형 프로그래밍 패러다임을 지원하는 다음과 같은 기능들이 있습니다.
✅ Function Type: 함수는 Kotlin의 일급 시민이며 파라메터나 반환값으로 정의할 수 있습니다.
✅ 람다 표현식으로 보다 쉽게 코드 블록을 전달 할 수 있습니다.
✅ 데이터 클래스로 Immutable한 값을 다룰 수 있습니다.
✅ 함수형으로 Collections을 다루기 좋은 고차원 API를 지원합니다.
그럼 우리 프로젝트에 적용된 Kotlin의 성능을 끌어올릴 수 있는 몇가지 팁을 함께 알아보겠습니다.
💡 힌트 1: 여러 CPU에서 병렬 처리를 위한 순수 함수
순수 함수에는 사이드 이펙트가 없습니다. 순수 함수는 동일한 입력값에 대해 항상 동일한 값을 반환합니다. 컴파일러가 함수 호출을 최적화하고 결과값을 예상되는 값으로 치환 할 수 있기 때문에 멀티 코어에서 병렬 처리를 사용하는 앱을 개발할 때 강력한 기능입니다.
💡 힌트 2: 코드를 재사용하는 고차 함수
고차 함수를 사용하면 함수를 매개변수로 전달하거나, 함수를 반환하거나, 이 두 가지를 동시에 수행할 수 있기 때문에 기존 동작을 재사용할 수 있습니다.
10번부터 13번행 코드는 각 필터 함수의 내부 구현이 루프로 되어있기 때문에 성능 문제가 발생할 수 있습니다. 이러한 문제를 개선하기 위해 우리는 몇가지 가능한 대안을 생각해 볼 수 있습니다. 이중 26~33행의 함수 합성은 기존 함수를 재사용하기 위해 둘 이상의 함수를 결합하는 것입니다. 내부적으로 and()
확장함수는 주어진 세가지 함수를 서로 호출합니다. infix
키워드는 중첩된 함수 호출을 피하면서 코드의 합성을 수행하는데 도움이 됩니다.
💡 힌트 3: 캡쳐링 람다
람다는 코드 블록입니다. 람다는 함수 매개변수로 직접 전달할 수도 있는데, 이는 함수를 값으로 취급한다는 의미입니다. 클로저는 외부 범위에 정의된 변수에 접근 할 수 있는 함수입니다. 클로저는 해당 함수 외부의 변수를 캡쳐하기 때문에 캡쳐링 람다라고도 합니다. Java 에서는 final
이나 사실상 final
인 변수만 캡쳐할 수 있다는 점이 이미 알려져 있습니다. Kotlin은 이러한 제약이 없기 때문에 val
이나 var
변수를 캡쳐할 수 있습니다.
내부적으로는 val
값을 캡쳐할때마다 Java에서와 같이 값이 복사됩니다. var
값은 캡쳐될때 Ref
클래스의 인스턴스로 저장됩니다. 캡쳐링 람다 사용시 주의할 점은 람다가 매개변수로 전달될때마다 새로운 Function
인스턴스가 생성된다는 점입니다. 이런 점을 잊지 말고 조심해서 사용해야 합니다.
변수를 캡쳐하지 않는 일반 람다의 경우
Function
의 싱글톤 인스턴스가 생성됩니다.
💡 힌트 4: inline 함수와 reified.
inline
함수는 람다의 오버헤드를 제거하는 역할을 합니다. 함수에 inline
키워드를 사용하면 해당 함수가 호출되는 위치의 바이트코드에 해당 본문이 직접 삽입됩니다. 이점은 Function
인스턴스 생성을 피함으로써 람다를 매개변수로 사용하는 함수의 오버헤드를 줄일 수 있습니다.
Function
유형의 인스턴스를 생성하지 않아 오버헤드가 제거된 것을 보여줍니다.매개변수로 여러 람다가 있는 함수의 경우 noinline
키워드를 사용하여 일부는 inline을 선택하지 않도록 선택 할 수 있습니다.
⚠️주의 :
- 매개변수가 없는 함수에 인라인 수정자를 사용하는 경우 경험상 중요한 성능상의 이점을 얻지 못합니다. 그러나 함수 본문을 인라인하여 성능을 향상시킬 수 있는 예외도 있습니다.
- 너무 많은 함수에 inline
키워드를 적용하는 경우 바이트 코드의 크기를 증가시킵니다.
주) inline 키워드와 reified 에 관련된 내용은 이 글에서 보다 자세히 다루고 있습니다.
💡 힌트 5: 컬렉션 및 시퀀스
컬렉션으로 작업할 때 일반적인 권장 사항은 읽기 전용 컬렉션을 사용하는 것입니다. 읽기 전용 컬렉션은 상태 불일치와 관련된 오류를 미리 방지할 수 있기 때문입니다. 다음으로 권장되는 사항은 매우 많은 수의 요소를 처리해야 할 경우 sequence
를 사용하는 것입니다.
시퀀스는 각각의 작업에 대해 컬렉션 전체의 값을 계산하지 않고 eager evaluation 를 수행합니다. 따라서 컬렉션이 포함하는 요소가 많을 경우 불필요한 계산을 하지 않아 성능상 유리하게 동작합니다.
💡 힌트 7: 문자열 템플릿
문자열을 연결해야 하는 경우 Java 에서는 StringBuilder
를 사용합니다. Kotlin의 문자열 템플릿을 사용하면 내부적으로 StringBuilder
클래스 를 사용합니다. 다만 Kotlin 1.5.20은 JVM 9+ 환경에서 StringConcatFactory.makeConcatWithConstants()
를 사용하기 때문에 StringBuilder.append()
를 계속 사용하고 싶다면 컴파일 옵션에 -Xstring-concat=inline
을 추가해야 합니다.
주)
StringBuilder
는 생성자에서 별도로 설정해주지 않으면 초기 capacity가 16 characters로 설정되어 재할당으로 인한 추가 비용이 발생하기 쉬웠고 loop 문 내부에서 사용시StringBuilder
객체가 계속 생성되는 일이 발생했습니다. 이런 취약점을 개선하기 위해StringConcatFactory
가 도입되었습니다.
💡 힌트 9: @JvmField
@JvmField 주석을 사용 하여 이러한 변수를 속성이 아닌 필드로 사용하고 싶다고 컴파일러에 알릴 수 있습니다. @JvmField 주석을 사용하여 getter 및 setter 호출의 오버헤드를 방지할 수도 있습니다.
💡 힌트 10: Range
범위를 사용할때 접근 방식에 따라서 런타임 오버헤드가 발생할 수 있습니다. 아래 첫 이미지에서 볼 수 있듯 범위에서는 nullable 한 타입을 사용하지 않는게 좋습니다. 불필요한 객체 생성이 있을 수 있기때문입니다. 마찬가지 이유로 Range를 참조형으로 생성하지 않는것이 좋습니다. 가장 바람직한 접근 방식은 오른쪽 마지막 이미지와 같습니다.
결론.
이 글에서 다루는 주제가 주는 인사이트는 새로운 기능을 구현하거나 코드를 리펙토링할때 호기심을 가지고 모든 가능성을 평가해야 한다는 것입니다. 여러 대안들에 대해 배후에서 어떤 일이 일어나고 있는지 이해하고 객관적인 기준을 적용하여 목적을 달성하기 위한 적절한 결정을 내리는것이 좋습니다. 명확하지 않거나 질문이 있는 경우 자유롭게 의견을 남겨주세요. 읽어주셔서 감사합니다! 🙏🏽