メインコンテンツまでスキップ

3. 応答形式およびエラー処理

V5 APIのすべての呼び出しはタイプ DTOまたはboolean/longを返します。失敗時には次のような安全なデフォルト値を返します。

3.1 V5 応答パターン​

メソッドパターン成功時の返り値失敗時の返り値
create(req) / update(id, req) / get(id)*ResponseV5 インスタンスnull
list() 形式List<*ResponseV5>空リスト (絶対に null ではない)
delete(id)truefalse
exists(id)true (あり)false (なしまたはエラー)
count() 形式long (≥0)0
Order ライフサイクル (start/end/pause/resume/abort)true (成功)false (遷移不可など)

例外はスローされません。 HTTPエラー、サーバーERROR、JSONパース失敗はすべて上記の表にある安全なデフォルト値に吸収されます。エラーの原因はデバッグログまたはenvelopeの _code/_message で確認します。

3.2 内部 wire format (参考)​

V5 サーバーは次のような envelope 形式で応答します。クライアントライブラリが自動的にパースするため、通常の使用時に意識する必要はありません。

// 단건 성공
{ "_status": "OK", "data": { ... } }

// 목록 성공
{ "_status": "OK", "data": [ { ... }, { ... } ] }

// 목록 — items 키 안에 배열
{ "_status": "OK", "data": { "items": [ ... ] } }

// 에러
{
"_status": "ERROR",
"_code": "E1002",
"_message": "validation failed: ...",
"_http_status": 400
}

3.3 V5 ErrorCode マッピング​

サーバーが ERROR 応答を送信するとき、_code フィールドに次のコードが含まれます。正本は製品の plantpulse.core.exception.ErrorCode であり、HTTP ステータスもその列挙型と共に決定されます。コードとステータスは対であるため、どちらか一方のみを見て分岐させても問題ありません。

コードHTTP名前一般的な原因
E1000400INVALID_REQUEST文法エラー、タイプ不一致
E1001400MISSING_PARAMETER必須パラメータの不足
E1002422VALIDATION_FAILED値の範囲・形式・ドメイン規則違反 (ID接頭辞など)
E1100401UNAUTHORIZEDapi_key の不足または無効
E1101429AUTH_TOO_MANY_ATTEMPTS同一IPの認証失敗蓄積 — Retry-After を参照
E1102401TOKEN_EXPIREDトークン期限切れ — 再発行で復旧します
E1103401TOKEN_REVOKEDトークン回収 — 同一トークンでは復旧しません
E1104401TOKEN_IP_MISMATCHトークンに紐付けられたIPと呼び出しIPが不一致
E1200403FORBIDDEN認証済みだが権限なし
E1300404NOT_FOUND該当IDなし
E1400405METHOD_NOT_ALLOWED許可されていないメソッド
E1500409CONFLICTリソース競合
E1501409DUPLICATE_ENTITY既に存在するエンティティ
E1600413PAYLOAD_TOO_LARGEリクエストボディが過大
E1700415UNSUPPORTED_MEDIA_TYPEサポートされていないメディアタイプ
E1800422IDEMPOTENCY_CONFLICT同一 Idempotency-Key + 異なるbody
E1801409IDEMPOTENCY_IN_PROGRESS同一キーを処理中 — しばらくして再試行
E1900429RATE_LIMIT_EXCEEDEDレートリミット超過
E5000500INTERNAL_ERRORサーバー内部エラー
E5100500SERVICE_ERRORサービス処理エラー
E5200500DAO_ERRORデータアクセスエラー
E5300500ENGINE_ERRORエンジン処理エラー
E5400502EXTERNAL_SERVICE_ERROR外部システム呼び出し失敗
E5500503SERVICE_UNAVAILABLEサービス利用不可 — 判定ではなく「できなかった」ことを意味します。 Retry-After を参照
E5503503SHUTTING_DOWNサーバー停止中 — 他のノードに転送するか Retry-After 分待機します
E5600504TIMEOUTサーバー側リクエストタイムアウト
429が2種類あります — 混同して処理すると復旧しません

E1900 (レートリミット) は速度を落とせば解除されます。E1101 (認証試行過多) は速度を落としても解除されません。資格情報自体が誤っており、そのIPがウィンドウが閉じるまでロックされている状態です。これらを同じバックオフでまとめると、後者は永遠に再試行を繰り返すことになります。

400 と 422 を区別します

400 はリクエストを「解釈」できないという意味であり、422 は解釈はできたが「内容」が規則に沿っていないという意味です(2026-08-31より)。形式を修正すべきか、値を修正すべきかがステータスコードで分かれます。人間はメッセージを読みますが、エージェントはステータスコードで次の試行を決定するためです。

よくある拒否事例​

HTTPreason意味
422asset_statement.eql.missing_columnsステートメント EQL がこの種類の必須項目を出力していません — 設備ステートメント
422asset_statement.eql.unknown_type存在しないステートメント種類です
422alarm.invalid.alarm_config_idアラーム設定作成時に直接指定したIDが入力されました。GET /alarm/config/new-id で受け取ってください
409—同一タグに2番目の帯域(バンド)アラーム設定を作成しようとしました。タグあたり1つのみです

3.4 エラー情報の抽出​

V5 サービスメソッドは envelope を直接公開しないため、エラーの詳細は次の2つの方法で確認します。

方法 1 — デバッグモード​

APIClient_V5 client = new APIClient_V5(proto, host, port, user, token, true);
client.connect();

SiteResponseV5 result = client.site().create(req);
if (result == null) {
// 콘솔 로그에서 envelope 확인 가능
System.err.println("등록 실패 — 디버그 로그 확인");
}

コンソール出力:

[V5] POST /api/v5/site body={...}
[V5] response: 400 {"_status":"ERROR","_code":"E1002","_message":"site_id must start with SITE_"}

方法 2 — BaseServiceV5 static メソッドの使用 (カスタム拡張)​

BaseServiceV5 のヘルパーは JSONObject 応答からエラー情報を抽出します。ライブラリを拡張して envelope をキャプチャするカスタムサービスを作成する場合に有用です。

import plantpulse.api.v5.service.BaseServiceV5;
import plantpulse.json.JSONObject;

// 환경 (envelope) JSONObject 가 있다면
String code = BaseServiceV5.getErrorCode(env); // 예: "E1002"
String message = BaseServiceV5.getErrorMessage(env); // 예: "validation failed: ..."
int httpStatus = BaseServiceV5.getHttpStatus(env); // 예: 400

3.5 安全な呼び出しパターン​

CRUD 呼び出し​

import plantpulse.api.v5.dto.request.SiteRequestV5;
import plantpulse.api.v5.dto.response.SiteResponseV5;

SiteRequestV5 req = new SiteRequestV5();
req.setSite_id("SITE_DJ");
req.setSite_name("대전공장");

SiteResponseV5 created = client.site().create(req);
if (created == null) {
// 실패 처리 — 예: 알람, 재시도, 다른 경로 시도
log.warn("사이트 생성 실패");
return;
}
System.out.println("등록 완료: " + created.getSite_id());

リスト呼び出し​

List<SiteResponseV5> sites = client.site().list();
// 실패해도 null 이 아님 — 그대로 순회 가능
for (SiteResponseV5 s : sites) {
System.out.println(s.getSite_id());
}
if (sites.isEmpty()) {
log.info("등록된 사이트 없음");
}

存在確認 → 照会​

if (client.site().exists("SITE_DJ")) {
SiteResponseV5 site = client.site().get("SITE_DJ");
// ...
}

Order ライフサイクル (boolean 返却)​

String orderId = "ORDER_000001";

if (!client.order().start(orderId)) {
// 전이 불가 (예: 이미 START 상태)
log.warn("작업 시작 실패 — 현재 상태 확인");
return;
}

// 작업 진행 ...

if (!client.order().end(orderId)) {
log.warn("작업 종료 실패");
}

3.6 再試行パターン​

V5 は自動再試行を行いません。一時的なネットワークエラーに備えた再試行が必要な場合は、直接実装してください。

import java.util.function.Supplier;

public static <T> T callWithRetry(Supplier<T> apiCall, int maxRetries, long baseDelayMs) {
int attempt = 0;
while (true) {
T result = apiCall.get();
if (result != null) return result; // 성공
if (++attempt >= maxRetries) return null; // 포기
try {
Thread.sleep(baseDelayMs * attempt); // 지수 백오프
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
return null;
}
}
}

// 사용
SiteResponseV5 site = callWithRetry(
() -> client.site().get("SITE_DJ"),
3,
1_000L);

503 · 429 と Retry-After​

再起動や過負荷で現在受け付けられない場合は、Retry-After (秒単位の整数) が共に送られてきます。Java クライアントはこのヘッダーを遵守しますが、基本として30秒を超えて待機することはありません。直接呼び出す場合は以下に従ってください。

状況応答
ウェブ層がダウンしている (再起動・再デプロイ) — プロキシが応答503 E5500, Retry-After: 15
サーバー停止進行中503 E5503, Retry-After: 15
後方ストレージ一時不可 (DB再起動など)503 E5500, Retry-After: 15
トークン別リクエスト上限超過429 E1900, Retry-After で上限を計算
ストリーム同時数超過429 E1900 または 503 E5500, Retry-After
  • 読み取りは、指定時間待機してから再度呼び出します。
  • 書き込みは、同一の Idempotency-Key で再度呼び出します。再起動で遮断されたリクエストは、既に反映されている可能性があります。
  • 500 は故障であり、502 E5400 は外部呼び出し失敗です。どちらも Retry-After は存在しません。

3.7 空応答と null 応答の区別​

V5 において null または false は次の2つの意味を共に含みます:

  1. サーバーにデータがない (NOT_FOUND, E1300)
  2. 呼び出しに失敗した (ネットワークエラー、検証失敗など)

これらを区別するには、exists() を併用してください。

if (!client.site().exists("SITE_DJ")) {
log.info("사이트가 등록되어 있지 않습니다");
// 신규 등록 로직
} else {
SiteResponseV5 site = client.site().get("SITE_DJ");
if (site == null) {
log.error("사이트는 존재하지만 조회 실패 — 일시적 오류 가능");
}
}

次のステップ​