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 |
- 지금 작성된 로컬액터를
java-akka-classic-cluster,java-akka-classic-infra스킬을 활용해 분산처리 가능한 모델로 확장하고 쿠버네티스에서 분산배포후 안전호출량 모듈이 잘작동하나 테스트도 수행할것
스킬을 잘작성하는경우 생성부터 테스트까지 플랜 수행이 잘되는것같으며 코드 생성부터 테스트까지 버벅거림없이 수행이 되었으며, 생성된 코드보다 코드를 생성하는 프롬프트(플랜,스킬) 관리가 중요해진것같습니다.
...
- https://mcp.webnori.com/ui/view/77f53edb-8a7f-4c3d-a465-2cb35c8e3396
- 클로드 코드에 자동메모리가 탑재되긴 했으나.. 셀프 제작 MCP-메모리는 테크정리 장기기억 메모리로 역할을 구분해 활용중에 있습니다. 메모리 기능이 기본탑재되어 MCP-메모리를 우선채택안해 이름을 부여하고 "메모라이저" 에 저장으로 바이브 경험을 "메모리" 키워드는 순정모드에게 양보