토이프로젝트/선착순 이벤트 쿠폰 시스템

Mysql 기반 선착순 쿠폰 발급 기능 개발 (1)

feel2 2024. 4. 8. 23:37
반응형

mysql, redis 설정

 

먼저 mysql과 redis를 도커 컨테이너로 띄우기 위해 docker-compose를 작성해 주자.

//docker-compose.yml

version: '3.7'
services:
  redis:
    container_name: coupon-redis
    image: redis:7.2-alpine
    command: redis-server --port 6380
    labels:
      - "name=redis"
      - "mode=standalone"
    ports:
      - 6380:6380
  mysql:
    container_name: coupon-mysql
    image: ubuntu/mysql:edge
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --explicit_defaults_for_timestamp=1
    ports:
      - 3306:3306
    environment:
      - MYSQL_DATABASE=coupon
      - MYSQL_USER=abcd
      - MYSQL_PASSWORD=1234
      - MYSQL_ROOT_PASSWORD=1234
      - TZ=UTC
    volumes:
      - ./mysql/init:/docker-entrypoint-initdb.d

 

이제 해당 디렉토리로 이동해서 다음과 같이 명령어를 입력하면 mysql과 redis 컨테이너가 올라간다.

 

$ docker-compose up -d

[+] Building 0.0s (0/0)                                                                                                                                                                                                                                                                                            
[+] Running 3/3
 ✔ Network mycoupon_default  Created                                                                                                                                                                                                                                                                          0.0s 
 ✔ Container coupon-mysql    Started                                                                                                                                                                                                                                                                          0.5s 
 ✔ Container coupon-redis    Started  

 

다음 명령어를 통해서 올라간 컨테이너가 확인 가능하다.

 

$ docker ps

이제 redis와 mysql를 사용 가능하도록 설정해주자.

mycoupon

제일 바깥의 root 디렉토리의 build.gradle 에 다음과 같은 dependency 들을 추가해주자.

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.3'
    id 'io.spring.dependency-management' version '1.1.4'
}

bootJar.enabled = false

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}
subprojects {
    apply plugin: "java"
    apply plugin: "io.spring.dependency-management"
    apply plugin: "org.springframework.boot"

    repositories {
        mavenCentral()
    }

    // 관리하는 모듈에 공통 dependencies
    dependencies {
         /** 새로 추가 **/
        implementation "org.springframework.boot:spring-boot-starter-data-jpa" // jpa 를 사용하기 위해서
        implementation "org.springframework.boot:spring-boot-starter-data-redis" // redis 사용을 위해서

        runtimeOnly "com.h2database:h2" // h2db 사용을 위해서
        runtimeOnly "com.mysql:mysql-connector-j" // mysql driver 를 사용하기 위해서

        implementation "com.querydsl:querydsl-jpa:5.0.0:jakarta" // querydsl 사용을 위해
        annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" // querydsl 사용을 위해
        /**  // 새로 추가 **/

        implementation 'org.springframework.boot:spring-boot-starter'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        compileOnly "org.projectlombok:lombok"
        annotationProcessor "org.projectlombok:lombok"
        implementation "org.springframework.boot:spring-boot-starter"
        annotationProcessor "jakarta.annotation:jakarta.annotation-api"
        annotationProcessor "jakarta.persistence:jakarta.persistence-api"
        testImplementation "org.springframework.boot:spring-boot-starter-test"

    }
}

tasks.named('test') {
    useJUnitPlatform()
}

mycoupon-core

mycoupon-core 의 application.yml 정보를 다음과 같이 수정해주자.

//application-core.yml

spring:
  config:
    activate:
      on-profile: local
  datasource:
    hikari:
      jdbc-url: jdbc:mysql://localhost:3306/coupon?useUnicode=yes&characterEncoding=UTF-8&rewriteBatchedStatements=true
      driver-class-name: com.mysql.cj.jdbc.Driver
      #      maximum-pool-size: 30000
      #      max-lifetime: 3000
      username: abcd
      password: 1234
  jpa:
    hibernate:
      ddl-auto: none
    show-sql: true
    properties:
      hibernate:
        format_sql: true
  data:
    redis:
      host: localhost
      port: 6380

---

spring:
  config:
    activate:
      on-profile: test
  datasource:
    url: jdbc:h2:mem:test;
    driverClassName: org.h2.Driver
    username: sa
    password:
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true
  data:
    redis:
      host: localhost
      port: 6380

공통 dependency 영역이 추가되었기 때문에 전체 gradle reload를 한번 해준다.

 

이제 다시 앱을 기동해보면 잘 되는것을 확인해 볼 수 있다.

 

mysql과 redis가 잘 붙었는지 확인해 보려면 올라오는 log를 확인해 보면 알 수 있다.


db, redis 연결

먼저 crate ddl 문을 다음과 같은 위치에 넣어두자.

 

CREATE TABLE `coupon`.`coupons`
(
    `id`                   BIGINT(20) NOT NULL AUTO_INCREMENT,
    `title`                VARCHAR(255) NOT NULL COMMENT '쿠폰명',
    `coupon_type`          VARCHAR(255) NOT NULL COMMENT '쿠폰 타입 (선착순 쿠폰, ..)',
    `total_quantity`       INT NULL COMMENT '쿠폰 발급 최대 수량',
    `issued_quantity`      INT          NOT NULL COMMENT '발급된 쿠폰 수량',
    `discount_amount`      INT          NOT NULL COMMENT '할인 금액',
    `min_available_amount` INT          NOT NULL COMMENT '최소 사용 금액',
    `date_issue_start`     datetime(6) NOT NULL COMMENT '발급 시작 일시',
    `date_issue_end`       datetime(6) NOT NULL COMMENT '발급 종료 일시',
    `date_created`         datetime(6) NOT NULL COMMENT '생성 일시',
    `date_updated`         datetime(6) NOT NULL COMMENT '수정 일시',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
    COMMENT '쿠폰 정책';

CREATE TABLE `coupon`.`coupon_issues`
(
    `id`           BIGINT(20) NOT NULL AUTO_INCREMENT,
    `coupon_id`    BIGINT(20) NOT NULL COMMENT '쿠폰 ID',
    `user_id`      BIGINT(20) NOT NULL COMMENT '유저 ID',
    `date_issued`  datetime(6) NOT NULL COMMENT '발급 일시',
    `date_used`    datetime(6) NULL COMMENT '사용 일시',
    `date_created` datetime(6) NOT NULL COMMENT '생성 일시',
    `date_updated` datetime(6) NOT NULL COMMENT '수정 일시',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
    COMMENT '쿠폰 발급 내역';

insert into coupons (title, coupon_type, total_quantity, issued_quantity, discount_amount, min_available_amount,
                     date_issue_start, date_issue_end, date_created, date_updated)
values ('선착순 쿠폰 이벤트','FIRST_COME_FIRST_SERVED',500,0,100000,10000,now(),now(),now(),now());

만약에 intelliJ 를 사용 중이라면 datagrip을 통해서 db에 붙을 수가 있다.

꼭 datagrip 이 아니더라도 db 툴로는 dbeaver, Heidsql 등등 많으니 알아보고 각자가 쓰기 편한 것을 쓰면 될 것 같다.

 

설정 내용은 다음과 같이 하였다.

 

 

연결이 잘 되었다면 아까 작성했던 쿼리를 실행해주자.

 

그럼 다음과 같이 table이 생성되고, 하나의 더미 데이터가 들어가는 것을 볼 수 있다.

같은 방법으로 redis도 연결하면 된다.


엔터티 작성

이제 본격적으로 코딩을 해보자.

각 도메인별로 엔터티를 작성해주기 전에 공통으로 상속받는 엔터티를 하나 만들어 주자.

이걸 상속받는 엔터티들은 자동으로 생성 시간과 수정 시간을 자동으로 BaseTimeEntity 가 주입해준다.

package com.example.mycouponcore.model;

import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDate;

@Getter
@MappedSuperclass //다른 entity 에서 상속할 것이기 때문에
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {

    @CreatedDate
    private LocalDate dateCreated;

    @LastModifiedDate
    private LocalDate dateUpdated;
}

 

제대로 작동하기 위해서는 메인 class에 @EnableJpaAuditing 달아주어야 한다.

//MyCouponCoreConfiguration

package com.example.mycouponcore;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableAutoConfiguration
@ComponentScan
@EnableJpaAuditing //추가
public class MyCouponCoreConfiguration {
}

 

다음으로 coupon 도메인에 대한 엔터티를 작성해주자.

package com.example.mycouponcore.model;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Table(name = "coupons")
public class Coupon extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    @Enumerated(value = EnumType.STRING)
    private CouponType couponType;

    private Integer totalQuantity;

    @Column(nullable = false)
    private int issuedQuantity;

    @Column(nullable = false)
    private int discountAmount;

    @Column(nullable = false)
    private int minAvailableAmount;

    @Column(nullable = false)
    private LocalDateTime dateIssueStart;

    @Column(nullable = false)
    private LocalDateTime dateIssueEnd;

}

 

coupon의 종류가 다를 수 있기 때문에 enum 타입의 CouponType을 따로 선언해 주자.

package com.example.mycouponcore.model;

public enum CouponType {
    FIRST_COME_FIRST_SERVED, //선착순 쿠폰
    DISCOUNT_COUPON
}

 

마지막으로 coupon_issues 엔터티를 작성해주자.


package com.example.mycouponcore.model;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;

import java.time.LocalDateTime;

@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Table(name = "coupon_issues")
public class CouponIssue extends BaseTimeEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private Long couponId;

    @Column(nullable = false)
    private Long userId;

    @Column(nullable = false)
    @CreatedDate
    private LocalDateTime dateIssued;

    private LocalDateTime dateUsed;

}

 

내용이 너무 길어져서 Mysql 기반 선착순 쿠폰 발급 기능 개발 (2) 에서 이어서 작성하겠다.

반응형