개발 일기

[사이드 프로젝트] 일관된 응답 구조 설계

Pleasant Pain 2025. 4. 2. 15:51

개요

이번 글은 "응답 구조 설계"에 대한 이야기다.

사이드 프로젝트에서 약관 도메인의 CRUD API를 구현하면서,  
수차례 API를 디버깅할 때마다 응답 형식이 제각각이라는 찜찜함을 느꼈다.

 

"내가 예상치 못한 예외가 발생했을 때 보안에 치명적인 응답을 하면 어떡하지?"

"응답에 있어 성공과 예외 발생을 무엇으로 구분하지?"

 

이건 단순히 보기 불편한 문제가 아니다.  

프론트엔드와 협업
,  

에러 상황의 예측,  
API 명세의 신뢰성까지 흔들릴 수 있는 문제였다.

그래서 생각했다.
"API는 항상 같은 방식으로 말해야 한다."

이번 글에서는  
스프링 환경에서 어떻게 일관된 응답 구조를 설계했고,  
왜 그게 중요한 선택이었는지를 정리해보려 한다.

 

 

 

목표

일관된 응답 구조 설계를 고민하게 된 첫 번째 이유는  
"예상치 못한 API 응답"이 시스템을 망칠 수도 있다는 위기감이었다.

예를 들어, 어플리케이션 운영 중에 누군가 DB를 잘못 조작해서  
의도치 않은 예외가 발생한다면?

그때 서버가 스택 트레이스를 그대로 노출하거나,  
보안에 취약한 정보까지 응답에 담는다면?

그건 단순한 오류가 아니라,  
서비스 전체를 위험에 빠뜨릴 수 있는 심각한 설계 실패다.

그래서 나는 생각했다.  
"정상이든 비정상이든, 응답은 항상 내가 설계한 틀 안에서 이뤄져야 한다."

이런 위협 요소를 걷어낸 후,  
다음으로 중요하게 생각한 건 협업과 디버깅의 편의성이었다.

예를 들어,  
"어떤 API는 아무 데이터도 응답하지 않는데 이게 성공인지 실패인지 알 수 없어." 
"어떤 API는 result 필드가 있고, 어떤 API는 data 필드만 있어. 헷갈려."

이런 상황은 협업자나 프론트엔드 개발자 입장에서 API를 신뢰할 수 없게 만든다.

그래서 나는 "항상 동일한 구조"로 응답을 설계하려 했다.

성공이면 success = true.  
실패면 success = false.  
결과 데이터는 항상 data 필드에 담고,  
에러 메시지는 항상 message에 담는다.

이렇게 명확하게 공식처럼 정해두면,  

  • 프론트 개발자도 응답 구조를 예측할 수 있고 
  • 디버깅도 빨라지고 
  • API 명세도 단순해진다.

이번 글을 통해  
나는 협업과 안정성을 모두 고려한 응답 구조 설계 방식을 정리해두고,  
앞으로 모든 프로젝트에 "기본 응답 구조의 원칙"을 세우고자 한다.

 

 

에러 응답 구조 설계

목표 챕터에서 얘기한 것과 같이

API 응답 과정에서 내가 의도하지 않은 에러가 발생했을 때,
안전성을 확보할 수 있는 설계가 필요했다.

 

따라서 먼저 아래와 같이 에러 응답 구조를 설계했다.

public record ApiErrorResponse(
	boolean success,
	String code,
	String message,
	Object dataOrNull // or null, or errors 리스트
) {
	public static ApiErrorResponse of(String code, String message) {
		return new ApiErrorResponse(false, code, message, null);
	}

	public static ApiErrorResponse of(String code, String message, Object data) {
		return new ApiErrorResponse(false, code, message, data);
	}
}

 

 

데이터를 담기 위해 존재한다고 판단하여 record로 설계했으며,
각 필드가 뜻하는 바는 다음과 같다.

  • success: API 응답이 성공된 케이스인지 실패된 케이스인지 구분하기 위함.
  • code: 어떤 종료의 에러가 발생된 것인지 구분하기 위함.
  • message: 에러가 발생된 구체적인 이유를 명시하기 위함.
  • data: 에러를 뜻하는 데이터가 다수 일 때 활용하기 위함으로 null을 허용하는 필드임을 명시했다.

 

이제 준비된 에러 응답 구조를 적용하기만 하면 된다.


그럼 이제 예외를 어디서 캐치해야할까?

API를 요청 받는 모든 컨트롤러에서 try-catch를 해야할까?


Spring에서는 이 상황에서 강력한 기능을 제공하고 있다

 

@RestControllerAdvice, @ExceptionHandler 활용

Spring은 Bean으로 등록된 모든 Controller 내부에서 발생한 예외를
한 곳에서 처리할 수 있게 도와주는 강력한 어노테이션을 제공한다


바로 @RestControllerAdvice다 
적용한 예시를 먼저 확인해보자

@RestControllerAdvice
public class GlobalExceptionHandler {
    private static final String INTERNAL_ERROR_CODE = "INTERNAL_SERVER_ERROR";
    private static final String INTERNAL_ERROR_MESSAGE = "서버 내부 오류로 인한 작업 실패";

    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiErrorResponse> handleUnexpectedException(Exception exception) {
        ApiErrorResponse response = ApiErrorResponse.of(INTERNAL_ERROR_CODE, INTERNAL_ERROR_MESSAGE);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
    }
}

 

여기서 만약 다른 예외를 구분해서 응답을 표현하고 싶다면?
예를 들어 Request Body에서 @Valid를 활용한 유효성 검증 에러를 캐치하고자 한다면?
이러한 요구를 처리해줄 또 다른 강력한 어노테이션이 바로 @ExceptionHandler다


바로 예시로 확인해보자

@RestControllerAdvice
public class GlobalExceptionHandler {
    private static final String INTERNAL_ERROR_CODE = "INTERNAL_SERVER_ERROR";
    private static final String INTERNAL_ERROR_MESSAGE = "서버 내부 오류로 인한 작업 실패";

    private static final String VALIDATION_ERROR_CODE = "VALIDATION_ERROR";


    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiErrorResponse> handleValidationException(Exception exception) {
        List<FieldError> fieldErrors = methodArgumentNotValidException.getBindingResult().getFieldErrors();
		String message = fieldErrors.stream()
		    .map(error -> String.format("[%s] %s", error.getField(), error.getDefaultMessage()))
		    .collect(Collectors.joining(" | "));

        ApiErrorResponse response = ApiErrorResponse.of(VALIDATION_ERROR_CODE, message);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiErrorResponse> handleUnexpectedException(Exception exception) {
        ApiErrorResponse response = ApiErrorResponse.of(INTERNAL_ERROR_CODE, INTERNAL_ERROR_MESSAGE);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
    }
}

 

여기서 주의할 점은 메서드 선언 순서다

예외에 대해 학습한 사람이라면 Exception의 추상화 레벨을 잘 알고 있을 것이라 판단한다

 

스프링은 가장 구체적인 예외 타입에 먼저 매칭되기 때문에, 추

상적인 Exception보다 세부 예외를 먼저 처리할 수 있도록 위에 선언해야 한다.

 

커스텀 예외는 Enum 활용

위에서 정리한 바와 같이 내가 예기치 못한 에러가 발생했을 때도,
일관된 에러가 응답되도록 안전 장치를 만들 수 있었다


그러나 스스로 의도한 에러는 어떻게 처리할 것인가?

여기서 의도한 에러란
API 요청 처리 과정 중 올바른 응답이 불가능할 것이라 판단하고,
의도적으로 예외를 발생시켜 사용자에게 요청이 실패함을 알리는 것이다

 

그러나 의도적으로 발생시킨 예외마저 시스템 내부 오류처럼 처리되면, 

의도와 전혀 다른 응답을 사용자에게 전달하게 된다.

그렇기에 의도한 에러에 대한 응답을 어떻게 관리할지도 매우 중요한 부분이다

사실 이 부분이 공통된 에러 응답 구조 설계의 핵심 부분이다

우선 의도적으로 에러를 발생 시키기 위해,
커스텀 예외를 만들 필요가 있다 판단했다.

 

처음 설계한 커스텀 예외는 다음과 같다.

public class BusinessException extends RuntimeException {
    private final String code;
    private final HttpStatus status;
    private final String message;

    public BusinessException(String code, HttpStatus status, String message) {
        super(message);
        this.code = code;
        this.status = status;
        this.message = message;
    }
}

 

ApiErrorResponse와 거의 구조가 같다.

의도적으로 발생한 에러를 뜻하기 위해 BusinessException이라 명명했다.

 

여기서 2가지 선택지가 존재했다.

 

첫 번째는 BusinessException을 상속 받아 여러 커스텀 예외를 추가하는 것이다.

 

WHY?

코드 재사용성도 있지만 무엇보다 ExceptionHandler에 의해 의도적인 예외가 캐치되기 위해서
BusinessException을 상속 받아 추상화를 활용할 필요가 있다.

 

예를 들면 아래와 같다.

public class CustomBadRequestException extends BusinessException {
    public CustomBadRequestException(String message) {
        super("BAD_REQUEST", HttpStatus.BAD_REQUEST, message);
    }
}

 

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ApiErrorResponse> handleUnexpectedException(BusinessException businessException) {
        ApiErrorResponse response = ApiErrorResponse.of(businessException.getCode(), businessException.getMessage());
        return ResponseEntity.status(BusinessException.getStatus()).body(response);
    }
}

 

그러나 이때, 과연 여러 커스텀 예외를 생성하는 것이 얼마나 실용성이 있을지 고려하게 되었다.

상수 같이 내부 필드의 설정 값이 이미 정해져 있을 뿐,

BusinessException과 완벽하게 동일한 기능을 수행한다.

 

BusinessException과 다른 것은 CustomBadRequestException이라는 클래스 명 뿐이다.

 

하지만 현재 의도된 예외 처리 구조에서 클래스명이 다르다는게 얼마나 이득이 있을까?

누군가 예외 클래스명을 다르게 명명함으로서 예외가 발생했을 때 디버깅이 더 빨라진다고들한다.

 

하지만 과연 예외 클래스명만 보고도 정말 디버깅이 쉬워졌었나?

예외 클래스명을 보고 어떤 종류의 예외이구나 파악하는 것은 인정하지만,
필자의 경험상 예외가 발생되면 결국 에러 메시지와 내부의 함수 호출 스택 트레이스 파악이 주요한 디버깅 과정이었다.


즉, 여러 커스텀 예외 클래스를 규정짓는 이유로

클래스명을 보고 디버깅이 쉬워진다는 것에 필자는 동의할 수 없었다.

 

현재 필자의 시각에선 예외 클래스명을 다르게 규정한다면 가장 주된 이유는 다음과 같아야한다.

 

각 구체적인 예외 클래스명마다 ExceptionHandler에서 다른 처리가 필요할 때.

 

위 GlobalExceptionHandler의 예시와 같이,
MethodArgumentNotValidException 예외에 대한 처리와

의도하지 못한 추상화된 Exception에 대한 처리는 달라야했다.

 

이렇게 예외에 따라 다르게 처리하는 과정이 필요할 때는 타입으로 구분지어 캐치할 수 밖에 없다.
즉, 최소한 이러한 이유가 충족되어야 여러 커스텀 예외를 만드는 선택이 합리적이라 할 수 있다.

 

하지만 BusinessException 구조상,
BusinessException을 상속 받는 커스텀 예외들은
ExceptionHandler에 의해 다르게 처리할 부분이 없다.

 

그렇기 때문에 여기서 두 번째 선택지를 고려하게 된다

바로 Enum 활용이다.

 

BusinessException를 구상하는 필드 값들은 message를 제외하면,
예외 케이스에 따른 정적인 데이터로 볼 수 있었다.

 

그리고 이러한 정적인 데이터를 하나의 묶음으로 관리하는 것에

enum 만큼 탁월한 자료구조가 있을까?


여기서 필자는 에러 케이스에 따라 에러 코드를 규정하고자했기에,
다음과 같은 ErrorCode enum을 설계하였다.

public enum ErrorCode {
    VALIDATION_ERROR(HttpStatus.BAD_REQUEST, "유효성 검증에 실패했거나, 요청 값이 잘못되었습니다."),
    BAD_REQUEST(HttpStatus.BAD_REQUEST, "유효성 검증에 실패했거나, 요청 값이 잘못되었습니다."),
    UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증 정보가 없거나, 인증에 실패했습니다."),
    FORBIDDEN(HttpStatus.FORBIDDEN, "접근 권한이 없습니다."),
    NOT_FOUND(HttpStatus.NOT_FOUND, "요청한 리소스를 찾을 수 없습니다."),
    CONFLICT(HttpStatus.CONFLICT, "이미 존재하거나, 처리된 요청입니다."),
    INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버에 문제가 발생했습니다. 잠시 후 다시 시도해주세요.");
    
    private final HttpStatus httpStatus;
    private final String defaultMessage;
    
    ErrorCode(HttpStatus httpStatus, String defaultMessage) {
        this.httpStatus = httpStatus;
        this.defaultMessage = defaultMessage;
    }

    public HttpStatus status() {
        return httpStatus;
    }

    public String message() {
        return defaultMessage;
    }

    public BusinessException exception() {
        return new BusinessException(this, this.defaultMessage);
    }

    public BusinessException exception(String customMessage) {
        return new BusinessException(this, customMessage);
    }
}

 

위와 같이 규정할 ErrorCode를 한 곳에서 관리할 뿐만 아니라,
규정된 ErrorCode에 따른 HttpStatus를 함께 관리하고,
ErrorCode가 BusinessException을 생성하도록 책임지게 하였다

 

또한, BusinessException의 경우 외부에서 생성되지 못하도록 접근제어자를 protected로 수정하였다

public class BusinessException extends RuntimeException {
	private final ErrorCode errorCode;

	protected BusinessException(ErrorCode errorCode, String message) {
		super(message);
		this.errorCode = errorCode;
	}

	public String errorCode() {
		return errorCode.name();
	}

	public HttpStatus status() {
		return errorCode.status();
	}
}

 

ExceptionHandler도 크게 수정할 필요가 없었다

@RestControllerAdvice
public class GlobalExceptionHandler {
    //...
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ApiErrorResponse> handleBusinessException(BusinessException businessException) {
        HttpStatus status = businessException.status();
        String responseCode = businessException.errorCode();
        ApiErrorResponse response = ApiErrorResponse.of(responseCode, businessException.getMessage());

        return ResponseEntity.status(status).body(response);
    }
    //...
}

 

이러한 설계를 바탕으로 불필요한 커스텀 익셉션을 늘리지 않고,
의도한 예외를 관리하기 위한 코드를 한 곳으로 응집 시킬 수 있었다.

 

 

 

여러 개의 예외 클래스를 만드는 대신, 

하나의 BusinessException으로 표현하고 ErrorCode로 세분화하는 구조는 

유지보수성과 응답 일관성 측면에서 훨씬 강력했다.

 

 

 

성공 응답 구조 설계

성공 응답도 에러 응답처럼 일관돼야 한다.

성공했다고 해서 설계에 느슨함이 허용되는 건 아니다.

 

물론 성공 응답은 전달하려는 데이터 내부 구조가 다를 수 밖에 없다.


에러 응답처럼 에러 사유에 대한 메시지 전달이 아닌,
요청자가 원하는 데이터를 전달함으로써 응답을 처리해야한다

그러나 경우에 따라 아무 데이터가 없어도 성공 응답을 줘야 할 때가 있다


따라서 최소한 성공과 실패 명확히 구분지을 수 있는 값을 제공하여

API 응답 이후 그 다음 로직이 실행될 수 있도록 보장해줘야한다

 

이러한 사고를 바탕으로 아래와 같은 응답 구조를 설계했다

public record ApiSuccessResponse<T>(boolean success, T data) {
    public static <T> ApiSuccessResponse<T> of(T data) {
        return new ApiSuccessResponse<>(true, data);
    }
    
    public static <T> ApiSuccessResponse<T> of() {
        return new ApiSuccessResponse<>(true, null);
    }
}

 

매우 간단하지만 위 설계에서 중요한 부분이 있다

바로 제네릭 활용이다

 

API 요청에 따라 응답 해야할 데이터 구조가 다를 수 밖에 없다.

따라서 전달할 data 표현을 제네릭을 활용하여 응답 구조를 설계했다

 

그리고 이를 에러 응답 구조에서와 같이,
성공 응답이라 할지라도, 어떤 종류의 성공 응답인지 나누어 구분하고 싶었다.

 

여기서 같은 방식으로 enum을 활용할지, 정적 메서드를 구현할지 고민을 했다.

 

곰곰히 생각해본 결과,
현재 단계에서는 성공 응답 종류가 에러 코드처럼 에러 종류가 많거나, 혹은 더 늘어날 여지가 없었다.

따라서 아래와 같은 간단한 유틸성 정적 메서드를 구현하여 성공 응답 종류를 구분 짓고자 한다

public class ApiResponses {
	public static <T> ResponseEntity<ApiSuccessResponse<T>> ok(T data) {
		return ResponseEntity.ok(ApiSuccessResponse.of(data));
	}

	public static <T> ResponseEntity<ApiSuccessResponse<T>> created(T data) {
		return ResponseEntity.status(HttpStatus.CREATED).body(ApiSuccessResponse.of(data));
	}

	public static <T> ResponseEntity<ApiSuccessResponse<T>> noContent() {
		return ResponseEntity.ok(ApiSuccessResponse.of());
	}
}

 

그리고 아래와 같이 컨트롤러에서 활용할 수 있다

import static com.reservation.common.support.response.ApiResponses.*;

@RestController
@RequestMapping("/terms")
@RequiredArgsConstructor
public class TermsController {
    private final TermsService termsService;
    
    @PostMapping
    public ResponseEntity<ApiSuccessResponse<Long>> createTerms(@Valid @RequestBody CreateTermsRequest request) {
        Long termsId = termsService.createTerms(request);
        return created(termsId);
    }
    
    @PutMapping
    public ResponseEntity<ApiSuccessResponse<Long>> updateTerms(@Valid @RequestBody UpdateTermsRequest request) {
        termsService.updateTerms(request);
        return noContent();
    }

    @GetMapping
    public ResponseEntity<ApiSuccessResponse<Page<AdminTermsDto>>> findTerms(@Valid @ModelAttribute TermsSearchCondition condition) {
        validate(bindingResult);
        Page<AdminTermsDto> terms = termsService.findTerms(condition);
        return ok(terms);
    }
}

 

 

응답은 단지 데이터를 전달하는 수단이 아니다. 

 

응답은 설계자의 의도와 서비스의 신뢰도를 표현하는 메시지다.

 

나는 앞으로도 API가 "성공했을 때조차 예측 가능하게" 만들기 위해,

응답 구조를 더 정교하게 다듬어갈 것이다.

 

 

 

에러 코드 고도화

현재 필자는 `ErrorCode` enum을 통해 에러 유형에 따라

 `HttpStatus`, `defaultMessage`를 함께 관리하고 있다. 

 

이 방식은 지금과 같은 간단한 사이드 프로젝트에선 꽤나 유용하다.

하지만 현실 세계의 복잡한 서비스에서는 아래와 같은 한계가 발생할 수 있다.

 

 

HttpStatus만으로 모든 오류를 표현할 수 없다

예를 들어보자.

사용자 인증 실패 상황이라고 해서,

무조건 `401 UNAUTHORIZED`를 던지는 게 과연 충분할까? 

 

  • 토큰이 만료된 것인지?
  • 토큰이 없었던 것인지?
  • 토큰은 유효하지만, 탈퇴한 사용자였는지? 

모두 같은 `401` 에러이지만, 원인은 전혀 다르다.

이렇게 되면 프론트엔드는 동일한 401 응답에 어떻게 대응해야 할지 알기 어렵다.

 
→ 이럴 때 필요한 게, HttpStatus를 넘어서 의미를 더 세분화하는 에러 코드다.
 
 
 

의도를 표현하는 에러 코드 체계가 필요하다

그래서 필자는 다음과 같은 방식으로 고도화된 에러 코드 구조를 고려하고 있다. 
 
 
public enum ErrorCode { 
    AUTH_TOKEN_EXPIRED("AUTH_001", HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다."), 
    AUTH_TOKEN_MISSING("AUTH_002", HttpStatus.UNAUTHORIZED, "토큰이 존재하지 않습니다."), 
    AUTH_USER_DELETED("AUTH_003", HttpStatus.UNAUTHORIZED, "탈퇴한 사용자입니다."), 
    ... 
}
 
 
 여기서 "AUTH_001" 같은 도메인 기반 코드 체계를 정의한다면?
  • 개발자는 로그 상에서 원인 추적이 쉬워지고,
  • 프론트엔드는 코드 값만 보고 어떤 대응을 해야 할지 예측 가능하며,
  • 기획자나 QA 입장에서도 에러가 왜 났는지 쉽게 파악할 수 있다.

 

 

에러 메시지는 내부와 외부의 균형이 필요하다

HTTP 응답은 브라우저 개발자 도구에서 누구나 확인할 수 있다.  
즉, 우리가 어떤 에러 메시지를 노출하느냐에 따라 보안이나 사용자 신뢰에 영향을 줄 수 있다.

예를 들어,  
"탈퇴한 사용자입니다." 같은 메시지를 외부에 그대로 노출하는 건  
프라이버시나 보안상 문제가 될 수 있다.

하지만 내부 QA나 프론트엔드 입장에서는  
"단순히 401이 아니라 탈퇴한 유저라는 걸 알아야"  
정확한 테스트와 대응을 할 수 있다.

그래서 필자는 생각했다.
"에러 메시지는 상황에 따라 숨길 수도 있어야 한다."
이럴 때 도메인 기반 에러 코드는 훌륭한 균형점을 제공한다.

{
  "success": false,
  "code": "AUTH_003",
  "message": "요청을 처리할 수 없습니다."
}

 

 

고도화는 지금보다 나은 협업을 위한 투자다

아직은 단순한 개인 프로젝트지만,
이런 에러 코드 체계를 머릿속에 두고 시작하는 것만으로도
앞으로의 설계 방향에 강력한 기준이 생긴다.

 

그리고 언젠가 더 복잡한 도메인,
더 많은 협업 대상이 있는 환경에서
진짜 무기가 되어줄 것이다.

 

 

 

 

생각 정리

개발 초기, 약관 API를 만들며  
수차례 디버깅을 하던 중 계속 찜찜한 기분을 떨칠 수 없었다.

예외가 발생했을 때 의도하지 않은 정보가 응답에 포함되거나,  
API마다 응답 포맷이 제각각이었던 상황.  


그 응답 구조를 내가 만들었다는 사실이 가장 불편했다.

그래서 생각했다.  
"지금 이 시점에서, 응답 구조부터 제대로 잡자."

그리고 정말 다행히도,  
스프링에는 이 문제를 해결할 강력한 도구들이 있었다.

  • @RestControllerAdvice
  • @ExceptionHandler


이 도구들 덕분에 에러를 한 곳에서 처리할 수 있었고,  
나는 그 시간 동안,  
"예외를 어떻게 구조화할 것인가"에 더 집중할 수 있었다.

의도적인 예외를 관리하기 위해  
커스텀 예외 클래스를 다수 설계할지,  
아니면 `enum` 기반의 `ErrorCode`를 만들지를 고민했고,  
최종적으로 단일 예외 클래스 + enum 중심 관리라는 설계로 방향을 정했다.

이 선택은  

  • 코드의 응집도를 높여주었고, 
  • 의도한 예외를 관리하기 쉽게 만들었으며, 
  • 불필요한 예외 클래스를 만들지 않도록 도와주었다.


성공 응답도 마찬가지다.

응답 구조가 제각각이라면,  
사용자는 "이게 정상인가? 실패인가?" 매번 해석해야 한다.

그래서 최소한 `success` 같은 명확한 기준을 제공하고,  
정적 메서드를 활용해 응답 의도를 드러내는 방식으로 개선했다.


그리고 마지막으로,  
ErrorCode 구조가 이대로 충분한가? 스스로에게 질문했다.

혼자 만드는 사이드 프로젝트에서는 지금 구조로도 충분하다.  
하지만 실제 서비스라면, 더 많은 상황, 더 다양한 에러 케이스를 만날 것이다.

그럴 때를 대비해  

  • 에러 코드를 체계적으로 분류하고 
  • 협업자들과 공유할 수 있는 에러 명세 문서를 갖춰야 한다.

그래야 보안도 지키고,  
동료에게도 명확한 피드백을 줄 수 있다.

이 모든 과정을 통해  
나는 다시금 확신하게 됐다.

일관된 응답 구조는 기술적인 선택이 아니라,
시스템의 안전성과 협업을 위한 설계자의 태도다.

 

 

내가 만든 API의 모든 응답이  
예상 가능하고, 안전하게 설계되어 있어야 한다.
그때서야 비로소, 그걸 사용하는 사람도 안심할 수 있다.