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
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# MySQL Database Credentials
# IMPORTANT: Copy this file to .env and change the values before deploying!
# cp .env.example .env

# MySQL Root Password
# Use a strong password with at least 16 characters including uppercase, lowercase, numbers, and special characters
MYSQL_ROOT_PASSWORD=your-strong-root-password-here

# MySQL User Password
# Use a strong password with at least 16 characters including uppercase, lowercase, numbers, and special characters
MYSQL_USER_PASSWORD=your-strong-user-password-here
2 changes: 2 additions & 0 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions api-gateway/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@ plugins {
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.cloud:spring-cloud-starter-config' // Config Client
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j' // Circuit Breaker
implementation 'org.springframework.boot:spring-boot-starter-actuator' // Health check
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.example.gateway;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

/**
* Circuit Breaker가 OPEN 상태일 때 실행되는 Fallback 컨트롤러
* 서비스 장애 시 사용자에게 적절한 응답 제공
*/
@RestController
@RequestMapping("/fallback")
public class FallbackController {
private static final Logger log = LoggerFactory.getLogger(FallbackController.class);

/**
* User Service 장애 시 Fallback 응답
*/
@GetMapping("/user-service")
public ResponseEntity<Map<String, Object>> userServiceFallback() {
return createFallbackResponse("user-service",
"User 서비스가 일시적으로 사용 불가능합니다. 잠시 후 다시 시도해주세요.");
}

/**
* Order Service 장애 시 Fallback 응답
*/
@GetMapping("/order-service")
public ResponseEntity<Map<String, Object>> orderServiceFallback() {
return createFallbackResponse("order-service",
"Order 서비스가 일시적으로 사용 불가능합니다. 잠시 후 다시 시도해주세요.");
}

/**
* 공통 Fallback 응답 생성 메서드
*
* @param serviceName 서비스 이름
* @param message 사용자에게 표시할 메시지
* @return Fallback 응답
*/
private ResponseEntity<Map<String, Object>> createFallbackResponse(String serviceName, String message) {
log.warn("Circuit breaker activated for service: {}", serviceName);

Map<String, Object> response = new HashMap<>();
response.put("status", "SERVICE_UNAVAILABLE");
response.put("message", message);
response.put("timestamp", LocalDateTime.now());
response.put("service", serviceName);

return ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(response);
}
}
2 changes: 2 additions & 0 deletions api-gateway/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
spring:
application:
name: api-gateway
config:
import: optional:configserver:http://localhost:8888
cloud:
gateway:
routes:
Expand Down
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ subprojects {

// 공통 의존성
dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
compileOnly 'org.projectlombok:lombok:1.18.30' // Java 21 호환 버전
annotationProcessor 'org.projectlombok:lombok:1.18.30'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Expand Down
9 changes: 9 additions & 0 deletions common/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,13 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}

// common은 라이브러리 모듈이므로 bootJar 비활성화
bootJar {
enabled = false // 실행 가능한 JAR 만들지 않음
}

jar {
enabled = true // 일반 라이브러리 JAR만 생성
}
33 changes: 33 additions & 0 deletions config-server/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Multi-stage build for optimization
FROM gradle:8.5-jdk21 AS builder
WORKDIR /app

# Copy Gradle files
COPY build.gradle settings.gradle ./
COPY gradle ./gradle

# Copy source code
COPY config-server ./config-server

# Build the application
RUN gradle :config-server:bootJar --no-daemon

# Runtime stage
FROM eclipse-temurin:21-jre
WORKDIR /app

# Install curl for healthcheck
RUN apt-get update && \
apt-get install -y curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

# Create non-root user for security
RUN addgroup --system spring && adduser --system --ingroup spring spring
USER spring:spring

# Copy JAR from builder
COPY --from=builder /app/config-server/build/libs/*.jar app.jar

EXPOSE 8888
ENTRYPOINT ["java", "-jar", "app.jar"]
9 changes: 9 additions & 0 deletions config-server/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
plugins {
id 'org.springframework.boot' version '3.1.5'
}

dependencies {
implementation 'org.springframework.cloud:spring-cloud-config-server'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.configserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableConfigServer // Config Server 활성화
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
31 changes: 31 additions & 0 deletions config-server/src/main/resources/application-docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
server:
port: 8888

spring:
application:
name: config-server
cloud:
config:
server:
native:
search-locations: classpath:/config

management:
endpoints:
web:
exposure:
include: health,info
endpoint:
health:
show-details: always

eureka:
client:
service-url:
defaultZone: http://eureka-server:8761/eureka/ # Docker 환경용
instance:
prefer-ip-address: true

logging:
level:
org.springframework.cloud.config: DEBUG
33 changes: 33 additions & 0 deletions config-server/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
server:
port: 8888 # Config Server 포트

spring:
application:
name: config-server
profiles:
active: native # 파일 시스템 기반 설정 사용 (Git도 가능)
cloud:
config:
server:
native:
search-locations: classpath:/config # 설정 파일 위치

management:
endpoints:
web:
exposure:
include: health,info
endpoint:
health:
show-details: always

eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true

logging:
level:
org.springframework.cloud.config: DEBUG
71 changes: 71 additions & 0 deletions config-server/src/main/resources/config/api-gateway.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
server:
port: 8080

spring:
cloud:
gateway:
routes:
# User Service 라우팅
- id: user-service
uri: lb://user-service # lb = load balanced (Eureka로 찾기)
predicates:
- Path=/api/users/**
filters:
- name: CircuitBreaker
args:
name: userServiceCircuitBreaker
fallbackUri: forward:/fallback/user-service
- RewritePath=/api/users/(?<segment>.*), /$\{segment}

# Order Service 라우팅
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- name: CircuitBreaker
args:
name: orderServiceCircuitBreaker
fallbackUri: forward:/fallback/order-service
- RewritePath=/api/orders/(?<segment>.*), /$\{segment}

discovery:
locator:
enabled: true
lower-case-service-id: true

eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true

# Resilience4j Circuit Breaker 설정
resilience4j:
circuitbreaker:
configs:
default:
registerHealthIndicator: true
slidingWindowSize: 10 # 최근 10개 요청 기준으로 판단
minimumNumberOfCalls: 5 # 최소 5번 호출 후 동작
failureRateThreshold: 50 # 실패율 50% 이상이면 OPEN
waitDurationInOpenState: 10000 # OPEN 상태에서 10초 대기
permittedNumberOfCallsInHalfOpenState: 3 # HALF_OPEN에서 3번 테스트
slowCallDurationThreshold: 2000 # 2초 이상이면 느린 호출
slowCallRateThreshold: 50 # 느린 호출 50% 이상이면 OPEN
instances:
userServiceCircuitBreaker:
baseConfig: default
orderServiceCircuitBreaker:
baseConfig: default

timelimiter:
configs:
default:
timeoutDuration: 3s # 3초 타임아웃

logging:
level:
org.springframework.cloud.gateway: DEBUG
io.github.resilience4j: DEBUG
35 changes: 35 additions & 0 deletions config-server/src/main/resources/config/order-service.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
server:
port: 8082

spring:
datasource:
url: jdbc:mysql://mysql-order:3306/orderdb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
driverClassName: com.mysql.cj.jdbc.Driver
username: user
password: password

jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.MySQLDialect
format_sql: true

eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true

logging:
level:
com.example: DEBUG

# 추가 설정: 비즈니스 로직 관련
app:
order:
max-order-amount: 1000000
delivery-fee: 3000
Loading