Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ repositories {
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.kafka:spring-kafka'
implementation 'org.springframework.ai:spring-ai-starter-model-google-genai'
implementation 'org.jetbrains.kotlin:kotlin-reflect'
Expand Down
64 changes: 4 additions & 60 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,10 @@ services:
- "8080"
# t3.small ๋ฉ”๋ชจ๋ฆฌ ํ•œ๋„ ๊ณ ๋ คํ•œ app ํ•œ๋„ (OTel Agent 80MB ํฌํ•จ 600M)
environment:
OTEL_SERVICE_NAME: catchtable-backend
# OTel Collector๋กœ ์ „์†ก (Collector์—์„œ path ๊ธฐ๋ฐ˜ ๋…ธ์ด์ฆˆ ํ•„ํ„ฐ๋ง ํ›„ Jaeger๋กœ ์ „๋‹ฌ)
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
OTEL_EXPORTER_OTLP_PROTOCOL: grpc
OTEL_TRACES_EXPORTER: otlp
OTEL_METRICS_EXPORTER: none # ๋ฉ”ํŠธ๋ฆญ์€ Micrometer/Prometheus๋กœ ์ฒ˜๋ฆฌ
OTEL_LOGS_EXPORTER: none # ๋กœ๊ทธ๋Š” Promtail/Loki๋กœ ์ฒ˜๋ฆฌ
OTEL_RESOURCE_ATTRIBUTES: deployment.environment=production
# ์ƒ˜ํ”Œ๋ง 100% โ€” ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ๋Š” Collector์—์„œ path ๊ธฐ๋ฐ˜์œผ๋กœ ์ฒ˜๋ฆฌ
OTEL_TRACES_SAMPLER: parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG: "1.0"
OTEL_INSTRUMENTATION_SPRING_SCHEDULING_ENABLED: "false"
OTEL_INSTRUMENTATION_LOGBACK_APPENDER_ENABLED: "true"
OTEL_INSTRUMENTATION_LOGBACK_MDC_ENABLED: "true"
# ๋ถ„์‚ฐ ํŠธ๋ ˆ์ด์‹ฑ(Jaeger + OTel Collector) ์ œ๊ฑฐ โ€” OTel javaagent SDK ์ „์ฒด ๋น„ํ™œ์„ฑํ™”.
# ๋ฉ”ํŠธ๋ฆญ์€ Micrometer/Prometheus, ๋กœ๊ทธ๋Š” Promtail/Loki๊ฐ€ ๋‹ด๋‹นํ•˜๋ฏ€๋กœ ํŠธ๋ ˆ์ด์‹ฑ๋งŒ ๋”.
# ํŠธ๋ ˆ์ด์‹ฑ ๋ณต์› ์‹œ: ์ด ์ค„ ์ œ๊ฑฐ ํ›„ export/sampler ์„ค์ • ๋ณต๊ตฌ + otel-collector/jaeger ์„œ๋น„์Šค ์žฌ์ถ”๊ฐ€.
OTEL_SDK_DISABLED: "true"
deploy:
resources:
limits:
Expand Down Expand Up @@ -135,50 +125,6 @@ services:
networks:
- catchtable-net

otel-collector:
image: otel/opentelemetry-collector-contrib:0.152.1
container_name: catchtable-otel-collector
volumes:
- ./monitoring/otel-collector-config.yml:/etc/otelcol-contrib/config.yaml:ro
command: ["--config=/etc/otelcol-contrib/config.yaml"]
expose:
- "4317" # OTLP gRPC (app์—์„œ ์ „์†ก)
depends_on:
- jaeger
deploy:
resources:
limits:
memory: 150M
restart: unless-stopped
networks:
- catchtable-net

jaeger:
image: jaegertracing/all-in-one:1.62.0
container_name: catchtable-jaeger
user: "0" # named volume mkdir ํผ๋ฏธ์…˜ ํšŒํ”ผ (๋‚ด๋ถ€ ๋„๊ตฌ๋ผ ๋ณด์•ˆ ์˜ํ–ฅ ์—†์Œ)
environment:
COLLECTOR_OTLP_ENABLED: "true"
SPAN_STORAGE_TYPE: badger
BADGER_EPHEMERAL: "false"
BADGER_DIRECTORY_VALUE: /badger/data
BADGER_DIRECTORY_KEY: /badger/key
BADGER_SPAN_STORE_TTL: 72h0m0s
volumes:
- jaeger-data:/badger
ports:
- "16686:16686" # Jaeger UI (์™ธ๋ถ€ ์ ‘๊ทผ)
expose:
- "4317" # OTLP gRPC (์•ฑ์—์„œ ์ „์†ก)
- "4318" # OTLP HTTP
deploy:
resources:
limits:
memory: 400M
restart: unless-stopped
networks:
- catchtable-net

grafana:
image: grafana/grafana:11.3.0
container_name: catchtable-grafana
Expand All @@ -194,7 +140,6 @@ services:
depends_on:
- prometheus
- loki
- jaeger
deploy:
resources:
limits:
Expand Down Expand Up @@ -277,7 +222,6 @@ networks:
volumes:
prometheus-data:
loki-data:
jaeger-data:
grafana-data:
promtail-positions:
kafka-prod-data:
Expand Down
13 changes: 0 additions & 13 deletions grafana/provisioning/datasources/datasources.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,3 @@ datasources:
access: proxy
url: http://loki:3100
editable: true
jsonData:
derivedFields:
- datasourceUid: jaeger
matcherRegex: 'trace_id=(\w+)'
name: TraceID
url: '$${__value.raw}'

- name: Jaeger
type: jaeger
access: proxy
url: http://jaeger:16686
uid: jaeger
editable: true
10 changes: 9 additions & 1 deletion src/main/java/com/catchtable/coupon/entity/Coupon.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@
import java.time.LocalDateTime;

@Entity
@Table(name = "coupons")
@Table(
name = "coupons",
uniqueConstraints = {
@UniqueConstraint(
name = "uq_coupons_user_template",
columnNames = {"user_id", "coupon_template_id"}
)
}
)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.catchtable.coupon.entity;

import com.catchtable.global.exception.CustomException;
import com.catchtable.global.exception.ErrorCode;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
Expand Down Expand Up @@ -50,11 +48,4 @@ public class CouponTemplate {
@Column(name = "is_deleted", nullable = false)
@Builder.Default
private Boolean isDeleted = false;

public void decreaseRemain() {
if (this.remain <= 0) {
throw new CustomException(ErrorCode.COUPON_EXHAUSTED);
}
this.remain--;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.catchtable.coupon.redis;

import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
* coupon-redis ํšŒ๋กœ์˜ ์ƒํƒœ ์ „ํ™˜์„ ์šด์˜ ๋กœ๊ทธ์— ๋‚จ๊ธด๋‹ค.
* ํšŒ๋กœ๊ฐ€ OPEN ์œผ๋กœ ๊ฐ”๋‹ค โ†’ Redis ์žฅ์•  ์‹ ํ˜ธ.
* HALF_OPEN โ†’ CLOSED ๋ฉด ๋ณต๊ตฌ ์™„๋ฃŒ.
* ์‚ฌํ›„ ์ถ”์ ยท์•Œ๋žŒ ๋ฃฐ ์ž‘์„ฑ์˜ ๊ทผ๊ฑฐ๊ฐ€ ๋œ๋‹ค.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CouponRedisCircuitBreakerLogger {

private static final String INSTANCE = "coupon-redis";

private final CircuitBreakerRegistry circuitBreakerRegistry;

@PostConstruct
void registerEventListeners() {
CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker(INSTANCE);
cb.getEventPublisher()
.onStateTransition(event ->
log.warn("[CircuitBreaker:{}] ์ƒํƒœ ์ „ํ™˜ {} โ†’ {}",
INSTANCE,
event.getStateTransition().getFromState(),
event.getStateTransition().getToState()))
.onCallNotPermitted(event ->
log.debug("[CircuitBreaker:{}] OPEN ์ƒํƒœ๋กœ ํ˜ธ์ถœ ์ฐจ๋‹จ๋จ", INSTANCE))
.onError(event ->
log.debug("[CircuitBreaker:{}] ํ˜ธ์ถœ ์‹คํŒจ: {}",
INSTANCE, event.getThrowable().toString()));
}
}
57 changes: 57 additions & 0 deletions src/main/java/com/catchtable/coupon/redis/CouponWarmupRunner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.catchtable.coupon.redis;

import com.catchtable.coupon.entity.CouponTemplate;
import com.catchtable.coupon.repository.CouponTemplateRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.List;

/**
* ์„œ๋ฒ„ ๋ถ€ํŒ… ์‹œ ๋ฐœ๊ธ‰ ๊ฐ€๋Šฅํ•œ ์ฟ ํฐ ํ…œํ”Œ๋ฆฟ์˜ Redis ์ƒํƒœ๋ฅผ ์ ๊ฒ€ยท๋ณต์›ํ•œ๋‹ค.
*
* ์‹œ๋‚˜๋ฆฌ์˜ค:
* - Redis ๊ฐ€ ์žฌ๊ธฐ๋™๋˜์–ด coupon:stock:{id} ํ‚ค๊ฐ€ ์‚ฌ๋ผ์ง„ ๊ฒฝ์šฐ,
* DB ์˜ remain ๊ฐ’์œผ๋กœ ์›Œ๋ฐ์—…ํ•˜์ง€ ์•Š์œผ๋ฉด Lua ๊ฐ€ NOT_AVAILABLE ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
* - DB ์—๋Š” ๋ฉ€์ฉกํ•œ ํ…œํ”Œ๋ฆฟ์ด ์žˆ๋Š”๋ฐ ๋ฐœ๊ธ‰ API ๊ฐ€ ์•ˆ ๋˜๋Š” ์šด์˜ ์‚ฌ๊ณ ๋กœ ์ง๊ฒฐ๋จ.
*
* ์•ˆ์ „์žฅ์น˜:
* - setIfAbsent ์˜๋ฏธ๋ผ Redis ๊ฐ€ ์ •์ƒ์ด๊ณ  ์šด์˜ ์ค‘ ์ฐจ๊ฐ๋œ ๊ฐ’์ด ์žˆ์œผ๋ฉด ๋ฎ์–ด์“ฐ์ง€ ์•Š๋Š”๋‹ค.
* - ๋”ฐ๋ผ์„œ ํ‰์‹œ ๋ถ€ํŒ…์—๋Š” no-op, Redis ์ฝœ๋“œ ์Šคํƒ€ํŠธ์ผ ๋•Œ๋งŒ ์‹คํšจ.
*
* ํ•œ๊ณ„:
* - "Redis ๊ฐ€ ์ฃฝ์€ ๋™์•ˆ ๋ฐœ๊ธ‰๋œ ๊ฑด์ˆ˜"๋Š” ๋ณต์› ๋ถˆ๊ฐ€. Redis HA + AOF ์˜์†ํ™”๊ฐ€ 1์ฐจ ๋ฐฉ์–ด์„ ์ด๋‹ค.
*/
@Slf4j
@Component
@Order(Integer.MAX_VALUE) // ๋‹ค๋ฅธ ApplicationRunner ์ดํ›„ ์‹คํ–‰
@RequiredArgsConstructor
public class CouponWarmupRunner implements ApplicationRunner {

private final CouponTemplateRepository couponTemplateRepository;
private final RedisCouponIssuer redisCouponIssuer;

@Override
public void run(ApplicationArguments args) {
List<CouponTemplate> activeTemplates = couponTemplateRepository.findActiveTemplates(LocalDateTime.now());
if (activeTemplates.isEmpty()) {
log.info("์ฟ ํฐ Redis ์›Œ๋ฐ์—…: ํ™œ์„ฑ ํ…œํ”Œ๋ฆฟ ์—†์Œ, ์Šคํ‚ต");
return;
}

for (CouponTemplate t : activeTemplates) {
try {
redisCouponIssuer.warmUpIfAbsent(t.getId(), t.getRemain(), t.getExpiredAt());
} catch (Exception e) {
// ํ•œ ํ…œํ”Œ๋ฆฟ ์‹คํŒจ๊ฐ€ ๋‹ค๋ฅธ ํ…œํ”Œ๋ฆฟ ์›Œ๋ฐ์—…์„ ๋ง‰์ง€ ์•Š๊ฒŒ ํ•œ๋‹ค.
log.error("์ฟ ํฐ Redis ์›Œ๋ฐ์—… ์‹คํŒจ: templateId={}", t.getId(), e);
}
}
log.info("์ฟ ํฐ Redis ์›Œ๋ฐ์—… ์™„๋ฃŒ: ํ™œ์„ฑ ํ…œํ”Œ๋ฆฟ {}๊ฑด ์ ๊ฒ€", activeTemplates.size());
}
}
Loading
Loading