개발 업무에서 규칙을 정하는 것은 중요한 부분이라는 것은 다들 알고 있는 사실일 것이다.
혼자서 개발하는 서비스 어플리케이션이라면 크게 중요하다고 생각하지 않을 수도 있다. 그러나 인원이 1명 이상 동시에 작업을 진행한다면 같은 리파지토리에 작업을 진행하기 때문에 서로의 관점도 틀리고 같은 로직에도 다양한 방법으로 개발이 가능하기 때문에 서로의 생각을 최소한으로 맞추는 것이 중요하다고 생각한다.
이전글에서는 전체적인 개발자가 개발을 하여 리포지토리에 푸시하고 머지하는 행위까지 전반적인 내용을 다루었다.
전체적인 개발 프로세스 정의 하기
어느 개발 조직이든 비슷하게 개발완료해서 repository 까지 저장되는데 아래의 그림처럼 진행 될 것이다.브랜치 전략이나 gitFlow 같은 전략을 이미 사용중이라면 그 룰도 개발 원칙에 포함되어
hbungshin.tistory.com
코딩 규칙을 정하면 장점은
- 일반적인 코드 작성을 강제 하기 때문에 서로 코드의 커뮤니케이션에 도움을 준다.
- 신규, 혹은 외부에서 작업을 같이 하는 일이 생긴다면 해당 규칙 가이드를 해줘서 코드의 건전성이 생길 수 있다.
- 코드리뷰를 진행할 때 상대방에게 서로 규칙이 존재하기 때문에 코드 리뷰할 때 도움을 준다.
- 코드 품질이 향상되고 가독성이 좋아진다.
klint 같은 코드 정적툴에 위에서 정한 룰들을 적용하면 코드에 대해서 강제하게 되고 코드 품질을 올릴 수 있다.
나의 주된 언어는 kotlin, java 위주로 개발을 진행했었고 요새 AI 관련 개발을 진행하고 있다.
Kotlin 언어에 대한 코팅 규칙
Kotlin 코딩 규칙을 따르되 각 상황에 맞게 추가로 컨벤션을 정하는 형태로 하는 것이 좋다.
예를 들어
isEmpty:Boolean = false
//아래 2가지는 동일하게 동작하는데 sonarqube 에서는 첫번째는 통과 아래는 실패로 나온다.
//개인적으로는 두번째가 가독성 측면에서는 좋다고 보지만 상황에 따라서 규칙을 정하면 된다.
if(!isEmpty)
if(isEmpty == false)
아래 내용은 전체를 커버하진 않지만 Kotlin 코딩 규칙을 정의 한 후에 추가로 정의하면 더 좋을 것이다.
개요
이 문서는 보충 규칙과 모범 사례를 제공한다. 따라서 코틀린 규칙은 따르고 몇 가지 예제를 통해서 규칙을 정의하도록 한다.
목표
- 목표: 읽기 가능한 코드를 지양한다. 변수, 클래스, 주석 등 다른 사람이 봤을 때 이해도가 높은 코드를 만드려고 노력한다.
- 가능한 많은 Kotlin 피쳐 사용을 사용하여 언어 본연에 충실한다.
- 코틀린 기능을 활용하여 코드를 판독 가능하도록 한다.
기본 스타일
기본적으로 Kotlin 코딩 규약을 따른다.
이름을 붙일 때 밑줄을 사용하지 않는다. 두 속성 간의 차이를 기준으로 백업 필드 이름을 지정하는 것이 좋다.
안드로이드 앱을 개발하는 데 있어서 내부 변수는 _ 를 추가하여 private 객체로 정의하고 _ 가 없는 객체는 외부에서 접근가능하게 만드는 것이 일반적으로 통용이 되었다. 아래 내용은 참고
// GOOD
private val modifiableElementList: MutableList<Element> = mutableListOf()
val elementList: List<Element> = modifiableElementList
// BAD
private val _elementList: MutableList<Element> = mutableListOf()
val elementList: List<Element> = _elementList
코멘트
또한 Klint와 github Action 같은 자동화를 한다면 소스 push 시점에서 Fail 체크가 가능하기 때문에 용이하다.
나 같은 경우는 klint를 주로 사용하는데 룰을 정의하고 push 전체 한번 체크 후 Push를 진행했던 것 같다.
클래스
각 클래스, 인터페이스 또는 오브젝트는 companion object 및 익명 오브젝트를 제외하고 문서 설명을 포함해야 한다.
문서에는 다음과 같은 두 가지 요구사항이 있다.
- Javadoc/Kdoc 규칙을 따르자. 예를 들어, 코멘트 스타일은 "이어야 한다./**... */
- 먼저 "유형이 무엇인지 , 유형이 무엇인지"를 설명하는 짧은 요약으로 명사 구절을 시작하자.
- 그다음, 선택적 문장은 사용법, 제한사항 등을 설명하기 위해 짧은 요약에 따를 수 있다.
참고: 설명서가 없는 기존 클래스를 찾으면 코멘트를 작성하자.
제어 흐름
"if"/"when" 표현/표현
- if-message:호출자가 결과를 소비한다(예: 할당, 반환 값 또는 함수 매개 변수).
- if-expression의 본문은 동일한 코드 라인에 모두 적합한 경우 단일 라인 형식(*)으로 갈 수 있으며, 그렇지 않은 경우 두 가지 줄의 본체에 대해 다중 라인 형식(**)을 적용해야 한다.
- if-datement:if-statement 본문은 다중 행 형식과 일치해야 한다.
- (*) 단일 줄 형식:컬브 브레이스({})를 사용하면 안 된다.
- (**) 다중 행 형식:({})가 필요하다.
다음 예제를 참조해 보자.
// Standard style
if (condition) {
statement()
}
// with else
if (condition) {
statement()
} else {
anotherStatement()
}
// Assignment on a line
val value = if (condition) valueA else valueB
val longLongLongLongLongNameValue = if (condition) valueA else valueB
// Assignment with block
val anotherValue = if (condition) {
longLongLongLongLongLongNameValueA
} else {
longLongLongLongLongLongNameValueB
}
val oneMoreValue =
if (longLongLongCondition) {
longLongLongLongLongLongNameValueA
} else {
longLongLongLongLongLongNameValueB
}
- 할당 또는 반환 값에 식을 사용할 경우 각 본문 블록에는 식이 하나만 있어야 한다
예를 들어, 다음 코드는 지양한다.
// BAD: 각 본문 블록에는 추가 명령문이 있습니다. 본문을 함수로 추출하여 "if" 표현식 값을 사용합니다.
val string = if (i % 2 == 0) {
methodCall()
anotherMethodCall()
"even"
} else {
furtherMoreMethodCall()
"odd"
}
식이 할당 또는 반환 값의 다른 목적으로 사용되어서는 안 될 경우 값이다.
예를 들어, 다음 코드는 지양한다.
// BAD: "if" 표현식의 어떤 값도 메서드 인수로 사용할 수 없습니다. 먼저 로컬 값으로 정의하세요.
function(if (i % 2 == 0) "even" else "odd")
// BAD:
"if" 표현식의 어떤 값도 함수 수신자로 사용할 수 없습니다. 먼저 로컬 값으로 정의하세요.
if (i % 2 == 0) {
"even"
} else {
"odd"
}.capitalize()
- 다중 조건 블록의 경우 인수가 없는 식을 사용해야 한다. 그렇지 않으면 가독성이 개선된 특수한 상황에서만 문을 사용해야 한다.
예제
// BAD
if (expression1) {
doStuff()
} else if (expression2) {
doOtherStuff()
} else {
doOtherOtherStuff()
}
// GOOD
when {
expression1 -> doStuff()
expression2 -> doOtherStuff()
else -> doOtherOtherStuff()
}
"while"/"for" 루프
마치 표현과 같이, 반면/대용 루프는 각 body 블록에 대해 브레이스를 가지고 있어야 하며 블록은 새로운 선으로 시작해야 한다.
(조건) 루프가 있는 동안 {}에 do을(를) 사용하지 말자.
빈 블록
빈 블록을 정의하려면 첫 번째 줄에 열린 브레이스를, 두 번째 줄에 닫힌 브레이스를 놓자.
https://android.github.io/kotlin-guides/style.html#empty-blocks).
try {
doSomething()
} catch (e: Exception) { // break the line for an empty block
}
:
멤버 변수 순서
클래스, enum, interface, object 등의 멤버 오더는 안드로이드 스튜디오에서 제공하는 자바의 오더 포맷터를 따른다.
코틀린 주문 포맷터나 공식 컨벤션 등이 없기 때문에 자바코드가 코틀린으로 전환되면서 일관성을 제공하는 데 도움이 될 것이다.
그러나 "정적 필드"는 동반 대상의 필드에 의해 실현되기 때문에 최종적으로 배치되어야 한다.
순서는 다음과 같다.
- 읽기 전용 속성(val)으로 정의
- 읽기-쓰기 속성(var)으로 정의
- 초기화 블록(초기)
- 보조 생성자(구성자)
- 함수()
- 읽기 전용 속성 확장(val T.x)
- 읽기-쓰기 속성 확장(var T.x)
- 함수 extention(T.foo())
- 중첩 또는 내부 클래스, 열거형, 인터페이스 및 개체
- companion object
다음 예제를 참조하자.
class Klass(val parameter: Value) {
// Read-only properties
val publicValue: Value = Value()
private val value: Value = Value()
// Read-write properties
var publicVariable: Value = Value()
private var variable: Value = Value()
// Initialization block
init {
// ...
}
// Other constructors
constructor(firstValue: Int, secondValue: Int) : this(Value())
// Functions
fun publicFunction() { ... }
private fun privateFunction() { ... }
// Extensions of read-only properties
val <T> T.publicReadonly: Value get() = Value()
private val <T> T.readonly: Value get() = Value()
// Extensions of read-write properties
var <T> T.publicReadWrite: Value
get()
set()
private var <T> T.readWrite: Value
get()
set()
// Extensions of functions
fun <T> T.publicFunction() { ... }
private fun <T>.privateFunction() { ... }
// Nested or inner classes, enums, interfaces, and objects
enum class EnumType private constructor(val enumField: Int) {
DEFAULT(1),
A_ENUM_TYPE(2),
}
private data class NestedDataClass(val int: Int)
// Constants
companion object {
const val PRIMITIVE_CONSTANT = 0
@JvmField
val NON_PRIMITIVE_CONSTANT: OtherClass = OtherClass()
}
}
각 범주에 대해 가능한 한 다음과 같은 규칙을 지키도록 권장한다.
- public 이 private 보다 member ordering 이 높다.
- 정적 항목이 동적으로 결정된 항목 이상임
- 추상화 수준이 높은 항목은 낮은 추상화 수준 항목보다 높다.
유형 선언
- 형식 선언은 반드시 지켜야 한다.
- 생성자 또는 메서드의 매개 변수
- 비유닛 리턴형 함수
- 전부 **var** properties
- 다음 형식 선언을 삭제하십시오.
- 단위반환형법
- "const" 한정자가 있는 모든 필드
- ... 에 대한 형식 선언을 유지하거나 취소할 수 있다.
- 로컬 값 및 변수
- 람다의 파라미터 유형
- val
- 가시성과 관계없이 생성자에 대한 호출을 사용하여 즉시 할당되는 속성
다음 예제를 참조하자.
// You must keep type for public values and any variables
private var variable: Type = Type() // Type required because `var`
// You must keep type if the right value is not direct constructor call or primitive value
private val array: Array<Int> = arrayOf(1, 2, 3) // `arrayOf` is not a constructor.
private val sum: Int = 1 + 2 // The right value is the result of `plus` function.
private val valueWithApply: Type = Type().apply { ... } // `.apply` is appended.
// You may keep or drop the type for values instantiated with constructor calls
// With constructor call
val value: Type = Type() // Kept (somewhat repetitive)
val value = Type() // Dropped
private val value1: Type = Type() // Kept (somewhat repetitive)
private val value2 = Type( // Dropped
parameter,
parameter2
)
// With primitive value assignment
private val value3: Int = 1 // Kept
private val value4 = 1 // Dropped
// Anti-pattern: dropping type declarations in situations other than explicit constructor calls
private val value = Type.generateInstance() // Not an actual constructor call
private val value2 = Type( // Starts with a constructor call...
parameter,
parameter2
).toOtherType() // But may be deceptive
// You must keep the type for private variables
private var variable: Superclass = Subclass() // The type may be different than the initial value, because it can be reassigned
/* ... */
variable = OtherSubclass() // The variable might take on another type
} else {
variable = Superclass() // ...or it might be assigned with the super-type
// Even if the type was dropped, this code would be compiled successfully due to Kotlin's inferred typing system, but this isn't immediately obvious to readers
// You must keep return type with non-Unit methods
fun nonUnitFunction(): Int = 1
// You must skip return type if it is Unit
fun unitFunction() = Unit
// You must keep parameter type
fun unitFunction(i: Int) {
// You may skip local value/variable type...
val j = 0
// or you may also keep the type.
val k: Int = i + j
// For lambdas, you can choose from any valid format.
val firstLambda: (Int) -> Int = { it + 1 }
val secondLambda = { i: Int -> i + 1 }
val thirdLambda: (someMeaningfulName: Int) -> Int = { anotherName -> anotherName + 1 }
}
함수 정의
헤더 포맷
헤더 형식은 Kotlin 코딩 규칙의 클래스 헤더 형식에 따른다. 헤더가 충분히 짧으면 반드시 한 줄에 써야 한다. 헤더가 긴 경우 각 매개변수 선언은 별도의 줄에 있어야 한다. 다음 예제를 참조하십시오.
fun shortHeader(): Int {
// ...
// ...
}
fun shortHeader(parameter: Int): Int {
// ...
// ...
}
fun longHeader(
parameter: Parameter,
anotherParameter: Parameter,
oneMoreParameter: Parameter
): Type {
// ...
// ...
}
fun longHeaderWithUnitReturnType(
parameter: Parameter
) {
// ...
// ...
}
Brace 및 = 정의 기호
- 하나의 표현식으로 함수를 정의하려면 반환 유형이 단위인 경우에도 ****= 기호를 사용하자.
- 여기에는 if, when, function calls with lambda와 같은 표현이 포함된다.
- 사용하다= Unit이 규칙의 예외는 보조 생성자가 다른 생성자에게 위임하는 경우다.
- 빈 함수를 정의하십시오.
다음 예제를 참조하자.
fun function(): Int = 1 + 2
fun function(parameter: Int): Int =
1 + parameter
fun anotherFunction(): Type = Type().also {
it.variable = "variable"
it.anotherVariable = "variable"
}
fun whenFunction(type: Type): String = when(type) {
Type1 -> "Type1"
Type2 -> "Type2"
}
fun unitFunction() = anotherUnitFunction()
fun emptyFunction() = Unit
// Exception for secondary constructors
constructor(type: Type) : super(type)
constructor(type: Type, anotherType: Type) : this(type)
상수 값
상수 값은 자본화됨_으로 선언해야 함스네이크_케이스_라이크_이것. 또한 동반 객체의 필드 또는 글로벌 값으로 정의해야 한다(다음 섹션 참조). 상수 값이 원시 값이나 문자열인 경우 const 수식어를 추가해야 한다. 기본 상수가 아닌 상수의 경우, Java에서 표시되도록 하려면 @JvmField 주석을 추가해야 한다.
const val PRIMITIVE_CONSTANT = 0
@JvmField
val NON_PRIMITIVE_CONSTANT: NonPrimitive = NonPrimitive()
global value 및 global function
글로벌 값, 객체(순수함수가 아닌 경우도 있음) 또는 확장자를 변경하지 않고 기능 또는 전역 값을 정의하려면 다음과 같이 적절한 패키지에 새 파일을 생성하십시오.
YourConstants.kt
**`package`** `com.linecorp.yourpackage`
`const **val**` `YOUR**_**CONSTANT**:**` `Int **=**` `0;`
**`@**JvmField`
**`val`** `YOUR**_**ANOTHER**_**CONSTANT**:**` `Value **=**` `Value();`
YourUtils.kt
**`package`** `com.linecorp.yourpackage`
`fun yourUtilityFunction()**:**` `ReturnValue {`
`// ...`
`**return**` `ReturnValue()`
`}`
Java에서 접속할 때 파일 이름 또한 클래스 이름으로 사용되기 때문에 파일 이름은 다음과 같이 제한된다.
- 파일 이름 접미사는 상수의 "Constants"여야 한다.
- 효용 함수의 파일 이름 접미사는 "Utils"여야 한다.
- 확장 기능 모음에 대한 파일 이름 접미사는 "확장"이어야 한다.
글로벌 확장을 만들려면, 다음 소유자에게 요청하는 것이 좋다.
Lamda
중첩된 람다(람다(람다))를 피하십시오.
중첩된 람다는 종종 읽을 수 없다. 게다가, 그것은 전형적인 코드 냄새의 신호다. 대신 메서드 참조, 메서드 접속사, 플랫맵 또는 기타 메서드를 사용하자. 예를 들어 다음과 같은 코드는 금지된다.
//BAD
list.map { innerList ->
innerList.map {
// DON'T DO THIS !!!
}
}
예외:
예외적으로 외부 람다가 글로벌 범위 또는 클래스의 최상위 수준(=연산자 포함 함수/값 할당 범위 포함) 바로 아래에 있으면 중첩된 람다를 사용할 수 있다. 허용 가능한 코드는 다음과 같다.
class Klass {
val intValue: Int by lazy {
intList.sumBy { it + it * it }
}
fun function(mutableObject: MutableClass?, list: List<Int>) = mutableObject?.apply {
firstEven = list.first { it % 2 == 0 }
firstOdd = list.first { it % 2 != 0 }
// And more
}
}
다음 코드는 허용되지 않는다.
//BAD
class Klass {
fun function(mutableObject: MutableClass?, list: List<Int>) {
mutableObject?.apply {
firstEven = list.first { it % 2 == 0 }
firstOdd = list.first { it % 2 != 0 }
}
// And more
}
긴 람다 피한다.
읽기 쉽도록 "긴" 람다는 피해야 한다. 다음 조건 중 하나를 만족하는 람다는 "긴" 람다.
- 반환 식을 제외한 2개 이상의 문장이 있는 람다.
- if 식, a when 식 또는 for/while 루프가 있는 람다.
다음 예를 참조하자.
// Short lambda expression: 1 statement and 1 return expression
list.map {
methodCall(it)
returnValue(it)
}
// Long lambda: with for when
dialogBuilder.setPositiveButton("OK") { dialog: DialogInterface, which: Int ->
when (which) {
0 -> // ...
else -> // ...
}
}
// Long lambda: 2 statements (the second line is not return expression)
obj.apply {
methodCall()
anotherMethodCall()
}
"긴" 람다를 사용하지 않으려면 표현식을 방법으로 추출하고 메서드 참조를 사용하여 가능하면 명명된 클래스를 만드십시오.
예외:
편의상 "긴" 람다(long)는 다음과 같은 경우에 허용된다.
- 반환 값으로 직접 사용되는 람다
- 함수의 마지막 인수로 직접 사용되는 람다. 여기서...
- 다른 주장은 람다가 아니다.
- 람다는 위임 또는 수정 수신기를 포함한 기능의 반환 값을 생성하는 데만 사용된다.
- 함수 반환 값은 호출자의 할당 또는 반환 값에 사용되지 않거나 직접 사용된다.
// OK: Long lambda directly used for return value
fun createComparator(): (Int, Int) -> Int = { x: Int, y: Int ->
// Code...
// Code...
result
}
// OK: Long lambda used for assignment directly.
val mappedList = list.map {
when (it) {
0 -> it
else -> it + 1
}
}
// OK: Long lambda used for delegation directly.
val view by lazy {
// initialization
// another initialization
inflatedView
}
다음 코드는 허용되지 않음:
// BAD: the result of function taking lambda is not directly used for assignment
val mappedList = list.map {
when (it) {
0 -> it
else -> it + 1
}
}
.map { /* another map */ }
// BAD: the result of function taking lambda is not directly used for assignment
val value: View = parent?.let {
// initialization
// another initialization
foundView
} ?: defaultView
로컬 값/변수와 관련하여 닫기를 반환하지 않음
대신 concrete 클래스 또는 객체를 만드십시오.
try-catch 블록별로 예상된 예외 유형을 명시적으로 지정하십시오.
전체를 다루지 않았지만 시간이 나면 하나씩 하나씩 추가를 더할 예정이다.
개발 문화를 잡아가는 데 있어서 기본의 규칙 + 조직과 팀원과 협의해서 정의를 한 후에 자동화 및 정적 분석 도구를 활용한다면
가독성과 읽기 쉬운 코드가 양산될 확률이 높다고 생각한다.
코드를 만드는 것은 사람, 인종(?)마다 틀리는 경우를 많이 보았다.
특히 한국인과 인도나 여타 다른 나라의 사람들이 만든 코드는 또 다른 매력이 있다.
물론 여러 가지 코드를 봐보면서 나의 것을 만드는 것도 좋다.
특히 AI 시대에 자기가 작성한 코드를 ChatGPT 도움으로 훨씬 괜찮은 코드가 양산된다.
물론 모든 걸 AI를 보고 만드는 것은 좋지 않은 습관이지만 AI 가 만든 코드를 보면서 더 좋은 코드를 양산할 수도 있다고 생각한다.
'프로세스' 카테고리의 다른 글
개발 조직에서 꼭 지켜야 할 코드 리뷰 원칙과 문화 (3) | 2025.05.17 |
---|---|
전체적인 개발 프로세스 정의 하기 (0) | 2025.04.07 |
Development Culture (0) | 2025.04.07 |
화성에서 온 남자, 금성에서 온 여자 (0) | 2025.03.02 |