영어 원문의 번역
이 페이지는 영어 원문을 번역했어요. 내용이 다르면 영어 원문이 기준이에요.
첫 조회 경로 정책¶
기대한 주문을 돌려주는데도, 조회 경로가 데이터베이스에 쓰기 때문에 실패하는 테스트를 실행해 봐요. 예제는 공개된 QueryAudit 0.6.0과 인메모리 H2를 써요. Java 17 이상, Git, 네트워크 연결이 필요해요.
1. 예상하지 못한 쓰기로 실패시키기¶
git clone https://github.com/haroya01/query-audit.git
cd query-audit
./gradlew -p examples/first-audit test -PextraWrite=true --rerun-tasks
이 테스트는 SELECT 1개를 허용하고 INSERT, UPDATE, DELETE는 0개만 허용해요.
주문의 상태는 여전히 기대한 값이지만, 추가된 UPDATE 때문에 정책이 실패해요.
QueryAudit: readsOnce() exceeded its query budget.
UPDATE: executed 1, expected at most 0.
UPDATE orders SET status = 'NEW' WHERE id = 1
이게 기대한 실패예요. 다운로드, 컴파일, 시작 오류는 캡처를 검증한 게 아니에요. 예산 메시지가 보이지 않으면 문제 해결을 참고하세요.
2. SQL과 호출 위치 확인하기¶
생성된 감사 결과를 열어요.
| 필드 | 실패한 실행의 결과 |
|---|---|
outcome | FAIL |
reports[0].summary.totalQueries | 2: SELECT 1개와 UPDATE 1개 |
reports[0].queries의 UPDATE 항목: sql | UPDATE orders SET status = ? WHERE id = ? |
그 항목의 stackTrace | example.audit.FirstAuditTest.writeOnReadPath:43로 시작해요 |
JSON은 리터럴 값을 가리고 애플리케이션 호출 위치는 남겨요. 이 릴리스에서는 예산 메시지의 첫 호출 위치가 JDBC 프록시일 수 있어요. 애플리케이션 코드를 찾을 때는 JSON 증거를 쓰세요.
3. 쓰기를 빼고 통과시키기¶
같은 테스트를 플래그 없이 실행해요.
새 JSON에는 SELECT 1개만 있고 쓰기는 없어요. 쿼리 정책과 기능 단언이 모두 통과해요.
쿼리 수 회귀도 확인해 보려면:
실행할 때마다 report.json이 바뀌어요. 세 가지 결과를 모두 검증하고 리포트를 남기려면:
검증기는 Gradle 종료 코드, JUnit 결과, 캡처된 SQL, 애플리케이션 호출 위치, 공개 아티팩트 버전을 확인해요. 결과는 examples/first-audit/build/reports/first-audit-verification/에 저장돼요.
4. 내 코드에 예산 적용하기¶
| 내 환경 | 다음 단계 |
|---|---|
| 기존 Spring Boot 데이터베이스 테스트 | 스타터를 설치하고 캡처를 검증해요 |
| 순수 JUnit 5 / JDBC | 계측된 DataSource를 노출해요 |
테스트의 기능 단언은 그대로 두세요. 테스트에 @EnableQueryInspector와 @ExpectQueries를 직접 붙이고, 서비스나 리포지토리가 계측된 DataSource를 쓰게 해요. 그 작업에 맞는 SELECT 상한을 정하고, INSERT/UPDATE/DELETE를 0으로 두면 조회 경로에 쓰기가 끼어드는 걸 막을 수 있어요.
예산은 상한이에요. select = 1은 캡처된 SELECT가 0개여도 통과하고, 적지 않은 속성은 검사하지 않아요. 0.7.2부터는 exact = true로 적은 속성을 정확한 개수로 바꿀 수 있어요. 통과를 믿기 전에 실제 작업으로 일부러 위반을 만들어 확인하세요. @EnableQueryInspector는 탐지 결과를 참고용으로만 두지만, 명시한 예산은 여전히 실패해요.
실행 가능한 전체 소스
픽스처는 감사가 시작되기 전에 준비돼요. 두 플래그는 테스트하는 작업만 바꾸고, 단언과 정책은 그대로예요.
package example.audit;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import io.queryaudit.junit5.EnableQueryInspector;
import io.queryaudit.junit5.ExpectQueries;
import javax.sql.DataSource;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
import org.h2.jdbcx.JdbcDataSource;
import org.junit.jupiter.api.Test;
@EnableQueryInspector
class FirstAuditTest {
static final DataSource DATA_SOURCE = dataSource();
@Test
@ExpectQueries(select = 1, insert = 0, update = 0, delete = 0)
void readsOnce() throws Exception {
readOne();
if (Boolean.getBoolean("queryAudit.demo.extraQuery")) {
readOne();
}
if (Boolean.getBoolean("queryAudit.demo.extraWrite")) {
writeOnReadPath();
}
}
private void readOne() throws Exception {
try (var connection = DATA_SOURCE.getConnection();
var statement = connection.createStatement();
var result = statement.executeQuery("SELECT status FROM orders WHERE id = 1 LIMIT 1")) {
assertTrue(result.next());
assertEquals("NEW", result.getString("status"));
}
}
private void writeOnReadPath() throws Exception {
try (var connection = DATA_SOURCE.getConnection();
var statement = connection.createStatement()) {
// The returned order is unchanged, but the read path now executes a write.
assertEquals(1, statement.executeUpdate("UPDATE orders SET status = 'NEW' WHERE id = 1"));
}
}
private static DataSource dataSource() {
var h2 = new JdbcDataSource();
h2.setURL("jdbc:h2:mem:firstaudit;DB_CLOSE_DELAY=-1");
// Prepare the fixture before the audited test starts.
try (var connection = h2.getConnection();
var statement = connection.createStatement()) {
statement.execute("CREATE TABLE orders (id INT PRIMARY KEY, status VARCHAR(20))");
statement.executeUpdate("INSERT INTO orders (id, status) VALUES (1, 'NEW')");
} catch (java.sql.SQLException exception) {
throw new IllegalStateException("Cannot prepare the order fixture", exception);
}
return ProxyDataSourceBuilder.create(h2).name("first-audit").build();
}
}
데이터베이스와 버전 범위
H2로는 캡처와 예산을 확인할 수 있어요. 인덱스에 의존하는 검사는 메타데이터가 없다는 안내를 출력할 수 있어요. 그 데이터베이스의 인덱스를 확인하려면 MySQL이나 PostgreSQL 모듈을 추가하세요. 예제는 저장소 빌드와 별개로 공개된 릴리스를 내려받아요. 스위트 전체를 CI에서 요구하기 전에 알려진 한계를 확인하세요.
변경이 생겨도 정책 유지하기¶
- 테스트의 쿼리 수를 계약 파일에 기록하고, 의도한 변경은 diff로 리뷰해요.
- 실패한 감사를 확인할 때는 SQL과 호출 위치를 봐요.
- 첫 CI 체크를 추가한 뒤 감사할 테스트를 요구하고 호환되는 실행을 비교해요.