콘텐츠로 이동

5. SQL API#

모든 SQL API는 JSON 형식의 요청 본문을 사용한다. 공통 요청 필드는sql, args이며, args를 생략하거나 null로 전달하면 빈 배열([])로 처리한다.

필드 필수 설명
sql O 실행할 SQL
args X PreparedStatement 파라미터 배열

예시:

{
  "sql": "SELECT ID, NAME FROM EMPLOYEES WHERE DEPARTMENT_ID = ?",
  "args": [10]
}

5.1 레코드 기반 조회#

엔드포인트 :

POST /dataapi/query/records

요청 예시:

{
  "sql": "SELECT ID, NAME FROM EMPLOYEES WHERE DEPARTMENT_ID = ?",
  "args": [10]
}

성공 응답:

{
  "records": [
    {
      "ID": 1,
      "NAME": "Alice"
    }
  ]
}

Records API는 결과 행(row)을 JSON 객체로 반환한다. JSON 객체의 Key는 SQL 결과의 컬럼 이름을 사용하며, AS 절로 별칭을 지정한 경우에는 별칭이 Key가 된다.

제약사항

  • JSON 객체의 Key는 중복될 수 없으므로, 조회 결과의 컬럼 이름(또는 별칭)은 서로 달라야 한다. 예를 들어, SELECT a AS X, b AS X ...처럼 동일한 별칭을 사용하면 DATA_009 오류가 발생한다.
  • 동일한 컬럼 이름이나 별칭을 반환해야 하는 경우에는 튜플 기반 API(/query/values)를 사용한다.

5.2 튜플 기반 조회#

엔드포인트 :

POST /dataapi/query/values

요청 예시:

{
  "sql": "SELECT ID FROM EMPLOYEES WHERE DEPARTMENT_ID = ?",
  "args": [10]
}

성공 응답:

{
  "columns": [
    {
      "name": "ID",
      "jdbcType": 4,
      "jdbcTypeName": "INTEGER",
      "nullable": false
    }
  ],
  "rows": [
    [1]
  ]
}

values 조회 API는 컬럼 정보(columns)와 조회 결과(rows)를 분리하여 반환한다.

columns에는 컬럼 이름, JDBC 타입, NULL 허용 여부 등의 정보가 포함되며, rows에는 각 행의 실제 데이터가 columns와 동일한 순서의 배열 형태로 출력된다. 이 요청은 매 행마다 컬럼 이름을 반복하여 전송하지 않으므로, 조회 결과가 많을수록 전송 데이터의 크기를 줄일 수 있다.

5.3 SQL 실행#

엔드포인트 :

POST /dataapi/execute

요청 예시1 - 단건 요청:

{
  "sql": "UPDATE EMPLOYEES SET SALARY = SALARY * ? WHERE DEPARTMENT_ID = ?",
  "args": [1.1, 10]
}

요청 예시2 - batch 요청:

{
  "sql": "INSERT INTO EMPLOYEES (ID, NAME) VALUES (?, ?)",
  "args": [
    [1, "Alice"],
    [2, "Bob"]
  ]
}

성공 응답:

{
  "affectedRows": 5
}

/execute API는 DML과 DDL을 실행하며, 영향을 받은 행 수를 affectedRows로 반환한다. SELECT 처럼 결과 집합을 반환하는 SQL은 실행할 수 없으며, 요청은 DATA_005 오류로 거부된다.

SQL의 실행은 args의 형태에 따라 단건 실행과 Batch 실행으로 동작한다. args가 1차원 배열이면 단건 실행으로, 2차원 배열이면 Batch 실행으로 처리된다.

실행 방식#

args 형태 실행 방식
1차원 배열 단건 실행
2차원 배열 Batch 실행

예를 들어,

  • [1, "Alice"]는 단건 실행으로 처리된다.
  • [[1, "Alice"], [2, "Bob"]]는 Batch 실행으로 처리된다.

Batch 실행#

Batch 실행에서는 args의 모든 요소가 행(row) 배열이어야 한다.

예를 들어 다음 요청은 올바른 Batch 요청이다.

"args": [
  [1, "Alice"],
  [2, "Bob"]
]

반면, 행 배열과 일반 값(Number, String, Boolean, null 또는 Structured Object)을 함께 사용할 수 없으며, 이러한 요청은 COMMON_002 오류로 거부된다.

Batch 실행은 INSERT, UPDATE, DELETE 문만 지원한다. DDL, SELECT, Stored Procedure 호출은 Batch 실행을 지원하지 않으며 DATA_008 오류가 발생한다.

Batch는 All-or-Nothing 방식으로 실행된다. 실행 중 하나라도 실패하면 전체 Batch가 Rollback되며, 성공 시 affectedRows에는 모든 작업의 영향을 받은 행 수의 합이 반환된다.

5.4 레코드 기반 스트리밍 조회#

엔드포인트 :

POST /dataapi/stream/records

요청 예시:

{
  "sql": "SELECT ID, NAME FROM EMPLOYEES ORDER BY ID",
  "args": []
}

성공 응답 Content-Type:

application/x-ndjson

성공 응답 예시:

{"record":{"ID":1,"NAME":"Alice"}}
{"complete":true,"rowCount":1}

/query/records는 조회 결과를 모두 생성한 후 하나의 JSON 문서로 반환하는 반면, /stream/records는 조회 결과를 NDJSON 형식으로 행 단위 전송한다. 따라서 조회 결과가 많거나 결과를 즉시 처리해야 하는 경우에 적합하다.

제약 사항은 5.1 레코드 기반 조회와 동일하다. 동일한 컬럼 이름이나 별칭을 반환해야 하는 경우에는 튜플 기반 스트리밍 조회 API(/stream/values)를 사용한다.

5.5 튜플 기반 스트리밍 조회#

엔드포인트 :

POST /dataapi/stream/values

요청:

{
  "sql": "SELECT ID, NAME FROM EMPLOYEES ORDER BY ID",
  "args": []
}

성공 응답 Content-Type:

application/x-ndjson

성공 응답 예시:

{"columns":[{"name":"ID","jdbcType":4,"jdbcTypeName":"INTEGER","nullable":false}]}
{"row":[1]}
{"complete":true,"rowCount":1}

/stream/values/query/values와 동일한 조회 결과를 반환하지만, 응답을 하나의 JSON 문서로 생성하지 않고 NDJSON 형식으로 순차 전송한다. 따라서 조회 결과가 많거나 결과를 즉시 처리해야 하는 경우에 적합하다.

응답은 columns, row, complete 순서의 JSON 객체로 구성된다.

  • columns는 컬럼 정보를 포함하며, 한 번만 전송된다.
  • row는 조회 결과의 각 행마다 반복 전송된다.
  • 마지막 JSON 객체에는 completerowCount가 포함된다.rowCount는 전송된 전체 행 수를 나타내며, 클라이언트는 complete 값을 확인하여 모든 조회 결과가 정상적으로 전송되었는지 확인할 수 있다.

5.6 Stored Procedure 지원 범위#

DataAPI는 Stored Procedure 전용 API를 제공하지 않는다. EXEC 문으로 실행 가능한 Stored Procedure만 지원하며, 반환 결과에 따라 다음 SQL API를 사용한다.

Stored Procedure 반환 결과 사용가능한 API
결과 집합(ResultSet)을 반환하지 않음 /execute
결과 집합(ResultSet)을 반환함 /query/records, /query/values, /stream/records, /stream/values

지원하지 않는 기능#

DataAPI는 다음 기능을 지원하지 않는다.

  • CallableStatement 전용 scalar OUTINOUT 파라미터
  • Procedure 반환값(Return Value)
  • multiple ResultSet 반환
  • getMoreResults() 반복 처리가 필요한 호출

5.7 NDJSON 스트리밍 응답 처리#

NDJSON 응답은 하나의 JSON 문서가 아니라 여러 개의 JSON 객체를 줄 단위로 순차 전송하는 형식이다. 따라서 클라이언트는 응답 본문 전체를 한 번에 파싱하지 않고, 각 줄을 독립적인 JSON 객체로 읽어 순차적으로 처리해야 한다. 실시간 응답을 확인하려면 curl-N 옵션을 사용한다.

curl 예제

curl -N -X POST http://localhost:8080/dataapi/stream/records \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "Accept: application/x-ndjson" \
  -d '{
    "sql": "SELECT ID, NAME FROM EMPLOYEES ORDER BY ID",
    "args": []
  }'

응답

{"record":{"ID":1,"NAME":"Alice"}}
{"record":{"ID":2,"NAME":"Bob"}}
{"complete":true,"rowCount":2}

spring 예제

아래 예제는 /stream/records 응답을 처리하는 예시이다.

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestClient;

ObjectMapper objectMapper = new ObjectMapper();
RestClient restClient = RestClient.builder()
        .baseUrl("http://localhost:8080/dataapi")
        .build();

StreamSummary summary = restClient
        .post()
        .uri("/stream/records")
        .headers(headers -> {
            headers.setBearerAuth(accessToken);
            headers.setContentType(MediaType.APPLICATION_JSON);
            headers.setAccept(List.of(MediaType.parseMediaType("application/x-ndjson")));
        })
        .body(new SqlRequest("SELECT ID, NAME FROM EMPLOYEES ORDER BY ID", List.of()))
        .exchange((request, response) -> {
            if (!response.getStatusCode().is2xxSuccessful()) {
                throw new IllegalStateException("stream request failed: " + response.getStatusCode());
            }

            boolean completed = false;
            long rowCount = 0;

            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(response.getBody(), StandardCharsets.UTF_8))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    if (line.isBlank()) {
                        continue;
                    }

                    JsonNode event = objectMapper.readTree(line);
                    if (event.has("record")) {
                        System.out.println("row " + event.get("record"));
                    } else if (event.path("complete").asBoolean(false)) {
                        completed = true;
                        rowCount = event.path("rowCount").asLong();
                    }
                }
            }

            if (!completed) {
                throw new IllegalStateException("stream closed before complete line");
            }
            return new StreamSummary(rowCount);
        });

record SqlRequest(String sql, List<Object> args) {}
record StreamSummary(long rowCount) {}

Note

/stream/values를 사용할 경우에는 첫 번째 JSON 객체의 columns 정보를 먼저 읽고, 이후 전달되는 row 배열을 해당 컬럼 순서에 맞추어 해석해야 한다.