π€ Facade? κ·Έκ±° μ°λ¦¬ νμ¬μμ κ·Έλ₯ 'ApiService'λΌκ³ λΆλ λλ°
TL;DR π‘ μ€λ¬΄μμ μ°λ ν¨ν΄μ μ΄λ¦μ΄ μμλ€. μ©μ΄λ₯Ό μκ³ λλ μ€λͺ μ΄ μ¬μμ§κ³ , ν μ€νΈλ μ¬μμ‘λ€.
π€ μ΄λ―Έ νκ³ μμλλ°, μ΄λ¦μ λͺ°λλ€
λΆνΈμΊ ν κ³Όμ λ‘ User APIλ₯Ό ꡬννλ©΄μ Facade ν¨ν΄μ μ μ©νλ€. κ·Έλ°λ° μ½λλ₯Ό μ§λ€ 보λ μ΄μνκ² μ΅μνλ€.
Facadeλ νλμ€μ΄λ‘ β건물μ μ λ©΄/μΈκ΄βμ΄λΌλ λ»μ΄λ€. 건물 μλ©΄μ κΉλν΄ λ³΄μ΄μ§λ§, λ€μλ 볡μ‘ν κ΅¬μ‘°κ° μ¨μ΄μλ€. λμμΈ ν¨ν΄μμλ κ°μ μλ―Έλ‘, 볡μ‘ν μλΈμμ€ν μ λ¨μν μΈν°νμ΄μ€λ‘ κ°μΈλ μν μ νλ€.
βμ΄? μ΄κ±° νμ¬μμ 맨λ νλ 건λ°?β π
νμ¬μμλ κ·Έλ₯ βμλΉμ€ μ‘°ν©νλ ν΄λμ€β μ λλ‘ λΆλ λ€. OrderFacade κ°μ μ΄λ¦ λμ OrderApiService, BuyerApiService μ΄λ° μμΌλ‘ XxxApiServiceλΌκ³ λΆλ λ€. νλ μΌμ λκ°μλ€:
- μ¬λ¬ Serviceλ₯Ό μ‘°ν©
- νΈλμμ κ²½κ³ κ΄λ¦¬
- DTO λ³ν
μ΄μ νμ¬μμλ Controller β Service β Repositoryλ‘ λ¨μνκ² μΌλλ°, μ§κΈ νμ¬μμ XxxApiService ꡬ쑰λ₯Ό μ²μ μ νλ€. 1λ
κ°κΉμ΄ μ΄ κ΅¬μ‘°λ‘ μ½λλ₯Ό μ§°λλ°, βFacade ν¨ν΄βμ΄λΌλ μ΄λ¦μ΄ μλ μ€ λͺ°λλ€.
β¨ μ©μ΄λ₯Ό μκ³ λλ λ¬λΌμ§ κ²
1. π£οΈ μ€λͺ μ΄ μ¬μμ‘λ€
μμ μλ μ΄λ° μμΌλ‘ μ€λͺ νλ€:
βμ΄ ν΄λμ€λ Serviceλ€μ μ‘°ν©ν΄μ Controllerμ μ λ¬νλ μν μ΄μμβ
μ§κΈμ:
βμ΄κ±΄ Facadeμμ. 볡μ‘ν μλΈμμ€ν μ λ¨μν μΈν°νμ΄μ€λ‘ κ°μΈλ κ±°μ£ β
ν λ¨μ΄λ‘ μλκ° μ λ¬λλ€.
2. ποΈ κ΅¬μ‘°κ° λͺ νν΄μ‘λ€
1
Controller β Facade β Service β Repository
Facadeκ° λ νλ λ μ΄μ΄μΈμ§ μ΄λ¦λ§ λ΄λ μ μ μλ€.
3. π§ͺ ν μ€νΈ κ²½κ³κ° 보μλ€
Facadeλ₯Ό μκ³ λλ βμ¬κΈ°μ λ ν μ€νΈν΄μΌ νμ§?βκ° λͺ νν΄μ‘λ€.
- Service ν μ€νΈ: λΉμ¦λμ€ λ‘μ§ (Fake Repositoryλ‘ λ¨μ ν μ€νΈ)
- Facade ν μ€νΈ: Service μ‘°ν©, DTO λ³ν (νμνλ©΄)
- E2E ν μ€νΈ: HTTP β DB μ 체 νλ¦
π DIPλ λ§μ°¬κ°μ§μλ€
Repository μΈν°νμ΄μ€λ₯Ό Domain λ μ΄μ΄μ λλ ꡬ쑰λ νμ¬μμ μ°λ λ°©μμ΄μλ€.
1
2
3
4
5
// Domain Layer
interface UserRepository {
fun findByLoginId(loginId: String): User?
fun save(user: User): User
}
1
2
3
4
5
6
7
8
// Infrastructure Layer
class UserRepositoryImpl(
private val jpaRepository: UserJpaRepository
) : UserRepository {
override fun findByLoginId(loginId: String): User? =
jpaRepository.findByLoginId(loginId)
// ...
}
μμ μλ βJPA μμ‘΄μ± λΆλ¦¬νλ €κ³ μ΄λ κ² νλ κ±°μΌβλΌκ³ μ€λͺ νλ€. μ§κΈμ βDIP(μμ‘΄μ± μμ μμΉ) μ μ©ν κ±°μΌβλΌκ³ λ§ν μ μλ€.
κ·Έλ¦¬κ³ μ΄ κ΅¬μ‘° λλΆμ ν
μ€νΈν λ FakeUserRepositoryλ₯Ό μ½κ² λΌμλ£μ μ μμλ€:
1
2
3
4
5
6
7
8
9
10
11
12
13
// ν
μ€νΈμ© Fake ꡬν체
class FakeUserRepository : UserRepository {
private val storage = mutableMapOf<Long, User>()
private var sequence = 1L
override fun save(user: User): User {
val id = sequence++
val savedUser = user.copy(id = id) // μ€μ μ μ₯μ²λΌ id μ±λ²
storage[id] = savedUser
return savedUser
}
// ...
}
1
2
3
4
5
6
7
8
// Mock λ°©μ
val userRepo = mock<UserRepository>()
whenever(userRepo.findByLoginId("test")).thenReturn(User(...))
whenever(userRepo.save(any())).thenReturn(User(...))
// Fake λ°©μ
val userRepo = FakeUserRepository()
userRepo.save(User(...))
π₯ μ΄λ² κ³Όμ μμ κ²ͺμ μ€μ κ³ λ―Ό
β οΈ νΈλμμ κ²½κ³ λ¬Έμ
λΉλ°λ²νΈ λ³κ²½ APIλ₯Ό λ§λ€λ©΄μ μ½μ§νλ€. μ²μ ꡬ쑰λ μ΄λ¬λ€:
1
2
3
4
5
// Facade
fun changePassword(loginId: String, loginPw: String, command: ChangePasswordCommand) {
val user = userService.authenticate(loginId, loginPw) // readOnly νΈλμμ
userService.changePassword(user, command) // λ€λ₯Έ νΈλμμ
}
authenticate()κ°@Transactional(readOnly = true)λΌμ, λ°νλ User μν°ν°κ° Detached μνκ° λλ€. μ΄νchangePassword()μμ μμ ν΄λ DBμ λ°μμ΄ μ λλ€.
κ²°κ΅ Service λ©μλ μκ·Έλμ²λ₯Ό λ°κΏμ ν΄κ²° β
1
2
3
4
5
6
7
// Service
@Transactional
fun changePassword(loginId: String, loginPw: String, command: ChangePasswordCommand) {
val user = findAndValidate(loginId, loginPw) // κ°μ νΈλμμ
λ΄μμ μ‘°ν
user.changePassword(passwordEncoder.encode(command.newPassword))
// Dirty CheckingμΌλ‘ μλ UPDATE
}
μ²μμ Facadeμ @Transactionalμ λΆμ¬μ μ 체λ₯Ό νλμ νΈλμμ
μΌλ‘ λ¬ΆμκΉ κ³ λ―Όνλ€. νμ§λ§ Facadeκ° νΈλμμ
μ κ΄λ¦¬νλ©΄ Service λ¨μ ν
μ€νΈκ° μ΄λ €μμ§ κ² κ°μμ, Service λ΄λΆμμ ν΄κ²°νλ λ°©ν₯μ μ ννλ€.
findAndValidate()λ authenticate()μ μ€λ³΅λμ§λ§, private λ©μλλ‘ μΆμΆν΄μ DRY μμΉμ μ§μΌ°λ€.
λμ€μ μ΄λ²€νΈ κΈ°λ°μΌλ‘ νμ₯νλ€λ©΄ μ΄λ»κ² ν΄μΌ ν μ§λ μμ§ λͺ¨λ₯΄κ² λ€. νΈλμμ κ²½κ³λ₯Ό μ΄λμ λμ§, μ΄λ²€νΈ λ°ν μμ μ μΈμ λ‘ ν μ§ λ± κ³ λ―Όμ΄ λ νμν κ² κ°λ€.
π λ§μ€νΉ λ‘μ§ μμΉ
μ΄λ¦ λ§μ€νΉμ μ΄λμ λμ§λ κ³ λ―Όμ΄μλ€. βνκΈΈλβ β βνκΈΈ*β λ³ννλ λ‘μ§μΈλ°:
- Aμ: Controller/DTOμμ μ²λ¦¬
- Bμ: Facade/Application Layerμμ μ²λ¦¬
- Cμ: Entityμ λ©μλλ‘ μ μ
κ²°κ΅ Cμμ μ ννλ€ π
1
2
// User μν°ν°
fun maskedName(): String = if (name.length <= 1) "*" else name.dropLast(1) + "*"
1
2
3
4
5
6
7
8
9
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Presentation Layer (Controller, DTO) β β Aμ: μ¬κΈ°μ λ§μ€νΉ?
βββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Application Layer (Facade, UserInfo) β β Bμ: μ¬κΈ°μ λ§μ€νΉ?
βββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Domain Layer (Entity, Service) β β Cμ: μ¬κΈ°μ λ§μ€νΉ? β
βββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Infrastructure Layer (Repository, JPA) β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
βνν λ°©μβμ΄λκΉ μμͺ½ λ μ΄μ΄μ λ μλ μμ§λ§, βμΈλΆμ μ΄λ¦μ μ΄λ»κ² 보μ¬μ€μ§βλ User λλ©μΈμ΄ μμμΌ ν μ§μμ΄λΌκ³ νλ¨ν΄μ Entityμ λλ€. μ λ΅μΈμ§λ λͺ¨λ₯΄κ² μ§λ§.
π λ°°μ΄ μ
μμ μμ λͺ¨λ¦μμ ν° λͺ¨λ¦μΌλ‘ λμκ°λ κ³Όμ μ΄λ€.
βκ·Έλ₯ μ΄λ κ² νλ©΄ νΈνλλΌβλ‘ μ½λλ₯Ό μ§°λ€. Facade, DIP κ°μ μ©μ΄λ₯Ό μκ³ λλ μ€νλ € λͺ¨λ₯΄λ κ² λ λ§μμ‘λ€. νΈλμμ μ νλ μ΄λ»κ² ν΄μΌ νμ§? μ΄λ²€νΈ κΈ°λ°μΌλ‘ νμ₯νλ©΄? ν μ€νΈ κ²½κ³λ μ΄λκΉμ§?
κ·Έλλ μ©μ΄λ₯Ό μκ³ λλ:
- λ€λ₯Έ κ°λ°μμ μν΅μ΄ μ¬μμ‘λ€
- ꡬ쑰λ₯Ό μ€λͺ ν λ κ·Όκ±°κ° μκ²Όλ€
- βμ μ΄λ κ² νλ κ±°μΌ?βμ λλ΅ν μ μκ² λλ€
μ΄λ―Έ μλ κ²μ μ΄λ¦μ λΆμ΄λ, λͺ°λλ κ²λ€μ΄ 보μ΄κΈ° μμνλ€.
λ€μμ λκ° βFacadeκ° λμΌ?βλΌκ³ λ¬ΌμΌλ©΄ μ΄λ κ² λλ΅ν κ² κ°λ€:
βμ¬λ¬ Service μ‘°ν©ν΄μ Controllerμ μ λ¬νλ κ·Έ ν΄λμ€ μμμ. κ·Έκ±°.β
