Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Code Block
themeEmacs
linenumberstrue
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(), "버킷 최대값이 서버 설정과 일치해야 합니다");
        }};
    }
}

코드 생성부터 테스트까지 버벅거림없이 수행이 되었으며, 생성된 코드보다 코드를 생성하는 프롬프트(플랜,스킬) 관리가 중요해진것같습니다.

작업을 최종 마무리하면  나만의 메모리에 너가 수행한 결과를 "메모라이저" 에 저장 하면 다음과 같은 메모리 조각이 추가되어 지금까지 생성여정을 간략하게 볼수 있습니다.