GraphQL·보안·전체 흐름 통합 테스트
단위 테스트와 slice 테스트가 모두 통과해도 어댑터 사이의 조립이 잘못될 수 있습니다. 여기서는 random port의 실제 서버와 PostgreSQL 컨테이너를 띄우고, JWT 인증을 거친 REST와 GraphQL이 같은 JPA 저장소를 보는지 확인합니다.
모든 사례를 이 큰 테스트에 복사하지는 않습니다. 사용자가 가장 중요하게 경험하는 생성–조회 경로 하나를 골라, 작은 테스트들이 보장한 조각이 실제 구성에서 연결되는지만 검증합니다.
통합 범위는 어댑터 사이의 계약을 검증한다
RestTestClient와 HttpGraphQlTester는 서로 다른 프로토콜 표현을 사용하지만 같은 실행 중인 애플리케이션에 요청합니다. REST로 만든 행을 GraphQL query에서 읽을 수 있다면 Controller 두 개가 인메모리 가짜 저장소에 따로 연결된 실수도 함께 잡을 수 있습니다.
JWT 발급 서버까지 테스트 대상으로 끌어오지 않고 JwtDecoder만 대체합니다. 이 테스트가 검증하려는 것은 서명 키 배포가 아니라, 검증된 scope가 보안 규칙을 지나 애플리케이션 흐름에 연결되는지이기 때문입니다.
검증 경로는 인증된 REST 생성, 인증된 GraphQL mutation, GraphQL 목록 조회 순서입니다. 어느 요청에서 멈췄는지를 보면 보안·HTTP 번역·저장소 공유 중 조사할 경계를 바로 좁힐 수 있습니다.
REST와 GraphQL을 같은 random-port 서버에서 검증한다
ch5-4에서 REST Controller를 JpaVersionedStudySessionService로 교체했고 ch6도 같은 서비스를 사용합니다. 다음 테스트는 이 누적 구성이 실제로 유지되는지 검증합니다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>test</scope>
</dependency>package com.andongmin.studylog;
import static org.mockito.Mockito.when;
import java.time.Instant;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.graphql.test.autoconfigure.tester.AutoConfigureHttpGraphQlTester;
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.graphql.test.tester.HttpGraphQlTester;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.client.RestTestClient;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.postgresql.PostgreSQLContainer;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureRestTestClient
@AutoConfigureHttpGraphQlTester
@Testcontainers
class StudySessionFlowTest {
@Container
@ServiceConnection
static final PostgreSQLContainer postgres =
new PostgreSQLContainer("postgres:18-alpine");
@Autowired
RestTestClient client;
@Autowired
HttpGraphQlTester graphQl;
@MockitoBean
JwtDecoder jwtDecoder;
@Test
void createsThroughRestAndGraphQlThenReadsGraphQl() {
Instant now = Instant.now();
when(jwtDecoder.decode("write-token")).thenReturn(new Jwt(
"write-token", now, now.plusSeconds(300),
Map.of("alg", "none"), Map.of("sub", "learner", "scope", "study.read study.write")));
client.post().uri("/api/study-sessions")
.header("Authorization", "Bearer write-token")
.header("X-API-Version", "1.0")
.body(Map.of("subject", "Spring", "topic", "Integration",
"studyDate", "2026-07-13", "minutes", 45))
.exchange()
.expectStatus().isCreated()
.expectHeader().exists("Location");
HttpGraphQlTester authenticated = graphQl.mutate()
.headers(headers -> headers.setBearerAuth("write-token"))
.build();
authenticated.document("""
mutation Create($input: CreateStudySessionInput!) {
createStudySession(input: $input) { id status }
}
""")
.variable("input", Map.of(
"subject", "Spring", "topic", "GraphQL",
"studyDate", "2026-07-13", "minutes", 50))
.execute()
.path("createStudySession.status").entity(String.class)
.isEqualTo("PLANNED");
authenticated.document("{ studySessions { topic } }")
.execute()
.path("studySessions[*].topic").entityList(String.class)
.contains("Integration", "GraphQL");
}
}401·403이면 decoder stub과 scope를, REST 생성만 실패하면 버전 헤더·DTO·상태 코드를, 생성은 성공하지만 GraphQL에서 보이지 않으면 두 Controller가 공유하는 서비스와 transaction을 확인합니다.
회원 권한부터 저장 결과까지 증거를 남긴다
다음 명령은 Docker를 사용하므로 순수 단위 테스트보다 느립니다. CI에서는 Docker 사용 가능 여부와 이미지 캐시를 별도로 관리하고, 로컬에서는 실패 로그와 컨테이너 로그를 함께 남깁니다.
./mvnw -q -Dtest=StudySessionFlowTest test실제 random-port 서버와 PostgreSQL 18을 사용해 JWT 인가, REST 생성, GraphQL mutation·query, JPA 저장이 한 흐름에서 성공해야 합니다.전체 테스트 수보다 계층별 실패 신호가 중요하다
같은 시나리오를 모든 레벨에 복제하지 않고 규칙, 변환, 통합 위험을 가장 작은 적절한 범위에서 검증합니다.
기능의 연결이 확인되었으니 다음 장부터는 "동작한다"를 넘어 지연 시간과 DB 비용을 측정하고 확장 한계를 찾습니다.