Go - 입금 및 출금 API 구현 하기 (transaction && rollback)
banking app 에서 customer id 와 account id 를 받아서 돈을 입금 또는 출금 하는 API 를 만들어 보겠습니다.
Router Handler 만들기
router.HandleFunc("/customers/{customer_id:[0-9]+}/account/{account_id:[0-9]+}", ah.MakeTransaction).
Methods(http.MethodPost)
customer id 와 acccount id 를 정규표현식으로 숫자만 받도록 정의하고 AccountHandler 객체를 만들고 MakeTransaction 을 HandleFunc 로 등록 했습니다.
Db 객체와 Handler 객체를 각각 파라미터로 의존성 주입하는 형태로 코딩을 하여 테스트 또는 변경되는 사항에 유연하게 대처 할수 있도록 했습니다.
Domain 정의 하기
domain 이란 쉽게 말해 비즈니스 로직을 구조체로 표현 한것입니다. dto (Data transfer object) 는 Client 의 Reqeust 와 Response 에 대한 구조체 정의라고 보는게 쉬울거 같습니다. 더좋은 의견이 있으시다면 댓글로 달아주세요
type Account struct {
AccountId string `db:"account_id"`
CustomerId string `db:"customer_id"`
OpeningDate string `db:"opening_date"`
AccountType string `db:"account_type"`
Amount float64 `db:"amount"`
Status string `db:"status"`
}
Go lang 은 Tagging 을 통해서 db 나 json 별칭을 달아줘서 마샬링 언마샬링을 좀더 쉽게 하도록 도와줍니다.
Dto 정의하기
dto/transaction.go
package dto
import (
"bangking/errs"
)
const WITHDRAWAL = "withdrawal"
const DEPOSIT = "deposit"
type TransactionRequest struct {
AccountId string `json:"account_id"`
Amount float64 `json:"amount"`
TransactionType string `json:"transaction_type"`
TransactionDate string `json:"transaction_date"`
CustomerId string `json:"-"`
}
func (r TransactionRequest) IsTransactionTypeWithdrawal() bool {
if r.TransactionType == WITHDRAWAL {
return true
}
return false
}
func (r TransactionRequest) Validate() *errs.AppError {
if r.TransactionType != WITHDRAWAL && r.TransactionType != DEPOSIT {
return errs.NewValidationError("Transaction type can only be deposit or withdrawal")
}
if r.Amount < 0 {
return errs.NewValidationError("Amount cannot be less than zero")
}
return nil
}
type TransactionResponse struct {
TransactionId string `json:"transaction_id"`
AccountId string `json:"account_id"`
Amount float64 `json:"new_balance"`
TransactionType string `json:"transaction_type"`
TransactionDate string `json:"transaction_date"`
}
Reqeust 와 Response 로 Client 와의 통신 객체 규격을 정했습니다.
그리고 Validate 라는 receiver 를 만들어서 Type check 와 잔고가 0 이하 일때는 출금 할수 없도록 validation check 를 합니다.
Repository recevier 만들기
/**
* transaction = make an entry in the transaction table + update the balance in the accounts table
*/
func (d AccountRepositoryDb) SaveTransaction(t Transaction) (*Transaction, *errs.AppError) {
// starting the database transaction block
tx, err := d.client.Begin()
if err != nil {
logger.Error("Error while starting a new transaction for bank account transaction: " + err.Error())
return nil, errs.NewUnexpectedError("Unexpected database error")
}
// inserting bank account transaction
result, _ := tx.Exec(`INSERT INTO transactions (account_id, amount, transaction_type, transaction_date)
values (?, ?, ?, ?)`, t.AccountId, t.Amount, t.TransactionType, t.TransactionDate)
// updating account balance
if t.IsWithdrawal() {
_, err = tx.Exec(`UPDATE accounts SET amount = amount - ? where account_id = ?`, t.Amount, t.AccountId)
} else {
_, err = tx.Exec(`UPDATE accounts SET amount = amount + ? where account_id = ?`, t.Amount, t.AccountId)
}
// in case of error Rollback, and changes from both the tables will be reverted
if err != nil {
tx.Rollback()
logger.Error("Error while saving transaction: " + err.Error())
return nil, errs.NewUnexpectedError("Unexpected database error")
}
// commit the transaction when all is good
err = tx.Commit()
if err != nil {
tx.Rollback()
logger.Error("Error while commiting transaction for bank account: " + err.Error())
return nil, errs.NewUnexpectedError("Unexpected database error")
}
// getting the last transaction ID from the transaction table
transactionId, err := result.LastInsertId()
if err != nil {
logger.Error("Error while getting the last transaction id: " + err.Error())
return nil, errs.NewUnexpectedError("Unexpected database error")
}
// Getting the latest account information from the accounts table
account, appErr := d.FindBy(t.AccountId)
if appErr != nil {
return nil, appErr
}
t.TransactionId = strconv.FormatInt(transactionId, 10)
// updating the transaction struct with the latest balance
t.Amount = account.Amount
return &t, nil
}
func (d AccountRepositoryDb) FindBy(accountId string) (*Account, *errs.AppError) {
sqlGetAccount := "SELECT account_id, customer_id, opening_date, account_type, amount from accounts where account_id = ?"
var account Account
err := d.client.Get(&account, sqlGetAccount, accountId)
if err != nil {
logger.Error("Error while fetching account information: " + err.Error())
return nil, errs.NewUnexpectedError("Unexpected database error")
}
return &account, nil
}
type AccountRepositoryDb struct {
client *sqlx.DB
}
SaveTransaction receiver 는 *sqlx.DB 를 타입으로 가지는 객체를 파라미터로 받습니다.
tx, err := d.client.Begin()
d.client.Begin() 으로 commit 또는 Update 를 하다 실패했을 시 rollback 하기 위함으로 tx 라는 객체를 만들었습니다.
if t.IsWithdrawal() {
_, err = tx.Exec(`UPDATE accounts SET amount = amount - ? where account_id = ?`, t.Amount, t.AccountId)
} else {
_, err = tx.Exec(`UPDATE accounts SET amount = amount + ? where account_id = ?`, t.Amount, t.AccountId)
}
type 이 withdrawal 인경우 출금임으로 잔액에서 - 하고 diposit 일경우 입금 이기때문에 + 합니다.
쭉 내려가면서 에러 체크를 하고 에러가 없을 경우
transactionId, err := result.LastInsertId()
if err != nil {
logger.Error("Error while getting the last transaction id: " + err.Error())
return nil, errs.NewUnexpectedError("Unexpected database error")
}
// Getting the latest account information from the accounts table
account, appErr := d.FindBy(t.AccountId)
if appErr != nil {
return nil, appErr
}
t.TransactionId = strconv.FormatInt(transactionId, 10)
// updating the transaction struct with the latest balance
t.Amount = account.Amount
return &t, nil
result 에서 LastInsertId 로 마지막 삽입된 Id 값을 받아옵니다. 그 Id 값으로 FindBy 를 조회한다음 account 객체를 리턴받습니다.
t <- Transaction 객체에 Id를 마지막 삽입된 Id 값으로 할당하고 Amount 를 업데이트 한뒤 서비스로 넘겨줍니다.
Service 구현하기
package service
import (
"bangking/domain"
"bangking/dto"
"bangking/errs"
"time"
)
const dbTSLayout = "2006-01-02 15:04:05"
type AccountService interface {
NewAccount(request dto.NewAccountRequest) (*dto.NewAccountResponse, *errs.AppError)
MakeTransaction(request dto.TransactionRequest) (*dto.TransactionResponse, *errs.AppError)
}
type DefaultAccountService struct {
repo domain.AccountRepository
}
//
.....
.....
//
func (s DefaultAccountService) MakeTransaction(req dto.TransactionRequest) (*dto.TransactionResponse, *errs.AppError) {
// incoming request validation
err := req.Validate()
if err != nil {
return nil, err
}
// server side validation for checking the available balance in the account
if req.IsTransactionTypeWithdrawal() {
account, err := s.repo.FindBy(req.AccountId)
if err != nil {
return nil, err
}
if !account.CanWithdraw(req.Amount) {
return nil, errs.NewValidationError("Insufficient balance in the account")
}
}
// if all is well, build the domain object & save the transaction
t := domain.Transaction{
AccountId: req.AccountId,
Amount: req.Amount,
TransactionType: req.TransactionType,
TransactionDate: time.Now().Format(dbTSLayout),
}
transaction, appError := s.repo.SaveTransaction(t)
if appError != nil {
return nil, appError
}
response := transaction.ToDto()
return &response, nil
}
request 로 넘어온 값을 처음 Validate receiver 로 validation 체크합니다.
error 가 없다면 account 에서 출금이 가능한지 확인합니다. 이상이 없다면 t 라는 변수에 domain Transaction 를 만들어 할당합니다.
그리고 SaveTransaction 으로 거래를 저장합니다. 저장된후 값은 domain 객체 이기 때문에 Response 로 응답하기 위해서 ToDto() 함수를 통해 convert 해줍니다.
func (t Transaction) ToDto() dto.TransactionResponse {
return dto.TransactionResponse{
TransactionId: t.TransactionId,
AccountId: t.AccountId,
Amount: t.Amount,
TransactionType: t.TransactionType,
TransactionDate: t.TransactionDate,
}
}
이렇게하여 Client 에서 DTO -> servcie(domain) -> DTO -> client 순으로 로직을 처리했습니다.
입금과 출금 테스트
'Go > Go - web' 카테고리의 다른 글
Go - Response body 를 Close 해야하는 이유 (0) | 2022.04.26 |
---|---|
Go - Restful 하게 micro web service 만들기 (0) | 2021.10.10 |
댓글
이 글 공유하기
다른 글
-
Go - Response body 를 Close 해야하는 이유
Go - Response body 를 Close 해야하는 이유
2022.04.26 -
Go - Restful 하게 micro web service 만들기
Go - Restful 하게 micro web service 만들기
2021.10.10