HTTP 포트
port:
ExternalApiService:
meta: http @url("https://api.example.com") @auth("Bearer token")
methods:
getUser:
meta: method @get("/api/users/{userId}")
params:
userId: long
return: UserDTO
createUser:
meta: method @post("/api/users")
params:
body: UserRequest @body
return: UserResponse
updateUser:
meta: method @HttpEndpoint(url="/api/users/{id}", method="PUT")
params:
id: long
body: UserRequest @body
return: UserResponse
| 위치 | 어노테이션 | 뜻 |
|---|---|---|
| 포트 | @url("baseUrl") | 서버의 기본 URL |
| 포트 | @auth("token") | 기본 Authorization 헤더 |
| 포트 | @timeout(connect=.., read=.., write=.., response=..) | 타임아웃(ms, 기본 10000 / 90000 / 90000 / 60000) |
| 메서드 | @get, @post, @put, @delete("/path") | 메서드와 경로 |
| 메서드 | @HttpEndpoint(url=, method=, contentType=) | 상세 형식 (PATCH 포함) |
| 파라미터 | @body | 요청 본문. URL 의 {이름} 은 경로 변수, 그 밖은 쿼리 파라미터 |
런타임 속성
포트마다 application.yml 의 {domainName}.{PortServiceName}.{속성} 이 DSL 을 덮어씁니다 - 예를 들어
erp.CreditCheckPort.url, .connectTimeout, .readTimeout, .authKey, .apiKey. 전역 기본값은
elasticore.http.connectTimeout, .readTimeout 입니다. 프록시, SSL, multipart form-data 는 포트 가이드
(blueprint/elasticore-dsl-port-guide.md)에 있습니다.
두 가지 클라이언트
| 런타임 프록시 (기본) | httpPort: spring (Spring Boot 4) | |
|---|---|---|
| 생성물 | @ExternalService 인터페이스 | Spring @HttpExchange 인터페이스 + @ImportHttpServices 의 <Domain>HttpServices + 기본값 properties 파일 |
| 등록 | @EnableElastiCore | Spring |
| base URL | 호출할 때마다 읽음({id}.url) | 애플리케이션이 뜰 때 한 번 정해짐 |
elasticore:
config:
httpPort: spring # 이 도메인의 HTTP 포트를 Spring HTTP 서비스 클라이언트로
implementation 'org.springframework.boot:spring-boot-starter-restclient'
@HttpExchange
public interface CreditCheckPort {
@GetExchange(url = "/partners/{bizNo}/credit")
CreditResultDTO checkCredit(@PathVariable("bizNo") String bizNo);
@PostExchange(url = "/partners/{bizNo}/review")
CreditResultDTO requestReview(@PathVariable("bizNo") String bizNo, @RequestBody(required = false) CreditResultDTO body);
}
그룹 이름은 <도메인>--<포트>(erp--credit-check-port)입니다. 예전 키는 그대로 통하고, Boot 4 키는
spring.http.serviceclient.erp--credit-check-port.base-url 등입니다. Spring Boot 3 에서 켜면 경고하고 런타임 프록시로
생성합니다. 몇몇 모양(form-urlencoded, @contentType 없는 multipart, 배열 파라미터 ...)은 런타임 방식으로 남고, 빌드
로그가 이유를 말합니다.
상대가 실패했을 때
포트는 실패를 판단하지 않습니다. 두 방식 모두 오류 응답의 본문을 반환값으로 돌려줍니다:
| 상대가 | 런타임 프록시 | httpPort: spring |
|---|---|---|
| 200, 읽을 수 있는 본문 | 반환값 | 반환값 |
| 4xx/5xx, JSON 본문 | 반환값 (없는 필드는 null) | 반환값 |
| 4xx/5xx, 반환 타입으로 읽을 수 없는 본문 | 예외 | 예외 |
| 답이 없음 - 연결 거부, 시간 초과 | 예외 (WebClientRequestException 등) | RestClientException |
순진하게 쓰면 틀린 채로 성공합니다: 택배사가 500 을 답해도 운송장 번호가 null 인 채 주문이 "배송됨" 으로
저장되고 200 이 나갑니다. 포트를 부르는 코드가 판단하고 io.elasticore.springboot3.http.PortCallException 을 던지면,
생성된 XxxApiErrors 가 502, "code": "upstream" 으로 답합니다:
@Override
@Transactional // 상태 변경과 호출을 한 트랜잭션으로: 실패하면 함께 되돌림
public PurchaseOrderDTO ship(String orderId) {
PurchaseOrderDTO shipped = super.ship(orderId);
ShipmentReply reply;
try {
reply = courier.ship(new ShipmentRequest(orderId));
} catch (RestClientException unreachable) { // 런타임 프록시면 WebClientException 도
throw new PortCallException("The courier could not be reached", unreachable);
}
if (reply == null || reply.getTrackingNo() == null) { // 오류 상태도 값으로 오므로 내용으로 판단
throw new PortCallException("The courier did not accept the shipment");
}
...
}
PortCallException 은 unchecked 라 트랜잭션 안에서 던지면 그 메서드가 바꾼 것이 되돌려집니다. 메시지 코드를 주면
번역되고(PortCallException(code, message, args...)), 상대의 상태와 본문을 남기려면
PortCallException(port, upstreamStatus, upstreamBody, message, cause). 설정으로 켜는 실패 정책은 아직 없습니다.
가짜 상대로 테스트하기
- 포트를 목으로 바꿉니다:
@MockitoBean CourierApiPort courier;(Spring Boot 3.4 이상, 이전은@MockBean). 성공, 오류 본문(null필드), 예외를 각각 돌려주게 해서 502 와 되돌림을 확인합니다. - 같은 서버 안의 가짜 컨트롤러(
/fake-courier)를 부르게 하려면 고정 포트로 띄웁니다(webEnvironment = DEFINED_PORT):httpPort: spring과RANDOM_PORT를 같이 쓰면 포트 0 을 부릅니다. - 바깥의 가짜 서버(MockWebServer, WireMock)는 그 주소를
{domain}.{Port}.url에 넣습니다.