본문 바로가기
개발

코틀린 코드 작성 규칙 (Coding Convention)

by 마스터누누 2020. 1. 20.
728x90
반응형

스타일 가이드 적용

이 스타일 가이드에 따라 IntelliJ 포맷터를 구성하기 위해서는 Kotlin 플러그인 버전 1.2.20 이상을 설치하고, Setting -> Editor -> Code Style -> Kotlin -> set from... 을 선택하세요. 그리고 메뉴에서 Predefined Style -> Kotlin style guide를 선택합니다.

 

스타일 가이드에 따라 코드가 적용되어있는지 확인하려면 Setting -> Inspections -> Kotlin -> Style issues -> File is not formatted according to project settings 를 선택합니다.

 

소스 코드 구성

디렉토리 구조

순수 코틀린 프로젝트에서 권장되는 디렉토리 구조는, 공통 루트 패키지가 생략된 구조입니다. 예를 들어, 모든 프로젝트의 코드가 org.example.kotlin 패키지 안에 있고, 해당 패키지의 서브 패키지라면 org.example.kotlin 패키지가 있는 파일은 소스 루트 바로 아래에 있어야합니다. 그리고 org.example.kotlin.network.socket의 파일은 소스 루트의 network -> socket 서브 디렉토리에 있어야합니다.

 

소스파일 이름 

만약, 코틀린이 하나의 클래스만 가지고 있다면, 파일 이름은 클래스 이름과 같고 .kt 확장자가 붙어야합니다. 파일에 여러 클래스가 포함되어있거나 top-level 선언만 있다면, 파일에 포함된 내용을 설명하는 이름을 지정하세요. 이 때, 첫글자가 대문자인 카멜 표기법(camel case)을 사용하세요(for Example, ProcessDeclarations.kt).

 

파일 이름은 파일의 코드가 수행하는 작업을 설명해야합니다. 따라서, "Util"과 같은 의미 없는 단어는 파일 이름에 사용하지 않아야합니다.

 

소스파일 구성

 코드들이 의미적으로 밀접하게 관련되어 있고, 파일 크기가 적당하게 유지 되는 한(수 백줄 내에서) 여러 선언(클래스, 최상위 함수, 속성)들을 하나의 코틀린 소스 파일에 넣는것을 권장합니다.

특히, 클래스의 확장 함수를 정의할 때는, 해당 클래스와 동일한 파일에 추가하세요. 특정 클라이언트에게만 적용되는 확장 기능을 정의할 때는, 해당 코드 옆에 정의하세요. "Foo의 모든 확장함수"를 담기 위해 별도의 파일을 만들지 마세요.

 

클래스 레이아웃

일반적으로, 클래스는 다음과 같이 정렬됩니다.

  • 속성 선언 및 초기화 블록
  • 보조 생성자
  • 함수 선언
  • Companion Object

알파벳순이나 가독성으로 메소드 선언을 정렬하지 말고 일반 메소드를 확장 메소드와 분리하지 마세요. 대신, 기능적으로 연관되어있는 메서드를 모아서, 위에서 아래 방향으로 읽을때 논리적으로 이해할 수 있도록 정렬하세요. 정렬 방식을 선택하고(고수준이 먼저오거나, 그 반대) 거기에 맞춰서 코드를 작성하세요.

 

중첩 클래스는 해당 클래스를 사용하는 코드 근처에 두세요. 중첩 클래스가 내부에 사용되어지지 않고 외부에서만 사용된다면, Companion Object 다음에 배치합니다.

 

오버로딩 레이아웃

오버로딩 메서드는 서로 이웃하게(근처에) 배치하세요.

 

명명 규칙(Naming rules)

코틀린의 패키지 및 클래스 명명 규칙은 매우 간단합니다.

  • 패키지 이름은 항상 소문자이며 밑줄을(org.example.project) 사용하지 마세요. 여러 단어로 된 이름을 사용하는건 권장하지 않지만, 여러 단어를 사용해야할 경우에는 단어를 함꼐 연결하거나 카멜 표기법(Camel Case) - (org.example.myProject)를 사용 할 수 있습니다.
  • 클래스와 객체 이름은 대문자로 시작하고 카멜 표기법을 사용합니다.
open class DeclarationProcessor { /*...*/ }

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

 

함수 이름(Function names)

함수, 속성 및 지역 변수의 이름은 소문자와 카멜 표기법(Camel Case)를 사용하고 밑줄(Underscore)은 사용하지 않습니다.

 

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

 

예외 : 클래스 인스턴스를 작성하는데 사용되는 팩토리 함수는 작성하는데 동일한 이름을 가질 수 있습니다.

 

abstract class Foo { /*...*/ }

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

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

 

테스트 메소드 명명 규칙

테스트 코드에서만 한 해, 백틱으로 묶은 공백이 있는 메소드 이름을 사용할 수 있습니다.(이러한 메소드 이름은 현재 Android 런타임에서 지원되지 않습니다). 또한, 테스트 코드에서는 밑줄도 허용이 됩니다.

 

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

 

프로퍼티 이름(Property names)

상수(const로 표시되는 프로퍼티, 또는 get이 없는 val 키워드로 선언된 불변 데이터)의 이름은 밑줄로 구분된 대문자를 사용해야합니다.

 

const val MAX_COUNT = 8
val USER_NAME_FIELD = "UserName"

 

변경 가능한 객체의 프로퍼티 값은 카멜 표기법(Camel Case)를 사용해야합니다.

 

val mutableCollection: MutableSet<String> = HashSet()

 

싱글톤 참조 프로퍼티는 객체 선언과 동일하게 이름을 지정할 수 있습니다.

 

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

 

열거형(Enum)의 경우, 사용법에 따라 밑줄로 구분된 이름(enum class Color{ RED, GREEN })또는 첫글자가 대문자인 카멜 표기법(Camel Case)를 사용하는것이 좋습니다.

 

 

백킹 프로퍼티(Backing properties)

클래스 내에 동일한 프로퍼티가 2개가 있을 때, 하나는 공용 API의 일부이고 다른 하나는 이에 대한 구현일 경우, private 프로퍼티 앞에는 밑줄(underscore)를 사용하세요.

 

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)

들여쓰기를 위해 탭을 사용하지말고 4개의 space를 사용하세요.

 

중괄호의 경우, 코드가 시작되는 줄에 여는 괄호를, 같은 레벨 코드 블록의 수직 아래에 닫는 괄호를 놓습니다.

 

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

 

(참고: 코틀린에서 세미콜론은 선택사항이므로 줄 바꿈이 중요합니다. 중괄호는 Java 스타일을 사용하므로 다른 서식을 사용하면 예상치 못한 동작이 발생할 수 있습니다.)

 

수평 공백(Horizontal whitespace)

이항 연산자 주위에 공백을 두세요( a + b ). 예외 : "range to" 연산자 (0..i) 주위에는 공백을 두지 마세요.

 

단항 연산자 주위에는 공백을 두지 마세요(a++)

 

제어 흐름 키워드(if, when, forwhile)와 해당 키워드 뒤 여는 괄호 사이에는 공백을 넣습니다.

 

기본 생성자 선언, 메소드 선언 또는 메소드 호출에서 여는 괄호 앞에는 공백을 넣지 마세요.

 

class A(val x: Int)

fun foo(x: Int) { ... }

fun bar() {
    foo(1)
}

 

다음의 괄호 -  ( ,  [ ,  ]  ,  )  뒤에 공백을 두지 마세요.

 

 . 이나 ?  앞뒤에 공간을 두지 마세요 : foo.bar().filter { it > 2 }.joinToString(), foo?.bar()

 

// 뒤에 공백을 넣습니다 : // This is a comment

 

Generic을 지정하는데 사용되는 꺾쇠 괄호 주위에 공백을 두지마세요 : class Map<K, V> { ... }

 

:: 연산자 주위에 공백을 두지 마세요 : Foo::class, String::length

 

nullable 타입을 표시하는 ? 연산자 뒤에 공백을 두지마세요. : String?

 

콜론(Colon)

다음과 같은 경우에는   :  뒤에 공백을 두세요

 

  • 타입과 슈퍼 타입을 분리하는데 사용될 때
  • 슈퍼 클래스 생성자 또는 같은 클래스의 다른 생성자에게 위임할 때
  • object 키워드 다음

선언과 type을 구분할 때 앞에 공백을 두지 마세요.

 

항상  :  뒤에 공백을 두세요

 

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 header formatting)

기본 생성자 파라미터가 있는 클래스의 생성자는 한 줄로 작성이 가능합니다.

 

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)

선언에 여러 수정자가 있는 경우 항상 다음 순서로 배치하세요.

 

public / protected / private / internal
expect / actual
final / open / abstract / sealed / const
external
override
lateinit
tailrec
vararg
suspend
inner
enum / annotation
companion
inline
infix
operator
data

 

모든 어노테이션은 수정자 앞에 배치하세요.

 

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

 

라이브러리에서 작업하지 않는 한 중복 수정자를 생략하세요(예 :public)

 

어노테이션 형식(Annotation formatting)

어노테이션은 일반적으로 각각의 줄에 배치됩니다. 

 

@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude

 

인수가 없는 주석은 같은 줄에 배치 할 수 있습니다.

 

@JsonExclude @JvmField
var x: String

 

인수가 없는 단일 주석은 해당 선언과 동일한 행에 배치 될 수 있습니다.

 

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

 

파일 어노테이션(File annotations)

파일 어노테이션은 어노테이션 다음에 (있는 경우) 패키지 문 앞에 배치됩니다.(패키지가 아니라 파일을 대상으로 한다는 것을 강조하기 위해)

 

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

package foo.bar

 

함수 형식(Function formatting)

함수가 싱글 라인으로 표현될 수 없으면 다음 구문을 사용하세요.

 

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

 

함수 인자에는 일반 들여쓰기(4개의 공백)을 사용하세요.

 

이유 - 생성자 매개 변수와의 일관성

 

함수의 Body 값이 단일 일 경우 다음과 같이 사용하는것이 좋습니다.

 

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

fun foo() = 1        // good

 

속성 형식(Property formatting)

간단한 읽기 전용 속성의 경우 한 줄 형식을 고려 하세요.

 

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

 

조금 복잡한 속성의 경우 항상 get이나 set 키워드를 별도의 줄에 넣으세요.

 

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

 

초기화 코드가 존재하고 길이가 긴 경우, 등호 뒤에 줄 바꿈을 추가하고 초기화 코드를 4개의 공백으로 들여씁니다.

 

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

 

제어문 형식(Formatting control flow statements)

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 문에서 분기가 한줄 이상인 경우 빈줄로 인접한 케이스 블록과 분리하세요.

 

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
)

 

연속 호출 래핑(Chained call wrapping)

다음과 같이  .  ?.  연산자를 통해 연속 호출시 :

 

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

 

람다 형식(LambDa formatting)

람다 표현식에서 중괄호와 매개 변수 다음 화살표에는 앞뒤로 공백을 두어야합니다. 단일 람다를 호출 하는 경우, 가능하다면 외부 괄호를 사용해야합니다.

 

list.filter { it > 10 }

 

람다에 label을 할당하는 경우 label과 여는 괄호 사이에 공백이 있으면 안됩니다.

 

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

 

여러 줄로 된 람다에서 매개 변수 이름을 선언할 떄, 첫 번째 줄에 이름을 입력한 다음 화살표와 줄 바꿈을 입력하세요.

 

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

 

파라미터 목록이 한 줄에 쓰기 너무 길다면, 개별적인 줄에 화살표를 두세요

 

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

 

주석(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) { /*...*/ }

// Do this instead:

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

 

중복 구조 피하기(Avoiding redundant constructs)

일반적으로 코틀린의 특정 구문이 중복으로 강조되는 경우, 명확하게 표현하기 위해 불필요한 구문은 삭제하세요.

 

Unit

함수가 Unit을 반환하면 반환 유형을 생략해야합니다.

 

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

}

 

세미콜론

가능하다면 세미콜론은 생략하세요.

 

문자열 템플릿

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

 

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

 

관용적 사용(Idiomatic use of language features)

불변성

불변 데이터를 사용하는 것을 선호하세요. 또한 초기화 후 수정되지 않는 경우에는 항상 로컬 변수와 속성을 val로 선언하세요.

 

불변 데이터 콜렉션형을 선언하려면 항상 불변 콜렉션 인터페이스(Collection, List, Set, Map)를 사용하세요. 팩토리 함수를 사용해서 콜렉션 인터페이스를 작성할 때, 가능하면 항상 불변 콜렉션을 리턴하는 함수를 사용하세요.

 

// Bad: use of mutable collection type for value which will not be mutated
fun validateValue(actualValue: String, allowedValues: HashSet<String>) { ... }

// Good: immutable collection type used instead
fun validateValue(actualValue: String, allowedValues: Set<String>) { ... }

// Bad: arrayListOf() returns ArrayList<T>, which is a mutable collection type
val allowedValues = arrayListOf("a", "b", "c")

// Good: listOf() returns 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") { /*...*/ }

 

타입 Aliases (Type aliases)

 

코드베이스에서 여러 번 사용되는 기능 또는 매개변수가 있는 경우 Aliases를 정하는것이 좋습니다.

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

 

람다 인자(Lambda parameters)

람다는 짧고 중첩이 되지 않기때문에, 매개 변수를 명시적으로 선언하는대신 it을 사용하는 것이 좋습니다. 매개 변수가 있는 nested 람다에서는 매개 변수를 항상 명시적으로 선언해야합니다.

 

람다 반환값

람다는 여러개의 값을 반환할 수 없습니다. 따라서 하나의 반환 포인트만 고려하여 람다를 설계하세요. 이것이 어려울 경우 람다를 익명 함수로 변환하세요.

 

명시적 인자(Named arguments)

메소드가 여러 기본형 매개 변수를 받을 때는 인수를 명시적으로 표기하는 구문을 사용할 수 있습니다.

 

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

 

조건문 사용

다음과 같이 조건문을 사용할 수 있습니다.

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 와 when 구문

다음과 같은 when 이진 조건에는 if를 사용하는 것이 좋습니다.

 

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

다음과 같은 if 구문을 사용하세요 : if (x == null) ... else ...

when 문은 3개 또는 4개 이상의 조건에서 사용하세요.

 

조건에서 널 입력 가능 Boolean 값 사용

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

 

반복문 사용

반복문은 고차 함수(filter, map etc.)를 사용하는 것이 좋습니다. 예외 : forEach(forEach의 수신자가 nullable이 아니거나 긴 메소드 호출 체인의 일부가 아니면, 일반 for 루프를 대신 사용하는 것이 좋습니다.)

 

여러 고차함수를 사용한 복잡한 표현식과 일반 루프 중에서 선택할 때 , 각 케이스마다의 작업 비용을 이해하고 성능을 고려하세요.

 

범위 반복문

범위를 반복하려면 until 함수를 사용하세요.

 

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

 

문자열 사용

문자열에 인자를 넣을 경우 문자열 템플릿을 사용하세요.

 

\n  문을 일반 문자열에 포함시키는 것 보다 여러줄의 문자열을 사용하는것이 좋습니다.

 

여러줄 문자열에서 들여쓰기를 사용할 경우, 내부 들여쓰기는 trimMargin, 외부는 trimIndent를 사용하세요

 

assertEquals(
    """
    Foo
    Bar
    """.trimIndent(), 
    value
)

val a = """if(a > 1) {
          |    return a
          |}""".trimMargin()

 

함수 vs 속성(function vs property)

경우에 따라 인수가 없는 함수는 읽기 전용 속성과 호환 될 수 있습니다. 이 때 문법은 유사하지만 각 경우 마다 스타일 규칙이 다를 수 있습니다. 

 

다음과 같은 경우에는 함수보다 속성을 선호합니다.

 

- throw가 아닐 때

- 계산 비용이 저렴할 때

- 객체 상태가 변경되지 않은 경우 호출에 대해 동일한 결과를 반환

반응형

댓글