정상우
hELLO.
정상우
전체 방문자
347,834
오늘
919
어제
691
  • hELLO. (120)
    • 컴퓨터과학 (4)
      • 알고리즘 & 자료구조 (4)
    • 언어 & 프레임워크 (63)
      • Go (23)
      • PHP & Laravel (40)
    • 웹 (7)
    • 블록체인 (12)
      • 메인넷 (9)
      • 암호화폐 플랫폼 (3)
    • 포트폴리오 (10)
    • 칼럼 (19)
      • 에세이 (4)
      • 개발자스럽게 살기 (13)
      • 회고 (2)
    • 티스토리 (5)

블로그 메뉴

  • ⚡ 개발자 이력서
  • 🌟 깃허브
  • 💻 강의
  • ✨ 예제코드
  • ⭐ 브런치
  • 태그 클라우드
  • 방명록

공지사항

  • 2차 도메인을 설정했습니다 ✨

인기 글

  • [Laravel] 라라벨 프레임워크⋯
    2021.06.10
    [Laravel] 라라벨 프레임워크⋯
  • 'REST' 를 보다 'RESTful' 하게⋯
    2021.08.14
    'REST' 를 보다 'RESTful' 하게⋯
  • JWT(JSON Web Token)의 개념부⋯
    2021.07.29
    JWT(JSON Web Token)의 개념부⋯
  • 깃허브를 포트폴리오로 쓰려면⋯
    2021.12.25
    깃허브를 포트폴리오로 쓰려면⋯
  • 암호화폐 트레이딩 봇을 만들었⋯
    2021.05.12
    암호화폐 트레이딩 봇을 만들었⋯

태그

  • 개발
  • 개발 리뷰
  • 라라벨
  • 코딩테스트
  • Algorithm
  • 포트폴리오
  • go
  • 프로그래머스
  • php
  • 블록체인

최근 댓글

  • 공유해주셔서 감사합니다:)) 덕⋯
    wanderlust_sol
  • 감사합니다 ~~ :)
    정상우
  • 고맙습니다 :)
    정상우
  • 자료 받으면서 원래 댓글 잘 안⋯
    뷰스토리_
  • 다크모드 지원하는 스킨 찾고⋯
    PilTok

최근 글

  • 개발자와 엔지니어, 그 사이에서
    2022.05.10
    개발자와 엔지니어, 그 사이에서
  • 아임포트(Iamport)로 결제기능⋯
    2022.04.03
    아임포트(Iamport)로 결제기능⋯
  • 아임포트(Iamport)로 결제기능⋯
    2022.04.01
    아임포트(Iamport)로 결제기능⋯
  • [Laravel] 카페24 호스팅에 라⋯
    2022.03.29
    [Laravel] 카페24 호스팅에 라⋯
  • 2021년 회고―, 성찰
    2021.12.31
    2021년 회고―, 성찰

티스토리

hELLO · Designed By 정상우.
정상우

hELLO.

아임포트(Iamport)로 결제기능 구현하기 - 준비
웹

아임포트(Iamport)로 결제기능 구현하기 - 준비

2022. 4. 1. 17:37

아임포트(Iamport)로 결제기능 구현하기 - 준비

아임포트(Iamport)로 결제기능 구현하기 - 일반결제

 

이번에 국내 서비스용 결제와 관련된 기능을 구현해보면서 사용해본 것이 아임포트(I'mport)다. 라라벨에는 물론 결제를 위한 Laravel Cashier 라는 패키지가 존재하고, Stripe, Paddle 과 연동할 수 있으나 국내 서비스에는 그다지 친화적이지 않은 측면이 있다. 따라서 구축하기도 쉽고 국내 서비스에 친화적인 아임포트를 사용해보기로 했다. 또 다른 국내 친화적인 서비스로는 Payple 이 있는데, 나중에 기회가 되면 써보기로 하자.

 

아임포트는 개발자 친화적으로 문서화가 되어있어서 개발자의 입장에서 볼 때 결제를 구현하기 상당히 수월하게 되어있다. 대표적으로 일반결제, 정기결제, 결제환불 이렇게 세가지로 되어있다. 자세한 내용을 확인하려면 아래의 문서를 참고하자. 우리가 구현할 것은 일반결제 하나 뿐이다.

 

https://docs.iamport.kr/

아임포트

아임포트와 같은 중개자를끼면 PG사의 모듈을 직접 연계했을 때와는 결제과정이 다소 바뀌게 되는데, 그림으로 한 눈에 알 수 있다.

PG사 모듈 직접 연계(좌) / 아임포트(우)

중개자가 있기때문에 과정이 조금은 더 복잡해질 수 있지만, 사실 아임포트를 인터페이스(Interface)의 관점으로 접근해본다면, 다양한 PG사에 연동하고 싶은 경우에 각각이 다른 구현방식을 취할 필요가 없고 개발자의 입장에서는 단순 파라매터만 바꾸는 것으로 사용할 수 있는 것이다.

이미테이션

아임포트 내부에서 하는 일을 코드로 느낌만 간략하게 살펴보면 아래와 같다. 실제로 이렇지는 않을 것이다. 그저 아임포트가 각 PG사와 중개자 역할을 한다는점에 초점을 맞춘다.

 

먼저, 각 KGInicis, KakaoPay 등의 PG사를 각각 만들어주자. 여기서 Context 클래스에 들어갈만한 것들은 카드, 가상계좌 등의 결제방법, 주문가격 등이 프로퍼티로 존재할 수 있다.

class Context 
{
    // 결제방법, 주문가격 등...
}

abstract class Pg {
    /**
     * Request Payment
     * 
     * @param Context $context
     * @return bool
     */
    public abstract function requestPay(Context $context);
}

/**
 * KG이니시스
 */
class KGInicis extends Pg
{
    public function requestPay(Context $context)
    {
        //
    }
}

/**
 * 카카오페이
 */
class KakaoPay extends Pg
{
    public function requestPay(Context $context)
    {
        //
    }
}

그 다음 Iamport 클래스를 하나 만들고 생성자에 사용하고자하는 PG사에 해당하는 클래스를 주입, PG사에서 구현 중인 Pg::requestPay() 를 호출, 고유주문번호를 생성해서 반환한다. 여기까지가 대략적으로 아임포트 내부에서 발생할 수 있는 일이다.

class Iamport 
{
    /**
     * 고유주문번호
     * 
     * @var string $impUid
     */
    private $impUid;

    /**
     * PG
     * 
     * @var Pg $pg
     */
    private $pg;

    /**
     * Create Iamport Instance
     * 
     * @param Pg $pg
     */
    public function __construct(Pg $pg) 
    {
        $this->pg = $pg;
    }

    /**
     * Request Payment
     * 
     * @param Context $context
     * @return string
     */
    public function requestPay(Context $context)
    {
        if (! $this->pg->requestPay($context)) {
            return '';
        }

        // 아임포트 고유 주문번호 생성 및 저장...

        return $this->impUid;
    }
}

이렇게 작성된 모듈을 클라이언트에선 어떻게 해야할까? 그저 아임포트에서 어떤 PG사와 연동할지만 넘겨주고 요청만 보내면 그만이다. 실제로 이러한 사용법은 우리가 아임포트를 클라이언트의 입장에서 사용하는 방법과 아주 유사하다고 볼 수 있다.

$iamport = new Iamport(new KGInicis());

$context = new Context();
if ($impUid = $iamport->requestPay($context)) {
    //
}

라라벨에서 아임포트 연동준비하기

라라벨 8.x 에서 실제 아임포트 모듈을 사용하여 실제를 만들어보자. 라라벨 프로젝트를 만드는 것부터 시작이다.

laravel new iamport-payments

설정

아임포트에 대한 설정을 추가해주자. 아임포트 관리자 콘솔 - 내정보에서 클라이언트 키(Key)와 시크릿 키를 확인할 수 있는데, .env, app/services.php 에 값을 설정해주자.

관리자 콘솔 - 내정보

config/services.php

아래의 설정은 정해진 설정이 아니라 임의로 추가한 것이기 때문에 필요하다면 수정할 수 있다. 이 경우 merchant_id 는 가맹점 식별코드를 말하는데 아임포트 관리자 콘솔에서 볼 수 있다. 마찬가지로 client_id, client_secret 도 확인할 수 있다. pg 의 경우 사용할 PG사의 식별 이름을 적으면 되는데, Javascript SDK 에서 IMP.requestPay() 의 명세에서 pg 항목을 보면 된다.

'iamport' => [
    'merchant_id' => env('IAMPORT_MERCHANT_ID'),
    'pg' => env('IAMPORT_PG'),
    'client_id' => env('IAMPORT_CLIENT_ID'),
    'client_secret' => env('IAMPORT_CLIENT_SECRET')
],

.env

다음과 같이 지정하고, 빈칸에는 관리자 콘솔에서 값을 보고 넣어주자. IAMPORT_PG 에 설정된 html5_inicis 는 KG이니시스를 의미한다.

IAMPORT_MERCHANT_ID=
IAMPORT_PG=html5_inicis
IAMPORT_CLIENT_ID=
IAMPORT_CLIENT_SECRET=

마이그레이션

마이그레이션 테이블은 두 개를 만든다. 예를 들어 쇼핑몰에는 장바구니, 결제 기능이 있는데 장바구니와 결제를 위한 테이블인 orders, payments 테이블을 만들어보자.

payments

payments 테이블에는 결제정보가 들어가게 되며 주문가격, 결제상태 등의 포함된다.

php artisan make:migration create_payments_table --create=payments
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePaymentsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('payments', function (Blueprint $table) {
            $table->uuid('merchant_uid')->primary();
            $table->string('imp_uid')->unique()->nullable();
            $table->unsignedBigInteger('amount')->default(0);
            $table->unsignedBigInteger('cancel_amount')->default(0);
            $table->string('pay_method')->default('card');
            $table->string('status')->default('unpaid');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('payments');
    }
}

merchant_uid 는 앱 내부에서 관리되는 고유의 값이므로 기본키로 설정한다. 값은 auto_increment 보다는 UUID 로 지정되도록 하자. imp_uid 는 결제가 완료되면 아임포트에서 날려주는 결제고유번호이다. 그 외에 amount, pay_method, status 는 각 주문가격, 결제방법, 결제상태를 의미한다. cancel_amount 는 환불된 금액을 의미한다. 이 또한 정해져있는 스키마는 아니기 때문에 다른 컬럼을 추가해도 되고 필요하지 않다면 빼도된다.

orders

주문은 장바구니와 같은 기능을 하게 될 것이며 해당 테이블에 담긴 데이터는 사용자가 구입을 위해 장바구니에 담아놓은 상태를 말한다. 아직 주문이 되지 않은 상품이 있을 수 있으므로 주문에 대한 정보는 비워둘 수 있도록 한다.

php artisan make:migration create_orders_table --create=orders
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateOrdersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->increments('id');
            $table->unsignedBigInteger('amount');
            $table->foreignUuid('merchant_uid')
                ->nullable()
                ->constrained('payments', 'merchant_uid');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('orders');
    }
}

모델

모델 또한 장바구니를 위한 Order, 결제를 위한 Payment, 이렇게 두 개가 존재할 수 있다.

Payment

결제를 위한 모델인 Payment 를 만들어보자.

php artisan make:model Payment
namespace App\Models;

use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Payment extends Model
{
    use HasFactory, Uuids;

    /**
     * The primary key for the model.
     *
     * @var string
     */
    protected $primaryKey = 'merchant_uid';

    /**
     * The attributes that are mass assignable.
     *
     * @var string[]
     */
    protected $fillable = [
        'merchant_uid',
        'imp_uid',
        'amount',
        'cancel_amount',
        'pay_method',
        'status'
    ];

    /**
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function orders()
    {
        return $this->hasMany(Order::class, 'merchant_uid');
    }
}

기본키를 merchant_uid 로 설정한 것과, 자동으로 증가하지 않도록 지정한 것에 주목하자. 또한 결제는 해당 결제에 해당하는 주문이 여러개 소속된다는 의미로 관계를 설정해주자. Uuids 라는 트레이트를 사용한 것을 볼 수 있는데, 기본키인 merchant_uid 는 자동으로 Uuid 가 지정되도록 할 것이다. 따라서 이에 해당하는 트레이트를 지정한 것이며 이는 내장이 아닌 직접 만든 것이다.

namespace App\Traits;

use Illuminate\Support\Str;

trait Uuids
{
    /**
     * Boot function from Laravel.
     */
    protected static function boot()
    {
        parent::boot();

        static::creating(function ($model) {
            if (empty($model->{$model->getKeyName()})) {
                $model->{$model->getKeyName()} = Str::uuid()->toString();
            }
        });
    }

    /**
     * Get the value indicating whether the IDs are incrementing.
     *
     * @return bool
     */
    public function getIncrementing()
    {
        return false;
    }
    
    /**
     * Get the auto-incrementing key type.
     *
     * @return string
     */
    public function getKeyType()
    {
        return 'string';
    }
}

static::creating() 으로 모델이 생성되었을 때 발생하는 이벤트를 리스닝하고 Model::$primaryKey 에 설정한 값에 Uuid 를 생성하여 지정한다. Model::getIncrementing() 으로 기본키가 자동으로 증가하는 키가 아님을 분명히 해줄 필요가 있다.

Order

장바구니를 위한 모델인 Order 를 하나 만들자.

php artisan make:model Order
namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    use HasFactory;

    /**
     * The attributes that are mass assignable.
     *
     * @var string[]
     */
    protected $fillable = [
        'amount'
    ];

    /**
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function payment()
    {
        return $this->belongsTo(Payment::class, 'merchant_uid');
    }

    /**
     * Not paid
     *
     * @param Builder $query
     *
     * @return Builder
     */
    public function scopeUnpaid(Builder $query)
    {
        return $query->whereNull('merchant_uid');
    }
}

하나의 주문은 하나의 결제에 소속될 수 있다. 또한 아직 주문되지 않은 결제들을 얻어내기 위해 unpaid 스코프를 지정해주는 것도 생각해볼 수 있다.

라우팅과 컨트롤러

컨트롤러 또한 두 개를 만들어야한다. 주문의 경우 리소스 컨트롤러로 지정하자.

IamportController

php artisan make:controller IamportController
namespace App\Http\Controllers;

use Illuminate\Http\Request;

class IamportController extends Controller
{
    /**
     * Iamport Webhook
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function webhook(Request $request)
    {
        //
    }
}

IamportController 에는 결제를 위한 로직이 담기게 될 것이며, 또한 메서드의 이름이 왜 webhook 인지는 다음 포스트에서 알 수 있다.

Route::post('/iamport-webhook', [\App\Http\Controllers\IamportController::class, 'webhook']);

OrderController

php artisan make:controller OrderController --resource
namespace App\Http\Controllers;

use Illuminate\Http\Request;

class OrderController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        //
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
        //
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        //
    }
}

Order 의 경우 사용자가 장바구니에 담은 목록을 표시하기 위한 index, 상품을 장바구니에 담기위한 create, store 가 있다. 일반적인 쇼핑몰에서는 create 는 필요하지 않고 상품 상세페이지가 대체하게 될 것이다.

Route::resource('orders', \App\Http\Controllers\OrderController::class)
    ->only(['index', 'create', 'store']);
    '웹' 카테고리의 다른 글
    • 아임포트(Iamport)로 결제기능 구현하기 - 일반결제
    • 'REST' 를 보다 'RESTful' 하게 API 만들기
    • 쿠키(Cookie)와 세션(Sessions)에 대해 알아보자
    • OAuth 2.0 클라이언트 만들기(feat. 깃허브)
    결제, 라라벨, 아임포트
    정상우
    정상우
    과거의 배움으로 현재를 바꾸고 미래를 만듭니다. #25+2살 #INFJ #개발자 #브런치작가
    댓글쓰기
    다음 글
    아임포트(Iamport)로 결제기능 구현하기 - 일반결제
    이전 글
    [Laravel] 카페24 호스팅에 라라벨 프로젝트 배포하기
    • 이전
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • ···
    • 120
    • 다음

    티스토리툴바