맛보기 시리즈1 - MSA

MJ·2025년 8월 29일

완전한 MSA 전자상거래 시스템 구현기 - JWT 인증부터 서비스 간 통신까지


"이론으로만 알던 MSA를 실제로 구현해보니 생각보다 복잡했지만, 그만큼 배운 것도 많았습니다."

안녕하세요! 오늘은 Spring BootReact를 사용해서 완전한 마이크로서비스 아키텍처(MSA) 시스템을 구현한 경험을 공유해보려 합니다. 단순한 Hello World가 아닌, 실제 상용 서비스 수준의 전자상거래 시스템을 만들어봤어요.

🎯 완성된 시스템 미리보기

회원가입 → 상품등록 → 장바구니 → 주문 → JWT 인증까지, 완전한 비즈니스 플로우를 가진 시스템입니다.

시스템 스크린샷 1
시스템 스크린샷 2


🤔 왜 MSA를 선택했을까?

모놀리식 아키텍처의 한계

전통적인 모놀리식 구조에서는 한 부분의 변경이 전체 시스템에 영향을 미치는 문제가 있었습니다.

모놀리식 구조의 문제점:
❌ 부분 배포 불가 → 전체 시스템 재배포 필요
❌ 기술 스택 고정 → 서비스별 최적 기술 선택 불가
❌ 확장성 제한 → 전체 시스템 스케일업만 가능
❌ 장애 전파 → 한 모듈 장애가 전체 시스템 다운

MSA가 제공하는 장점

🔥 핵심 장점들:

  1. 독립적인 배포와 확장

    • 각 서비스를 개별적으로 배포/확장 가능
    • 트래픽이 많은 서비스만 스케일 아웃
  2. 기술적 다양성

    • 서비스별로 최적의 기술 스택 선택
    • User Service는 Spring Boot, Notification Service는 Node.js 등
  3. 장애 격리

    • 한 서비스의 장애가 다른 서비스에 영향 없음
    • Circuit Breaker 패턴으로 장애 전파 방지
  4. 팀 독립성

    • 서비스별로 팀이 독립적으로 개발
    • API 계약만 지키면 내부 구현은 자유

🏗️ 시스템 아키텍처 설계

전체 시스템 구조

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]

핵심 구성 요소

컴포넌트포트역할
Frontend3000React 사용자 인터페이스
API Gateway8080단일 진입점, 라우팅, CORS
Eureka Server8761서비스 발견 및 등록
User Service8081사용자 관리 + JWT 인증
Product Service8082상품 관리, 재고 관리
Order Service8083주문 처리, 서비스 간 통신

선택한 기술 스택

백엔드:

  • Java 17 (LTS 안정성)
  • Gradle 8.10 (멀티모듈 빌드)
  • Spring Boot 3.2 (최신 안정 버전)
  • Spring Cloud Gateway (비동기 API 게이트웨이)
  • Netflix Eureka (서비스 발견)
  • OpenFeign (선언적 HTTP 클라이언트)

프론트엔드:

  • React 18 (최신 Hook 기반)
  • Axios (HTTP 클라이언트)

🛠️ 단계별 구현 과정

1단계: 프로젝트 기반 구조 설정

가장 중요한 것은 올바른 멀티모듈 Gradle 구조를 만드는 것이었습니다.

settings.gradle

rootProject.name = 'msa-system'

include 'eureka-server'
include 'api-gateway'
include 'user-service'
include 'product-service'
include 'order-service'

루트 build.gradle

plugins {
    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 에러가 발생합니다!

2단계: 서비스 발견 시스템 (Eureka Server)

MSA의 핵심인 서비스 발견 시스템부터 구축했습니다.

EurekaServerApplication.java

@SpringBootApplication
@EnableEurekaServer  // 이 한 줄로 Eureka 서버 완성!
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

application.yml

server:
  port: 8761

eureka:
  client:
    register-with-eureka: false  # 자기 자신은 등록하지 않음
    fetch-registry: false
  server:
    enable-self-preservation: false  # 개발환경용 설정

결과: http://localhost:8761 에서 아름다운 Eureka 대시보드 완성!

3단계: 핵심 비즈니스 서비스들

User Service - JWT 인증 시스템

가장 까다로웠던 부분입니다. 단순 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();
    }
}

Product Service - 재고 관리 시스템

단순한 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;
}

Order Service - 서비스 간 통신

가장 복잡한 서비스입니다. 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);
    }
}

4단계: API Gateway - 단일 진입점

모든 요청을 받아서 적절한 서비스로 라우팅하는 관문 역할입니다.

application.yml

spring:
  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: "*"

JWT 인증 필터

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());
        };
    }
}

5단계: React 프론트엔드 - 사용자 경험

탭 기반의 직관적인 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(); // 주문 목록 새로고침
};

🔥 핵심 기술적 해결책들

1. JSON 순환 참조 문제

문제: 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;
}

2. @PathVariable 파라미터 인식 실패

문제: DELETE API에서 500 에러 발생

// 해결책: 컴파일 시 파라미터 정보 포함
tasks.withType(JavaCompile) {
    options.compilerArgs += ['-parameters']
}

3. 서비스 간 통신 최적화

OpenFeign 사용으로 선언적 HTTP 클라이언트 구현:

@FeignClient(name = "product-service")
public interface ProductClient {
    @GetMapping("/products/{id}")
    ProductResponse getProduct(@PathVariable Long id);
}

// 사용법이 정말 간단!
ProductResponse product = productClient.getProduct(1L);

4. CORS 문제 완전 해결

API Gateway에서 중앙화된 CORS 설정:

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowed-origins: "*"
            allowed-methods: "*" 
            allowed-headers: "*"

🚀 완성된 기능들

✅ 완전한 비즈니스 플로우

  1. 👤 사용자 관리: 회원가입, 로그인, JWT 토큰 발급
  2. 📦 상품 관리: CRUD, 재고 관리, 카테고리 검색
  3. 🛒 장바구니: 실시간 추가/삭제, 수량 조절
  4. 📋 주문 처리: 자동 재고 차감, 트랜잭션 처리
  5. 🔐 보안: JWT 기반 인증, API Gateway 필터

✅ MSA 핵심 패턴 구현

  • 서비스 발견 패턴 (Eureka)
  • API 게이트웨이 패턴 (Spring Cloud Gateway)
  • 서비스 간 통신 패턴 (OpenFeign)
  • 데이터베이스 per 서비스 (각 서비스별 독립 DB)

✅ 사용자 경험

  • 직관적인 탭 기반 UI
  • 실시간 피드백 (성공/실패 메시지)
  • 반응형 디자인 (모바일 지원)

🎯 실제 테스트 시나리오

전체 플로우 테스트

1. 사용자 등록 → "홍길동" 회원가입
2. 상품 등록 → "iPhone 15" (가격: 1,200,000원, 재고: 10개)
3. 장바구니 추가 → iPhone 15 * 2개 담기
4. 주문 생성 → 총 2,400,000원 주문
5. 재고 확인 → iPhone 15 재고가 8개로 자동 차감!
6. 주문 조회 → 생성된 주문 정보 확인

API 테스트 (Postman)

# 사용자 생성
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

장애 격리 테스트

  • Product Service 중단 → Order Service에서 Circuit Breaker 작동
  • User Service 중단 → 인증이 필요 없는 상품 조회는 정상 작동

🔮 향후 확장 계획

현재 시스템은 완전한 MSA의 기초를 다졌습니다. 다음 단계로는:

고급 MSA 패턴 적용

  • Circuit Breaker (Resilience4j)
  • 분산 추적 (Sleuth + Zipkin)
  • 중앙화 로깅 (ELK Stack)
  • 메시지 큐 (RabbitMQ, Kafka)

클라우드 네이티브 전환

  • Kubernetes 배포
  • Istio 서비스 메시 적용
  • Prometheus + Grafana 모니터링

DevOps 파이프라인

  • Jenkins CI/CD 구축
  • Docker 컨테이너화 완료
  • Helm Chart 배포 자동화

💡 학습 포인트와 조언

1. 단계적 접근의 중요성

처음부터 모든 기능을 만들려 하지 마세요. 저는 이런 순서로 진행했습니다:
1. 단일 서비스로 시작 → 2. Eureka 추가 → 3. 서비스 분리 → 4. Gateway 추가

2. 로그를 꼼꼼히 확인하자

MSA에서는 여러 서비스 간의 상호작용이 복잡합니다. 문제 발생 시 각 서비스의 로그를 모두 확인해야 해요.

3. 독립 테스트 먼저

각 서비스를 독립적으로 먼저 테스트한 후에 통합 테스트를 진행하세요.

4. 문서화는 필수

해결한 문제들을 반드시 문서로 기록하세요. 같은 문제를 다시 겪지 않기 위해서죠.


📚 참고 자료


🎉 마무리

이번 MSA 구현 프로젝트를 통해 정말 많은 것을 배웠습니다. 단순한 Hello World 수준을 넘어서 실제 서비스 수준의 기능을 구현할 수 있었고, MSA의 장점과 복잡성을 모두 경험해볼 수 있었어요.

가장 인상적이었던 부분:

  • 서비스 간 통신이 이렇게 간단할 수 있다니! (OpenFeign 덕분)
  • JWT 인증을 API Gateway에서 중앙화 처리하는 것의 편리함
  • React와 Spring Boot의 완벽한 조합

앞으로 MSA를 시작하려는 분들께:

  • 작게 시작해서 점진적으로 확장하세요
  • 각 단계별로 충분히 테스트하고 넘어가세요
  • 문제를 겪는 것은 자연스러운 과정입니다. 포기하지 마세요!

완전한 소스 코드는 GitHub에서 확인하실 수 있습니다. 질문이나 피드백은 언제든 환영입니다! 🚀


이 글이 도움이 되셨다면 좋아요와 댓글 부탁드려요! 여러분의 MSA 구현 경험도 궁금합니다. 🙌

profile
..

0개의 댓글