Page History
...
| Code Block | ||||
|---|---|---|---|---|
| ||||
package com.example.cafe24;
import akka.actor.ActorRef;
import akka.actor.ActorSystem;
import akka.testkit.javadsl.TestKit;
import com.example.cafe24.actor.SafeApiCallerActor;
import com.example.cafe24.api.DummyCafe24Server;
import com.example.cafe24.message.Messages.ApiRequest;
import com.example.cafe24.message.Messages.ApiResponse;
import org.junit.jupiter.api.*;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import static org.junit.jupiter.api.Assertions.*;
/**
* SafeApiCallerActor 백프레셔 장치 검증 테스트.
*
* <p>시나리오:
* <ol>
* <li>hello → world 변환 검증</li>
* <li>일반 단어 에코 검증</li>
* <li>직접 호출(백프레셔 미적용) 시 429 발생 확인</li>
* <li>백프레셔 적용 시 대량 burst 요청이 429 없이 모두 성공</li>
* </ol>
*/
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class SafeApiCallerTest {
private static ActorSystem system;
private static DummyCafe24Server dummyServer;
private static final int SERVER_PORT = 18080;
private static final int BUCKET_CAPACITY = 10;
private static final int LEAK_RATE = 2; // 초당 2회 감소
@BeforeAll
static void setup() throws Exception {
system = ActorSystem.create("test-system");
dummyServer = new DummyCafe24Server(SERVER_PORT, BUCKET_CAPACITY, LEAK_RATE);
dummyServer.start();
// 서버 준비 대기
Thread.sleep(500);
}
@AfterAll
static void teardown() {
dummyServer.stop();
TestKit.shutdownActorSystem(system);
}
@BeforeEach
void resetServer() throws InterruptedException {
dummyServer.reset();
// 버킷 초기화 후 안정화 대기
Thread.sleep(200);
}
@Test
@Order(1)
@DisplayName("hello 요청 시 world 응답 반환")
void testHelloWorld() {
new TestKit(system) {{
ActorRef caller = system.actorOf(
SafeApiCallerActor.props("http://localhost:" + SERVER_PORT, 2));
caller.tell(new ApiRequest("hello", getRef()), getRef());
ApiResponse response = expectMsgClass(Duration.ofSeconds(5), ApiResponse.class);
assertEquals("hello", response.word());
assertEquals("world", response.result());
assertEquals(200, response.statusCode());
expectNoMessage(Duration.ofMillis(200));
}};
}
@Test
@Order(2)
@DisplayName("일반 단어 에코 응답 반환")
void testEchoResponse() {
new TestKit(system) {{
ActorRef caller = system.actorOf(
SafeApiCallerActor.props("http://localhost:" + SERVER_PORT, 2));
caller.tell(new ApiRequest("akka-stream", getRef()), getRef());
ApiResponse response = expectMsgClass(Duration.ofSeconds(5), ApiResponse.class);
assertEquals("akka-stream", response.word());
assertEquals("akka-stream", response.result());
assertEquals(200, response.statusCode());
expectNoMessage(Duration.ofMillis(200));
}};
}
@Test
@Order(3)
@DisplayName("직접 호출: 대량 동시 요청 시 429 발생 확인 (백프레셔 미적용)")
void testDirectCallCauses429() {
HttpClient client = HttpClient.newHttpClient();
int totalRequests = 20;
List<CompletableFuture<HttpResponse<String>>> futures = new ArrayList<>();
// 백프레셔 없이 20건 동시 호출 → 버킷(capacity=10) 초과 → 429 발생
for (int i = 0; i < totalRequests; i++) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:" + SERVER_PORT + "/api/echo?word=direct-" + i))
.timeout(Duration.ofSeconds(5))
.GET()
.build();
futures.add(client.sendAsync(request, HttpResponse.BodyHandlers.ofString()));
}
long count429 = futures.stream()
.map(CompletableFuture::join)
.filter(r -> r.statusCode() == 429)
.count();
long count200 = futures.stream()
.map(CompletableFuture::join)
.filter(r -> r.statusCode() == 200)
.count();
System.out.println("[직접호출] 총 " + totalRequests + "건 → 200: " + count200 + "건, 429: " + count429 + "건");
assertTrue(count429 > 0, "백프레셔 없이 동시 " + totalRequests + "건 호출 시 429가 발생해야 합니다");
}
@Test
@Order(4)
@DisplayName("백프레셔 적용: 대량 burst 요청이 429 없이 모두 200 성공")
void testBackpressurePrevents429() {
new TestKit(system) {{
// 백프레셔 적용 - 초당 2건으로 throttle (버킷 leak rate와 동일)
ActorRef caller = system.actorOf(
SafeApiCallerActor.props("http://localhost:" + SERVER_PORT, 2));
int totalRequests = 15;
for (int i = 0; i < totalRequests; i++) {
caller.tell(new ApiRequest("safe-" + i, getRef()), getRef());
}
// 모든 응답 수집 (throttle로 인해 ~8초 소요)
List<ApiResponse> responses = new ArrayList<>();
for (int i = 0; i < totalRequests; i++) {
ApiResponse response = expectMsgClass(Duration.ofSeconds(30), ApiResponse.class);
responses.add(response);
}
// 모든 응답이 200이어야 함 (429 없음)
long successCount = responses.stream()
.filter(r -> r.statusCode() == 200)
.count();
System.out.println("[백프레셔] 총 " + totalRequests + "건 → 200: " + successCount + "건");
for (ApiResponse r : responses) {
System.out.println(" word=" + r.word() + ", result=" + r.result()
+ ", bucket=" + r.bucketUsed() + "/" + r.bucketMax());
}
assertEquals(totalRequests, successCount,
"백프레셔 적용 시 모든 요청이 429 없이 성공해야 합니다");
// 에코 검증
for (ApiResponse r : responses) {
assertTrue(r.result().startsWith("safe-"),
"에코 응답이어야 합니다: " + r.result());
}
}};
}
@Test
@Order(5)
@DisplayName("버킷 상태 헤더가 응답에 포함됨")
void testBucketHeadersInResponse() {
new TestKit(system) {{
ActorRef caller = system.actorOf(
SafeApiCallerActor.props("http://localhost:" + SERVER_PORT, 2));
caller.tell(new ApiRequest("header-test", getRef()), getRef());
ApiResponse response = expectMsgClass(Duration.ofSeconds(5), ApiResponse.class);
assertTrue(response.bucketMax() > 0, "버킷 최대값이 포함되어야 합니다");
assertTrue(response.bucketUsed() > 0, "버킷 사용량이 포함되어야 합니다");
assertEquals(BUCKET_CAPACITY, response.bucketMax(), "버킷 최대값이 서버 설정과 일치해야 합니다");
}};
}
}
|
지원가능 스킬
...
목록
다음 과제:
...
분산환경 클러스터 모드로의 확장
| 스킬 | 명령어 | 플랫폼 |
|---|---|---|
| Java Akka Classic | /java-akka-classic | Java + Akka Classic 2.7.x |
| Kotlin Pekko Typed | /kotlin-pekko-typed | Kotlin + Pekko Typed 1.4.x |
| C# Akka.NET | /dotnet-akka-net | C# + Akka.NET 1.5.x |
| Java Akka Classic Test | /java-akka-classic-test | Java + Akka Classic TestKit |
| Kotlin Pekko Typed Test | /kotlin-pekko-typed-test | Kotlin + Pekko Typed ActorTestKit |
| C# Akka.NET Test | /dotnet-akka-net-test | C# + Akka.TestKit.Xunit2 |
| Java Akka Classic Cluster | /java-akka-classic-cluster | Java + Akka Classic Cluster 2.7.x |
| Kotlin Pekko Typed Cluster | /kotlin-pekko-typed-cluster | Kotlin + Pekko Typed Cluster 1.4.x |
| C# Akka.NET Cluster | /dotnet-akka-net-cluster | C# + Akka.NET Cluster 1.5.x |
| Java Akka Classic Infra | /java-akka-classic-infra | Java + Akka Classic + Docker/K8s |
| Kotlin Pekko Typed Infra | /kotlin-pekko-typed-infra | Kotlin + Pekko Typed + Docker/K8s |
| C# Akka.NET Infra | /dotnet-akka-net-infra | C# + Akka.NET + Docker/K8s |
| AI Agent Pipeline (.NET) | /actor-ai-agent | C# + Akka.NET + LLM |
| AI Agent Pipeline (Java) | /actor-ai-agent-java | Java + Akka Classic + LLM |
| AI Agent Pipeline (Kotlin) | /actor-ai-agent-kotlin | Kotlin + Pekko Typed + LLM |
...