본문 바로가기
Kotlin

[Kotlin] 공식 코틀린 코딩 컨벤션 소개

by 노력남자 2023. 6. 6.
반응형

이번 포스팅에선 코틀린 공식 홈페이지에서 소개하고 있는 코틀린 코딩 컨벤션을 정리하려고 한다.
 
단순 번역하는 것도 있지만 상세한 설명을 추가해보려고 한다.
 
가보자!
 

코틀린 코딩 컨벤션

 
일반적으로 알려진 쉽게 따라갈 수 있는 코딩 규칙은 Kotlin을 사용하는 프로젝트에 필수적입니다. Kotlin 프로젝트에서 코드 스타일과 코드 구성에 대한 가이드라인을 제공합니다.
 

IDE에서 스타일 구성하기 (Configure style in IDE)


Kotlin에 대해 가장 인기 있는 두 개의 IDE 인 IntelliJ IDEA와 Android Studio는 코드 스타일에 대한 강력한 지원을 제공합니다. 이들을 구성하여 지정된 코드 스타일과 일관되게 코드를 자동으로 포맷할 수 있습니다.


- 스타일 가이드 적용하기


Settings/Preferences - Editor - Code Style - Kotlin으로 이동 -> Set from...클릭 -> Kotlin style guide 클릭
 


- 코드가 스타일 가이드를 따르는지 확인하기


Settings/Preferences - Editor - Inspections - General로 이동 -> Incorrect formatting inspection 체크
 

 
네이밍 규칙과 같은 스타일 가이드에서 설명하는 다른 문제를 확인하는 추가적인 검사는 기본적으로 활성화되어 있습니다.
 

소스 코드 구성 (Source code organization)


- 디렉토리 구조


순수한 Kotlin 프로젝트에서는 일반적으로 패키지 구조를 따르는 디렉토리 구조가 권장됩니다. 공통 루트 패키지는 생략됩니다. 
 
예를 들어, 프로젝트의 모든 코드가 org.example.kotlin 패키지와 그 하위 패키지에 있는 경우, org.example.kotlin 패키지의 파일은 소스 루트 바로 아래에 배치되어야 하며, org.example.kotlin.network.socket에 있는 파일은 소스 루트의 network/socket 하위 디렉토리에 있어야 합니다.
 

 
main.kotlin.com.example.kotlin ~ 으로 시작하는 게 아니라 공통 루트 패키지인 main.kotlin은 생략된다.

On JVM: Kotlin이 Java와 함께 사용되는 프로젝트에서는 Kotlin 소스 파일이 Java 소스 파일과 동일한 소스 루트에 있어야 하며, 동일한 디렉토리 구조를 따라야 합니다. 각 파일은 해당 패키지 문에 해당하는 디렉토리에 저장되어야 합니다.
 

- 소스 파일 이름


Kotlin 파일에 단일 클래스 또는 인터페이스(관련된 최상위 선언을 포함할 수도 있음)가 포함되어 있는 경우, 해당 파일의 이름은 클래스의 이름과 동일하며 확장자로 .kt를 추가해야 합니다. 이는 모든 종류의 클래스와 인터페이스에 적용됩니다. 파일에 여러 개의 클래스가 포함되어 있거나 최상위 선언만 있는 경우, 파일이 포함하는 내용을 설명하는 이름을 선택하고 파일을 그에 맞게 명명해야 합니다. 첫 글자는 대문자로 쓰고 나머지는 대문자와 소문자를 혼용하는 대문자 카멜 표기법(또는 파스칼 표기법)을 사용해야 합니다. 
 
예를 들어, ProcessDeclarations.kt와 같이 사용합니다.

파일 이름은 파일의 코드가 하는 일을 설명해야 합니다. 따라서 파일 이름에 Util과 같은 의미 없는 단어를 사용하는 것을 피해야 합니다.


- 소스 파일 구성


하나의 Kotlin 소스 파일에 여러 선언(클래스, 최상위 함수 또는 프로퍼티)을 배치하는 것은 의미적으로 밀접하게 관련되어 있고 파일 크기가 합리적인 범위(수백 줄을 초과하지 않는)를 유지하는 한 권장됩니다.
 
예)
 

// UserService.kt

class UserService() {

    fun getUser(): User? { ... }

    fun updateUser() { ... }
}

data class User(
    val id: Long,
    val type: Type,
    val name: String? = "default",
    val address: Address,
    val phones: List<String>,
    val active: Boolean
) {

    data class Address(
        val zipCode: String,
        val basicAddress: String,
        val detailAddress: String
    )

    enum class Type {
        ADMIN,
        USER
    }
}

 
코틀린 사용하면서 의아했던 부분이다. 하나의 클래스 파일에 여러 클래스가 들어간다니 신기했다.
 
특히, 클래스의 모든 클라이언트에게 관련이 있는 확장 함수를 정의할 때는 해당 클래스 자체와 동일한 파일에 위치시키는 것이 좋습니다.
 
특정 클라이언트에게만 의미 있는 확장 함수를 정의할 때는 해당 클라이언트 코드 옆에 위치시키세요. 
 

// Item.kt

class Item(val name: String, val price: Int) { ... }

fun Item.applyDiscount(discountPercentage: Double): Int { ... }

 
어떤 클래스의 모든 확장을 보관하기 위해 파일을 생성하는 것을 피하십시오.
 

- 클래스 레이아웃

 
클래스의 내용은 다음 순서로 작성되어야 합니다:
 

  1. 속성 선언과 초기화 블록
  2. 보조 생성자
  3. 메서드 선언
  4. Companion object

 
메서드 선언을 알파벳 순서나 가시성에 따라 정렬하지 마세요. 또한 일반 메서드와 확장 메서드를 분리하지 마세요. 대신, 관련된 내용을 함께 두어 클래스를 위에서 아래로 읽는 사람이 논리를 따를 수 있도록 하세요. 상위 수준의 내용을 먼저 작성하거나 그 반대로 작성할 수 있으며, 선택한 순서를 고수하세요.

중첩된 클래스는 해당 클래스를 사용하는 코드 바로 옆에 위치시키세요. 클래스가 외부에서 사용되고 클래스 내부에서 참조되지 않는 경우에는 동반 객체 다음에 배치하세요.
 
예)
 

class MyClass {

    // 속성 선언
    private val property1: String
    private val property2: Int

    // 초기화 블록
    init {
        property1 = "Hello"
        property2 = 42
    }

    // 보조 생성자
    constructor(param1: String) { ... }

    // 보조 생성자
    constructor(param1: String, param2: Int) { ... }

    // 메소드 선언
    fun method1() { ... }

    // 메소드 선언
    fun method2() {
    	InternalNestedClass()
    	... 
    }

    // 내부에서 사용된 Nested classes
    private class InternalNestedClass { ... }

    // Companion object
    companion object { ... }

    // Nested classes
    class NestedClass { ... }

    // 외부에서 사용된 Nested classes 
    class ExternalNestedClass { ... }
}

 

- 인터페이스 구현 레이아웃

 
인터페이스를 구현할 때는 인터페이스의 멤버와 동일한 순서로 구현 멤버를 유지하세요. (필요한 경우 구현에 사용되는 추가 비공개 메서드와 함께 섞어 사용하세요).

 

interface MyInterface {
    fun method1()
    fun method2()
    fun method3()
}

class MyClass : MyInterface {
    override fun method1() { ... }

    override fun method2() { ... }

    override fun method3() { ... }

    private fun helperMethod() { ... }
}


- 오버로드 레이아웃


클래스 내에서 항상 오버로드된 메서드를 함께 두세요.
 

class Calculator {
    fun add(num1: Int, num2: Int): Int {
        return num1 + num2
    }

    fun add(num1: Double, num2: Double): Double {
        return num1 + num2
    }

    fun subtract(num1: Int, num2: Int): Int {
        return num1 - num2
    }

    fun subtract(num1: Double, num2: Double): Double {
        return num1 - num2
    }
}

 

네이밍 룰 (Naming rules)

 
Kotlin에서의 패키지와 클래스 명명 규칙은 꽤 간단합니다:
 

  • 패키지 이름은 항상 소문자로 작성하고 언더스코어를 사용하지 않습니다 (org.example.project). 여러 단어를 사용하는 것은 일반적으로 권장되지 않지만, 필요한 경우 단어를 연결하거나 카멜 케이스를 사용할 수 있습니다 (org.example.myProject).
  • 클래스와 객체의 이름은 대문자로 시작하고 카멜 케이스를 사용합니다:

 

open class DeclarationProcessor { /*...*/ }

object EmptyDeclarationProcessor : DeclarationProcessor() { /*...*/ }

 

- 메소드

메소드, 속성 및 지역 변수의 이름은 소문자로 시작하고 캐멀 케이스(camel case)를 사용하며 밑줄은 사용하지 않습니다.

 

fun processDeclarations() { /*...*/ }
var declarationCount = 1

 

예외: 클래스의 인스턴스를 생성하는 팩토리 함수는 추상 반환 유형과 동일한 이름을 가질 수 있습니다.

 

interface Foo { /*...*/ }

class FooImpl : Foo { /*...*/ }

fun Foo(): Foo { return FooImpl() }

 

- 테스트 메소드

 
테스트 메서드의 이름을 작성할 때는 백틱(`)으로 묶인 공백을 포함할 수 있습니다. 이러한 메서드 이름은 현재 Android 런타임에서는 지원되지 않습니다. 또한 테스트 코드에서는 메서드 이름에 밑줄을 사용할 수도 있습니다.
 

class MyTestCase {
     @Test fun `ensure everything works`() { /*...*/ }

     @Test fun ensureEverythingWorks_onAndroid() { /*...*/ }
}

 

- 속성

 
상수의 이름(값이 변경되지 않는 속성으로 const로 표시되거나 사용자 지정 get 함수가 없는 최상위 또는 객체 val 속성)은 대문자 밑줄로 구분된 이름(스크림 스네이크 케이스)을 사용해야 합니다.
 

const val MAX_COUNT = 8
val USER_NAME_FIELD = "UserName"

 
행동 또는 가변 데이터를 가진 객체를 보유하는 최상위 또는 객체 속성의 이름은 카멜 케이스 이름을 사용해야 합니다.
 

val mutableCollection: MutableSet<String> = HashSet()

 
싱글톤 객체에 대한 참조를 보유하는 속성의 이름은 객체 선언과 동일한 네이밍 스타일을 사용할 수 있습니다.
 

val PersonComparator: Comparator<Person> = /*...*/


열거형 상수의 경우, 사용 방식에 따라 대문자 밑줄로 구분된 이름 (screaming snake case) (enum class Color { RED, GREEN }) 또는 upper camel case를 사용하는 것이 괜찮습니다.
 
Screaming Snake Case:
Screaming Snake Case는 모든 글자를 대문자로 쓰고, 단어 간에는 밑줄(_)을 사용하여 분리합니다. 주로 프로그래밍에서 상수를 나타낼 때 사용합니다. 예를 들어, MAXIMUM_SPEED 또는 DEFAULT_COLOR와 같이 표현됩니다.

Upper Camel Case (또는 Pascal Case):
Upper Camel Case는 각 단어의 첫 글자를 대문자로 쓰고, 나머지는 소문자로 씁니다. 단어 간에는 구분 기호 없이 바로 연결합니다. 주로 클래스 이름을 지을 때 사용됩니다. 예를 들어, ProductList, AccountService와 같이 표현됩니다.


- 백킹 속성


만약 클래스에 두 개의 속성이 있는데, 이들은 개념적으로는 같지만 하나는 공개 API의 일부이고 다른 하나는 구현 세부 사항인 경우, 비공개 속성의 이름에는 밑줄을 접두사로 사용합니다.
 

class C {
    private val _elementList = mutableListOf<Element>()

    val elementList: List<Element>
         get() = _elementList
}

 

- 좋은 이름 선택하기


클래스의 이름은 일반적으로 해당 클래스가 무엇인지 설명하는 명사 또는 명사구로 구성됩니다: List, PersonReader.

메서드의 이름은 일반적으로 해당 메서드가 무엇을 수행하는지 설명하는 동사 또는 동사구로 구성됩니다: close, readPersons. 메서드가 객체를 변경하는지 여부 또는 새로운 객체를 반환하는지를 암시하는 것도 좋습니다. 예를 들어, sort는 컬렉션을 제자리에서 정렬하지만, sorted는 정렬된 컬렉션의 사본을 반환합니다.

이름은 개체의 목적이 명확하게 나타나도록해야하므로, 이름에 의미 없는 단어(Manager, Wrapper)를 사용하는 것은 피하는 것이 좋습니다.

선언 이름의 일부로 약어를 사용할 때, 두 글자로 구성된 경우 대문자로 쓰고(IOStream), 길이가 길 경우 첫 글자만 대문자로 쓰는 것이 좋습니다(XmlFormatter, HttpInputStream).
 

형식 (Formatting)

 

- 들여쓰기


들여쓰기에는 네 개의 공백을 사용하십시오. 탭을 사용하지 마십시오.

중괄호에 대해서는, 개념이 시작되는 줄의 끝에 여는 중괄호를 놓고, 닫는 중괄호는 여는 구조와 수평으로 정렬된 별도의 줄에 놓으십시오.
 

if (elements != null) {
    for (element in elements) {
        // ...
    }
}

 
Kotlin에서는 세미콜론은 선택 사항이며, 따라서 줄 바꿈이 중요합니다. 언어 설계는 Java 스타일의 중괄호를 가정하고 있으며, 다른 서식 스타일을 사용하려고하면 예상치 못한 동작을 만날 수 있습니다.


- 가로 공백

 

  • 이항 연산자 주위에 공백을 넣으세요 (a + b). 예외: "범위 연산자" 주위에는 공백을 넣지 마세요 (0..i).
  • 단항 연산자 주위에 공백을 넣지 마세요 (a++).
  • 제어 흐름 키워드 (if, when, for, while)와 해당하는 여는 괄호 사이에 공백을 넣으세요.
  • 기본 생성자 선언, 메소드 선언 또는 메소드 호출에서 여는 괄호 앞에 공백을 넣지 마세요.
  • (, [, 뒤에는 공백을 넣지 마세요.
  • . 또는 ? 주위에 공백을 넣지 마세요.: foo.bar().filter { it > 2 }.joinToString(), foo?.bar()
  • //: 뒤에 공백을 넣으세요. // 이것은 주석입니다.
  • 타입 매개변수를 지정하는 데 사용되는 꺽쇠 괄호 주위에 공백을 넣지 마세요: class Map<K, V> { ... }
  • :: Foo::class, String::length 주위에 공백을 넣지 마세요.
  • nullable 타입을 표시하는 ? 앞에 공백을 넣지 마세요: String?

 
일반적으로 어떤 종류의 가로 정렬도 피하세요. 식별자의 이름을 길이가 다른 이름으로 변경하더라도 선언 또는 사용되는 부분의 형식을 영향을 주지 않아야 합니다.
 

val firstName   = "John"
val lastName    = "Doe"
val age         = 30

 

- 콜론(:)

 
다음 경우에는 ":" 앞에 공백을 넣으세요:
 

  • 타입과 상위 타입을 구분하는 데 사용될 때
  • 슈퍼클래스 생성자로 위임하거나 동일한 클래스의 다른 생성자로 위임할 때
  • "object" 키워드 뒤에 사용될 때
  • 선언과 해당하는 타입을 구분하는 ":" 앞에는 공백을 넣지 마세요.
  • ":" 뒤에는 항상 공백을 넣으세요.

 

abstract class Foo<out T : Any> : IFoo {
    abstract fun foo(a: Int): T
}

class FooImpl : Foo() {
    constructor(x: String) : this(x) { /*...*/ }

    val x = object : IFoo { /*...*/ }
}

 

- 클래스 헤더


몇 개의 주 생성자 매개변수를 가진 클래스는 한 줄로 작성할 수 있습니다.
 

class Person(id: Int, name: String)

 
더 긴 헤더를 가진 클래스는 주 생성자의 각 매개변수를 개별 줄에 들여쓰기로 나누어 작성해야 합니다. 또한, 마지막 괄호는 새로운 줄에 위치해야 합니다. 상속을 사용하는 경우, 슈퍼클래스의 생성자 호출 또는 구현된 인터페이스 목록은 괄호와 동일한 줄에 위치해야 합니다.
 

class Person(
    id: Int,
    name: String,
    surname: String
) : Human(id, name) { /*...*/ }

 
다중 인터페이스의 경우, 슈퍼클래스의 생성자 호출은 먼저 위치하고, 각 인터페이스는 개별 줄에 위치해야 합니다.
 

class Person(
    id: Int,
    name: String,
    surname: String
) : Human(id, name),
    KotlinMaker { /*...*/ }

 
긴 상위 타입 목록을 가진 클래스의 경우, 콜론 뒤에 줄 바꿈을 넣고 모든 상위 타입 이름을 수평으로 정렬하세요.
 

class MyFavouriteVeryLongClassHolder :
    MyLongHolder<MyFavouriteVeryLongClass>(),
    SomeOtherInterface,
    AndAnotherOne {

    fun foo() { /*...*/ }
}

 
클래스 헤더가 길 때 클래스 헤더와 본문을 명확하게 구분하기 위해 다음 두 가지 방법 중 하나를 사용하세요:

  • 클래스 헤더 뒤에 빈 줄을 추가하세요 (위의 예시와 같이).
  • 여는 중괄호를 별도의 줄에 위치시키세요.

 

class MyFavouriteVeryLongClassHolder :
    MyLongHolder<MyFavouriteVeryLongClass>(),
    SomeOtherInterface,
    AndAnotherOne
{
    fun foo() { /*...*/ }
}

 
생성자 매개변수에는 일반 들여쓰기 (4개의 공백)를 사용하세요. 이렇게 하면 주 생성자에서 선언된 속성들이 클래스 본문에서 선언된 속성들과 동일한 들여쓰기를 가지게 됩니다.
 

- 수정자(modifiers) 순서

 
만약 선언에 여러 개의 수정자(modifiers)가 있다면, 항상 다음의 순서로 배치하세요:
 

public / protected / private / internal
expect / actual
final / open / abstract / sealed / const
external
override
lateinit
tailrec
vararg
suspend
inner
enum / annotation / fun // as a modifier in `fun interface`
companion
inline / value
infix
operator
data


모든 어노테이션을 수정자(modifiers) 앞에 배치하세요.
 

@Named("Foo")
private val foo: Foo

 
라이브러리 작업을 하지 않는 한, 중복된 수정자(예: public)는 생략하세요.
 

class MyClass { // public class에서 public 생략
    // class body
}

static final String DEFAULT_NAME = "John"; // public static ...에서 public 생략

 

- 어노테이션


어노테이션은 해당 선언 바로 전에 위치하며, 동일한 들여쓰기로 따로 라인에 작성합니다.
 

@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude

 
인자가 없는 어노테이션은같은 줄에 위치할 수 있습니다.
 

@JsonExclude @JvmField
var x: String

 
인자가 없는 단일 어노테이션은 해당 선언과 동일한 줄에 위치할 수 있습니다.
 

@Test fun foo() { /*...*/ }

 

- @file 어노테이션

 
파일 어노테이션은 파일 어노테이션(있는 경우) 뒤, package 문 전에 위치하며, package와는 빈 줄로 구분됩니다 (파일에 대한 대상이며 패키지에 대한 대상이 아님을 강조하기 위함).
 

/** License, copyright and whatever */
@file:JvmName("FooBar")

package foo.bar

 

- 메소드


메소드의 시그니처가 한 줄에 들어가지 않는 경우, 다음과 같은 문법을 사용하세요.
 

fun longMethodName(
    argument: ArgumentType = defaultValue,
    argument2: AnotherArgumentType,
): ReturnType {
    // body
}

 
메소드의 매개변수는 일반적인 들여쓰기 (4칸 공백)를 사용하세요. 이는 생성자 매개변수와 일관성을 유지하는 데 도움이 됩니다.

본문이 단일 표현식으로 구성된 메소드에는 표현식 본문을 사용하는 것이 좋습니다.
 

fun foo(): Int {     // bad
    return 1
}

fun foo() = 1        // good

 

- 표현식 본문(Expression bodies)

 
표현식 본문을 가진 함수의 첫 번째 줄이 선언과 같은 줄에 들어가지 않는 경우, = 기호를 첫 번째 줄에 두고 표현식 본문은 4칸 들여쓰기를 해주세요.
 

fun f(x: String, y: String, z: String) =
    veryLongFunctionCallWithManyWords(andLongParametersToo(), x, y, z)

 

- 속성

 

매우 간단한 읽기 전용 속성의 경우, 한 줄 포맷을 고려해보세요.
 

val isEmpty: Boolean get() = size == 0

 
더 복잡한 속성의 경우, 항상 get과 set 키워드를 별도의 줄에 작성하세요.
 

val foo: String
    get() { /*...*/ }

 
초기값이 있는 속성의 경우, 초기값이 긴 경우 = 기호 뒤에 줄 바꿈을 추가하고 초기값을 4칸 들여쓰기 해주세요.
 

private val defaultCharset: Charset? =
    EncodingRegistry.getInstance().getDefaultCharsetForPropertiesFiles(file)

 

- 제어 흐름문


if나 when 문의 조건이 여러 줄인 경우, 문의 본문 주위에 항상 중괄호를 사용하세요. 조건의 각 후속 라인은 문의 시작과 비교하여 4칸 들여쓰기를 해주세요. 조건의 닫는 괄호는 별도의 줄에 여는 중괄호와 함께 작성하세요.
 

if (!component.isSyncing &&
    !hasAnyKotlinRuntimeInScope(module)
) {
    return createKotlinNotConfiguredPanel(module)
}

 
이렇게 하면 조건과 문의 본문을 정렬하는 데 도움이 됩니다.

else, catch, finally 키워드 및 do-while 루프의 while 키워드는 이전 중괄호와 같은 줄에 위치하세요.
 

if (condition) {
    // body
} else {
    // else part
}

try {
    // body
} finally {
    // cleanup
}

 

when 문에서 한 분기가 여러 줄인 경우, 인접한 case 블록과의 구분을 위해 빈 줄로 분리하는 것을 고려해보세요.
 

private fun parsePropertyValue(propName: String, token: Token) {
    when (token) {
        is Token.ValueToken ->
            callback.visitValue(propName, token.value)

        Token.LBRACE -> { // ...
        }
    }
}

 
짧은 분기는 중괄호 없이 조건과 동일한 줄에 작성하세요.
 

when (foo) {
    true -> bar() // good
    false -> { baz() } // bad
}

 

- 메서드 호출


긴 인자 목록에서는 여는 괄호 다음에 줄 바꿈을 추가하세요. 인자는 4칸 들여쓰기 해주세요. 관련된 여러 인자는 동일한 줄에 그룹화하세요.
 

drawSquare(
    x = 10, y = 10,
    width = 100, height = 100,
    fill = true
)

 
인자 이름과 값 사이를 구분하는 = 기호 주변에 공백을 추가하세요.
 

- 체인된 호출(Wrap chained calls)


체인된 호출을 줄 바꿈할 때는 . 문자나 ?. 연산자를 다음 줄에 작성하고, 들여쓰기를 한 번 추가하세요.
 

val anchor = owner
    ?.firstChild!!
    .siblings(forward = true)
    .dropWhile { it is PsiComment || it is PsiWhiteSpace }


보통 체인의 첫 번째 호출은 그 전에 줄 바꿈을 추가해야 합니다. 그러나 코드가 그렇게 더 의미를 가지는 경우에는 생략해도 괜찮습니다.
 

val anchor = owner?.firstChild!!
    .siblings(forward = true)
    .dropWhile { it is PsiComment || it is PsiWhiteSpace }

 

- 람다

 

람다 표현식에서는 중괄호 주위와 매개변수와 본문을 구분하는 화살표 주위에 공백을 사용해야 합니다. 단일 람다를 받는 호출이 있는 경우에는 가능하면 괄호 밖으로 전달하세요.
 

list.filter { it > 10 }

 
람다에 레이블을 지정하는 경우, 레이블과 여는 중괄호 사이에 공백을 넣지 마세요.
 

fun foo() {
    ints.forEach lit@{
        // ...
    }
}

 
여러 줄로 이루어진 람다에서 매개변수 이름을 선언할 때는, 이름을 첫 번째 줄에 작성한 후에 화살표와 줄 바꿈을 추가하세요.
 

appendCommaSeparated(properties) { prop ->
    val propertyValue = prop.get(obj)  // ...
}


매개변수 목록이 한 줄에 들어가지 않을 정도로 긴 경우, 화살표를 별도의 줄에 작성하세요.
 

foo {
   context: Context,
   environment: Env
   ->
   context.configureEnv(environment)
}


- 트레일링 콤마 (Trailing commas)


Trailing commas (마지막 쉼표)는 요소들의 시리즈에서 마지막 항목 뒤에 있는 쉼표입니다.
 

class Person(
    val firstName: String,
    val lastName: String,
    val age: Int, // trailing comma
)

 
Trailing commas를 사용하는 것에는 여러 가지 이점이 있습니다:
 

  • 변경된 값에 집중할 수 있도록 버전 관리 차이점이 더 깔끔해집니다.
  • 요소를 추가하거나 재정렬하기 쉽습니다. 요소를 조작할 때 쉼표를 추가하거나 삭제할 필요가 없습니다.
  • 객체 초기화와 같은 코드 생성을 간소화할 수 있습니다. 마지막 요소에도 쉼표를 사용할 수 있습니다.
  • 마지막 쉼표는 완전히 선택적입니다. 쉼표 없이도 코드는 작동합니다. Kotlin 스타일 가이드는 선언 위치에서의 마지막 쉼표 사용을 권장하고, 호출 위치에서는 재량에 따라 선택하도록 권장합니다.


IntelliJ IDEA 포매터에서 마지막 쉼표를 활성화하려면 Settings/Preferences | Editor | Code Style | Kotlin으로 이동한 다음 Other 탭을 열고 Use trailing comma 옵션을 선택하세요.
 

 
상황별 Trailing commas 사용법은 여기서 확인하자.
 

문서 주석 (Documentation comments)

 
긴 문서 주석의 경우, 여는 /**를 별도의 줄에 위치시키고, 이후의 각 줄을 별표(*)로 시작하세요.
 

/**
 * This is a documentation comment
 * on multiple lines.
 */

 
짧은 주석은 한 줄에 작성할 수 있습니다.
 

/** This is a short documentation comment. */

 
일반적으로 @param 및 @return 태그의 사용을 피하세요. 대신, 매개변수와 반환값에 대한 설명을 문서 주석에 직접 포함시키고, 해당 매개변수가 언급될 때마다 링크를 추가하세요. 긴 설명이 필요하고 본문 흐름에 맞지 않는 경우에만 @param과 @return을 사용하세요.
 

// Avoid doing this:

/**
 * Returns the absolute value of the given number.
 * @param number The number to return the absolute value for.
 * @return The absolute value.
 */
fun abs(number: Int): Int { /*...*/ }

// Do this instead:

/**
 * Returns the absolute value of the given [number].
 */
fun abs(number: Int): Int { /*...*/ }

 

중복된 구문을 피하세요 (Avoid redundant constructs)

 
일반적으로, Kotlin에서 특정 구문 구성요소가 선택적이고 IDE에서 중복으로 표시되는 경우에는 코드에서 생략해야 합니다. "명확성"을 위해 불필요한 구문 요소를 코드에 남기지 마세요.
 

- Unit 반환 타입

 
함수가 Unit을 반환하는 경우, 반환 타입은 생략되어야 합니다.
 

fun foo() { // ": Unit" is omitted here

}

 

- 세미콜론(;)

 
가능한 경우 세미콜론을 생략하세요.
 

- String templates


문자열 템플릿에 간단한 변수를 삽입할 때 중괄호를 사용하지 마세요. 긴 표현식에만 중괄호를 사용하세요.
 

println("$name has ${children.size} children")

 

언어 기능의 관용적인 사용(Idiomatic use of language features)

(관용적: 습관적으로 늘 쓰는 것, 오랫동안 써서 굳어진 대로 늘 쓰는 것, 습관적으로 늘 쓰는 것)


- 불변성 (Immutability)


변경할 수 없는 데이터를 사용하는 것이 좋습니다. 초기화 이후에 수정되지 않는 경우에는 항상 지역 변수와 속성을 var 대신 val로 선언하세요.

변경되지 않는 컬렉션을 선언할 때는 항상 불변 컬렉션 인터페이스 (Collection, List, Set, Map)를 사용하세요. 컬렉션 인스턴스를 생성하기 위해 팩토리 함수를 사용할 때도 가능한 경우 불변 컬렉션 유형을 반환하는 함수를 사용하세요.

 

// 나쁜 예: 변경되지 않을 값에 대해 가변 컬렉션 유형을 사용함
fun validateValue(actualValue: String, allowedValues: HashSet<String>) { ... }

// 좋은 예: 변경되지 않을 값에 대해 불변 컬렉션 유형 사용
fun validateValue(actualValue: String, allowedValues: Set<String>) { ... }

// 나쁜 예: arrayListOf()는 ArrayList<T>를 반환하며, 이는 가변 컬렉션 유형입니다
val allowedValues = arrayListOf("a", "b", "c")

// 좋은 예: listOf()는 List<T>를 반환합니다
val allowedValues = listOf("a", "b", "c")

 

- 기본 매개변수 값 (Default Parameter Values)


오버로드된 함수를 선언하는 대신, 기본 매개변수 값을 갖는 함수를 선언하는 것이 좋습니다.
 

// Bad
fun foo() = foo("a")
fun foo(a: String) { /*...*/ }

// Good
fun foo(a: String = "a") { /*...*/ }

 

- 타입 별칭 (Type aliases)


코드베이스에서 여러 번 사용되는 함수형 타입이나 타입 매개변수를 갖는 타입이 있다면, 해당 타입에 대해 타입 별칭을 정의하는 것이 좋습니다.
 

typealias MouseClickHandler = (Any, MouseEvent) -> Unit
typealias PersonIndex = Map<String, Person>


이름 충돌을 피하기 위해 비공개(private) 또는 내부(internal) 타입 별칭을 사용하는 경우, 패키지 및 임포트(Packages and Imports) 섹션에서 언급한 "import ... as ..." 구문을 사용하는 것이 좋습니다.
 

- Lambda 매개변수


짧고 중첩되지 않은 람다식에서는 매개변수를 명시적으로 선언하는 대신 it 규칙을 사용하는 것이 권장됩니다. 매개변수가 있는 중첩된 람다식에서는 항상 매개변수를 명시적으로 선언해야 합니다.
 

val numbers = listOf(1, 2, 3, 4, 5)

// 짧고 중첩되지 않은 람다
val squaredSum = numbers.sumBy { it * it }

// 길고 중첩된 람다
val squaredSum = numbers.fold(0) { sum, number ->
    val square = number * number
    sum + square
}

 

- 람다식 내 반환문 (Returns in a lambda)

 
람다식에서는 여러 개의 레이블이 지정된 반환문을 사용하는 것을 피하십시오. 람다식을 다시 구조화하여 단일 종료 지점을 갖도록하는 것이 좋습니다. 그것이 불가능하거나 명확하지 않은 경우, 람다식을 익명 함수로 변환하는 것을 고려하십시오.

람다식의 마지막 문장에는 레이블이 지정된 반환문을 사용하지 마십시오.
 

// 레이블된 리턴문을 사용한 람다 표현식
val lambda: (Int) -> Int = { x ->
    return@lambda if (x == 0) {
        0
    } else if (x > 0) {
        1
    } else {
        -1
    }
}

// 레이블된 리턴문을 사용하지 않은 익명 함수
val anonymous: (Int) -> Int = { x ->
    if (x == 0) {
        0
    } else if (x > 0) {
        1
    } else {
        -1
    }
}

 

- 명명된 인자 (Named arguments)


여러 개의 원시 타입 매개변수를 받는 메소드나 Boolean 타입의 매개변수가 있는 경우, 매개변수의 의미가 문맥으로부터 명확하게 이해되지 않는 한, 명명된 인자 구문을 사용하세요.
 

drawSquare(x = 10, y = 10, width = 100, height = 100, fill = true)

 

- 조건문 (Conditional statements)

 
try, if, 그리고 when의 표현식 형태를 사용하는 것을 선호하세요.
 

return if (x) foo() else bar()

return when(x) {
    0 -> "zero"
    else -> "nonzero"
}

 
위의 방식이 다음 방식보다 선호됩니다:
 

if (x)
    return foo()
else
    return bar()
    
when(x) {
    0 -> return "zero"
    else -> return "nonzero"
}

 

- if vs when

 
단일 조건에 대해서는 when 대신 if를 사용하는 것을 선호하세요. 예를 들어, 다음과 같은 문법으로 if를 사용하세요:
 

if (x == null) ... else ...


아래와 같은 when은 사용하지 마세요.

when (x) {
    null -> // ...
    else -> // ...
}

 
옵션이 세 개 이상인 경우에는 when을 사용하는 것을 선호합니다.
 

- 조건문에서 Nullable Boolean을 사용하는 경우(Nullable Boolean values in conditions)


조건문에서 nullable Boolean 값을 사용해야 하는 경우, if (value == true) 또는 if (value == false) 체크를 사용하세요.
 

val value: Boolean? = null

if (value == true) {
    ...
} else if (value == false) {
    ...
} else {
    ...
}

 

- 반복 (Loops)

 
루프 대신 고차 함수 (filter, map 등)를 사용하는 것을 선호하세요. 예외적으로, forEach는 (nullable한 수신자인 경우나 forEach가 긴 호출 체인의 일부로 사용되는 경우를 제외하고) 일반적인 for 루프를 사용하는 것이 좋습니다.

여러 개의 고차 함수를 사용하는 복잡한 표현식과 루프 사이에서 선택할 때, 각 경우에서 수행되는 작업의 비용을 이해하고 성능 고려 사항을 염두에 두세요.
 

val numbers = listOf(1, 2, 3, 4, 5)

// filter와 map을 사용하여 짝수를 제곱한 리스트를 얻는 예
val result = numbers.filter { it % 2 == 0 }.map { it * it }

// for 루프를 사용하여 짝수를 제곱한 리스트를 얻는 예
val result = mutableListOf<Int>()
for (number in numbers) {
    if (number % 2 == 0) {
        result.add(number * number)
    }
}

 

- 범위에 대한 반복 (Loops on ranges)

 
열린 범위를 반복하려면 until 함수를 사용하십시오.
 

for (i in 0..n - 1) { /*...*/ }  // bad
for (i in 0 until n) { /*...*/ }  // good


- 문자열 (Strings)


문자열 연결 대신 문자열 템플릿을 선호하십시오.

일반 문자열 리터럴에 \n 이스케이프 시퀀스를 삽입하는 대신 여러 줄 문자열을 선호하십시오.

여러 줄 문자열에서 들여쓰기를 유지하려면 결과 문자열이 내부 들여쓰기를 필요로하지 않는 경우 trimIndent를 사용하거나 내부 들여쓰기가 필요한 경우 trimMargin을 사용하십시오.
 

println("""
    Not
    trimmed
    text
    """
       )

println("""
    Trimmed
    text
    """.trimIndent()
       )

println()

val a = """Trimmed to margin text:
          |if(a > 1) {
          |    return a
          |}""".trimMargin()

println(a)

 
자바와 코틀린의 다중 문자열 차이점은 여기에서 확인하세요.
 

- 메소드 vs 속성 (Functions vs properties)

 
경우에 따라서, 인수가 없는 메소드는 읽기 전용 속성과 교환하여 사용할 수 있습니다. 의미론적으로는 유사하지만, 언제 어떤 것을 선호해야 하는지에 대한 스타일 관례가 있습니다.

다음과 같은 경우에 메소드보다 속성을 선호합니다.

  • 예외를 던지지 않는 경우
  • 계산 비용이 적거나 첫 번째 실행에서 캐시된 경우
  • 객체 상태가 변경되지 않는 한 호출 간에 동일한 결과를 반환하는 경우

 

- 확장 함수 (Extension funtions)


확장 함수를 적극적으로 사용하세요. 주로 객체에서 작동하는 함수가 있는 경우 해당 객체를 수신자로 받는 확장 함수로 만드는 것을 고려해보세요. API 오염을 최소화하기 위해 확장 함수의 가시성을 가능한 한 제한하세요. 필요에 따라 지역 확장 함수, 멤버 확장 함수 또는 비공개 가시성을 가진 최상위 확장 함수를 사용하세요.
 

- 인픽스 함수 (Infix functions)


두 개의 객체에서 비슷한 역할을 하는 경우에만 함수를 인픽스로 선언하세요. 좋은 예시는 and, to, zip입니다. 나쁜 예시는 add입니다.

수신자 객체를 변경(mutate)하는 경우 메서드를 인픽스로 선언하지 마세요.
 

- 팩토리 함수 (Factory functions)


클래스에 대한 팩토리 함수를 선언할 경우, 클래스 자체와 동일한 이름을 사용하지 않도록 주의하세요. 팩토리 함수의 동작이 특별한 이유가 명확하게 드러나는 구별된 이름을 사용하는 것이 좋습니다. 특별한 의미가 없다면 클래스와 동일한 이름을 사용할 수 있습니다.
 

class Point(val x: Double, val y: Double) {
    companion object {
        fun fromPolar(angle: Double, radius: Double) = Point(...)
    }
}


여러 개의 오버로딩된 생성자가 있는 경우, 서로 다른 슈퍼클래스 생성자를 호출하지 않고 기본 인수 값으로 단일 생성자로 축소할 수 없는 경우, 오버로딩된 생성자 대신 팩토리 함수를 사용하는 것이 좋습니다.
 

// Bad
class Person {
    var name: String = ""
    var age: Int = 0
    
    constructor(name: String) {
        this.name = name
    }
    
    constructor(age: Int) {
        this.age = age
    }
    
    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
    }
}

// Good
class Person private constructor(val name: String, val age: Int) {
    companion object {
        // Factory function to create a Person object with name only
        fun createWithName(name: String): Person {
            return Person(name, 0)
        }
        
        // Factory function to create a Person object with age only
        fun createWithAge(age: Int): Person {
            return Person("", age)
        }
        
        // Factory function to create a Person object with both name and age
        fun createWithNameAndAge(name: String, age: Int): Person {
            return Person(name, age)
        }
    }
}

 

- 플랫폼 타입 (Platform types)


플랫폼 타입의 표현식을 반환하는 공개 함수/메서드는 Kotlin 타입을 명시적으로 선언해야 합니다.
 

fun apiCall(): String = MyJavaApi.getProperty("name")

 
플랫폼 타입의 표현식으로 초기화되는 모든 속성(패키지 레벨 또는 클래스 레벨)은 Kotlin 타입을 명시적으로 선언해야 합니다.
 

class Person {
    val name: String = MyJavaApi.getProperty("name")
}

 
플랫폼 타입의 표현식으로 초기화되는 로컬 값은 타입 선언을 가질 수도 있고 가지지 않을 수도 있습니다.
 

fun main() {
    val name = MyJavaApi.getProperty("name")
    println(name)
}

 

- 스코프 메소드 (Scope funtions apply/with/run/also/let)

 
Kotlin은 주어진 객체의 컨텍스트에서 코드 블록을 실행하는 일련의 함수를 제공합니다: let, run, with, apply, also. 적절한 스코프 함수를 선택하기 위한 안내는 "Scope Functions"을 참조하세요.
 

라이브러리 코딩 컨벤션

 
라이브러리를 작성할 때는 API의 안정성을 보장하기 위해 추가적인 규칙을 따르는 것이 권장됩니다:

  • 멤버의 가시성을 항상 명시적으로 지정하세요 (의도치 않게 선언을 공개 API로 노출하는 것을 피하기 위해).
  • 함수의 반환 타입과 속성의 타입을 항상 명시적으로 지정하세요 (구현이 변경될 때 반환 타입을 실수로 변경하는 것을 방지하기 위해).
  • 라이브러리의 모든 공개 멤버에는 KDoc 주석을 제공하세요. 단, 문서화가 필요하지 않은 오버라이드에 대해서는 예외로 처리합니다 (라이브러리의 문서 생성을 지원하기 위해).


라이브러리를 작성할 때 API에 대한 최상의 관례와 고려해야 할 아이디어에 대해 더 자세히 알아보려면 "library creators' guidelines"를 참조하세요.

반응형

댓글