"MSA 도입하셨다고요?" 면접관이 파고드는 마이크로서비스 아키텍처의 모든 것

TeamGrit·2026년 2월 15일

Interview-Question

목록 보기
12/12
post-thumbnail

안녕하세요, QueryDaily 팀입니다.

"마이크로서비스 아키텍처로 전환했습니다!"

이력서에 이렇게 쓰면 면접관의 눈이 반짝입니다. 그리고 바로 꼬리 질문이 날아옵니다.

"왜 MSA로 전환하셨나요?"
"서비스 간 통신은 어떻게 하셨나요?"
"분산 트랜잭션은 어떻게 처리하셨나요?"
"모놀리식과 비교했을 때 어떤 문제를 겪으셨나요?"

혹시 막막하셨나요? MSA는 단순히 "서비스를 쪼개는 것"이 아닙니다.
서비스 분리 기준, 데이터 관리, 장애 처리, 모니터링 등 고려해야 할 것이 산더미입니다.

오늘은 MSA가 무엇인지, 언제 도입해야 하는지, 그리고 어떤 문제가 발생하고 어떻게 해결하는지 완벽하게 정리해 드리겠습니다.


마이크로서비스란 무엇인가?

마이크로서비스 아키텍처(MSA)를 한 문장으로 정의하면:

독립적으로 배포 가능한 작은 서비스들의 집합으로 애플리케이션을 구성하는 아키텍처 패턴

모놀리식 vs 마이크로서비스

모놀리식 (Monolithic)

┌─────────────────────────────────┐
│      하나의 거대한 애플리케이션      │
│                                 │
│  ┌──────────────────────────┐  │
│  │   사용자 관리               │  │
│  ├──────────────────────────┤  │
│  │   주문 관리                │  │
│  ├──────────────────────────┤  │
│  │   결제 처리                │  │
│  ├──────────────────────────┤  │
│  │   배송 관리                │  │
│  └──────────────────────────┘  │
│                                 │
│      공유 데이터베이스             │
└─────────────────────────────────┘

마이크로서비스 (Microservices)

┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐
│ 사용자     │  │  주문     │  │  결제     │  │  배송     │
│ 서비스     │  │ 서비스    │  │ 서비스    │  │ 서비스      │
└────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘
     │             │              │              │
┌────▼─────┐  ┌───▼──────┐  ┌───▼──────┐  ┌───▼──────┐
│  User DB │  │ Order DB │  │Payment DB│  │Delivery DB│
└──────────┘  └──────────┘  └──────────┘  └──────────┘

왜 MSA를 도입하는가?

1. 독립적인 배포

[모놀리식]
결제 기능 수정 → 전체 애플리케이션 재배포
→ 배포 시간: 30분
→ 위험도: 높음 (모든 기능 영향)

[MSA]
결제 서비스만 수정 → 결제 서비스만 재배포
→ 배포 시간: 5분
→ 위험도: 낮음 (결제 기능만 영향)

2. 기술 스택의 자유

// 사용자 서비스: Kotlin + Spring Boot
@RestController
class UserController {
    @GetMapping("/users/{id}")
    fun getUser(@PathVariable id: Long): User {
        return userService.findById(id)
    }
}

// 추천 서비스: Python + FastAPI (ML 라이브러리 활용)
@app.get("/recommendations/{user_id}")
def get_recommendations(user_id: int):
    return recommendation_engine.predict(user_id)

// 실시간 채팅: Node.js + Socket.io (비동기 이벤트 처리)
io.on('connection', (socket) => {
    socket.on('message', (msg) => {
        io.emit('message', msg);
    });
});

각 서비스가 최적의 기술을 선택할 수 있습니다.

3. 확장성

[트래픽 상황]
- 사용자 서비스: 100 req/s
- 주문 서비스: 1000 req/s (블랙프라이데이 이벤트)
- 결제 서비스: 100 req/s

[모놀리식]
전체 애플리케이션을 10배 확장 → 비효율적

[MSA]
주문 서비스만 10배 확장 → 비용 효율적

4. 장애 격리

[모놀리식]
결제 서비스 장애 → 전체 애플리케이션 다운 💥

[MSA]
결제 서비스 장애 → 결제만 불가, 나머지 정상 ✅
+ Circuit Breaker로 빠른 실패 처리

MSA 도입 시 해결해야 할 과제

1. 서비스 간 통신

동기 통신: REST API

// 주문 서비스에서 사용자 서비스 호출
@Service
class OrderService(
    private val webClient: WebClient
) {
    suspend fun createOrder(request: CreateOrderRequest): Order {
        // 사용자 정보 조회
        val user = webClient.get()
            .uri("http://user-service/api/users/${request.userId}")
            .retrieve()
            .awaitBody<User>()
        
        // 주문 생성
        return orderRepository.save(Order(
            userId = user.id,
            productId = request.productId,
            // ...
        ))
    }
}

문제점:

  • 사용자 서비스가 다운되면 주문도 불가
  • 네트워크 지연 누적 (Latency)

비동기 통신: 메시지 큐

// 주문 서비스: 이벤트 발행
@Service
class OrderService(
    private val kafkaTemplate: KafkaTemplate<String, OrderCreatedEvent>
) {
    fun createOrder(request: CreateOrderRequest): Order {
        val order = orderRepository.save(Order(request))
        
        // 이벤트 발행 (Fire-and-forget)
        kafkaTemplate.send("order-created", OrderCreatedEvent(
            orderId = order.id,
            userId = order.userId,
            amount = order.amount
        ))
        
        return order
    }
}

// 배송 서비스: 이벤트 구독
@KafkaListener(topics = ["order-created"])
fun handleOrderCreated(event: OrderCreatedEvent) {
    // 배송 준비
    deliveryService.prepareDelivery(event.orderId)
}

// 알림 서비스: 이벤트 구독
@KafkaListener(topics = ["order-created"])
fun handleOrderCreated(event: OrderCreatedEvent) {
    // 주문 확인 알림 발송
    notificationService.sendOrderConfirmation(event.userId)
}

장점:

  • 서비스 간 결합도 낮음
  • 장애 전파 방지
  • 확장성 좋음

2. 분산 트랜잭션

문제 상황:

주문 생성 플로우:
1. 주문 서비스: 주문 생성
2. 결제 서비스: 결제 처리
3. 재고 서비스: 재고 차감

→ 결제는 성공했는데 재고 차감 실패하면? 😱

해결책: Saga 패턴

Choreography 방식 (이벤트 기반)

// 1. 주문 서비스: 주문 생성 후 이벤트 발행
fun createOrder(request: CreateOrderRequest): Order {
    val order = orderRepository.save(Order(request, status = PENDING))
    eventPublisher.publish(OrderCreatedEvent(order.id))
    return order
}

// 2. 결제 서비스: 결제 처리 후 이벤트 발행
@EventListener
fun onOrderCreated(event: OrderCreatedEvent) {
    try {
        val payment = processPayment(event)
        eventPublisher.publish(PaymentCompletedEvent(event.orderId, payment.id))
    } catch (e: PaymentFailedException) {
        eventPublisher.publish(PaymentFailedEvent(event.orderId))
    }
}

// 3. 재고 서비스: 재고 차감 후 이벤트 발행
@EventListener
fun onPaymentCompleted(event: PaymentCompletedEvent) {
    try {
        decreaseStock(event.orderId)
        eventPublisher.publish(StockDecreasedEvent(event.orderId))
    } catch (e: InsufficientStockException) {
        eventPublisher.publish(StockDecreaseFailedEvent(event.orderId))
    }
}

// 4. 주문 서비스: 최종 상태 업데이트 또는 보상 트랜잭션
@EventListener
fun onStockDecreased(event: StockDecreasedEvent) {
    orderRepository.updateStatus(event.orderId, COMPLETED)
}

@EventListener
fun onPaymentFailed(event: PaymentFailedEvent) {
    orderRepository.updateStatus(event.orderId, FAILED)
}

@EventListener
fun onStockDecreaseFailed(event: StockDecreaseFailedEvent) {
    // 보상 트랜잭션: 결제 취소
    paymentService.refund(event.orderId)
    orderRepository.updateStatus(event.orderId, FAILED)
}

Orchestration 방식 (중앙 관리)

// Saga Orchestrator
@Service
class OrderSagaOrchestrator(
    private val orderService: OrderService,
    private val paymentService: PaymentService,
    private val stockService: StockService
) {
    suspend fun executeOrderSaga(request: CreateOrderRequest): SagaResult {
        var orderId: Long? = null
        var paymentId: Long? = null
        
        try {
            // Step 1: 주문 생성
            orderId = orderService.createOrder(request)
            
            // Step 2: 결제 처리
            paymentId = paymentService.processPayment(orderId, request.amount)
            
            // Step 3: 재고 차감
            stockService.decreaseStock(request.productId, request.quantity)
            
            // 모든 단계 성공
            orderService.completeOrder(orderId)
            return SagaResult.success(orderId)
            
        } catch (e: Exception) {
            // 보상 트랜잭션 (Rollback)
            if (paymentId != null) {
                paymentService.refund(paymentId)
            }
            if (orderId != null) {
                orderService.cancelOrder(orderId)
            }
            
            return SagaResult.failure(e.message)
        }
    }
}

3. 데이터 일관성

문제:

사용자 서비스의 User 정보가 변경되었을 때,
주문 서비스는 어떻게 알 수 있나?

해결책 1: Event Sourcing

// 사용자 정보 변경 시 이벤트 발행
@Service
class UserService {
    fun updateUserInfo(userId: Long, info: UserInfo) {
        userRepository.update(userId, info)
        
        // 이벤트 발행
        eventPublisher.publish(UserInfoUpdatedEvent(
            userId = userId,
            name = info.name,
            email = info.email
        ))
    }
}

// 주문 서비스에서 필요한 사용자 정보 캐싱
@Service
class UserInfoCache {
    private val cache = ConcurrentHashMap<Long, UserInfo>()
    
    @EventListener
    fun onUserInfoUpdated(event: UserInfoUpdatedEvent) {
        cache[event.userId] = UserInfo(event.name, event.email)
    }
    
    fun getUserInfo(userId: Long): UserInfo? {
        return cache[userId]
    }
}

해결책 2: CQRS (Command Query Responsibility Segregation)

[Command Side]
사용자 서비스 → User DB (Write)

[Query Side]
사용자 정보 Read Model → Elasticsearch (Read 최적화)

[동기화]
User DB 변경 → Event → Read Model 업데이트

4. API Gateway

클라이언트가 여러 서비스를 직접 호출하면 복잡합니다.

[문제]
모바일 앱 → 사용자 서비스 (3초)
         → 주문 서비스 (2초)
         → 추천 서비스 (4초)
총 9초 소요 + 3번의 네트워크 왕복

[해결: API Gateway]
모바일 앱 → API Gateway → 병렬로 3개 서비스 호출 → 결과 조합
총 4초 소요 + 1번의 네트워크 왕복

Spring Cloud Gateway 예시:

@Configuration
class GatewayConfig {
    
    @Bean
    fun routeLocator(builder: RouteLocatorBuilder): RouteLocator {
        return builder.routes()
            .route("user-service") { r ->
                r.path("/api/users/**")
                    .filters { f ->
                        f.addRequestHeader("X-Gateway-Request-Id", UUID.randomUUID().toString())
                         .circuitBreaker { c ->
                             c.setName("userServiceCircuitBreaker")
                              .setFallbackUri("forward:/fallback/user")
                         }
                    }
                    .uri("lb://user-service")
            }
            .route("order-service") { r ->
                r.path("/api/orders/**")
                    .uri("lb://order-service")
            }
            .build()
    }
}

서비스 디스커버리

서비스 인스턴스가 동적으로 변하는 환경에서 서로를 찾는 방법입니다.

Netflix Eureka 예시:

# application.yml (Eureka Server)
server:
  port: 8761

eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
// User Service (Eureka Client)
@SpringBootApplication
@EnableEurekaClient
class UserServiceApplication

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

spring:
  application:
    name: user-service
// Order Service에서 User Service 호출
@Service
class OrderService(
    @LoadBalanced private val webClient: WebClient.Builder
) {
    suspend fun getUser(userId: Long): User {
        // "user-service"는 논리적 이름, Eureka가 실제 IP로 변환
        return webClient.build()
            .get()
            .uri("http://user-service/api/users/$userId")
            .retrieve()
            .awaitBody()
    }
}

Circuit Breaker 패턴

장애가 전파되는 것을 방지합니다.

@Service
class PaymentService(
    private val paymentClient: PaymentClient
) {
    
    @CircuitBreaker(name = "payment", fallbackMethod = "paymentFallback")
    fun processPayment(amount: Long): PaymentResult {
        return paymentClient.pay(amount)
    }
    
    // Fallback 메서드
    fun paymentFallback(amount: Long, ex: Exception): PaymentResult {
        log.error("결제 서비스 장애, Fallback 실행", ex)
        return PaymentResult.pending("결제 서비스가 일시적으로 불가합니다. 잠시 후 다시 시도해주세요.")
    }
}

Circuit Breaker 상태:

[CLOSED] 정상 상태
→ 실패 횟수 임계값 초과
[OPEN] 모든 요청 즉시 차단 (Fallback 실행)
→ 일정 시간 경과
[HALF_OPEN] 일부 요청만 허용
→ 성공 시 CLOSED, 실패 시 다시 OPEN

모니터링과 추적

MSA에서는 하나의 요청이 여러 서비스를 거칩니다.

분산 추적 (Distributed Tracing): Zipkin + Sleuth

사용자 요청 → API Gateway → User Service → Order Service → Payment Service
    [Trace ID: abc-123]
           └─ [Span ID: 001] Gateway
                  └─ [Span ID: 002] User Service
                         └─ [Span ID: 003] Order Service
                                └─ [Span ID: 004] Payment Service
# application.yml
spring:
  sleuth:
    sampler:
      probability: 1.0  # 100% 샘플링 (개발 환경)
  zipkin:
    base-url: http://localhost:9411

Zipkin UI에서 전체 요청 흐름과 각 서비스의 소요 시간을 시각화할 수 있습니다.


면접 예상 질문

Q1. "MSA와 모놀리식의 차이는 무엇인가요?"

핵심 답변: 모놀리식은 모든 기능이 하나의 애플리케이션에 통합되어 있어 배포가 간단하지만, 규모가 커지면 배포 속도가 느려지고 장애가 전체에 영향을 줍니다. MSA는 기능별로 독립적인 서비스로 분리하여 각 서비스를 독립적으로 배포하고 확장할 수 있지만, 서비스 간 통신과 분산 트랜잭션 같은 복잡성이 증가합니다.

Q2. "언제 MSA를 도입해야 하나요?"

핵심 답변: 팀 규모가 크고, 서비스 간 배포 주기가 다르며, 특정 기능만 높은 트래픽을 받을 때 도입을 고려합니다. 단, 초기 스타트업이나 작은 서비스에서는 모놀리식이 더 효율적입니다. MSA는 조직의 성숙도, 인프라 역량, 비즈니스 복잡도를 고려하여 신중히 결정해야 합니다.

Q3. "분산 트랜잭션은 어떻게 처리하나요?"

핵심 답변: 2PC(Two-Phase Commit)는 성능과 가용성 문제로 실무에서 거의 사용하지 않습니다. 대신 Saga 패턴을 사용합니다. Choreography 방식은 각 서비스가 이벤트를 주고받으며 자율적으로 동작하고, Orchestration 방식은 중앙 오케스트레이터가 전체 플로우를 관리합니다. 실패 시 보상 트랜잭션으로 롤백합니다.

Q4. "서비스 간 통신은 동기와 비동기 중 무엇을 선택하나요?"

핵심 답변: 즉시 응답이 필요한 경우 동기 통신(REST, gRPC)을 사용하고, 느슨한 결합이 필요하거나 이벤트 기반 처리가 적합한 경우 비동기 통신(Kafka, RabbitMQ)을 사용합니다. 실무에서는 두 방식을 혼합하여 사용하며, Circuit Breaker로 장애 전파를 방지하고 타임아웃을 적절히 설정합니다.

Q5. "MSA 도입 시 가장 어려웠던 점은?"

핵심 답변 팁: 실제 경험을 구체적으로 말하세요. 예: "데이터 정합성 문제가 가장 어려웠습니다. 주문과 결제가 분리되면서 결제 실패 시 주문 상태를 어떻게 동기화할지 고민이 많았습니다. 결국 Saga 패턴을 도입하여 보상 트랜잭션으로 해결했고, 각 단계의 상태를 명확히 정의하는 것이 중요하다는 것을 배웠습니다."


마무리

오늘 배운 내용을 정리하면 이렇습니다:

  • MSA는 독립 배포, 기술 자유도, 확장성, 장애 격리의 장점이 있지만 복잡성이 증가합니다
  • 서비스 간 통신은 동기(REST)와 비동기(메시지 큐)를 적절히 혼합하여 사용합니다
  • 분산 트랜잭션은 Saga 패턴으로 해결하며, 보상 트랜잭션으로 롤백합니다
  • API Gateway, Service Discovery, Circuit Breaker 등 MSA를 위한 인프라 패턴이 필수입니다

MSA는 단순히 "서비스를 쪼개는 것"이 아닙니다. 비즈니스 도메인을 이해하고, 서비스 경계를 설정하며, 분산 시스템의 복잡성을 관리하는 종합적인 설계 능력이 필요합니다. 면접관이 MSA를 묻는 이유도, 대규모 시스템 설계 경험트레이드오프를 고려한 의사결정 능력을 평가하기 위함입니다.

이력서에 적힌 'MSA 전환 경험' 한 줄에서 시작될 수 있는 수많은 꼬리 질문들, QueryDaily와 함께 미리 대비해 보세요.


tags: MSA 마이크로서비스 시스템설계 아키텍처 백엔드면접

👉 팀그릿 더 알아보기

profile
우리는 당신의 가능성을 믿는 사람들입니다. '되는 사람'이 되는 방법을 이야기합니다.

0개의 댓글