> For the complete documentation index, see [llms.txt](https://kopens.gitbook.io/plantpulse-platform/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://kopens.gitbook.io/plantpulse-platform/developer/api-manual/error-handling.md).

# 3. 응답 형식 및 에러 처리

V5 API의 모든 호출은 **타입 DTO** 또는 **boolean/long**을 반환합니다. 실패 시에는 다음과 같은 안전 기본값을 반환합니다.

## 3.1 V5 응답 패턴

| 메서드 패턴                                        | 성공 반환               | 실패 반환                    |
| --------------------------------------------- | ------------------- | ------------------------ |
| `create(req)` / `update(id, req)` / `get(id)` | `*ResponseV5` 인스턴스  | `null`                   |
| `list()` 계열                                   | `List<*ResponseV5>` | **빈 리스트** (절대 `null` 아님) |
| `delete(id)`                                  | `true`              | `false`                  |
| `exists(id)`                                  | `true` (있음)         | `false` (없거나 오류)         |
| `count()` 계열                                  | `long` (≥0)         | `0`                      |
| Order 라이프사이클 (`start/end/pause/resume/abort`) | `true` (성공)         | `false` (전이 불가 등)        |

> **예외가 throw되지 않습니다.** HTTP 오류, 서버 ERROR, JSON 파싱 실패는 모두 위 표의 안전 기본값으로 흡수됩니다. 에러 원인은 디버그 로그나 envelope의 `_code`/`_message`에서 확인합니다.

## 3.2 내부 wire format (참고)

V5 서버는 다음과 같은 envelope 형태로 응답합니다. 클라이언트 라이브러리가 자동으로 파싱하므로, 일반 사용 시에는 신경 쓸 필요가 없습니다.

```json
// 단건 성공
{ "_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` 필드에 다음 코드가 들어옵니다.

| 코드      | 의미                      | 일반적 원인                |
| ------- | ----------------------- | --------------------- |
| `E1001` | INVALID\_INPUT          | 필수 필드 누락, 타입 불일치      |
| `E1002` | VALIDATION\_FAILED      | ID 접두사 위반 (`SITE_` 등) |
| `E1100` | UNAUTHORIZED            | api\_key 누락 또는 무효     |
| `E1101` | FORBIDDEN               | 권한 부족                 |
| `E1200` | CONFLICT                | 중복 ID, FK 충돌          |
| `E1300` | NOT\_FOUND              | 해당 ID 없음              |
| `E1400` | DOMAIN\_RULE\_VIOLATION | Order 상태 전이 불가 등      |
| `E1500` | DEPENDENCY\_FAILED      | 외부 시스템 응답 실패          |
| `E1900` | INTERNAL\_ERROR         | 서버 내부 오류              |

## 3.4 에러 정보 추출

V5 서비스 메서드는 envelope을 직접 노출하지 않으므로, 에러 상세는 다음 두 가지 방법으로 확인합니다.

### 방법 1 — 디버그 모드

```java
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를 캡처하는 커스텀 서비스를 만들 때 유용합니다.

```java
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 호출

```java
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());
```

### 목록 호출

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

### 존재 확인 → 조회

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

### Order 라이프사이클 (boolean 반환)

```java
String orderId = "ORD_20260514_001";

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

// 작업 진행 ...

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

## 3.6 재시도 패턴

V5는 자동 재시도를 하지 않습니다. 일시적 네트워크 오류를 대비한 재시도가 필요하면 직접 구현하시면 됩니다.

```java
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);
```

## 3.7 빈 응답과 null 응답 구별

V5에서 `null` 또는 `false`는 다음 두 가지 의미를 모두 포함합니다:

1. **서버에 데이터가 없음** (`NOT_FOUND`, `E1300`)
2. **호출이 실패함** (네트워크 오류, 검증 실패 등)

이를 구별하려면 `exists()`를 함께 사용하시면 됩니다.

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

## 다음 단계

* [System 서비스](/plantpulse-platform/developer/api-manual/system-service.md) — 헬스체크
* [Site 서비스](/plantpulse-platform/developer/api-manual/site-service.md) — 마스터 데이터 시작점
