코틀린 컨벤션 정리, 한글 번역
스똬뚜!
본 글은, 왜 코틀린 공식 다큐멘트가 이런 순서로 작성되어있는지 작성자의 마음에서 이해하고자 노력하였다. 그래서 매 문단은 질문형으로 시작함을 참고하여 읽어주면 너무탱큐베리캄사!
자, 그러면 본격적으로 정식 다큐멘트 본격 해부!
정식 다큐멘트 링크: https://kotlinlang.org/docs/coding-conventions.html#directory-structure
참고한 추가 블로그들 (일부 코드들은 해당 블로그들을 참고했습니다.): https://medium.com/mj-studio/코틀린-이렇게-작성하시면-됩니다-94871a1fa646
https://medium.com/@joongwon/kotlin-코딩-컨벤션-정리-7681cde920ce
1. Configure style in IDE
"자, 바쁜 현대인이여!! 긴 공식 독스? 안 읽을 것 알아. 그리고 심지어 컨벤션이지. 알아 너네가 관심있는 건 아래의 내용이 다 담겨서 자동으로 해주는 기능을 찾는거지? 엿다! 급한 사람들은 이것만 보고 나가면 됨><"
ide에 설정하는 방법이 젤 먼저 등장!!
자세한 방법은 타 블로그에도 설명이 잘 되어있다!
- 적용하기: Settings/Preferences | Editor | Code Style | Kotlin. > Set from... -> Kotlin style guide.
- 검토하기: Settings/Preferences | Editor | Inspections | General.
2. Source code organization
2-1. Directory structure
"올, 우선 여기까지 읽을 것이라는 건, 이제 본격적으로 배워보겠다는거지? 그러면 정말 본질적인 질문부터 차근차근 시작해보자."
kotlin 프로그램의 경우 이런 구조로 코드를 짭니다!
바닐라 코틀린 프로젝트의경우 directory structure의 경우 common root package가 제외되어있는 package structure를 따라하라구~
예) org.example.kotlin package와 그 subpackage에 코드들이 있는 경우, org.example.kotlin 패키지는 source root에 있어야 하며, org.example.kotlin.network.socket은 network/socket이라는 source root의 subdirectory에 있어야 한다.
2-2. Source file names
"그래, 구조는 알았어. 그러면 이제 그 구조에 맞게 파일을 만들면서 코드를 짤건데, 파일명은 어떻게 하면 좋을까?"
파일 이름은 "클래스의 이름.kt"로 해줘. 만약 클래스가 여러개 있는 파일이라면 이들을 포함하는 것으로 파일명을 해줘(잘 하진 않아.). 그리고 upper camel case로 해줘. 예) ProcessDeclaration.kt
그리고 주의!! 파일명은 그 파일 안에 있는 코드를 설명할 수 있어야 해. 무의미한 이름은 지양해줘. 예) Util
2-3. Source file organization
그래 파일명을 이제 알았으니, 구체적으로 소프 파일을 어떻게 구성할지 고민해볼까?
코틀린에서 여러가지 선언들(클래스, 전역 함수나 전역 변수)을 하나의 파일에 정의하는 것은 다음과 같은 조건들이 충족되는 한에서만 추천한다구~
- 그것들이 밀접하게 관련되어있고!
- 한 파일에 두어도 몇백줄을 넘기지 않는 선에서!
2-4. Class layout
자 이제 본격적으로 클래스 레이아웃을 보자구!
프로퍼티, init 블록, 부생성자, 메서드, 동반 객체 순으로 짠다! class도 다 순서가 있다 이말이야~
class A {
프로퍼티
private var age: Int? = null
init 블록
init {
age = 24
}
부 생성자
constructor(age: Int) {
this.age = age
}
메서드
fun printAge() = println(age)
fun useNested() {
NestedForInternal()
// ...
}
class NestedForInternal {
}
동반 객체 (컴패니언 객체)
companion object {
const val DURATION = 300
}
class NestedForExternalClient {
}
}
아, 그리고 메서드들을 알파벳 순 (X), 비슷한 기능들끼리 묶어서 보이고, 큰 레벨에서 작은 레벨 혹은 그 반대로 짜는 편! 즉, 관련된 것들을 한데 모아 클래스를 읽는 사람이 위에서 아래로 무슨 일이 일어나고 있는지 로직을 따라갈 수 있도록!!
그리고 Nestest Class의 경우~
- 그 클래스를 사용하는 클래스 내의 코드나 메소드 바로 밑에 위치!
- 만약 외부에서 사용하는 경우라면 Companion 객체 이후에 선언~!
2-5. Interface implementation layout
클래스를 알아봤으니 인퍼페이스도 알아봐야겠지?
인터페이스의 경우 클래스와 거의 동일하게 해주면 됨!
interface User {
var age: Int
var name: String
}
class GameUser : User {
override var age = 24
override var name = "MJ"
}
2-6. Overload layout
overloads도 알아보자구~
Always put overloads next to each other in a class. → 움? 해석 못했음…ㅠ
3. Naming rules
이제부터는, 큰 것들은 얼추 알아봤으니, 이름 짓는 법들을 알아보자구~
- package: 알파벳 소문자로 언더스코어(_) 없이! 예) (org.example.project )
- classes & objects: uppercase camel case!!
- 예)
open class DeclarationProcessor { /*...*/ }
object EmptyDeclarationProcessor : DeclarationProcessor() { /*...*/ }
3-1. Function names
클래스 다음은 뭐다? 함수, 메소드, 변수지~
함수, 메소드, 변수, 클래스 내 속성, 지역 변수 등은 다 camelCase!
예)
fun processDeclarations() { /*...*/ }
var declarationCount = 1
예외)
팩토리 패턴에서 팩토리 함수를 간혹 클래스 이름과 동일한 이름으로 작성해주는 경우도 있음!
interface Foo { /*...*/ }
class FooImpl : Foo { /*...*/ }
fun Foo(): Foo { return FooImpl() }
3-2. Names for test methods
테스트 메소드도 중요하다구우우우~
백틱(`) 으로 감싼 자유로운 문자열이나 언더스코어 사용이 가능! 간혹 안드로이드 런타임에서 백틱사용이 불가능한 경우가 있어유~
참고한 블로그 필자 분의 경우 주로 Unit 테스트는 백틱으로 감싸고 Instrumentation 테스트는 뒤에 _onAndroid 를 붙여주는, 예제와 같은 방식을 활용함~~
class MyTestCase {
@Test fun `ensure everything works`() { /*...*/ }
@Test fun ensureEverythingWorks_onAndroid() { /*...*/ }
}
3-3. Property names
불변하는 속성, const 상수, Top Level 상수들은?!
불변하는 속성, const 상수, Top Level 상수들, enum 클래스: 대문자와 언더스코어!
const val MAX_COUNT = 8
val USER_NAME_FIELD = "UserName"
enum class Color { RED, GREEN }
object properties나 mutable data: camel case
val mutableCollection: MutableSet<String> = HashSet()
3-4. Names for backing properties
pravate한 변수들은 어떠할까!?
정보은닉을 위해 실제 값은 private로 숨기고자 할 때 이렇게 한다.
- 실제 키는 height로 되어있고 가짜 키는 _height라고 할 때
class Celebration {
private val _height: Int = 250
val height: Int
get() = _height + 5
}
3-5. Choose good names
자, 이건 좀 브로드하게 적용되는 거긴 하지만, 이제 전반적인 규칙은 알았으니, 이름을 어떻게하면 잘 짓는 것인지 고민을 같이 해볼까?!
필요없는 접미사, 접두사를 뺴자. 예) manager, wrapper, userList // userList → users
단, Manager의 경우 구글에서 여러 시스템과 소통할 수 있는 클래스들의 접미사로 Manager를 붙이기 때문에 때에 따라서 다르게 접근해야 함.
Formatting
4-1. Indentation
포멧팅에 있어 가장 기본적인 것부터 해볼까?! 우선 들여쓰기 규칙을 보자구
들여쓰기는 4개 스페이스! Tab은 X!
중괄호 열고 코드는 그 다음 줄에~
if (elements != null) {
for (element in elements) {
// ...
}
}
4-2. Horizontal whitespace
그 다음에 기본적인 띄여쓰기 보자~
- binary operator에는 스페이스를 둔다: ( a + b )
- 단, range to 에서 0..i의 경우 스페이스 X
- unary operator에는 스페이스 X: ( a++ )
- ( if, when, for, and while )와 같은 control flow keywords에는 스페이스!
- 주 생성자 선언, 메소드 선언, 메서도 호출 등의 opening parenthesis에 대해서는 스페이스 X
class A(val x: Int) fun foo(x: Int) { ... } fun bar() { foo(1) }
- 예)
- 스페이스 X: ( , [ , or before ] , )
- 스페이스 X: . or ?. : 예) foo.bar().filter { it > 2 }.joinToString() , foo?.bar()
- 스페이스 O: // 예) // This is a comment
- 스페이스 X: 타입: angle bracket에 쓰인 type parameter의 경우. 예) class Map<K,V> { … }
- 스페이스 X: :: 예) Foo::class, String::length
- 스페이스 X: nullable type 명시하기 위한 곳에 ? 안 쓴다. 예) String?
4-3. Colon
Colon은 좀 많이 쓰고 특별하니까 따로 취급해주자고
- 스페이스 O:
- type과 supertype을 분리할 때
- 생성자 관련될 때
- object keyword 다음에
- :를 쓴 다음엔 무조건 한 칸 띈다.
- 스페이스 X:
- 선언과 타입을 구분할 때는 안 띄움~!
abstract class Foo<out T : Any> : IFoo {
abstract fun foo(a: Int): T
}
class FooImpl : Foo() {
constructor(x: String) : this(x) { /*...*/ } // 여기서 x: String 이렇게 x:에만 띄여쓰기가 없는 것을 알 수 있음!!
val x = object : IFoo { /*...*/ }
}
4-4. Class headers
함수 해더부터 차근차근 알아보자
대체로 생성자 패러미터들이 3개 이상되면 한 줄에 하나씩 분리해서 쓰는 편!
class Person(
id: Int,
name: String,
surname: String
) : Human(id, name),
KotlinMaker { /*...*/ }
4-5. Modifiers order
모디파이의 경우 순서가 많이 있으니 다음의 순서대로 넣어주세요
Modifier: 선언 앞이나 변수 앞에 붙이는 언어의 예약어. 외우진 말자. 그냥 이런 게 있다 알고 찾아보면 되는 거~!
public / protected / private / internal -> 가시성 제한자
expect / actual -> 코틀린 멀티플랫폼의 예약어, 사용할 일 X
final / open / abstract / sealed / const -> 상속의 제한이나 추상 클래스, 봉인 클래스, 상수를 정의
external -> JNI의 함수여서 C나 C++를 호출하는 함수를 의미
override -> 오버라이딩 지시자
lateinit -> 변수 지연 초기화 지시자
tailrec -> 재귀함수를 반복함수로 최적화 시켜줌
vararg -> 함수에서 길이를 모르는 인자를 배열로 받게해줌
suspend -> 코루틴 suspend 함수
inner -> Nested Class의 귀속화
enum / annotation / fun // as a modifier in `fun interface` -> enum 클래스, 어노테이션 클래스, 함수형 인터페이스를 정의
companion -> 컴패니언 객체
inline -> 인라인 함수
infix -> 함수를 infix 호출을 할 수 있게해주는 지시자
operator -> 연산자 오버로딩
data -> 데이터 클래스
예)
inline infix fun Int.hello(a: Int) {
} // 와 같은 순서로 작성한다~
annotations의 경우 modifiers 전에 붙인다.
@Named("Foo")
private val foo: Foo
또한, 중복되는 modifier는 제거한다. 예) public. kotlin에서는 defautl가 public이다.
4-6. Annotations
Annotations은 거의 한 줄을 따로 뗀다고 생각하면 편함!
@JsonExclude @JvmField
var x: String
단, argument가 없는 싱글 annotation의 경우 한 라인에 다 있어도 된다.
@Test fun foo() { /*...*/ }
→ 우테코 과정에서는 두 줄로 분리했는데 이렇게 해도 되었다니~~ㅎㅎㅎ
4-7. File annotations
파일 어노테이션은 항상 코멘트 다음으로 최상단!
파일 커맨트 → file annotations → package 순!
/** License, copyright and whatever */
@file:JvmName("FooBar")
<- 여기 한 줄 띈 거 확인!~
package foo.bar
4-8. Functions
함수는 인자의 크기에 따라 줄을 나누세요~
함수의 인자가 최대 줄 수 (표준 스타일 가이드에선 120)을 넘기면 모두 multi-line으로 해주세요~
// Bad
fun longMethodName(argument: ArgumentType = defaultValue,
argument2: AnotherArgumentType,
): ReturnType {
// body
}
// Good
fun longMethodName(
argument: ArgumentType = defaultValue,
argument2: AnotherArgumentType,
): ReturnType {
// body
}
또한, 한 줄 짜리 바디가 있는 함수라면 한 줄로 무조건 하세요~~
fun foo(): Int { // bad
return 1
}
fun foo() = 1 // good
4-9. Expression bodies
experssion body가 너무 긴 경우~~
다음처럼 두 문장으로 해주세요~
fun f(x: String, y: String, z: String) =
veryLongFunctionCallWithManyWords(andLongParametersToo(), x, y, z)
4-10. Properties
프로퍼티 중요하다~
val isEmpty: Boolean get() = size == 0 // 한줄로 표현이 가능할 때
val foo: String // 한줄로 표현이 불가능할 때
get() { /*...*/ }
속성 초기화에서 한 줄로 표현이 안 된다면 = 뒤로 개행~
private val defaultCharset: Charset? =
EncodingRegistry.getInstance().getDefaultCharsetForPropertiesFiles(file)
4-11. Control flow statements
if, else, try, catch, finally, when들에 대해서
if (!component.isSyncing &&
!hasAnyKotlinRuntimeInScope(module)
) {
return createKotlinNotConfiguredPanel(module)
}
if (condition) {
// body
} else {
// else part
}
try {
// body
} finally {
// cleanup
}
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-12. Method calls
만약 함수 호출을 할때 인자들이 길어서 한줄에 표현되지 않는다면 무조건 ( 뒤에 개행을 해주시고 서로 연관있는 인자들끼리 한줄로 표현해주시면 됨~~
drawSquare(
x = 10, y = 10,
width = 100, height = 100,
fill = true
)
-> x,y 묶고, width, height 묶고~
4-13. Wrap chained calls
옵셔널 체이닝을 사용하거나 체이닝을 사용하는데 한 줄을 넘어간다면 ?. 와 . 가 제일 앞에 오고 들여쓰기를 해서 작성
val anchor = owner
?.firstChild!!
.siblings(forward = true)
.dropWhile { it is PsiComment || it is PsiWhiteSpace }
4-13. Lambda
이제 람다에 대해 알아보자.
list.filter({ it > 10 }) // bad
list.filter { it > 10 } // good
람다에 라벨링을 해준다면 람다와 @ 사이에 여백을 두지 말아달라구~
fun foo() {
ints.forEach lit@{
// ...
}
}
만약 람다식을 사용할때 기본으로 주어지는 it 대신 다른 이름을 쓴다면 -> 뒤에 개행을 해주고 람다식을 작성!
appendCommaSeparated(properties) { prop ->
val propertyValue = prop.get(obj) // ...
}
4-14. Trailing commas
코틀린 1.4부터 추가된 문법~
class Person(
val firstName: String,
val lastName: String,
val age: Int, // trailing comma
)
이걸 하는 이유?!
- version-control diffs cleane
- easy to add and reorder elements - 콤마 신경 안 써도 되니까!
- simplifies code generation, for example, for object initializers.
물론 안 해도 됨~~
5. Documentation comments
주석 남기는 법도 배우자
긴 주석은 피하고 주석 본문에 의미를 담자!!
@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) { /*...*/ }
6. Avoid redundant constructs
코틀린의 경우 여러가지 추론에 의한 문법 형성이 잘 되어있음!! 그러니 쓸모없이 반복은 지우자.
- 아무것도 반환하지 않는 함수의 Uniy 반환형
fun foo() { // ": Unit" is omitted here
}
- 세미콜론(;)
- 문자열 템플릿에서 안써도 되는 중괄호
"$name" // good
"${name}" // bad
"${user.name}" // good.
println("$name has ${children.size} children")
7. Idiomatic use of language features
7-1. Immutability 불변성.
불변과 가변의 세상으로 가보자고!
코틀린의 var VS val: val을 디폴트로
리스트들도: MutableList VS List: List가 디폴트로
// 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")
7-2. Default parameter values
함수의 기본 인자를 사용할 수 있으면 바로 사용~ 정의와 선언 한 번에 해라~
// Bad
fun foo() = foo("a")
fun foo(a: String) { /*...*/ }
// Good
fun foo(a: String = "a") { /*...*/ }
7-3. Type aliases
반복되는 것이 있다면, type alises를 고민해봐라
typealias MouseClickHandler = (Any, MouseEvent) -> Unit
typealias PersonIndex = Map<String, Person>
단, 이름 출동을 피하기 위해 private or internal type alias를 이용한다면 import …를 활용해라.
7-4. Lambda parameters
람다가 짧고 nested 되어있지 않다면 it를 그냥 쓰는것이 좋음!
// good
list.forEach {
println(it)
}
// bad
list.forEach { n ->
println(n)
}
7-5. Returns in a lambda
람다의 라벨 반환은 사용하지 않는 것을 추천! 필요하다면 코드를 변경하라구~
fun main(vararg args: String) {
val list = listOf(1, 2, 3)
list.map {
return it // error
}
}
→ 에러가 나는 이유: map함수가 인라인 함수이기 때문. map 함수에 전달된 람다가 논로컬반환이 되어 main함수에서 반환을 하려고 하기 때문에.
fun main(vararg args: String) {
val list = listOf(1, 2, 3)
list.map label@{
return@label it // 사실 이건 그냥 label들 빼고 it만 써도 됨.
}
}
→ 위와 같은 문제를 해결하기 위해 람다에 라벨링을 해서 람다 스코프에서 값을 반환함
7-6. Named arguments
변수 이름은 직관적으로 맞게 잘 지으라고!
drawSquare(x = 10, y = 10, width = 100, height = 100, fill = true)
7-7. Conditional statements
return을 if나 when 앞에 쓰는 것이 개별적으로 안에 쓰는 것보다 좋다~~
return if (x) foo() else bar() 가
if (x)
return foo()
else
return bar()
보다 낫다.
return when(x) {
0 -> "zero"
else -> "nonzero"
} 가
when(x) {
0 -> return "zero"
else -> return "nonzero"
}
보다 낫다.
7-8. if versus when
when은 3개 이상 옵션이 있을 때 선호~ 아닌 경우에는 if문 선호~
if (x == null) ... else ... 가 낫다.
when (x) {
null -> // ...
else -> // ...
} 보다는~
7-9. Nullable Boolean values in conditions
조건문에 있어서는 nullable Boolean을 사용해주세요!!
if (value == true) or if (value == false)
7-10. Loops
filter, map, foreach를 쓰는 게 더 좋다?
filter, map과 같은 higher-order functions을 쓰는 것을 더 고려해!! 단, foreach의 경우 receiver가 nullable이거나 longer call chain에 속하는 게 아니라면 for를 더 선호!
7-11. Loops on ranges
n-1 vs until
for (i in 0..n - 1) { /*...*/ } // bad
for (i in 0 until n) { /*...*/ } // good
7-12. Strings
trimIndent()와 trimMargin()을 적극 활용하자!
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)
7-13. Functions vs properties
코틀린은 속성이 getter 와 setter 로 함수의 역할을 대신할 수 있기에 속성을 함수보다 선호할 때도 있으니 체크!
- 에러를 던지지 않는다
- 계산하기 쉽다
- 객체의 상태가 불변하는 한 같은 값을 반환한다
7-14. Extension functions
가능하면 쪼개고, 접근 제어자로 제어해달라!
Extention functions을 잘 만들자! 쪼개고 접근 제어하고!
7-15. Infix functions
비슷한 기능을 할 때만 infix를 정의하자!
Good examples: and, to, zip. Bad example: add .
7-16. Factory functions
팩토리 함수에서 클래스와 같은 이름을 쓸 수는 있지만 이는 권장되는 문법이 아닙니다유. 의미있는 이름을 붙여주어요~~
class Point(val x: Double, val y: Double) {
companion object {
fun fromPolar(angle: Double, radius: Double) = Point(...)
}
}
7-17. Platform types
platform type expression을 제공하는 public 함수/메소드의 경우 kotlin 타입을 명시해달라구~
fun apiCall(): String = MyJavaApi.getProperty("name")
class Person {
val name: String = MyJavaApi.getProperty("name")
}
7-18. Scope functions apply/with/run/also/let
코틀린을 코틀린답게!
apply/with/run/also/let에 대해서 알아보자! 링크: https://kotlinlang.org/docs/scope-functions.html
8. Coding conventions for libraries
라이브러리 가져올 때 컨벤션으로 끄읕! 이 부분은 아직 명확하게 이해가 안되어서 영문으로!ㅎㅎ
- Always explicitly specify member visibility (to avoid accidentally exposing declarations as public API)
- Always explicitly specify function return types and property types (to avoid accidentally changing the return type when the implementation changes)
- Provide KDoc comments for all public members, with the exception of overrides that do not require any new documentation (to support generating documentation for the library)
이제 끄읕!! 이 많은 것을 다 외울 수는 없으니!! 기본적인 법칙으로 이것만 이해하자!!
그리고 실제로 우테코를 하면서 주시는 피드백들을 잘 반영하면 여기서 말하는 컨벤션은 거의 다 챙긴다는 것을 알 수 있었다!!!
다만, 아쉬운 것은 여전히 main문을 어디에 둬야할지에 대해서….. 맨 밑이 좋은 지 맨 위가 좋은 지에 대해서 알 수가 없었다. 이건 개별 기업마다 컨벤션이 있는걸로?!