[Kotlin] Coding Convention Guide

황용호·2025년 4월 16일

1. 소개

본 가이드는 Kotlin 공식 코딩 컨벤션을 바탕으로 하여, 코틀린 프로그래밍 시 일관된 코딩 스타일을 유지하기 위한 권장 사항을 제공합니다.


2. 네이밍 컨벤션

2.1 네이밍 구성

  • 네이밍은 그 목적이나 대상을 확실하게 표현하는 것이 좋습니다. 의미 없는 범용 단어만으로 구성하지 않습니다. (Helper❌, Wrapper❌)
  • 클래스 / 오브젝트 / 인터페이스 / 변수 / 속성의 이름은 그 것이 무엇인지 직접적으로 나타내는 단어, 주로 명사나 명사구를 선택하는 것이 좋습니다. 동사같이 동작 자체를 명시하는 단어는 사용하지 않습니다. (ArrayList⭕️, FileReader⭕️, RunCommand❌ → CommandRunner⭕️)
    다만, 변수 / 속성이 특정 상태(주로 참/거짓)를 나타내고 있다면 동사(주로 수동형) 또는 형용사를 사용할 수 있습니다. (isDirty, sorted, hasDigit)
  • 함수 / 메쏘드 의 이름은 작업을 표시하는 동사 또는 동사구를 사용하는 것이 좋습니다. (close, readFile, appendNode)
  • 클래스의 멤버 함수의 경우 클래스 명이 의미가 확실한 명사형인 경우, 동사로만 멤버 함수명으로 지정할 수 있습니다. (FileStream.readStream()❌ → FileStream.read()⭕️)
  • 네이밍 구성 단어 중 두 글자의 약어는 대문자로 표시할 수 있습니다. IOStream, myPKField
  • 세 글자 이상의 약어는 기존 컨벤션을 준수합니다. fun addXmlDeclaration() { ... }

2.2 package 명

  • 패키지 명은 항상 소문자와 숫자, .(점)만을 사용합니다. 단 숫자는 단어 가장 앞에 사용하지 않습니다. (com.company.departure⭕️, com.company.departure.v1.0❌)
  • 명사만 사용하고 가급적 연결된 단어를 사용하지 않습니다. (com.mycompany.myproject❌)
  • 연결 단어가 불가피한 경우 camel case를 사용할 수는 있지만 권장하지는 않습니다. (com.myCompany.myProject)

2.3 class / object / interface 명

  • 클래스 명과 오브젝트 명, 인터페이스 명은 모두 파스칼 케이스(PascalCase, 단어 첫 글자가 대문자)로 표시합니다.
class MyClass {}
data class SomeDataClass {}
object DerivedObject : BaseObject() {}
interface MyTableRepository : JpaRepository<MyEntity, IdClass> 

2.4 function, variable, property, constance 명

  • 함수 명과 변수 명은 모두 카멜 케이스를 사용하며 밑줄(_, underscore)은 사용하지 않습니다.
class MyClass {
  private var myPrivateProperty: Int = 0
  ...
  fun getPrivatePropertyPlusOne() = myPrivateProperty + 1
}
  • 명시적이든 아니든 상수의 성격으로 정의되는 속성과 열거형 클래스(enum class)는 모든 글자가 대문자인 단어를 밑줄로 연결한 형태(SCREAMING_SNAKE_CASE)를 사용합니다.
object NullResponse {
    const val STRING = "null"
    const val INT = -1
    const val LONG = -1L
    const val DOUBLE = 0.0
    val DATE = LocalDate.MIN
    const val YEAR = 1970
}
enum class PeriodType(val value: String, val comment: String) {
    UNKNOWN("", ""),
    STUDY_PERIOD("P", "학습 기간"),
    STUDY_START("S", "학습 시작일"),
    STUDY_END("E", "학습 종료일");
}
  • 의미적으로는 동일하지만 외부에 공개되는 속성과 매핑되는 동일한 이름의 내부 private 속성 명 앞에는 예외적으로 밑줄(Under score)을 붙여 줄 수 있습니다.
class SomeClass {
    private val _propertyList = mutableListOf<Property>()
    val propertyList: List<Property>
         get() = _propertyList
}

2.5 테스트 함수

  • 유일하게 테스트 함수에서만 백틱(`)을 이용하여 자유 문장형 네이밍이 가능합니다.
  • 단, 문장에 /, \, ., <, >, [, ] 문자는 사용할 수 없습니다.
class MyTestCase {
     @Test 
     fun `과정 별 수료 현황 API를 테스트하고 Rest-Doc으로 OAS 문서를 생성한다`() { ... }
}

3. 코드 서식

3.1 들여 쓰기(Indentation) 및 공백(spacing)

  • 들여 쓰기는 공백(스페이스) 문자 4개를 사용합니다. 탭 문자(\t)는 사용하지 않습니다.

  • 중괄호 ({ })

    • 여는 중괄호({)는 중괄호가 필요한 구문 줄 끝에 한 칸 공백을 넣어서 배치하고, 닫는 중괄호(})는 별도 줄의 여는 중괄호가 있는 구문과 수직 줄맞춤된 위치에 배치합니다.
    if (elements != null) {
        for (element in elements) {
            // ...
        }
    }
    • 중괄호 내 내용이 매우 간략하고 전체 줄이 right-margin을 넘지 않는다면 필요한 구문 끝에 한 줄로 배치할 수도 있습니다. 이 때는 중괄호 내부 앞뒤에 한 칸 공백을 넣어 줍니다.
    fun from(v: String) = values().find { v.equals(it.value, true) }
  • 괄호 (( ))

    • 흐름 제어 키워드 (if, when, for, while) 뒤의 여는 괄호 앞에 공백을 한 칸 넣습니다.
    • 생성자 선언, 함수/메서드 선언 및 호출 시 여는 괄호 앞에 공백을 넣지 않습니다.
    class A(val x: Int)
    fun foo(x: Int) { 
    	if (x > 100) ... 
    }
    fun bar() {
        foo(1)
    }
  • 콜론 (:)

    • 콜론 뒤에는 항상 공백을 넣어 줍니다.
    • 다음의 경우에 콜론 앞에 공백을 넣어 주고 이외의 경우에는 콜론 앞에 공백을 넣지 않습니다.
      • 상위 클래스에서 상속 받을 때
      • 상위 클래스 또는 같은 클래스의 오버로드된 생성자를 호출(위임)할 때
      • 타입과 서브 타입을 구별할 때
      • 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 { ... }
      }
  • 이항 연산자 앞 뒤에 공백을 넣습니다. ( a + b * c ) 단, 범위 연산자(..) 앞 뒤에는 공백을 넣지 않습니다. (0..i)

  • (), [], <> 사이에 공백을 넣지 않습니다.

    fun <T : Annotation> collectConstantsAsClass(clazz: Class<T>): List<Class<*>> { ... }
  • ., ?., :: 앞뒤에 공백을 넣지 않습니다. ?: 앞 뒤에는 공백을 넣어 줍니다.

    list?.filter { it != "" }?.joinToString(", ") { it.toString() } ?: ""
  • // 또는 , 뒤에 공백을 넣어 줍니다. // 주석입니다. (1, 2, 3)

  • Nullable 지정자 (?) 앞에 공백을 넣지 않습니다.

    class StringToCourseTypeConverter : Converter<String, CourseType> {
        override fun convert(source: String): CourseType? {
            return CourseType.values().firstOrNull {
                it.value.equals(source, true)
            } ?: findEnumInsensitiveCase(CourseType::class.java, source)
        }
    }

3.2 줄 바꿈

  • 코드 한 줄은 특별한 예외 상황이 아니면 항상 120자 내로 유지합니다.
  • 한 줄 길이가 너무 길거나 가독성을 위해 별도의 줄 바꿈이 필요할 때 다음의 원칙을 적용합니다.
    • 함수 또는 클래스 선언 및 호출 시 매개변수 리스트가 많을 경우
      • 괄호의 시작은 동일하며 첫 번째 매개 변수부터 아래 줄로 줄바꿈하여 하나씩 나열합니다. 단, 길이가 짧고 밀접하게 연관된 여러 매개 변수를 같은 줄에 배치할 수 있습니다.
      • 이 때 줄바꿈된 매개 변수에는 한 칸 들여쓰기를 합니다.
      • 닫는 괄호는 새 줄에 있어야 하며 여는 괄호의 구문과 align이 맞아야 합니다.
      class CompletionRequest(
          val page: Int = 1, // 페이지 번호
          val perPage: Int = DEFAULT_PER_PAGE, // 페이지당 건수
          val trainingCenterSeq: Int = 0, // 연수원 코드
          var fromDate: LocalDate, // 기간 검색 시작일
          var toDate: LocalDate // 기간 검색 종료일
      ) {
          fun setRequest(
              page: Int = 1, perPage: Int = DEFAULT_PER_PAGE,
              trainingCenterSeq: Int = 0,
              fromDate: LocalDate, toDate: LocalDate
          ) { ... }
      }
    • 상속받을 상위 유형이 많을 경우
      • 상속을 위한 콜론(:)을 줄 마지막에 붙이고 상속 받을 유형들을 다음 줄로 줄바꿈하여 하나씩 나열합니다.
      • 매개변수와 동일하게 상위 유형 나열 시 한 칸 들여쓰기를 합니다.
      • 클래스 바디 구문은 나열한 상위 유형들과 구분을 하기 위해 클래스 바디 첫 줄에 빈 줄을 넣어 주거나 여는 중괄호를 한 줄 밑에 넣어 줍니다.
        class MyFavouriteVeryLongClassHolder :
            MyLongHolder<MyFavouriteVeryLongClass>(),
            SomeOtherInterface,
            AndAnotherOne {
            fun foo() { /*...*/ }
        }
        또는
        class MyFavouriteVeryLongClassHolder :
            MyLongHolder<MyFavouriteVeryLongClass>(),
            SomeOtherInterface,
            AndAnotherOne
        {
            fun foo() { /*...*/ }
        }
    • 호출 체인 (Call Chain)
      • 함수 호출 체인이 길게 이어질 경우 . 또는 ?. 연산자로 시작하는 한칸 들여쓰기로 줄바꿈을 합니다.
      • 원칙적으로 체인의 첫번째 호출부터 줄바꿈하는 것이 좋지만 의미에 따라 첫 번째 호출은 같은 줄에 두어도 무방합니다.
        RestDocumentationRequestBuilders
            .get("$API_BASE_PATH/completion/course")
            .characterEncoding("UTF-8")
            .queryParam("page", "1")
            .queryParam("perPage", "5")
            .queryParam("trainingCenterSeq", "111")
            .queryParam("periodSearchType", PeriodSearchType.BY_PERIOD.value)
            .queryParam("periodSearchYear", "2023")
            .queryParam("periodSearchMonth", "12")
            .queryParam("periodType", PeriodType.STUDY_START.value)
            .queryParam("companySeq", "208")
            .queryParam("companySeq", "224")
            .queryParam("courseType", CourseType.ONLINE.value)
            .queryParam("exceptCourse", ExceptCourseType.SAFETY.value, ExceptCourseType.FREE.value)
            .queryParam("fromDate", "2023-01-01")
            .queryParam("toDate", "2023-12-31")
        또는
        RestDocumentationRequestBuilders.get("$API_BASE_PATH/completion/course")
            .characterEncoding("UTF-8")
            .queryParam("page", "1")
            .queryParam("perPage", "5")
            .queryParam("trainingCenterSeq", "111")
            .queryParam("periodSearchType", PeriodSearchType.BY_PERIOD.value)
            .queryParam("periodSearchYear", "2023")
            .queryParam("periodSearchMonth", "12")
            .queryParam("periodType", PeriodType.STUDY_START.value)
            .queryParam("companySeq", "208")
            .queryParam("companySeq", "224")
            .queryParam("courseType", CourseType.ONLINE.value)
            .queryParam("exceptCourse", ExceptCourseType.SAFETY.value, ExceptCourseType.FREE.value)
            .queryParam("fromDate", "2023-01-01")
            .queryParam("toDate", "2023-12-31")

4. 주석 및 문서화

  • 긴 내용의 주석은 /** 로 열어서 */ 로 끝나는 주석 형식을 사용해 주세요. 주석 중간의 라인은 * 로 시작하게 합니다.
  • @param 이나 @return 태그는 사용하지 않는 것이 좋습니다.
  • TODO: FIXME: 같은 태그는 적극 활용하는 것이 좋습니다.
    /**
      * status = 1이고 learningTime = 0이면
      * 전체 교육 시간을 리턴한다.
      * TODO: 전체 교육 시간 계산 산식이 변경될 경우 이 함수를 수정해 주자.
      * by hyh 2024/01/10
      */

5. 기타 사항

  • 타입 추론이 가능한 경우 가급적 타입을 명시적으로 선언하지 않습니다. val name = "Kotlin"
  • 상수 성격의 문자열이나 리터럴 값을 소스 코드에 하드 코딩하기 보다는 그 의미를 담고 있는 상수형 변수나 enum class를 활용하는 것이 좋습니다. 특히 코드 성격의 상수는 가급적 상수형으로 만들어 주세요.
    object YnFlag {
        const val Y = "Y"
        const val N = "N"
        const val y = "y"
        const val n = "n"
        const val UNKNOWN = ""
        fun isY(target: String, ignoreCase: Boolean = true) = target.equals(Y, ignoreCase)
        fun isN(target: String, ignoreCase: Boolean = true) = target.equals(N, ignoreCase)
        fun getYn(condition: Boolean) = if (condition) Y else N
    }
    fun String.isY(ignoreCase: Boolean = true) = equals(YnFlag.Y, ignoreCase)
    fun String.isN(ignoreCase: Boolean = true) = equals(YnFlag.N, ignoreCase)
    fun Boolean.getYn() = YnFlag.getYn(this)
    ...
    var checked = YnFlag.Y
    if (checked.isY()) { ... }
  • 코틀린에서 제공하는 Conditional statement를 사용하면 코드가 훨씬 간결해 집니다.
    fun checkCondition(someCondition: Boolean): String {
        var conditionState: Int
        if (someCondition)
            conditionState = 1
        else 
            conditionState = 0
        ...
        when(conditionState) {
            0 -> return "False"
            else -> return "True"
        }
    }
    이 코드는 다음과 같이 작성하는 것이 훨씬 간결합니다.
    fun checkCondition(someCondition: Boolean): String {
        var conditionState = if (someCondition) 1 else 0
        ...
        return when(conditionState) {
            0 -> "False"
            else -> "True"
        }
    }
  • 후행 콤마(Trailing Comma, 일련의 콤마로 나열된 요소들 중 마지막 요소 뒤에 붙는 콤마)는 여러 이점이 있으므로 사용이 권장됩니다.
  • 문장 마지막에 붙는 세미콜론(;)은 코틀린에서는 생략이 가능하므로 가능한한 생략합니다.
  • 문자열 템플릿에 단일 변수만 삽입할 때는 중괄호를 생략하는 것을 권고합니다.
  • 가능한한 지역 변수보다 코틀린에서 제공하는 스코프 함수(apply, with, run, also, let)를 사용해 주세요.

0개의 댓글