Slices: 마이크로서비스에 적합한 크기

발행: (2026년 1월 19일 오전 06:49 GMT+9)
15 min read
원문: Dev.to

Source: Dev.to

번역을 진행하려면 번역이 필요한 전체 텍스트를 제공해 주시겠어요? 텍스트를 주시면 요청하신 대로 한국어로 번역해 드리겠습니다.

The Granularity Trap

마이크로서비스를 도입하는 모든 팀은 결국 같은 벽에 부딪힙니다: 서비스는 얼마나 크게 만들어야 할까?

  • 너무 작게 만들면 네트워크 호출, 분산 트랜잭션, 배포 복잡성에 빠집니다.

    • 단순한 “사용자 프로필 조회” 작업조차 이제 다섯 개 서비스가 관여하고, 그 중 세 개는 데이터베이스 테이블에 대한 프록시에 불과합니다.
    • 지연 시간이 누적됩니다.
    • 디버깅이 고고학이 됩니다.
  • 너무 크게 만들면 다시 모놀리식이 됩니다.

    • 서로 다른 팀이 서로 방해합니다.
    • 배포에 조정이 필요합니다.
    • 마이크로서비스의 “micro”가 아이러니하게 됩니다.

표준 조언—“경계가 있는 컨텍스트당 하나의 서비스” 혹은 “서비스는 독립적으로 배포 가능해야 한다”—는 타당해 보이지만 실행 가능한 가이드를 제공하지 못합니다.

  • 한 컨텍스트는 어디서 끝나고 다른 컨텍스트는 어디서 시작할까?
  • 무엇이 “독립적으로 배포 가능”하게 만드는가?

팀들은 극단 사이를 오가며, “너무 작은” 서비스를 더 큰 서비스로 리팩터링하고, 다시 “너무 큰” 서비스는 나누는 작업을 반복합니다. 근본적인 질문에 답이 없기 때문에 이 사이클은 계속됩니다: 올바른 경계를 결정하는 기준은 무엇인가?

문제는 크기가 아니라 경계다.

잘 정의된 경계는 다음과 같은 구체적인 특성을 가집니다:

  1. 명확한 계약 – 호출자는 무엇을 요청할 수 있고 무엇을 받을지 정확히 알고 있습니다.
  2. 명시적인 의존성 – 구성 요소는 외부에서 무엇이 필요한지 선언합니다.
  3. 내부 자유 – 구현 세부 사항은 호출자에 영향을 주지 않도록 변경될 수 있습니다.

크기는 경계에서 따라오는 것이지, 그 반대가 아닙니다. 구성 요소는 자신의 경계를 완전히 소유할 때, 즉 계약을 이행하는 데 필요한 모든 것이 내부에 존재하고 외부와의 모든 상호작용이 명시적인 의존성을 통해 이루어질 때 적절한 크기가 됩니다.

대부분의 마이크로서비스 설계가 실패하는 이유는 경계를 다음과 같은 기준으로 그리기 때문입니다:

  • 기술 계층 (API 게이트웨이, 비즈니스 로직, 데이터베이스 접근)
  • 조직 구조 (팀 소유권)

이 두 접근법 모두 실제 구성 요소 간의 계약에 초점을 맞추지 않기 때문에 안정적인 경계를 만들지 못합니다.

Source:

Introducing Slices

Slice는 계약에 의해 정의되는 배포 가능한 단위입니다. 단일 어노테이션이 있는 인터페이스를 작성하면 됩니다:

@Slice
public interface OrderService {
    Promise createOrder(CreateOrderRequest request);
    Promise getOrder(GetOrderRequest request);
    Promise cancelOrder(CancelOrderRequest request);
}

그게 전부입니다. 어노테이션 프로세서는 나머지—팩토리 메서드, 의존성 연결, 배포 메타데이터—를 모두 생성합니다.

인터페이스가 경계다

  • 메서드가 계약을 정의한다 – 각 메서드는 요청을 받아 응답 Promise를 반환합니다.
  • 요청/응답 타입이 명시적이다 – 숨겨진 파라미터나 주변 컨텍스트가 없습니다.
  • 기본적으로 비동기다Promise가 성공과 실패 경로를 모두 처리합니다.

구현은 이 인터페이스 뒤에 존재합니다. 구현이 단순하든 복잡하든, 다른 slice를 호출하든 완전히 독립적이든 관계없습니다. 경계는 이를 신경 쓰지 않습니다.

Slices는 Aether 위에서 실행됩니다. Aether는 slice 계약을 중심으로 설계된 분산 런타임입니다. 서비스 디스커버리, 직렬화, slice 간 통신을 직접 설정할 필요가 없습니다—Aether가 slice 인터페이스 선언을 기반으로 자동으로 처리합니다. 클러스터가 살아 있는 한 모든 slice 간 호출은 결국 성공하며, 런타임이 재시도, 장애 조치, 복구를 투명하게 관리합니다.

Forge는 현실적인 조건에서 slice를 테스트할 수 있는 개발 환경을 제공합니다—부하 생성, 혼돈 주입, 백엔드 시뮬레이션 등. 스테이징에 배포해 압력 하에서 slice가 어떻게 동작하는지 확인하는 대신, 로컬에서 Forge를 실행하고 관찰하면 됩니다.

개발 경험은 간단합니다:

  1. @Slice 인터페이스를 작성한다.
  2. 구현한다.
  3. Forge로 테스트한다.
  4. Aether에 배포한다.

어노테이션 프로세서는 모든 보일러플레이트—팩토리, 의존성 연결, 라우팅 메타데이터—를 생성합니다.

명시적 의존성

전통적인 서비스 아키텍처에서는 의존성을 설정 파일, 환경 변수, 런타임 디스커버리 등에 숨깁니다. 서비스가 무엇을 필요로 하는지 알기 위해 코드를 읽고, 네트워크 호출을 추적하고, 혹은 프로덕션에서 실패할 때까지 기다려야 합니다.

Slices는 인터페이스 자체에 의존성을 선언합니다:

@Slice
public interface OrderService {
    Promise createOrder(CreateOrderRequest request);
    // Other methods...

    // Factory method declares dependencies explicitly
    static OrderService orderService(InventoryService inventory,
                                     PaymentService payment) {
        return OrderServiceFactory.orderService(Aspect.identity(),
                                                inventory, payment);
    }
}

어노테이션 프로세서는 모든 것을 연결하는 팩토리를 생성합니다:

public final class OrderServiceFactory {
    private OrderServiceFactory() {}

    public static OrderService orderService(
            Aspect aspect,
            InventoryService inventory,
            PaymentService payment) {
        return aspect.apply(new OrderServiceImpl(inventory, payment));
    }
}
  • 팩토리 메서드 시그니처가 의존성을 선언합니다.
  • 서비스 로케이터도 없고, 런타임 디스커버리도 없으며, 현실과 일치하지 않을 수도 있는 설정 파일도 없습니다.
  • 의존성은 컴파일 시점에 보이고, 배포 전에 검증됩니다.

이 명시성은 중요합니다:

  • 코드를 읽음으로써 의존성 그래프를 추적할 수 있습니다.
  • 다른 구현을 전달해 대체 구현으로 테스트할 수 있습니다.
  • Forge는 어떤 작업을 시작하기 전에 전체 그래프를 검증합니다.

세 가지 런타임 모드 (동일한 Slice 코드)

ModeDescription
Ember다중 클러스터 노드를 가진 단일 프로세스 런타임. 빠른 시작, 간단한 디버깅. 로컬 개발에 이상적입니다.
ForgeEmber + 부하 생성 및 혼돈 주입. 어디에도 배포하지 않고 slice가 압력 하에서 어떻게 동작하는지 테스트합니다.
Aether전체 분산 클러스터. 모든 복원력 보장을 갖춘 프로덕션 배포.

코드는 어느 모드에서 실행되고 있는지 알 필요가 없습니다. Slice 인터페이스, 구현, 그리고 의존성은 동일하게 유지됩니다.

stay identical. 런타임은 차이를 처리합니다—인터‑슬라이스 호출이 인‑프로세스인지 크로스‑네트워크인지 투명하게 처리됩니다.

Impact on the Development Workflow

  1. Write & debug in Ember – 빠른 인‑프로세스 실행.
  2. Stress‑test in Forge – 부하와 장애를 주입합니다.
  3. Deploy to Aether – 프로덕션 수준의 복원력.

어떠한 단계에서도 계약을 다시 작성하거나, 설정을 변경하거나, 환경에 맞게 코드를 조정할 필요가 없습니다. 슬라이스 모델은 서비스의 규모가 아니라 boundaries에 집중할 수 있게 해줍니다.

Slices and Their “Right Size”

환경에 맞게 slice 코드를 수정하시나요?
경계가 명확하고 배포가 유연하면 “적정 크기”에 대한 질문은 사라집니다.

When Is a Slice the Right Size?

  • Interface – 일관된 작업 집합을 캡처합니다.
  • Dependencies – slice가 실제로 필요로 하는 것을 정확히 반영합니다.
  • Implementation – 계약을 이행할 수 있습니다.

최소·최대 크기는 없습니다.

  • 인증 slice는 두 개의 메서드를 노출할 수 있습니다.
  • 주문 처리 slice는 스무 개의 메서드를 노출할 수 있습니다.

크기는 도메인에 따라 결정되며, 코드 라인 수나 팀 구조에 대한 임의의 규칙에 의해 정해지지 않습니다.

Recoverability

Slice를 잘못 설계해도 복구 가능합니다:

SituationWhat Happens
Split a slice that grew too complex경계가 바뀌지만 호출자는 새 인터페이스만 보게 됩니다.
Merge slices that were artificially separated호출자는 여전히 단일 인터페이스를 보며, 내부 구현이 단순히 결합될 뿐입니다.

Slice를 리팩터링하는 것은 코드 리팩터링일 뿐이며, 인프라를 다시 작성하는 것이 아닙니다.

JBCT 패턴의 홈인 슬라이스

각 슬라이스 메서드는 데이터‑변환 파이프라인입니다:

Parse input (validated request types)

Gather data (dependencies, other slices)

Process (business logic)

Respond (typed response)

여섯 가지 JBCT 패턴—Leaf, Sequencer, Fork‑Join, Condition, Iteration, Aspects—은 슬라이스 메서드 내부 및 전체에서 구성됩니다. 슬라이스는 관련된 변환 집합을 둘러싼 배포 경계에 불과합니다.

이 조합이 효과적인 이유

  • JBCT는 슬라이스 내부일관된 구조를 제공합니다.
  • Slices는 슬라이스 일관된 경계를 제공합니다.

함께 사용하면 아키텍처 엔트로피의 두 주요 원인을 제거합니다:

  1. 일관성 없는 구현 패턴
  2. 불명확한 서비스 경계

서비스 세분화에서 계약‑중심 설계로

마이크로서비스 세분화 문제는 잘못된 질문을 하기 때문에 계속됩니다:

  • 잘못된 질문:서비스는 얼마나 커야 하나요?” – 좋은 답이 없습니다.
  • 올바른 질문:구성 요소 간 계약은 무엇인가요?” – 정확한 답변.

Slices는 초점을 크기에서 경계로 전환합니다:

  1. 인터페이스 정의.
  2. 의존성을 명시적으로 선언.
  3. 배포 토폴로지를 운영 요구에 맞게 조정하도록 하여 코드 구조를 강제하지 않음.

결과

  • 경계구성상 명확합니다.
  • 의존성설계상 가시적입니다.
  • 배포 유연성은 코드를 다시 작성할 필요가 없습니다.

Part of Java Backend Coding Technology – 예측 가능하고 테스트 가능한 백엔드 코드를 작성하기 위한 방법론.

Back to Blog

관련 글

더 보기 »