프로젝트에서 회원 가입을 할 경우 포인트 잔액(PointBalance)을 함께 생성해줘야 하는 요구사항이 추가되었다. 즉 Member 엔티티를 생성할 때 PointBalance 엔티티도 함께 생성해줘야 했다. 먼저 회원가입 로직이 어떻게 구성되어 있는지 확인해보자.
@Service
class MemberService(
private val memberRepository: MemberRepository,
) {
@Transactional
fun signUp(newMember: NewMember) {
val member = memberRepository.save(
Member(
email = newMember.email,
password = newMember.password,
nickname = newMember.nickname,
role = newMember.role,
)
)
}
}
간단하게 생각해보면 MemberService에 PointBalanceRepository를 추가하고 함께 생성해주면 될 것 같다.
@Service
class MemberService(
private val memberRepository: MemberRepository,
private val pointBalanceRepository: PointBalanceRepository,
) {
@Transactional
fun signUp(newMember: NewMember) {
val member = memberRepository.save(
Member(
email = newMember.email,
password = newMember.password,
nickname = newMember.nickname,
role = newMember.role,
)
)
pointBalanceRepository.save(
PointBalance(
memberId = member.id,
)
)
}
}
즉 Member 도메인이 PointBalance 도메인을 의존하게 되는 구조다. 이 경우 PointBalance가 변경될 경우 PointBalance뿐만 아니라 Member도 수정이 필요해진다.
나는 Member와 PointBalance의 관계에 대해 먼저 정의했다.
- Member는 PointBalance보다 더 많은 곳에서 범용적으로 사용된다.
- Member가 PointBalance를 의존할 경우 PointBalance가 변경될 때마다 Member도 영향을 받는다는 의미다.
- 영향을 받은 Member가 변경되면 Member를 의존하는 수많은 도메인들도 영향을 받는다. 즉 변경이 전파된다.
- 따라서 PointBalance가 Member를 의존해야 하고, Member는 PointBalance를 모르는 게 더 변경에 유연한 구조다.
앞서 본 코드처럼 MemberService가 PointBalance를 아는 구조에서, pointBalance에 member의 nickname을 기록해야 하는 요구사항이 추가된다면 어떻게 될까? PointBalance에 nickname 필드가 추가되면서 변경사항이 생기고, 이를 의존하는 MemberService에서도 nickname을 넣어줘야 한다. 즉 하나의 요구사항으로 두 곳을 수정하게 된다.
@Transactional
fun signUp(newMember: NewMember) {
val member = memberRepository.save(
Member(
email = newMember.email,
password = newMember.password,
nickname = newMember.nickname,
role = newMember.role,
)
)
pointBalanceRepository.save(
PointBalance(
memberId = member.id,
// +닉네임 추가
nickname = member.nickname,
)
)
}
이처럼 덜 중요한 도메인을 변경할 때 더 중요한 도메인에 영향을 주는 것을 막으려면 PointBalance가 Member를 아는 구조가 되어야 한다. 그러기 위해서 인터페이스를 이용한 방법과 이벤트를 사용한 방법이 있는데, 필자는 다음과 같은 이유로 인터페이스를 선택했다.
- 프로젝트의 규모가 작다(단일 서버).
- PointBalance 생성은 반드시 되어야 함(한 트랜잭션으로 묶고 싶음).
MemberSignedUpHandler 인터페이스를 만들고, 회원가입할 때 다른 도메인이 수행해야 하는 작업이 있다면 이를 구현하도록 하였다.
fun interface MemberSignedUpHandler {
fun onSignedUp(member: Member)
}
@Component
class PointBalanceInitializer(
private val pointBalanceRepository: PointBalanceRepository,
) : MemberSignedUpHandler {
override fun onSignedUp(member: Member) {
pointBalanceRepository.save(
PointBalance(
memberId = member.id,
)
)
}
}
@Service
class MemberService(
private val memberRepository: MemberRepository,
private val memberSignedUpHandlers: List<MemberSignedUpHandler>,
) {
@Transactional
fun signUp(newMember: NewMember) {
val member = memberRepository.save(
Member(
email = newMember.email,
password = newMember.password,
nickname = newMember.nickname,
role = newMember.role,
)
)
memberSignedUpHandlers.forEach { it.onSignedUp(member) }
}
}
이렇게 하면 PointBalance의 요구사항이 추가/변경되어도 Member는 PointBalance를 모르기 때문에 변경되지 않는다. 도메인의 의존 방향을 역전시킴으로써 변경이 전파되는 문제를 해결했다. 도메인 간 결합도를 낮추는 방법 중 이벤트를 활용하는 방법도 있는데 이는 다음에 다루도록 하겠다.