Spring 7기 프로젝트/코드 개선 + 테스트 코드 프로젝트

[트러블슈팅] 인증 시스템 설계 - 모놀리스에서 MSA까지

JuNo_12 2025. 6. 10. 16:31

안녕하세요! 백엔드 스프링 캠프에서 공부하고 있는 학생입니다.

인증 시스템을 자세히 다뤄보다가 어떻게 하면 각 도메인들이 REST한 API를 구현할 수 있을지 고민해봤습니다.

특히 실무에서도 바로 사용할 수 있는 코드를 작성하고 싶었는데, 그 과정에서 겪었던 고민들과 해결 과정을 공유해보려고 합니다.


시작점: 현재 상황 분석

구현한 기능들

우선 인증 시스템을 다뤄보았습니다:

 구현한 기능들:
- JWT + Refresh Token 이중 토큰 시스템
- 토큰 블랙리스트 (Redis + DB 이중화)
- 로그인 시도 횟수 제한 (Rate Limiting)
- 비밀번호 정책 (재사용 방지, 복잡성 검증)
- BCrypt 암호화
- Spring Security 완전 적용

 

하지만 여기서 더 깊이 들어가려고하면 어떻게 해야할까요?


MSA(Microservices Architecture)란?

MSA의 기본 개념

MSA(마이크로서비스 아키텍처)는 하나의 큰 애플리케이션을 여러 개의 작은 서비스로 분리하여 개발하는 아키텍처 패턴입니다.

 

모놀리식 vs MSA 비교

 

모놀리식 아키텍처 (현재 상태):

┌─────────────────────────────────┐
│        Spring Boot App          │
│  ┌─────────┬─────────┬────────┐ │
│  │  User   │  Todo   │Comment │ │
│  │ Domain  │ Domain  │ Domain │ │
│  └─────────┴─────────┴────────┘ │
│         Same Database           │
└─────────────────────────────────┘

 

MSA 아키텍처 (목표 상태):

┌──────────────┐    ┌──────────────┐    ┌───────────────┐
│ User Service │    │ Todo Service │    │Comment Service│
│ (Port:8081)  │    │ (Port:8082)  │    │   (Port:8083) │
│              │    │              │    │               │
│   User DB    │    │   Todo DB    │    │  Comment DB   │
└──────────────┘    └──────────────┘    └───────────────┘
        │                   │                     │
        └─────── HTTP/gRPC 통신 ──────────────────┘

 

MSA의 핵심 특징

  1. 독립 배포: 각 서비스를 별도로 배포 가능
  2. 기술 스택 자유도: 서비스마다 다른 언어/DB 사용 가능
  3. 장애 격리: 한 서비스 장애가 전체에 영향 안 줌
  4. 확장성: 필요한 서비스만 스케일 아웃 가능

MSA에서의 통신 방식

1. 동기 통신 (Synchronous Communication)

HTTP REST API 통신:

// Todo Service에서 User Service 호출
@Service
public class TodoService {
    
    private final WebClient userServiceClient;
    
    public Mono<TodoResponse> createTodo(CreateTodoRequest request) {
        // 1. User Service에 사용자 정보 요청
        return userServiceClient
            .get()
            .uri("/users/{userId}", request.getUserId())
            .retrieve()
            .bodyToMono(UserDto.class)
            .flatMap(user -> {
                // 2. 사용자 존재하면 Todo 생성
                Todo todo = new Todo(request.getTitle(), user.getId());
                return todoRepository.save(todo);
            })
            .map(this::toResponse);
    }
}

 

직관적이고 구현이 간단하지만, 서비스 간 결합이 강해지고, 장애가 전파될 위험성이 존재합니다.

 

2. 비동기 통신 (Asynchronous Communication)

Message Queue 활용:

// User Service에서 이벤트 발행
@Service
public class UserService {
    
    private final KafkaTemplate<String, Object> kafkaTemplate;
    
    public void createUser(CreateUserRequest request) {
        User user = userRepository.save(new User(request));
        
        // 이벤트 발행
        UserCreatedEvent event = new UserCreatedEvent(user.getId(), user.getEmail());
        kafkaTemplate.send("user-events", event);
    }
}

// Todo Service에서 이벤트 수신
@KafkaListener(topics = "user-events")
public void handleUserCreated(UserCreatedEvent event) {
    // 사용자 캐시 업데이트 등 처리
    userCacheService.updateCache(event.getUserId(), event.getEmail());
}

서비스 간 결합이 느슨하고, 높은 가용성이 있습니다. 하지만 구현이 복잡하고 데이터 일관성에 대해 고려해야합니다.


첫 번째 고민: 도메인 간 의존성 문제

MSA의 개념을 이해한 후, 가장 먼저 마주한 문제는 현재 모놀리식 구조에서의 도메인 간 의존성이었습니다.

MSA 전환이 필요한 이유

현재 모놀리식의 한계:

  1. 확장성 문제:
    • Todo 기능에만 트래픽 몰려도 전체 서버 스케일업 필요
    • 사용자 관리 기능 변경 시 전체 서비스 재배포
  2. 기술 스택 제약:
    • 모든 기능이 Spring Boot + MySQL 고정
    • Comment 서비스만 NoSQL 쓰고 싶어도 불가능
  3. 개발팀 협업 어려움:
    • 여러 팀이 같은 코드베이스 수정 시 충돌
    • 배포 시점 조율 필요

 

MSA로 얻을 수 있는 이점:

팀 A (User 담당)     팀 B (Todo 담당)     팀 C (Comment 담당)
      ↓                    ↓                    ↓
독립적 개발/배포      독립적 개발/배포      독립적 개발/배포
Spring + MySQL      Spring + Redis       Node.js + MongoDB

MSA 통신 패턴 상세

Pattern 1: API Gateway 패턴

Frontend Application
        ↓
┌─────────────────┐
│   API Gateway   │ ← 단일 진입점
│   (Port: 8080)  │
└─────────────────┘
        ↓
┌─────────┬─────────┬─────────┐
│User Svc │Todo Svc │Comment  │
│ :8081   │ :8082   │ :8083   │
└─────────┴─────────┴─────────┘

 

 

Spring Cloud Gateway 예시:

spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: http://localhost:8081
          predicates:
            - Path=/api/users/**
        - id: todo-service  
          uri: http://localhost:8082
          predicates:
            - Path=/api/todos/**

 

Pattern 2: Event-Driven Architecture

User Service ─┐
              ├─→ Message Broker ─┐
Todo Service ─┘   (Kafka/RabbitMQ) ├─→ Comment Service
                                  └─→ Notification Service

 

이벤트 기반 흐름:

// 1. User 생성 시
userService.createUser() → UserCreatedEvent 발행

// 2. 다른 서비스들이 독립적으로 처리
todoService.handleUserCreated() → 사용자별 기본 Todo 생성
commentService.handleUserCreated() → 사용자 캐시 준비  
emailService.handleUserCreated() → 환영 이메일 발송

MSA 전환 시 고려사항

1. 데이터 일관성 문제

ACID vs BASE:

// 모놀리식 (ACID)
@Transactional
public void createTodoWithUser() {
    User user = userRepository.save(newUser);      // 같은 트랜잭션
    Todo todo = todoRepository.save(newTodo);      // 원자성 보장
}

// MSA (BASE - 결국 일관성)
public void createTodoWithUser() {
    userService.createUser(newUser);               // User Service
    // ↓ 약간의 지연 발생 가능
    todoService.createTodo(newTodo);               // Todo Service  
}

 

2. 분산 트랜잭션 패턴

Saga 패턴 예시:

// 주문 처리 Saga
1. 결제 서비스 → 결제 처리
2. 재고 서비스 → 재고 차감  
3. 배송 서비스 → 배송 시작

// 실패 시 Compensation
❌ 배송 실패 시:
3. 배송 취소
2. 재고 복구
1. 결제 환불

도메인 분리 방법 고민

// AuthService에서 User 도메인 직접 의존
@Service
public class AuthService {
    private final UserRepository userRepository; // ❌ User 도메인 의존
    private final RefreshTokenService refreshTokenService;
}

// UserService에서 Auth 도메인 의존
@Service  
public class UserService {
    private final RefreshTokenService refreshTokenService; // ❌ Auth 도메인 의존
    private final PasswordPolicyService passwordPolicyService;
}

"이렇게 도메인끼리 얽혀있으면 나중에 MSA로 전환할 때 어떻게 하지?"

도메인 분리 방법 고민

처음에는 이벤트 기반 아키텍처를 도입해보려고 했습니다.

// 이벤트 방식으로 시도해봤던 구조
@Async
@EventListener
public void handleUserSigninEvent(UserSigninEvent event) {
    // 비동기로 감사 로그 처리
    auditLogRepository.save(createAuditLog(event));
}

하지만 곧 깨달았습니다. "현재 프로젝트 규모에서는 이벤트가 오히려 오버엔지니어링이다!"

 

현실적인 문제들 발견

  1. 성능 오버헤드: 스레드 풀, 큐 관리 비용
  2. 복잡성 증가: 디버깅 어려움, 비동기 테스트의 복잡함
  3. 데이터 일관성: eventual consistency 문제
  4. 팀 러닝커브: 한 달 반 배운 수준에서는 과도함

 

방향 전환: 현실적인 개선 방향

우선순위 재정립

"완벽한 아키텍처보다는 실무에서 바로 쓸 수 있는 견고한 시스템"

이 목표로 방향을 전환했습니다.

 


두 번째 고민: MSA 준비를 위한 기술 학습

RestTemplate vs WebClient - MSA 관점에서의 선택

다음으로 마주한 고민은 MSA 환경에서의 서비스 간 통신 방법이었습니다.

"MSA에서는 서비스가 분리되니까 HTTP 통신이 필수인데, 어떤 클라이언트를 써야 할까?"

MSA에서 HTTP 통신의 중요성

모놀리식에서는 메서드 호출로 충분했지만, MSA에서는 상황이 달라집니다:

// 모놀리식: 메서드 호출
@Service
class TodoService {
    private final UserRepository userRepository;
    
    public Todo createTodo(Long userId, String title) {
        User user = userRepository.findById(userId); // 같은 프로세스
        return new Todo(title, user);
    }
}

// MSA: HTTP 통신 필요
@Service  
class TodoService {
    private final WebClient userServiceClient;
    
    public Mono<Todo> createTodo(Long userId, String title) {
        return userServiceClient
            .get()
            .uri("/users/{id}", userId) // 다른 서비스 호출
            .retrieve()
            .bodyToMono(User.class)
            .map(user -> new Todo(title, user));
    }
}

기술 선택 과정

1. RestTemplate 검토

// RestTemplate (전통적 방식)
@Service
public class ExternalApiService {
    
    private final RestTemplate restTemplate;
    
    public UserDto getUser(Long userId) {
        return restTemplate.getForObject("/users/{id}", UserDto.class, userId);
    }
}

 

장점:

  • 간단한 사용법
  • 팀원들이 이해하기 쉬움
  • 안정성

단점:

  • Maintenance Mode (새 기능 추가 없음)
  • 동기식 블로킹
  • 높은 동시성에서 성능 저하

 

2. WebClient 검토

// WebClient (리액티브 방식)
@Service
public class ExternalApiService {
    
    private final WebClient webClient;
    
    public Mono<UserDto> getUser(Long userId) {
        return webClient
            .get()
            .uri("/users/{id}", userId)
            .retrieve()
            .bodyToMono(UserDto.class);
    }
}

 

장점:

  • 미래 지향적 (적극 개발 중)
  • 높은 성능 (논블로킹)
  • MSA에 적합

MSA 성능 고려사항

RestTemplate의 MSA 한계:

// 동시에 1000개 요청이 User Service로 몰릴 때
// RestTemplate: 1000개 스레드 생성 → 메모리 부족 위험
// WebClient: 4-8개 스레드로 처리 → 안정적 처리

 

MSA에서 WebClient가 유리한 이유:

  1. 서비스 간 대량 통신: MSA에서는 서비스 간 호출이 빈번
  2. 장애 전파 방지: 논블로킹으로 타임아웃 시에도 다른 요청에 영향 없음
  3. 백프레셔 대응: 다운스트림 서비스 과부하 시 적절한 대응
// WebClient의 MSA 친화적 기능들
public Mono<TodoResponse> createTodoWithFallback(CreateTodoRequest request) {
    return userServiceClient
        .get()
        .uri("/users/{id}", request.getUserId())
        .retrieve()
        .bodyToMono(User.class)
        .timeout(Duration.ofSeconds(3))           // 타임아웃 설정
        .retry(2)                                 // 재시도 정책
        .onErrorResume(ex -> {                   // 장애 시 fallback
            log.warn("User Service 호출 실패, 기본값 사용");
            return Mono.just(User.defaultUser());
        })
        .flatMap(user -> createTodo(request, user));
}

결정: 학습은 하되 실제 적용은 신중하게

현재 프로젝트: 기존 구조 유지하면서 품질 개선에 집중
개인 학습: WebClient와 리액티브 프로그래밍 개념 습득

이유는 다음과 같습니다:

  1. 팀 프로젝트 성공이 우선 - 검증된 기술로 안정성 확보
  2. 미래 준비는 개인 학습으로 - MSA 전환 시 활용할 지식 축적
  3. 점진적 적용 - 다음 프로젝트에서 경험 바탕으로 도입

깨달음

"완벽한 아키텍처보다 현재 수준에서 최선의 품질이 더 중요하다"

처음에는 이벤트 기반 아키텍처, 리액티브 프로그래밍 등 최신 기술을 모두 적용하고 싶었습니다.

하지만 프로젝트를 진행하면서 깨달은 것은:

  • 현재 팀 수준에 맞는 기술 선택이 중요
  • 안정성과 품질이 새로운 기술보다 우선
  • 미래를 위한 학습은 개인적으로 진행하되, 프로젝트에는 신중하게 적용

앞으로의 계획

MSA 전환 준비 로드맵

1단계: 현재 (모놀리식) 

  • 도메인별 패키지 분리 완료
  • 견고한 예외 처리 구조 완성
  • JWT 인증 시스템 완성

 

2단계: 준비 단계 (진행 중)

  • WebClient 학습: MSA 통신 준비
  • 이벤트 기반 패턴 학습: 서비스 간 데이터 동기화
  • Docker 컨테이너화: 독립 배포 준비
  • 분산 시스템 개념: 장애 처리, 모니터링

 

3단계: MSA 적용 (다음 프로젝트)

  • 서비스 분리: User, Todo, Comment 독립 서비스
  • API Gateway 도입: Spring Cloud Gateway
  • 서비스 디스커버리
  • 모니터링

 

MSA 전환 시 예상되는 아키텍처

┌─────────────────────┐
│    API Gateway      │
│   (Spring Cloud)    │
└─────────────────────┘
           │
    ┌──────┼──────┐
    ▼      ▼      ▼
┌─────────┬─────────┬─────────┐
│User Svc │Todo Svc │Comment  │
│         │         │Service  │
│WebClient│WebClient│WebClient│
│         │  ◄──────┤         │
│User DB  │ Todo DB │Comment  │
│(MySQL)  │(Redis)  │DB(Mongo)│
└─────────┴─────────┴─────────┘
           │
    ┌──────┼──────┐
    ▼      ▼      ▼
┌─────────────────────┐
│   Message Broker    │
│  (Kafka/RabbitMQ)   │
└─────────────────────┘

이런 구조에서는 WebClient의 비동기 통신이 핵심 역할을 하게 됩니다.


지속적인 학습 목표

현재 프로젝트에서는 실무에 바로 적용 가능한 수준의 품질을 완성하되,

개인적으로는 미래의 MSA 환경을 위한 기술들을 꾸준히 학습해나갈 계획입니다.


마무리

기술적 완성도만큼이나 현실적인 판단력이 중요하다는 것을 배웠습니다.

최신 기술을 무작정 도입하는 것보다는:

  • 현재 상황에 맞는 최적의 선택
  • 견고하고 확장 가능한 기본기
  • 미래를 위한 점진적 준비

이것들이 더 중요하다는 것을 깨달았습니다.

앞으로도 실무에서 바로 사용할 수 있는 코드를 작성하면서, 동시에 MSA 시대를 준비하는 개발자로 성장해나가겠습니다.