313 lines
9.6 KiB
Markdown
313 lines
9.6 KiB
Markdown
# Bio Backend
|
|
|
|
## 기술 스택
|
|
|
|
- **Framework**: Spring Boot
|
|
- **Database**: PostgreSQL
|
|
- **ORM**: Spring Data JPA + QueryDSL
|
|
- **Security**: Spring Security + JWT
|
|
- **Build Tool**: Gradle
|
|
- **Container**: Docker + Kubernetes
|
|
- **API Documentation**: Swagger (SpringDoc OpenAPI)
|
|
|
|
## 개발 가이드
|
|
|
|
### 1. 프로젝트 구조
|
|
|
|
```
|
|
src/main/java/com/bio/bio_backend/
|
|
├── domain/ # 도메인별 패키지
|
|
│ └── base/ # 기본 도메인
|
|
│ └── member/ # 회원 도메인
|
|
│ ├── controller/ # API 엔드포인트
|
|
│ ├── service/ # 비즈니스 로직
|
|
│ ├── repository/ # 데이터 접근
|
|
│ ├── entity/ # JPA 엔티티
|
|
│ ├── dto/ # 데이터 전송 객체
|
|
│ ├── mapper/ # MapStruct 매퍼
|
|
│ └── enums/ # 도메인 열거형
|
|
├── global/ # 공통 설정
|
|
│ ├── config/ # 설정 클래스
|
|
│ ├── security/ # 보안 설정
|
|
│ ├── exception/ # 예외 처리
|
|
│ ├── aop/ # AOP 로깅
|
|
│ ├── filter/ # HTTP 로깅 필터
|
|
│ ├── utils/ # 유틸리티
|
|
│ ├── constants/ # 상수 정의
|
|
│ ├── dto/ # 공통 DTO
|
|
│ ├── entity/ # 공통 엔티티
|
|
│ └── annotation/ # 커스텀 어노테이션
|
|
├── BioBackendApplication.java
|
|
└── ServletInitializer.java
|
|
```
|
|
|
|
### 2. API 응답 표준화 (ApiResponseDto)
|
|
|
|
#### 응답 구조
|
|
|
|
모든 API 응답은 `ApiResponseDto<T>` 형태로 표준화되어 있습니다.
|
|
|
|
```java
|
|
public class ApiResponseDto<T> {
|
|
private int code; // HTTP 상태 코드
|
|
private String message; // 응답 메시지 (ApiResponseCode enum 값)
|
|
private String description; // 응답 설명
|
|
private T data; // 실제 데이터 (제네릭 타입)
|
|
}
|
|
```
|
|
|
|
#### 응답 예시
|
|
|
|
**성공 응답 (201 Created)**
|
|
|
|
```json
|
|
{
|
|
"code": 201,
|
|
"message": "COMMON_SUCCESS_CREATED",
|
|
"description": "Created successfully",
|
|
"data": {
|
|
"seq": 1,
|
|
"userId": "user123",
|
|
"name": "홍길동"
|
|
}
|
|
}
|
|
```
|
|
|
|
**실패 응답 (409 Conflict)**
|
|
|
|
```json
|
|
{
|
|
"code": 409,
|
|
"message": "USER_ID_DUPLICATE",
|
|
"description": "User ID already exists"
|
|
}
|
|
```
|
|
|
|
#### 사용 방법
|
|
|
|
**Controller에서 응답 생성**
|
|
|
|
```java
|
|
@PostMapping("/members")
|
|
public ResponseEntity<ApiResponseDto<CreateMemberResponseDto>> createMember(@RequestBody CreateMemberRequestDto requestDto) {
|
|
// ... 비즈니스 로직 ...
|
|
|
|
// 성공 응답
|
|
ApiResponseDto<CreateMemberResponseDto> apiResponse =
|
|
ApiResponseDto.success(ApiResponseCode.COMMON_SUCCESS_CREATED, responseDto);
|
|
|
|
return ResponseEntity.status(HttpStatus.CREATED).body(apiResponse);
|
|
}
|
|
```
|
|
|
|
**공용 응답 코드 사용**
|
|
|
|
```java
|
|
// ApiResponseCode.java
|
|
public enum ApiResponseCode {
|
|
// 공용 성공 코드
|
|
COMMON_SUCCESS(HttpStatus.OK.value(), "요청 성공"),
|
|
COMMON_SUCCESS_CREATED(HttpStatus.CREATED.value(), "성공적으로 생성되었습니다"),
|
|
COMMON_SUCCESS_UPDATED(HttpStatus.OK.value(), "성공적으로 수정되었습니다"),
|
|
COMMON_SUCCESS_DELETED(HttpStatus.OK.value(), "성공적으로 삭제되었습니다"),
|
|
COMMON_SUCCESS_RETRIEVED(HttpStatus.OK.value(), "성공적으로 조회되었습니다"),
|
|
|
|
// 공용 오류 코드
|
|
COMMON_BAD_REQUEST(HttpStatus.BAD_REQUEST.value(), "필수 요청 본문이 누락되었거나 오류가 발생했습니다"),
|
|
COMMON_UNAUTHORIZED(HttpStatus.UNAUTHORIZED.value(), "인증에 실패했습니다"),
|
|
COMMON_FORBIDDEN(HttpStatus.FORBIDDEN.value(), "접근이 거부되었습니다"),
|
|
COMMON_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "리소스를 찾을 수 없습니다"),
|
|
COMMON_CONFLICT(HttpStatus.CONFLICT.value(), "충돌이 발생했습니다"),
|
|
COMMON_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR.value(), "서버에서 오류가 발생했습니다"),
|
|
|
|
// 사용자 관련 코드
|
|
USER_ID_DUPLICATE(HttpStatus.CONFLICT.value(), "이미 존재하는 사용자 ID입니다"),
|
|
USER_NOT_FOUND(HttpStatus.UNAUTHORIZED.value(), "사용자를 찾을 수 없습니다. 인증에 실패했습니다"),
|
|
USER_PASSWORD_INVALID(HttpStatus.UNAUTHORIZED.value(), "비밀번호가 올바르지 않습니다")
|
|
}
|
|
```
|
|
|
|
#### 핵심 규칙
|
|
|
|
- **모든 API 응답**: `ApiResponseDto<T>`로 감싸서 반환
|
|
- **공용 응답 코드**: `COMMON_` 접두사로 시작하는 범용 코드 사용
|
|
- **일관된 구조**: `code`, `message`, `description`, `data` 필드로 표준화
|
|
- **제네릭 활용**: `<T>`를 통해 다양한 데이터 타입 지원
|
|
|
|
### 3. JWT 인증 시스템
|
|
|
|
#### 토큰 구성
|
|
|
|
- **Access Token**: 15분 (900,000ms) - API 요청 시 사용
|
|
- **Refresh Token**: 7일 (604,800,000ms) - 쿠키에 저장, 자동 갱신
|
|
|
|
#### 자동 토큰 갱신
|
|
|
|
**모든 API 요청마다 자동으로 처리됩니다:**
|
|
|
|
1. **Access Token 유효**: 정상 요청 처리
|
|
2. **Access Token 만료**: Refresh Token으로 자동 갱신
|
|
3. **새 Access Token 발급**: 응답 헤더에 자동 설정
|
|
4. **Refresh Token 갱신**: 보안을 위해 매번 새로운 토큰 발급
|
|
|
|
#### 주요 컴포넌트
|
|
|
|
- **`JwtTokenIssuanceFilter`**: 로그인 시 토큰 발급
|
|
- **`JwtTokenValidationFilter`**: 요청마다 토큰 검증 및 갱신
|
|
- **`JwtUtils`**: 토큰 생성, 검증, 갱신 유틸리티
|
|
|
|
### 4. API 문서화 (Swagger)
|
|
|
|
#### Swagger UI 접속
|
|
|
|
- **URL**: `http://localhost:8080/service/swagger-ui.html`
|
|
- **API Docs**: `http://localhost:8080/service/api-docs`
|
|
|
|
#### 주요 어노테이션
|
|
|
|
```java
|
|
@Tag(name = "Member", description = "회원 관리 API")
|
|
@Operation(summary = "회원 가입", description = "새로운 회원을 등록합니다.")
|
|
@ApiResponses(value = {
|
|
@ApiResponse(responseCode = "201", description = "회원 가입 성공"),
|
|
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터",
|
|
content = @Content(schema = @Schema(implementation = ApiResponseDto.class))),
|
|
@ApiResponse(responseCode = "409", description = "중복된 사용자 정보",
|
|
content = @Content(schema = @Schema(implementation = ApiResponseDto.class)))
|
|
})
|
|
```
|
|
|
|
#### 설정 파일
|
|
|
|
- **SwaggerConfig.java**: OpenAPI 기본 정보 설정
|
|
- **application.properties**: Swagger UI 커스터마이징
|
|
|
|
### 5. 트랜잭션 관리
|
|
|
|
#### 기본 설정
|
|
|
|
```java
|
|
@Service
|
|
@Transactional(readOnly = true) // 클래스 레벨: 읽기 전용 기본값
|
|
public class MemberServiceImpl {
|
|
|
|
// 읽기 전용 메서드 (별도 어노테이션 불필요)
|
|
public MemberDto selectMember(long seq) { ... }
|
|
|
|
// 쓰기 작업 메서드 (개별 @Transactional 적용)
|
|
@Transactional
|
|
public MemberDto createMember(MemberDto dto) { ... }
|
|
}
|
|
```
|
|
|
|
#### 핵심 규칙
|
|
|
|
- **클래스 레벨**: `@Transactional(readOnly = true)` 기본 설정
|
|
- **메서드별**: 데이터 수정 시에만 `@Transactional` 개별 적용
|
|
- **설정**: `spring.jpa.open-in-view=false` (성능 최적화)
|
|
|
|
### 6. 오류 등록 및 사용
|
|
|
|
#### 오류 코드 등록
|
|
|
|
```java
|
|
// ApiResponseCode.java
|
|
public enum ApiResponseCode {
|
|
USER_ID_DUPLICATE(HttpStatus.CONFLICT.value(), "User ID already exists"),
|
|
}
|
|
```
|
|
|
|
#### 오류 사용 방법
|
|
|
|
```java
|
|
// Service에서 예외 발생
|
|
throw new ApiException(ApiResponseCode.USER_ID_DUPLICATE);
|
|
|
|
// Controller에서 예외 처리 (자동)
|
|
// GlobalExceptionHandler가 ApiException을 잡아서 응답 변환
|
|
```
|
|
|
|
#### 핵심 규칙
|
|
|
|
- **오류 코드**: `ApiResponseCode` enum에 모든 오류 정의
|
|
- **예외 클래스**: `ApiException`으로 비즈니스 로직 예외 처리
|
|
- **자동 처리**: `GlobalExceptionHandler`가 일관된 응답 형태로 변환
|
|
|
|
### 7. 로깅 시스템
|
|
|
|
#### @LogExecution 어노테이션 사용법
|
|
|
|
**메서드 실행 로깅을 위한 필수 어노테이션입니다.**
|
|
|
|
##### 기본 사용법
|
|
|
|
```java
|
|
@LogExecution("회원 등록")
|
|
@PostMapping("/register")
|
|
public ResponseEntity<ApiResponseDto<CreateMemberResponseDto>> createMember(@RequestBody CreateMemberRequestDto requestDto) {
|
|
// 메서드 실행 시 자동으로 로그 출력
|
|
}
|
|
```
|
|
|
|
##### 로그 출력 예시
|
|
|
|
**메서드 시작 시:**
|
|
|
|
```
|
|
[START] 회원 등록 | 호출경로: MemberController.createMember | 사용자: 고명빈(kmbin92)
|
|
```
|
|
|
|
**메서드 성공 시:**
|
|
|
|
```
|
|
[SUCCESS] 회원 등록 | 호출경로: MemberController.createMember | 사용자: 고명빈(kmbin92) | 시간: 200ms
|
|
```
|
|
|
|
**메서드 실패 시:**
|
|
|
|
```
|
|
[FAILED] 회원 등록 | 호출경로: MemberController.createMember | 사용자: 고명빈(kmbin92) | 시간: 23ms | 오류: 이미 존재하는 사용자 ID입니다
|
|
```
|
|
|
|
##### 어노테이션 설정
|
|
|
|
```java
|
|
@LogExecution("사용자 정보 조회")
|
|
public UserDto getUserInfo() { }
|
|
|
|
@LogExecution("주문 처리")
|
|
public OrderDto processOrder() { }
|
|
```
|
|
|
|
##### 로깅 시스템 구성
|
|
|
|
1. **MethodExecutionLoggingAspect**: 메서드 실행 로깅 (AOP)
|
|
2. **HttpLoggingFilter**: HTTP 요청/응답 로깅 (필터)
|
|
3. **logback-spring.xml**: 로그 파일 관리 및 설정
|
|
|
|
**중요**: `@LogExecution` 어노테이션이 없으면 메서드 실행 로그가 출력되지 않습니다
|
|
|
|
### 8. MapStruct
|
|
|
|
**매퍼 인터페이스**
|
|
|
|
```java
|
|
@Mapper(componentModel = "spring")
|
|
public interface MemberMapper {
|
|
MemberDto toDto(Member member);
|
|
Member toEntity(MemberDto dto);
|
|
}
|
|
```
|
|
|
|
**사용 예시**
|
|
|
|
```java
|
|
// Entity → DTO
|
|
MemberDto dto = memberMapper.toDto(member);
|
|
|
|
// DTO → Entity
|
|
Member entity = memberMapper.toEntity(dto);
|
|
```
|
|
|
|
**자동 생성**: 컴파일 시 `MemberMapperImpl` 구현체 생성
|