Post

πŸ€” Facade? κ·Έκ±° 우리 νšŒμ‚¬μ—μ„œ κ·Έλƒ₯ 'ApiService'라고 λΆˆλ €λŠ”λ°

πŸ€” 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에 μ „λ‹¬ν•˜λŠ” κ·Έ 클래슀 μžˆμž–μ•„. κ·Έκ±°.”

This post is licensed under CC BY 4.0 by the author.