Home 선착순 쿠폰 발급 시스템 (2) 환경 구성
Post
Cancel

선착순 쿠폰 발급 시스템 (2) 환경 구성

선착순 쿠폰 발급 이벤트

  • 한정된 수량의 쿠폰을 먼저 신청한 사용자에게 제공하는 이벤트

요구 사항

  • 이벤트 기간 내에(예시 2024-02-01일 오후 1시~ 2024-02-10일 오후 1시) 발급
  • 선착순 이벤트는 유저당 1번의 쿠폰 발급
  • 선착순 쿠폰의 최대 쿠폰 발급 수량 설정

쿠폰 발급 기능

  • 쿠폰 발급 기능
    • 쿠폰 발급 기간 검증 (기간 내 발급)
    • 쿠폰 발급 수량 검증
      • 쿠폰 전체 발급 수량
      • 중복 발급 요청 검증 (중복 참여 방지)
    • 쿠폰 발급 (검증 후 발급)
      • 쿠폰 발급 수량 증가
      • 쿠폰 발급 기록 저장
        • 쿠폰 ID
        • 유저 ID

쿠폰 발급 기능 구현의 목표

  • 정확한 발급 수량 제어 (동시성 이슈 처리)
  • 높은 처리량
    • 복잡한 쿠폰 구조는 생략

환경 구성

image-20240222005655938

  • 기본 시스템 생성 후 src 폴더 지움
  • 멀티 모듈로 구축

image

image

  • coupon-core 모듈 생성
  • api와 컨슈머에서 공통적으로 사용하는 기능
  • 엔티티, 레포지토리 정의

image

  • coupon-api 모듈 생성
  • api 서버를 통해 유저의 요청 처리
  • 쿠폰 검증 및 요청 처리

image

  • coupon-consumer 모듈 생성
  • 비동기 구조로 발급, 큐에 목록을 읽어 처리하는 서버
  • 쿠폰 발급 처리 및 Persistance에 저장

image

  • 총 3개의 멀티 모듈이 생성 되었다.

    image

  • coupon-management-system 하위에 다른 멀티 모듈 gradle을 include 시킨다.
  • setting.gradle.kts에 추가하면 된다.
1
include("coupon-core", "coupon-api", "coupon-consummer")
  • build.gradle 세팅 부분
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
subprojects {
	apply(plugin = "java")
	apply(plugin = "io.spring.dependency-management")
	apply(plugin = "org.springframework.boot")

	repositories {
		mavenCentral()
	}

	dependencies {
		implementation("org.springframework.boot:spring-boot-starter-data-jpa")
		implementation("org.springframework.boot:spring-boot-starter-data-redis")
		compileOnly("org.projectlombok:lombok")
		annotationProcessor("org.projectlombok:lombok")
		runtimeOnly("com.h2database:h2")
		runtimeOnly("com.mysql:mysql-connector-j")
		implementation("org.springframework.boot:spring-boot-starter")
		implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta")
		implementation("org.springframework.boot:spring-boot-starter-actuator")
		implementation("io.micrometer:micrometer-registry-prometheus")
		annotationProcessor("com.querydsl:querydsl-apt:5.0.0:jakarta")
		annotationProcessor("jakarta.annotation:jakarta.annotation-api")
		annotationProcessor("jakarta.persistence:jakarta.persistence-api")
		testImplementation("org.springframework.boot:spring-boot-starter-test")
	}
}
  • subprojects 은 최상단 프로젝트인 coupon-management-system 하위의 서브프로젝트인 coupon-api, coupon-consumer, coupon-core 는 아래 설정값을 적용한다는 의미이다.

  • /coupon-core/build.gradle.kts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
val bootJar: org.springframework.boot.gradle.tasks.bundling.BootJar by tasks

bootJar.enabled = false

repositories {
	mavenCentral()
}

dependencies {
	implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
	implementation("com.fasterxml.jackson.core:jackson-databind")
	implementation("org.redisson:redisson-spring-boot-starter:3.16.4")
	implementation("org.springframework.boot:spring-boot-starter")
	implementation("com.github.ben-manes.caffeine:caffeine")
	testImplementation("org.springframework.boot:spring-boot-starter-test")
}

tasks.withType<Test> {
	useJUnitPlatform()
}

이런 식으로 설정 반영

  • /coupon-api/build.gradle.kts
  • /coupon-consumer/build.gradle.kts
1
2
3
4
5
6
7
8
9
10
dependencies {
    implementation(project(":coupon-core"))
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

tasks.withType<Test> {
    useJUnitPlatform()
}

각각 역시 이런 식으로 의존성 추가

  • implementation(project(“:coupon-core”)) 의미는 coupon-core의 모듈을 가져다가 쓰겠다는 의미
  • coupon-core는 다른 모듈에 import하는 형태로 사용되므로 couponCoreApplication은 필요가 없다.
  • 사용 할 서버는 coupon-api 웹서버, coupon-consumer 웹서버 두개
1
2
3
4
5
6
7
8
9
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan
@EnableAutoConfiguration
public class CouponCoreConfiguration {
}

  • 공통으로 사용한 쿠폰코어설정을 만들어준다.
1
2
3
4
5
6
7
8
9
10
@Import(CouponCoreConfiguration.class)
@SpringBootApplication
public class CouponApiApplication {

	public static void main(String[] args) {
		System.setProperty("spring.config.name", "application-core, application-api");
		SpringApplication.run(CouponApiApplication.class, args);
	}

}

CouponCoreConfiguration이 만들어졌다면 각 coupon-api, coupon-consumer 애플리케이션에 공통으로 사용한 CouponCoreConfiguration을 @Import 어노테이션을 통해 추가한다.

또한 application.yml파일을 가져올때 환경변수를 각각 application-core, application-api와 application-core, application-consumer로 가져온다.

resources/application-core.yml

resources/application-consumer.yml

resources/application-api.yml


Mysql & Redis 환경 구성

Docker-compose를 통한 데이터 소스 구성

docker-compose는 여러 컨테이너를 하나의 애플리케이션처럼 정의하고 관리하는 도구. docker-compose.yml이라는 파일을 사용하여 컨테이너 구성을 정의하고, 단일 명령어로 컨테이너를 생성, 시작, 중지, 삭제할 수 있다.

  1. 우선 도커 데스크톱을 설치 https://www.docker.com/products/docker-desktop/
  2. /coupon-management-system/docker-compose.yml 생성 및 compose 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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

Docker-compose.yml 분석

버전

  • version: '3.7': Docker Compose 파일 형식의 버전을 지정. 이 버전은 서비스, 네트워크, 볼륨 등을 정의하는 데 사용할 수 있는 여러 기능을 제공.

서비스

두 개의 주요 서비스가 명확하게 정의되어 있다.

1. Redis

  • container_name: coupon-redis: Redis 컨테이너의 이름을 설정.

  • image: redis:7.2-alpine: 공식 Redis 이미지(버전 7.2, 작은 공간을 위해 Alpine Linux 기반)를 사용.

  • command: redis-server --port 6380: 기본 Redis 명령을 재정의하여 포트 6380에서 실행.

  • labels:

    컨테이너에 메타데이터를 추가.

    • name=redis
    • mode=standalone
  • ports: - 6380:6380: 호스트 머신의 포트 6380을 Redis 컨테이너 내부의 포트 6380에 매핑하여 Redis 서비스를 외부에서 사용.

2. MySQL

  • container_name: coupon-mysql: MySQL 컨테이너의 이름을 설정.

  • image: ubuntu/mysql:edge: Ubuntu 기반 MySQL 이미지(edge 태그는 덜 안정적이지만 기능이 풍부한 버전을 의미)를 사용.

  • command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --explicit_defaults_for_timestamp=1:

    다음과 같은 특정 구성으로 기본 MySQL 명령을 재정의.

    • UTF-8(utf8mb4) 문자 인코딩을 사용.
    • 정확한 타임스탬프 처리를 적용.
  • ports: - 3306:3306: 호스트 머신의 포트 3306을 MySQL 컨테이너 내부의 포트 3306(표준 MySQL 포트)에 매핑.

  • environment:

    컨테이너 내부에서 환경 변수를 설정.

    • MYSQL_DATABASE=coupon: ‘coupon’이라는 데이터베이스를 생성.
    • MYSQL_USER=abcd and MYSQL_PASSWORD=1234: 데이터베이스 사용자와 비밀번호를 생성.
    • MYSQL_ROOT_PASSWORD=1234: 데이터베이스의 루트 비밀번호를 설정.
    • TZ=UTC: 컨테이너의 시간대를 UTC로 설정.
  • volumes: - ./mysql/init:/docker-entrypoint-initdb.d: 로컬 디렉토리 ./mysql/init을 컨테이너의 초기화 디렉토리에 연결. 이를 통해 ./mysql/init 내부에 SQL 스크립트를 저장하면 MySQL 컨테이너가 시작될 때 실행.

  1. 터미널에서 도커 컴포즈 파일 실행
1
$ docker-compose up -d

docker-compose up -d 명령어 분석

up 명령은 docker-compose.yml 파일에서 정의된 컨테이너를 모두 생성하고 시작한다. 옵션 없이 실행하면 모든 컨테이너를 시작하지만, 옵션을 사용하여 특정 컨테이너만 시작하거나 다른 설정을 변경할 수 있다.

-d 옵션은 컨테이너를 백그라운드에서 실행하도록 지정한다.

  1. application-core.yml 환경변수 수정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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: 10
      max-lifetime: 30000
      connection-timeout: 3000
      username: abcd
      password: 1234
  jpa:
    hibernate:
      ddl-auto: none
    show-sql: true
    properties:
      hibernate:
        format_sql: true
  data:
    redis:
      host: localhost
      port: 6380

application-core.yml 분석

1. spring.config.activate.on-profile: local

  • 이 섹션은 “local”이라는 Spring 프로필이 활성화된 경우에만 spring 섹션 내의 설정이 활성화된다는 것을 나타냄. 이를 통해 로컬 개발 환경과 프로덕션 환경에 대해 서로 다른 설정을 사용할 수 있다.

2. spring.datasource.hikari

  • jdbc-url:

    MySQL 데이터베이스에 대한 JDBC 연결 문자열.

    • jdbc:mysql://localhost:3306/coupon - 로컬 MySQL 서버(포트 3306)의 coupon 데이터베이스에 연결한다.
    • useUnicode=yes&characterEncoding=UTF-8 - 다양한 문자의 올바른 처리를 위해 UTF-8 인코딩을 적용한다.
    • rewriteBatchedStatements=true - 일괄 업데이트를 위한 잠재적인 최적화.
  • driver-class-name: MySQL용 JDBC 드라이버를 지정 (com.mysql.cj.jdbc.Driver).

  • maximum-pool-size: Hikari 연결 풀에서 허용되는 최대 연결 수를 설정 (여기서는 10).

  • max-lifetime: 연결 풀에서 연결의 최대 수명 (30000 밀리초).

  • connection-timeout: 연결을 기다리는 최대 시간 (3000 밀리초).

  • username and password: 데이터베이스 사용자 이름과 비밀번호.

3. spring.jpa

  • hibernate.ddl-auto: none
    • JPA/Hibernate가 시작 시 데이터베이스 스키마를 자동으로 수정하지 못하도록 함. 이는 프로덕션 환경에서 데이터 무결성을 유지하는 데 매우 중요.
  • show-sql: true
    • 디버깅에 유용하도록 생성된 SQL 문을 콘솔에 기록함.
  • properties.hibernate.format_sql: true
    • Hibernate에게 SQL 출력을 더 읽기 쉬운 방식으로 형식화하도록 지시함.

4. spring.data.redis

  • host and port: Redis 인스턴스의 호스트 이름 (localhost)과 포트 (6380).

선착순 쿠폰 발급 시스템 (1) 발급 처리 구성도

선착순 쿠폰 발급 시스템 (3) LOCUST 부하 테스트 구축