Skip to content

6. 갑자기 회원 자신이 가진 쿠폰을 주변에게 나눠주고 싶어진다면?

이건창 edited this page May 18, 2024 · 10 revisions

결과물

  • 링크 : #6

개요

선물 기능을 통해 회원간 인터렉션이 됐으면 했다. 이커머스 세상에서는 우린 독립된 구매자로 인식하고 지인이 어떤 행동을 하는지 파악하기 어렵다. 지인이 어떤 걸 주문했는지, 어떤 걸 추천하는지 알아가면 선택에 고민을 줄여 구매 확률을 높일 수 있어보였다.

사용자간 인터렉션을 하기 위한 공간을 만들기 위해서는 쿠폰을 선물하며 서로 친구임을 파악하는건 어떨까. 쿠폰을 나눠주며 이 가게 맛있어!, 굉장한 쿠폰을 받았어!라고 대화하며 독립된 구매자가 아닌 인터렉션하는 구매자로 변신해보는건 어떨까 생각했다.

요구사항은 다음처럼 정의했다.

  • 선물 할 쿠폰과 선물 할 회원의 이름을 입력해 쿠폰을 나눠준다.
    • 회원은 특정 이름을 가진 회원에게 쿠폰을 선물할 수 있다.
    • 선물받는 회원은 가게 est-delivery 회원이어야 한다.
    • 선물하는 쿠폰은 사용하지 않은 쿠폰이어야 한다.
    • 선물하는 쿠폰은 받은 쿠폰 중 하나여야 한다.
    • 쿠폰을 가진 회원은 쿠폰을 선물하면 해당 쿠폰을 더 이상 사용 할 수 없도록 삭제해야 한다.
    • 쿠폰을 선물받은 회원은 사용하지 않은 쿠폰북에 새롭게 선물받은 쿠폰이 추가된다.

유스케이스의 pseudo 코드를 작성하면 다음과 같다.

  1. 이름으로 회원을 조회한다.
  2. 선물 할 쿠폰을 조회한다.
  3. 선물받을 회원 쿠폰북에 해당 쿠폰을 추가한다.
  4. 선물한 회원 쿠폰북에 해당 쿠폰을 삭제한다.

검증 조건은 다음과 같다.

  1. 회원이 검색되지 않으면 쿠폰을 선물할 수 없다.
  2. 쿠폰이 존재해야 하고 사용하지 않은 쿠폰이어야 한다.
  3. 동일한 회원이어서는 안된다.

구현

갑자기 유스케이스에 서비스 로직이 안들어가는게 좋지 않을까 하는 생각이 들었다. 굳이 의미가 뻔한 로직들은 외부로 걷어내보려 한다. 이렇게 작성해보는건 어떨까? 그럼 유스케이스에서 포트를 남발하느 일은 없을테다.

class GiftCouponService(
    loadMemberStatePort: LoadMemberStatePort,
    loadCouponStatePort: LoadCouponStatePort,
    updateMemberStatePort: UpdateMemberStatePort,
    private val getReceiver: (GiftCouponCommand) -> Member = { loadMemberStatePort.findById(it.receiverId).toMember() },
    private val getSender: (GiftCouponCommand) -> Member = { loadMemberStatePort.findById(it.senderId).toMember() },
    private val getCoupon: (GiftCouponCommand) -> Coupon = { loadCouponStatePort.findByCouponId(it.couponId).toCoupon() },
    private val updateMember: (Member) -> Unit = { updateMemberStatePort.update(MemberState.from(it)) }
) : GiftCouponUseCase {
    /**
     * 선물 할 쿠폰과 선물 할 회원의 이름을 입력해 쿠폰을 나눠준다.
     *
     * 1. 이름으로 회원을 조회한다.
     * 2. 선물 할 쿠폰을 조회한다.
     * 3. 선물받을 회원 쿠폰북에 해당 쿠폰을 추가한다.
     * 4. 선물한 회원 쿠폰북에 해당 쿠폰을 삭제한다.
     *
     * @param giftCouponCommand 선물할 쿠폰 정보와 선물할 회원 정보
     */
    override fun giftCoupon(giftCouponCommand: GiftCouponCommand) {
        val sender = getSender(giftCouponCommand)
        val receiver = getReceiver(giftCouponCommand)
        val coupon = getCoupon(giftCouponCommand)
        receiver.receiveCoupon(coupon)
        sender.sendCoupon(coupon, receiver)
        updateMember(receiver)
        updateMember(sender)
    }
}

사실 이전부터 트랜잭션을 유스케이스에서 노출하고 싶지 않았다. 그리고 트랜잭션 범위를 조절 하면 좋겠다. 그래서 일부 영역만을 트랜잭션 처리할 수 있는 코드를 생성했다.

@Component
class TransactionArea {
    @Transactional
    fun <T> run(supplier: () -> T): T {
        return supplier()
    }
}

그럼 유스케이스에서는 다음처럼 트랜잭션을 영역으로 처리할 수 있다. 작업 영역은 좁을 수록 커넥션 유지 시간이 줄어들고 영역에 빼도 동작이 달라지지 않기 때문에 받을 사람을 찾는 로직은 트랜잭션에서 제외했다.

override fun giftCoupon(giftCouponCommand: GiftCouponCommand) {
    val receiver = getReceiver(giftCouponCommand)

    transactionArea.run {
        val sender = getSender(giftCouponCommand)
        val coupon = getCoupon(giftCouponCommand)
        receiver.receiveCoupon(coupon)
        sender.sendCoupon(coupon, receiver)
        updateMember(receiver)
        updateMember(sender)
    }
}

마지막으로

헥사고날의 장점을 직접 이해하기 위해 새로운 기능을 추가해보려 한다. 5. 첫 번째 구현 완료에서 이야기 했듯이 헥사고날의 장점은 유스케이스 추가에 유연하다는 점이다. 이런 장점을 잘 활용할 수 있을지 직접 경험해보려고 쿠폰 서비스를 제공한다면 이런 기능을 사용해보는게 어떨까 싶은걸 가져와봤다.

결과적으로 유스케이스가 쉽게 추가가 됐다. 심지어 인프라 영역은 손도 대지 않았다. 이번 검증 과정을 통해 헥사고날 아키텍처의 장점을 제대로 느낄 수 있었다. 다음 기능은 횡단적 관심사 추가, 스키마 변경, 모듈 분리 등을 추가해서 진행해볼 예정이다.

아쉬운점은 쿠폰을 선물할 때 보내는 회원이 쿠폰을 전송하도록 행위를 추가하려면 직접 전달하도록 구현했는데 괜히 복잡하게 만든게 아닌가 싶다.

// ASIS
receiver.receiveCoupon(coupon)
sender.useCoupon(coupon)

// TOBE 
sender.sendCoupon(coupon, receiver)