TL;DR
Repository 메서드명을 getXXX()대신 findByXXXOrNull()로 작성한 이유는, 메서드명만 보고도 null 가능성을 인지하고 안전하게 처리하도록 유도하기 위함이었다.
# Repository 네이밍 컨벤션
## 조회 메서드
- findByXxxOrNull(): null을 반환할 수 있는 조회
- getByXxx(): 없으면 예외를 던지는 조회
## 예시
- findByUsernameOrNull("user123") → User? 반환
- getByUsername("user123") → User 반환 또는 예외
자연스럽게 작성했던 getUser()
interface UserRepository {
fun getUser(username: String): User?
}
"유저를 가져온다(get)" - 말이 되는 것 같았다. Java에서도 getter를 getXXX()로 작성하니까 익숙했고, 별다른 의문 없이 넘어갔다.
하지만 테스트 코드를 작성하면서 계속 이런 코드를 보게 되었다
@Test
fun test() {
val user = userRepository.getUser("testuser")
// 어? user가 null일 수도 있는데...
assertThat(user).isNotNull()
assertThat(user?.username).isEqualTo("testuser")
}
메서드명 getUser()만 봐서는 null을 반환할 수 있다는 사실이 전혀 드러나지 않았다.
문제 인식
null 가능성이 숨겨진 메서드명
// 기존 코드
fun getUserByUsername(username: String): User? {
val user = userRepository.getUser(username) // ← null일 수 있다는 게 안 보임
return user
}
물론 반환 타입이 User? 이니까 IDE가 경고를 해주긴 한다. 하지만 코드를 읽는 사람 입장에서, 메서드명만으로는 "이 메서드가 null을 반환할 수 있구나"를 바로 알기 어렵다. (특히 자바)
특히 팀 프로젝트나 시간이 지난 후 코드를 다시 볼 때, 메서드명은 가장 먼저 보는 정보다. 메서드명에 의도가 드러나지 않으면, 매번 시그니처를 확인해야 한다.
해결 / 탐색
Spring Data JPA의 컨벤션
interface UserJpaRepository : JpaRepository<User, Long> {
// Spring Data JPA는 findBy를 사용
fun findByUsername(username: String): User?
}
Spring Data JPA는 조회 메서드에 findBy를 사용한다. 이유가 있을까?
찾아보니, Spring 생태계에서는 다음과 같은 암묵적 규칙이 있었다
| 메서드 접두사 | 의미 | Null 가능성 |
|---|---|---|
get |
반드시 값을 반환 | 없음 - 없으면 예외 발생 |
find |
값을 찾아서 반환 | 있음 - 없으면 null/empty |
// get: 없으면 예외를 던진다
fun getUser(id: Long): User // User 타입 (non-null)
// find: 없으면 null을 반환한다
fun findUser(id: Long): User? // User? 타입 (nullable)
결정
findByXxxOrNull 네이밍 선택
이 컨벤션을 알고 나니, 내 코드에도 일관성 있게 적용하고 싶었다. 하지만 단순히 findByUsername()보다 더 명확하게 하고 싶었다.
Kotlin의 표준 라이브러리를 보니 좋은 패턴이 있었다
// Kotlin stdlib
list.first() // 없으면 예외
list.firstOrNull() // 없으면 null
OrNull 접미사를 붙여서 "없을 수도 있다"는 의미를 메서드명에 명시하는 패턴이다.
이 패턴을 Repository에 적용하기로 결정했다
interface UserRepository {
fun save(user: User): User
// ✅ find + OrNull로 null 가능성 명시
fun findByUsernameOrNull(username: String): User?
fun findByIdOrNull(id: Long): User?
}
적용 결과
Before: 메서드명에서 의도가 불명확
// ❌ getUser()는 null을 반환할 수 있다는 게 드러나지 않음
@Test
fun test() {
val user = userRepository.getUser("testuser")
// user가 null일까? 아닐까? 메서드 시그니처를 봐야 안다
}
After: 메서드명만으로 null 가능성 인지
// ✅ findByUsernameOrNull()은 이름만 봐도 null 가능성이 명확
@Test
fun test() {
val user = userRepository.findByUsernameOrNull("testuser")
assertThat(user).isNotNull()
}
// Service 레이어
fun getUserByUsername(username: String): User? {
return userRepository.findByUsernameOrNull(username)
}
예외 처리 패턴과의 조화
// Case 1: null을 허용하는 경우
fun getPointOrNull(username: String): Point? {
val user = userRepository.findByUsernameOrNull(username) ?: return null
return pointRepository.findByUserIdOrNull(user.id)
}
// Case 2: 반드시 있어야 하는 경우
fun chargePoint(username: String, amount: Long): Point {
val user = userRepository.findByUsernameOrNull(username)
?: throw CoreException(ErrorType.NOT_FOUND, "존재하지 않는 사용자입니다.")
val point = pointRepository.findByUserIdOrNull(user.id)
?: Point.of(userId = user.id, initialAmount = 0L)
point.charge(amount)
return pointRepository.save(point)
}
메서드명에 OrNull이 명시되어 있으니, Elvis 연산자(?:)와 함께 사용하기도 직관적이다.
프로젝트 전체에 적용
// UserRepository
interface UserRepository {
fun save(user: User): User
fun findByUsernameOrNull(username: String): User?
fun findByIdOrNull(id: Long): User?
}
// PointRepository
interface PointRepository {
fun save(point: Point): Point
fun findByUserIdOrNull(userId: Long): Point?
}
규칙이 일관되니 코드 전체의 예측 가능성이 높아졌다. 새로운 Repository 메서드를 추가할 때도 고민할 필요가 없다
- null을 반환할 수 있으면 → findByXxxOrNull()
- 무조건 값을 반환하거나 예외를 던지면 → getByXxx()
배운 점
네이밍은 단순한 취향이 아니다
1. 메서드명은 가장 먼저 보는 문서다
fun getUser(id: Long): User?
fun findByIdOrNull(id: Long): User?
2. 컨벤션을 따르면 소통 비용이 줄어든다
Spring Data JPA와 Kotlin stdlib의 패턴을 따르니, 별도 설명 없이도 의도가 전달되었다.
3. 작은 디테일이 안전성을 만든다
val user = userRepository.findByUsernameOrNull(username)
?: throw CoreException(ErrorType.NOT_FOUND, "존재하지 않는 사용자")
배운점
네이밍은 중요하다
getUser() vs findByUsernameOrNull() - 단순히 이름 차이일 뿐이지만, 이 작은 차이가 코드의 안전성과 가독성에 큰 영향을 미쳤다.
"메서드 이름이야 어떻게 짓든 동작은 똑같은 거 아니야?"라고 생각할 수도 있다. 하지만 코드는 사람이 읽는것..
메서드명 하나로 null 처리를 유도하고, 버그를 예방하고, 코드 리뷰 시간을 줄일 수 있다면 충분히 고민할 가치가 있다고 생각한다.