공통 예외 처리, 응답 형식, Feign/Web/JSON 설정을 제공하는 공통 모듈입니다.
⚠️ 배포 전.env파일에 GitHub 인증 정보가 설정되어 있어야 합니다..env.example을 복사하여.env파일을 생성하고 값을 채워주세요.
cp .env.example .env# .env
GITHUB_USER=깃허브유저명
GITHUB_TOKEN=ghp_xxxxxxxxxxxx
GITHUB_TOKEN은 GitHub → Settings → Developer settings → Personal access tokens에서write:packages권한으로 발급합니다.
1. build.gradle에서 버전 수정
// 기능 추가 시 버전 올리기
version = '0.0.1-SNAPSHOT'
version = '0.0.2-SNAPSHOT'2. 배포
./gradlew publish| 버전 | 변경 내용 |
|---|---|
0.0.1-SNAPSHOT |
• 공통 응답 구조 추가 (ApiResponse, ErrorCode, SuccessCode)• 공통 예외 처리 추가 ( BusinessException, GlobalExceptionHandler)• JPA 기본 엔티티 및 설정 추가 ( BaseEntity, BaseUserEntity, JpaConfig)• 자동 설정 등록 ( CommonAutoConfiguration) |
0.0.2-SNAPSHOT |
• Feign 설정 추가 (FeignConfig, FeignErrorDecoder)• PageableResolver 추가 ( CustomPageableResolver, WebConfig)• JsonUtil 추가 (JSON 직렬화/역직렬화 정적 유틸)• ObjectMapper 빈 등록 (JavaTimeModule, FAIL_ON_UNKNOWN_PROPERTIES 설정) |
0.0.3-SNAPSHOT |
• JPA 관련 코드 제거 (BaseEntity, BaseUserEntity, JpaConfig) → common-jpa로 분리 |
0.0.4-SNAPSHOT |
• AuthContext 추가 (Gateway 주입 헤더에서 인증 사용자 정보를 정적 메서드로 추출)• UserRole enum 추가 (공통 역할 타입 — role별 분기 처리 시 사용)• AuthContext.getRole() 반환 타입 String → UserRole 변경 |
GitHub Packages는 public 패키지도 인증이 필요합니다. 팀원 각자 본인 GitHub 계정으로 토큰을 발급하여
.env에 넣어주세요. 토큰 발급 — GitHub → Settings → Developer settings → Personal access tokens →read:packages권한
def env = [:]
def envFile = rootProject.file(".env")
if (envFile.exists()) {
envFile.eachLine { line ->
if (line && !line.startsWith("#") && line.contains("=")) {
def (key, value) = line.split("=", 2)
env[key.trim()] = value.trim()
}
}
}
repositories {
mavenCentral()
maven {
url = 'https://maven.pkg.github.com/first-ticket/common'
credentials {
username = env['GITHUB_USER']
password = env['GITHUB_TOKEN']
}
}
}
dependencies {
implementation 'com.first-ticket:common:0.0.3-SNAPSHOT'
}서비스 루트에
.env파일이 있어야 합니다.
# .env
GITHUB_USER=깃허브유저명
GITHUB_TOKEN=ghp_xxxxxxxxxxxx
com.firstticket.common
├── exception
│ ├── BusinessException.java ← 베이스 예외
│ └── GlobalExceptionHandler.java ← 전역 예외 처리
├── response
│ ├── ApiResponse.java ← 공통 응답 래퍼
│ ├── ErrorCode.java ← 에러 코드 interface
│ ├── SuccessCode.java ← 성공 코드 interface
│ ├── CommonErrorCode.java ← 공통 에러 코드
│ └── CommonSuccessCode.java ← 공통 성공 코드
├── feign
│ ├── FeignConfig.java ← Feign 설정, 헤더 전파
│ └── FeignErrorDecoder.java ← Feign 에러 처리
├── web
│ ├── WebConfig.java ← CustomPageableResolver 등록
│ ├── CustomPageableResolver.java ← 페이지 크기 강제
│ ├── AuthContext.java ← Gateway 주입 헤더 → 정적 메서드로 추출
│ └── UserRole.java ← 공통 사용자 역할 enum (CUSTOMER / HOST / ADMIN)
└── json
├── JsonConfig.java ← ObjectMapper 빈 등록
└── JsonUtil.java ← JSON 직렬화/역직렬화 정적 유틸
BusinessException을 상속받아 서비스 전용 예외 클래스를 생성합니다.
// domain/exception/SampleException.java
public class SampleException extends BusinessException {
public SampleException(SampleErrorCode errorCode) {
super(errorCode);
}
}ErrorCode interface를 구현한 enum을 생성합니다.
// domain/exception/SampleErrorCode.java
@Getter
@RequiredArgsConstructor
public enum SampleErrorCode implements ErrorCode {
SAMPLE_NOT_FOUND(HttpStatus.NOT_FOUND, "샘플을 찾을 수 없습니다"),
ALREADY_CANCELLED(HttpStatus.BAD_REQUEST, "이미 취소된 샘플입니다"),
INVALID_AMOUNT(HttpStatus.BAD_REQUEST, "유효하지 않은 금액입니다");
private final HttpStatus status;
private final String message;
}SuccessCode interface를 구현한 enum을 생성합니다.
// presentation/SampleSuccessCode.java
@Getter
@RequiredArgsConstructor
public enum SampleSuccessCode implements SuccessCode {
SAMPLE_CREATED(HttpStatus.CREATED, "샘플이 생성되었습니다"),
SAMPLE_UPDATED(HttpStatus.OK, "샘플이 수정되었습니다"),
SAMPLE_DELETED(HttpStatus.OK, "샘플이 삭제되었습니다"),
SAMPLE_FOUND(HttpStatus.OK, "샘플을 조회했습니다");
private final HttpStatus status;
private final String message;
}throw new SampleException(SampleErrorCode.ALREADY_CANCELLED);// presentation/SampleController.java
@RestController
@RequiredArgsConstructor
@RequestMapping("/samples")
public class SampleController {
private final SampleService sampleService;
@PostMapping
public ResponseEntity<ApiResponse<SampleResponse>> create(
@RequestBody SampleCreateRequest request) {
SampleResult result = sampleService.create(request.toCommand());
return ApiResponse.success(SampleSuccessCode.SAMPLE_CREATED,
SampleResponse.from(result));
}
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable Long id) {
sampleService.delete(id);
return ApiResponse.success(SampleSuccessCode.SAMPLE_DELETED);
}
}{
"success": true,
"code": "SAMPLE_CREATED",
"message": "샘플이 생성되었습니다",
"timestamp": "2024-01-01T00:00:00",
"data": {
"id": 1,
"name": "샘플"
}
}{
"success": false,
"code": "ALREADY_CANCELLED",
"message": "이미 취소된 샘플입니다",
"timestamp": "2024-01-01T00:00:00"
}서비스 간 HTTP 통신을 위한 Feign 설정이 자동으로 적용됩니다.
// infrastructure/client/UserClient.java
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/users/{id}")
UserResponse getUser(@PathVariable UUID id);
}모든 Feign 요청에 아래 헤더가 자동으로 전파됩니다.
| 헤더 | 설명 |
|---|---|
X-User-Id |
현재 요청한 유저 ID |
X-User-Role |
현재 요청한 유저 권한 |
Feign 호출 실패 시 아래 에러 코드로 변환됩니다.
| 코드 | HTTP 상태 | 메시지 |
|---|---|---|
FEIGN_BAD_REQUEST |
400 | 외부 서비스 요청이 올바르지 않습니다 |
FEIGN_UNAUTHORIZED |
401 | 외부 서비스 인증에 실패했습니다 |
FEIGN_FORBIDDEN |
403 | 외부 서비스 접근 권한이 없습니다 |
FEIGN_NOT_FOUND |
404 | 외부 서비스 리소스를 찾을 수 없습니다 |
FEIGN_SERVER_ERROR |
500 | 외부 서비스 오류가 발생했습니다 |
허용된 페이지 크기 외 요청 시 기본값으로 강제 적용됩니다.
| 항목 | 값 |
|---|---|
| 허용 페이지 크기 | 10, 30, 50 |
| 기본 페이지 크기 | 10 |
⚠️ CustomPageableResolver가 적용되려면 컨트롤러 파라미터에org.springframework.data.domain.Pageable을 사용해야 합니다.
import org.springframework.data.domain.Pageable;
@GetMapping
public ResponseEntity<ApiResponse<SampleListResponse>> getList(Pageable pageable) {
...
}GET /samples?page=0&size=30&sort=createdAt
허용되지 않은 사이즈 요청 시 기본값 10으로 처리됩니다.
GET /samples?size=100 → size=10 으로 강제 적용
JSON 직렬화/역직렬화를 정적 메서드로 제공합니다.
ObjectMapper 빈이 자동으로 주입되므로 별도 설정 없이 바로 사용할 수 있습니다.
// 직렬화
String json = JsonUtil.toJson(sampleDto);
// 역직렬화
SampleDto dto = JsonUtil.fromJson(json, SampleDto.class);
// 제네릭 타입 역직렬화
List<SampleDto> list = JsonUtil.fromJson(json, new TypeReference<List<SampleDto>>() {});직렬화/역직렬화 실패 시 예외를 던지지 않고 null을 반환합니다.
호출하는 쪽에서 null 체크가 필요합니다.
| 설정 | 값 | 설명 |
|---|---|---|
JavaTimeModule |
등록 | LocalDateTime, Instant 등 Java 8 날짜/시간 타입 지원 |
WRITE_DATES_AS_TIMESTAMPS |
false | 날짜를 ISO-8601 문자열로 직렬화 |
FAIL_ON_UNKNOWN_PROPERTIES |
false | 알 수 없는 필드 무시 (Feign, Kafka 통신 시 필요한 필드만 선택적으로 받을 수 있음) |
Gateway가 주입한 X-User-Id, X-User-Role 헤더를 정적 메서드로 추출합니다.
컨트롤러 파라미터 선언 없이 필요한 시점에 어디서든 호출 가능합니다.
UUID userId = AuthContext.getUserId(); // X-User-Id → UUID
UserRole role = AuthContext.getRole(); // X-User-Role → UserRole| 헤더 | 필수 여부 | 누락 시 |
|---|---|---|
X-User-Id |
필수 | UNAUTHORIZED (401) |
X-User-Role |
필수 | UNAUTHORIZED (401) |
- HTTP 요청 컨텍스트 안에서만 동작합니다.
@Async, Kafka 컨슈머, 스케줄러 등 요청 범위 밖에서는 사용할 수 없습니다.X-User-Role헤더값이UserRole에 정의되지 않은 값이면UNAUTHORIZED (401)발생합니다.
Gateway가 전파하는 X-User-Role 헤더값에 대응하는 공통 역할 enum입니다.
동일 API에서 role별 동작이 달라지는 경우 타입 안전하게 분기 처리할 수 있습니다.
| 역할 | 설명 |
|---|---|
CUSTOMER |
일반 사용자 |
HOST |
주최자 |
ADMIN |
관리자 |
UserRole role = AuthContext.getRole();
switch (role) {
case ADMIN -> // 관리자 전용 로직
case HOST -> // 호스트 전용 로직
case CUSTOMER -> // 일반 사용자 로직
}| 코드 | HTTP 상태 | 메시지 |
|---|---|---|
INVALID_INPUT |
400 | 입력값이 올바르지 않습니다 |
UNAUTHORIZED |
401 | 인증에 실패했습니다 |
FORBIDDEN |
403 | 접근 권한이 없습니다 |
NOT_FOUND |
404 | 요청한 리소스를 찾을 수 없습니다 |
METHOD_NOT_ALLOWED |
405 | 허용되지 않는 HTTP 메서드입니다 |
CONFLICT |
409 | 이미 존재하는 리소스입니다 |
INTERNAL_SERVER_ERROR |
500 | 서버 내부 오류가 발생했습니다 |
| 코드 | HTTP 상태 | 메시지 |
|---|---|---|
OK |
200 | 요청이 성공했습니다 |
CREATED |
201 | 리소스가 생성되었습니다 |
DELETED |
200 | 리소스가 삭제되었습니다 |