api를 설계할 때는 많은 제약 조건이 필요하다
A 타입은 INT이고 2,000보다 커야한다
B는 다른 데이터들과 중복되면 안되지만, Nullable 해야한다
C는 빌드된 enum 값에 한해서만 요청해야하며,
D는nation테이블에 정의된 name에 대해서만 요청을 해야한다
Spring Boot로 개발을 하게 되면 JPA 라이브러리와 @Valid 어노테이션 등을 통해 데이터의 제약 조건을 설정했던 경험이 있을 것이다
많은 조건이 있는 상황에서 클라이언트와 해당 정보를 누가/어떻게/어디까지 공유해야 할지 공부하고 적용했던 경험을 공유하려 한다
타입 제약 (Type Constraint) & 유일성 제약 (Uniqueness Constraint) & 도메인 제약(Domain Constraint)
제약(Constraint)은 모두 DB 테이블 생성 시, 스키마에 정의하여 만들 수 있습니다.
CREATE TABLE users (
-- 1. 타입 제약 (BIGINT, VARCHAR, INT 등)
id BIGINT AUTO_INCREMENT,
email VARCHAR(255) NOT NULL,
birth_year INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- PRIMARY KEY 제약 (기본키)
CONSTRAINT pk_user_id PRIMARY KEY (id),
-- 2. 유일성 제약 조건 (UNIQUE)
-- 중복된 이메일이 들어오는 것을 DB 레벨에서 절대적으로 막아줍니다.
CONSTRAINT uk_user_email UNIQUE (email),
-- 3. 도메인 범위 제약 조건 (CHECK)
-- 자바의 @Min(2000) 처럼, 2000 이상 정수만 저장되도록 DB가 검증합니다.
CONSTRAINT ck_user_birth_year CHECK (birth_year >= 2000)
);
DB 수준에서 제약을 걸지만, Spring Boot에서는 DB 커넥션을 최소화하기 위해 WAS 수준에서 제약 조건을 검증하고 예외처리한다
비즈니스 규칙 (Business Rule)
도메인 규칙 (Domain Rule)이라고도 한다
제약은 특정 필드 상태에 대해 강제하는 정적인 특징을 가진다
이에 반해 비즈니스 규칙은 객체의 상태에 따라 처리 규칙이 바뀌는 동적인 규칙이다
// Order 엔티티
public void changeAddress(String newAddress) {
// 비즈니스 규칙 검증
if (this.status == OrderStatus.DELIVERING) {
throw new IllegalStateException("배송 중, 주소를 변경할 수 없습니다.");
}
this.address = newAddress;
}
여러 제약과 규칙들 모두 데이터 무결성을 위해 정확히 설계해야 하며, 해당 정보가 클라이언트와 공유가 돼야 UI/UX 수준에서도 의도된 설계가 가능하다
서버의 여러 변수들 중에는 고정되어있는 값과 수시로 변경되는 값이 존재한다
예를 들어, mbti는 E/I, N/S, F/T, P/J 처럼 고정돼 있는 16가지 enum 형태로 관리 할 수 있다
반면 국가 정보는 내 서비스가 관리하는 국가 상태에 따라 삭제되기도 하고 값이 수정되기도 하는 동적인 데이터에 가깝다
동적=런타임 시점, 정적=컴파일 시점에 결정된다고 이해하면 편하다
https://json-schema.org/overview/what-is-jsonschema
JSON 데이터의 구조, 타입, 제약 조건을 정의하고 유효성을 검증하기 위한 표준 형식이다
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/user-registration.schema.json",
"title": "UserRegistration",
"type": "object",
// 1. 필수 값 설정 및 허용되지 않은 뜬금없는 필드 원천 차단
"required": ["username", "age", "password", "couponCount", "depositUnit", "roles", "interests"],
"additionalProperties": false,
"properties": {
// 2. 문자열(String) 패턴 및 길이 검증
"username": {
"type": "string",
"minLength": 4,
"maxLength": 16,
"pattern": "^[a-z0-9-_]+$",
"description": "유저 ID: 4~16자의 영문 소문자, 숫자, 특수문자(-, _)만 허용"
},
"password": {
"type": "string",
"format": "password",
"minLength": 8,
"description": "비밀번호: 최소 8자리 이상 필수"
},
// 3. 숫자(Number/Integer) 범위 및 배수 검증
"age": {
"type": "integer",
"minimum": 19,
"maximum": 120,
"description": "나이: 성인 인증을 위해 19세 이상, 120세 이하만 허용"
},
"couponCount": {
"type": "integer",
"minimum": 0,
"maximum": 5,
"description": "보유 쿠폰 수: 0개에서 최대 5개까지만 보유 가능"
},
"depositUnit": {
"type": "integer",
"minimum": 1000,
"multipleOf": 1000,
"description": "충전 단위: 최소 1,000원 이상이며, 무조건 1,000원 단위(배수)로만 가능"
},
// 4. 열거형(Enum) 및 배열(Array) 데이터 정적 검증
"roles": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {
"type": "string",
"enum": ["USER", "VIP", "ADMIN"]
},
"description": "권한: 최소 1개 이상의 권한이 필요하며, 지정된 내부 등급(enum) 외에는 입력 불가 및 중복 금지"
},
"interests": {
"type": "array",
"minItems": 1,
"maxItems": 3,
"uniqueItems": true,
"items": {
"type": "string"
},
"description": "관심사 태그: 최소 1개, 최대 3개까지만 등록 가능하며 태그 간 중복 불가능"
}
}
}
단일 진실 공급원(Single Souce of Truth)
자료의 중복, 비적합성을 해결하기 위해 자료의 스키마, 정보 등을 한 곳에서만 생성/수정 하는 방법
기본적인 웹/앱, 클라이언트-서버 분리 환경에서는 서버 측이 스키마 정보의 단일 공급원이 되며, Swagger와 직접 작성한 메타 데이터 API를 통해 전달한다
필드들의 타입, 유일성, 도메인 제약 모두 Swagger를 통해 공유할 수 있다

Swagger 문서도 Json Schema를 따르기 때문에 모든 Validation을 관리할 수 있을 거 같지만, 동적으로 변하는 데이터를 관리할 수 없다
Swagger 문서 정보는 Spring Boot에서 사용된 io.swagger, jakarta annotation을 통해 문서화를 만들기 때문에 소스코드 레벨 시점에 문서가 결정된다
또한, DB에 저장하는 동적 데이터 특성 상 Swagger에서 어떤 입력값을 허용하는지 알려주지 못한다
따라서 동적 요청 데이터나 카테고리 등은 직접 작성한 메타 데이터 API를 통해 응답해줬다
GET ~/meta/nations을 통해 동적 나라 데이터를,

GET ~/meta/exam-types로 각기 다른 어학성적 스키마를 동적으로 응답했다

Json Schema는 아니지만,
학교의 실제 이름과 영어 카테고리를 매핑하는 enum 정보도
GET ~/meta/majors로 전달했다

openapi-generator나 MSW(Mock Server Worker) 처럼 이름만 들어본 개념들이 있지만, 클라이언트 정확히 어떤 방식으로 해당 문서를 활용하는지는 모르겠다
따라서 위 방식이 클라이언트 친화적(?)인지는 추후 공부하면 좋겠다