Akka.NET 모범 사례
이 스킬을 사용해야 할 때
다음과 같은 경우에 이 스킬을 사용하십시오:
- 액터 통신 패턴 설계
- EventStream과 DistributedPubSub 사이에서 결정
- 액터 내 오류 처리 구현
- 감독 전략 이해
- Props 패턴과 DependencyResolver 선택
- 노드 간 작업 분산 설계
- 클러스터 인프라 유무에 관계없이 실행 가능한 테스트 가능한 액터 시스템 생성
- 로컬 테스트 시나리오를 위한 클러스터 샤딩 추상화
참조 파일
- work-distribution-patterns.md: 데이터베이스 큐, Akka.Streams 스로틀링, 아웃박스 패턴
- cluster-local-abstractions.md: GenericChildPerEntityParent, IPubSubMediator, 실행 모드 연결
- async-cancellation-patterns.md: 액터 범위 CancellationToken, 연결된 CTS, 타임아웃 처리
1. EventStream vs DistributedPubSub
중요: EventStream은 로컬 전용입니다
Context.System.EventStream은 단일 ActorSystem 프로세스에만 로컬입니다. 클러스터 노드 간에 동작하지 않습니다.
// 잘못된 예: 이것은 단일 서버에서만 동작합니다.
// 두 번째 서버를 추가하면 서버 2의 구독자는 서버 1의 이벤트를 수신하지 못합니다.
Context.System.EventStream.Subscribe(Self, typeof(PostCreated));
Context.System.EventStream.Publish(new PostCreated(postId, authorId));
EventStream이 적절한 경우:
- 단일 프로세스 내 로깅 및 진단
- 진정한 단일 프로세스 애플리케이션을 위한 로컬 이벤트 버스
- 개발/테스트 시나리오
다중 노드에는 DistributedPubSub 사용
여러 클러스터 노드에 걸쳐 있어야 하는 이벤트의 경우 Akka.Cluster.Tools.PublishSubscribe를 사용하십시오:
using Akka.Cluster.Tools.PublishSubscribe;
public class TimelineUpdatePublisher : ReceiveActor
{
private readonly IActorRef _mediator;
public TimelineUpdatePublisher()
{
// DistributedPubSub 중재자 가져오기
_mediator = DistributedPubSub.Get(Context.System).Mediator;
Receive<PublishTimelineUpdate>(msg =>
{
// 토픽에 게시 - 모든 노드의 모든 구독자에게 도달
_mediator.Tell(new Publish($"timeline:{msg.UserId}", msg.Update));
});
}
}
DistributedPubSub용 Akka.Hosting 구성
builder.WithDistributedPubSub(role: null); // 모든 역할에서 사용 가능, 또는 역할 지정
토픽 디자인 패턴
| 패턴 | 토픽 형식 | 사용 사례 |
|---|---|---|
| 사용자별 | timeline:{userId} | 타임라인 업데이트, 알림 |
| 엔티티별 | post:{postId} | 게시물 참여 업데이트 |
| 브로드캐스트 | system:announcements | 시스템 전체 알림 |
| 역할 기반 | workers:rss-poller | 작업 분산 |
2. 감독 전략
주요 설명: 감독은 자식을 위한 것입니다
액터에 정의된 감독 전략은 해당 액터가 자식을 감독하는 방법을 결정하며, 액터 자체가 감독되는 방법이 아닙니다.
public class ParentActor : ReceiveActor
{
// 이 전략은 ParentActor 자체가 아닌 ParentActor의 자식에게 적용됩니다.
protected override SupervisorStrategy SupervisorStrategy()
{
return new OneForOneStrategy(
maxNrOfRetries: 10,
withinTimeRange: TimeSpan.FromSeconds(30),
decider: ex => ex switch
{
ArithmeticException => Directive.Resume,
NullReferenceException => Directive.Restart,
ArgumentException => Directive.Stop,
_ => Directive.Escalate
});
}
}
기본 감독 전략
기본 OneForOneStrategy에는 이미 속도 제한이 포함되어 있습니다:
- 1초 이내 10회 재시작 = 액터가 영구 중지됨
- 이는 무한 재시작 루프를 방지합니다
특별한 요구 사항이 없다면 사용자 정의 전략이 거의 필요하지 않습니다.
사용자 정의 감독을 정의해야 하는 경우
좋은 이유:
- 액터가 복구 불가능한 상태 손상을 나타내는 예외를 발생시킴 -> 재시작
- 재시작을 유발해서는 안 되는 예외(예상된 실패)를 발생시킴 -> 재개
- 자식 실패가 형제에게 영향을 미쳐야 함 ->
AllForOneStrategy사용 - 기본값과 다른 재시도 제한이 필요함
나쁜 이유:
- "그냥 안전을 위해" - 기본값이 이미 안전합니다
- 액터가 무엇을 하는지 이해하지 못함 - 먼저 이해하십시오
3. 오류 처리: 감독 vs Try-Catch
Try-Catch를 사용해야 하는 경우 (대부분의 경우)
다음과 같은 경우 try-catch를 사용하십시오:
- 실패가 예상됨 (네트워크 시간 초과, 잘못된 입력, 외부 서비스 중단)
- 예외가 발생한 정확한 이유를 알고 있음
- 정상적으로 처리할 수 있음 (재시도, 오류 응답 반환, 로깅 후 계속)
- 재시작이 도움이 되지 않음 (동일한 오류가 다시 발생)
public class RssFeedPollerActor : ReceiveActor
{
public RssFeedPollerActor()
{
ReceiveAsync<PollFeed>(async msg =>
{
try
{
var feed = await _httpClient.GetStringAsync(msg.FeedUrl);
var items = ParseFeed(feed);
// 항목 처리...
}
catch (HttpRequestException ex)
{
// 예상된 실패 - 로깅 후 재시도 예약
_log.Warning("피드 {Url}을(를) 사용할 수 없음: {Error}", msg.FeedUrl, ex.Message);
Context.System.Scheduler.ScheduleTellOnce(
TimeSpan.FromMinutes(5), Self, msg, Self);
}
catch (XmlException ex)
{
// 잘못된 피드 형식 - 로깅 후 불량으로 표시
_log.Error("피드 {Url}의 형식이 잘못되었음: {Error}", msg.FeedUrl, ex.Message);
Sender.Tell(new FeedPollResult.InvalidFormat(msg.FeedUrl));
}
});
}
}
감독이 처리하도록 해야 하는 경우
다음과 같은 경우 예외를 전파(감독 트리거)하십시오:
- 예외가 발생한 원인을 전혀 모름
- 액터의 상태가 손상되었을 수 있음
- 재시작이 도움이 될 것임 (새 상태, 리소스 재연결)
- 프로그래밍 오류임 (잘못된 논리로 인한 NullReferenceException, InvalidOperationException)
안티 패턴: 알 수 없는 예외 삼키기
// 잘못된 예: 예외를 삼키면 문제가 숨겨집니다.
catch (Exception ex)
{
_log.Error(ex, "작업 처리 중 오류 발생");
// 액터가 잠재적으로 손상된 상태로 계속 실행됩니다.
}
// 올바른 예: 알려진 예외는 처리하고, 알 수 없는 예외는 전파합니다.
catch (HttpRequestException ex)
{
// 알려진 예상 실패 - 정상적으로 처리
_log.Warning("HTTP 요청 실패: {Error}", ex.Message);
Sender.Tell(new WorkResult.TransientFailure());
}
// 알 수 없는 예외는 감독으로 전파됩니다.
4. Props vs DependencyResolver
일반 Props를 사용해야 하는 경우
다음과 같은 경우 Props.Create()를 사용하십시오:
- 액터가
IServiceProvider또는IRequiredActor<T>를 필요로 하지 않음 - 모든 종속성을 생성자를 통해 전달할 수 있음
- 액터가 간단하고 자체 포함됨
// DI가 필요 없는 간단한 액터
public static Props Props(PostId postId, IPostWriteStore store)
=> Akka.Actor.Props.Create(() => new PostEngagementActor(postId, store));
DependencyResolver를 사용해야 하는 경우
다음과 같은 경우 resolver.Props<T>()를 사용하십시오:
- 액터가 범위 서비스를 생성하기 위해
IServiceProvider가 필요함 - 액터가 다른 액터에 대한 참조를 얻기 위해
IRequiredActor<T>를 사용함 - 액터가 이미 DI 컨테이너에 있는 많은 종속성을 가짐
// DI로 등록
builder.WithActors((system, registry, resolver) =>
{
var actor = system.ActorOf(resolver.Props<OrderProcessorActor>(), "order-processor");
registry.Register<OrderProcessorActor>(actor);
});
원격 배포 고려 사항
원격 배포는 거의 필요하지 않습니다. 원격 배포를 하지 않는다면(아마 하지 않을 것입니다):
Props.Create(() => new Actor(...))를 클로저와 함께 사용해도 괜찮습니다.- "직렬화 문제" 경고는 적용되지 않습니다.
대부분의 애플리케이션에서는 원격 배포 대신 클러스터 샤딩을 사용하십시오. 자동으로 분산을 처리합니다.
5. 작업 분산 패턴
백그라운드 작업(RSS 피드, 이메일 전송 등)이 많을 때 한 번에 모두 처리하지 마십시오. 이는 떼밀기 문제(Thundering Herd Problem)를 유발합니다.
이 문제를 해결하는 세 가지 패턴:
- 데이터베이스 기반 작업 큐 -
FOR UPDATE SKIP LOCKED를 사용하여 자연스러운 노드 간 분산 구현 - Akka.Streams 속도 제한 - 단일 노드 내 처리 속도 제한
- 내구성 있는 큐(아웃박스 패턴) - 데이터베이스 기반 아웃박스로 안정적인 처리 보장
전체 코드 샘플은 work-distribution-patterns.md를 참조하십시오.
6. 일반적인 실수 요약
| 실수 | 잘못된 이유 | 수정 |
|---|---|---|
| 노드 간 pub/sub에 EventStream 사용 | EventStream은 로컬 전용입니다 | DistributedPubSub 사용 |
| 액터를 "보호"하기 위해 감독 정의 | 감독은 자식을 보호합니다 | 계층 구조 이해 |
| 모든 예외 캐치 | 버그를 숨기고 상태를 손상시킵니다 | 예상된 오류만 캐치 |
| 항상 DependencyResolver 사용 | 불필요한 복잡성을 추가합니다 | 가능하면 일반 Props 사용 |
| 모든 백그라운드 작업을 한 번에 처리 | 떼밀기, 리소스 고갈 | 데이터베이스 큐 + 속도 제한 사용 |
| 예상된 실패에 대해 예외 발생 | 불필요한 재시작을 유발합니다 | 결과 유형 반환, 메시징 사용 |
7. 빠른 참조
통신 패턴 결정 트리
액터 간 통신이 필요합니까?
├── 동일 프로세스만? -> EventStream 괜찮음
├── 클러스터 노드 간?
│ ├── 점대점? -> ActorSelection 또는 알려진 IActorRef 사용
│ └── 게시/구독? -> DistributedPubSub 사용
└── 외부 시스템으로 발사 후 망각? -> 아웃박스 패턴 고려
오류 처리 결정 트리
액터에서 예외 발생?
├── 예상된 실패(HTTP 시간 초과, 잘못된 입력)?
│ └── try-catch, 정상적으로 처리, 계속
├── 상태가 손상되었을 수 있음?
│ └── 감독이 재시작하게 함
├── 원인 불명?
│ └── 감독이 재시작하게 함
└── 프로그래밍 오류(null 참조, 잘못된 논리)?
└── 감독이 재시작하게 하고 버그 수정
Props 결정 트리
액터 Props 생성?
├── 액터가 IServiceProvider를 필요로 합니까?
│ └── resolver.Props<T>() 사용
├── 액터가 IRequiredActor<T>를 필요로 합니까?
│ └── resolver.Props<T>() 사용
├── 생성자 매개변수가 있는 간단한 액터?
│ └── Props.Create(() => new Actor(...)) 사용
└── 원격 배포가 필요합니까?
└── 아마 아닙니다 - 대신 클러스터 샤딩 사용
8. 클러스터/로컬 모드 추상화
클러스터 프로덕션 환경과 로컬/테스트 환경 모두에서 실행해야 하는 애플리케이션의 경우 추상화 패턴을 사용하여 구현 간에 전환하십시오:
AkkaExecutionMode열거형 - 사용할 구현 제어(LocalTest vs Clustered)GenericChildPerEntityParent- 동일한IMessageExtractor를 사용하여 로컬에서 샤딩 동작 모방IPubSubMediator- 교체 가능한 로컬/클러스터 구현을 위해 DistributedPubSub 추상화
전체 구현 코드는 cluster-local-abstractions.md를 참조하십시오.
9. 액터 로깅
ILoggingAdapter 사용, ILogger 사용 안 함
액터에서는 DI로 주입된 ILogger<T> 대신 Context.GetLogger()의 ILoggingAdapter를 사용하십시오:
public class MyActor : ReceiveActor
{
private readonly ILoggingAdapter _log = Context.GetLogger();
public MyActor()
{
Receive<MyMessage>(msg =>
{
_log.Info("사용자 {UserId}에 대한 메시지 처리 중", msg.UserId);
_log.Error(ex, "{MessageType} 처리 실패", msg.GetType().Name);
});
}
}
ILoggingAdapter를 사용해야 하는 이유:
- Akka의 로깅 파이프라인 및 감독과 통합됨
- v1.5.57부터 의미론적/구조적 로깅 지원
- 메서드 이름:
Info(),Debug(),Warning(),Error()(Log*변형 아님) - DI 필요 없음 - 액터 컨텍스트에서 직접 가져옴
ILogger를 액터에 주입하지 마십시오 - Akka의 로깅 인프라를 우회합니다.
의미론적 로깅 (v1.5.57+)
// 더 나은 로그 집계 및 쿼리를 위한 명명된 자리 표시자
_log.Info("주문 {OrderId}이(가) 고객 {CustomerId}에 대해 처리됨", order.Id, order.CustomerId);
// 위치 기반보다 명명된 자리 표시자 선호
// 좋음: {OrderId}, {CustomerId}
// 피해야 할 것: {0}, {1}
10. CancellationToken으로 비동기 작업 관리
액터가 PipeTo를 통해 비동기 작업을 시작할 때, 제대로 관리되지 않으면 해당 작업이 액터보다 오래 지속될 수 있습니다. 주요 방법:
- PostStop의 액터 CTS -
PostStop()에서 항상 취소 및 해제 - 작업당 새 CTS - 새 작업을 시작하기 전에 이전 작업 취소
- 모든 곳에 토큰 전달 - EF Core 쿼리, HTTP 호출 등
- 시간 초과를 위한 연결된 CTS - 외부 호출은 중단을 방지하기 위해 짧은 시간 초과 설정
- 정상적인 처리 - catch 블록에서 시간 초과와 종료 구분
전체 구현 코드는 async-cancellation-patterns.md를 참조하십시오.