

"이론으로만 알던 MSA를 실제로 구현해보니 생각보다 복잡했지만, 그만큼 배운 것도 많았습니다."
안녕하세요! 오늘은 Spring Boot와 React를 사용해서 완전한 마이크로서비스 아키텍처(MSA) 시스템을 구현한 경험을 공유해보려 합니다. 단순한 Hello World가 아닌, 실제 상용 서비스 수준의 전자상거래 시스템을 만들어봤어요.
회원가입 → 상품등록 → 장바구니 → 주문 → JWT 인증까지, 완전한 비즈니스 플로우를 가진 시스템입니다.
전통적인 모놀리식 구조에서는 한 부분의 변경이 전체 시스템에 영향을 미치는 문제가 있었습니다.
모놀리식 구조의 문제점:
❌ 부분 배포 불가 → 전체 시스템 재배포 필요
❌ 기술 스택 고정 → 서비스별 최적 기술 선택 불가
❌ 확장성 제한 → 전체 시스템 스케일업만 가능
❌ 장애 전파 → 한 모듈 장애가 전체 시스템 다운
🔥 핵심 장점들:
독립적인 배포와 확장
기술적 다양성
장애 격리
팀 독립성
graph TD
A[React Frontend :3000] --> B[API Gateway :8080]
B --> C[Eureka Server :8761]
B --> D[User Service :8081]
B --> E[Product Service :8082]
B --> F[Order Service :8083]
F --> E[Product Service via OpenFeign]
D --> G[H2 DB - users]
E --> H[H2 DB - products]
F --> I[H2 DB - orders]
| 컴포넌트 | 포트 | 역할 |
|---|---|---|
| Frontend | 3000 | React 사용자 인터페이스 |
| API Gateway | 8080 | 단일 진입점, 라우팅, CORS |
| Eureka Server | 8761 | 서비스 발견 및 등록 |
| User Service | 8081 | 사용자 관리 + JWT 인증 |
| Product Service | 8082 | 상품 관리, 재고 관리 |
| Order Service | 8083 | 주문 처리, 서비스 간 통신 |
백엔드:
프론트엔드:
가장 중요한 것은 올바른 멀티모듈 Gradle 구조를 만드는 것이었습니다.
settings.gradlerootProject.name = 'msa-system'
include 'eureka-server'
include 'api-gateway'
include 'user-service'
include 'product-service'
include 'order-service'
build.gradleplugins {
id 'org.springframework.boot' version '3.2.0' apply false
id 'io.spring.dependency-management' version '1.1.4' apply false
}
subprojects {
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
// 🔥 핵심: @PathVariable 파라미터 인식을 위한 설정
tasks.withType(JavaCompile) {
options.compilerArgs += ['-parameters']
}
}
💡 중요한 포인트: -parameters 옵션이 없으면 DELETE API에서 500 에러가 발생합니다!
MSA의 핵심인 서비스 발견 시스템부터 구축했습니다.
EurekaServerApplication.java@SpringBootApplication
@EnableEurekaServer // 이 한 줄로 Eureka 서버 완성!
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
application.ymlserver:
port: 8761
eureka:
client:
register-with-eureka: false # 자기 자신은 등록하지 않음
fetch-registry: false
server:
enable-self-preservation: false # 개발환경용 설정
결과: http://localhost:8761 에서 아름다운 Eureka 대시보드 완성!
가장 까다로웠던 부분입니다. 단순 CRUD가 아닌 완전한 JWT 인증 시스템을 구현했어요.
@Component
public class JwtTokenProvider {
private final String secretKey = "mySecretKeyForJwtTokenGenerationAndValidation1234567890";
private final long validityInMilliseconds = 86400000; // 24시간
public String createToken(User user) {
Claims claims = Jwts.claims().setSubject(user.getUsername());
claims.put("userId", user.getId());
claims.put("username", user.getUsername());
claims.put("role", user.getRole().toString());
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(validity)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
}
단순한 CRUD를 넘어서 실시간 재고 관리까지 구현했습니다.
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Double price;
private Integer stock; // 🔥 재고 관리의 핵심
private String category;
}
가장 복잡한 서비스입니다. OpenFeign을 사용한 서비스 간 통신과 트랜잭션 처리를 구현했어요.
@FeignClient(name = "product-service")
public interface ProductClient {
@GetMapping("/products/{id}")
ProductResponse getProduct(@PathVariable Long id);
@PutMapping("/products/{id}/stock")
void updateStock(@PathVariable Long id, @RequestBody Integer stock);
}
주문 처리 로직:
@Service
@Transactional
public class OrderService {
public Order createOrder(OrderRequest orderRequest) {
// 1. 상품 정보 조회 (다른 마이크로서비스 호출!)
ProductResponse product = productClient.getProduct(productId);
// 2. 재고 확인
if (product.getStock() < quantity) {
throw new RuntimeException("재고 부족!");
}
// 3. 재고 차감 (다른 마이크로서비스 업데이트!)
productClient.updateStock(productId, product.getStock() - quantity);
// 4. 주문 생성
return orderRepository.save(order);
}
}
모든 요청을 받아서 적절한 서비스로 라우팅하는 관문 역할입니다.
application.ymlspring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service # 로드밸런서 사용!
predicates:
- Path=/users/**
- id: product-service
uri: lb://product-service
predicates:
- Path=/products/**
- id: order-service
uri: lb://order-service
predicates:
- Path=/orders/**
# 🔥 CORS 문제 해결
globalcors:
cors-configurations:
'[/**]':
allowed-origins: "*"
allowed-methods: "*"
allowed-headers: "*"
API Gateway에서 모든 요청을 가로채서 JWT 검증을 수행합니다.
@Component
public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory<JwtAuthenticationFilter.Config> {
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
// 인증이 필요 없는 경로들
if (isPublicPath(request.getURI().getPath())) {
return chain.filter(exchange);
}
// JWT 토큰 검증
String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return onError(exchange, "Invalid Authorization header");
}
String token = authHeader.substring(7);
if (!jwtUtil.validateToken(token)) {
return onError(exchange, "Invalid JWT token");
}
// 검증 성공시 사용자 정보를 헤더에 추가
Long userId = jwtUtil.getUserIdFromToken(token);
ServerHttpRequest modifiedRequest = exchange.getRequest()
.mutate()
.header("X-User-Id", userId.toString())
.build();
return chain.filter(exchange.mutate().request(modifiedRequest).build());
};
}
}
탭 기반의 직관적인 UI로 모든 기능을 쉽게 사용할 수 있게 했습니다.
function App() {
const [activeTab, setActiveTab] = useState('users');
const [users, setUsers] = useState([]);
const [products, setProducts] = useState([]);
const [cart, setCart] = useState([]);
const [orders, setOrders] = useState([]);
}
const addToCart = (product) => {
const existingItem = cart.find(item => item.id === product.id);
if (existingItem) {
setCart(cart.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
));
} else {
setCart([...cart, { ...product, quantity: 1 }]);
}
setMessage(`${product.name}이(가) 장바구니에 추가되었습니다!`);
};
const createOrder = async () => {
const orderRequest = {
userId: users[0].id,
items: cart.map(item => ({
productId: item.id,
quantity: item.quantity
}))
};
await axios.post('/orders', orderRequest);
setCart([]);
setMessage('주문이 성공적으로 생성되었습니다!');
fetchOrders(); // 주문 목록 새로고침
};
문제: Order ↔ OrderItem 양방향 참조로 인한 StackOverflowError
// 해결책: Jackson 어노테이션 사용
@Entity
public class Order {
@OneToMany(mappedBy = "order")
@JsonManagedReference // 부모에서 자식 직렬화
private List<OrderItem> orderItems;
}
@Entity
public class OrderItem {
@ManyToOne
@JsonBackReference // 자식에서 부모 직렬화 제외
private Order order;
}
문제: DELETE API에서 500 에러 발생
// 해결책: 컴파일 시 파라미터 정보 포함
tasks.withType(JavaCompile) {
options.compilerArgs += ['-parameters']
}
OpenFeign 사용으로 선언적 HTTP 클라이언트 구현:
@FeignClient(name = "product-service")
public interface ProductClient {
@GetMapping("/products/{id}")
ProductResponse getProduct(@PathVariable Long id);
}
// 사용법이 정말 간단!
ProductResponse product = productClient.getProduct(1L);
API Gateway에서 중앙화된 CORS 설정:
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowed-origins: "*"
allowed-methods: "*"
allowed-headers: "*"
1. 사용자 등록 → "홍길동" 회원가입
2. 상품 등록 → "iPhone 15" (가격: 1,200,000원, 재고: 10개)
3. 장바구니 추가 → iPhone 15 * 2개 담기
4. 주문 생성 → 총 2,400,000원 주문
5. 재고 확인 → iPhone 15 재고가 8개로 자동 차감!
6. 주문 조회 → 생성된 주문 정보 확인
# 사용자 생성
POST http://localhost:8080/users
{
"name": "홍길동",
"email": "hong@test.com",
"phone": "010-1234-5678"
}
# 상품 생성
POST http://localhost:8080/products
{
"name": "iPhone 15",
"description": "최신 아이폰",
"price": 1200000,
"stock": 10,
"category": "전자제품"
}
# 주문 생성 (서비스 간 통신 테스트)
POST http://localhost:8080/orders
{
"userId": 1,
"items": [
{
"productId": 1,
"quantity": 2
}
]
}
# 서비스 인스턴스 복제 테스트
docker-compose up -d --scale user-service=3 --scale product-service=2
현재 시스템은 완전한 MSA의 기초를 다졌습니다. 다음 단계로는:
처음부터 모든 기능을 만들려 하지 마세요. 저는 이런 순서로 진행했습니다:
1. 단일 서비스로 시작 → 2. Eureka 추가 → 3. 서비스 분리 → 4. Gateway 추가
MSA에서는 여러 서비스 간의 상호작용이 복잡합니다. 문제 발생 시 각 서비스의 로그를 모두 확인해야 해요.
각 서비스를 독립적으로 먼저 테스트한 후에 통합 테스트를 진행하세요.
해결한 문제들을 반드시 문서로 기록하세요. 같은 문제를 다시 겪지 않기 위해서죠.
이번 MSA 구현 프로젝트를 통해 정말 많은 것을 배웠습니다. 단순한 Hello World 수준을 넘어서 실제 서비스 수준의 기능을 구현할 수 있었고, MSA의 장점과 복잡성을 모두 경험해볼 수 있었어요.
가장 인상적이었던 부분:
앞으로 MSA를 시작하려는 분들께:
완전한 소스 코드는 GitHub에서 확인하실 수 있습니다. 질문이나 피드백은 언제든 환영입니다! 🚀
이 글이 도움이 되셨다면 좋아요와 댓글 부탁드려요! 여러분의 MSA 구현 경험도 궁금합니다. 🙌