인프라/Architecture

조회 메서드 네이밍 컨벤션 getXXX vs findXXX

dami97 2025. 10. 31. 17:01
반응형

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 처리를 유도하고, 버그를 예방하고, 코드 리뷰 시간을 줄일 수 있다면 충분히 고민할 가치가 있다고 생각한다.