무한 스크롤과 랜덤 정렬, 커서 기반 페이징과 함께 쓸 수 있을까?

인스타그램과 같은 피드 구조를 구현하실 때, 무한 스크롤과 랜덤 정렬을 동시에 적용하고 싶으셨던 적이 있으신가요?
저 역시 유사한 구조를 구현하려다 예상치 못한 충돌을 겪었고, 이를 해결하기 위해 고정된 랜덤 시드를 도입하는 방식으로 문제를 풀게 되었습니다.

 

이 글에서는 그 과정을 정리하여 공유드리고자 합니다. 특히 커서 기반 페이징과 랜덤 정렬은 어떻게 공존할 수 있는가에 대한 고민이 중심입니다.


문제 상황: 커서 기반 페이징과 랜덤 정렬의 충돌

처음 의도는 다음과 같았습니다.

  • 피드에 게시글을 무한 스크롤로 불러오고 싶다
  • 사용자마다 매번 다른 랜덤 정렬 순서로 보여주고 싶다

이 두 가지를 동시에 적용하려고 했을 때 한 가지 중요한 사실을 깨달았습니다.

커서 기반 페이징은 고정된 정렬 순서를 전제로 작동하는데,
랜덤 정렬은 매번 다른 결과를 만들기 때문에, 두 방식은 정면으로 충돌한다.”

커서 기반 방식은 이전 페이지의 마지막 항목을 기준으로 다음 항목의 위치를 정확히 예측할 수 있어야 합니다. 하지만 랜덤 정렬을 적용하면 동일한 조건에서조차 결과가 달라지므로, 페이징 커서가 무의미해지는 문제가 발생합니다.


해결 방향: 고정된 랜덤 시드(Random Seed)의 도입

그래서 다음과 같은 방식으로 문제를 해결하였습니다.

  1. 사용자가 피드를 처음 요청할 때, 서버가 랜덤 시드(seed) 를 생성하여 클라이언트에 전달합니다.
  2. 이후 무한 스크롤을 통해 페이지를 추가로 요청할 경우, 클라이언트는 이 랜덤 시드를 계속 유지합니다.
  3. 게시글 ID와 랜덤 시드를 조합하여 고정된 정렬 키(sort key) 를 계산합니다.
  4. 이 정렬 키를 기준으로 커서 기반 페이징을 적용합니다.

이렇게 하면 사용자에게는 세션 중 일관된 랜덤 정렬 피드를 보여줄 수 있으며, 커서 기반의 안정적인 페이징도 가능합니다.


구현 방식 예시

📌 정렬 키 계산 방식

게시글마다 다음과 같은 방식으로 정렬 키를 계산합니다:

sortKey = hash(seed + postId); // 예: CRC32 해시 사용

이 정렬 키는 동일한 시드에 대해서는 항상 같은 값을 반환하므로,
정렬 순서 역시 변하지 않습니다.


📌 커서 기반 페이징 로직

SELECT * FROM posts
WHERE hash(seed + post_id) > :lastCursor
ORDER BY hash(seed + post_id)
LIMIT 20;

이 방식은 해시값 기준으로 오름차순 정렬하여 커서 이후의 게시글을 페이징 처리합니다.
사용자가 계속 스크롤할수록, 정해진 랜덤 순서대로 다음 게시글이 불러와집니다.


추가 고민: 새로운 게시글이 추가된다면?

이 구조에서 새롭게 떠오른 질문이 하나 있었습니다.

“그렇다면, 피드를 보는 도중에 새로운 게시글이 생긴다면 어떻게 되는가?”

결론부터 말씀드리면, 문제 없습니다.
새로운 게시글은 동일한 시드로 해시 계산을 하게 되므로 기존 피드 내 적절한 위치에 배치되지만,
사용자는 계속해서 기존 커서 이후 정렬된 게시글만 불러오기 때문에,
세션 중에는 피드 순서가 유지되고 새 게시글로 인한 페이징 오류도 발생하지 않습니다.


사용자 경험 흐름 정리

  1. 피드 최초 요청 시
    • 서버는 고정된 random_seed를 생성해 응답에 포함합니다.
  2. 무한 스크롤 페이지 요청 시
    • 클라이언트는 동일한 random_seed를 포함하여 요청합니다.
  3. 새로고침 또는 앱 재시작 시
    • 새로운 random_seed가 발급되어, 전혀 새로운 랜덤 피드가 생성됩니다.

정리하며

처음에는 무한 스크롤과 랜덤 정렬을 동시에 사용한다는 것이 불가능해 보였지만,
랜덤 시드 기반의 정렬 키 생성이라는 아이디어를 통해 이 둘을 자연스럽게 결합할 수 있었습니다.

항목 설명
문제 커서 기반 페이징과 매번 달라지는 랜덤 정렬은 충돌하는 구조
해결 고정된 랜덤 시드를 발급하고 해시 정렬 키를 만들어 커서 기반 페이징에 활용
결과 한 세션 동안은 일관된 랜덤 순서, 새로고침 시 새로운 피드 제공 가능
장점 성능과 UX의 균형 유지, 확장성 있는 구조

후속 과제

  • 추후 팔로우/팔로잉 기능이 추가되었을 때, 정렬 기준이 복합적으로 될 수 있어 정렬 로직을 재조정할 필요가 있습니다.
  • 사용자 맞춤형 피드에 적용하려면, 시드 외에도 필터/우선순위 로직과의 결합을 고려해야 합니다.
  • 캐싱 전략, 정렬 키 생성의 성능 최적화 등도 장기적으로는 고민해볼 만한 부분입니다.

 

1. MSA의 부상과 현실적인 문제

마이크로서비스 아키텍처(MSA)는 IT 산업이 발전하면서 많은 직군이 기술 중심적으로 변화하고, 트래픽 증가에 따라 확장성 요구가 커지면서 주목받게 되었다. 초기에는 MSA가 서비스 확장성과 분산 환경에 적합한 솔루션으로 각광받았지만, 실제 운영에서 여러 문제점이 드러나면서 MSA를 도입할지 여부를 두고 논쟁이 벌어지는 상황이 되었다.

현재 IT 업계에서는 "MSA를 도입해야 하는가?" 또는 "MSA를 포기하고 다시 모놀로틱으로 돌아갈 것인가?" 하는 두 가지 입장이 대립하고 있다.


2. MSA 도입의 주요 문제점

 

(1) 기술적 부채 증가

MSA를 도입하면 서비스가 여러 개로 나뉘면서 개별적인 테스트가 어려워지고, 전체 시스템의 일관성을 유지하기 위한 테스트 코드와 통합 테스트 비용이 기하급수적으로 증가한다.

  • 단위 테스트(Unit Test) 관리 어려움: 각 마이크로서비스별로 독립적인 테스트 코드 작성 필요
  • 통합 테스트(Integration Test) 부담 증가: 여러 서비스가 동시에 동작해야 하므로 통합 테스트가 어렵고 불완전할 가능성이 높음
  • QA 과정 복잡화: 모든 서비스가 올바르게 동작하는지 확인하기 위해 QA 단계가 많아짐

결과적으로, 테스트 비용 증가와 유지보수 부담으로 인해 기술적 부채가 누적되기 쉬운 구조가 된다.

 

 

(2) 운영 관리 및 인프라 비용 부담

MSA는 서비스가 분리됨에 따라 운영 및 인프라 관리의 복잡도가 증가하고, 이를 유지하기 위해서는 상당한 자본과 인력이 필요하다.

  • 각 서비스의 배포 및 유지보수 관리 필요: 배포 자동화(CI/CD), 모니터링, 로깅, 보안 등을 위한 추가적인 인프라가 필요함
  • 네트워크 비용 증가: 마이크로서비스 간 통신이 많아지면서 네트워크 트래픽이 증가하고, API Gateway, Service Mesh 같은 추가적인 계층이 필요해짐
  • 데이터 일관성 유지 어려움: 각 서비스가 독립적인 데이터베이스를 가질 경우, 분산 트랜잭션 관리가 복잡해짐 (예: SAGA 패턴 적용 부담)
  • 전문적인 운영 인력 필요: DevOps, SRE(Site Reliability Engineering) 등의 역할이 필수적으로 요구됨

대기업 수준의 자본력이 있는 기업이 아니라면, MSA를 완전히 운영하는 것이 현실적으로 어렵다.

 

 

(3) 개발 및 배포 속도 저하
MSA의 핵심 목표 중 하나는 빠른 배포와 지속적인 배포를 가능하게 하는 것이다. 하지만 실제 운영 환경에서는 다음과 같은 이유로 인해 배포 속도가 예상보다 느려질 수 있다.

1. 서비스 간 의존성 증가   

마이크로서비스가 서로 긴밀하게 연결되어 있을 경우, 특정 서비스의 변경이 다른 서비스에 영향을 미칠 가능성이 높아진다. 특히, 도메인 경계를 명확하게 나누지 못하면 여러 서비스 간 동시 배포가 필요해지고, 배포 전 조율이 복잡해졌다.

2. 데이터 마이그레이션 및 버전 관리의 어려움

개별 서비스의 데이터 스키마가 변경될 경우, 해당 데이터를 참조하는 모든 서비스에서 이를 반영해야 한다.

API 버전 관리 및 데이터 이중화(Migration + Shadow DB) 등의 기법을 적용하지 않으면, 배포 시 충돌이 발생할 가능성이 높았다.  

3. 장애 대응 복잡화
모놀리식 환경에서는 하나의 애플리케이션 내부에서 오류를 수정할 수 있지만, 제대로 MSA에서 고찰하지 않으면 오히려 특정 서비스 장애가 다른 서비스로 전파될 가능성이 크다. 서킷 브레이커(Circuit Breaker), 폴백(Fallback) 전략, 분산 트랜잭션 관리(Saga Pattern) 등이 적절히 적용되지 않으면 장애가 전체 시스템에 영향을 미칠 수 있다. 실제로 인지하지 못하고 있던 부분에서 문제가 발생하는 경우가 많았다. 이는 MSA 사용에 깊은 고민이 필요하다는 반증이었다.   

결론적으로, 이론적으로는 MSA가 빠른 배포를 가능하게 하지만, 실제로는 이러한 복잡성을 해결하지 않으면 오히려 배포 속도가 저하될 수 있었다.


3. 왜 MSA를 포기하는가?

MSA는 특정 조건에서는 효과적인 아키텍처지만, 모든 기업이나 모든 프로젝트에 적합한 것은 아니다. 특히, MSA를 유지하는 데 필요한 비용, 인력, 인프라 등의 현실적인 문제가 존재하며, 다음과 같은 이유로 MSA를 포기하는 기업도 늘어나고 있다. [ 우리의 이번 프로젝트도 같은 이유이다. ]

 

(1) 초기 개발 속도와 유지보수 비용의 불균형

  • 스타트업이나 소규모 프로젝트에서는 MSA보다 모놀로틱 아키텍처가 개발 속도 면에서 유리
  • 초기부터 MSA를 도입하면 개발 복잡도가 높아지고, 핵심 비즈니스 로직보다 아키텍처 관리에 더 많은 시간을 소비하게 됨
  • 시간이 지날수록 MSA 유지보수 비용이 증가하여 개발 속도가 저하됨

(2) MSA가 필요한 수준의 트래픽이 아님

  • MSA는 수백만 이상의 트랜잭션을 처리해야 하는 대규모 트래픽 환경에서 빛을 발함
  • 하지만, 트래픽이 적은 경우에는 분리된 서비스 간 통신 비용이 오히려 증가하여 성능이 저하될 수 있음
  • 즉, 트래픽 대비 과도한 아키텍처 분리가 오히려 시스템 성능을 떨어뜨릴 수 있다

(3) 대규모 시스템을 운영할 인프라와 인력이 부족

  • MSA를 제대로 운영하려면 DevOps, SRE, CI/CD 엔지니어, 클라우드 아키텍트 등의 전문 인력이 필요
  • 그러나, 소규모 조직이나 중견 기업에서는 이러한 인력을 충분히 확보하기 어렵고, 운영 비용도 감당하기 어려움

4. 결론: 언제 MSA를 도입해야 하는가?

⭕ MSA가 적합한 경우

  1. 고트래픽, 글로벌 서비스 운영 → 수백만 트랜잭션을 처리해야 하는 서비스 (예: Netflix, Amazon)
  2. 독립적으로 확장해야 하는 서비스 → 특정 기능(예: 결제, 추천 엔진 등)만 빠르게 확장해야 하는 경우
  3. 다양한 기술 스택을 활용해야 하는 경우 → 각 서비스별로 최적의 기술을 적용해야 할 필요성이 있는 경우
  4. 서비스 간 의존성을 최소화할 수 있는 경우 → 비즈니스 도메인이 명확히 구분되는 경우

❌ MSA를 피하는 것이 나은 경우

  1. 소규모 프로젝트나 스타트업 → 초기 개발 속도가 중요한 경우
  2. 서비스 간 결합도가 높은 경우 → 데이터 정합성을 유지해야 하고, 트랜잭션이 강하게 결합된 경우
  3. 운영 비용을 감당하기 어려운 경우 → DevOps, SRE, CI/CD 엔지니어 등의 인력을 확보하기 어려운 경우
  4. 개발 및 배포 속도가 중요한 경우 → 팀 규모가 작고 빠른 피드백 루프가 필요한 경우

결론적으로, MSA는 특정 규모 이상의 시스템에서는 효과적일 수 있지만, 기술적 부채, 운영 비용, 유지보수 복잡성 등의 현실적인 문제를 고려해야 한다. 모놀로틱 아키텍처를 적절히 활용하면서 필요할 때만 MSA로 전환하는 점진적인 접근이 가장 현실적인 선택지일 것이다.

백엔드에서 "관리자"와 "사장"은 어떻게 다를까?

서비스를 개발하다 보면, 관리자(Admin)와 사장(Owner 혹은 CEO)의 개념을 시스템 내부에서 어떻게 정의할지 고민하게 됩니다.
현실 세계에서는 이 둘의 차이가 명확하지만, 백엔드 시스템 구조에서는 어떻게 구분하고 구현하는 게 좋을까요?
이 글에서는 실제 백엔드 설계 관점에서 "관리자"와 "사장"의 차이점을 정리해봅니다.


1. 현실 세계에서의 관리자 vs 사장

구분 관리자(Admin) 사장(Owner/CEO)
역할 시스템 또는 조직 내 운영 담당 조직 전체 방향성과 의사결정 담당
권한 운영 관리, 유저 승인/삭제 등 최고 권한자, 관리자까지 제어 가능
대상 실무 중심 전략 중심, 전체 소유자

일반적으로 관리자(admin)는 시스템이나 서비스 내부를 운영하는 사람이고, 사장(owner)는 그 시스템을 소유하거나 최종 책임을 지는 사람입니다.


2. 백엔드 시스템에서의 구분

백엔드 시스템에서는 흔히 Role 기반 권한 제어 또는 Permission 기반 권한 제어를 사용합니다.
이 경우 “관리자”와 “사장”은 다음과 같이 구현할 수 있습니다.

Role 기반 구현 예시

{
  "username": "adminUser",
  "role": "admin"
}
{
  "username": "ceoUser",
  "role": "owner"
}

권한 미들웨어 예시 (Node.js)

function authorize(role) {
  return function (req, res, next) {
    if (req.user.role !== role) {
      return res.status(403).json({ message: "권한이 없습니다." });
    }
    next();
  };
}

이처럼 role 값을 기준으로 각 사용자의 접근 권한을 제한할 수 있습니다.
여기서 owner는 모든 권한을 포함한 최상위 계정이고, admin은 실무 운영 권한을 가지는 경우가 일반적입니다.


3. 시스템 설계 시 고려할 점

항목 관리자(Admin) 사장(Owner)
역할 범위 서비스 운영 및 유지관리 전체 시스템 제어 및 계정 관리
구현 방식 role: "admin" role: "owner" 또는 is_owner: true
권한 범위 제한적 (콘텐츠, 유저 등) 무제한 (관리자 추가/삭제까지 가능)
사용자 수 여러 명 존재 가능 보통 한 명 (혹은 매우 제한적)

4. 더 세밀한 권한이 필요할 때: Permission 기반

간단한 시스템은 Role만으로도 충분하지만, 서비스가 복잡해지면 세분화된 권한(Permissions)이 필요합니다.

{
  "username": "adminUser",
  "permissions": ["user:read", "post:edit"]
}
{
  "username": "ceoUser",
  "permissions": ["*"]
}

사장은 모든 권한(*)을 부여받는 구조로, 보다 유연한 설계가 가능합니다.


5. 결론

  • 관리자와 사장은 현실에서는 명확히 다르지만, 백엔드에서는 어떻게 권한 구조를 설계했는지에 따라 달라집니다.
  • 보통 사장은 최고 권한자(owner)로, 관리자(admin)는 실무 권한을 가진 계정으로 구현합니다.
  • 작은 서비스는 role 기반으로 충분하지만, 세밀한 권한이 필요한 경우 permissions 기반으로 확장하는 것이 좋습니다.

 

1. 문제 상황

마이크로서비스 아키텍처(MSA)에서는 각 서비스가 독립적으로 운영되기 때문에 데이터 정합성 문제가 발생할 수 있다. 기존 모놀로틱 환경에서는 하나의 트랜잭션 내에서 데이터 정합성을 보장할 수 있었지만, MSA 환경에서는 서비스 간 네트워크 통신 지연, 장애, 중복 요청, 부분 실패 등으로 인해 정합성을 유지하기 어려운 상황이 발생했다.

 

(1) 주요 데이터 정합성 문제

  • 동기 호출 시 서비스 장애 문제:
    • 한 서비스가 다른 서비스를 호출하는 도중 장애가 발생하면 데이터 불일치 가능성 증가
    • 예: 주문 서비스에서 결제 요청을 보냈지만, 결제 서비스 장애로 인해 반영되지 않은 경우
  • 비동기 이벤트 기반 처리 시 중복 및 유실 문제:
    • 이벤트 처리 중 중복 실행이 발생하거나, 네트워크 문제로 인해 메시지가 유실될 가능성
    • 예: 결제 완료 이벤트가 Kafka에서 중복 소비되는 경우

2. 해결책: Feign Client & Kafka 활용

이러한 문제를 해결하기 위해 Spring Cloud Feign Client(동기 호출)와 Kafka(비동기 메시징) 를 결합하여 데이터 정합성을 유지하는 전략을 사용했다.


3. Feign Client를 이용한 동기 호출 정합성 관리

Feign Client는 서비스 간 REST API 호출을 쉽게 할 수 있도록 지원하는 Spring Cloud의 라이브러리이다.

 

(1) Feign Client 적용

Spring Cloud Feign Client를 사용하여 마이크로서비스 간 통신을 간소화하고, 서킷 브레이커를 적용하여 장애 감지 및 복구를 수행했다.

Feign Client 설정

@FeignClient(name = "payment-service", fallback = PaymentFallback.class)
public interface PaymentClient {
    @PostMapping("/payments")
    PaymentResponse processPayment(@RequestBody PaymentRequest request);
}

서킷 브레이커 적용 (Resilience4j 사용)

@Retry(name = "paymentRetry", fallbackMethod = "fallbackPayment")
public PaymentResponse processPayment(PaymentRequest request) {
    return paymentClient.processPayment(request);
}

public PaymentResponse fallbackPayment(PaymentRequest request, Throwable throwable) {
    System.err.println("Payment failed Order ID: " + request.getOrderId());
    System.err.println("Error: " + throwable.getMessage());

    // 1. 실패한 요청을 Dead Letter Queue로 전송
    sendToDLQ(request, throwable);

    // 2. 관리자에게 알림 발송
    notifyAdmin(request, throwable);

    // 3. 장애 내역을 데이터베이스에 저장
    logPaymentFailure(request, throwable);

    return new PaymentResponse("FAILED", request.getOrderId());
}

 

(2) Feign Client를 이용한 데이터 정합성 보장 방법

  1. 서킷 브레이커 적용: 서비스 장애 발생 시 빠르게 감지하고, 대체 로직을 실행하여 데이터 불일치 방지
  2. 재시도(Retry) 메커니즘 도입: 일시적인 네트워크 장애 시 자동으로 재시도하여 요청 실패 방지
  3. Fallback 처리: 결제 요청 실패 시 주문 상태를 "결제 대기"로 변경하고, 사용자를 위한 별도 알림 전송

4. Kafka를 이용한 비동기 이벤트 기반 정합성 관리

Kafka는 이벤트 드리븐 방식으로 데이터 정합성을 유지하는 데 효과적이다. 서비스 간 동기 통신을 최소화하고, 장애 발생 시에도 데이터가 유실되지 않도록 이벤트 저장 및 재처리 메커니즘을 적용했다.

 

(1) Kafka 이벤트 발행

주문이 완료되면 Kafka를 통해 결제 이벤트를 발행한다.

@Component
public class OrderEventPublisher {
    private final KafkaTemplate<String, PaymentEvent> kafkaTemplate;

    public void publishPaymentEvent(PaymentEvent event) {
        kafkaTemplate.send("payment-topic", event);
    }
}

 

(2) Kafka 이벤트 소비 (결제 서비스)

    @KafkaListener(topics = "payment-topic", groupId = "payment-group")
    public void processPayment(PaymentEvent event, Acknowledgment acknowledgment) {
        try {
            paymentService.processPayment(event);
            acknowledgment.acknowledge();
        } catch (Exception e) {
            System.err.println("Payment failed event: " + event.getOrderId() + ". Sending to DLQ.");
            sendToDLQ(event);
        }
    }

    private void sendToDLQ(PaymentEvent event) {
        kafkaTemplate.send(new ProducerRecord<>("payment-dlq", event));
    }

 

(3) Kafka를 이용한 데이터 정합성 보장 방법

  1. 이벤트 저장 및 재처리:
    • Kafka는 메시지를 저장하므로, 서비스 장애 발생 시 이벤트를 다시 처리할 수 있음
    • 예: 결제 서비스가 다운되었을 경우, 복구 후 동일한 결제 요청을 다시 처리 가능
  2. Idempotency(멱등성) 보장:
    • 이벤트 소비 시 동일한 요청이 중복 실행되지 않도록 결제 요청 ID를 기준으로 중복 확인
    • 예: 동일한 orderId에 대해 중복 결제 요청이 실행되지 않도록 데이터베이스에서 체크
  3. Dead Letter Queue(DLQ) 활용:
    • 여러 번 재시도 후에도 실패한 이벤트를 별도의 큐(DLQ)에 저장하여 추후 처리
    • 예: 결제 실패 이벤트를 DLQ로 이동 후 운영팀이 수동 확인

5. 결론

  • Feign Client를 사용하여 서비스 간 동기 호출 시 서킷 브레이커, 재시도, Fallback을 적용하여 데이터 정합성을 유지
  • Kafka를 활용하여 이벤트 기반 아키텍처를 구성하고, 장애 발생 시에도 이벤트를 재처리할 수 있도록 보장
  • Idempotency 및 Dead Letter Queue(DLQ) 기법을 활용하여 중복 처리 및 데이터 유실 문제 해결

 

Elasticsearch 클러스터는 마스터 노드, 데이터 노드, 코디네이터 노드로 구성되며, 각 노드가 다운될 경우 시스템의 안정성과 가용성에 영향을 미칠 수 있다. 따라서 각 노드 유형별로 장애 발생 시 대응 방안을 마련해야 한다.


1. 마스터 노드 장애 복구 전략

(1) 마스터 노드의 역할

  • 클러스터의 헬스 체크, 노드 관리, 샤드 할당, 설정 변경 등의 중요한 역할을 수행
  • 마스터 노드가 장애를 일으키면 클러스터 상태를 업데이트하거나 새로운 노드를 추가할 수 없음

(2) 마스터 노드 장애 발생 시 증상

  • GET _cluster/health 요청 시 red 또는 yellow 상태
  • 클러스터 상태 변경이 불가능
  • 노드 추가 및 샤드 할당 불가

(3) 복구 방법

  1. 다른 마스터 노드가 있는지 확인
    • 정상적으로 선출된 새로운 마스터 노드가 있으면 자동 복구 진행됨
  2. GET _cat/master?v
  3. 마스터 노드가 1개뿐인 경우 클러스터가 중단됨
    • 최소 3개의 마스터 노드를 유지해야 함
    • 마스터 노드가 모두 다운된 경우 수동 복구 필요
  4. 새로운 마스터 노드 추가
    • 기존 마스터 노드가 복구 불가능할 경우 새로운 노드를 추가하고 elasticsearch.yml에서 마스터 노드로 설정
    node.master: true
    
  5. 클러스터 재시작 후 상태 확인
  6. GET _cluster/health
  7. 마스터 노드 선출이 안 될 경우 강제 복구
    • 강제로 클러스터를 재구성하여 노드를 재배치
  8. curl -XPOST "http://localhost:9200/_cluster/reroute?retry_failed=true"

예방 조치

  • 마스터 노드는 반드시 3개 이상 유지하여 선출이 가능하도록 구성
  • 마스터 전용 노드를 설정하여 데이터 노드와 분리
  • 고가용성을 위해 마스터 노드를 다른 물리 서버에 배포

2. 데이터 노드 장애 복구 전략

(1) 데이터 노드의 역할

  • 인덱스 데이터 저장 및 검색 요청 처리
  • 장애 발생 시 저장된 데이터가 유실될 가능성이 있음

(2) 데이터 노드 장애 발생 시 증상

  • 일부 데이터 조회 불가
  • 클러스터 헬스 체크 시 yellow 또는 red 상태
  • 샤드 불균형 발생

(3) 복구 방법

  1. 장애 노드 상태 확인
    • 노드가 다운되었는지 확인하고 문제 원인 파악 (디스크 공간 부족, 메모리 문제 등)
  2. GET _cat/nodes?v
  3. 샤드 재할당 확인
    • UNASSIGNED 상태의 샤드가 있으면 자동 복구되지 않았음을 의미
  4. GET _cat/shards?v
  5. 노드 재시작 후 샤드 복구 확인
    • 데이터 노드를 다시 시작한 후, 클러스터가 자동으로 샤드를 복구하는지 확인
    systemctl restart elasticsearch
    
  6. 수동 샤드 복구 실행 (필요 시)
  7. POST _cluster/reroute?retry_failed=true
  8. 샤드 강제 할당 (긴급 복구 필요 시)
    • 할당이 실패한 이유를 확인한 후 적절한 노드로 수동 할당
  9. POST _cluster/allocation/explain

예방 조치

  • 복제 샤드 설정(number_of_replicas: 1 이상)으로 장애 발생 시 자동 복구 가능하도록 설정
  • 디스크 사용량 모니터링 (GET _cat/allocation?v 명령어 활용)
  • 데이터 노드 장애 발생 시 ILM(인덱스 수명 주기 관리) 적용하여 오래된 데이터를 자동으로 Cold 노드로 이동

3. 코디네이터 노드 장애 복구 전략

(1) 코디네이터 노드의 역할

  • 검색 쿼리 및 집계 요청을 분산하여 성능 최적화
  • 장애 발생 시 데이터 노드의 부하가 증가할 수 있음

(2) 코디네이터 노드 장애 발생 시 증상

  • 검색 및 집계 쿼리 속도 저하
  • CPU 사용량 증가
  • 클러스터 전체의 응답 속도 저하

(3) 복구 방법

  1. 장애 노드 상태 확인
    • 코디네이터 노드가 다운되었는지 확인
  2. GET _cat/nodes?v
  3. 노드 재시작
    • 노드가 단순한 장애(메모리 부족, 네트워크 문제)라면 재시작으로 해결 가능
  4. systemctl restart elasticsearch
  5. 노드 추가 및 부하 분산
    • 코디네이터 노드가 과부하로 다운되었다면 새로운 코디네이터 노드 추가
    node.master: false
    node.data: false
    node.ingest: false
    
  6. 쿼리 로드 밸런싱 설정
    • 애플리케이션에서 Elasticsearch에 요청할 때 라운드 로빈 방식을 사용하여 요청을 여러 노드로 분산

예방 조치

  • 검색 요청이 많은 경우 코디네이터 노드를 2개 이상 운영
  • 로드 밸런서를 활용하여 요청 부하를 분산
  • 쿼리 최적화 적용 (필요한 필드만 검색, 애그리게이션 최소화)

4. 결론

  • 마스터 노드 장애 시: 3개 이상의 마스터 노드를 유지하여 자동 선출 가능하도록 구성
  • 데이터 노드 장애 시: 복제 샤드를 설정하고 ILM을 적용하여 자동 복구 가능하도록 운영
  • 코디네이터 노드 장애 시: 로드 밸런서를 활용하여 부하를 분산하고, 검색 요청이 많은 경우 추가적인 코디네이터 노드를 운영

ELK vs Loki: 로깅 시스템 선택

1. 문제 발생

기존에는 ELK(Elasticsearch, Logstash, Kibana) 스택을 사용하여 데이터 분석과 로깅을 관리하고 있었으나, MSA(Microservices Architecture) 도입을 고려하면서 대용량 서버 운영에 대한 새로운 요구사항이 생겼다. 이에 따라 분석과 로깅의 역활이 분리될 필요가 있었고 로깅은 스케일링이 용이하고 관리 비용이 적은 시스템으로 도입될 필요가 생겼다.

 

생각의 발달은 아래의 우아한 형제들의 기술 블로그를 토대로 발전시켰다.

 

따끈따끈한 전사 로그 시스템 전환기: ELK Stack에서 Loki로 전환한 이유 | 우아한형제들 기술블로그

안녕하세요. 클라우드모니터링플랫폼팀의 이연수입니다. 우아한형제들의 모니터링시스템 구축 및 관리, 운영을 하고 있습니다. 작년부터 올해 초까지 팀에서 전사 로그 시스템을 전환을 진행

techblog.woowahan.com

 

2. 문제 원인

  • ELK는 강력한 검색 기능을 제공하지만, 운영 비용이 높음
    • Elasticsearch의 샤드 관리가 복잡하고, 인덱스 저장 공간을 많이 차지함.
    • 대량의 로그를 저장할 경우 샤드 개수 증가로 인해 성능 튜닝이 필요함.
    • Logstash의 리소스 사용량이 높고, 수많은 로그 데이터를 처리하기 위해 별도의 클러스터링이 필요함.
  • Loki는 메타데이터 기반의 경량 로그 저장 시스템
    • 로그 데이터를 직접 인덱싱하지 않고, 레이블을 기반으로 저장.
    • 레이블 단위로 로그를 청크(Chunk) 단위로 압축하여 저장하므로 저장 공간을 절약할 수 있음.
    • 복잡한 샤드 관리가 필요 없으며, 확장성이 뛰어남.

 

3. 문제 해결책 수립

해결 방법 비교

비교 항목 ELK (Elasticsearch, Logstash, Kibana) Loki (Grafana Loki)
로그 저장 방식 전체 로그를 인덱싱하여 검색 가능 로그 내용을 색인화하지 않고, 레이블을 기반으로 저장
검색 성능 고급 검색 기능 제공 (Full-text Search, Aggregation 지원) 레이블 기반 필터링, Full-text Search 미지원
저장 공간 인덱싱된 데이터로 인해 저장 공간 요구량 높음 청크 단위 압축 저장으로 공간 절약
운영 및 확장성 샤드 관리, 리소스 최적화 필요 샤드 관리 불필요, 간단한 확장 가능
리소스 사용량 높은 CPU, 메모리 사용량 상대적으로 낮은 리소스 사용량
적용 사례 정밀한 검색 및 분석이 필요한 환경 MSA 및 대용량 로그 저장이 필요한 환경

 

4. 문제 해결: Loki 도입 결정

(1) MSA 환경에서의 확장성 고려

  • MSA 기반 서비스는 여러 개의 마이크로서비스에서 로그를 생성하며, 각 서비스의 로그를 독립적으로 관리하면서도 효율적인 검색이 필요하다.
  • Loki는 샤드와 같은 복잡한 관리가 필요 없고, 로그 저장 방식이 가볍기 때문에 대량의 서버를 운영하는 데 적합하다.

(2) 저장 공간 최적화

  • ELK는 모든 로그를 색인화하는 방식이기 때문에 저장 공간이 많이 필요하며, 특히 대량 로그 저장 시 운영 비용이 증가한다.
  • Loki는 로그 내용을 직접 색인화하지 않고, 레이블 기반으로만 색인하여 저장 공간을 절약할 수 있다.

(3) 인프라 운영 비용 절감

  • ELK는 검색 기능이 강력하지만, Logstash와 Elasticsearch의 리소스 사용량이 높아 운영 비용이 증가한다.
  • 반면, Loki는 가벼운 구조를 가지고 있어 운영 비용이 낮고, 확장성이 뛰어나다.

 

5. 결론

  • 기존 ELK는 강력한 검색 기능이 필요할 때 유용하지만, 저장 공간과 운영 비용이 많이 소모된다.
  • MSA 기반 대규모 로그 관리에서는 Loki가 더 적합하며, 샤드 관리 없이 확장성이 뛰어나고 운영이 간편하다.
  • Loki는 로그 데이터를 직접 색인하지 않으며, 청크 단위로 저장하여 저장 공간을 절약할 수 있다.
  • 결과적으로, ELK에서 Loki로 전환하여 MSA 환경에서 더욱 효율적인 로그 관리를 수행할 수 있다.

1. 문제 상황

현재 진행 중인 프로젝트는 모놀로틱(monolithic) 아키텍처를 사용하고 있지만, 이를 운영하면서 몇 가지 한계를 경험하게 되었다.

  • 코드베이스가 커지면서 유지보수 부담 증가
  • 기능 추가 및 변경 시 배포 부담 증가
  • 특정 기능만 확장하고 싶어도 전체 시스템을 스케일링해야 하는 비효율성
  • 장애 발생 시 서비스 전체가 영향을 받는 단일 장애점(SPOF, Single Point of Failure) 문제

이러한 문제를 해결하기 위해 마이크로서비스 아키텍처(MSA, Microservices Architecture) 도입을 고려했다. 다만, 현재 프로젝트는 작은 규모의 애플리케이션이기 때문에 즉각적인 MSA 전환보다는 개념 이해와 학습을 통한 대규모 시스템 설계의 가능성을 탐색하는 것이 목적이다.


2. 모놀로틱 아키텍처의 한계

모놀로틱 아키텍처는 개발 초기에는 단순하고 빠르게 개발할 수 있다는 장점이 있지만, 규모가 커질수록 다음과 같은 문제가 발생한다.

 

(1) 유지보수 및 개발 속도 저하

  • 코드베이스가 방대해지면서 코드 변경 및 충돌이 증가
  • 여러 팀이 같은 코드베이스에서 작업하면서 작업 분리가 어려움
  • 특정 기능을 수정해도 다른 부분에 영향을 미칠 가능성이 높음

(2) 배포 및 확장성 문제

  • 작은 변경 사항이 있어도 전체 시스템을 다시 빌드하고 배포해야 함
  • 특정 기능만 확장하려 해도 전체 애플리케이션을 스케일링해야 하는 비효율성
  • 대용량 트래픽이 발생할 경우 서비스 일부만 확장할 수 없는 구조

(3) 장애 발생 시 전체 시스템 영향

  • 한 모듈에서 장애가 발생하면 전체 애플리케이션에 영향을 미침
  • 단일 장애점(SPOF, Single Point of Failure) 문제로 인해 특정 서비스의 오류가 전체 시스템을 다운시킬 수 있음

(4) 기술 스택 변경 어려움

  • 새로운 기술을 적용하려면 전체 시스템을 변경해야 함
  • 특정 기능만 최신 기술로 변경하고 싶어도 전체 애플리케이션을 수정해야 함

이러한 문제로 인해, 대규모 애플리케이션에서는 MSA가 더욱 적합한 아키텍처로 고려된다.


3. MSA(마이크로서비스 아키텍처) 적용 이유

MSA는 애플리케이션을 여러 개의 독립적인 서비스로 분리하여 개발, 배포, 확장성을 최적화하는 아키텍처이다.

 

(1) MSA의 주요 개념

  • 서비스 분리: 하나의 애플리케이션을 여러 개의 독립적인 서비스로 구성
  • 독립적인 배포: 각 서비스는 개별적으로 배포 가능
  • 개별 확장성: 특정 서비스만 개별적으로 확장 가능
  • 다양한 기술 스택 사용 가능: 서비스별로 적절한 기술을 선택하여 적용 가능

(2) MSA의 장점

항목 설명
유지보수 용이 서비스가 분리되어 있어 변경이 용이함
배포 유연성 특정 서비스만 개별 배포 가능, 전체 시스템 재배포 불필요
확장성 향상 특정 기능만 개별적으로 확장 가능하여 리소스 활용 최적화
기술 스택 다양화 각 서비스별로 다른 기술 및 데이터베이스 사용 가능
장애 영향 최소화 특정 서비스 장애 발생 시 전체 시스템에 영향 없음

 

(3) MSA 도입 시 고려해야 할 점

하지만, MSA는 모든 프로젝트에 적합한 것은 아니다. 특히, 작은 프로젝트에서는 오히려 관리 부담이 증가할 수 있다.

  • 운영 복잡성 증가: 여러 개의 마이크로서비스를 관리해야 함
  • 분산 시스템의 문제: 네트워크 트래픽 증가, 데이터 일관성 유지 어려움
  • 배포 및 모니터링 도구 필요: 개별 서비스의 배포 및 장애 감지를 위한 추가 인프라 필요

따라서, 현재 프로젝트는 소규모 애플리케이션이므로 MSA를 바로 적용하기보다는, 모듈화된 설계를 먼저 고려하고, 향후 대규모 시스템 설계 및 학습 목적으로 MSA 개념을 연구하는 방향이 적절하다.


4. 적용 방안: 현재 프로젝트에 맞는 전략

현재 진행 중인 프로젝트는 작은 규모이므로, 완전한 MSA 도입이 아니라 모놀로틱을 개선하는 형태로 접근하는 것이 적절하다.

 

(1) 멀티 모듈 아키텍처 도입

  • 현재 프로젝트를 하나의 코드베이스로 유지하되, 기능별로 모듈을 분리하여 관리
  • 예) user-service, order-service, payment-service 등으로 모듈화
  • 이를 통해 유지보수성과 확장성을 확보하면서도 운영 복잡도를 최소화

(2) MSA 전환을 대비한 설계 원칙 적용

  • 서비스 간의 명확한 API 설계를 통해 MSA로 확장할 수 있도록 준비
  • 데이터베이스를 하나로 묶지 않고, 서비스별로 논리적인 데이터 분리를 고려
  • 개별 서비스의 독립성을 유지하면서도, 초기에는 모놀로틱 환경에서 개발

(3) 학습 및 연구 목적으로 MSA 개념 도입

  • Kubernetes(K8s), 서비스 디스커버리, API Gateway 등 MSA 관련 기술을 학습하고 테스트 환경에서 운영
  • 실제 운영 환경에서는 작은 프로젝트에 적합한 구조를 유지하며, 대규모 프로젝트 전환 시 MSA를 본격적으로 적용할 계획

5. 결론

  • 현재 프로젝트는 작은 규모이므로 MSA를 도입하기보다는 모놀로틱을 개선하는 형태로 운영하는 것이 적절하다.
  • 그러나, 향후 대규모 시스템을 구상하고 학습하는 목적에서 MSA 개념을 연구하고 설계 원칙을 일부 도입할 필요가 있다.
  • 멀티 모듈 아키텍처를 우선 적용하고, 점진적으로 MSA로 전환할 수 있도록 유연한 설계를 고려하는 것이 바람직하다.

1. 문제 발생

엘라스틱 서치는 한번 생성한 인덱스의 매핑(테이블 구조)을 변경할 수 없다.

Long ㅁ
string ㅁ

위와 같이 2개의 타입으로 인덱스를 만들었다면, long2개인 인덱스나, 1개의 타입이 추가된 인덱스로 만드려면 무조건 새로 만들어야한다는 문제점이 존재한다.

 

그럼 엘라스틱 서치의 확장성을 위해서 해야하는 전략은 무엇이 있을까?

= 사실 처음부터 제대로 필요한 데이터만 미리 저장하고 인덱스를 수정할 일이 없으면 베스트이긴하다.

하지만 그렇지 못하면, 방법은 크게 5가지 존재한다.

 

2. 문제 원인

  • 엘라스틱 서치는 매핑이 고정되기 때문에 인덱스를 생성한 후에는 필드 타입을 변경할 수 없음.
  • 동적 매핑(Dynamic Mapping)을 사용할 수 있지만, 예상치 못한 데이터 타입 오류가 발생할 가능성이 있음.
  • **필드 개수 제한(1,000개)**이 있기 때문에, 동적 매핑을 사용할 경우 불필요한 필드가 쌓여 인덱스가 비효율적으로 커질 수 있음.
  • 실수로 잘못된 타입을 지정하면 전체 인덱스를 새로 만들어야 하는 부담이 생김.

 

3. 문제 해결책 수립

1. 동적 매핑(Dynamic Mapping)

  • 동적 매핑을 사용하면 새로운 필드가 자동으로 추가되고, 기본 매핑 유형이 설정된다.
  • 하지만 이 방법은 예상치 못한 매핑 오류가 발생할 수 있어 주의가 필요하다.

2. 별도 인덱스 생성 및 데이터 마이그레이션

  • 기존 인덱스를 유지한 상태에서 새로운 매핑을 가진 인덱스를 생성하고, 데이터를 점진적으로 이전하는 방법이다.
  • Zero-downtime 마이그레이션을 지원합니다.
  • 예: index_v1 -> index_v2 로 새 인덱스를 생성하고 데이터 이전 후 기존 인덱스를 삭제한다.

3. 인덱스 에일리어스(Index Alias) 사용

  • 인덱스 에일리어스를 사용하여 클라이언트 측에서 인덱스 이름을 변경하지 않고 새로운 인덱스로 리다이렉션할 수 있다.
  • 예: my_index_alias가 index_v1을 가리키다가, 이후 index_v2로 변경 가능하다.

4. 멀티 인덱스 전략

  • 여러 인덱스를 사용하는 방식으로, 시간 기반 인덱싱(예: 일별/월별)을 통해 확장성을 개선할 수 있다.
  • 오래된 인덱스를 분리 관리하거나, 필요 시 새 인덱스를 추가해도 전체 매핑 구조에 영향을 덜 받는다.

5. 스키마리스(No Schema) 접근

  • 데이터를 다소 유연하게 처리하는 방식으로, JSON 기반 필드를 최대한 활용한다.
  • 하지만 검색 성능과 정확도에 영향이 있을 수 있다.

 

4. 문제 해결

하지만 필자는 이 중 3개의 전략을 선택하게 되었다.

  1. 별도 인덱스 생성 및 데이터 마이그레이션
  2. 인덱스 Alias 사용
  3. 멀티 인덱스 전략

기술 선택의 이유는 실제로 동적 매핑은 비교적 간단하게, 인덱스 템플릿[인덱스 청사진]에서 지정할 수 있지만, 내가 원하지 않는 타입의 인덱스를 저장한다거나.

POST my_index/_doc/1
{
  "price": 10000
}

POST my_index/_doc/2
{
  "price": "10000원"
}

둘은 엄연히 타입이 다르다...

맨 처음 인덱스를 만들 당시 개발자가 실수로 다른 형태로 저장할 경우 인덱스 수정도 안되니 다시 만들어야한다.

실제로 바쁜 상황에서는 가끔 정신줄 놓고 string으로 구현할 걸 숫자가 보인다고 long으로 만드는 경우도 있었다.

거기에 별로 발생할 일은 없지만, 엘라스틱 서치 인덱스 내에 저장할 수 있는 필드의 수는 1000개가 한계인데, 동적 매핑의 경우 이 위험도 발생할 수 있다.

 

설령 위처럼 큰 에러가 발생하지 않더라도, 잘못된 타입을 매핑하면 결국 인덱싱 속도가 느려지고 클러스터 리소스 사용량이 증가한다.

 

마지막 방식인 스키마리스는 이 동적 매핑의 응용으로 사전에 데이터 구조(매핑)을 정의하지 않고 json 기반 동적 매핑을 활용하여 데이터를 유연하게 저장하는 방식을 사용한다.

자연스럽게 남은 선택지는

  1. 별도 인덱스 생성 및 데이터 마이그레이션
  2. 인덱스 Alias 사용
  3. 멀티 인덱스 전략

로 좁혀지게 되었다.

 

 

그러면 이중 1개만 사용해도 되는데 굳이 왜 3개를 다 사용하게 되었는가?

 

사실 1,2번은 세트이다. alias는 간단히 말하면 명찰이다.

예를 들어 노드 a1,a2,a3가 있다 이들은 모두 a라는 명찰사용하고 있다고 생각해보자, 그럼 손님은 a1,2,3를 같은 a라는 직원으로 여기게 된다.

 

손님 = 클라이언트

a = 노드의 별명

a1,2,3 = 실제 노드이름

 

클라이언트인 우리(스프링 프로젝트)의 입장에서는 결국 a에게 데이터를 달라고 하면되고, a1,a2,a3에 있는 데이터를 a라는 이름으로 받아올 수 있게된다.

 

그럼 메인 의사 결정인, 확장성의 이야기로 돌아가보자, alias로 2개의 노드를 1개의 대상으로 볼 수 있다면,

a1 = a

string a
long b

위와 같은 상황에서, a에 새로 필요한 인덱스 형태인 a2를 추가해보자

a2, a1 = a

string a
long b
string c

클라이언트 입장에서는 a에 a2가 추가되었어도 결국은 a로 보이고 a1이 없어져도 결국 a로만 보인다.

즉, 필요한 형태인 a2를 만들고 a1의 내용을 복사한 후 a2을 지우면, 클라이언트는 실상 코드 변경없이도 노드 연결을 유지할 수 있다.

 

이렇게

  1. 별도 인덱스 생성 및 데이터 마이그레이션
  2. 인덱스 Alias 사용

를 선택하게 되었다.

 

그럼 3번인 멀티 인덱스 전략은 왜 나왔는가, 위는 데이터 수정이 필요할 경우의 확장성이라면 아래는 말 그대로 데이터를 효율적으로 사용하기 위한 확장성이라 보면된다.

 

멀티 인덱스는 예시로 이야기하면, 시간 기반 인덱싱 처럼(일별, 월별로 데이터를 나눠서 저장) 데이터를 시간, 특징을 기준으로 나눠 저장하면 이미 분업이 되어 있으므로 특정 데이터 예를 들면, 1달간의 데이터만 요구된다면, 전체 다 저장된 인덱스에서 접근하기 보단 근 1달치만 저장된 인덱스를 가져오면 더 편하다.

 

인덱스 관리에도 유리하고 새 인덱스를 추가해도 전체 매핑 구조에 영향이 덜 미친다.

엘라스틱 서치(Elasticsearch)와 검색 자동 완성 문제 해결

1. 문제 발생

엘라스틱 서치를 이용해 검색 자동 완성 기능을 구현했지만, 일부 검색어가 정상적으로 인식되지 않는 문제가 발생했다. 검색어에 따라 자동 완성이 예상과 다르게 작동하며, 특정 단어가 검색되지 않는 경우도 있었다.

 

2. 문제 원인

  • 엘라스틱 서치에서 기본적으로 제공하는 Nori 형태소 분석기는 완벽한 형태소 분석을 수행하지 않는다.
  • 예를 들어, "용광로"라는 단어가 있을 때, Nori는 이를 다음과 같이 토큰화할 수 있다.
{
  "tokens": [
    { "token": "용", "start_offset": 0, "end_offset": 1, "type": "word", "position": 0 },
    { "token": "왕식", "start_offset": 1, "end_offset": 3, "type": "word", "position": 1 },
    { "token": "용광로", "start_offset": 4, "end_offset": 7, "type": "word", "position": 2 }
  ]
}
  • 이러한 방식은 공백 및 사전 단어를 기준으로 최선의 토큰화를 수행하기 때문에, 원하는 자동 완성 결과를 보장하지 않는다.
  • 신조어나 외래어, 대명사를 제대로 구분하지 못하는 경우도 발생하여, 일부 검색어가 정상적으로 처리되지 않는 문제가 있었다.

 

3. 문제 해결책 수립

해결 방법 비교

해결 방법 설명 장점 단점
AI 기반 형태소 분석기 구축 AI 모델을 학습시켜 최적화된 형태소 분석 수행 가장 정확한 분석 가능 높은 비용과 긴 개발 기간 필요
유료 형태소 분석 API 사용 신뢰할 수 있는 형태소 분석 API를 활용 별도 개발 필요 없음 API 사용료 부담
다른 형태소 분석기 추가 Nori 외에 N-gram 등의 분석기를 병행 사용 구현 난이도 적당, 검색 품질 향상 최적화 과정 필요

 

4. 문제 해결: Nori + Edge-N-gram 조합 사용

현 프로젝트에서는 비용과 개발 시간을 고려하여 Nori 형태소 분석기와 Edge-N-gram을 조합하는 방식을 선택했다.

  • Edge-N-gram을 사용하여 자동 완성 기능을 보완
    • 예를 들어, "용광로"라는 단어가 있다면, Edge-N-gram을 적용했을 때 다음과 같이 인덱싱할 수 있다.
      ["용", "용광", "용광로"]
      
    • 이를 통해 사용자가 "용"을 입력해도 "용광로"가 자동 완성될 수 있도록 함.
  • Nori 형태소 분석기를 그대로 유지하여 의미 기반 검색 가능
    • 단순한 완전 일치 검색이 아니라, 문맥을 고려한 검색이 가능하도록 유지

결론: 엘라스틱 서치의 기본 형태소 분석기인 Nori만으로는 검색 자동 완성 기능을 완벽히 구현하기 어렵다. 따라서, 자동 완성을 위해 Edge-N-gram을 적용하고, 의미 기반 검색을 위해 Nori 형태소 분석기를 함께 활용하는 방식으로 해결했다.

1. 문제 상황

기존의 모놀로틱 아키텍처에서는 모든 기능이 하나의 코드베이스에서 동작하여 서비스 간 결합도가 높고, 확장성과 유지보수성이 제한되는 문제가 발생했다. 이를 해결하기 위해 도메인 주도 설계(DDD, Domain-Driven Design) 원칙을 적용하여 MSA(Microservices Architecture)로 전환하는 방안을 고려했다.

 

하지만, 무조건적으로 서비스를 잘게 분리하면 오히려 결합도가 높아지고 성능 저하 및 복잡성이 증가할 가능성이 있어 서비스 간 결합도를 고려하여 적절한 기준을 설정하여 분리하는 것이 중요했다.


2. MSA 분리 기준

(1) DDD 원칙에 기반한 서비스 분리

MSA를 설계할 때 가장 중요한 것은 도메인의 경계를 명확하게 나누는 것이다. 이를 위해 DDD의 Bounded Context(경계 컨텍스트) 개념을 적용하여 각 서비스가 독립적인 도메인 로직을 가지도록 구성했다. 그러나, 서비스 간의 트랜잭션 처리, 데이터 일관성 유지, 성능 고려, 네트워크 요청 수 증가 고려 등의 이유로 너무 세분화된 서비스는 묶어서 운영하는 전략을 사용하기로 했다.


3. 실제 프로젝트의 서비스 분리

아래와 같이 비즈니스 도메인과 트래픽 특성을 고려하여 MSA를 구성했다.

 

(1) user-chat 서비스

  • 분리 이유: 우리 프로젝트에서는 채팅이 상품 주문 완료 후에만 가능하므로, 주문과 채팅 중 트래픽이 몰릴 경우 주문이 우선순위를 가져야 한다.
  • 서비스 결합도 고려: 채팅은 주문과 완전히 독립적이지 않으므로 완전히 분리하지 않고 같은 서비스에서 운영
  • 설계 방향:
    • 주문 관련 로직을 우선 실행하고, 채팅 요청은 비동기 메시징 시스템(Kafka, RabbitMQ 등)을 활용하여 처리
    • 채팅 서버는 주문 서버와 분리된 워커(worker) 또는 비동기 큐 방식으로 운영

(2) game-product-review-order 서비스

  • 분리 이유:
    • 게임 → 상품 → 주문 → 리뷰로 이어지는 선형적인 관계를 가지는 도메인
    • 상품과 주문은 락(Lock) 관리가 필요하여 강한 결합도를 가짐
    • 리뷰는 주문이 있어야만 생성 가능하므로 해당 도메인에 포함
  • 설계 방향:
    • 상품과 주문은 트랜잭션을 유지해야 하므로 하나의 서비스에서 운영
    • 리뷰는 트랜잭션 요구사항이 적지만, 주문과 긴밀하게 연결되므로 같은 서비스에서 관리
    • 데이터 정합성을 위해 이벤트 소싱(Event Sourcing) 또는 CQRS 적용 검토

(3) security 서비스

  • 분리 이유:
    • 보안 관련 기능(인증, 권한 관리 등)은 다른 도메인과 직접적인 연관이 없음
    • 서비스 전체의 보안 레이어로 동작해야 함
  • 설계 방향:
    • JWT, OAuth2, SSO 등을 활용하여 인증 기능 구현
    • API Gateway에서 인증 및 권한 관리 수행
    • 인증/인가 로직이 필요한 서비스에서 security 서비스와 통신하도록 구성

(4) payment-bill 서비스

  • 분리 이유:
    • 결제 서비스는 트래픽이 급격히 증가할 가능성이 높음
    • 결제 데이터는 강한 일관성이 필요하며, 스케일링을 고려해야 함
  • 설계 방향:
    • 결제 시스템은 **독립적인 데이터베이스(RDS)**를 사용하여 확장성 확보
    • 트랜잭션 안전성을 유지하기 위해 읽기/쓰기 분리(Read Replica 적용)
    • 트래픽 증가 시 Auto Scaling을 적용하여 결제 처리량 조절
    • 외부 결제 시스템(PG사)과의 통합을 고려하여 API 라우팅 최적화

(5) community-comment 서비스

  • 분리 이유:
    • 트래픽이 가장 많이 몰리는 서비스이므로, 독립적으로 확장 가능해야 함
    • 커뮤니티와 댓글은 관계가 있지만, 높은 트래픽을 처리하기 위해 분리가 필요
  • 설계 방향:
    • 데이터베이스 RDS를 읽기/쓰기 분리하여 트래픽 부하 감소
    • 캐싱(Redis, ElasticSearch 등) 도입하여 성능 최적화
    • Kafka와 같은 메시지 큐를 활용하여 비동기 처리 도입
    • API Gateway에서 Rate Limiting 적용하여 DDoS 공격 방어

4. 결론

(1) 적용한 MSA 분리 원칙

  • DDD 원칙에 따라 도메인을 명확하게 구분하여 서비스 분리
  • 트랜잭션 및 강한 결합이 필요한 서비스는 묶어서 운영하여 데이터 정합성을 유지
  • 트래픽 분산이 중요한 서비스는 별도로 분리하여 확장성 확보
  • 읽기/쓰기 부하를 분산하기 위해 RDS의 Read Replica 및 Auto Scaling 활용

(2) 예상되는 효과

1. 확장성 향상 → 서비스별로 독립적인 스케일링이 가능하여 성능 최적화

2. 배포 유연성 증가 → 특정 서비스만 개별 배포 가능하여 배포 속도 증가

3. 트래픽 처리 최적화 → 결제, 커뮤니티 등 고트래픽 서비스의 성능 향상

4. 데이터 정합성 유지 → 강한 결합이 필요한 도메인은 묶어서 관리하여 안정성 유지

 

(3) 향후 개선 방향

  • 서비스 간 통신에서 성능 최적화를 위해 gRPC 적용 검토
  • 데이터 정합성을 높이기 위해 이벤트 소싱(Event Sourcing) 및 CQRS 적용 검토
  • 모니터링 강화를 위해 Prometheus, ELK Stack을 활용한 실시간 로깅 및 트래픽 분석 도입

이번 MSA 분리는 현재 프로젝트의 구조와 트래픽 특성을 고려하여 최적의 아키텍처를 설계하는 데 집중하였다. 향후 운영 과정에서 지속적으로 성능 및 확장성을 고려하여 개선해 나갈 계획이다.

+ Recent posts