-
들어가기에 앞서
-
기초 설정
-
1. 스타일 가이드 적용하기
-
2. 코드가 스타일을 따르는 지 검사 받기
-
소스코드에 대한 구성
-
디렉토리 구조
-
소스 파일 이름
-
소스 파일 정리
-
클래스 내부 레이아웃
-
인터페이스 구현
-
오버로딩 레이아웃
-
네이밍 컨벤션
-
패키지
-
클래스
-
메서드
-
팩토리 메서드
-
테스트 메서드
-
프로퍼티
-
백킹 프로퍼티*
-
제대로 된 이름 짓기
-
포매팅
-
가로 공백
-
콜론
-
클래스 헤더
-
키워드 혼합 순서
-
어노테이션
-
파일 어노테이션
-
메서드 선언
-
메서드 호출
-
프로퍼티
-
흐름제어문
-
람다
-
후행 쉼표
-
문서 설명 (/** */)
-
중복 구조 피하기
-
문자열 템플릿
-
언어 기능의 관용적 사용
-
var 대신 val을 사용 권장
-
컬렉션은 불변을 먼저 고려
-
파라미터 디폴트 값
-
타입 별칭
-
명명된 인자
-
조건문
-
if vs when
-
조건에서 Boolean? 타입을 사용하는 경우
-
루프
-
문자열
-
함수 VS 프로퍼티
-
infix 메서드 사용
-
팩토리 메서드
-
라이브러리용 코딩컨벤션
-
마치며
들어가기에 앞서
💡 아주 기초가 되는 내용들은 소거했습니다. 이 점 참고바랍니다.
기초 설정
코틀린은 JetBrains에서 개발된 언어이기 때문에 IntelliJ와 Android Studio 등 IDEA에서 코딩 스타일에 대해 강력한 지원을 해주고 있습니다. 이러한 지원을 제대로 받기 위해서는 다음과 같은 설정이 필요합니다.
💡 설정은 IntelliJ를 기준으로 합니다.
1. 스타일 가이드 적용하기
- Settings/Preferences > Editor > Code Style > Kotlin 선택
- Set from…. 클릭
- Kotlin style guide 선택
2. 코드가 스타일을 따르는 지 검사 받기
- Settings/Preferences > Editor > Inspections > General 선택
- Incorrecting formatting 항목을 ✔️ 표시로 변경
소스코드에 대한 구성
디렉토리 구조
코틀린만 사용되는 구조일 때
- 코틀린만 사용되는 프로젝트의 디렉토리 구조는 공통 루트 패키지인 (src/main/kotlin)을 생략한 나머지 패키지의 구조를 따릅니다. 아래 패키지 구조에서 나머지 패키지는 com/example/dirStructure 를 말하고 있네요
└─src
└─ main
└─ kotlin <- 여기까지 공통 루트 패키지!
└─ com <- 여기부터 나머지 패키지 구조!
└─ example
└─ dirStructure
- 이 때, 프로젝트 내부의 모든 파일이 com/example/dirStrucrue 아래에서만 정의되고 있다면 여러분이 새로운 파일을 만들 때는 (com/example/dirStructure) 아래에만 생성해야 한다는 뜻입니다.
💡 연습을 한 번 해봅시다. dirStructure 상위에는 패키지 외에 어떠한 파일도 존재하지 않는다고 가정해보죠.
- network/socket 패키지가 추가된다면 어떤 패키지 아래에 정의를 해야 할까요?
- 추가로 MySocket.kt 파일을 만든다면 어디에 만들어야 할까요?
...
└─ dirStructure
└─ network
└─ socket
└─ MySocket.kt
- 정답은 간단합니다. dirStructure 상위 디렉토리에는 어떠한 파일도 생성되어있지 않고 있으니 dirStructure 아래로 패키지를 생성하고 이곳에 MySocket.kt 파일을 만들어 코드를 작성하면 됩니다.
- 자바와 함께 사용되는 구조일 때
- 자바와 함께 사용되는 구조일 때는 자바의 패키지 구조에 맞춰야 합니다. 자바 파일(.java) 과 같은 위치에 코틀린 파일이 위치해야하고, 디렉토리 구조도 자바의 구조를 따릅니다.
💡 왜 코틀린은 자바의 패키지 구조를 따라가나요?
- 이는 자바의 파일은(.java) 최상단에 패키지가 명시 되어야 하는데, 파일이 패키지와 같은 디렉토리에 위치하지 않는다면 에러가 나기 때문입니다.
// MySocket의 실제 위치는 com.example.javadir.network.socket 이지만
// 패키지를 com.example.javadir 같이 선언하면 다음과 같이 에러가 발생합니다.
// Package name 'com.example.javadir' does not correspond to the file path 'com.example.javadir.network.socket'
package com.example.javadir
public class MySocket {
...
}
- 반대로 코틀린의 파일은 패키지 선언의 위치가 실제 디렉토리와 일치하지 않아도 에러가 발생하지 않습니다.
- 따라서 에러가 나지 않는 코틀린이 자바와의 상호운용을 위해 자바를 맞춰주는 거라고 생각하시면 됩니다.
소스 파일 이름
- 코틀린의 확장자 : .kt
- 파일 내부에 단 하나의 인터페이스, 클래스만 존재할 때
- 해당 인터페이스, 클래스 명과 같은 이름으로 파일을 생성해야 합니다. 이는 모든 유형의 인터페이스, 클래스에 적용됩니다.
- 예를 들어, MyClass 라는 하나의 클래스만 파일 내에 존재한다면 파일명도 MyClass.kt라는 이름이 되어야 합니다.
파일 내부에 여러 개의 클래스가 존재하거나 최상위(Top-Level) 프로퍼티, 함수만 존재할 때
- 여러 개의 항목들을 공통적으로 설명할 수 있는 이름을 파일명으로 사용합니다.
// Console.kt
interface Input {
fun read(input: String?)
}
interface Output {
fun print(message: String?)
}
class Reader : Input {
private var inputValue = ""
override fun read(input: String?) {
input?.let { inputValue = it }
}
}
class Printer : Output {
override fun print(message: String?) {
message?.let { println(it) }
}
}
- 위 코드처럼 Input, Output, Reader, Printer 등 콘솔 입출력에 관련된 클래스 및 인터페이스가 한 파일에 동시에 존재할 때는 이를 모두 아우르는 Console.kt라는 이름으로 정의할 수 있겠습니다.
소스 파일 정리
소스 파일 정리 : 이 코드를 어디에 포함시킬지에 대한 내용을 다룹니다.
- 위에서 보았듯이, 코틀린은 한 파일 내에 독립된 클래스를 여러개 선언할 수 있습니다.
- 각 요소의 관계가 밀접한 경우에는 이런식으로 한 파일에 여러 클래스를 작성하는 것을 권장합니다.
- 각 클래스의 내용이 너무 길어지면 따로 나누는 것이 더 낫습니다.
- 각 요소 간 밀접한 관계가 없는 경우 클래스 별로 파일을 만들어야 합니다.
- 각 요소의 관계가 밀접한 경우에는 이런식으로 한 파일에 여러 클래스를 작성하는 것을 권장합니다.
- 확장 함수는 2가지로 나누어 정리할 수 있습니다.
- 모든 클라이언트가 확장 함수를 사용하는 경우
- 클래스 내부에 확장 함수를 위치시킵니다.
- 특정 클라이언트에서만 사용하는 경우
- 해당 클라이언트 코드에 함께 위치시킵니다.
- 모든 클라이언트가 확장 함수를 사용하는 경우
클래스 내부 레이아웃
클래스의 각 요소는 다음 순서로 배치시킵니다.
- 프로퍼티 선언 및 초기화 블록
- 부 생성자
- 메소드 선언
- 메소드의 선언은 알파벳순으로 정렬하는게 아닙니다. 또한 확장 함수를 분리하지 마세요.
- 클래스를 위에서 살펴보면서 메서드를 봤을 때 서로 관련있는 순서로 논리적 흐름에 맞게 나열하세요.
- 동반 객체
- 총 예시
class StudentRegisterResponse(
private val id: Int,
private val room: Int,
private val name: String,
) {
// 1. 프로퍼티 선언 및 초기화 블록
private var nickname: String = ""
init {
println("학생 가입 결과")
// ..
}
// 2. 부 생성자
constructor(id: Int, room: Int, name: String, nickname: String) : this(id, room, name) {
this.nickname = makeNickNameWithRealName(nickname)
}
// 3. 메소드 선언
private fun makeNickNameWithRealName(nickname: String) = "$nickname : $name"
// 중첩 내부 클래스
class NestedInternalClass {
/*...*/
}
// 4. 동반 객체
companion object {
fun from(student: Student): StudentRegisterResponse {
return StudentRegisterResponse(
id = student.name,
room = student.room,
name = student.name,
nickname = student.nickname
)
}
}
// 중첩 외부 클래스
class NestedExternalClass {
/*...*/
}
}
인터페이스 구현
인터페이스의 구현은 인터페이스에 선언된 순서로 작성을 해야합니다.
오버로딩 레이아웃
오버로딩된 항목들은 항상 나란히 배치해주세요
네이밍 컨벤션
각 케이스 정리
- 카멜 케이스 : camelCase
- 파스칼 케이스 또는 어퍼 카멜 케이스 : PascalCase, UpperCamelCase
- 스네이크 케이스 : snake_case
패키지
되도록 한 단어로 패키지 명을 작성해주시고, 단어 간 연결을 사용해야 한다면 모든 문자를 카멜케이스로 작성합니다.
- 예시) dirStructure
클래스
파스칼 케이스 또는 어퍼 카멜 케이스를 사용합니다.
- 예시) MyAweSomeClass
메서드
카멜케이스를 사용합니다. 밑줄을 사용하지 않습니다.
팩토리 메서드
클래스의 인스턴스를 반환하는 팩토리 메서드는 클래스의 이름을 그대로 사용해도 됩니다.
interface Foo { .. }
class FooImpl: Foo {
fun Foo(): Foo {
return FooImpl()
}
}
테스트 메서드
테스트 메서드에서만 백틱```을 사용하여 메서드 이름을 만들 수 있습니다.
class StudentCreationTest {
@Test
fun `학생이 생성된다`() {
..
}
}
프로퍼티
- 일반 프로퍼티 → 카멜 케이스를 사용합니다.
- 예시) val userName: String = “홍길동”
- 상수(const), 최상위(Top-Level) 프로퍼티 → 대문자로 이루어진 스네이크 케이스(스크리밍 스네이크 케이스)를 사용합니다. 예시
- 예시) const val PI = 3.14
- 동작 또는 변경 가능한(mutable) 최상위 프로퍼티 → 카멜 케이스를 사용합니다.
- 예시) val mutableHashSet<String> = HashSet()
- 이넘(Enum) → 대문자로 이루어진 스네이크 케이스(스크리밍 스네이크 케이스) 를 사용합니다.
- 예시) Color.RED, Color.GREEN
백킹 프로퍼티*
- 클래스에 동일한 개념을 가진 프로퍼티 2개가 있지만, 사용자에게 공개되는 프로퍼티와 실제 구현을 위한 프로퍼티로 나뉠 수 있습니다.
- 이런 경우, 실제 구현을 위한 프로퍼티를 백킹 프로퍼티라고 합니다.
- 백킹 프로퍼티는 맨 앞에 언더스코어(_)를 사용합니다.
class C {
private val _elementList = mutableListOf<Element>() // 백킹 프로퍼티
val elementList: List<Element>
get() = _elementList
}
제대로 된 이름 짓기
- 클래스는 이 클래스가 무엇인지 설명하기 위해 명사로 작성합니다.
- 예시) Printer, List, Map
- 메소드는 수행하는 작업을 설명하기 이해 동사로 작성합니다.
- 예시) printMessage(), add(), set()
포매팅
들여쓰기
- 4개의 스페이스를 사용합니다. 탭을 사용하지 마세요
- 중괄호의 경우 자바의 중괄호 스타일과 같습니다. 시작 중괄호는 중괄호가 열리는 코드 끝에 배치하고, 닫는 중괄호는 마지막 코드 다음 줄에 배치합니다.
if (elements != null) {
elements.map {
println(it)
}
}
- 세미콜론은 선택 사항입니다. 하지만 가급적 피하세요
가로 공백
// 주석을 시작할 때 공백을 넣습니다.
val sum = a + b// 이진 연산자 사이에 공백을 넣습니다.
val range = 0..10 // 범위 연산자 사이에는 공백을 넣지 않습니다.
for (i in range) { // 흐름 제어문의 시작에는 공백을 넣습니다.
doSometing()
}
// 주 생성자 선언 및 메서드 선언, 호출에는 공백을 넣지 않습니다.
class Student(val id: Int?) {
fun study(id: Int) {
println("$id 번 학생이 공부합니다.")
}
}
fun main() {
val student: Student = Student(100) // () 앞뒤와, [] 앞뒤에 공백을 넣지 않습니다.
println(Student::class) // 메소드 참조(::)에도 공백을 넣지 않습니다.
val map = HashMap<Int, String> { ... } // 타입 파라미터 정의 중 "<" 앞뒤와 ">" 앞뒤에 공백을 넣지 않습니다.
student.study() // . 또는 ?. 주변에도 공백을 넣지 않습니다.
}
콜론
- : 의 앞에 공백을 넣는 경우
- 조상 클래스를 상속 받을 때
- 생성자를 위임할 때
- 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) { /*...*/ } // 상속이 여러개일 때는 한 줄에 하나씩 수평 배치
키워드 혼합 순서
각각의 키워드를 혼합할 때는 다음 순서를 따르세요
public / protected / private / internal
expect / actual
final / open / abstract / sealed / const
external
override
lateinit
tailrec
vararg
suspend
inner
enum / annotation / fun
companion
inline / value
infix
operator
data
어노테이션
- 프로퍼티 위에 할당 할 어노테이션을 같은 들여쓰기로 작성합니다.
- 만약 한 필드에 어노테이션을 여러 개 할당한다면 같은 줄에 띄어쓰기로 구분합니다.
@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude
@JsonExclude @JvmField
var x: String
파일 어노테이션
- 파일 어노테이션은 해당 파일의 주석 뒤에 배치 되며 패키지와 한 줄의 공백을 주어 배치시킵니다.
/** 파일 주석 어쩌고 저쩌고 */
@file:JvmName("FooBar")
package foo.bar
메서드 선언
- 한 줄로 정리하기 어려운 함수 시그니처는 여러 줄로 나누어 작성합니다.
fun doSomething(
param1: String = "param",
param2: Int = 2
): ReturnType {
// 본문
}
- 단일 표현식은 fun foo() = 1 와 같이 한줄로 작성하는 것이 권장됩니다.
- 그러나 너무 한 줄에 너무 긴 내용을 가지고 있다면 = 아래에 한 줄을 추가하고 4칸의 들여쓰기를 하여 작성합니다.
fun foo() =
veryLongFunctionCallWithManyWords(andLongParametersToo(), x, y, z)
메서드 호출
메서드를 호출할 때 넘겨줘야 하는 파라미터가 많은 경우에는 여는 괄호( 뒤에 줄바꿈을 넣고 4칸을 들여쓰기 하여 작성합니다.
drawSquare(
x = 10, y = 10, // 관련 있는 항목을 묶음
width = 100, height = 100,
fill = true
)
메서드 체이닝을 사용할 때는 도트 . 를 기준으로 줄바꿈을 하여 작성합니다.
val anchor = owner
?.firstChild!!
.siblings(forward = true)
.dropWhile { it is PsiComment || it is PsiWhiteSpace }
프로퍼티
- 간단하게 표현 가능한 경우에는 한 줄로 프로퍼티를 표현합니다.
- 복잡한 프로퍼티의 경우 get, set을 각각 다른 줄에 배치합니다.
- 프로퍼티의 초기화를 할 때 초기화 값이 긴 경우, = 다음 줄에 값을 배치합니다.
val isEmpty = Boolean get() = size == 0
var foo: String
get() { /* ... */ }
set(value) {
foo = value
}
var defaultCharset = Charset? =
EncodingRegistry.getInstance().getDefaultCharsetForPropertiesFiles(file)
흐름제어문
- if 또는 when 문에서 조건이 여러 개일때, 서로 다른 줄에 배치하고 열고 닫는 중괄호를 바디 위/아래 줄에 각각 배치합니다.
if (!component.isSyncing &&
!hasAnyKotlinRuntimeInScope(module)
) { // 바디의 윗 줄
return createKotlinNotConfiguredPanel(module)
} // 바디의 아래 줄
- when 식에서 분기가 여러 개로 나뉘고, 분기 별로 본문의 코드가 길어질 때는 줄바꿈 및 중괄호{} 를 사용하여 표현합니다.
- when 식에서 분기가 여러 개로 나뉘지만, 분기 별 본문 코드가 짧을 때는 한 줄에 표현합니다.
when (color) {
Color.RED -> {
// 겁나 긴 글
}
Color.BLUE -> {
// 겁나 긴 글
}
}
// ----------------------------------------------------
when (boolFlag) {
true -> 겁나 짧은 글
false -> 겁나 짧은 글
}
람다
- 람다의 인자가 하나라면 중괄호{} 시작과 끝 사이에 공백이 있어야 합니다.
- 인자가 하나인 경우에는 element → 와 같이 화살표를 사용하기 보다는 it 키워드 사용을 권장합니다.
- 하지만 람다 안에 다른 람다를 호출하는 경우 등에서는 화살표로 명시하는 것이 가독성에 좋습니다.
- 만약 람다의 매개변수가 2개 이상이라면, 매개변수와 본문을 화살표 → 의 각 앞 뒤에 공백이 있어야 합니다.
- 다중 매개변수를 받는 람다의 본문이 한 줄 이상일 때는 화살표 → 를 기준으로 줄 바꿈을 합니다.
- 각 매개변수의 이름이 너무 길다면 화살표를 따로 배치합니다.
- 람다에 여러 개의 레이블(@)이 지정된 리턴문을 할당하지 마세요.
- 가능하다면 람다가 하나의 리턴을 가질 수 있게끔 수정하고 불가능 한 경우에는 람다를 익명 객체로 사용하는 것을 고려하세요
- 람다의 마지막 문에 레이블이 지정된 리턴을 사용하지 마세요
list.filter { it %2 == 0 }
list.mapIndexed { index, char -> println("$index -> $char") }
list.mapIndexed { index, char ->
// 1줄
// 2줄
// ..
// n줄
}
후행 쉼표
- 후행 쉼표란 어떤 요소 중 가장 마지막에 위치하는 요소 뒤에 붙은 쉼표를 뜻합니다.
- 후행 쉼표는 필수는 아니지만 코틀린 스타일 가이드의 권장 사항이며, 선택 사항입니다.
- 이넘, 값 매개변수, 클래스 프로퍼티 및 파라미터, 함수 파라미터, 인덱스 요소, 람다의 파라미터, when 구문 등 다양한 곳에서 후행 쉼표를 사용할 수 있습니다.
class Student(
private val id: Int,
private val room: Int,
private val name: String, // 후행 쉼표
) {
문서 설명 (/** */)
- 다큐멘테이션을 위한 주석으로 /** */ 를 사용합니다.
- 이 주석 사이에 새 줄이 생기게 되면 * 과 함께 공백 1칸을 배치합니다.
- 일반적으로 @param, @return 등의 태그는 본문 설명에 맞지 않는 긴 설명이 필요할 때만 작성하세요. 그 외에는 이런 태그를 사용하지 않는 걸 추천하며, 직접 매개변수와 리턴값에 대한 설명을 주석에 포함시키고, 매개변수가 언급될 때 마다 링크를 추가하세요.
/**
* 문서 설명의 첫 줄입니다.
* 여러 줄로 설명하고 있습니다.
*/
/** 한 줄로 문서 설명을 끝낼 수 있다면 이렇게 작성하세요 */
// 예시
/**
* 주어진 Int형 매개변수 [number]의 절대 값을 리턴합니다.
*/
fun abs(number: Int): Int { /*...*/ }
중복 구조 피하기
- 코틀린의 특정 구문 구조는 선택 사항인 경우가 많습니다.(예시 : 메서드의 반환 타입이 Unit인 경우 작성해도 되고 안해도 됩니다)
- 명확성을 위해서 불필요한 구조는 코드에 남겨두지 마세요
- 라이브러리 개발 시에는 얘기가 달라집니다. 자세한 내용은 아래에서 확인하세요!
문자열 템플릿
- 문자열 템플릿에서 student.id 등 도트 연산을 사용하는 템플릿 및 템플릿 뒤에 공백을 넣고 싶지 않은 경우에만 중괄호{} 사용을 권합니다.
println("${student.id}번 학생의 이름은 $name 입니다.") // 도트 연산을 사용하는 경우 중괄호 사용
println("${student.id}번 학생의 이름은 ${name}입니다.") // $name 뒤에 공백이 없게 사용하고 싶음
언어 기능의 관용적 사용
var 대신 val을 사용 권장
- 로컬 변수 및 프로퍼티가 초기화 된 후 값 변경이 필요하지 않다면 무조건 val을 사용하세요
컬렉션은 불변을 먼저 고려
- 변경되지 않는 컬렉션의 선언은 불변 컬렉션을 사용하세요.
- 팩토리 메서드를 통해 컬렉션의 인스턴스를 반환하는 경우 가능하다면 항상 불변 컬렉션을 리턴하세요
// Bad: HashSet은 변경 가능한 컬렉션
fun validateValue(actualValue: String, allowedValues: HashSet<String>) { ... }
// Good: Set은 변경 불가능한 컬렉션
fun validateValue(actualValue: String, allowedValues: Set<String>) { ... }
// Bad: arrayListOf()는 ArrayList<T>를 리턴하는데, 이는 변경 가능한 컬렉션
val allowedValues = arrayListOf("a", "b", "c")
// Good: listOf()는 List<T>를 리턴하고, 이는 변경 불가능한 컬렉션
val allowedValues = listOf("a", "b", "c")
파라미터 디폴트 값
- 파라미터의 디폴트 값을 명시하여 오버로드 하게 되는 일을 막으세요
// Bad
fun foo() = foo("a")
fun foo(a: String) { /*...*/ }
// Good
fun foo(a: String = "a") { /*...*/ }
타입 별칭
- 코드에서 여러 번 사용되는 함수형 타입 또는 매개변수가 확실한 유형이 있는 경우, 별칭(*Alias)를 사용하세요
- 이런 별칭을 다른 파일에서 임포트하여 가져올 때 이름의 충돌이 생길 수 있기 때문에 이를 피하기 위해서는 import .. as ... 을 사용합니다.
typealias MouseClickHandler = (Any, MouseEvent) -> Unit
typealias PersonIndex = Map<String, Person>
명명된 인자
다음과 같은 경우에는 인자의 이름을 명시하세요.
- 여러 개의 매개변수가 모두 기본 자료형에 속하는 경우
- Boolean 타입의 매개변수가 있는 경우
// 여러 개의 매개변수가 모두 기본 자료형에 속하는 경우
drawSquare(
x = 10, y = 10,
width = 100, height = 100,
fill = true
)
giveTrueOrFalseFlag(flag = true) // Boolean 타입의 매개변수
조건문
- try / if / when 식을 사용하는 것을 선호합니다.
return if (x) foo() else bar()
return when(x) {
0 -> "zero"
else -> "nonzero"
}
if vs when
- 조건이 두개라면 if 를 사용하는 것이 권장됩니다.
- 조건이 3개 이상일 때 부터 when을 사용하는 것을 권장합니다.
if (x == null) ... else ... // 조건이 2개
when (x) { // 조건이 3개
null -> // ...
1 -> // ...
else -> // ...
}
조건에서 Boolean? 타입을 사용하는 경우
- if (Boolean?) 과 조건식에 Boolean? 타입을 사용하는 경우에는 미리 Boolean 타입이 null이 아닌지 검사합니다.
- 예 : if (value == true) 또는 if (value == false)
루프
- 루프보다 filter, map 등 고차 함수를 사용하는 것이 권장됩니다.
- forEach는 그 안의 요소가 null 일 수 있는 타입이 들어있거나, 더 긴 체인 메서드로 연결되는 경우에 사용하는 것을 권장하며, 그 외에는 일반적인 for 루프 사용을 권장합니다.
- 그 외의 모든 경우에서 일반적인 루프 또는 고차 함수 중 어떤 것을 사용할 지 고민할 때는 수행 작업 시간 또는 어느 것이 더 최적화된 성능일지를 고려하세요.
문자열
- 문자열 연결 (”” + “”)보다 문자열 템플릿($)을 사용하는 것을 권장합니다.
- 여러 줄을 표현할 때는 \\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 프로퍼티
- 인자가 없는 함수는 읽기 전용 프로퍼티와 상호 교환이 가능합니다. 둘의 의미는 비슷하지만 어느 쪽을 선호해야 할까요?
- 기본 알고리즘이 다음과 같은 경우 함수보다 프로퍼티를 선호합니다.
- throw를 사용하지 않는 경우
- 계산 비용이 적거나 첫 번째 실행에서 캐시되는 경우
- 변경되지 않는 객체를 사용하여 호출에 항상 동일한 객체를 리턴하는 경우
infix 메서드 사용
- 유사한 역할을 수행하는 두 객체에서 동작할 때만 infix로 선언합니다.
- 수신 객체의 값을 변경 시키는 경우에는 infix로 선언하지 않습니다.
팩토리 메서드
- 팩토리 메서드를 선언하는 경우, 클래스 이름과 같은 이름을 사용하지 마세요.
- 팩토리 메서드의 동작의 이유가 명확해지는 이름을 사용하세요.
- 아무런 의미가 없는 경우에만 클래스 이름과 동일한 이름을 사용합니다.
- 슈퍼 클래스 생성자에게 위임하지 않는 경우, 오버로드 된 생성자를 팩토리 메서드로 대체하는 것이 좋습니다.
라이브러리용 코딩컨벤션
- 라이브러리를 작성할 때는 API 안정성을 위해 다음과 같은 추가 규칙을 따르는 것이 좋습니다.
- 멤버의 가시성을 항상 명시적으로 지정하세요 (실수로 노출되는 것을 방지하기 위해)
- 함수의 리턴 타입과 프포러티 타입을 명시적으로 지정하세요(구현이 변경될 때 실수로 반환 유형이 변경되는 것을 방지하기 위해)
- 라이브러리 문서 제공을 위해 오버라이드를 제외한 라이브러리의 공개 멤버에 KDoc 주석을 작성합니다.
마치며
- 코틀린 코드를 작성할 때 나열된 모든 항목을 하나하나 염두해가면서 코딩할 필요는 없습니다.(IDE가 많이 도와주기도 하고, 계속 코딩하다보면 자연스럽게 가독성 좋은 코드를 쓰게 됩니다)
- 코딩 컨벤션을 이해하고 본인의 코드에 잘 적용하낟면 다른 이들에게 더 나은 가독성을 제공해줄 수 있으며, 원활한 코드리뷰, 그 외 협업에 많은 이점이 됩니다.
- 따라서 코딩 컨벤션을 지키기 위한 꾸준한 노력을 하되, 외우려고 하지는 마세요!
'Kotlin & Java' 카테고리의 다른 글
들어가기에 앞서
💡 아주 기초가 되는 내용들은 소거했습니다. 이 점 참고바랍니다.
기초 설정
코틀린은 JetBrains에서 개발된 언어이기 때문에 IntelliJ와 Android Studio 등 IDEA에서 코딩 스타일에 대해 강력한 지원을 해주고 있습니다. 이러한 지원을 제대로 받기 위해서는 다음과 같은 설정이 필요합니다.
💡 설정은 IntelliJ를 기준으로 합니다.
1. 스타일 가이드 적용하기
- Settings/Preferences > Editor > Code Style > Kotlin 선택
- Set from…. 클릭
- Kotlin style guide 선택
2. 코드가 스타일을 따르는 지 검사 받기
- Settings/Preferences > Editor > Inspections > General 선택
- Incorrecting formatting 항목을 ✔️ 표시로 변경
소스코드에 대한 구성
디렉토리 구조
코틀린만 사용되는 구조일 때
- 코틀린만 사용되는 프로젝트의 디렉토리 구조는 공통 루트 패키지인 (src/main/kotlin)을 생략한 나머지 패키지의 구조를 따릅니다. 아래 패키지 구조에서 나머지 패키지는 com/example/dirStructure 를 말하고 있네요
└─src
└─ main
└─ kotlin <- 여기까지 공통 루트 패키지!
└─ com <- 여기부터 나머지 패키지 구조!
└─ example
└─ dirStructure
- 이 때, 프로젝트 내부의 모든 파일이 com/example/dirStrucrue 아래에서만 정의되고 있다면 여러분이 새로운 파일을 만들 때는 (com/example/dirStructure) 아래에만 생성해야 한다는 뜻입니다.
💡 연습을 한 번 해봅시다. dirStructure 상위에는 패키지 외에 어떠한 파일도 존재하지 않는다고 가정해보죠.
- network/socket 패키지가 추가된다면 어떤 패키지 아래에 정의를 해야 할까요?
- 추가로 MySocket.kt 파일을 만든다면 어디에 만들어야 할까요?
...
└─ dirStructure
└─ network
└─ socket
└─ MySocket.kt
- 정답은 간단합니다. dirStructure 상위 디렉토리에는 어떠한 파일도 생성되어있지 않고 있으니 dirStructure 아래로 패키지를 생성하고 이곳에 MySocket.kt 파일을 만들어 코드를 작성하면 됩니다.
- 자바와 함께 사용되는 구조일 때
- 자바와 함께 사용되는 구조일 때는 자바의 패키지 구조에 맞춰야 합니다. 자바 파일(.java) 과 같은 위치에 코틀린 파일이 위치해야하고, 디렉토리 구조도 자바의 구조를 따릅니다.
💡 왜 코틀린은 자바의 패키지 구조를 따라가나요?
- 이는 자바의 파일은(.java) 최상단에 패키지가 명시 되어야 하는데, 파일이 패키지와 같은 디렉토리에 위치하지 않는다면 에러가 나기 때문입니다.
// MySocket의 실제 위치는 com.example.javadir.network.socket 이지만
// 패키지를 com.example.javadir 같이 선언하면 다음과 같이 에러가 발생합니다.
// Package name 'com.example.javadir' does not correspond to the file path 'com.example.javadir.network.socket'
package com.example.javadir
public class MySocket {
...
}
- 반대로 코틀린의 파일은 패키지 선언의 위치가 실제 디렉토리와 일치하지 않아도 에러가 발생하지 않습니다.
- 따라서 에러가 나지 않는 코틀린이 자바와의 상호운용을 위해 자바를 맞춰주는 거라고 생각하시면 됩니다.
소스 파일 이름
- 코틀린의 확장자 : .kt
- 파일 내부에 단 하나의 인터페이스, 클래스만 존재할 때
- 해당 인터페이스, 클래스 명과 같은 이름으로 파일을 생성해야 합니다. 이는 모든 유형의 인터페이스, 클래스에 적용됩니다.
- 예를 들어, MyClass 라는 하나의 클래스만 파일 내에 존재한다면 파일명도 MyClass.kt라는 이름이 되어야 합니다.
파일 내부에 여러 개의 클래스가 존재하거나 최상위(Top-Level) 프로퍼티, 함수만 존재할 때
- 여러 개의 항목들을 공통적으로 설명할 수 있는 이름을 파일명으로 사용합니다.
// Console.kt
interface Input {
fun read(input: String?)
}
interface Output {
fun print(message: String?)
}
class Reader : Input {
private var inputValue = ""
override fun read(input: String?) {
input?.let { inputValue = it }
}
}
class Printer : Output {
override fun print(message: String?) {
message?.let { println(it) }
}
}
- 위 코드처럼 Input, Output, Reader, Printer 등 콘솔 입출력에 관련된 클래스 및 인터페이스가 한 파일에 동시에 존재할 때는 이를 모두 아우르는 Console.kt라는 이름으로 정의할 수 있겠습니다.
소스 파일 정리
소스 파일 정리 : 이 코드를 어디에 포함시킬지에 대한 내용을 다룹니다.
- 위에서 보았듯이, 코틀린은 한 파일 내에 독립된 클래스를 여러개 선언할 수 있습니다.
- 각 요소의 관계가 밀접한 경우에는 이런식으로 한 파일에 여러 클래스를 작성하는 것을 권장합니다.
- 각 클래스의 내용이 너무 길어지면 따로 나누는 것이 더 낫습니다.
- 각 요소 간 밀접한 관계가 없는 경우 클래스 별로 파일을 만들어야 합니다.
- 각 요소의 관계가 밀접한 경우에는 이런식으로 한 파일에 여러 클래스를 작성하는 것을 권장합니다.
- 확장 함수는 2가지로 나누어 정리할 수 있습니다.
- 모든 클라이언트가 확장 함수를 사용하는 경우
- 클래스 내부에 확장 함수를 위치시킵니다.
- 특정 클라이언트에서만 사용하는 경우
- 해당 클라이언트 코드에 함께 위치시킵니다.
- 모든 클라이언트가 확장 함수를 사용하는 경우
클래스 내부 레이아웃
클래스의 각 요소는 다음 순서로 배치시킵니다.
- 프로퍼티 선언 및 초기화 블록
- 부 생성자
- 메소드 선언
- 메소드의 선언은 알파벳순으로 정렬하는게 아닙니다. 또한 확장 함수를 분리하지 마세요.
- 클래스를 위에서 살펴보면서 메서드를 봤을 때 서로 관련있는 순서로 논리적 흐름에 맞게 나열하세요.
- 동반 객체
- 총 예시
class StudentRegisterResponse(
private val id: Int,
private val room: Int,
private val name: String,
) {
// 1. 프로퍼티 선언 및 초기화 블록
private var nickname: String = ""
init {
println("학생 가입 결과")
// ..
}
// 2. 부 생성자
constructor(id: Int, room: Int, name: String, nickname: String) : this(id, room, name) {
this.nickname = makeNickNameWithRealName(nickname)
}
// 3. 메소드 선언
private fun makeNickNameWithRealName(nickname: String) = "$nickname : $name"
// 중첩 내부 클래스
class NestedInternalClass {
/*...*/
}
// 4. 동반 객체
companion object {
fun from(student: Student): StudentRegisterResponse {
return StudentRegisterResponse(
id = student.name,
room = student.room,
name = student.name,
nickname = student.nickname
)
}
}
// 중첩 외부 클래스
class NestedExternalClass {
/*...*/
}
}
인터페이스 구현
인터페이스의 구현은 인터페이스에 선언된 순서로 작성을 해야합니다.
오버로딩 레이아웃
오버로딩된 항목들은 항상 나란히 배치해주세요
네이밍 컨벤션
각 케이스 정리
- 카멜 케이스 : camelCase
- 파스칼 케이스 또는 어퍼 카멜 케이스 : PascalCase, UpperCamelCase
- 스네이크 케이스 : snake_case
패키지
되도록 한 단어로 패키지 명을 작성해주시고, 단어 간 연결을 사용해야 한다면 모든 문자를 카멜케이스로 작성합니다.
- 예시) dirStructure
클래스
파스칼 케이스 또는 어퍼 카멜 케이스를 사용합니다.
- 예시) MyAweSomeClass
메서드
카멜케이스를 사용합니다. 밑줄을 사용하지 않습니다.
팩토리 메서드
클래스의 인스턴스를 반환하는 팩토리 메서드는 클래스의 이름을 그대로 사용해도 됩니다.
interface Foo { .. }
class FooImpl: Foo {
fun Foo(): Foo {
return FooImpl()
}
}
테스트 메서드
테스트 메서드에서만 백틱```을 사용하여 메서드 이름을 만들 수 있습니다.
class StudentCreationTest {
@Test
fun `학생이 생성된다`() {
..
}
}
프로퍼티
- 일반 프로퍼티 → 카멜 케이스를 사용합니다.
- 예시) val userName: String = “홍길동”
- 상수(const), 최상위(Top-Level) 프로퍼티 → 대문자로 이루어진 스네이크 케이스(스크리밍 스네이크 케이스)를 사용합니다. 예시
- 예시) const val PI = 3.14
- 동작 또는 변경 가능한(mutable) 최상위 프로퍼티 → 카멜 케이스를 사용합니다.
- 예시) val mutableHashSet<String> = HashSet()
- 이넘(Enum) → 대문자로 이루어진 스네이크 케이스(스크리밍 스네이크 케이스) 를 사용합니다.
- 예시) Color.RED, Color.GREEN
백킹 프로퍼티*
- 클래스에 동일한 개념을 가진 프로퍼티 2개가 있지만, 사용자에게 공개되는 프로퍼티와 실제 구현을 위한 프로퍼티로 나뉠 수 있습니다.
- 이런 경우, 실제 구현을 위한 프로퍼티를 백킹 프로퍼티라고 합니다.
- 백킹 프로퍼티는 맨 앞에 언더스코어(_)를 사용합니다.
class C {
private val _elementList = mutableListOf<Element>() // 백킹 프로퍼티
val elementList: List<Element>
get() = _elementList
}
제대로 된 이름 짓기
- 클래스는 이 클래스가 무엇인지 설명하기 위해 명사로 작성합니다.
- 예시) Printer, List, Map
- 메소드는 수행하는 작업을 설명하기 이해 동사로 작성합니다.
- 예시) printMessage(), add(), set()
포매팅
들여쓰기
- 4개의 스페이스를 사용합니다. 탭을 사용하지 마세요
- 중괄호의 경우 자바의 중괄호 스타일과 같습니다. 시작 중괄호는 중괄호가 열리는 코드 끝에 배치하고, 닫는 중괄호는 마지막 코드 다음 줄에 배치합니다.
if (elements != null) {
elements.map {
println(it)
}
}
- 세미콜론은 선택 사항입니다. 하지만 가급적 피하세요
가로 공백
// 주석을 시작할 때 공백을 넣습니다.
val sum = a + b// 이진 연산자 사이에 공백을 넣습니다.
val range = 0..10 // 범위 연산자 사이에는 공백을 넣지 않습니다.
for (i in range) { // 흐름 제어문의 시작에는 공백을 넣습니다.
doSometing()
}
// 주 생성자 선언 및 메서드 선언, 호출에는 공백을 넣지 않습니다.
class Student(val id: Int?) {
fun study(id: Int) {
println("$id 번 학생이 공부합니다.")
}
}
fun main() {
val student: Student = Student(100) // () 앞뒤와, [] 앞뒤에 공백을 넣지 않습니다.
println(Student::class) // 메소드 참조(::)에도 공백을 넣지 않습니다.
val map = HashMap<Int, String> { ... } // 타입 파라미터 정의 중 "<" 앞뒤와 ">" 앞뒤에 공백을 넣지 않습니다.
student.study() // . 또는 ?. 주변에도 공백을 넣지 않습니다.
}
콜론
- : 의 앞에 공백을 넣는 경우
- 조상 클래스를 상속 받을 때
- 생성자를 위임할 때
- 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) { /*...*/ } // 상속이 여러개일 때는 한 줄에 하나씩 수평 배치
키워드 혼합 순서
각각의 키워드를 혼합할 때는 다음 순서를 따르세요
public / protected / private / internal
expect / actual
final / open / abstract / sealed / const
external
override
lateinit
tailrec
vararg
suspend
inner
enum / annotation / fun
companion
inline / value
infix
operator
data
어노테이션
- 프로퍼티 위에 할당 할 어노테이션을 같은 들여쓰기로 작성합니다.
- 만약 한 필드에 어노테이션을 여러 개 할당한다면 같은 줄에 띄어쓰기로 구분합니다.
@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude
@JsonExclude @JvmField
var x: String
파일 어노테이션
- 파일 어노테이션은 해당 파일의 주석 뒤에 배치 되며 패키지와 한 줄의 공백을 주어 배치시킵니다.
/** 파일 주석 어쩌고 저쩌고 */
@file:JvmName("FooBar")
package foo.bar
메서드 선언
- 한 줄로 정리하기 어려운 함수 시그니처는 여러 줄로 나누어 작성합니다.
fun doSomething(
param1: String = "param",
param2: Int = 2
): ReturnType {
// 본문
}
- 단일 표현식은 fun foo() = 1 와 같이 한줄로 작성하는 것이 권장됩니다.
- 그러나 너무 한 줄에 너무 긴 내용을 가지고 있다면 = 아래에 한 줄을 추가하고 4칸의 들여쓰기를 하여 작성합니다.
fun foo() =
veryLongFunctionCallWithManyWords(andLongParametersToo(), x, y, z)
메서드 호출
메서드를 호출할 때 넘겨줘야 하는 파라미터가 많은 경우에는 여는 괄호( 뒤에 줄바꿈을 넣고 4칸을 들여쓰기 하여 작성합니다.
drawSquare(
x = 10, y = 10, // 관련 있는 항목을 묶음
width = 100, height = 100,
fill = true
)
메서드 체이닝을 사용할 때는 도트 . 를 기준으로 줄바꿈을 하여 작성합니다.
val anchor = owner
?.firstChild!!
.siblings(forward = true)
.dropWhile { it is PsiComment || it is PsiWhiteSpace }
프로퍼티
- 간단하게 표현 가능한 경우에는 한 줄로 프로퍼티를 표현합니다.
- 복잡한 프로퍼티의 경우 get, set을 각각 다른 줄에 배치합니다.
- 프로퍼티의 초기화를 할 때 초기화 값이 긴 경우, = 다음 줄에 값을 배치합니다.
val isEmpty = Boolean get() = size == 0
var foo: String
get() { /* ... */ }
set(value) {
foo = value
}
var defaultCharset = Charset? =
EncodingRegistry.getInstance().getDefaultCharsetForPropertiesFiles(file)
흐름제어문
- if 또는 when 문에서 조건이 여러 개일때, 서로 다른 줄에 배치하고 열고 닫는 중괄호를 바디 위/아래 줄에 각각 배치합니다.
if (!component.isSyncing &&
!hasAnyKotlinRuntimeInScope(module)
) { // 바디의 윗 줄
return createKotlinNotConfiguredPanel(module)
} // 바디의 아래 줄
- when 식에서 분기가 여러 개로 나뉘고, 분기 별로 본문의 코드가 길어질 때는 줄바꿈 및 중괄호{} 를 사용하여 표현합니다.
- when 식에서 분기가 여러 개로 나뉘지만, 분기 별 본문 코드가 짧을 때는 한 줄에 표현합니다.
when (color) {
Color.RED -> {
// 겁나 긴 글
}
Color.BLUE -> {
// 겁나 긴 글
}
}
// ----------------------------------------------------
when (boolFlag) {
true -> 겁나 짧은 글
false -> 겁나 짧은 글
}
람다
- 람다의 인자가 하나라면 중괄호{} 시작과 끝 사이에 공백이 있어야 합니다.
- 인자가 하나인 경우에는 element → 와 같이 화살표를 사용하기 보다는 it 키워드 사용을 권장합니다.
- 하지만 람다 안에 다른 람다를 호출하는 경우 등에서는 화살표로 명시하는 것이 가독성에 좋습니다.
- 만약 람다의 매개변수가 2개 이상이라면, 매개변수와 본문을 화살표 → 의 각 앞 뒤에 공백이 있어야 합니다.
- 다중 매개변수를 받는 람다의 본문이 한 줄 이상일 때는 화살표 → 를 기준으로 줄 바꿈을 합니다.
- 각 매개변수의 이름이 너무 길다면 화살표를 따로 배치합니다.
- 람다에 여러 개의 레이블(@)이 지정된 리턴문을 할당하지 마세요.
- 가능하다면 람다가 하나의 리턴을 가질 수 있게끔 수정하고 불가능 한 경우에는 람다를 익명 객체로 사용하는 것을 고려하세요
- 람다의 마지막 문에 레이블이 지정된 리턴을 사용하지 마세요
list.filter { it %2 == 0 }
list.mapIndexed { index, char -> println("$index -> $char") }
list.mapIndexed { index, char ->
// 1줄
// 2줄
// ..
// n줄
}
후행 쉼표
- 후행 쉼표란 어떤 요소 중 가장 마지막에 위치하는 요소 뒤에 붙은 쉼표를 뜻합니다.
- 후행 쉼표는 필수는 아니지만 코틀린 스타일 가이드의 권장 사항이며, 선택 사항입니다.
- 이넘, 값 매개변수, 클래스 프로퍼티 및 파라미터, 함수 파라미터, 인덱스 요소, 람다의 파라미터, when 구문 등 다양한 곳에서 후행 쉼표를 사용할 수 있습니다.
class Student(
private val id: Int,
private val room: Int,
private val name: String, // 후행 쉼표
) {
문서 설명 (/** */)
- 다큐멘테이션을 위한 주석으로 /** */ 를 사용합니다.
- 이 주석 사이에 새 줄이 생기게 되면 * 과 함께 공백 1칸을 배치합니다.
- 일반적으로 @param, @return 등의 태그는 본문 설명에 맞지 않는 긴 설명이 필요할 때만 작성하세요. 그 외에는 이런 태그를 사용하지 않는 걸 추천하며, 직접 매개변수와 리턴값에 대한 설명을 주석에 포함시키고, 매개변수가 언급될 때 마다 링크를 추가하세요.
/**
* 문서 설명의 첫 줄입니다.
* 여러 줄로 설명하고 있습니다.
*/
/** 한 줄로 문서 설명을 끝낼 수 있다면 이렇게 작성하세요 */
// 예시
/**
* 주어진 Int형 매개변수 [number]의 절대 값을 리턴합니다.
*/
fun abs(number: Int): Int { /*...*/ }
중복 구조 피하기
- 코틀린의 특정 구문 구조는 선택 사항인 경우가 많습니다.(예시 : 메서드의 반환 타입이 Unit인 경우 작성해도 되고 안해도 됩니다)
- 명확성을 위해서 불필요한 구조는 코드에 남겨두지 마세요
- 라이브러리 개발 시에는 얘기가 달라집니다. 자세한 내용은 아래에서 확인하세요!
문자열 템플릿
- 문자열 템플릿에서 student.id 등 도트 연산을 사용하는 템플릿 및 템플릿 뒤에 공백을 넣고 싶지 않은 경우에만 중괄호{} 사용을 권합니다.
println("${student.id}번 학생의 이름은 $name 입니다.") // 도트 연산을 사용하는 경우 중괄호 사용
println("${student.id}번 학생의 이름은 ${name}입니다.") // $name 뒤에 공백이 없게 사용하고 싶음
언어 기능의 관용적 사용
var 대신 val을 사용 권장
- 로컬 변수 및 프로퍼티가 초기화 된 후 값 변경이 필요하지 않다면 무조건 val을 사용하세요
컬렉션은 불변을 먼저 고려
- 변경되지 않는 컬렉션의 선언은 불변 컬렉션을 사용하세요.
- 팩토리 메서드를 통해 컬렉션의 인스턴스를 반환하는 경우 가능하다면 항상 불변 컬렉션을 리턴하세요
// Bad: HashSet은 변경 가능한 컬렉션
fun validateValue(actualValue: String, allowedValues: HashSet<String>) { ... }
// Good: Set은 변경 불가능한 컬렉션
fun validateValue(actualValue: String, allowedValues: Set<String>) { ... }
// Bad: arrayListOf()는 ArrayList<T>를 리턴하는데, 이는 변경 가능한 컬렉션
val allowedValues = arrayListOf("a", "b", "c")
// Good: listOf()는 List<T>를 리턴하고, 이는 변경 불가능한 컬렉션
val allowedValues = listOf("a", "b", "c")
파라미터 디폴트 값
- 파라미터의 디폴트 값을 명시하여 오버로드 하게 되는 일을 막으세요
// Bad
fun foo() = foo("a")
fun foo(a: String) { /*...*/ }
// Good
fun foo(a: String = "a") { /*...*/ }
타입 별칭
- 코드에서 여러 번 사용되는 함수형 타입 또는 매개변수가 확실한 유형이 있는 경우, 별칭(*Alias)를 사용하세요
- 이런 별칭을 다른 파일에서 임포트하여 가져올 때 이름의 충돌이 생길 수 있기 때문에 이를 피하기 위해서는 import .. as ... 을 사용합니다.
typealias MouseClickHandler = (Any, MouseEvent) -> Unit
typealias PersonIndex = Map<String, Person>
명명된 인자
다음과 같은 경우에는 인자의 이름을 명시하세요.
- 여러 개의 매개변수가 모두 기본 자료형에 속하는 경우
- Boolean 타입의 매개변수가 있는 경우
// 여러 개의 매개변수가 모두 기본 자료형에 속하는 경우
drawSquare(
x = 10, y = 10,
width = 100, height = 100,
fill = true
)
giveTrueOrFalseFlag(flag = true) // Boolean 타입의 매개변수
조건문
- try / if / when 식을 사용하는 것을 선호합니다.
return if (x) foo() else bar()
return when(x) {
0 -> "zero"
else -> "nonzero"
}
if vs when
- 조건이 두개라면 if 를 사용하는 것이 권장됩니다.
- 조건이 3개 이상일 때 부터 when을 사용하는 것을 권장합니다.
if (x == null) ... else ... // 조건이 2개
when (x) { // 조건이 3개
null -> // ...
1 -> // ...
else -> // ...
}
조건에서 Boolean? 타입을 사용하는 경우
- if (Boolean?) 과 조건식에 Boolean? 타입을 사용하는 경우에는 미리 Boolean 타입이 null이 아닌지 검사합니다.
- 예 : if (value == true) 또는 if (value == false)
루프
- 루프보다 filter, map 등 고차 함수를 사용하는 것이 권장됩니다.
- forEach는 그 안의 요소가 null 일 수 있는 타입이 들어있거나, 더 긴 체인 메서드로 연결되는 경우에 사용하는 것을 권장하며, 그 외에는 일반적인 for 루프 사용을 권장합니다.
- 그 외의 모든 경우에서 일반적인 루프 또는 고차 함수 중 어떤 것을 사용할 지 고민할 때는 수행 작업 시간 또는 어느 것이 더 최적화된 성능일지를 고려하세요.
문자열
- 문자열 연결 (”” + “”)보다 문자열 템플릿($)을 사용하는 것을 권장합니다.
- 여러 줄을 표현할 때는 \\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 프로퍼티
- 인자가 없는 함수는 읽기 전용 프로퍼티와 상호 교환이 가능합니다. 둘의 의미는 비슷하지만 어느 쪽을 선호해야 할까요?
- 기본 알고리즘이 다음과 같은 경우 함수보다 프로퍼티를 선호합니다.
- throw를 사용하지 않는 경우
- 계산 비용이 적거나 첫 번째 실행에서 캐시되는 경우
- 변경되지 않는 객체를 사용하여 호출에 항상 동일한 객체를 리턴하는 경우
infix 메서드 사용
- 유사한 역할을 수행하는 두 객체에서 동작할 때만 infix로 선언합니다.
- 수신 객체의 값을 변경 시키는 경우에는 infix로 선언하지 않습니다.
팩토리 메서드
- 팩토리 메서드를 선언하는 경우, 클래스 이름과 같은 이름을 사용하지 마세요.
- 팩토리 메서드의 동작의 이유가 명확해지는 이름을 사용하세요.
- 아무런 의미가 없는 경우에만 클래스 이름과 동일한 이름을 사용합니다.
- 슈퍼 클래스 생성자에게 위임하지 않는 경우, 오버로드 된 생성자를 팩토리 메서드로 대체하는 것이 좋습니다.
라이브러리용 코딩컨벤션
- 라이브러리를 작성할 때는 API 안정성을 위해 다음과 같은 추가 규칙을 따르는 것이 좋습니다.
- 멤버의 가시성을 항상 명시적으로 지정하세요 (실수로 노출되는 것을 방지하기 위해)
- 함수의 리턴 타입과 프포러티 타입을 명시적으로 지정하세요(구현이 변경될 때 실수로 반환 유형이 변경되는 것을 방지하기 위해)
- 라이브러리 문서 제공을 위해 오버라이드를 제외한 라이브러리의 공개 멤버에 KDoc 주석을 작성합니다.
마치며
- 코틀린 코드를 작성할 때 나열된 모든 항목을 하나하나 염두해가면서 코딩할 필요는 없습니다.(IDE가 많이 도와주기도 하고, 계속 코딩하다보면 자연스럽게 가독성 좋은 코드를 쓰게 됩니다)
- 코딩 컨벤션을 이해하고 본인의 코드에 잘 적용하낟면 다른 이들에게 더 나은 가독성을 제공해줄 수 있으며, 원활한 코드리뷰, 그 외 협업에 많은 이점이 됩니다.
- 따라서 코딩 컨벤션을 지키기 위한 꾸준한 노력을 하되, 외우려고 하지는 마세요!