코틀린, 이렇게 작성하시면 됩니다

MJ Studio
MJ Studio
Published in
27 min readOct 3, 2020

--

코딩 컨벤션의 개념과 코틀린의 코딩 컨벤션들(Coding conventions)에 대해

글을 읽기 전에

코틀린에 처음이시거나 문법을 아직 제대로 공부하지 않으셨다면 여러가지 참고자료들을 추천드리겠습니다.

한 악필의 블로거가 쓴 코틀린 정리글입니다. 별로 추천드리진 않습니다.

추천해 드리고 싶은 책입니다. 제가 코틀린을 처음 쓸 때 읽었으며 코틀린 1.4.x 버전이 출시되었지만 잘 쓴 글은 쉽게 가치를 잃지 않는 것 같습니다.

코틀린 정식 홈페이지의 튜토리얼입니다.

코딩 컨벤션이 뭔가요?

코틀린은 Jetbrain 사에서 개발한 모던한 프로그래밍 언어입니다. 최근 1.4.x 으로 업그레이드가 되었으며 계속해서 새로운 기능들과 직관적인 문법을 발전시킴으로써 사랑받는 언어입니다.

코틀린의 문법은 쉽고 무궁무진하지만, 특정 코드를 작성할 때 지키면 좋은 규칙들이 있다는 것을 아시나요? 어떤 언어든 이러한 규칙들은 존재합니다. 각 언어의 특성에 맞게 이런 규칙들은 언어마다 강할 수도 있고 조금 덜 규제되어있을 수도 있습니다.

이러한 규칙들을 코딩 컨벤션(Coding convention)이라고 합니다. 코딩 컨벤션은 파일 이름, 줄 바꿈 간격, 변수와 클래스 등 식별자의 이름(이름을 짓는 규칙을 따로 네이밍 컨벤션이라 부르기도 합니다.), 클래스 내에서의 변수들의 정렬법 등이 있을 수 있습니다.

또한 한 프로젝트 내에서 팀으로 일하는 개발자들끼리 특정한 도메인 개념에 대해서 이렇게 이름을 붙이자고 하는 것도 프로젝트 범위의 코딩 컨벤션이 되기도 합니다. 예를 들어 게임을 만들 때 한 유저를 Person이라 안 부르고 User 라고 부르기로 팀원들끼리 합의해두는 것입니다.

코딩 컨벤션 왜써야하나요?

그러면 코딩 컨벤션을 공부하고 그렇게 코드를 작성할 시에 우리가 얻을 수 있는 이점은 무엇일까요?

1. 일관된 코드 스타일을 유지할 수 있습니다.

val age = 24
val NameString: String = "MJ"

나이를 나타내는 정수형 상수 age와 이름을 나타내는 문자열 상수 NameString 이 있습니다. 여기서 두 상수는 사람에 대한 정보를 저장하는데 굉장히 생김새가 다릅니다. 이 짧은 두 줄짜리 코드에도 세 가지다른 점이 있습니다.

첫째로, 나이는 처음 문자가 a로 소문자로 시작하지만 이름은 N으로 대문자로 시작합니다. 이는 후에 설명하겠지만 코틀린에서는 변수/상수를 age처럼 선언하는 것이 camelCase 라고 부르는 규칙에 맞는 명명법입니다.

둘째로, age 에는 코틀린의 타입 추론(Type inference)을 이용해 24를 대입했기 때문에 상수가 자동으로 Int 형으로 선언되었지만 NameString 에서는 이를 : String 을 통해 명시적으로 선언하고 있습니다.

셋째로, NameString 은 상수명에도 String 을 포함하여 “이 상수는 문자열입니다" 라고 표현해주고 있지만 age 는 그렇지 않습니다. 보통 age 라고 쓰고 불필요한 타입명은 상수명에 포함시켜주지 않는 것이 좋습니다. 이는 다음과 같은 사이트에서 짧게 확인할 수 있는 유명한 저서 Clean Code에도 포함된 내용입니다.

자, 어떤 걸 느끼셨나요? 별문제 없다고 느끼셨을 수도 있습니다. 겨우 변수 두 개를 어떻게 선언하는 것이 프로젝트에 큰 영향을 끼치지 않는다는 것엔 저도 동의합니다. 그러나 이것이 변수명이 아닌 파일명이고 클래스 내의 변수들의 선언된 위치라든지에 관한 것이라면 어떨까요? 파일이 수만 개가 되는 프로젝트를 상상해보세요. 질서 없이 만들어진 코드와 프로젝트는 마치 단어들이 정렬되지 않은 1,800장짜리 영어사전을 보는 느낌일 것입니다. 궁극적으로 이는 프로젝트와 코드의 심각한 가독성 저하와 이어집니다.

2. 원활한 협업이 가능합니다.

일관된 코드 스타일이 유지되었다면 협업은 더욱 수월해집니다. 남들이 짠 코드를 보고 이런 의미에서 이렇게 짰겠구나 하고 불필요한 의사소통 없이 코드만 보고 파악이 가능합니다. 앞서 들었던 예시인 UserPerson을 꺼내자면, 모든 유저와 관련된 클래스 이름들에 합의하에 User를 붙여 UserRidings, UserSkills, UserItems 등으로 부르기로 했는데 어느날 PersonFriends 라는 클래스의 존재를 처음 봤을때는 굉장히 당황스럽겠죠? 이러한 일을 미연에 방지합니다.

3. 다른 사람(혹은 자신)의 코드를 읽기가 쉽습니다.

개발자나 프로그래머나 해커라고 하면 영화에서 표현되기를 쉬지않고 키보드를 두드리며 무언가를 써내려가다 나즈막히 “Got it” 같은 이상한 말을 내뱉기도 합니다. 혹시나 개발을 하는데 이러한 사람이 주위에 있다면 유심히 살펴보세요. 그사람은 개발이나 프로그래밍을 하고있는것이 아니라 한컴타자연습 따위에서 신기록을 갱신했을 가능성이 더 큽니다. 조금만 개발 공부를 해보신 분이라면 이건 말도 안된다는 것을 아시겠죠.

왜 말도 안될까요? 개발자(코딩을 하는 직업을 개발자라 통칭하겠습니다.)는 단언컨대 코드를 읽는 시간이 코드를 작성하는 시간보다 압도적으로 많기 때문입니다. 하루종일 남의 코드(혹은 자신의 코드)를 읽는 시간이 많기 때문에 코딩 컨벤션이 지켜진 코드를 읽는다면, 더욱 저자의 의도를 파악하기 쉬워집니다. 또는 이는 추후에 우리의 코드를 볼 다른 사람들에 대한 예의이기도 합니다.

네이밍 컨벤션

앞서 잠깐 언급했지만, 어떤 식별자나 파일명을 짓는데는 여러가지 네이밍 컨벤션이 사용되곤 합니다.

camelCase: 낙타의 혹같다고 하여 camelCase라 부릅니다. 처음은 소문자로 시작하고 단어마다 단어의 첫 글자는 대문자로 표기하는 방법입니다. 주로 코틀린의 함수명이나 변수명에 사용됩니다.

val age = 24fun printMyAge(){
println(age)
}

camel case with an uppercase first letter: camelCase에서 첫 글자도 대문자로 표기해주는 방법입니다.

ProcessDeclarations.kt

uppercased underscore-separated: 주로 코틀린에서 상수를 정의할 때 쓰는 방식입니다. 모두 대문자로 표기한 snake_case라고 보시면 됩니다.

const val ANIM_DURATION_MS = 300

snake_case: 주로 파이썬에서 사용되는 네이밍 컨벤션으로 뱀같다고 하여 붙여진 이름입니다. 언더스코어(_)를 이용해 단어를 나눠주며 모두 소문자입니다.

my_name = 'MJ'def print_my_name:
print(my_name)

kebab-case: snake_case와 비슷하지만 중간에 하이픈(-)을 삽입해서 표기해줍니다.

my-icon.png

이정도만 살펴보셔도 좋습니다.

디자이너와의 협업 툴인 제플린에서 아이콘을 다운로드 받을때도 여러가지로 설정을 할 수 있게 해줍니다.

코틀린의 코딩 컨벤션

이제 본격적으로 코틀린의 창시자들이 직접 제시해주는 여러가지 코딩 컨벤션들을 살펴보도록 하겠습니다. 사실 이렇게 언어를 만들어준 사람들이 이렇게 코딩 컨벤션을 적용하라라고 제시해주는 것은 행운입니다. JavaScript같은 경우는 끝에 세미콜론을 붙이냐 안붙이냐를 시작으로 수천 수만가지 규칙들을 직접 정의해서 입맛에 맞게 씁니다. 제가 이 글에서 담은 코틀린의 코딩 컨벤션은 공식 페이지의 핵심 내용만을 요약, 번역한 것에 불과합니다.

스타일 가이드를 적용하라

우선 Android Studio, Intellij IDEA등 Jetbrain사의 통합개발환경을 사용하신다면, 다음과 같이 설정해주실 수 있습니다.

코틀린 스타일 가이드 설정창에서 우측 상단의 Set from… 을 눌러주시고 제일 위에 있는 Kotlin Style Guide를 선택해주시면 알아서 코딩 컨벤션 스타일들이 변경됩니다. 이제 에디터에 다음과 같은 코드를 작성하고,

val a= 1

Reformat 키를 누르면

val a = 1

다음과 같이 스타일 가이드 설정에 따라 포맷이 됨을 볼 수 있습니다.

Reformat 키는 설정의 Keymap에서 다음과 같이 변경 가능합니다. 저는 IDE 단축키 하나하나를 하드코어하게 변경해서 쓰는 편이라 원래 무슨 키인지 잘 모르겠습니다.

만약 현재 코드에 스타일 가이드가 어긋난 부분이 있을 때 눈에띄고 해주고 싶다면 다음과 같이 설정의 Inspection에서 Kotlin | Style issues | File is not formatted according to project settings 항목을 원하는 경고 레벨로 설정해주시면 됩니다.

그러면 다음과 같이 에디터에서 경고를 띄워줍니다.

소스코드 파일 위치

Java와 Kotlin은 중복된 이름과 클래스 경로에서의 원하는 클래스의 위치를 찾기위해 package라는 개념을 사용합니다. 만약 프로젝트의 기본 패키지 이름이 happy.mjstudio.estrella 라면 프로젝트 내의 파일 제일 위엔 다음과 같이 적혀있을 것입니다.

package happy.mjstudio.estrella.presentation.screen.main

패키지 이름에 온점(.)으로 분리된 이름들은 디렉토리(폴더) 구조에서 하나의 디렉토리를 의미해야 합니다. 즉, 위와 같은 패키지명을 갖는 파일은 루트의 경로가 presentation/screen/main 이라는 디렉토리에 들어있는 파일이 되어야 합니다.

다행히 저는 코딩 컨벤션을 잘 지키고 있었군요.

소스코드 파일 이름

만약 소스코드 파일이 하나의 클래스나 인터페이스, 예를 들어 User라는 클래스만 갖는다면, 그 파일의 이름은 User.kt 가 되어야 합니다.

만약 여러가지를 한번에 갖는 소스코드 파일이라면(이러한 구조는 피하는게 좋습니다만), 그 여러가지를 대변할 수 있는 이름을 앞글자가 대문자인 camelCase로 지어주시면 됩니다.

ProcessDeclarations.kt

소스파일 구조

코틀린에서 여러가지 선언들(클래스, 전역 함수나 전역 변수)을 하나의 파일에 정의하는 것은 다음과 같은 조건들이 충족되는 한에서만 추천합니다.

  1. 그것들이 밀접하게 관련되어있다.
  2. 한 파일에 두어도 몇백줄을 넘기지 않는다.

특히 이것은 확장 함수를 선언할 때 유용합니다. 어떤 클래스 A에 대한 확장 함수를 여러 다른 파일에서 사용할 땐, 그 확장 함수를 A의 선언된 파일 안에 넣는것이 바람직합니다. 그 확장 함수를 A를 사용하는 클래스들 B,C,D 중에 B에서만 이용한다면 B에 넣는것이 바람직합니다. A의 모든 확장함수들을 갖고있는 AExtensions.kt 따위를 만들지 마세요.

클래스 구조

클래스 내엔 여러가지 구성요소들이 들어갈 수 있는데, 이것들엔 바람직한 순서가 있습니다.

  • 속성 선언들과 초기화 블록
  • 부생성자들(secondary constructors)
  • 메소드들
  • 컴패니언 객체

예를 들어 다음과 같습니다.

class User {
// 속성
private var age: Int? = null

// 초기화 블록
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 {

}
}

이러한 순서를 단순히 사전순이나 접근지시제어자 순으로 정렬하지 마세요. 일반적인 메소드와 확장 메소드를 구분하지 마시고 그저 관련있는 것들을 묶어서 정렬한다는 식으로 생각하시면 됩니다. 위에서 언급한 큰 정렬을 지켜주는 한에서 말입니다.

또한 클래스 안에 클래스가 들어가는 Nested Class는 그 클래스를 사용하는 클래스 내의 코드나 메소드 바로 밑에 위치시켜주시거나 이 클래스 내에서 사용하는 것이 아니라 외부에서 사용하는 것이라면 Companion 객체 이후에 선언해주시면 됩니다.

그리고 추상화 단계에 따라 메소드들을 정렬하는 방법도 있는데, 그 내용은 이 글의 범위를 벗어나고 당장 다룰 주제는 아닙니다.

인터페이스 구현체

인터페이스에 선언된 순서대로 클래스에서도 구현을 해주세요

interface User {
var age: Int
var name: String
}

class GameUser : User {
override var age = 24
override var name = "MJ"
}

패키지 이름

패키지 이름은 언더스코어(_)를 사용하지 않은 순수한 알파벳 소문자로만 구성해주세요. camelCase를 쓸 수도 있지만 비추천합니다.

org.example.project

클래스, 전역 객체 이름

앞서 보였듯이 젤 첫자리가 대문자인 camelCase로 선언해주세요

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

함수, 메소드, 변수, 클래스내 속성, 지역 변수 이름

camelCase로 작성해주시면 됩니다. 예외는 팩토리 패턴에서 팩토리 함수를 간혹 클래스 이름과 동일한 이름으로 작성해주는 경우가 있습니다.

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

테스트 함수의 이름

백틱(`) 으로 감싼 자유로운 문자열이나 언더스코어 사용이 가능합니다. 간혹 안드로이드 런타임에서 백틱사용이 불가능한 경우가 있습니다.

저는 주로 Unit 테스트는 백틱으로 감싸고 Instrumentation 테스트는 뒤에 _onAndroid 를 붙여주는 편입니다. 예제처럼요!

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

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

불변하는 속성, const 상수, Top Level 상수들

주로 대문자와 언더스코어를 이용해 작성합니다.

const val MAX_COUNT = 8
val USER_NAME_FIELD = "UserName"

Backing 속성들

Backing 속성들은 사용하려는 이름 앞에 언더스코어(_)를 삽입해서 쓰곤 합니다. 우리는 클래스를 정의할 때 정보은닉을 하고자 실제 값은 private 를 이용해 숨겨두고 보여져야 할 데이터들만 노출하는 방식을 쓸 때가 있습니다. 예를 들어 방송용 키를 5cm크다고 말하는 연예인이 있다고 생각해보죠.

class Celebration {
private val _height: Int = 250
val height: Int
get() = _height + 5
}

위와 같은 클래스에서 _height는 실제 키고 height는 방송용 키입니다. _heightheightbacking 속성이라고 합니다.

이는 Android JetpackLiveData를 사용할 때도 실제 MutableLiveData로 선언된 데이터는 private로 숨겨놓고 LiveDataViewModel에서 노출해주고 싶을때도 사용됩니다.

class MyViewModel {
private val _height: MutableLiveData<Int>(250)
val height: LiveData<Int> = _height
}

이름을 잘지어라

사실 이건 코틀린에만 한정된 이야기는 아닙니다만, 조금 이야기를 드리면 좋을 것 같습니다. 서버랑 통신을 할때 UserInfo 나 UserData 라는 클래스로 데이터를 받는다고 생각해봅시다. 뒤에 Info나 Data가 진짜 의미가 있는 변수명일까요? 그럼 다른 변수들은 정보나 데이터가 아닌가요? 이럴땐 단순하게 User라고 변수명을 지어주시면 됩니다.

또한 userList 같은 컬렉션으로 정보를 저장하는 변수가 있다고 합시다. 뒤에 List가 정말 필요한가요? 그냥 단순히 users 라고 나타낼순 없을까요? 정답은 가능하다입니다.

의미없는 접미사, 접두사를 제거하는것이 좋습니다. 이는 ManagerWrapper 등도 포함됩니다. 하지만 무작정 제거하라는 뜻이 아닙니다. 예를 들어 안드로이드의 Context에서 getSystemManager로 얻을 수 있는 여러가지 Manager 들은 항상 Manager가 붙어있는데, 이건 안좋은 명명법일까요? 그렇지 않습니다. 구글의 개발팀은 그저 시스템과 소통할 수 있는 여러 클래스들은 접미사로 Manager를 붙이겠다고 그들만의 코딩 컨벤션을 정의한 것입니다. 그렇기때문에 이는 허용됩니다. 오히려 InputMethodManager 같은 클래스를 InputMethod라고만 써놓으면 더 의미가 헷갈릴 수 있겠죠.

포매팅

포매팅에 관한 부분은 예를 들어 다음과 같은 것입니다.

“세미콜론을 붙일까 말까? if 뒤에 소괄호가 나오기 전에 한칸을 띌까 말까?”

이런 것들은 제일 처음에 설정한 IDE의 스타일 가이드를 잘 이용하면 그렇게 유심히 볼 필요가 없기 때문에 정리하지 않겠습니다. 하지만 몇가지만 살펴보죠.

Modifiers

Modifier는 선언 앞이나 변수 앞에 붙이는 언어의 예약어들입니다. 한글말로 무엇이라고 하는지 잘 기억이 안나네요 원래 없었던것 같기도 합니다.

이 Modifiers들이 한번에 여러가지가 사용되면 우선순위에 따라 정렬해주시면 됩니다.

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보다 우선순위가 높으므로 인라인인 중위표기법 함수를 만들 땐 inline을 infix보다 앞에 써줍니다.

inline infix fun Int.hello(a: Int) {

}

정말 많기도 하군요. 간단한 설명을 화살표와 함께 적어두었습니다. 이 모든걸 알 필요는 없습니다. 저도 Modifier 우선순위는 외우지 않습니다. 그냥 코틀린엔 이러한 Modifier들이 있다고 알아두시면 좋을 것 같습니다. 최소한 변수명을 이걸로 쓰는 일들이 없게말이죠.

함수 구조

함수의 인자가 최대 줄 수(표준 스타일 가이드에선 120)를 넘어가면 모두 multi-line으로 처리를 해주는게 좋습니다.

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

그리고 함수는 될수있는 한 무조건 단일식 함수를 선호하시길 바랍니다.

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

속성 구조

속성이 한줄로 표현되지 않을 땐 들여쓰기와 함께 그 다음 줄에 표현해주시면 됩니다.

val isEmpty: Boolean get() = size == 0 // 한줄로 표현이 가능할 때val foo: String // 한줄로 표현이 불가능할 때
get() { /*...*/ }

만약 속성의 초기화에서 한줄로 표현되지 않을 땐 = 뒤로 개행을 해주고 들여쓰기 해주시면 됩니다.

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

if문 when문

조건이 한줄로 표현되지 않을땐 함수 인자와는 조금 다르게 다음과 같이 쓰실 수 있습니다.

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

when 문에서 굳이 중괄호를 쓸 필요가 없다면 생략해주면 됩니다.

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

함수 호출 구조

만약 함수 호출을 할때 인자들이 길어서 한줄에 표현되지 않는다면 무조건 (뒤에 개행을 해주시고 서로 연관있는 인자들끼리 한줄로 표현해주시면 됩니다.

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

위의 예시를 보면 좌표를 나타내는 x,y가 한 줄에 있고 나머지 것들도 서로 연관이 있음을 볼 수 있습니다.

함수 호출 체이닝 구조

옵셔널 체이닝을 사용하거나 체이닝을 사용하는데 한 줄을 넘어간다면 ?.. 가 제일 앞에 오고 들여쓰기를 해서 작성해주시면 됩니다.

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

람다 구조

람다를 사용할 땐 코틀린의 마지막 람다식 인수 규칙을 이용해 람다를 소괄호 밖으로 빼줄 수 있고, 만약 인자가 람다 하나라면 소괄호를 아예 안쓸 수도 있습니다.

무조건 마지막 람다식 인수 규칙을 선호해주세요.

list.filter({ it > 10 }) // bad
list.filter { it > 10 } // good

만약 람다에 라벨링을 해준다면 람다와 @ 사이에 여백을 두지 마세요.

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

만약 람다식을 사용할때 기본으로 주어지는 it 대신 다른 이름을 쓴다면 -> 뒤에 개행을 해주고 람다식을 작성해주세요.

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

Trailing 콤마

Trailing 콤마는 코틀린 1.4 부터 추가된 문법입니다.

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

기존엔 저렇게 마지막 인자에 콤마를 쓸 수 없었는데, 이제 쓸 수 있죠.

이것은 자유롭게 사용할 수 있지만, 코틀린 스타일 가이드에서는 선언부에선 사용하고 호출부에선 자유롭다고 합니다.

주석 구조

긴 주석에는 /** 를 이용해 주석을 달아주시면 됩니다.

JavaDoc 스타일에는 @return 이나 @author 를 통해 주석을 남기기도 합니다.

그러나 우리가 라이브러리 작성자나 매우 긴 주석이 필요한 경우가 아니라면 이건 피하는게 좋습니다. 그냥 주석 본문에 의미를 담는게 더 효율적입니다.

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

필요없는 요소들은 모두 배제하라

코틀린에서는 여러가지 추론에 의한 문법 형성이 잘되어있습니다. 대표적인 예로 함수에서 반환형을 명시하지 않으면 반환형을 자동으로 Unit으로 추론합니다. 이렇게 쓸모없는 요소들은 다음과 같습니다. 모두 배제하는게 좋습니다.

  • 아무것도 반환하지 않는 함수의 Unit 반환형
  • 세미콜론 ;
  • 문자열 템플릿에서의 안써도 되는 중괄호
"$name" // good
"${name}" // bad
"${user.name}" // good

불변성 선호

코틀린엔 var가 있고 val이 있습니다. 변하지 않는 데이터는 무조건 val로 선언하셔야 합니다.

코틀린엔 MutableList 나 List등 가변성과 불변성을 의미하는 컬렉션들이 각각 있습니다. 변하지않는 데이터를 저장할땐 무조건 Mutable이 붙여져있지 않은 컬렉션을 선호해주세요.

함수의 기본 인자

함수의 기본 인자를 쓸 수 있다면 쓰는것이 좋습니다.

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

람다의 it

람다가 짧고 nested 되어있지 않다면 it를 그냥 쓰는것이 좋습니다.

// goodlist.forEach {
println(it)
}
// badlist.forEach { n ->
println(n)
}

람다의 라벨반환

람다의 라벨반환은 사용하지 않는것이 좋습니다. 이것이 필요한 경우라면 코드를 수정하라고 권고하고 있습니다.

다음과 같은 코드를 봅시다.

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
}
}

하지만 이런 문법을 사용하지 말라고 권고하고 있습니다. 위와 같은 상황에서는 그냥 it만 써주면 코틀린에서는 람다 스코프내에서 반환이 됩니다.

람다나 when식, if식에서도 명시적으로 return 이 없으면 마지막에 존재하는 수가 반환됩니다.

fun main(vararg args: String) {
val list = listOf(1, 2, 3)

list.map {
it
}
}
val age = if (true) { // age is 24
24
} else {
10
}

조건문에서는 statement(문)가 아닌 expression(식)을 선호하라

왜 코틀린에서는 if를 if문이라 안부르고 if식이라고 부를까요?

그건 바로 은 반환하는 값이 있지만 은 없기 때문입니다.

코틀린에서는 if나 when에서 앞서 언급했듯이 값을 바로 반환할 수 있습니다.

// bad
var age: Int
if (true) {
age = 24
} else {
age = 10
}
// good
val age = if (true) {
24
} else {
10
}

반복문에서는 번거로운 .. 대신 until 을 사용하라

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

0부터 n-1의 범위까지 0..n-1 보다 0 until n을 써주는 것이 가독성이 좋습니다.

코틀린에서 ..IntRange를 반환하며 범위에 대해 닫힌구간, 닫힌구간입니다. 그래서 0..30, 1, 2, 3입니다.

함수 vs 속성

코틀린은 속성이 gettersetter로 함수의 역할을 대신할 수 있습니다.

속성을 함수보다 다음과 같은 경우에 선호하길 권장합니다.

  • 에러를 던지지 않는다
  • 계산하기 쉽다
  • 객체의 상태가 불변하는 한 같은 값을 반환한다

팩토리 함수

앞서 언급했습니다만, 팩토리 함수에서 클래스와 같은 이름을 쓸 수는 있지만 이는 권장되는 문법이 아닙니다. 의미있는 이름을 붙여주세요.

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

위의 Point 에서 팩토리 함수 이름을 Point로 지을 수도 있겠지만, fromPolar로 지은 것 처럼 말입니다.

코틀린의 스코프 함수들의 사용 경우

코틀린엔 아름다운 문법을 만들어주는 let, run, with, apply 그리고 also라는 스코프 함수가 존재합니다. 이들이 각각 언제 사용되야 하는지는 다음과 같은 링크를 참고해주세요.

이걸 다 알아야 하나요?

이로써 도큐먼트에 명시된 대부분의 코틀린의 코딩 컨벤션을 살펴보았습니다. 이걸 모두 다 머리속에 역사시험보듯이 달달 외워야 할까요? 절대 아닙니다. 쓰다보면 몇개는 IDE의 교정에 따라 저절로 습득하게 되는 것도 있고, 저도 절대 다 외우고있지 않습니다.

이것들은 단지 원활한 협업과 버전관리 및 가독성을 위한 하나의 가이드라인일 뿐입니다. 하지만 코딩 컨벤션이 무엇인지를 알고 중요성을 인식하고 만약 새로운 언어를 배울 때 이를 떠올리고 찾아보게 된다면 새로운 언어도 손쉽게 습득하고 좀더 프로스러운 코드작성이 가능해질것임은 자명합니다.

긴 글 읽어주셔서 감사합니다 🙌

저도 다시 정리를 해가며 공부를하며 의미있는 시간이었습니다.

--

--