Ver3.5 ๐ Spring Boot 3.0๊ณผ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ ๊ตฌํ ์ ๋ต

๐ Spring Boot 3.0๊ณผ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ ๊ตฌํ ์ ๋ต
์น๊ตฌ์ฒ๋ผ ์ฝ๊ณ ์ฌ๋ฏธ์๊ฒ ๋ฐฐ์ฐ๋ MSA์ ๋ชจ๋ ๊ฒ! ๐
๐ค Spring Boot 3.0, ๋ญ๊ฐ ๋ฌ๋ผ์ก์ด?
์ผ, ์์งํ ๋งํด์ Spring Boot 2.x์์ 3.0์ผ๋ก ๋์ด๊ฐ๋ ๊ฑด ๋จ์ํ ๋ฒ์ ์
์ด ์๋์ผ.
์ด๊ฑด ํจ๋ฌ๋ค์์ ์ ํ์ด๋ผ๊ณ ๋ด์ผ ํด. ๐ฅ
2022๋
11์์ ์ ์ ์ถ์๋ Spring Boot 3.0์ Java 17์ ์ต์ ์๊ตฌ ๋ฒ์ ์ผ๋ก ์ค์ ํ๊ณ ,
๊ธฐ์กด์ javax.* ํจํค์ง๊ฐ ์ ๋ถ jakarta.*๋ก ๋ฐ๋์์ด.
์ด๊ฒ ์ฒ์์ "์ ๊ท์ฐฎ์..." ์ถ์ง๋ง, ์ฌ์ค ์ด ๋ณํ๊ฐ ์์ฒญ๋ ๋ฏธ๋๋ฅผ ์ด์ด์ค๋ค๊ณ !
ํนํ GraalVM Native Image ์ง์์ ์ง์ง ๊ฒ์์ฒด์ธ์ ์ผ.
๊ธฐ์กด JVM ์์์ ๋๋ฆฌ๋ Spring ์ฑ์ ๋ค์ดํฐ๋ธ ๋ฐ์ด๋๋ฆฌ๋ก ์ปดํ์ผํ๋ฉด
์์ ์๊ฐ์ด ์์ญ ๋ฐ๋ฆฌ์ด๋ก ์ค์ด๋ค๊ณ , ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋๋ ํ ์ค์ด๋ค์ด.
๋ง์ดํฌ๋ก์๋น์ค ํ๊ฒฝ์์ ์ด๊ฒ ์ผ๋ง๋ ์ค์ํ์ง ์์ง? ์ปจํ
์ด๋ ์๋ฐฑ ๊ฐ ๋์ธ ๋ ์ฐจ์ด๊ฐ ์ด๋ง์ด๋งํด! ๐
Spring Boot 3.0๊ณผ ํจ๊ป ์ฐ๋ฉด ์ฝ๋๊ฐ ํจ์ฌ ๊ฐ๊ฒฐํ๊ณ ์์ ํด์ ธ. ๊ผญ ๊ฐ์ด ๊ณต๋ถํด๋ด!
๐๏ธ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ(MSA)๋ ๋ญ๋ฐ?
MSA๋ฅผ ํ ์ค๋ก ์ค๋ช
ํ๋ฉด ์ด๋:
"ํ๋์ ํฐ ์ฑ์ ์ฌ๋ฌ ๊ฐ์ ์๊ณ ๋
๋ฆฝ์ ์ธ ์๋น์ค๋ก ์ชผ๊ฐ์ ์ด์ํ๋ ๋ฐฉ์"
์์ ์ ์ผํ๋ชฐ ํ๋ ๋ง๋ค๋ฉด ์ฃผ๋ฌธ, ๊ฒฐ์ , ํ์, ๋ฐฐ์ก, ์๋ฆผ ๊ธฐ๋ฅ์ด ์ ๋ถ ํ๋์ ๊ฑฐ๋ํ ํ๋ก์ ํธ ์์ ์์์์.
์ด๊ฑธ ๋ชจ๋๋ฆฌ์(Monolithic) ์ํคํ
์ฒ๋ผ๊ณ ํด.
๊ทผ๋ฐ ์ด๊ฒ ์ปค์ง๋ฉด ์ปค์ง์๋ก ๋ฌธ์ ๊ฐ ์๊ฒจ. ๊ฒฐ์ ๊ธฐ๋ฅ ํ๋ ์์ ํ๋๋ฐ ์ ์ฒด ์๋น์ค๊ฐ ๋ค์ด๋๊ฑฐ๋,
๋ฐฐํฌ ํ ๋ฒ ํ๋ ค๋ฉด ๋ช ์๊ฐ์ฉ ๊ฑธ๋ฆฌ๊ฑฐ๋... ๊ฐ๋ฐ์๋ค์ ์
๋ชฝ์ด์ง ๐ฑ
โก ํน์ ๊ธฐ๋ฅ๋ง ์ค์ผ์ผ์์์ด ๋ถ๊ฐ๋ฅ
โข ๊ธฐ์ ์คํ ๋ณ๊ฒฝ์ด ๊ฑฐ์ ๋ถ๊ฐ๋ฅ
โฃ ํ์ด ์ปค์ง์๋ก ๋ฐฐํฌ ์ถฉ๋ ๋น๋ฒ
โค ์ฝ๋๋ฒ ์ด์ค๊ฐ ๊ฑฐ๋ํด์ ธ์ ์ดํดํ๊ธฐ ์ด๋ ค์
โก ํ์ํ ์๋น์ค๋ง ์ค์ผ์ผ์์
โข ์๋น์ค๋ง๋ค ๋ค๋ฅธ ๊ธฐ์ ์คํ ์ฌ์ฉ ๊ฐ๋ฅ
โฃ ์ฅ์ ๊ฒฉ๋ฆฌ (ํ๋ ์ฃฝ์ด๋ ๋ค๋ฅธ ๊ฑด ์ด์์์)
โค ์์ ํ์ด ๊ฐ ์๋น์ค๋ฅผ ๋ ๋ฆฝ์ ์ผ๋ก ๊ฐ๋ฐ
๋ฌผ๋ก MSA๊ฐ ๋ง๋ฅ์ ์๋์ผ. ๋ถ์ฐ ์์คํ
์ ๋ณต์ก์ฑ, ๋คํธ์ํฌ ์ง์ฐ, ๋ฐ์ดํฐ ์ผ๊ด์ฑ ๋ฌธ์ ๋ฑ
์๋ก์ด ๋์ ๊ณผ์ ๋ค์ด ์๊ธฐ๊ฑฐ๋ . ๊ทธ๋์ Spring Boot 3.0์ ์ํ๊ณ๊ฐ ์ด ๋ฌธ์ ๋ค์ ํด๊ฒฐํ๋ ๋ฐ ์์ฒญ ๋์์ด ๋ผ!
โ๏ธ Spring Boot 3.0์ผ๋ก MSA ํ๋ก์ ํธ ์์ํ๊ธฐ
์, ์ด์ ์ง์ง ์ค์ ์ผ๋ก ๋ค์ด๊ฐ๋ณด์! ๐ฅ
Spring Boot 3.0 ๊ธฐ๋ฐ MSA ํ๋ก์ ํธ๋ฅผ ์ด๋ป๊ฒ ๊ตฌ์ฑํ๋์ง ๋จ๊ณ๋ณ๋ก ์ดํด๋ณผ๊ฒ.
๋ฉํฐ ๋ชจ๋ ๋ฐฉ์ ๋๋ ๋ ๋ฆฝ ๋ ํฌ์งํ ๋ฆฌ ๋ฐฉ์ ์ค ์ ํ. ์ด๊ธฐ์ ๋ฉํฐ ๋ชจ๋์ด ๊ด๋ฆฌํ๊ธฐ ํธํด.
start.spring.io์์ Spring Boot 3.x, Java 17+, ํ์ํ ์์กด์ฑ ์ ํ
Spring Cloud, Spring Security, Actuator ๋ฑ MSA ํ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ถ๊ฐ
Spring Cloud Gateway๋ก ๋ผ์ฐํ ๊ท์น ์ค์
Eureka Server ๋๋ Consul๋ก ์๋น์ค ๋ฑ๋ก/๋ฐ๊ฒฌ ๊ตฌํ
์ค์ build.gradle ์์๋ฅผ ๋ณด๋ฉด ์ด๋ฐ ์์ด์ผ:
// build.gradle (Spring Boot 3.x MSA ๊ธฐ๋ณธ ์ค์ )
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.0'
id 'io.spring.dependency-management' version '1.1.4'
}
java {
sourceCompatibility = JavaVersion.VERSION_17
}
dependencies {
// Spring Boot ๊ธฐ๋ณธ
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// Spring Cloud (MSA ํต์ฌ!)
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.cloud:spring-cloud-starter-config'
// ๋ณด์
implementation 'org.springframework.boot:spring-boot-starter-security'
// ๋ชจ๋ํฐ๋ง
implementation 'io.micrometer:micrometer-registry-prometheus'
// ํ
์คํธ
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:2023.0.0"
}
}
๋ฒ์ ํธํ์ฑ ํ๋ spring.io/projects/spring-cloud์์ ๊ผญ ํ์ธํ์!
๐ API Gateway ๊ตฌํ - ๋ชจ๋ ์์ฒญ์ ๊ด๋ฌธ
API Gateway๋ MSA์ ํ๋ก ํธ ๋์ด์ผ.
ํด๋ผ์ด์ธํธ๊ฐ ์ง์ ๊ฐ ๋ง์ดํฌ๋ก์๋น์ค์ ์ ๊ทผํ๋ ๊ฒ ์๋๋ผ,
๋ชจ๋ ์์ฒญ์ด Gateway๋ฅผ ํตํด ๋ค์ด์ค๊ณ ์ ์ ํ ์๋น์ค๋ก ๋ผ์ฐํ
๋๋ ๊ตฌ์กฐ์ง.
Spring Cloud Gateway๋ WebFlux ๊ธฐ๋ฐ์ ๋น๋๊ธฐยท๋
ผ๋ธ๋กํน ๋ฐฉ์์ผ๋ก ๋์ํด์
์ฑ๋ฅ์ด ๊ธฐ์กด Zuul๋ณด๋ค ํจ์ฌ ๋ฐ์ด๋. Spring Boot 3.0๊ณผ ์ฐฐ๋ก๊ถํฉ์ด์ผ! ๐ก
// application.yml - API Gateway ๋ผ์ฐํ
์ค์
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://USER-SERVICE # ๋ก๋๋ฐธ๋ฐ์๋ฅผ ํตํด Eureka์์ ์ฐพ์
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1 # /api ์ ๊ฑฐ ํ ์ ๋ฌ
- name: CircuitBreaker # ์ํท๋ธ๋ ์ด์ปค ์ ์ฉ
args:
name: userServiceCB
fallbackUri: forward:/fallback/user
- id: order-service
uri: lb://ORDER-SERVICE
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1
- AddRequestHeader=X-Request-Source, gateway # ํค๋ ์ถ๊ฐ
- id: payment-service
uri: lb://PAYMENT-SERVICE
predicates:
- Path=/api/payments/**
- Method=POST,GET # ํน์ HTTP ๋ฉ์๋๋ง ํ์ฉ
filters:
- StripPrefix=1
Gateway์์ JWT ์ธ์ฆ ํํฐ๋ฅผ ์ ์ญ์ผ๋ก ์ ์ฉํ๋ฉด
๊ฐ ๋ง์ดํฌ๋ก์๋น์ค์์ ์ธ์ฆ ๋ก์ง์ ์ค๋ณต ๊ตฌํํ ํ์๊ฐ ์์ด. ์ฝ๋ ์ฌ์ฌ์ฉ์ ๋ํ์! ๐ช
// JwtAuthenticationFilter.java
@Component
public class JwtAuthenticationFilter implements GlobalFilter, Ordered {
private final JwtTokenProvider jwtTokenProvider;
// ์ธ์ฆ ๋ถํ์ ๊ฒฝ๋ก
private static final List<String> WHITELIST = List.of(
"/api/users/login",
"/api/users/register",
"/actuator/health"
);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getPath().value();
// ํ์ดํธ๋ฆฌ์คํธ ๊ฒฝ๋ก๋ ํต๊ณผ
if (WHITELIST.stream().anyMatch(path::startsWith)) {
return chain.filter(exchange);
}
// Authorization ํค๋์์ JWT ์ถ์ถ
String token = extractToken(exchange.getRequest());
if (token == null || !jwtTokenProvider.validateToken(token)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ํค๋์ ์ถ๊ฐํด์ ํ์ ์๋น์ค๋ก ์ ๋ฌ
String userId = jwtTokenProvider.getUserId(token);
ServerHttpRequest mutatedRequest = exchange.getRequest()
.mutate()
.header("X-User-Id", userId)
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
}
@Override
public int getOrder() {
return -1; // ์ต์ฐ์ ์คํ
}
}
๐ ์๋น์ค ๋์ค์ปค๋ฒ๋ฆฌ - ์๋น์ค๋ค์ด ์๋ก๋ฅผ ์ฐพ๋ ๋ฐฉ๋ฒ
MSA์์ ์๋น์ค๋ค์ ๋์ ์ผ๋ก IP๊ฐ ๋ฐ๋๊ณ , ์ธ์คํด์ค ์๋ ์์๋ก ๋ณํด.
๊ทธ๋ผ ์๋น์ค A๊ฐ ์๋น์ค B๋ฅผ ์ด๋ป๊ฒ ์ฐพ์? ๐ค
๋ฐ๋ก ์๋น์ค ๋์ค์ปค๋ฒ๋ฆฌ๊ฐ ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํด์ค!
Spring Cloud Netflix Eureka๊ฐ ๊ฐ์ฅ ๋ง์ด ์ฐ์ด๋๋ฐ,
๊ฐ ์๋น์ค๊ฐ ์์๋ ๋ Eureka Server์ ์์ ์ ๋ฑ๋กํ๊ณ ,
๋ค๋ฅธ ์๋น์ค๋ฅผ ํธ์ถํ ๋ Eureka์์ ์ฃผ์๋ฅผ ์กฐํํ๋ ๋ฐฉ์์ด์ผ.
// EurekaServerApplication.java
@SpringBootApplication
@EnableEurekaServer // ์ด ํ ์ค๋ก Eureka Server ์์ฑ!
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
// application.yml (Eureka Server)
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false # ์๊ธฐ ์์ ์ ๋ฑ๋ก ์ ํจ
fetchRegistry: false
server:
enableSelfPreservation: false # ๊ฐ๋ฐํ๊ฒฝ์์๋ ๋นํ์ฑํ
// ๊ฐ ๋ง์ดํฌ๋ก์๋น์ค application.yml (Eureka Client)
spring:
application:
name: ORDER-SERVICE # ์๋น์ค ์ด๋ฆ (๋๋ฌธ์ ๊ถ์ฅ)
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
preferIpAddress: true
instanceId: ${spring.application.name}:${server.port}
spring-cloud-starter-netflix-eureka-client์์Spring Cloud LoadBalancer๊ฐ ๊ธฐ๋ณธ ๋ก๋๋ฐธ๋ฐ์๋ก ์ฌ์ฉ๋ผ.
๊ธฐ์กด Ribbon์ deprecated ๋์ผ๋ ๋ง์ด๊ทธ๋ ์ด์ ์ ๊ผญ ํ์ธํด!
๐จ ์๋น์ค ๊ฐ ํต์ - REST vs ๋ฉ์์ง ํ
MSA์์ ์๋น์ค ๊ฐ ํต์ ๋ฐฉ์์ ํฌ๊ฒ ๋ ๊ฐ์ง์ผ.
์ด๋ค ๊ฑธ ์ ํํ๋๋์ ๋ฐ๋ผ ์์คํ
์ ์ฑ๊ฒฉ์ด ์์ ํ ๋ฌ๋ผ์ ธ!
Spring WebClient OpenFeign RestTemplate(deprecated)
โ ๊ตฌํ์ด ๊ฐ๋จํ๊ณ ์ง๊ด์
โ ์ฆ๊ฐ์ ์ธ ์๋ต ํ์ธ ๊ฐ๋ฅ
โ ํธ์ถ๋ ์๋น์ค๊ฐ ์ฃฝ์ผ๋ฉด ํธ์ถํ ์๋น์ค๋ ์ํฅ๋ฐ์
โ ์ฐ์ ์ฅ์ (Cascading Failure) ์ํ
Apache Kafka RabbitMQ AWS SQS
โ ์๋น์ค ๊ฐ ๊ฒฐํฉ๋ ๋ฎ์ (๋์จํ ๊ฒฐํฉ)
โ ํ ์๋น์ค๊ฐ ์ฃฝ์ด๋ ๋ฉ์์ง๋ ํ์ ๋ณด์กด
โ ๋์ฉ๋ ํธ๋ํฝ ์ฒ๋ฆฌ์ ์ ๋ฆฌ
โ ์ฆ๊ฐ์ ์ธ ์๋ต์ด ํ์ํ ๊ฒฝ์ฐ์ ๋ถ์ ํฉ
โ ๊ตฌํ ๋ณต์ก๋ ์ฆ๊ฐ
Spring Boot 3.0์์ OpenFeign์ผ๋ก ๋๊ธฐ ํต์ ํ๋ ์์๋ฅผ ๋ณด์:
// OrderService์์ UserService ํธ์ถํ๋ Feign Client
@FeignClient(
name = "USER-SERVICE", // Eureka์ ๋ฑ๋ก๋ ์๋น์ค ์ด๋ฆ
fallback = UserClientFallback.class // ์ํท๋ธ๋ ์ด์ปค ํด๋ฐฑ
)
public interface UserServiceClient {
@GetMapping("/users/{userId}")
UserResponse getUserById(@PathVariable Long userId);
@PostMapping("/users/{userId}/points")
void deductPoints(
@PathVariable Long userId,
@RequestBody PointDeductRequest request
);
}
// ํด๋ฐฑ ๊ตฌํ (์๋น์ค ์ฅ์ ์ ๋์ฒด ์๋ต)
@Component
public class UserClientFallback implements UserServiceClient {
@Override
public UserResponse getUserById(Long userId) {
// ๊ธฐ๋ณธ๊ฐ ๋ฐํ ๋๋ ์บ์๋ ๋ฐ์ดํฐ ๋ฐํ
return UserResponse.defaultUser(userId);
}
@Override
public void deductPoints(Long userId, PointDeductRequest request) {
// ์คํจ ์ด๋ฒคํธ๋ฅผ ๋ฉ์์ง ํ์ ๋ฐํํด์ ๋์ค์ ์ฌ์ฒ๋ฆฌ
log.error("ํฌ์ธํธ ์ฐจ๊ฐ ์คํจ - userId: {}, ์ฌ์ฒ๋ฆฌ ํ์ ๋ฑ๋ก", userId);
}
}
์ด๋ฒ์ Kafka๋ฅผ ์ด์ฉํ ๋น๋๊ธฐ ์ด๋ฒคํธ ๋ฐํ ์์์ผ:
// ์ฃผ๋ฌธ ์๋ฃ ์ด๋ฒคํธ ๋ฐํ (Order Service)
@Service
@RequiredArgsConstructor
public class OrderService {
private final KafkaTemplate<String, OrderCompletedEvent> kafkaTemplate;
private static final String TOPIC = "order-completed";
public OrderResponse createOrder(OrderRequest request) {
// ์ฃผ๋ฌธ ์ฒ๋ฆฌ ๋ก์ง...
Order savedOrder = orderRepository.save(order);
// ์ด๋ฒคํธ ๋ฐํ - ๊ฒฐ์ /์๋ฆผ ์๋น์ค๊ฐ ๊ตฌ๋
ํด์ ์ฒ๋ฆฌ
OrderCompletedEvent event = OrderCompletedEvent.builder()
.orderId(savedOrder.getId())
.userId(request.getUserId())
.totalAmount(savedOrder.getTotalAmount())
.items(savedOrder.getItems())
.build();
kafkaTemplate.send(TOPIC, String.valueOf(savedOrder.getId()), event);
return OrderResponse.from(savedOrder);
}
}
// ์ด๋ฒคํธ ๊ตฌ๋
(Payment Service)
@Service
@RequiredArgsConstructor
public class PaymentEventConsumer {
@KafkaListener(topics = "order-completed", groupId = "payment-group")
public void handleOrderCompleted(OrderCompletedEvent event) {
log.info("์ฃผ๋ฌธ ์๋ฃ ์ด๋ฒคํธ ์์ - orderId: {}", event.getOrderId());
// ๊ฒฐ์ ์ฒ๋ฆฌ ๋ก์ง...
paymentService.processPayment(event);
}
}
๐ก๏ธ ์ํท๋ธ๋ ์ด์ปค ํจํด - ์ฅ์ ์ ํ๋ฅผ ๋ง์๋ผ!
MSA์์ ๊ฐ์ฅ ๋ฌด์์ด ๊ฑด ์ฐ์ ์ฅ์ (Cascading Failure)์ผ.
์๋น์ค A โ B โ C โ D ์์๋ก ํธ์ถํ๋๋ฐ D๊ฐ ์ฃฝ์ผ๋ฉด?
C๊ฐ D๋ฅผ ๊ธฐ๋ค๋ฆฌ๋ค ํ์์์, B๊ฐ C๋ฅผ ๊ธฐ๋ค๋ฆฌ๋ค ํ์์์, A๋ ์ฃฝ์ด๋ฒ๋ ค... ๐ฑ
์ด๊ฑธ ๋ง๋ ๊ฒ ๋ฐ๋ก ์ํท๋ธ๋ ์ด์ปค(Circuit Breaker) ํจํด์ด์ผ!
์ ๊ธฐ ํ๋ก์ ์ฐจ๋จ๊ธฐ์ฒ๋ผ, ์ฅ์ ๊ฐ ๊ฐ์ง๋๋ฉด ํ๋ก๋ฅผ ๋์ด๋ฒ๋ฆฌ๋ ๊ฑฐ์ง.
Spring Boot 3.0์์๋ Resilience4j๊ฐ ์ํท๋ธ๋ ์ด์ปค ํ์ค์ด์ผ:
// application.yml - Resilience4j ์ํท๋ธ๋ ์ด์ปค ์ค์
resilience4j:
circuitbreaker:
instances:
userService:
slidingWindowSize: 10 # ์ต๊ทผ 10๋ฒ์ ์์ฒญ ๊ธฐ์ค
failureRateThreshold: 50 # 50% ์คํจ ์ OPEN
waitDurationInOpenState: 10s # 10์ด ํ HALF-OPEN
permittedNumberOfCallsInHalfOpenState: 3
retry:
instances:
userService:
maxAttempts: 3 # ์ต๋ 3๋ฒ ์ฌ์๋
waitDuration: 1s
timelimiter:
instances:
userService:
timeoutDuration: 3s # 3์ด ํ์์์
// ์๋น์ค์์ ์ฌ์ฉ
@Service
public class OrderService {
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
@Retry(name = "userService")
@TimeLimiter(name = "userService")
public CompletableFuture<UserResponse> getUser(Long userId) {
return CompletableFuture.supplyAsync(() ->
userServiceClient.getUserById(userId)
);
}
// ํด๋ฐฑ ๋ฉ์๋
public CompletableFuture<UserResponse> getUserFallback(Long userId, Exception e) {
log.warn("User Service ์ฅ์ - ๊ธฐ๋ณธ๊ฐ ๋ฐํ, userId: {}", userId);
return CompletableFuture.completedFuture(UserResponse.defaultUser(userId));
}
}
๐ ๋ถ์ฐ ์ถ์ ๊ณผ ๋ชจ๋ํฐ๋ง - ์ด๋์ ๋ฌธ์ ๊ฐ ์๊ฒผ๋์ง ์์์ผ์ง!
MSA์์ ํ๋์ ์์ฒญ์ด ์ฌ๋ฌ ์๋น์ค๋ฅผ ๊ฑฐ์น๋ฉด,
๋ฌธ์ ๊ฐ ์๊ฒผ์ ๋ "์ด๋ ์๋น์ค์์ ๋๋ ค์ง ๊ฑฐ์ผ?"๋ฅผ ์๊ธฐ๊ฐ ์ ๋ง ์ด๋ ค์.
์ด๊ฑธ ํด๊ฒฐํ๋ ๊ฒ ๋ถ์ฐ ์ถ์ (Distributed Tracing)์ด์ผ!
Spring Boot 3.0์์๋ Micrometer Tracing์ด ๊ธฐ๋ณธ ํตํฉ๋์ด ์์ด.
Zipkin์ด๋ Jaeger์ ์ฐ๋ํ๋ฉด ์์ฒญ ํ๋ฆ์ ์๊ฐ์ ์ผ๋ก ๋ณผ ์ ์์ด. ๐
// build.gradle - ๋ถ์ฐ ์ถ์ ์์กด์ฑ
dependencies {
// Micrometer Tracing (Spring Boot 3.x ํ์ค)
implementation 'io.micrometer:micrometer-tracing-bridge-brave'
implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
// Prometheus ๋ฉํธ๋ฆญ
implementation 'io.micrometer:micrometer-registry-prometheus'
// Spring Boot Actuator (ํฌ์ค์ฒดํฌ, ๋ฉํธ๋ฆญ ์๋ํฌ์ธํธ)
implementation 'org.springframework.boot:spring-boot-starter-actuator'
}
// application.yml
management:
tracing:
sampling:
probability: 1.0 # 100% ์ํ๋ง (์ด์์์ 0.1 ์ ๋๋ก)
zipkin:
tracing:
endpoint: http://zipkin:9411/api/v2/spans
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
tags:
application: ${spring.application.name}
Grafana ๋์๋ณด๋ ์๊ฐํ
Zipkin / Jaeger ๋ถ์ฐ ์ถ์
ELK Stack ๋ก๊ทธ ์์งยท๋ถ์
Spring Boot Actuator ํฌ์ค์ฒดํฌ ์๋ํฌ์ธํธ
์ด ์กฐํฉ์ด๋ฉด MSA ์ด์ ์ ๋ฌธ์ ๋ฐ์ ์ ์์ธ ํ์ ์ด ํจ์ฌ ๋นจ๋ผ์ ธ!
๐ณ Docker + Kubernetes๋ก MSA ๋ฐฐํฌํ๊ธฐ
MSA๋ ์ปจํ
์ด๋ ์์ด๋ ์ด์ํ๊ธฐ ์ ๋ง ํ๋ค์ด.
์๋น์ค๊ฐ 10๊ฐ, 20๊ฐ๊ฐ ๋๋ฉด ๊ฐ๊ฐ ์๋์ผ๋ก ๋ฐฐํฌํ๋ ๊ฑด ๋ถ๊ฐ๋ฅ์ ๊ฐ๊น๊ฑฐ๋ .
Docker๋ก ์ปจํ
์ด๋ํํ๊ณ , Kubernetes(K8s)๋ก ์ค์ผ์คํธ๋ ์ด์
ํ๋ ๊ฒ ํ์
ํ์ค์ด์ผ! ๐ข
Spring Boot 3.0์ Buildpacks ์ง์์ผ๋ก Dockerfile ์์ด๋ ์ด๋ฏธ์ง ๋น๋๊ฐ ๊ฐ๋ฅํด:
# Dockerfile (Spring Boot 3.x ์ต์ ํ)
FROM eclipse-temurin:17-jre-alpine AS base
WORKDIR /app
# ๋ ์ด์ด ์บ์ฑ ์ต์ ํ (Spring Boot ๋ ์ด์ด๋ JAR)
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
RUN java -Djarmode=layertools -jar app.jar extract
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=base /app/dependencies/ ./
COPY --from=base /app/spring-boot-loader/ ./
COPY --from=base /app/snapshot-dependencies/ ./
COPY --from=base /app/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
# Gradle๋ก ์ง์ Docker ์ด๋ฏธ์ง ๋น๋ (Buildpacks ์ฌ์ฉ)
./gradlew bootBuildImage --imageName=my-service:latest
# ๋๋ build.gradle์ ์ค์
tasks.named("bootBuildImage") {
imageName = "my-registry/order-service:${version}"
builder = "paketobuildpacks/builder:base"
environment = ["BP_JVM_VERSION": "17"]
}
# kubernetes/order-service-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
labels:
app: order-service
spec:
replicas: 3 # 3๊ฐ ์ธ์คํด์ค ์ด์
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: my-registry/order-service:1.0.0
ports:
- containerPort: 8082
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: EUREKA_CLIENT_SERVICEURL_DEFAULTZONE
value: "http://eureka-server:8761/eureka/"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe: # ํฌ์ค์ฒดํฌ
httpGet:
path: /actuator/health/liveness
port: 8082
initialDelaySeconds: 30
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8082
initialDelaySeconds: 20
/actuator/health/liveness์ /actuator/health/readiness ์๋ํฌ์ธํธ๊ฐ์๋์ผ๋ก ์์ฑ๋๋๊น K8s ์ค์ ์ด ํจ์ฌ ๊ฐํธํด์ก์ด!
โก GraalVM Native Image - MSA์ ๋ฏธ๋
Spring Boot 3.0์ ๊ฐ์ฅ ํซํ ๊ธฐ๋ฅ์ด ๋ฐ๋ก GraalVM Native Image ์ง์์ด์ผ!
๊ธฐ์กด JVM ๋ฐฉ์๊ณผ ๋น๊ตํ๋ฉด ์ฐจ์ด๊ฐ ์ด๋ง์ด๋งํด:
# Native Image ๋น๋ (GraalVM ์ค์น ํ์)
./gradlew nativeCompile
# ๋๋ Docker๋ก Native Image ๋น๋
./gradlew bootBuildImage --imageName=order-service:native \
-Pspring.aot.enabled=true
# build.gradle์ AOT ์ค์
plugins {
id 'org.graalvm.buildtools.native' version '0.9.28'
}
graalvmNative {
binaries {
main {
imageName = 'order-service'
buildArgs.add('--initialize-at-build-time')
buildArgs.add('-H:+ReportExceptionStackTraces')
}
}
}
โก ๋์ ํด๋์ค ๋ก๋ฉ ์ ํ
โข ์ผ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ Native ๋ฏธ์ง์
โฃ ๋น๋ ์๊ฐ์ด JVM ๋ฐฉ์๋ณด๋ค ํจ์ฌ ๊ธธ์ด (CI/CD ํ์ดํ๋ผ์ธ ๊ณ ๋ ค ํ์)
Spring Boot 3.x๋ AOT(Ahead-of-Time) ์ฒ๋ฆฌ๋ก ์ด ๋ฌธ์ ๋ค์ ๋ง์ด ํด๊ฒฐํ์ด!
๐ MSA ๋ณด์ ์ ๋ต - Spring Security 6.0
Spring Boot 3.0์ Spring Security 6.0์ ๊ธฐ๋ณธ์ผ๋ก ์ฌ์ฉํด.
MSA ํ๊ฒฝ์์ ๋ณด์์ ๋ ๋ณต์กํด์ง๋๋ฐ, ์ฃผ์ ์ ๋ต์ ์ดํด๋ณด์.
OAuth 2.0 ํ์ค ์ธ์ฆ ํ๋ ์์ํฌ
Spring Authorization Server ์์ฒด ์ธ์ฆ ์๋ฒ ๊ตฌ์ถ
Keycloak ์คํ์์ค IAM ์๋ฃจ์
API Gateway์์ JWT ๊ฒ์ฆ ํ, ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ํค๋๋ก ์ ๋ฌํ๋ ํจํด์ด ๊ฐ์ฅ ์ผ๋ฐ์ ์ด์ผ.
// Spring Security 6.0 ์ค์ (Lambda DSL ๋ฐฉ์)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
// CSRF ๋นํ์ฑํ (REST API๋ ์ธ์
์์)
.csrf(csrf -> csrf.disable())
// ์ธ์
์ฌ์ฉ ์ ํจ (JWT ์ฌ์ฉ)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// ์์ฒญ ๊ถํ ์ค์
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health/**").permitAll()
.requestMatchers(HttpMethod.POST, "/users/register").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
// JWT ํํฐ ์ถ๊ฐ
.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class
)
// ์์ธ ์ฒ๋ฆฌ
.exceptionHandling(ex -> ex
.authenticationEntryPoint(customAuthenticationEntryPoint)
.accessDeniedHandler(customAccessDeniedHandler)
)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
๐ MSA ๋ฐ์ดํฐ ๊ด๋ฆฌ ์ ๋ต - ๊ฐ์์ DB๋ฅผ ๊ฐ์ ธ๋ผ!
MSA์ ํต์ฌ ์์น ์ค ํ๋๊ฐ "Database per Service"์ผ.
๊ฐ ์๋น์ค๊ฐ ์์ ๋ง์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๊ฐ์ง๊ณ , ๋ค๋ฅธ ์๋น์ค์ DB์ ์ง์ ์ ๊ทผํ์ง ์๋ ๊ฑฐ์ง.
๊ทผ๋ฐ ์ด๋ฌ๋ฉด ๋ฐ์ดํฐ ์ผ๊ด์ฑ ๋ฌธ์ ๊ฐ ์๊ฒจ.
์ฃผ๋ฌธ ์๋น์ค์์ ์ฃผ๋ฌธ์ ์์ฑํ๊ณ , ์ฌ๊ณ ์๋น์ค์์ ์ฌ๊ณ ๋ฅผ ์ฐจ๊ฐํ๋ ๊ฒ ์์์ ์ผ๋ก ์ด๋ฃจ์ด์ ธ์ผ ํ๋๋ฐ
๊ฐ์ ๋ค๋ฅธ DB๋ฅผ ์ฐ๋ฉด ํธ๋์ญ์
์ด ์ ๋์์? ๐ฐ
์ด๊ฑธ ํด๊ฒฐํ๋ ํจํด์ด ๋ฐ๋ก SAGA ํจํด์ด์ผ!
โ ๊ฒฐํฉ๋ ๋ฎ์, ๊ตฌํ ๋ณต์ก๋ ๋์
Orchestration SAGA: ์ค์ ์ค์ผ์คํธ๋ ์ดํฐ๊ฐ ๊ฐ ์๋น์ค์ ๋ช ๋ น์ ๋ด๋ฆฌ๊ณ ๊ฒฐ๊ณผ๋ฅผ ์์ง
โ ํ๋ฆ ํ์ ์ฌ์, ์ค์ผ์คํธ๋ ์ดํฐ๊ฐ ๋จ์ผ ์ฅ์ ์ ์ด ๋ ์ ์์
์คํจ ์ ๋ณด์ ํธ๋์ญ์ (Compensating Transaction)์ผ๋ก ๋กค๋ฐฑ ์ฒ๋ฆฌ!
๋ํ CQRS(Command Query Responsibility Segregation) ํจํด๋ MSA์์ ์์ฃผ ์ฐ์ฌ.
์ฐ๊ธฐ(Command)์ ์ฝ๊ธฐ(Query) ๋ชจ๋ธ์ ๋ถ๋ฆฌํด์ ๊ฐ๊ฐ ์ต์ ํํ๋ ๋ฐฉ์์ด์ผ.
์๋ฅผ ๋ค์ด ์ฐ๊ธฐ๋ MySQL, ์ฝ๊ธฐ๋ Elasticsearch๋ Redis๋ฅผ ์ฐ๋ ์์ผ๋ก!
๐ฏ ์ค์ MSA ๊ตฌํ ์ฒดํฌ๋ฆฌ์คํธ
์, ์ด์ ์ค์ ๋ก MSA ํ๋ก์ ํธ๋ฅผ ์์ํ ๋ ์ฒดํฌํด์ผ ํ ๊ฒ๋ค์ ์ ๋ฆฌํด๋ณผ๊ฒ.
์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ๊ฐ๋ฐ ์ฌ๋ฅ์ ๊ณต์ ํ๊ฑฐ๋ ๋ฐฐ์ธ ๋๋ ์ด ์ฒดํฌ๋ฆฌ์คํธ๊ฐ ๋์์ด ๋ ๊ฑฐ์ผ! ๐
์๋น์ค๊ฐ ์ฑ์ฅํ๋ฉด์ ์ ์ง์ ์ผ๋ก MSA๋ก ์ ํํ๋ ๊ฒ ํ๋ช ํ ์ ๋ต์ด์ผ.
"Strangler Fig Pattern"์ ๊ฒ์ํด๋ด! ๐ฟ
๐ Spring Boot 3.0 MSA ์ต์ ํธ๋ ๋
2024๋ ํ์ฌ Spring Boot 3.x MSA ์ํ๊ณ์์ ์ฃผ๋ชฉ๋ฐ๋ ํธ๋ ๋๋ค์ด์ผ:
spring.threads.virtual.enabled=true ํ ์ค๋ก ํ์ฑํ!๊ธฐ์กด ์ค๋ ๋ ํ ๋ฐฉ์๋ณด๋ค ํจ์ฌ ๋ง์ ๋์ ์์ฒญ์ ์ฒ๋ฆฌํ ์ ์์ด.
WebFlux ์์ด๋ ๋์ ๋์์ฑ์ ๋ฌ์ฑํ ์ ์๊ฒ ๋์ด. ๐
WebFlux(๋น๋๊ธฐ ์น) + R2DBC(๋น๋๊ธฐ DB ์ ๊ทผ)๋ก
I/O ๋ฐ์ด๋ ์์ ์์ ๊ทนํ์ ์ฑ๋ฅ์ ๋ฐํํด.
Spring AI ํ๋ก์ ํธ๊ฐ ํ๋ฐํ ๊ฐ๋ฐ ์ค์ด์ผ.
OpenAI, Anthropic, Ollama ๋ฑ ๋ค์ํ AI ๋ชจ๋ธ๊ณผ ์ฐ๋ ๊ฐ๋ฅ!
// Java 21 Virtual Threads ํ์ฑํ (Spring Boot 3.2+)
// application.yml
spring:
threads:
virtual:
enabled: true
// ๋๋ ์ฝ๋๋ก ์ค์
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
return protocolHandler -> {
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}
๐ฌ ๋ง๋ฌด๋ฆฌ - MSA, ์ธ์ ์จ์ผ ํ ๊น?
์์งํ ๋งํ ๊ฒ. MSA๊ฐ ํญ์ ์ ๋ต์ ์๋์ผ.
ํ์ด ์๊ณ , ์๋น์ค๊ฐ ๋จ์ํ๋ค๋ฉด ๋ชจ๋๋ฆฌ์์ด ํจ์ฌ ํจ์จ์ ์ผ ์ ์์ด.
MSA๋ฅผ ๊ณ ๋ คํด์ผ ํ ์์ ์ ์ด๋ด ๋์ผ:
โก ํน์ ๊ธฐ๋ฅ๋ง ๋ ๋ฆฝ์ ์ผ๋ก ์ค์ผ์ผ์์์ด ํ์ํ ๋
โข ์๋น์ค์ ์ผ๋ถ ๊ธฐ๋ฅ์ด ๋ค๋ฅธ ๊ธฐ์ ์คํ์ด ํ์ํ ๋
โฃ ์ฅ์ ๊ฒฉ๋ฆฌ๊ฐ ๋น์ฆ๋์ค์ ์ผ๋ก ์ค์ํด์ง ๋
โค ๋ฐฐํฌ ์ฃผ๊ธฐ๋ฅผ ์๋น์ค๋ณ๋ก ๋ ๋ฆฝ์ ์ผ๋ก ๊ฐ์ ธ๊ฐ๊ณ ์ถ์ ๋
Spring Boot 3.0์ ์ด ๋ชจ๋ MSA ๊ตฌํ์ ์ํ ์ต๊ณ ์ ๋๊ตฌ๋ฅผ ์ ๊ณตํด.
GraalVM Native Image, Virtual Threads, ๊ฐ์ ๋ Observability...
Java ์ํ๊ณ๊ฐ ์ด๋ ๊ฒ ๋น ๋ฅด๊ฒ ๋ฐ์ ํ๋ ๊ฒ ์ ๋ง ์ ๊ธฐํ๊ณ ํฅ๋ฏธ๋กญ์ง ์์? ๐
์ด ๊ธ์์ ๋ค๋ฃฌ ๋ด์ฉ๋ค์ ํ๋์ฉ ์ค์ตํด๋ณด๋ฉด์ ์ตํ๋๊ฐ๋ฉด ๋ผ.
์ฌ๋ฅ๋ท์์ Spring Boot๋ MSA ๊ด๋ จ ๋ฉํ ๋ง์ ์ฐพ์๋ณด๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ!
์ค๋ฌด ๊ฒฝํ์ด ์๋ ๊ฐ๋ฐ์์๊ฒ ์ง์ ๋ฐฐ์ฐ๋ฉด ํจ์ฌ ๋น ๋ฅด๊ฒ ์ฑ์ฅํ ์ ์๊ฑฐ๋ . ๐ฑ
MSA๋ ์ฒ์์ ๋ณต์กํด ๋ณด์ด์ง๋ง, ํ๋์ฉ ์ดํดํ๋ค ๋ณด๋ฉด
์ ํ๋ ๋๊ท๋ชจ ์๋น์ค๋ค์ด ์ด ๋ฐฉ์์ ์ ํํ๋์ง ์์ฐ์ค๋ฝ๊ฒ ๊นจ๋ซ๊ฒ ๋ ๊ฑฐ์ผ.
ํ์ดํ
! ๐ช
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.