35장. Repository 설계하기
도메인 객체를 만들었으니
이제 그것을 어딘가에 저장해야 합니다.
데이터를 저장하고 꺼내는 역할을
Repository가 맡습니다.
이 장에서는 Repository를
어떻게 설계하는지 배웁니다.
핵심은 8장에서 배운
“인터페이스로 경계를 만드는” 것입니다.
35.1 Repository가 필요한 이유
만약 Service가
저장 방식을 직접 알고 있다면 어떨까요?
// 나쁜 예: Service가 저장 방식에 직접 의존
class UserService {
fun register(user: User) {
val connection = database.connect() // 저장 방식이 코드에 박힘
// ...
}
}
이러면 저장 방식이 바뀔 때마다
Service를 통째로 고쳐야 합니다.
Repository는
“어떻게 저장하는가“를 숨겨 줍니다.
Service는 “저장한다“는 사실만 알면 됩니다.
Service는 “무엇을 할지“에 집중하고,
Repository는 “어떻게 저장할지“를 담당합니다.
35.2 Repository Interface
Repository는 인터페이스로 정의합니다.
interface UserRepository {
fun save(user: User): User
fun findById(id: Long): User?
fun findAll(): List<User>
}
우리 예제에는 세 종류가 필요합니다.
UserRepository
OrderRepository
ProductRepository
인터페이스에는
“무엇을 할 수 있는가“만 적습니다.
“어떻게 하는가“는 적지 않습니다.
조회 결과가 없을 수 있으므로findById의 반환 타입은 User?입니다. (5장 null)
35.3 Memory Repository 만들기
가장 간단한 구현은
메모리(Map)에 저장하는 것입니다.
데이터베이스 없이도
서비스를 동작시키고 테스트할 수 있습니다.
class MemoryUserRepository : UserRepository {
private val storage = mutableMapOf<Long, User>()
private var sequence = 0L
override fun save(user: User): User {
val id = if (user.id == 0L) ++sequence else user.id
val saved = user.copy(id = id)
storage[id] = saved
return saved
}
override fun findById(id: Long): User? = storage[id]
override fun findAll(): List<User> = storage.values.toList()
}
mutableMapOf로 저장소를 만들고, (12장)copy로 id를 채운 새 객체를 저장합니다. (9장)
여기서 배운 문법들이
자연스럽게 쓰이고 있습니다.
35.4 조회 결과와 Nullable
조회는 “없을 수도 있는” 작업입니다.
그래서 반환 타입을 nullable로 둡니다.
override fun findById(id: Long): User? = storage[id]
Service에서는
없는 경우를 어떻게 다룰지 결정합니다. (24장)
val user = userRepository.findById(id)
?: throw UserNotFoundException(id)
“없음“을 null로 정직하게 표현하고,
그 처리를 Service에 맡기는 것입니다.
이것이 5장에서 배운
null 안전성이 실무에 녹아드는 지점입니다.
35.5 Repository 구현체 교체하기
인터페이스로 나눈 덕분에
구현체를 자유롭게 바꿀 수 있습니다.
UserRepository (인터페이스)
↑
├─ MemoryUserRepository (개발/테스트용)
└─ DatabaseUserRepository (실제 운영용)
Service는 인터페이스만 바라보므로,
어떤 구현체를 넣어도 그대로 동작합니다.
// 테스트에서는 메모리 구현
val service = UserService(MemoryUserRepository())
// 운영에서는 데이터베이스 구현 (나중에)
// val service = UserService(DatabaseUserRepository())
31장에서 Fake로 교체했던 것과
정확히 같은 원리입니다.
35.6 데이터 저장 방식과 비즈니스 로직 분리하기
지금까지의 설계가 주는 선물은 이것입니다.
저장 방식(기술)과
비즈니스 로직(규칙)이 분리된다.
- 도메인/Service는 “규칙“에 집중
- Repository 구현체는 “저장 기술“에 집중
덕분에 이런 일이 가능합니다.
- 데이터베이스를 바꿔도 Service는 그대로
- 메모리 구현으로 빠르게 테스트
- 규칙을 고칠 때 저장 코드를 건드리지 않음
이 분리가 바로
스프링 같은 프레임워크가 권하는 구조입니다.
우리는 그것을 프레임워크 없이 직접 만든 셈입니다.
35장을 마치며
이 장에서 우리는 다음을 배웠습니다.
- Repository가 저장 방식을 숨겨 주는 이유
- 인터페이스로 Repository를 정의하는 법
- 메모리 구현으로 간단히 저장소 만들기
- 조회 결과를 nullable로 정직하게 표현하기
- 구현체 교체와 관심사 분리의 효과
다음 장에서는
이 Repository를 사용하는 Service와
의존성 주입을 다룹니다.