728x90

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 순으로 로직을 처리했습니다.

 

입금과 출금 테스트

728x90

'Go > Go - web' 카테고리의 다른 글

Go - Response body 를 Close 해야하는 이유  (0) 2022.04.26
Go - Restful 하게 micro web service 만들기  (0) 2021.10.10