Aspectran은 RESTful 웹 서비스를 구축할 때 응답의 일관성을 유지하고, 개발자가 HTTP 상태 코드, 헤더, 데이터 변환을 직관적이고 세밀하게 제어할 수 있도록 표준화된 응답 모델을 제공합니다.
이 가이드는 RestResponse와 그 구현체들을 활용하여 Aspectran의 표준 규격에 맞는 API를 설계하는 모든 방법을 상세히 다룹니다.
1. 핵심 개념 (Core Concepts)
Aspectran의 REST 응답 처리는 단순히 원시 데이터를 반환하는 것을 넘어, 응답의 성공 여부(success), 데이터 페이로드(data), 에러 정보(error)를 하나의 표준 규격으로 캡슐화하여 클라이언트와 일관되게 소통하는 것을 지향합니다.
1.1 응답 컨테이너: ResponsePayload
모든 표준 REST 응답(SuccessResponse, FailureResponse)은 내부적으로 com.aspectran.web.support.rest.response.ResponsePayload 객체에 담겨 전달됩니다. 이 객체는 클라이언트가 응답을 일관되게 처리할 수 있도록 다음과 같은 표준 구조를 가집니다.
- success: 작업의 성공 여부를 나타내는 불리언(
boolean) 값입니다. (필수 포함) - data: 작업 결과로 클라이언트에 전달할 실제 데이터 객체(
Object)입니다.- 성공 응답(
success: true):data가null이 아니면 직렬화에 포함되며,null인 경우data필드가 생략되어{"success": true}만 반환됩니다. - 실패 응답(
success: false): 데이터가 필요한 특수한 경우(예: 부분 성공 결과나 디버깅 메타데이터)에만 포함됩니다.
- 성공 응답(
- error: 작업이 실패했을 때 에러의 상세 정보를 담는
ErrorPayload객체입니다.code: 에러를 식별할 수 있는 고유 코드 (String, 필수)message: 에러에 대한 상세 설명 메시지 (String, 선택)
ResponsePayload.toEntity() 메서드는 불필요한 null 필드 직렬화를 방지하고 깔끔한 JSON/APON/XML 구조를 생성하도록 다음과 같은 조건부 맵 엔티티를 구성합니다.
| 응답 상황 | success | data 필드 | error 필드 | 생성되는 페이로드 예시 (JSON) |
|---|---|---|---|---|
| 성공 (데이터 있음) | true | 포함 (Object) | 생략 | {"success": true, "data": {...}} |
| 성공 (데이터 없음) | true | 생략 | 생략 | {"success": true} |
| 실패 (에러 코드/메시지) | false | 생략 (기본) | 포함 (ErrorPayload) | {"success": false, "error": {"code": "...", "message": "..."}} |
| 실패 (데이터+에러) | false | 포함 (Object) | 포함 (ErrorPayload) | {"success": false, "data": {...}, "error": {...}} |
1.2 민감 데이터 보호 메커니즘 (Data Protection)
RestResponse 및 ResponsePayload는 인증 토큰, 임시 비밀번호, 개인식별정보(PII), 금융 결제 데이터 등 민감한 정보가 서버 로그나 디버깅 문자열(toString())에 평문으로 남지 않도록 보장하는 데이터 보호 기능(protect())을 내장하고 있습니다.
- HTTP 직렬화와 문자열 표현의 역할 분리:
protect()가 활성화되더라도 실제 클라이언트에게 응답을 전송할 때 호출되는toEntity()는 원본 데이터를 손실 없이 온전하게 유지합니다. 즉, 클라이언트는 정상적인 비즈니스 데이터를 수신할 수 있습니다.- 반면, 로깅이나 디버깅 목적으로 호출되는
toString()에서는 데이터 필드가 자동으로data=[PROTECTED]로 마스킹됩니다.
- 상태 자동 전파 (Propagation):
SuccessResponse나FailureResponse에서.protect()를 호출하면, 응답 객체 내부의ResponsePayload에도 보호 설정이 즉시 동기화됩니다.- 보호 설정 이후에
setData()를 통해 데이터를 갱신하거나 교체하더라도 보호 상태가 그대로 유지됩니다.
1.3 표준 클래스 계층 구조
Aspectran의 REST 응답 체계는 유연한 확장성과 일관된 변환 로직을 위해 다음과 같은 계층 구조로 설계되어 있습니다.
com.aspectran.core.activity.response.transform.CustomTransformer (Interface)
└─ com.aspectran.web.activity.response.RestResponse (Interface)
└─ com.aspectran.web.activity.response.AbstractRestResponse (Abstract Class)
└─ com.aspectran.web.activity.response.DefaultRestResponse (Class)
├─ com.aspectran.web.support.rest.response.SuccessResponse (Class)
└─ com.aspectran.web.support.rest.response.FailureResponse (Class)
- RestResponse: RESTful HTTP 응답을 조작하기 위한 Fluent API 규격을 정의하는 핵심 인터페이스입니다.
CustomTransformer를 상속하여 Aspectran의 자체 변환(Self-Transformation) 메커니즘과 완벽하게 통합됩니다. - AbstractRestResponse: 상태 코드, 헤더 맵, 미디어 타입, 지능형 콘텐츠 협상 알고리즘(
determineAcceptContentType)의 공통 기능을 구현한 추상 클래스입니다. - DefaultRestResponse: JSON, APON, XML, Plain Text, HTML 형식으로의 직렬화 로직을 내장한 기본 구현체입니다.
ResponsePayload를 전달받으면toEntity()를 호출하여 표준 구조로 자동 변환합니다. - SuccessResponse: 2xx 상태 코드와 함께 성공적인 결과를 반환할 때 사용합니다. 내부적으로
ResponsePayload(true, data)를 생성하며, 기본 상태 코드는200 OK입니다. - FailureResponse: 4xx 또는 5xx 상태 코드와 함께 실패 원인을 반환할 때 사용합니다. 내부적으로
ResponsePayload(false)및ErrorPayload를 생성하며, 체이닝 메서드로 에러 코드와 메시지를 설정할 수 있습니다.
2. 응답 적용 방식 (How to Apply)
Aspectran에서 RestResponse를 사용하여 클라이언트에게 응답을 전달하는 방식은 크게 두 가지가 있습니다.
2.1 액션 메서드의 리턴값으로 사용 (추천)
가장 권장되는 방식으로, 액션 메서드의 리턴 타입을 RestResponse (또는 SuccessResponse, FailureResponse)로 지정합니다.
자동 변환 (Self-Transformation)
RestResponse는 CustomTransformer 인터페이스를 구현하고 있습니다. Aspectran 엔진은 액션의 결과값이 CustomTransformer 타입인 경우, 별도의 @Transform 어노테이션이나 XML의 <transform> 설정이 없더라도 객체 스스로가 정의한 변환 로직(transform(Activity))을 실행합니다. 즉, 응답 포맷 결정과 직렬화 로직을 프레임워크가 아닌 응답 객체에게 완전히 위임합니다.
@Request("/api/data")
// 별도의 @Transform이나 @Dispatch 설정이 필요 없음!
public RestResponse getData() {
return new SuccessResponse(dataService.findAll());
}
내부 동작 메커니즘 (Internal Mechanism)
Aspectran은 다음과 같은 2단계 과정을 통해 리턴된 RestResponse를 처리합니다:
- 어노테이션 파싱 시점 (Configuration Parsing):
- 컨텍스트 초기화 시
AnnotatedConfigParser가 액션 메서드의 반환 타입을 분석합니다. - 반환 타입이
CustomTransformer인터페이스의 하위 타입(RestResponse)인 경우, 해당 Translet의 응답 규칙으로CustomTransformResponse를 자동으로 등록합니다.
- 컨텍스트 초기화 시
- 런타임 응답 처리 시점 (Runtime Response Phase):
- 액션 메서드가 실행되어
RestResponse인스턴스를 반환하면, 이 객체는ProcessResult내의ActionResult에 저장됩니다. - 이어서
CustomTransformResponse.respond(Activity)가 실행될 때,findTransformer()메서드가 실행 결과 목록을 역순으로 탐색하여CustomTransformer인스턴스를 동적으로 획득합니다. - 최종적으로 획득한
RestResponse.transform(Activity)가 호출되어 지능형 콘텐츠 협상에 따른 실제 HTTP 응답 직렬화가 완료됩니다.
- 액션 메서드가 실행되어
2.2 Translet을 통한 명시적 변환
메서드의 리턴 타입이 void이거나, Aspect(Interceptor, Advice)와 같이 리턴값을 통해 응답을 직접 제어하기 어려운 환경에서는 translet.transform(RestResponse) 메서드를 호출합니다. 호출 즉시 해당 응답 객체를 사용하여 변환 및 응답 프로세스가 진행됩니다.
@Before
public void checkAuth(Translet translet) {
if (!isAuthenticated()) {
// 즉시 403 Forbidden 에러 응답으로 변환 프로세스 실행
translet.transform(new FailureResponse().forbidden().setError("FORBIDDEN", "인증이 필요합니다."));
}
}
2.3 응답 라이프사이클과 디버그 로깅 (Response Lifecycle & Debug Logging)
Aspectran은 일관된 요청 처리 흐름 추적을 위해 프레임워크 라이프사이클 단계별로 표준화된 디버그 로그를 제공합니다:
Translet {...}: 요청 준비 및 매핑 단계Advice {...}: AOP 인터셉터 및 어드바이스 실행 단계Action {...}: 비즈니스 액션 메서드 실행 단계Response {...}: 최종 응답 변환 및 전송 단계
프레임워크의 Response 메타데이터 로그
RestResponse를 통한 응답 처리 시, CustomTransformResponse는 다음과 같은 규칙 메타데이터를 디버그 로그로 출력합니다:
DEBUG [TOW task-2] Response {type=transform, format=custom, transformer=com.aspectran.web.support.rest.response.SuccessResponse} - c.a.c.a.r.t.CustomTransformResponse.respond(86)
프레임워크 코어는 응답 본문(페이로드) 자체를 로그에 출력하지 않고, 어떤 응답 규칙과 Transformer 클래스가 실행되었는가에 대한 메타데이터만 안전하게 기록합니다. 이는 다음과 같은 운영상의 중요 이유 때문입니다:
- 보안 및 개인정보 보호 (PII): 로그인 토큰, 비밀번호, 개인 식별 정보 등 민감한 데이터가 로그 파일에 평문으로 영구 저장되는 것을 방지합니다.
- 성능 및 I/O 최적화: 대용량 목록 조회나 바이너리 데이터 응답 시 발생하는 대규모 문자열 생성(GC 부하)과 디스크 I/O 병목을 차단합니다.
- 로그 가독성 (Noise 방지): 거대한 응답 데이터가 전체 로그를 뒤덮어 정작 중요한 장애 원인이나 에러 추적 로그를 가리는 현상을 방지합니다.
애플리케이션 레벨의 응답 본문 로깅 패턴
특정 API를 개발하거나 디버깅하는 과정에서 응답 본문 데이터를 로그로 확인해야 할 경우, 비즈니스 액션 메서드 내에서 직접 출력하거나 공통 Aspect를 활용할 수 있습니다.
이때 프레임워크의 라이프사이클 로그(Response {...})와의 혼선을 방지하기 위해, 입력 파라미터 로그(Parameters {...})와 짝을 이루는 Result 프리픽스를 사용하는 것을 권장합니다:
@Request("/sample/api/v1/countingNumbers")
public RestResponse countingNumbers(Translet translet) {
if (logger.isDebugEnabled()) {
logger.debug("Parameters {}", translet.getAllParameters());
}
// 비즈니스 로직 수행
Map<String, Object> data = Map.of("comment", "숫자 세기", "list", List.of(1, 2, 3));
RestResponse response = new SuccessResponse(data);
if (logger.isDebugEnabled()) {
// 프레임워크의 "Response" 로그와 명확히 구분되는 "Result" 프리픽스 사용
logger.debug("Result {}", response);
}
return response;
}
이 패턴을 적용하면 로그 스트림에서 요청의 흐름과 입력, 결과 데이터가 완벽하게 일목요연해집니다:
DEBUG ... Action {method=app.apigateway.server.api.sample.SampleActivity.countingNumbers(Translet)} - c.a.core.activity.CoreActivity.execute(654)
DEBUG ... Parameters {start=1, end=3} - a.a.s.a.sample.SampleActivity.countingNumbers(40)
DEBUG ... Result {success=true, data={comment=숫자 세기, list=[1, 2, 3]}} - a.a.s.a.sample.SampleActivity.countingNumbers(77)
DEBUG ... Response {type=transform, format=custom, transformer=com.aspectran.web.support.rest.response.SuccessResponse} - c.a.c.a.r.t.CustomTransformResponse.respond(86)
또한 여러 API에 대해 공통으로 응답 본문을 로깅해야 하는 경우에는 ApiCommonAspect와 같은 공통 Aspect에서 @After 어드바이스를 통해 translet.getProcessResult()를 조회하여 선택적(특정 URL 패턴 또는 로깅 어노테이션 기반)으로 처리할 수 있습니다.
3. 지원되는 컨텐츠 타입 (Supported Content Types)
Aspectran의 DefaultRestResponse는 별도의 서드파티 라이브러리 설정 없이도 클라이언트의 요구에 맞춰 데이터를 다양한 미디어 타입으로 자동 변환(Transformation)합니다.
| 미디어 타입 (Media Type) | 확장자 (Extension) | 설명 |
|---|---|---|
application/json | .json | 가장 널리 쓰이는 표준 JSON 형식 (JsonWriter, JSONP callback 지원) |
application/apon | .apon | Aspectran 고유의 객체 표기법 (APON, 간결한 텍스트 기반 객체 포맷) |
application/xml | .xml | 표준 XML 형식 (XmlTransformResponse, name 지정 시 루트 태그 적용) |
text/plain | .txt | 데이터의 문자열 표현(ToString) 출력 |
text/html | .html, .htm | HTML 형식의 문자열 출력 |
컨텐츠 결정 우선순위 (지능형 콘텐츠 협상 알고리즘)
AbstractRestResponse.determineAcceptContentType(Activity)는 다음과 같은 4단계 우선순위로 최적의 미디어 타입을 결정합니다.
- URL 경로 확장자 (
favorPathExtension):- 요청 경로 끝에
.json,.xml,.apon,.txt,.html,.htm등이 명시되어 있으면 해당 미디어 타입을 최우선으로 선택합니다. (기본값:true) - 알 수 없는 확장자가 요청되고
ignoreUnknownPathExtensions가false인 경우406 Not Acceptable에러를 발생시킵니다.
- 요청 경로 끝에
- HTTP Accept 헤더 (
ignoreAcceptHeader):- 확장자가 없거나 확장자로 타입을 결정하지 못한 경우, 클라이언트가 전송한
Accept헤더의 우선순위(Quality Value 등)를 분석하여 서버가 지원하는 미디어 타입 목록(supportedContentTypes)과 매칭합니다.
- 확장자가 없거나 확장자로 타입을 결정하지 못한 경우, 클라이언트가 전송한
- 기본 미디어 타입 (
defaultContentType):Accept헤더가*/*이거나 매칭되는 미디어 타입이 없을 때, 인스턴스에 설정된defaultContentType또는 Translet 설정 키response.defaultContentType(RestResponse.RESPONSE_DEFAULT_CONTENT_TYPE)에 정의된 기본값을 사용합니다.
- 406 Not Acceptable:
- 위 모든 단계에서 적합한 미디어 타입을 찾지 못한 경우, HTTP 상태 코드
406 Not Acceptable을 반환합니다.
- 위 모든 단계에서 적합한 미디어 타입을 찾지 못한 경우, HTTP 상태 코드
4. HTTP 응답 상태 코드 제어 (Fluent API)
AbstractRestResponse는 HTTP 상태 코드를 직관적으로 설정할 수 있는 다양한 메서드를 제공합니다. 이 메서드들은 메서드 체이닝(Method Chaining)을 지원하여 코드를 간결하고 가독성 높게 작성할 수 있도록 돕습니다.
4.1 성공 응답 (2xx Success)
ok(): 200 OK. 요청이 성공적으로 처리되었음을 나타냅니다. (기본값)created(): 201 Created. 새 리소스가 성공적으로 생성되었음을 나타냅니다.created(String location): 201 Created. 생성된 리소스의 URI를Location응답 헤더에 자동으로 추가합니다.accepted(): 202 Accepted. 요청이 접수되었으나 처리가 아직 완료되지 않았음을 나타냅니다.noContent(): 204 No Content. 성공했으나 응답 바디에 보낼 데이터가 없음을 나타냅니다.
4.2 리다이렉션 (3xx Redirection)
movedPermanently(): 301 Moved Permanently. 리소스의 URI가 영구적으로 변경되었음을 나타냅니다.seeOther(): 303 See Other. 요청한 리소스를 다른 URI에서 찾아야 함을 나타냅니다.notModified(): 304 Not Modified. 캐시된 리소스가 변경되지 않았음을 나타냅니다.temporaryRedirect(): 307 Temporary Redirect. 임시 리다이렉션을 나타냅니다.
4.3 클라이언트 오류 (4xx Client Errors)
badRequest(): 400 Bad Request. 잘못된 파라미터나 요청 구문 오류를 나타냅니다.unauthorized(): 401 Unauthorized. 대상 리소스에 대한 유효한 인증 자격 증명이 없음을 나타냅니다.forbidden(): 403 Forbidden. 서버가 요청을 이해했으나 승인을 거부했음을 나타냅니다. (권한 부족 등)notFound(): 404 Not Found. 서버가 요청받은 리소스를 찾을 수 없음을 나타냅니다.methodNotAllowed(): 405 Method Not Allowed. 요청에 사용된 HTTP 메서드가 리소스에서 허용되지 않음을 나타냅니다.notAcceptable(): 406 Not Acceptable. 요청의 Accept 헤더에 맞는 응답 콘텐츠를 생성할 수 없음을 나타냅니다.conflict(): 409 Conflict. 서버의 현재 상태와 요청이 충돌했음을 나타냅니다.preconditionFailed(): 412 Precondition Failed. 대상 리소스에 대한 접근 사전 조건이 실패했음을 나타냅니다.unsupportedMediaType(): 415 Unsupported Media Type. 요청 페이로드의 미디어 타입이 지원되지 않음을 나타냅니다.
4.4 서버 오류 (5xx Server Errors)
internalServerError(): 500 Internal Server Error. 서버 내부에서 예기치 못한 오류가 발생하여 요청을 수행할 수 없음을 나타냅니다.
4.5 일반 상태 코드 설정 메서드
setStatus(int status): 정수형 HTTP 상태 코드를 직접 지정합니다.setStatus(HttpStatus status):com.aspectran.web.support.http.HttpStatus열거형 상수를 지정합니다.getStatus(): 현재 설정된 HTTP 상태 코드 정수값을 반환합니다.
5. 데이터, 헤더 및 직렬화 설정 메서드
상태 코드 외에도 응답의 세부적인 데이터와 직렬화 방식을 정밀하게 제어할 수 있는 메서드들이 제공됩니다.
setData(Object data): 응답 페이로드의data필드에 들어갈 객체를 명시적으로 설정합니다.setData(String name, Object data): 데이터에 이름을 부여하여 루트 키로 사용합니다. 특히 XML로 변환할 때 최상위 엘리먼트 이름을 지정하거나, JSON 변환 시 루트 객체 감싸기에 유용합니다.setHeader(String name, String value): 특정 헤더 값을 설정합니다. 이미 존재하는 헤더라면 값을 덮어씁니다.addHeader(String name, String value): 특정 헤더에 값을 추가합니다. 동일한 이름의 헤더에 복수 개의 값을 추가할 때 유용합니다.prettyPrint(boolean prettyPrint): 응답 데이터(JSON, APON, XML)를 줄바꿈과 들여쓰기를 포함하여 읽기 좋은 형태로 포맷팅할지 여부를 설정합니다.nullWritable(boolean nullWritable): 데이터 객체를 직렬화할 때null값을 가진 필드도 출력 결과에 포함할지 여부를 설정합니다. (기본값:false)stringifyContext(StringifyContext stringifyContext): 직렬화 세부 옵션을 담고 있는StringifyContext를 직접 지정합니다.defaultContentType(MediaType defaultContentType): 인스턴스별 기본 미디어 타입을 동적으로 지정합니다.favorPathExtension(boolean)/ignoreAcceptHeader(boolean)/ignoreUnknownPathExtensions(boolean): 콘텐츠 협상 옵션을 인스턴스별로 재정의합니다.protect(): 응답 데이터(본문)를 로깅 및 문자열 표현(toString())에서 안전하게 보호합니다. 클라이언트에 전송되는 HTTP 본문은 정상 유지되지만, 로그에는data=[PROTECTED]로 마스킹되어 평문 노출을 방지합니다.protect(boolean protect): 응답 데이터의 보호 여부를 명시적으로 설정합니다.isProtected(): 현재 응답 데이터가 보호 설정되어 있는지 여부를 불리언(boolean)으로 반환합니다.
6. 실무 적용 예제 (Practical Examples)
Aspectran의 URL 패턴 매핑 구문인 ${...}과 표준 응답 클래스를 활용한 실무 예제입니다.
예제 1: 기본적인 성공 응답 (200 OK)
조회된 엔티티나 DTO를 클라이언트에게 반환하는 가장 일반적인 케이스입니다.
@Request("/api/user/${userId}")
public RestResponse getUser(String userId) {
User user = userService.getUserById(userId);
if (user == null) {
return new FailureResponse()
.notFound()
.setError("USER_NOT_FOUND", "사용자를 찾을 수 없습니다: " + userId);
}
// 200 OK 상태 코드와 함께 데이터를 표준 ResponsePayload로 캡슐화하여 반환
return new SuccessResponse(user);
}
응답 데이터 샘플 (JSON):
{
"success": true,
"data": {
"userId": "user-01",
"username": "aspectran",
"email": "user@aspectran.com"
}
}
응답 데이터 샘플 (APON - /api/user/user-01.apon 요청 시):
success: true
data: {
userId: user-01
username: aspectran
email: user@aspectran.com
}
응답 데이터 샘플 (XML - /api/user/user-01.xml 요청 시):
<?xml version="1.0" encoding="UTF-8"?>
<response>
<success>true</success>
<data>
<userId>user-01</userId>
<username>aspectran</username>
<email>user@aspectran.com</email>
</data>
</response>
예제 2: 리소스 생성 및 Location 헤더 (201 Created)
새로운 데이터를 생성한 후, 클라이언트에게 생성된 리소스의 접근 URI를 Location 헤더와 함께 전달하는 케이스입니다.
@RequestToPost("/api/orders")
public RestResponse createOrder(Order order) {
orderService.placeOrder(order);
// 201 Created 상태 코드를 설정하고 Location 헤더를 자동으로 추가
return new SuccessResponse(order)
.created("/api/orders/" + order.getId());
}
응답 헤더 및 바디 샘플 (HTTP):
HTTP/1.1 201 Created
Location: /api/orders/ORD-1004
Content-Type: application/json;charset=UTF-8
{
"success": true,
"data": {
"orderId": "ORD-1004",
"item": "Aspectran Pro License",
"status": "COMPLETED"
}
}
예제 3: 유효성 검사 실패 및 에러 처리 (400 Bad Request)
클라이언트 입력값이 유효하지 않을 때 명확한 에러 코드와 사용자 친화적인 메시지를 함께 반환합니다.
@RequestToPost("/api/register")
public RestResponse register(User user) {
if (StringUtils.isEmpty(user.getEmail())) {
return new FailureResponse()
.badRequest() // HTTP 400 상태 코드 지정
.setError("REQUIRED_FIELD", "이메일은 필수 항목입니다.");
}
userService.register(user);
return new SuccessResponse("Registered successfully");
}
응답 데이터 샘플 (JSON - 유효성 검사 실패 시):
{
"success": false,
"error": {
"code": "REQUIRED_FIELD",
"message": "이메일은 필수 항목입니다."
}
}
예제 4: 권한 인증 및 접근 제어 (401 Unauthorized, 403 Forbidden)
API 엔드포인트에 대한 접근 권한을 검사하고 실패 시 사유를 알립니다.
@Request("/api/admin/settings")
public RestResponse getAdminSettings(UserInfo userInfo) {
if (userInfo == null) {
// 로그인되지 않은 사용자 (401 Unauthorized)
return new FailureResponse()
.unauthorized()
.setError("UNAUTHORIZED", "로그인이 필요합니다.");
}
if (!userInfo.hasRole("ADMIN")) {
// 관리자 권한이 없는 사용자 (403 Forbidden)
return new FailureResponse()
.forbidden()
.setError("ACCESS_DENIED", "관리자 권한이 필요합니다.");
}
return new SuccessResponse(configService.getAdminConfig());
}
응답 데이터 샘플 (JSON - 권한 부족 시):
{
"success": false,
"error": {
"code": "ACCESS_DENIED",
"message": "관리자 권한이 필요합니다."
}
}
예제 5: 동적 요청 바디 처리 (APON Parameters 활용)
클라이언트가 전송한 JSON 요청 바디를 Aspectran의 com.aspectran.utils.apon.Parameters 객체로 직접 주입받아 처리하는 케이스입니다. 별도의 DTO 클래스를 일일이 정의하지 않고도 가변적이거나 동적인 요청 페이로드를 유연하게 다룰 수 있으며, 필수 필드 검증과 응답 생성을 간결하게 구현할 수 있습니다.
@RequestToPost("/api/todos")
public RestResponse createTodo(Parameters parameters) {
String title = parameters.getString("title");
if (StringUtils.isEmpty(title)) {
return new FailureResponse()
.badRequest()
.setError("INVALID_TITLE", "할 일 제목을 입력해야 합니다.");
}
Todo todo = new Todo();
todo.setTitle(title);
todo.setCompleted(parameters.getBoolean("completed", false));
todo.setOrder(parameters.getInt("order", 0));
todoService.addTodo(todo);
return new SuccessResponse(todo)
.created("/api/todos/" + todo.getId());
}
요청 데이터 샘플 (JSON Body):
{
"title": "Aspectran 문서 작성",
"completed": false,
"order": 1
}
응답 데이터 샘플 (JSON - 201 Created):
{
"success": true,
"data": {
"id": 101,
"title": "Aspectran 문서 작성",
"completed": false,
"order": 1
}
}
예제 6: 응답 포맷팅 및 커스텀 헤더 제어
클라이언트나 개발자가 디버깅하기 쉽도록 데이터를 정렬 출력하고 캐시 방지 및 커스텀 메타데이터 헤더를 추가합니다.
@Request("/api/system/stats")
public RestResponse getSystemStats() {
Map<String, Object> stats = statsService.getGlobalStats();
return new SuccessResponse(stats)
.prettyPrint(true) // 줄바꿈과 들여쓰기를 포함한 가독성 높은 출력
.nullWritable(false) // null 값 필드 생략
.setHeader("Cache-Control", "no-cache, no-store, must-revalidate")
.addHeader("X-Node-Id", "node-01")
.addHeader("X-Response-Time", String.valueOf(System.currentTimeMillis()));
}
예제 7: 민감 데이터 보호와 안전한 로깅 (protect() 활용)
인증 토큰, 임시 비밀번호, 금융 결제 정보 등 보안상 민감한 데이터를 클라이언트에게 반환해야 하지만, 서버 내부의 공통 Aspect나 디버그 로그에는 해당 데이터가 평문으로 남지 않아야 하는 경우 protect() 메서드를 체이닝합니다.
@RequestToPost("/api/auth/token")
public RestResponse issueToken(AuthRequest authRequest) {
TokenInfo tokenInfo = authService.generateToken(authRequest);
// .protect()를 체이닝하여 로깅 시 데이터 마스킹 활성화
RestResponse response = new SuccessResponse(tokenInfo).protect();
if (logger.isDebugEnabled()) {
// response.toString() 호출 시 data=[PROTECTED]로 안전하게 출력됨
logger.debug("Result {}", response);
}
return response;
}
클라이언트가 수신하는 실제 HTTP 응답 (JSON Body): 클라이언트는 toEntity()를 통해 정상적으로 직렬화된 실제 비즈니스 데이터를 수신합니다.
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.t-IDcBQ...",
"tokenType": "Bearer",
"expiresIn": 3600
}
}
서버 로그 스트림 (Debug Log): 서버 내부의 액션 로그 또는 공통 Aspect 로거에서는 데이터 필드가 안전하게 [PROTECTED]로 마스킹되어 평문 노출이 원천 차단됩니다.
DEBUG ... Result {success=true, data=[PROTECTED]} - a.a.s.a.auth.AuthActivity.issueToken(52)
DEBUG ... Response {type=transform, format=custom, transformer=com.aspectran.web.support.rest.response.SuccessResponse} - c.a.c.a.r.t.CustomTransformResponse.respond(86)
예제 8: 백엔드 API Relay (RestRequest 활용)
RestRequest 클래스를 사용하여 외부 마이크로서비스나 내부 클러스터 노드의 REST API를 호출하고, 그 결과를 가공 없이 현재 서버의 응답으로 바로 중계(Relay)하는 패턴입니다. RestRequest.retrieve()는 원격 응답 상태에 따라 이미 SuccessResponse 또는 FailureResponse를 구성하여 반환하므로 완벽한 대칭성을 가집니다.
@Request("/relay/weather")
public RestResponse relayWeatherRequest(CloseableHttpClient httpClient) {
String targetUrl = "https://internal-api.example.com/weather/today";
try {
RestRequest restRequest = new RestRequest(httpClient);
// RestRequest.retrieve() 호출 결과는 이미 SuccessResponse 또는 FailureResponse 형태임
RestResponse response = restRequest.get()
.url(targetUrl)
.retrieve();
// 반환받은 표준 응답 객체를 Translet의 응답으로 그대로 전달
return response;
} catch (IOException e) {
return new FailureResponse()
.internalServerError()
.setError("RELAY_ERROR", "대상 원격 서버와 통신 중 오류가 발생했습니다: " + e.getMessage());
}
}
예제 9: 전역 예외 처리(Exception Handling / Aspect)에서의 일관된 응답 변환
애플리케이션 전역에서 발생하는 예외를 Aspect나 Exception Rule에서 가로채어 표준 FailureResponse로 변환하면, 클라이언트에게 항상 일관된 형식의 에러 응답을 보장할 수 있습니다.
@Component
@Aspect
public class GlobalRestExceptionAdvice {
private static final Logger logger = LoggerFactory.getLogger(GlobalRestExceptionAdvice.class);
@AfterThrown(target = "activity", thrown = "java.lang.IllegalArgumentException")
public void handleIllegalArgument(Translet translet, Exception ex) {
logger.warn("Invalid client argument: {}", ex.getMessage());
RestResponse response = new FailureResponse()
.badRequest()
.setError("INVALID_PARAMETER", ex.getMessage());
translet.transform(response);
}
@AfterThrown(target = "activity", thrown = "java.lang.Exception")
public void handleGeneralException(Translet translet, Exception ex) {
logger.error("Unhandled server exception occurred", ex);
RestResponse response = new FailureResponse()
.internalServerError()
.setError("INTERNAL_SERVER_ERROR", "서버 내부 오류가 발생했습니다.");
translet.transform(response);
}
}
7. 모범 사례 및 권장 사항 (Best Practices)
- Map 대신 표준 클래스 사용: 응답 바디를 구성하기 위해
Map.of("success", true, "data", ...)를 임의로 작성하거나 DTO를 직접 만들어success필드를 채우는 방식은 피해야 합니다. 항상SuccessResponse와FailureResponse를 사용하여ResponsePayload표준 규격을 준수하십시오. - 데이터가 없는 성공 응답의 간결성: 삭제(Delete)나 단순 상태 변경 요청에서 응답 데이터가 필요 없을 때는
new SuccessResponse()또는new SuccessResponse().noContent()를 반환하여 클라이언트에게 불필요한 페이로드 전송을 최소화하십시오. - 일관된 에러 코드 체계 구축:
FailureResponse에서 사용하는 에러 코드(ErrorPayload.code)는 프로젝트 전반에서 식별 가능하고 고유하도록 상수(Enum 또는 Static Constants)로 관리하는 것이 좋습니다. - 프론트엔드 통신 규약 일치: Aspectran 표준 응답을 수신하는 웹 프론트엔드나 클라이언트는 응답의 최상위
success불리언 필드를 검사(if (res.success) { ... })하여 성공 여부를 분기하고, 실패 시res.error.code와res.error.message를 활용하도록 설계하십시오. - 콘텐츠 협상 활용: API 설계 시 클라이언트가
.json,.apon,.xml등의 확장자를 붙여 요청하면 Aspectran이 해당 포맷으로 즉시 변환하므로, 동일한 비즈니스 로직으로 다중 포맷 API를 손쉽게 제공할 수 있습니다. - 안전한 응답 로깅과 라이프사이클 로그의 분리: 프레임워크가 출력하는
Response로그는 응답 처리 규칙의 메타데이터({type=transform, format=custom, transformer=...})만을 안전하게 기록합니다. 애플리케이션이나 API 디버깅을 위해 응답 페이로드를 직접 로깅할 때는Result프리픽스를 사용하여 프레임워크 로그와의 혼선을 방지하고, 개인정보(PII)나 인증 토큰 등 민감한 데이터가 로그에 평문으로 남지 않도록 주의하십시오. - 민감 데이터 응답의
protect()적용: 인증 토큰, 일회용 비밀번호(OTP), 주민등록번호, 계좌 정보 등을 반환하는 API는 액션 메서드에서new SuccessResponse(data).protect()와 같이 보호 설정을 활성화하십시오. 공통 로깅 Aspect나 비즈니스 로그(logger.debug("Result {}", response))에서 응답 객체를 출력하더라도 데이터 본문이 자동으로[PROTECTED]로 마스킹되므로, 개발 및 운영 로깅의 편의성과 개인정보 보호를 완벽하게 양립할 수 있습니다.