
1편에서는 코틀린 클린코드의 큰 기준을 먼저 잡아봤습니다. 좋은 코드는 단순히 짧은 코드가 아니라, 의도가 빠르게 읽히고 안전하게 수정할 수 있는 코드라는 점을 말씀드렸습니다. 이번 2편에서는 그 기준을 가장 직접적으로 만드는 요소인 이름 짓기를 다뤄보겠습니다. 시리즈 전체 흐름이 궁금하시다면 코틀린 클린코드 허브 페이지를, 1편을 아직 읽지 않으셨다면 코틀린 클린코드란 무엇인가부터 함께 보시면 더 흐름이 잘 잡히실 것입니다.
실무에서 코드 리뷰를 하다 보면, 복잡한 알고리즘보다 모호한 이름 때문에 이해가 늦어지는 경우가 훨씬 많습니다. 특히 코틀린은 타입 추론이 강하고 문법이 간결해서, 이름이 제 역할을 하지 못하면 코드가 더 빠르게 불투명해집니다. 타입 선언이 줄어든 자리를 결국 이름이 채워야 하기 때문입니다.
좋은 이름은 주석보다 먼저 읽히는 문서입니다. 코틀린에서는 그 역할이 더 중요합니다.
이번 글에서는 Kotlin 공식 네이밍 규칙을 바탕으로, 실무에서 오래 가는 이름을 짓는 기준을 정리해드리겠습니다. 단순히 camelCase 규칙만 나열하지 않고, 변수명·함수명·클래스명·Boolean 이름·컬렉션 이름·상수·테스트 이름까지 실제 예제와 함께 설명드리겠습니다.
- 왜 코틀린에서는 이름이 더 중요한가
- Kotlin 공식 네이밍 규칙 핵심 정리
- 실무에서 바로 쓰는 네이밍 원칙 7가지
- 실무에서 자주 보이는 네이밍 실수
- 이름 짓기 체크리스트
- 정리와 다음 글 안내
왜 코틀린에서는 이름이 더 중요할까요?
코틀린은 타입 추론 덕분에 코드가 짧아집니다. 하지만 그만큼 이름이 맡아야 하는 설명 책임은 커집니다. 아래 코드는 문법적으로는 아무 문제도 없지만, 실제로 읽는 입장에서는 맥락을 바로 파악하기 어렵습니다.
fun process(list: List<Order>): Int {
val result = list
.filter { it.status == OrderStatus.PAID }
.sumOf { it.totalPrice }
return result
}
process, list, result는 모두 너무 넓은 이름입니다. 이 함수가 무엇을 처리하는지, 어떤 값을 반환하는지, 왜 필요한지 이름만으로는 거의 드러나지 않습니다. 같은 로직도 아래처럼 바꾸면 의도가 훨씬 빨리 읽힙니다.
fun calculatePaidOrderTotal(orders: List<Order>): Int {
val paidOrders = orders.filter { order -> order.status == OrderStatus.PAID }
return paidOrders.sumOf { order -> order.totalPrice }
}
두 코드의 차이는 알고리즘이 아닙니다. 읽는 사람이 머릿속에서 해석해야 하는 부담이 다릅니다. 첫 번째 코드는 “이 함수가 뭘 하지?”를 생각하게 만들고, 두 번째 코드는 “아, 결제 완료 주문의 총액을 구하는구나”를 바로 떠올리게 만듭니다.
코틀린에서는 이런 차이가 더 크게 느껴집니다. 타입 추론, expression body, scope function, 컬렉션 체이닝까지 결합되면, 이름이 부정확할수록 코드가 빨리 압축되어 버리기 때문입니다. 그래서 코틀린 클린코드에서 네이밍은 선택사항이 아니라, 가독성의 출발점에 가깝습니다.
Kotlin 공식 네이밍 규칙 핵심 정리
실무에서 이름을 지을 때는 감각만 믿기보다, 먼저 공식 규칙을 기준선으로 잡아두는 편이 좋습니다. 코틀린 공식 coding conventions에서 중요한 규칙만 뽑아 정리하면 다음과 같습니다.
1. 패키지명은 소문자로 쓰고, 파일명은 역할이 드러나게 짓습니다
패키지명은 모두 소문자로 작성하고, 일반적으로 언더스코어는 쓰지 않습니다. 파일에 단일 클래스나 인터페이스가 들어 있다면 보통 클래스명과 파일명을 맞추고, top-level 선언 위주라면 파일이 담고 있는 역할이 드러나게 이름을 짓는 편이 좋습니다. Util처럼 의미가 약한 이름은 가능하면 피하시는 것이 좋습니다.
package com.example.order
// 좋은 예
// OrderValidation.kt
fun validateOrderAmount(amount: Int) { /* ... */ }
fun validateOrderItems(items: List<OrderItem>) { /* ... */ }
// 아쉬운 예
// OrderUtil.kt
파일명까지 신경 써야 하는 이유는 단순히 보기 좋기 때문만이 아닙니다. 프로젝트가 커질수록 파일명 자체가 탐색 도구가 되기 때문입니다. “이 로직이 어디 있지?”를 찾을 때 OrderUtil.kt는 힌트를 주지 못하지만, OrderValidation.kt는 의도를 바로 드러냅니다.
2. 클래스와 객체 이름은 UpperCamelCase, 그리고 보통 명사형으로 짓습니다
클래스와 객체 이름은 UpperCamelCase를 사용합니다. 동시에 “무엇인지”를 설명하는 명사 또는 명사구가 되는 편이 자연스럽습니다.
class UserProfile
object EmptyCart
class OrderPriceCalculator
class PersonReader
반대로 Manager, Wrapper, Handler처럼 역할이 흐릿한 단어는 신중하게 써야 합니다. 물론 정말 매니저 역할을 하는 객체라면 괜찮지만, 대부분은 이름을 구체화할 여지가 남아 있습니다.
3. 함수명, 프로퍼티명, 지역 변수명은 lowerCamelCase를 사용합니다
함수, 프로퍼티, 지역 변수는 소문자로 시작하는 camelCase를 사용하고, 일반적으로 언더스코어는 넣지 않습니다. 테스트 코드가 아닌 일반 코드에서 user_name, load_user 같은 이름은 코틀린 스타일과 잘 맞지 않습니다.
fun findActiveUsers() { /* ... */ }
val declarationCount = 1
val createdAt = LocalDateTime.now()
val orderItems = listOf<OrderItem>()
4. 상수는 UPPER_SNAKE_CASE를 사용하고, backing property는 언더스코어를 붙일 수 있습니다
const val 또는 깊게 불변인 top-level/object 프로퍼티는 보통 대문자와 언더스코어를 사용합니다. 반면 공개 프로퍼티 뒤에 숨은 private 구현용 프로퍼티는 언더스코어 접두사를 사용하는 방식이 공식 문서에 나와 있습니다.
const val MAX_RETRY_COUNT = 3
const val DEFAULT_PAGE_SIZE = 20
class Cart {
private val _items = mutableListOf<CartItem>()
val items: List<CartItem>
get() = _items
}
5. 테스트 메서드는 예외적으로 백틱이나 언더스코어를 사용할 수 있습니다
운영 코드와 달리 테스트 코드에서는 이름이 “문장”처럼 읽히는 편이 더 유리할 때가 많습니다. 그래서 테스트 코드에서는 백틱으로 공백이 포함된 이름을 쓰거나, 언더스코어를 사용하는 방식이 허용됩니다. 다만 안드로이드 환경에서는 런타임 버전에 따라 제약이 있을 수 있으므로, 프로젝트 환경에 맞춰 선택하시는 편이 안전합니다.
class OrderValidatorTest {
@Test
fun `주문 금액이 0원이면 예외가 발생한다`() {
// ...
}
@Test
fun validateOrderAmount_throwsException_whenAmountIsZero() {
// ...
}
}
다만 이 예외는 테스트 코드에 한정해서 보는 편이 좋습니다. 운영 코드까지 문장형 이름이나 언더스코어를 넓게 가져가면 일관성이 깨질 수 있습니다.
6. 약어가 들어가면 표기 규칙도 맞춰야 합니다
코틀린 공식 문서는 약어 사용 규칙도 제시합니다. 두 글자 약어는 둘 다 대문자로, 세 글자 이상 약어는 첫 글자만 대문자로 쓰는 방식입니다.
class IOStreamReader
class XmlResponseParser
class HttpRequestLogger
XMLParser, HTTPRequestLogger처럼 전부 대문자로 몰아 쓰는 습관은 자바 코드베이스에서 자주 보이지만, 코틀린 스타일에서는 조금 다듬는 편이 더 자연스럽습니다.
실무에서 바로 쓰는 네이밍 원칙 7가지
1. 타입보다 의미를 이름에 담으세요
실무에서 가장 흔한 실수는 이름에 비즈니스 의미 대신 자료형을 넣는 것입니다. userList, mapData, responseDto 같은 이름은 타입만 말하고, “무엇을 담는지”는 충분히 설명하지 못합니다.
val userList = userRepository.findAll()
val mapData = response.associate { it.id to it.name }
val responseDto = orderService.getOrder()
아래처럼 “무엇인지”가 먼저 드러나도록 바꾸면 읽는 속도가 확실히 달라집니다.
val activeUsers = userRepository.findActiveUsers()
val userNamesById = response.associate { user -> user.id to user.name }
val orderSummary = orderService.getOrderSummary()
물론 모든 경우에 타입 정보가 불필요한 것은 아닙니다. 다만 이름의 첫 번째 책임은 자료구조 설명이 아니라 도메인 의미 전달이어야 합니다. 타입은 IDE가 보여주지만, 의미는 이름만이 보여줍니다.
2. 클래스는 명사, 함수는 동사로 시작하면 대부분 틀리지 않습니다
이 원칙은 단순하지만 매우 강력합니다. 클래스는 보통 “무엇인가”를 나타내고, 함수는 “무엇을 하는가”를 나타냅니다. 그래서 클래스에는 명사형, 함수에는 동사형이 자연스럽습니다.
class PaymentApproval
class OrderSummary
class UserSession
fun approvePayment(command: ApprovePaymentCommand)
fun publishArticle(article: Article)
fun refreshSession(sessionId: String)
이 기준이 무너지면 코드가 빠르게 헷갈립니다. 예를 들어 클래스 이름이 CreateUser이면 “행위”처럼 보이고, 함수 이름이 userCreation이면 “데이터”처럼 보입니다. 이름만 읽고도 역할을 구분할 수 있어야 코드 흐름이 매끄러워집니다.
3. 함수 이름은 부작용과 반환 방식을 함께 드러내야 합니다
함수 이름이 특히 중요한 이유는, 읽는 사람이 함수 본문을 열기 전에 기대를 만들기 때문입니다. 그 기대와 실제 동작이 다르면 버그가 생기기 쉽습니다. 대표적인 예가 “원본을 바꾸는 함수”와 “새 값을 반환하는 함수”를 구분하지 않는 경우입니다.
fun orderByCreatedAt(orders: MutableList<Order>) {
orders.sortBy { it.createdAt }
}
fun orderByCreatedAt(orders: List<Order>): List<Order> {
return orders.sortedBy { it.createdAt }
}
이렇게 이름이 같으면 호출부에서 혼란이 생깁니다. 하나는 원본을 바꾸고, 다른 하나는 새 리스트를 돌려주기 때문입니다. 이름에 차이를 주면 의도가 바로 드러납니다.
fun sortOrdersByCreatedAt(orders: MutableList<Order>) {
orders.sortBy { it.createdAt }
}
fun sortedOrdersByCreatedAt(orders: List<Order>): List<Order> {
return orders.sortedBy { it.createdAt }
}
이 원칙은 조회 계열에도 그대로 적용됩니다. 예를 들어 어떤 함수가 없으면 null을 반환하는지, 예외를 던지는지, 기본값을 채우는지에 따라 이름을 더 분명하게 가져가는 편이 좋습니다. Kotlin 표준 라이브러리의 first와 firstOrNull, run과 runCatching이 좋은 힌트가 됩니다.
fun findUserOrNull(userId: Long): User? {
return userRepository.findByIdOrNull(userId)
}
fun requireUser(userId: Long): User {
return userRepository.findByIdOrNull(userId)
?: throw IllegalArgumentException("사용자를 찾을 수 없습니다. id=$userId")
}
이름만 읽어도 “없을 수 있는 함수”와 “반드시 있어야 하는 함수”가 구분되면, 호출부에서의 실수도 줄어듭니다.
4. Boolean 이름은 질문처럼 읽히게 만드세요
Boolean 값은 특히 이름이 중요합니다. 이름이 애매하면 조건문에서 읽는 속도가 크게 떨어집니다. 코틀린에서는 is, has, can, should 같은 접두어를 자주 사용하는 이유가 여기에 있습니다.
val login = user.lastLoginAt != null
val done = order.status == OrderStatus.COMPLETED
val notAvailable = product.stock == 0
if (!notAvailable) {
// ...
}
의미는 알 수 있지만, 머릿속에서 한 번 더 해석해야 합니다. 아래처럼 바꾸면 조건문 자체가 훨씬 자연스럽게 읽힙니다.
val isLoggedIn = user.lastLoginAt != null
val isCompleted = order.status == OrderStatus.COMPLETED
val isOutOfStock = product.stock == 0
val isAvailable = product.stock > 0
if (isAvailable) {
// ...
}
특히 부정형 Boolean + 부정 연산자의 조합은 피하는 편이 좋습니다. if (!isNotAvailable)보다 if (isAvailable)가 훨씬 빨리 읽히기 때문입니다.
5. Boolean 파라미터는 이름만큼 호출 방식도 중요합니다
함수 이름을 잘 지어도, Boolean 파라미터가 여러 개 있으면 호출부에서 의미가 사라질 수 있습니다. 이런 경우에는 파라미터 이름을 분명하게 짓고, 호출할 때 named argument를 함께 쓰는 편이 안전합니다.
fun sendWelcomeEmail(
userId: Long,
sendCoupon: Boolean,
saveHistory: Boolean,
) {
// ...
}
sendWelcomeEmail(1L, true, false)
위 호출부는 함수 구현을 열어보지 않으면 true, false가 무엇을 뜻하는지 알기 어렵습니다. 아래처럼 쓰면 읽는 비용이 크게 줄어듭니다.
sendWelcomeEmail(
userId = 1L,
sendCoupon = true,
saveHistory = false,
)
이 부분은 네이밍과 직접 연결됩니다. 파라미터 이름이 읽기 좋지 않으면 named argument를 써도 효과가 반감되기 때문입니다.
6. 컬렉션 이름, 개수 이름, 단일 객체 이름을 구분하세요
코드 리뷰에서 자주 보는 문제 중 하나가, 단수·복수 구분이 흐려지는 경우입니다. 컬렉션인데 단수형을 쓰거나, 개수인데 size 같은 모호한 이름으로 끝내면 문맥을 놓치기 쉽습니다.
val user = userRepository.findActiveUsers()
val size = user.size
val idList = user.map { it.id }
이 코드는 돌아가지만, user가 한 명인지 여러 명인지 이름만 보면 바로 알기 어렵습니다. 아래처럼 바꾸면 흐름이 분명해집니다.
val activeUsers = userRepository.findActiveUsers()
val activeUserCount = activeUsers.size
val activeUserIds = activeUsers.map { user -> user.id }
이 규칙은 사소해 보이지만, 컬렉션 처리 코드에서는 누적해서 큰 차이를 만듭니다. 특히 람다 안팎에서 변수 이름이 비슷하게 겹칠 때 더 중요합니다.
7. 프로젝트 전체에서 같은 의미에는 같은 단어를 쓰세요
좋은 이름은 하나만 잘 짓는다고 끝나지 않습니다. 팀과 프로젝트 전체에서 용어를 일관되게 맞춰야 효과가 커집니다. 같은 대상을 어떤 곳에서는 user, 어떤 곳에서는 member, 또 다른 곳에서는 account라고 부르면, 읽는 사람은 차이가 있는지부터 의심하게 됩니다.
class UserService {
fun findMember(id: Long): User? = TODO()
fun deactivateAccount(id: Long) = TODO()
fun sendUserWelcomeEmail(id: Long) = TODO()
}
이 코드는 각 이름이 완전히 틀렸다고 보긴 어렵지만, 한 클래스 안에서 용어가 섞여 있다는 점이 문제입니다. 같은 도메인 개념이라면 하나의 단어로 통일하는 편이 좋습니다.
class UserService {
fun findUser(id: Long): User? = TODO()
fun deactivateUser(id: Long) = TODO()
fun sendUserWelcomeEmail(id: Long) = TODO()
}
비슷한 맥락에서 item, element, entry, entity를 뒤섞는 습관도 주의하셔야 합니다. 팀에서 한 번 용어를 정했다면, 관련 API와 변수명도 함께 맞춰가는 편이 좋습니다.
실무에서 자주 보이는 네이밍 실수
의미 없는 접미사를 너무 많이 붙이는 경우
Manager, Service, Util, Info, Data 같은 단어는 때로 필요하지만, 자주 남용됩니다. 문제는 이런 단어가 “무슨 책임을 가지는지”를 충분히 설명하지 못할 때가 많다는 점입니다.
class PaymentManager
class PaymentData
class PaymentInfo
object PaymentUtil
조금만 더 구체화하면 코드 읽기가 훨씬 쉬워집니다.
class PaymentAuthorizer
data class PaymentSummary(
val amount: Int,
val approvedAt: LocalDateTime?,
)
class PaymentReceiptFormatter
object PaymentMaskingPolicy
핵심은 “그 객체가 실제로 무슨 일을 하는지”를 이름에 남기는 것입니다. 이름이 구체화되면 자연스럽게 책임도 분명해집니다.
팀 내부 약어를 과하게 사용하는 경우
모든 약어가 나쁜 것은 아닙니다. URL, HTTP, XML처럼 널리 알려진 약어는 충분히 사용할 수 있습니다. 하지만 팀 내부에서만 통하는 축약어를 코드 전반에 퍼뜨리면, 시간이 지나서 이해 비용이 커집니다.
val usrNm = request.unm
val prdAmt = item.amt
val ordDt = LocalDate.now()
몇 글자 줄이자고 의미를 희생하면, 결국 더 비싼 비용을 치르게 됩니다.
null 반환, 예외 발생, 기본값 반환이 이름에서 구분되지 않는 경우
이 문제는 네이밍과 오류 처리 규칙이 겹치는 지점입니다. 예를 들어 어떤 함수는 없으면 null을 반환하고, 어떤 함수는 예외를 던지는데 둘 다 getUser라고 부르면 호출부에서 실수하기 쉽습니다.
반환 정책이 다르면 이름도 함께 맞춰가는 편이 좋습니다. 예를 들어 findUserOrNull, requireUser, getUserOrDefault처럼 규칙을 정해두면 팀 전체가 같은 기대를 공유하기 쉬워집니다.
이름으로 설명해야 할 것을 주석으로 대신하는 경우
아래처럼 주석이 이름을 대신하는 코드도 자주 보입니다.
// 결제 완료 주문만 합산한다
fun process(list: List<Order>): Int {
return list.filter { it.status == OrderStatus.PAID }
.sumOf { it.totalPrice }
}
주석이 나쁜 것은 아니지만, 이런 경우라면 주석보다 이름을 먼저 바꾸는 편이 더 좋습니다.
fun calculatePaidOrderTotal(orders: List<Order>): Int {
return orders.filter { order -> order.status == OrderStatus.PAID }
.sumOf { order -> order.totalPrice }
}
주석은 이름과 구조만으로는 전달되지 않는 맥락을 설명할 때 더 가치가 있습니다. 이름으로 충분히 설명 가능한 내용이라면, 우선 이름부터 개선해보시는 편이 좋습니다.
좋은 이름은 한 번에 완성되지 않습니다
실무에서는 처음부터 완벽한 이름을 짓기 어렵습니다. 그래서 중요한 것은 “처음에 맞혔느냐”보다, 구현이 구체화되면서 이름도 함께 더 정확하게 바꿔갈 수 있느냐입니다. 처음에는 saveUser로 시작했더라도, 나중에 실제 책임이 “신규 회원 등록”으로 좁혀졌다면 registerUser가 더 나은 이름이 될 수 있습니다.
좋은 이름을 짓기 위한 가장 현실적인 방법은, 코드를 다 작성한 뒤 이름만 다시 훑어보는 시간을 따로 가지는 것입니다. 함수 본문을 다 쓴 다음에도 이름이 여전히 추상적이라면, 그 함수가 실제로 무엇을 하는지 더 정확하게 반영하도록 바꾸셔야 합니다. 네이밍은 구현 전에 끝나는 일이 아니라, 구현을 이해한 뒤 다시 맞춰가는 작업에 가깝습니다.
이름 짓기 체크리스트
- 이름만 읽어도 무엇을 담는 값인지, 무엇을 하는 함수인지 떠오르는지 확인합니다.
- 타입 설명보다 도메인 의미가 먼저 드러나는 이름인지 점검합니다.
- 클래스는 명사형, 함수는 동사형으로 읽히는지 확인합니다.
- 원본을 바꾸는 함수와 새 값을 반환하는 함수의 이름이 구분되는지 봅니다.
- Boolean 이름이
is,has,can,should처럼 질문 형태로 자연스럽게 읽히는지 점검합니다. - 컬렉션은 복수형, 개수는
Count나Total처럼 의미가 드러나는 이름인지 확인합니다. - 같은 개념에 대해
user,member,account처럼 용어가 섞여 있지 않은지 살펴봅니다. Util,Manager,Info같은 넓은 이름으로 책임을 흐리고 있지 않은지 확인합니다.- 테스트 코드가 아니라면 언더스코어나 문장형 이름을 무분별하게 쓰고 있지 않은지 점검합니다.
- 주석이 이름을 대신하고 있다면, 주석보다 이름부터 먼저 개선할 수 없는지 생각해봅니다.
마무리
코틀린 네이밍 컨벤션은 단순히 형식을 맞추기 위한 규칙이 아닙니다. 패키지는 소문자로, 클래스는 UpperCamelCase로, 함수와 프로퍼티는 lowerCamelCase로 쓰는 규칙 자체도 중요하지만, 그보다 더 중요한 것은 이름이 의도를 얼마나 정확하게 드러내는가입니다. 좋은 이름은 주석을 줄이고, 리뷰를 빠르게 만들고, 수정 실수를 줄여줍니다.
이번 글에서는 코틀린 공식 규칙 위에 실무적인 원칙을 겹쳐서 정리해봤습니다. 결국 핵심은 복잡한 기술이 아닙니다. 짧은 이름보다 오해 없는 이름, 멋진 이름보다 예측 가능한 이름을 선택하시는 것이 좋습니다.
다음 글에서는 코틀린 함수 설계를 다뤄보겠습니다. 함수 길이, 매개변수 수, early return, expression body, 작은 함수 분리 기준까지 이어서 정리해드리겠습니다.
코틀린 클린코드 시리즈 이어서 보기
이 글을 기준으로 앞뒤 흐름을 연결하면 내용이 더 잘 잡힙니다. 아래 글을 이어서 읽어보세요.