Easy Understanding

스프링부트로 페이 시스템 구축 도전해보기(초보 난이도) (4) - 송금 시스템 개발(by Kotlin) 본문

Spring

스프링부트로 페이 시스템 구축 도전해보기(초보 난이도) (4) - 송금 시스템 개발(by Kotlin)

appleg1226 2020. 10. 26. 22:09

초보 주의

예전 개발 경험이 적을 때 작성한 거라서 지금 저의 시점으로 보니 너무 도움이 안 되는 글이네요...

솔직히 지우고 싶으나 풋풋함을 기억하기 위해서 남겨둡니다. 

(심지어 지금 결제 도메인 개발 중이라 더 부끄러움)

 

Why Kotlin?

Kotlin은 Java의 뒤를 이어서 Java를 대체할 수 있는 언어로 나온 핫한 언어입니다.

안드로이드에서는 공식 언어로 지원을 받고 있을 정도로 검증된 언어이기도 합니다.

 

- 요새 많은 회사들에서 Java에서 Kotlin으로의 전환을 고려하고 있다는 점

- Kotlin의 활용성이 최신 언어적 특성을 반영한 트렌디한 언어라는 점

- Java와의 높은 호환성

- 그리고 기존 Springboot 프레임워크를 사용하여 러닝커브가 낮다는 점

 

이런 점들을 고려하여 Kotlin을 공부하고 이번 시스템에 간단하게 적용해 보았습니다.


송금 시스템 동작 순서

일단 송금 시스템은 다음의 순서로 진행된다고 이전 글에서 정리를 했습니다.

 

1. 한 유저가 다른 유저에게 돈을 송금한다.(이 과정에서 인증이 이루어진다)

   이 때 보낸 유저의 페이머니에서는 송금 금액을 빼지만, 받은 유저는 아직은 송금 금액을 받지 못한다.

2. 받은 유저가 승인을 누르는 순간, 보낸 유저에게는 승인 알림이, 받은 유저는 송금 금액을 받게 된다.

3. 만약 받는 유저가 일정 시간 내에 승인을 누르지 않을 경우 알림을 보냅니다.

 

그래서 저는 (1) 송금 요청 API, (2) 송금 완료 API

이렇게 두 가지를 만드는 것이 이번 프로젝트의 목적입니다.

 

'(3) 받지 않을 시 알림 보내기'의 경우는 이후에 다른 프레임워크 라이브러리를 적용하여 구현할 예정입니다.

 


프로젝트를 시작하기 전에 Kotlin 코드를 썼을 때 어떤 점이 다른가를 먼저 알아보고 넘어가겠습니다.

Kotlin으로 했을 때 Java와 같은 점

1. 패키지/클래스 스타일을 똑같게 가져갈 수 있습니다.

기존에 자바로 Spring 프로젝트를 했던 구조와 똑같이 가져갔습니다.

controller, service, repository, domain의 전형적인 폴더 구조를 이용했습니다.

 

 

2. 스프링에서 쓰던 라이브러리를 그대로 쓸 수 있다.

기본적인 자료구조는 살짝 다르지만,

java8에서 쓸 수 있는 온갖 라이브러리들(날짜 등)을 간단하게 import해서 사용 가능합니다.

 

Kotlin으로 했을 때 Java와 다른 점

1. 당연하게도 클래스 선언 / 변수 선언 등 세부 문법이 다르다.

이건 아래 세부 코드에서 설명을 하겠습니다. 

틀이 비슷하지만, 세부적으로 코틀린만의 특징이 있습니다.

 

2. 생성자 관련 코드가 달라서 Lombok 같은 utility가 필요없다.

코틀린은 객체를 생성하는 부분이 살짝 다릅니다.

그래서 constructor를 생성하는 데 noarg plugin 등을 적용해야 하는 등 셋팅이 필요합니다.

 

 

이외에도 많지만 이번 프로젝트에서 느꼈던 특징 몇 가지만 정리했습니다.


Domain 세부 코드

enum class EXCHANGE_TYPE {
    PAYMENT, SEND
}

@Document
data class Exchange (
    @Id
    var paymentId: String = "",
    var exchangeType: EXCHANGE_TYPE? = null,
    var myId: String = "",
    var otherId: String = "",
    var money: Long = 0,
    var exchangeDate: LocalDateTime? = null,
    var isComplete: Boolean = false
)

- 송금 내역을 저장하는 도메인 클래스입니다.

 

- enum class: 자바의 enum과 비슷합니다. class라는 단어가 들어갔다는 차이밖에 없습니다.

 

- data class: toString, equals, hashcode 등이 자동으로 포함된 클래스 선언입니다.

 

- 변수 관련: "var 변수이름: 변수타입 = 초기화코드" 

   맨 앞에는 var/val이 들어가는데, var는 가변 val은 불변인 변수 앞에 붙입니다.

   그리고 그 뒤에 변수 이름이 먼저 들어가고, 그 뒤로 콜론과 변수 타입이 들어옵니다.

   자바와 반대네요.

   

- 그리고 변수 타입에는 null이 되어도 되는 것에 ?를 붙여놨습니다.

   여기에선 변수들을 초기화 해줘야합니다.(안 해줘도 되는데 이럴 경우 noarg 플러그인을 설정해줘야 합니다.)

 

- 생성자는?: 위의 선언을 보시면 몸통부(중괄호)가 없습니다. 원래 없는 게 아니라, 생략할 수 있습니다.

 

Repository 세부 코드

interface ExchangeRepisotory : MongoRepository<Exchange, String>

- 'implements MongoRepository<>'는 ': MongoRepository<>' 와 동일합니다. 

 

- 몸체가 없을 경우 중괄호를 생략할 수 있습니다.

 

 Service 세부 코드

@Service
class TransferHandler @Autowired constructor(val exchangeRepository: ExchangeRepisotory, val payUserRepository: PayUserRepository){

    companion object{
        private val log = LoggerFactory.getLogger(TransferHandler::class.java)
    }

    fun transferMoneyRequest(transferRequest: TransferRequest): Exchange {
        val exchage = Exchange(paymentId = exchangeIdGenerator(LocalDateTime.now()),
                exchangeType = EXCHANGE_TYPE.SEND,
                myId = transferRequest.sendUserId,
                otherId = transferRequest.receiveUserId,
                money = transferRequest.money,
                isComplete = false,
                exchangeDate = LocalDateTime.now())

        saveExchangeRecord(exchage)
        updateSendUser(transferRequest.sendUserId, transferRequest.money)
        log.info("송금이 정상 요청 되었습니다. 상대방이 24시간 내에 수락하면 송금이 완료됩니다.")
        return exchage
    }

    fun decideTransfer(paymentId: String){
        val exchange = exchangeRepository.findByIdOrNull(paymentId) ?: throw NoSuchElementException("Not Found Exchange")
        updateExchangeComplete(exchange)
        updateReceiveUser(exchange.otherId, exchange.money)
        log.info("송금이 전부 완료 되었습니다.")
    }

    private fun saveExchangeRecord(exchange: Exchange){
        exchangeRepository.save(exchange)
        log.info("saved exchange: " + exchange.paymentId)
    }

    private fun exchangeIdGenerator(dateTime: LocalDateTime): String {
        return dateTime.year.toString() +
                dateTime.monthValue +
                dateTime.dayOfMonth +
                UUID.randomUUID().toString().substring(0, 7)
    }

    private fun updateExchangeComplete(exchange: Exchange){
        exchange.isComplete = true
        exchangeRepository.save(exchange)
        log.info("거래가 정상 완료 되었습니다.")
    }

    private fun updateSendUser(userId: String, money: Long){
        val userInfo = payUserRepository.findByIdOrNull(userId) ?: throw NoSuchElementException("Not Found User")
        userInfo.payMoney -= money
        payUserRepository.save(userInfo)
        log.info("송금 요청: 금액이 계좌에서 빠져나감")
    }

    private fun updateReceiveUser(userId: String, money: Long){
        val userInfo = payUserRepository.findByIdOrNull(userId) ?: throw NoSuchElementException("Not Found User")
        userInfo.payMoney += money
        payUserRepository.save(userInfo)
        log.info("송금 완료: 금액이 계좌로 들어옴")
    }
}

- 생성자 주입: JAVA에서는 생성자 주입을 private final과 롬복의 @RequiredArgsConstructor로 했었습니다.

   여기에서는 비슷한 역할을 @Autowired constructor(val exchangeRepository: ExchangeRepisotory)로 구현하면

   됩니다.

 

- companion object: 클래스 내/외부적으로 static과 똑같은 효과를 보고싶은 것들을 저 안에 넣으면 됩니다.

  그렇게 되면 다른 함수들에서 java의 static처럼 사용할 수 있습니다. 

  제가 사용한 것은 spring에 내장되어 있는 slf4j logger입니다.

 

- transferMoneyRequest: 송금 요청이 들어오면 해당 메서드에서 요청을 처리합니다.

   먼저 요청을 토대로 거래 내역을 객체로 만든 후 document로 저장을 한 다음,

   나의 페이머니만 차감을 시킵니다. 

 

- decideTransfer: 송금 요청을 받은 상대방이 송금을 승인하면, 거래를 완료 상태로 바꾸고 

   받는 사람의 페이머니를 증가시킵니다.

Controller 세부 코드

@RestController
class DefaultController @Autowired constructor(val transferHandler: TransferHandler){

    companion object{
        private val log = LoggerFactory.getLogger(DefaultController::class.java)
    }

    @PostMapping("/transfer")
    fun transferRequest(@RequestBody transferRequest: TransferRequest): ResponseEntity<Exchange>{
        val result = transferHandler.transferMoneyRequest(transferRequest)
        return ResponseEntity(result, HttpStatus.OK)
    }

    @PostMapping("/transfer/complete/{pay-id}")
    fun decideTransfer(@PathVariable("pay-id") paymentId: String): ResponseEntity<String>{
        transferHandler.decideTransfer(paymentId)
        return ResponseEntity("complete", HttpStatus.OK)
    }
}

- 코틀린의 문법에만 맞춰서 controller 메서드를 만들었습니다.

- 위에서 service 메서드를 생성자 주입해주고,

- 각 메서드에서는 서비스 메서드들을 호출했으며, 매개변수-반환 클래스도 이전 스프링에서 만든 것과 차이가 없습니다. 


Kotlin 사용 후기

코틀린은 간단한 스프링 프로젝트를 구현하는 데에는 자바와 큰 차이점이 없어서 큰 거부감이 들지 않았습니다.

그래서 앞으로 부담감 없이 더 도전해볼 수 있는 여지가 남았고 더욱 시도해 볼 생각입니다.

 

더 구현해야 하는 것

만약 송금을 했는데 상대방이 받지 않아서 취소되는 상황이라면?

그리고 데이터베이스를 더욱 빨리 접근할 수 있는 상황을 만들고 싶다면 

아직 해당 기능들은 더욱 보완할 필요가 있습니다.

 

그것들을 다음 포스트에서 진행해보도록 하겠습니다. 

 

참고 도서: Kotlin in action, 깡샘의 코틀린 프로그래밍

실제 구현 코드: github.com/appleg1226/sample-pay-system