2024. 9. 9. 16:50ㆍTechnology/Others
T라 미숙해 (feat. 제네릭, 어디까지 써봤니?) - 1
T라 미숙해 (feat. 제네릭, 어디까지 써봤니?) - 1
Event 페이지나 Lounge 페이지에서 일어나는 행위를 Log로 남겨야 하는 요구사항이 있었다. 여러 가지 방법이 있었겠지만, 계속 뒤바뀌는 상황에서 일단 RDB에 적당히 일반화된 포맷으로 저장하도록
zins.tistory.com
T라 미숙해 (feat. 제네릭, 어디까지 써봤니?) - 2
T라 미숙해 (feat. 제네릭, 어디까지 써봤니?) - 2
T라 미숙해 (feat. 제네릭, 어디까지 써봤니?) - 1 T라 미숙해 (feat. 제네릭, 어디까지 써봤니?) - 1Event 페이지나 Lounge 페이지에서 일어나는 행위를 Log로 남겨야 하는 요구사항이 있었다. 여러 가지
zins.tistory.com
2편을 통해 왜 추상화를 시작했고, 어떤 결과가 나왔는지 충분히 설명이 된 것 같다. 하지만 그에 그치지 않고 createDispatch()도 Dispatch로 추상화된 EmailDispatch, SmsDispatch를 만드는 똑같은 구조의 모습이기 때문에, 이 부분까지 추상화가 가능할지 연구해보고 적용해보았다.
DispatchService의 register에서 Dispatch를 만들기 위해 사용되는 createDispatch()는 EmailService, SmsService가 각자 override 하도록 아래와 같이 구성했었다.
DispatchService.kt
@Transactional
abstract class DispatchService<D : Dispatch<A, P>, A : Account, P : Purpose>(
private val dispatchRepository: DispatchRepository<D, A, P>,
) {
fun register(
account: A,
message: String,
purpose: P,
): D {
val dispatch: D = createDispatch(account, message, purpose)
return dispatchRepository.save(dispatch)
}
// ..중략
abstract fun createDispatch(
account: A,
message: String,
purpose: P,
): D
}
EmailService.kt
@Service
class EmailService(
emailRepository: EmailRepository,
) : DispatchService<EmailDispatch, EmailAddress, EmailPurpose>(emailRepository) {
override fun createDispatch(
account: EmailAddress,
message: String,
purpose: EmailPurpose,
): EmailDispatch {
return EmailDispatch.create(account, message, purpose)
}
}
SmsService.kt
@Service
class SmsService(
smsRepository: SmsRepository,
) : DispatchService<SmsDispatch, PhoneNumber, SmsPurpose>(smsRepository) {
override fun createDispatch(
account: PhoneNumber,
message: String,
purpose: SmsPurpose,
): SmsDispatch {
return SmsDispatch.create(account, message, purpose)
}
}
EmailDispatch와 SmsDispatch의 companion object에 구현한 create() 메소드를 사용하는 부분을 CreateDispatch라는 interface로 뽑아서 create() 메소드가 상속받아 사용하게 하고, DispatchService가 이를 사용하도록 해볼 것이다.
companion object의 create() 메소드가 상속받을 CreateDispatch라는 interface를 만든다.
CreateDispatch.kt
interface CreateDispatch<D : Dispatch<A, P>, A : Account, P : Purpose> {
fun create(
account: A,
message: String,
purpose: P,
): D
}
반환 값인 EmailDispatch와 SmsDispatch는 Dispatch로 추상화했고, EmailAddress와 PhoneNumber는 Account로, EmailPurpose와 SmsPurpose는 Purpose로 추상화해둔 덕분에 위와 같은 구조로 설계할 수 있었다.
위 메소드를 아래처럼 companion object가 override 할 수 있다.
EmailDispatch.kt
@Entity
class EmailDispatch(
id: Long? = null,
account: EmailAddress,
message: String,
purpose: EmailPurpose,
) : Dispatch<EmailAddress, EmailPurpose>(id, account, message, purpose) {
companion object : CreateDispatch<EmailDispatch, EmailAddress, EmailPurpose> {
override fun create(
account: EmailAddress,
message: String,
purpose: EmailPurpose,
) = EmailDispatch(account = account, message = message, purpose = purpose)
}
}
SmsDispatch.kt
@Entity
class SmsDispatch(
id: Long? = null,
account: PhoneNumber,
message: String,
purpose: SmsPurpose,
) : Dispatch<PhoneNumber, SmsPurpose>(id, account, message, purpose) {
companion object : CreateDispatch<SmsDispatch, PhoneNumber, SmsPurpose> {
override fun create(
account: PhoneNumber,
message: String,
purpose: SmsPurpose,
) = SmsDispatch(account = account, message = message, purpose = purpose)
}
}
이제 CreateDispatch의 create() 메소드가, 그러니까 EmailDispatch와 SmsDispatch의 companion object 메소드의 create() 메소드가 아래처럼 쓰이도록 변경해볼 것이다.
DispatchService.kt
@Transactional
abstract class DispatchService<D : Dispatch<A, P>, A : Account, P : Purpose>(
private val dispatchRepository: DispatchRepository<D, A, P>,
) {
fun register(
account: A,
message: String,
purpose: P,
): D {
// 이 부분에서 사용되도록 해보자
val dispatch: D = create(account, message, purpose)
return dispatchRepository.save(dispatch)
}
// ..생략
}
1단계. 사용할 companion objcet의 메소드가 구현된 대상이 Dispatch라는 것을 인식하여 담을 수 있는 공간을 만든다.
private var dispatchClass: KClass<D>
2단계. 그리고 init 단계에서 그 공간에 DispatchService를 정의할 때 첫 번째로 구성한 D : Dispatch를 담도록 한다.
@Transactional
// 1. D : Dispatch<A, P>라고 첫 번째로 정의되어 있으니
abstract class DispatchService<D : Dispatch<A, P>, A : Account, P : Purpose>(
private val dispatchRepository: DispatchRepository<D, A, P>,
) {
init {
(javaClass.genericSuperclass as ParameterizedType).actualTypeArguments.also { typeArguments ->
// 2. typeArguments 중 0번째를 Class<D>로 꺼낸다
dispatchClass = (typeArguments[0] as Class<D>).kotlin
}
}
private var dispatchClass: KClass<D>
// ..생략
}
3단계. 이제 DispatchService를 구현할 때 D : Dispatch 자리에 들어오는 대상이 Dispatch Class 타입이라는 것을 인식할 수 있으니, 아래처럼 그 Class의 companion object 메소드를 쓰도록 instance를 꺼낸다.
@Transactional
abstract class DispatchService<D : Dispatch<A, P>, A : Account, P : Purpose>(
private val dispatchRepository: DispatchRepository<D, A, P>,
) {
init {
(javaClass.genericSuperclass as ParameterizedType).actualTypeArguments.also { typeArguments ->
dispatchClass = (typeArguments[0] as Class<D>).kotlin
}
}
private var dispatchClass: KClass<D>
fun register(
account: A,
message: String,
purpose: P,
): D {
// 1. 인식한 dispatchClass의 companionObject를 꺼낸다
val companionObject: KClass<*> = dispatchClass.companionObject!!
// 2. objectInstance를 CreateDispatch로 캐스팅하여 꺼낸다
val instance = companionObject.objectInstance!! as CreateDispatch<D, A, P>
// 3. 여기서 사용하면 끝!
val dispatch: D = instance.create(account, message, purpose)
return dispatchRepository.save(dispatch)
}
// ..생략
}
위와 같이 추상화를 하면, DispatchService의 createDispatch()는 삭제해도 된다. 또한 아래처럼 EmailService와 SmsService가 매우 간단해진다.
EmailService.kt
@Service
class EmailService(
emailRepository: EmailRepository,
) : DispatchService<EmailDispatch, EmailAddress, EmailPurpose>(emailRepository)
SmsService.kt
@Service
class SmsService(
smsRepository: SmsRepository,
) : DispatchService<SmsDispatch, PhoneNumber, SmsPurpose>(smsRepository)
완성된 전체 코드는 아래 링크에서 확인할 수 있다.
https://github.com/iamzin/Example-Generic
GitHub - iamzin/Example-Generic
Contribute to iamzin/Example-Generic development by creating an account on GitHub.
github.com
또 다른 이번 추상화의 장점은, KakaoAlimTalk이나 WahtsApp과 같이 다른 Dispatch가 생겨도 일관성 있게 간결한 코드를 유지할 수 있다는 점이다.
2편에서도 강조했지만, 예제처럼 간단한 상황이거나 무조건적으로 추상화를 해야한다는 것은 아니다. 현업 코드는 비즈니스 상황에 따라 더욱 복잡하고 변화할 가능성이 높을 것이기 때문에 이를 고려하여 적절히 판단해야 한다.
시간이 주어지거나 병행이 가능한 상황이라면 적당한 상황에 최선의 판단을 통해 개선하는 작업도 틈틈이 해보는 것을 추천한다. 그 시간과 기회가 더 나은 개발을 할 수 있는 노력에 투자할 수 있는 타이밍이라고 생각한다.
앞으로 또 어떤 상황에 어떤 고민이 들지, 어떻게 해결해나갈 수 있을지 기대하며 T라 미숙했던 제네릭 적응기를 마쳐본다!😌
'Technology > Others' 카테고리의 다른 글
T라 미숙해 (feat. 제네릭, 어디까지 써봤니?) - 2 (1) | 2024.09.08 |
---|---|
T라 미숙해 (feat. 제네릭, 어디까지 써봤니?) - 1 (3) | 2024.09.08 |
선배 Proxy 고려해서 equals 오버라이드 해주세요, 탕탕 후루후루~ (2) | 2024.07.26 |
Test Code 500개 도달 여정 (feat. DIP, 맞다이로 드루와) (5) | 2024.05.14 |
Controller 먼저? Domain 먼저? (0) | 2024.02.10 |