diff --git a/DEVELOPMENT_GUIDE.md b/DEVELOPMENT_GUIDE.md deleted file mode 100644 index 8050350..0000000 --- a/DEVELOPMENT_GUIDE.md +++ /dev/null @@ -1,1602 +0,0 @@ -# Spring Batch 개발 가이드 - -## 목차 -1. [프로젝트 개요](#1-프로젝트-개요) -2. [프로젝트 구조](#2-프로젝트-구조) -3. [추상 클래스 구조](#3-추상-클래스-구조) -4. [새로운 배치 Job 생성 가이드](#4-새로운-배치-job-생성-가이드) -5. [예제: 전체 구현 과정](#5-예제-전체-구현-과정) -6. [베스트 프랙티스](#6-베스트-프랙티스) -7. [트러블슈팅](#7-트러블슈팅) - ---- - -## 1. 프로젝트 개요 - -### 1.1 기술 스택 -- **Spring Boot**: 3.2.1 -- **Spring Batch**: 5.1.0 -- **Quartz Scheduler**: 2.5.0 -- **PostgreSQL**: 42.7.4 -- **WebClient**: REST API 호출 -- **Thymeleaf**: 웹 UI -- **Bootstrap 5**: 프론트엔드 - -### 1.2 주요 기능 -- REST API 기반 데이터 수집 배치 -- Quartz 기반 스케줄링 관리 (DB 영속화) -- 웹 UI를 통한 배치 모니터링 -- 타임라인 차트 (일/주/월 단위) -- 대시보드 및 실행 이력 조회 -- 통일된 추상화 구조 (Reader/Processor/Writer) -- REST API 표준 응답 형식 - -### 1.3 아키텍처 패턴 - -#### 배치 처리 패턴 -``` -External API → Reader → Processor → Writer → Database - ↓ ↓ ↓ - (API Call) (Transform) (Batch Insert) -``` - -#### 계층 구조 -``` -Controller (REST API) - ↓ -Service (비즈니스 로직) - ↓ -Repository (데이터 액세스) - ↓ -Database (PostgreSQL) -``` - -#### 추상화 구조 -``` -com.snp.batch/ -├── SnpBatchApplication.java # Spring Boot 메인 클래스 -│ -├── common/ # 공통 추상화 모듈 -│ ├── batch/ # 배치 작업 추상화 -│ │ ├── config/ -│ │ │ └── BaseJobConfig # Job/Step 설정 템플릿 -│ │ ├── entity/ -│ │ │ └── BaseEntity # Entity 공통 감사 필드 -│ │ ├── processor/ -│ │ │ └── BaseProcessor # Processor 템플릿 -│ │ ├── reader/ -│ │ │ └── BaseApiReader # API Reader 템플릿 -│ │ ├── repository/ -│ │ │ └── BaseJdbcRepository # JDBC Repository 템플릿 -│ │ └── writer/ -│ │ └── BaseWriter # Writer 템플릿 -│ │ -│ └── web/ # 웹 API 추상화 -│ ├── controller/ -│ │ └── BaseController # 컨트롤러 템플릿 -│ ├── dto/ -│ │ └── BaseDto # 공통 DTO 필드 -│ ├── service/ -│ │ ├── BaseService # 서비스 인터페이스 -│ │ ├── BaseServiceImpl # 서비스 구현 템플릿 (JDBC) -│ │ ├── BaseProxyService # 프록시 서비스 템플릿 -│ │ └── BaseHybridService # 하이브리드 서비스 템플릿 -│ └── ApiResponse # 공통 API 응답 래퍼 -│ -├── global/ # 전역 클래스 (애플리케이션 레벨) -│ ├── config/ # 전역 설정 (Quartz, Swagger 등) -│ ├── controller/ # 전역 컨트롤러 (Batch, Web UI) -│ ├── dto/ # 전역 DTO (Job 실행, 스케줄 등) -│ ├── model/ # 전역 Entity (스케줄 정보 등) - JPA 허용 -│ └── repository/ # 전역 Repository - JPA 허용 -│ -├── jobs/ # 배치 Job 구현 (도메인별, JDBC 전용) -│ ├── sample/ # 샘플 제품 데이터 Job -│ │ ├── batch/ # 배치 작업 -│ │ └── web/ # 웹 API (선택) -│ └── shipimport/ # 선박 데이터 Import Job -│ └── batch/ # 배치 작업만 -│ -├── service/ # 전역 서비스 -└── scheduler/ # 스케줄러 (Quartz Job, Initializer) -``` - ---- - -## 2. 프로젝트 구조 - -``` -src/main/java/com/snp/batch/ -├── SnpBatchApplication.java # Spring Boot 메인 클래스 -│ -├── common/ # 공통 추상화 모듈 -│ ├── batch/ # 배치 작업 공통 Base 클래스 -│ │ ├── config/ -│ │ │ └── BaseJobConfig.java # Job/Step 설정 템플릿 -│ │ ├── entity/ -│ │ │ └── BaseEntity.java # 공통 감사 필드 (JPA 제거) -│ │ ├── processor/ -│ │ │ └── BaseProcessor.java # Processor 템플릿 -│ │ ├── reader/ -│ │ │ └── BaseApiReader.java # API Reader 템플릿 -│ │ ├── repository/ -│ │ │ └── BaseJdbcRepository.java # JDBC Repository 템플릿 -│ │ └── writer/ -│ │ └── BaseWriter.java # Writer 템플릿 -│ │ -│ └── web/ # 웹 API 공통 Base 클래스 -│ ├── controller/ -│ │ └── BaseController.java # 컨트롤러 템플릿 -│ ├── dto/ -│ │ └── BaseDto.java # 공통 DTO 필드 -│ ├── service/ -│ │ ├── BaseService.java # 서비스 인터페이스 -│ │ ├── BaseServiceImpl.java # 서비스 구현 템플릿 (JDBC) -│ │ ├── BaseProxyService.java # 프록시 서비스 템플릿 -│ │ └── BaseHybridService.java # 하이브리드 서비스 템플릿 -│ └── ApiResponse.java # 공통 API 응답 래퍼 -│ -├── global/ # 전역 클래스 (JPA 허용) -│ ├── config/ # 전역 설정 -│ │ ├── QuartzConfig.java # Quartz 스케줄러 설정 -│ │ └── SwaggerConfig.java # Swagger 설정 -│ │ -│ ├── controller/ # 전역 컨트롤러 -│ │ ├── BatchController.java # 배치 관리 REST API -│ │ └── WebViewController.java # Thymeleaf 뷰 컨트롤러 -│ │ -│ ├── dto/ # 전역 DTO -│ │ ├── DashboardResponse.java # 대시보드 응답 -│ │ ├── JobExecutionDetailDto.java # 실행 상세 정보 -│ │ ├── JobExecutionDto.java # 실행 이력 DTO -│ │ ├── ScheduleRequest.java # 스케줄 등록/수정 요청 -│ │ ├── ScheduleResponse.java # 스케줄 조회 응답 -│ │ └── TimelineResponse.java # 타임라인 응답 -│ │ -│ ├── model/ # 전역 Entity (JPA) -│ │ └── JobScheduleEntity.java # 스케줄 Entity (JPA) -│ │ -│ └── repository/ # 전역 Repository (JPA) -│ ├── JobScheduleRepository.java # JpaRepository (JPA) -│ └── TimelineRepository.java # 타임라인 Repository -│ -├── jobs/ # 도메인별 배치 Job (JDBC 전용) -│ │ -│ ├── sample/ # 샘플 제품 데이터 Job -│ │ ├── batch/ # 배치 작업 -│ │ │ ├── config/ -│ │ │ │ └── ProductDataImportJobConfig.java -│ │ │ ├── dto/ -│ │ │ │ ├── ProductApiResponse.java -│ │ │ │ └── ProductDto.java -│ │ │ ├── entity/ -│ │ │ │ └── ProductEntity.java # extends BaseEntity (JPA 제거) -│ │ │ ├── processor/ -│ │ │ │ └── ProductDataProcessor.java -│ │ │ ├── reader/ -│ │ │ │ └── ProductDataReader.java -│ │ │ ├── repository/ -│ │ │ │ ├── ProductRepository.java -│ │ │ │ └── ProductRepositoryImpl.java # extends BaseJdbcRepository -│ │ │ └── writer/ -│ │ │ └── ProductDataWriter.java -│ │ │ -│ │ └── web/ # 웹 API -│ │ ├── controller/ -│ │ │ └── ProductWebController.java -│ │ ├── dto/ -│ │ │ └── ProductWebDto.java -│ │ └── service/ -│ │ └── ProductWebService.java -│ │ -│ └── shipimport/ # 선박 데이터 Import Job -│ └── batch/ # 배치 작업 (웹 API 없음) -│ ├── config/ -│ │ └── ShipImportJobConfig.java -│ ├── dto/ -│ │ ├── ShipApiResponse.java -│ │ └── ShipDto.java -│ ├── entity/ -│ │ └── ShipEntity.java # extends BaseEntity (JPA 제거) -│ ├── processor/ -│ │ └── ShipDataProcessor.java -│ ├── reader/ -│ │ └── ShipDataReader.java -│ ├── repository/ -│ │ ├── ShipRepository.java -│ │ └── ShipRepositoryImpl.java # extends BaseJdbcRepository -│ └── writer/ -│ └── ShipDataWriter.java -│ -├── service/ # 전역 서비스 -│ ├── BatchService.java # 배치 실행 관리 -│ ├── QuartzJobService.java # Quartz-Batch 연동 -│ └── ScheduleService.java # 스케줄 DB 영속화 -│ -└── scheduler/ # 스케줄러 - ├── QuartzBatchJob.java # Quartz Job 구현체 - └── SchedulerInitializer.java # 스케줄 자동 로드 -``` - -**주요 특징**: -- `common/batch/`: 배치 작업 전용 Base 클래스 (JDBC 기반) -- `common/web/`: 웹 API 전용 Base 클래스 (JDBC 기반) -- `global/`: JPA 사용 허용 (간단한 CRUD만) -- `jobs/`: 모든 Job은 JDBC 전용 (성능 최적화) - ---- - -## 3. 추상 클래스 구조 - -### 3.0 공통 베이스 클래스 - -#### 3.0.1 BaseEntity - -**목적**: 모든 Entity의 공통 감사(Audit) 필드 관리 - -**위치**: `com.snp.batch.common.batch.entity.BaseEntity` - -**제공 필드**: -```java -@CreatedDate -private LocalDateTime createdAt; // 생성 일시 (자동 설정) - -@LastModifiedDate -private LocalDateTime updatedAt; // 수정 일시 (자동 업데이트) - -private String createdBy; // 생성자 (기본값: "SYSTEM") -private String updatedBy; // 수정자 (기본값: "SYSTEM") -``` - -**사용 방법 (jobs 패키지 - JDBC 전용)**: -```java -/** - * Ship Entity - JDBC Template 기반 - * JPA 어노테이션 사용 금지 - * 컬럼 매핑은 주석으로 명시 - */ -@Data -@SuperBuilder -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode(callSuper = true) -public class ShipEntity extends BaseEntity { - /** - * 기본 키 (자동 생성) - * 컬럼: id (BIGSERIAL) - */ - private Long id; - - /** - * 선박 이름 - * 컬럼: ship_name (VARCHAR(100)) - */ - private String shipName; - - // createdAt, updatedAt, createdBy, updatedBy는 BaseEntity에서 상속 -} -``` - -**사용 방법 (global 패키지 - JPA 허용)**: -```java -@Entity -@Table(name = "job_schedule") -@Data -@SuperBuilder -@NoArgsConstructor -@AllArgsConstructor -public class JobScheduleEntity extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "job_name", unique = true, nullable = false) - private String jobName; - // createdAt, updatedAt, createdBy, updatedBy는 자동 관리됨 -} -``` - -**주요 기능**: -- jobs 패키지: JDBC 기반, JPA 어노테이션 없음, RowMapper로 수동 매핑 -- global 패키지: JPA 기반, @PrePersist/@PreUpdate로 자동 관리 -- 공통 감사 필드: createdAt, updatedAt, createdBy, updatedBy - ---- - -#### 3.0.2 BaseDto - -**목적**: 모든 DTO의 공통 감사 필드 제공 - -**위치**: `com.snp.batch.common.web.dto.BaseDto` - -**제공 필드**: -```java -private LocalDateTime createdAt; -private LocalDateTime updatedAt; -private String createdBy; -private String updatedBy; -``` - -**사용 방법**: -```java -@Data -@EqualsAndHashCode(callSuper = true) -public class ShipDto extends BaseDto { - private String shipName; - private String shipType; - // 부모 클래스의 감사 필드 자동 상속 -} -``` - ---- - -#### 3.0.3 BaseService - -**목적**: Service 계층의 공통 CRUD 인터페이스 정의 - -**위치**: `com.snp.batch.common.web.service.BaseService` - -**제공 메서드**: -```java -D create(D dto); // 생성 -Optional findById(ID id); // 단건 조회 -List findAll(); // 전체 조회 -Page findAll(Pageable pageable); // 페이징 조회 -D update(ID id, D dto); // 수정 -void deleteById(ID id); // 삭제 -boolean existsById(ID id); // 존재 여부 확인 -D toDto(T entity); // Entity → DTO 변환 -T toEntity(D dto); // DTO → Entity 변환 -``` - ---- - -#### 3.0.4 BaseServiceImpl - -**목적**: BaseService의 기본 구현 제공 - -**위치**: `com.snp.batch.common.web.service.BaseServiceImpl` - -**필수 구현 메서드**: -```java -protected abstract BaseJdbcRepository getRepository(); // Repository 반환 -protected abstract String getEntityName(); // Entity 이름 (로깅용) -protected abstract void updateEntity(T entity, D dto); // Entity 업데이트 로직 -protected abstract ID extractId(T entity); // Entity에서 ID 추출 -``` - -**참고**: jobs 패키지에서는 JDBC 기반 Repository를 사용합니다 (BaseJdbcRepository 상속). - -**사용 예제**: -```java -@Service -@RequiredArgsConstructor -public class ProductWebService extends BaseServiceImpl { - - private final ProductRepository productRepository; - - @Override - protected BaseJdbcRepository getRepository() { - return productRepository; - } - - @Override - protected String getEntityName() { - return "Product"; - } - - @Override - protected void updateEntity(ProductEntity entity, ProductWebDto dto) { - entity.setProductName(dto.getProductName()); - entity.setCategory(dto.getCategory()); - entity.setPrice(dto.getPrice()); - } - - @Override - protected Long extractId(ProductEntity entity) { - return entity.getId(); - } - - @Override - public ProductWebDto toDto(ProductEntity entity) { - return ProductWebDto.builder() - .productId(entity.getProductId()) - .productName(entity.getProductName()) - .category(entity.getCategory()) - .price(entity.getPrice()) - .build(); - } - - @Override - public ProductEntity toEntity(ProductWebDto dto) { - return ProductEntity.builder() - .productId(dto.getProductId()) - .productName(dto.getProductName()) - .category(dto.getCategory()) - .price(dto.getPrice()) - .build(); - } -} -``` - ---- - -#### 3.0.5 BaseController - -**목적**: REST Controller의 공통 CRUD API 제공 - -**위치**: `com.snp.batch.common.web.controller.BaseController` - -**필수 구현 메서드**: -```java -protected abstract BaseService getService(); // Service 반환 -protected abstract String getResourceName(); // 리소스 이름 (로깅용) -``` - -**제공 API**: -```java -POST / → create(D dto) # 생성 -GET /{id} → getById(ID id) # 단건 조회 -GET / → getAll() # 전체 조회 -GET /page → getPage(Pageable) # 페이징 조회 -PUT /{id} → update(ID id, D dto) # 수정 -DELETE /{id} → delete(ID id) # 삭제 -GET /{id}/exists → exists(ID id) # 존재 여부 -``` - -**사용 예제**: -```java -@RestController -@RequestMapping("/api/ships") -@RequiredArgsConstructor -public class ShipController extends BaseController { - - private final ShipService shipService; - - @Override - protected BaseService getService() { - return shipService; - } - - @Override - protected String getResourceName() { - return "Ship"; - } - - // 추가 커스텀 API가 필요한 경우 여기에 정의 -} -``` - ---- - -#### 3.0.6 ApiResponse - -**목적**: 통일된 API 응답 형식 제공 - -**위치**: `com.snp.batch.common.web.ApiResponse` - -**필드 구조**: -```java -private boolean success; // 성공 여부 -private String message; // 메시지 -private T data; // 응답 데이터 -private String errorCode; // 에러 코드 (실패 시) -``` - -**사용 방법**: -```java -// 성공 응답 -ApiResponse response = ApiResponse.success(shipDto); -ApiResponse response = ApiResponse.success("Ship created", shipDto); - -// 실패 응답 -ApiResponse response = ApiResponse.error("Ship not found"); -ApiResponse response = ApiResponse.error("Validation failed", "ERR_001"); -``` - -**응답 예제**: -```json -{ - "success": true, - "message": "Success", - "data": { - "shipName": "Titanic", - "shipType": "Passenger" - }, - "errorCode": null -} -``` - ---- - -### 3.1 BaseApiReader - -**목적**: REST API에서 데이터를 읽어오는 ItemReader 구현 패턴 제공 - -**위치**: `com.snp.batch.common.batch.reader.BaseApiReader` - -**필수 구현 메소드**: -```java -protected abstract String getApiPath(); // API 경로 -protected abstract List extractDataFromResponse(Object response); // 응답 파싱 -protected abstract Class getResponseType(); // 응답 클래스 -protected abstract String getReaderName(); // Reader 이름 -``` - -**선택적 오버라이드 메소드**: -```java -protected void addQueryParams(UriBuilder uriBuilder) {} // 쿼리 파라미터 추가 -protected void beforeApiCall() {} // API 호출 전처리 -protected void afterApiCall(List data) {} // API 호출 후처리 -protected void handleApiError(Exception e) {} // 에러 처리 -``` - -**제공되는 기능**: -- API 호출 및 데이터 캐싱 (한 번만 호출) -- 순차적 데이터 반환 (read() 메소드) -- 자동 로깅 및 에러 핸들링 - -**사용 예제**: -```java -@Component -public class ShipDataReader extends BaseApiReader { - - public ShipDataReader(WebClient webClient) { - super(webClient); - } - - @Override - protected String getApiPath() { - return "/api/v1/ships"; - } - - @Override - protected List extractDataFromResponse(Object response) { - ShipApiResponse apiResponse = (ShipApiResponse) response; - return apiResponse.getData(); - } - - @Override - protected Class getResponseType() { - return ShipApiResponse.class; - } - - @Override - protected String getReaderName() { - return "ShipDataReader"; - } -} -``` - ---- - -### 3.2 BaseProcessor - -**목적**: 데이터 변환 및 처리 로직의 템플릿 제공 - -**위치**: `com.snp.batch.common.batch.processor.BaseProcessor` - -**필수 구현 메소드**: -```java -protected abstract O transform(I item) throws Exception; // 데이터 변환 -protected abstract boolean shouldProcess(I item); // 처리 여부 판단 -protected abstract String getProcessorName(); // Processor 이름 -``` - -**선택적 오버라이드 메소드**: -```java -protected void beforeProcess(I item) {} // 전처리 -protected void afterProcess(I input, O output) {} // 후처리 -protected void handleProcessError(I item, Exception e) {} // 에러 처리 -protected void onItemFiltered(I item) {} // 필터링 로깅 -``` - -**제공되는 기능**: -- DTO → Entity 변환 패턴 -- 데이터 필터링 (shouldProcess 기반) -- 자동 로깅 및 에러 핸들링 - -**사용 예제**: -```java -@Component -public class ShipDataProcessor extends BaseProcessor { - - @Override - protected ShipEntity transform(ShipDto dto) { - return ShipEntity.builder() - .shipId(dto.getShipId()) - .shipName(dto.getShipName()) - .shipType(dto.getShipType()) - .build(); - } - - @Override - protected boolean shouldProcess(ShipDto dto) { - // 유효성 검사: shipId가 있는 경우만 처리 - return dto.getShipId() != null && !dto.getShipId().isEmpty(); - } - - @Override - protected String getProcessorName() { - return "ShipDataProcessor"; - } - - @Override - protected void onItemFiltered(ShipDto dto) { - log.warn("Ship ID가 없어 필터링됨: {}", dto); - } -} -``` - ---- - -### 3.3 BaseWriter - -**목적**: 데이터베이스 저장 로직의 템플릿 제공 - -**위치**: `com.snp.batch.common.batch.writer.BaseWriter` - -**필수 구현 메소드**: -```java -protected abstract void writeItems(List items) throws Exception; // 저장 로직 -protected abstract String getWriterName(); // Writer 이름 -``` - -**선택적 오버라이드 메소드**: -```java -protected void beforeWrite(List items) {} // 저장 전처리 -protected void afterWrite(List items) {} // 저장 후처리 -protected void handleWriteError(List items, Exception e) {} // 에러 처리 -protected List filterItems(List items) {} // 아이템 필터링 -protected void validateBatchSize(List items) {} // 배치 크기 검증 -``` - -**제공되는 기능**: -- 배치 저장 패턴 (Chunk 단위) -- Null 아이템 자동 필터링 -- 배치 크기 검증 및 경고 -- 자동 로깅 및 에러 핸들링 - -**사용 예제**: -```java -@Component -@RequiredArgsConstructor -public class ShipDataWriter extends BaseWriter { - - private final ShipRepository shipRepository; - - @Override - protected void writeItems(List items) { - shipRepository.saveAll(items); - } - - @Override - protected String getWriterName() { - return "ShipDataWriter"; - } - - @Override - protected void afterWrite(List items) { - log.info("Ship 데이터 저장 완료: {} 건", items.size()); - } -} -``` - ---- - -### 3.4 BaseJobConfig - -**목적**: Batch Job 설정의 표준 템플릿 제공 - -**위치**: `com.snp.batch.common.batch.config.BaseJobConfig` - -**필수 구현 메소드**: -```java -protected abstract String getJobName(); // Job 이름 -protected abstract ItemReader createReader(); // Reader 생성 -protected abstract ItemProcessor createProcessor(); // Processor 생성 -protected abstract ItemWriter createWriter(); // Writer 생성 -``` - -**선택적 오버라이드 메소드**: -```java -protected String getStepName() {} // Step 이름 (기본: {jobName}Step) -protected int getChunkSize() {} // Chunk 크기 (기본: 100) -protected void configureJob(JobBuilder jobBuilder) {} // Job 커스터마이징 -protected void configureStep(StepBuilder stepBuilder) {} // Step 커스터마이징 -``` - -**제공되는 기능**: -- Job 및 Step 자동 생성 -- Chunk 기반 처리 설정 -- Processor가 없는 경우도 지원 - -**사용 예제**: -```java -@Configuration -@RequiredArgsConstructor -public class ShipDataImportJobConfig extends BaseJobConfig { - - private final ShipDataReader shipDataReader; - private final ShipDataProcessor shipDataProcessor; - private final ShipDataWriter shipDataWriter; - - public ShipDataImportJobConfig( - JobRepository jobRepository, - PlatformTransactionManager transactionManager, - ShipDataReader shipDataReader, - ShipDataProcessor shipDataProcessor, - ShipDataWriter shipDataWriter) { - super(jobRepository, transactionManager); - this.shipDataReader = shipDataReader; - this.shipDataProcessor = shipDataProcessor; - this.shipDataWriter = shipDataWriter; - } - - @Override - protected String getJobName() { - return "shipDataImportJob"; - } - - @Override - protected ItemReader createReader() { - return shipDataReader; - } - - @Override - protected ItemProcessor createProcessor() { - return shipDataProcessor; - } - - @Override - protected ItemWriter createWriter() { - return shipDataWriter; - } - - @Override - protected int getChunkSize() { - return 50; // 커스텀 Chunk 크기 - } - - @Bean(name = "shipDataImportJob") - public Job shipDataImportJob() { - return job(); - } - - @Bean(name = "shipDataImportStep") - public Step shipDataImportStep() { - return step(); - } -} -``` - ---- - -### 3.5 BaseJdbcRepository - -**목적**: JDBC 기반 Repository의 CRUD 템플릿 제공 - -**위치**: `com.snp.batch.common.batch.repository.BaseJdbcRepository` - -**필수 구현 메소드**: -```java -protected abstract String getTableName(); // 테이블 이름 -protected abstract RowMapper getRowMapper(); // RowMapper -protected abstract ID extractId(T entity); // ID 추출 -protected abstract String getInsertSql(); // INSERT SQL -protected abstract String getUpdateSql(); // UPDATE SQL -protected abstract void setInsertParameters(PreparedStatement ps, T entity); -protected abstract void setUpdateParameters(PreparedStatement ps, T entity); -protected abstract String getEntityName(); // Entity 이름 (로깅용) -``` - -**제공되는 기능**: -- findById, findAll, count, existsById -- save, insert, update -- batchInsert, batchUpdate, saveAll -- deleteById, deleteAll -- 자동 트랜잭션 처리 - -**사용 예제**: -```java -@Repository -public class ShipRepository extends BaseJdbcRepository { - - public ShipRepository(JdbcTemplate jdbcTemplate) { - super(jdbcTemplate); - } - - @Override - protected String getTableName() { - return "ships"; - } - - @Override - protected RowMapper getRowMapper() { - return (rs, rowNum) -> ShipEntity.builder() - .shipId(rs.getString("ship_id")) - .shipName(rs.getString("ship_name")) - .shipType(rs.getString("ship_type")) - .build(); - } - - @Override - protected String extractId(ShipEntity entity) { - return entity.getShipId(); - } - - @Override - protected String getInsertSql() { - return "INSERT INTO ships (ship_id, ship_name, ship_type) VALUES (?, ?, ?)"; - } - - @Override - protected String getUpdateSql() { - return "UPDATE ships SET ship_name = ?, ship_type = ? WHERE ship_id = ?"; - } - - @Override - protected void setInsertParameters(PreparedStatement ps, ShipEntity entity) throws SQLException { - ps.setString(1, entity.getShipId()); - ps.setString(2, entity.getShipName()); - ps.setString(3, entity.getShipType()); - } - - @Override - protected void setUpdateParameters(PreparedStatement ps, ShipEntity entity) throws SQLException { - ps.setString(1, entity.getShipName()); - ps.setString(2, entity.getShipType()); - ps.setString(3, entity.getShipId()); - } - - @Override - protected String getEntityName() { - return "Ship"; - } -} -``` - ---- - -## 4. 새로운 배치 Job 생성 가이드 - -### 4.1 사전 준비 - -1. **도메인 파악** - - 어떤 데이터를 수집할 것인가? (예: 선박 데이터, 사용자 데이터 등) - - API 엔드포인트는 무엇인가? - - 데이터 구조는 어떻게 되는가? - -2. **데이터베이스 테이블 생성** - ```sql - CREATE TABLE ships ( - ship_id VARCHAR(50) PRIMARY KEY, - ship_name VARCHAR(100) NOT NULL, - ship_type VARCHAR(50), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ); - ``` - -### 4.2 단계별 구현 - -#### Step 1: DTO 클래스 생성 (jobs/{domain}/batch/dto 패키지) - -**API 응답 DTO**: -```java -@Data -public class ShipApiResponse { - private List data; - private int totalCount; -} -``` - -**데이터 DTO**: -```java -@Data -@Builder -public class ShipDto { - private String imoNumber; - private String coreShipInd; - private String datasetVersion; -} -``` - -#### Step 2: Entity 클래스 생성 (jobs/{domain}/batch/entity 패키지) - -**JDBC 기반 Entity (JPA 어노테이션 없음)**: -```java -/** - * Ship Entity - JDBC Template 기반 - * JPA 어노테이션 사용 금지 - */ -@Data -@SuperBuilder -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode(callSuper = true) -public class ShipEntity extends BaseEntity { - /** - * 기본 키 - * 컬럼: id (BIGSERIAL) - */ - private Long id; - - /** - * IMO 번호 - * 컬럼: imo_number (VARCHAR(20), UNIQUE) - */ - private String imoNumber; - - /** - * Core Ship Indicator - * 컬럼: core_ship_ind (VARCHAR(10)) - */ - private String coreShipInd; - - // createdAt, updatedAt, createdBy, updatedBy는 BaseEntity에서 상속 -} -``` - -#### Step 3: Repository 구현 (jobs/{domain}/batch/repository 패키지) - -```java -@Repository -public class ShipRepository extends BaseJdbcRepository { - - public ShipRepository(JdbcTemplate jdbcTemplate) { - super(jdbcTemplate); - } - - // 추상 메소드 구현 (위의 3.5 예제 참고) -} -``` - -#### Step 4: Reader 구현 (jobs/{domain}/batch/reader 패키지) - -```java -@Component -public class ShipDataReader extends BaseApiReader { - - public ShipDataReader(WebClient webClient) { - super(webClient); - } - - // 추상 메소드 구현 (위의 3.1 예제 참고) -} -``` - -#### Step 5: Processor 구현 (jobs/{domain}/batch/processor 패키지) - -```java -@Component -public class ShipDataProcessor extends BaseProcessor { - - // 추상 메소드 구현 (위의 3.2 예제 참고) -} -``` - -#### Step 6: Writer 구현 (jobs/{domain}/batch/writer 패키지) - -```java -@Component -@RequiredArgsConstructor -public class ShipDataWriter extends BaseWriter { - - private final ShipRepository shipRepository; - - // 추상 메소드 구현 (위의 3.3 예제 참고) -} -``` - -#### Step 7: JobConfig 구현 (jobs/{domain}/batch/config 패키지) - -```java -@Configuration -@RequiredArgsConstructor -public class ShipDataImportJobConfig extends BaseJobConfig { - - private final ShipDataReader shipDataReader; - private final ShipDataProcessor shipDataProcessor; - private final ShipDataWriter shipDataWriter; - - public ShipDataImportJobConfig( - JobRepository jobRepository, - PlatformTransactionManager transactionManager, - ShipDataReader shipDataReader, - ShipDataProcessor shipDataProcessor, - ShipDataWriter shipDataWriter) { - super(jobRepository, transactionManager); - this.shipDataReader = shipDataReader; - this.shipDataProcessor = shipDataProcessor; - this.shipDataWriter = shipDataWriter; - } - - // 추상 메소드 구현 (위의 3.4 예제 참고) - - @Bean(name = "shipDataImportJob") - public Job shipDataImportJob() { - return job(); - } - - @Bean(name = "shipDataImportStep") - public Step shipDataImportStep() { - return step(); - } -} -``` - -#### Step 8: 테스트 및 실행 - -1. **애플리케이션 시작** - ```bash - mvn spring-boot:run - ``` - -2. **웹 UI에서 확인** - - http://localhost:8080 - - "shipDataImportJob" 확인 - - "즉시 실행" 버튼 클릭 - -3. **로그 확인** - ``` - ShipDataReader API 호출 시작 - ShipDataReader API 응답 성공: 100 건 - ShipDataReader 데이터 100건 조회 완료 - ShipDataWriter 데이터 저장 시작: 50 건 - ShipDataWriter 데이터 저장 완료: 50 건 - ``` - ---- - -## 5. 예제: 전체 구현 과정 - -### 5.1 시나리오 -- **목적**: 외부 API에서 사용자 데이터를 수집하여 데이터베이스에 저장 -- **API**: `GET /api/v1/users?status=active` -- **필터링**: 이메일이 있는 사용자만 저장 - -### 5.2 파일 구조 -``` -src/main/java/com/snp/batch/ -└── jobs/user/ - └── batch/ # 배치 작업 - ├── config/ - │ └── UserDataImportJobConfig.java - ├── dto/ - │ ├── UserDto.java - │ └── UserApiResponse.java - ├── entity/ - │ └── UserEntity.java # extends BaseEntity (JPA 제거) - ├── processor/ - │ └── UserDataProcessor.java # extends BaseProcessor - ├── reader/ - │ └── UserDataReader.java # extends BaseApiReader - ├── repository/ - │ ├── UserRepository.java # 인터페이스 - │ └── UserRepositoryImpl.java # extends BaseJdbcRepository - └── writer/ - └── UserDataWriter.java # extends BaseWriter -``` - -### 5.3 테이블 생성 SQL -```sql -CREATE TABLE users ( - user_id BIGINT PRIMARY KEY, - username VARCHAR(100) NOT NULL, - email VARCHAR(255), - status VARCHAR(50), - imported_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); -``` - -### 5.4 코드 구현 - -각 클래스의 전체 구현 코드는 섹션 3의 추상 클래스 사용 예제를 참고하세요. - ---- - -## 6. 베스트 프랙티스 - -### 6.1 네이밍 컨벤션 - -| 구분 | 패턴 | 예제 | -|------|------|------| -| Job 이름 | `{domain}DataImportJob` | `shipDataImportJob` | -| Step 이름 | `{domain}DataImportStep` | `shipDataImportStep` | -| Reader | `{Domain}DataReader` | `ShipDataReader` | -| Processor | `{Domain}DataProcessor` | `ShipDataProcessor` | -| Writer | `{Domain}DataWriter` | `ShipDataWriter` | -| JobConfig | `{Domain}DataImportJobConfig` | `ShipDataImportJobConfig` | -| Repository | `{Domain}Repository` | `ShipRepository` | -| Entity | `{Domain}Entity` | `ShipEntity` | -| DTO | `{Domain}Dto` | `ShipDto` | - -### 6.2 패키지 구조 - -**배치 전용 Job** (예: shipimport): -``` -com.snp.batch.jobs.{domain}/ -└── batch/ - ├── config/ - │ └── {Domain}DataImportJobConfig.java # extends BaseJobConfig - ├── dto/ - │ ├── {Domain}Dto.java - │ └── {Domain}ApiResponse.java - ├── entity/ - │ └── {Domain}Entity.java # extends BaseEntity (JPA 제거) - ├── processor/ - │ └── {Domain}DataProcessor.java # extends BaseProcessor - ├── reader/ - │ └── {Domain}DataReader.java # extends BaseApiReader - ├── repository/ - │ ├── {Domain}Repository.java # 인터페이스 - │ └── {Domain}RepositoryImpl.java # extends BaseJdbcRepository - └── writer/ - └── {Domain}DataWriter.java # extends BaseWriter -``` - -**배치 + 웹 API Job** (예: sample): -``` -com.snp.batch.jobs.{domain}/ -├── batch/ # 배치 작업 -│ ├── config/ -│ ├── dto/ -│ ├── entity/ -│ ├── processor/ -│ ├── reader/ -│ ├── repository/ -│ └── writer/ -└── web/ # 웹 API - ├── controller/ - │ └── {Domain}WebController.java # extends BaseController - ├── dto/ - │ └── {Domain}WebDto.java # extends BaseDto - └── service/ - └── {Domain}WebService.java # extends BaseServiceImpl (JDBC) -``` - -### 6.3 Chunk 크기 선택 가이드 - -| 데이터 크기 | Chunk 크기 | 사용 시나리오 | -|-------------|-----------|--------------| -| 소량 (< 1,000) | 50-100 | 간단한 API 데이터 | -| 중량 (1,000-10,000) | 100-500 | 일반적인 배치 작업 | -| 대량 (> 10,000) | 500-1,000 | 대용량 데이터 처리 | - -### 6.4 에러 처리 전략 - -1. **API 호출 실패** - - `BaseApiReader.handleApiError()` 오버라이드 - - 빈 리스트 반환 (Job 실패 방지) 또는 예외 던지기 - -2. **데이터 변환 실패** - - `BaseProcessor.handleProcessError()` 오버라이드 - - 문제 데이터 로깅 후 null 반환 (다음 데이터 계속 처리) - -3. **저장 실패** - - `BaseWriter.handleWriteError()` 오버라이드 - - 재시도 로직 또는 부분 저장 구현 - -### 6.5 로깅 전략 - -```java -// Reader에서 -@Override -protected void afterApiCall(List data) { - log.info("API 호출 성공: {} 건 조회", data.size()); -} - -// Processor에서 -@Override -protected void onItemFiltered(ShipDto dto) { - log.debug("필터링됨: {}", dto); -} - -// Writer에서 -@Override -protected void afterWrite(List items) { - log.info("저장 완료: {} 건", items.size()); -} -``` - -### 6.6 성능 최적화 - -1. **배치 Insert 사용** - ```java - @Override - protected void writeItems(List items) { - shipRepository.batchInsert(items); // saveAll() 대신 - } - ``` - -2. **API 페이징 처리** - ```java - @Override - protected void addQueryParams(UriBuilder uriBuilder) { - uriBuilder.queryParam("page", 1); - uriBuilder.queryParam("size", 1000); - } - ``` - -3. **경량 쿼리 사용** - - 불필요한 JOIN 제거 - - 필요한 컬럼만 SELECT - ---- - -## 7. 트러블슈팅 - -### 7.1 일반적인 문제 - -#### 문제 1: Job이 실행되지 않음 -**증상**: 웹 UI에서 Job 목록이 보이지 않음 - -**해결 방법**: -1. `@Bean` 어노테이션 확인 - ```java - @Bean(name = "shipDataImportJob") - public Job shipDataImportJob() { - return job(); - } - ``` - -2. JobConfig 클래스에 `@Configuration` 어노테이션 확인 - -3. 로그 확인: "Job 생성: shipDataImportJob" 메시지 확인 - ---- - -#### 문제 2: API 호출 실패 -**증상**: "API 호출 실패" 로그, 데이터 0건 - -**해결 방법**: -1. WebClient 설정 확인 (`application.yml`) - ```yaml - api: - base-url: http://api.example.com - ``` - -2. API 경로 확인 - ```java - @Override - protected String getApiPath() { - return "/api/v1/ships"; // 슬래시 확인 - } - ``` - -3. 네트워크 연결 테스트 - ```bash - curl http://api.example.com/api/v1/ships - ``` - ---- - -#### 문제 3: 데이터가 저장되지 않음 -**증상**: "저장 완료" 로그는 있지만 DB에 데이터 없음 - -**해결 방법**: -1. 트랜잭션 확인 - ```java - @Override - protected void writeItems(List items) { - shipRepository.saveAll(items); // 트랜잭션 내에서 실행되는지 확인 - } - ``` - -2. SQL 로그 활성화 (`application.yml`) - ```yaml - logging: - level: - org.springframework.jdbc.core: DEBUG - ``` - -3. 데이터베이스 연결 확인 - ```bash - psql -U postgres -d batch_db - SELECT * FROM ships; - ``` - ---- - -#### 문제 4: Chunk 처리 중 일부만 저장됨 -**증상**: 100건 조회 중 50건만 저장됨 - -**해결 방법**: -1. Processor의 `shouldProcess()` 확인 - ```java - @Override - protected boolean shouldProcess(ShipDto dto) { - // false 반환 시 필터링됨 - return dto.getShipId() != null; - } - ``` - -2. 필터링 로그 확인 - ```java - @Override - protected void onItemFiltered(ShipDto dto) { - log.warn("필터링됨: {}", dto); // 로그 추가 - } - ``` - ---- - -#### 문제 5: 메모리 부족 오류 (OutOfMemoryError) -**증상**: 대량 데이터 처리 중 OOM 발생 - -**해결 방법**: -1. Chunk 크기 줄이기 - ```java - @Override - protected int getChunkSize() { - return 50; // 기본 100에서 감소 - } - ``` - -2. JVM 힙 메모리 증가 - ```bash - java -Xmx2g -jar snp-batch.jar - ``` - -3. API 페이징 처리 구현 - ---- - -### 7.2 로그 레벨 설정 - -**application.yml**: -```yaml -logging: - level: - com.snp.batch: DEBUG # 배치 애플리케이션 - com.snp.batch.common.batch: INFO # 배치 추상 클래스 (INFO) - com.snp.batch.common.web: INFO # 웹 추상 클래스 (INFO) - org.springframework.batch: INFO # Spring Batch - org.springframework.jdbc.core: DEBUG # SQL 쿼리 - org.springframework.web.reactive: DEBUG # WebClient -``` - ---- - -### 7.3 디버깅 팁 - -1. **Step 실행 상태 확인** - ```sql - SELECT * FROM BATCH_STEP_EXECUTION - WHERE JOB_EXECUTION_ID = {executionId}; - ``` - -2. **Step Context 확인** - ```sql - SELECT * FROM BATCH_STEP_EXECUTION_CONTEXT - WHERE STEP_EXECUTION_ID = {stepExecutionId}; - ``` - -3. **Job Parameter 확인** - ```sql - SELECT * FROM BATCH_JOB_EXECUTION_PARAMS - WHERE JOB_EXECUTION_ID = {executionId}; - ``` - ---- - -## 8. 자주 묻는 질문 (FAQ) - -### Q1: Processor 없이 Reader → Writer만 사용할 수 있나요? -**A**: 네, 가능합니다. `createProcessor()`에서 `null`을 반환하면 됩니다. - -```java -@Override -protected ItemProcessor createProcessor() { - return null; // Processor 없이 Reader → Writer -} -``` - ---- - -### Q2: 여러 개의 Writer를 사용할 수 있나요? -**A**: `CompositeItemWriter`를 사용하면 가능합니다. - -```java -@Override -protected ItemWriter createWriter() { - CompositeItemWriter compositeWriter = new CompositeItemWriter<>(); - compositeWriter.setDelegates(Arrays.asList( - shipDataWriter, - auditLogWriter - )); - return compositeWriter; -} -``` - ---- - -### Q3: API 페이징을 지원하나요? -**A**: 현재 `BaseApiReader`는 단일 호출만 지원합니다. 페이징이 필요한 경우 커스텀 Reader를 구현하세요. - ---- - -### Q4: 스케줄 등록은 어떻게 하나요? -**A**: 웹 UI에서 "스케줄 등록" 버튼을 클릭하여 Cron 표현식을 입력하면 됩니다. - -``` -예제 Cron 표현식: -- 매일 오전 2시: 0 0 2 * * ? -- 매시간: 0 0 * * * ? -- 매주 월요일 오전 9시: 0 0 9 ? * MON -``` - ---- - -### Q5: Job 실행 이력은 어디서 확인하나요? -**A**: 웹 UI의 다음 위치에서 확인할 수 있습니다: -1. 대시보드: 최근 실행 이력 (최근 10건) -2. Job 상세 페이지: 특정 Job의 모든 실행 이력 -3. 타임라인 차트: 일/주/월 단위 시각화 - ---- - -## 9. 추가 리소스 - -### 9.1 공식 문서 -- [Spring Batch 공식 문서](https://docs.spring.io/spring-batch/docs/current/reference/html/) -- [Spring Boot 공식 문서](https://docs.spring.io/spring-boot/docs/current/reference/html/) -- [Quartz Scheduler](http://www.quartz-scheduler.org/documentation/) - -### 9.2 프로젝트 파일 -- `application.yml`: 애플리케이션 설정 -- `schema-postgresql.sql`: 데이터베이스 스키마 -- `BaseApiReader.java`: API Reader 추상 클래스 (src/main/java/com/snp/batch/common/batch/reader/BaseApiReader.java:1) -- `BaseJobConfig.java`: Job Config 추상 클래스 (src/main/java/com/snp/batch/common/batch/config/BaseJobConfig.java:1) -- `BaseJdbcRepository.java`: JDBC Repository 추상 클래스 (src/main/java/com/snp/batch/common/batch/repository/BaseJdbcRepository.java:1) - ---- - ---- - -## 10. 추상화 클래스 체크리스트 - -새로운 배치 Job을 만들 때 다음 체크리스트를 참고하세요. - -### 10.1 필수 구현 항목 - -- [ ] **패키지 구조 생성** - - [ ] `jobs/{domain}/batch/` 디렉토리 생성 (배치 작업) - - [ ] `jobs/{domain}/web/` 디렉토리 생성 (웹 API 필요 시) - -- [ ] **DTO 생성** (`jobs/{domain}/batch/dto/` 패키지) - - [ ] API 응답 DTO (예: `ShipApiResponse`) - - [ ] 데이터 DTO (예: `ShipDto`) - -- [ ] **Entity 생성** (`jobs/{domain}/batch/entity/` 패키지) - - [ ] `BaseEntity` 상속 - - [ ] `@SuperBuilder`, `@NoArgsConstructor`, `@AllArgsConstructor` 추가 - - [ ] `@EqualsAndHashCode(callSuper = true)` 추가 - - [ ] **JPA 어노테이션 사용 금지** (@Entity, @Table, @Column 등) - - [ ] 컬럼 매핑 정보는 주석으로 명시 - -- [ ] **Repository 구현** (`jobs/{domain}/batch/repository/` 패키지) - - [ ] 인터페이스 생성 (예: `ShipRepository`) - - [ ] 구현체 생성 (예: `ShipRepositoryImpl`) - - [ ] `BaseJdbcRepository` 상속 (JDBC 전용) - - [ ] `getTableName()`, `getInsertSql()`, `getUpdateSql()` 구현 - - [ ] `setInsertParameters()`, `setUpdateParameters()` 구현 - - [ ] `getRowMapper()` 구현 (RowMapper 클래스 생성) - - [ ] 커스텀 쿼리 메서드 정의 - -- [ ] **Reader 구현** (`jobs/{domain}/batch/reader/` 패키지) - - [ ] `BaseApiReader` 상속 - - [ ] `getApiPath()` 구현 - - [ ] `extractDataFromResponse()` 구현 - - [ ] `getResponseType()` 구현 - - [ ] `getReaderName()` 구현 - -- [ ] **Processor 구현** (`jobs/{domain}/batch/processor/` 패키지) - - [ ] `BaseProcessor` 상속 - - [ ] `transform()` 구현 (DTO → Entity 변환) - - [ ] `shouldProcess()` 구현 (필터링 로직) - - [ ] `getProcessorName()` 구현 - -- [ ] **Writer 구현** (`jobs/{domain}/batch/writer/` 패키지) - - [ ] `BaseWriter` 상속 - - [ ] `writeItems()` 구현 (Repository 호출) - - [ ] `getWriterName()` 구현 - -- [ ] **JobConfig 구현** (`jobs/{domain}/batch/config/` 패키지) - - [ ] `BaseJobConfig` 상속 - - [ ] `getJobName()` 구현 - - [ ] `createReader()` 구현 - - [ ] `createProcessor()` 구현 - - [ ] `createWriter()` 구현 - - [ ] `getChunkSize()` 구현 (선택사항, 기본값: 100) - - [ ] `@Bean` 메서드로 Job과 Step 등록 - -### 10.2 선택 구현 항목 - -- [ ] **웹 API 구현** (REST API 제공 시) - - [ ] **DTO 생성** (`jobs/{domain}/web/dto/` 패키지) - - [ ] `BaseDto` 상속 (웹 전용 DTO) - - [ ] **Service 구현** (`jobs/{domain}/web/service/` 패키지) - - [ ] `BaseServiceImpl` 상속 (JDBC 기반) - - [ ] `getRepository()` 구현 (배치 Repository 재사용) - - [ ] `toDto()`, `toEntity()` 구현 - - [ ] CRUD 메서드 오버라이드 (필요 시) - - [ ] **Controller 구현** (`jobs/{domain}/web/controller/` 패키지) - - [ ] `BaseController` 상속 - - [ ] `@RequestMapping` 설정 - - [ ] 커스텀 API 추가 (필요 시) - -- [ ] **에러 핸들링** - - [ ] `BaseApiReader.handleApiError()` 오버라이드 - - [ ] `BaseProcessor.handleProcessError()` 오버라이드 - - [ ] `BaseWriter.handleWriteError()` 오버라이드 - -- [ ] **로깅 강화** - - [ ] `beforeApiCall()`, `afterApiCall()` 구현 - - [ ] `beforeProcess()`, `afterProcess()` 구현 - - [ ] `beforeWrite()`, `afterWrite()` 구현 - -### 10.3 테스트 항목 - -- [ ] **단위 테스트** - - [ ] Reader 테스트 (API 모킹) - - [ ] Processor 테스트 (변환 로직 검증) - - [ ] Writer 테스트 (Repository 모킹) - -- [ ] **통합 테스트** - - [ ] Job 실행 테스트 - - [ ] 데이터베이스 저장 검증 - -- [ ] **성능 테스트** - - [ ] 대용량 데이터 처리 테스트 - - [ ] Chunk 크기 최적화 - ---- - ---- - -## 📚 관련 문서 - -### 핵심 문서 -- **[README.md](README.md)** - 프로젝트 개요 및 빠른 시작 가이드 -- **[CLAUDE.md](CLAUDE.md)** - 프로젝트 형상관리 문서 (세션 연속성) -- **[SWAGGER_GUIDE.md](SWAGGER_GUIDE.md)** - Swagger API 문서 사용 가이드 - -### 아키텍처 문서 -- **[docs/architecture/ARCHITECTURE.md](docs/architecture/ARCHITECTURE.md)** - 프로젝트 아키텍처 상세 설계 -- **[docs/architecture/PROJECT_STRUCTURE.md](docs/architecture/PROJECT_STRUCTURE.md)** - Job 중심 패키지 구조 가이드 - -### 구현 가이드 -- **[docs/guides/PROXY_SERVICE_GUIDE.md](docs/guides/PROXY_SERVICE_GUIDE.md)** - 외부 API 프록시 패턴 구현 가이드 -- **[docs/guides/SHIP_API_EXAMPLE.md](docs/guides/SHIP_API_EXAMPLE.md)** - Maritime API 연동 실전 예제 - -### 보안 문서 -- **[docs/security/README.md](docs/security/README.md)** - 보안 전략 개요 (계획 단계) - ---- - -**마지막 업데이트**: 2025-10-16 -**버전**: 1.3.0 - ---- - -## 변경 이력 - -### v1.3.0 (2025-10-16) -- ✅ 프로젝트 구조를 현행화: `common/batch/`와 `common/web/` 분리 반영 -- ✅ jobs 패키지 구조 업데이트: `batch/`와 `web/` 서브패키지 구조 반영 -- ✅ 모든 Base 클래스 위치 경로 수정 (common.base → common.batch/web) -- ✅ JDBC vs JPA 사용 구분 명확화 (jobs는 JDBC 전용, global은 JPA 허용) -- ✅ Entity 예제 업데이트: JPA 어노테이션 제거, 주석 기반 매핑 설명 추가 -- ✅ 체크리스트 강화: 패키지 구조, Repository 구현, 웹 API 구현 세분화 - -### v1.2.0 (2025-10-15) -- 문서 간 상호 참조 링크 추가 -- 관련 문서 섹션 추가 - -### v1.1.0 (2025-10-14) -- 추상화 클래스 체크리스트 추가 -- 예제 코드 개선 - -### v1.0.0 (2025-10-13) -- 초기 버전 작성 diff --git a/SWAGGER_GUIDE.md b/SWAGGER_GUIDE.md deleted file mode 100644 index 5e6f34f..0000000 --- a/SWAGGER_GUIDE.md +++ /dev/null @@ -1,517 +0,0 @@ -# Swagger API 문서화 가이드 - -**작성일**: 2025-10-16 -**버전**: 1.0.0 -**프로젝트**: SNP Batch - Spring Batch 기반 데이터 통합 시스템 - ---- - -## 📋 Swagger 설정 완료 사항 - -### ✅ 수정 완료 파일 -1. **BaseController.java** - 공통 CRUD Controller 추상 클래스 - - Java import alias 오류 수정 (`as SwaggerApiResponse` 제거) - - `@Operation` 어노테이션 내 `responses` 속성으로 통합 - - 전체 경로로 어노테이션 사용: `@io.swagger.v3.oas.annotations.responses.ApiResponse` - -2. **ProductWebController.java** - 샘플 제품 API Controller - - Java import alias 오류 수정 - - 커스텀 엔드포인트 Swagger 어노테이션 수정 - -3. **SwaggerConfig.java** - Swagger/OpenAPI 3.0 설정 - - 서버 포트 동적 설정 (`@Value("${server.port:8081}")`) - - 상세한 API 문서 설명 추가 - - Markdown 형식 설명 추가 - -4. **BatchController.java** - 배치 관리 API (이미 올바르게 구현됨) - ---- - -## 🌐 Swagger UI 접속 정보 - -### 접속 URL -``` -Swagger UI: http://localhost:8081/swagger-ui/index.html -API 문서 (JSON): http://localhost:8081/v3/api-docs -API 문서 (YAML): http://localhost:8081/v3/api-docs.yaml -``` - -### 제공되는 API 그룹 - -> **참고**: BaseController는 추상 클래스이므로 별도의 API 그룹으로 표시되지 않습니다. -> 상속받는 Controller(예: ProductWebController)의 `@Tag`로 모든 CRUD 엔드포인트가 그룹화됩니다. - -#### 1. **Batch Management API** (`/api/batch`) -배치 작업 실행 및 스케줄 관리 - -**엔드포인트**: -- `POST /api/batch/jobs/{jobName}/execute` - 배치 작업 실행 -- `GET /api/batch/jobs` - 배치 작업 목록 조회 -- `GET /api/batch/jobs/{jobName}/executions` - 실행 이력 조회 -- `POST /api/batch/executions/{executionId}/stop` - 실행 중지 -- `GET /api/batch/schedules` - 스케줄 목록 조회 -- `POST /api/batch/schedules` - 스케줄 생성 -- `PUT /api/batch/schedules/{jobName}` - 스케줄 수정 -- `DELETE /api/batch/schedules/{jobName}` - 스케줄 삭제 -- `PATCH /api/batch/schedules/{jobName}/toggle` - 스케줄 활성화/비활성화 -- `GET /api/batch/dashboard` - 대시보드 데이터 -- `GET /api/batch/timeline` - 타임라인 데이터 - -#### 2. **Product API** (`/api/products`) -샘플 제품 데이터 CRUD (BaseController 상속) - -**모든 엔드포인트가 "Product API" 그룹으로 통합 표시됩니다.** - -**공통 CRUD 엔드포인트** (BaseController에서 상속): -- `POST /api/products` - 제품 생성 -- `GET /api/products/{id}` - 제품 조회 (ID) -- `GET /api/products` - 전체 제품 조회 -- `GET /api/products/page?offset=0&limit=20` - 페이징 조회 -- `PUT /api/products/{id}` - 제품 수정 -- `DELETE /api/products/{id}` - 제품 삭제 -- `GET /api/products/{id}/exists` - 존재 여부 확인 - -**커스텀 엔드포인트**: -- `GET /api/products/by-product-id/{productId}` - 제품 코드로 조회 -- `GET /api/products/stats/active-count` - 활성 제품 개수 - ---- - -## 🛠️ 애플리케이션 실행 및 테스트 - -### 1. 애플리케이션 빌드 및 실행 - -```bash -# Maven 빌드 (IntelliJ IDEA에서) -mvn clean package -DskipTests - -# 애플리케이션 실행 -mvn spring-boot:run -``` - -또는 IntelliJ IDEA에서: -1. `SnpBatchApplication.java` 파일 열기 -2. 메인 메서드 왼쪽의 ▶ 아이콘 클릭 -3. "Run 'SnpBatchApplication'" 선택 - -### 2. Swagger UI 접속 - -브라우저에서 다음 URL 접속: -``` -http://localhost:8081/swagger-ui/index.html -``` - -### 3. API 테스트 예시 - -#### 예시 1: 배치 작업 목록 조회 -```http -GET http://localhost:8081/api/batch/jobs -``` - -**예상 응답**: -```json -[ - "sampleProductImportJob", - "shipDataImportJob" -] -``` - -#### 예시 2: 배치 작업 실행 -```http -POST http://localhost:8081/api/batch/jobs/sampleProductImportJob/execute -``` - -**예상 응답**: -```json -{ - "success": true, - "message": "Job started successfully", - "executionId": 1 -} -``` - -#### 예시 3: 제품 생성 (샘플) -```http -POST http://localhost:8081/api/products -Content-Type: application/json - -{ - "productId": "TEST-001", - "productName": "테스트 제품", - "category": "Electronics", - "price": 99.99, - "stockQuantity": 50, - "isActive": true, - "rating": 4.5 -} -``` - -**예상 응답**: -```json -{ - "success": true, - "message": "Product created successfully", - "data": { - "id": 1, - "productId": "TEST-001", - "productName": "테스트 제품", - "category": "Electronics", - "price": 99.99, - "stockQuantity": 50, - "isActive": true, - "rating": 4.5, - "createdAt": "2025-10-16T10:30:00", - "updatedAt": "2025-10-16T10:30:00" - } -} -``` - -#### 예시 4: 페이징 조회 -```http -GET http://localhost:8081/api/products/page?offset=0&limit=10 -``` - -**예상 응답**: -```json -{ - "success": true, - "message": "Retrieved 10 items (total: 100)", - "data": [ - { "id": 1, "productName": "Product 1", ... }, - { "id": 2, "productName": "Product 2", ... }, - ... - ] -} -``` - ---- - -## 📚 Swagger 어노테이션 가이드 - -### BaseController에서 사용된 패턴 - -#### ❌ 잘못된 사용법 (Java에서는 불가능) -```java -// Kotlin의 import alias는 Java에서 지원되지 않음 -import io.swagger.v3.oas.annotations.responses.ApiResponse as SwaggerApiResponse; - -@ApiResponses(value = { - @SwaggerApiResponse(responseCode = "200", description = "성공") -}) -``` - -#### ✅ 올바른 사용법 (수정 완료) -```java -// import alias 제거 -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; - -@Operation( - summary = "리소스 생성", - description = "새로운 리소스를 생성합니다", - responses = { - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "200", - description = "생성 성공" - ), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "500", - description = "서버 오류" - ) - } -) -@PostMapping -public ResponseEntity> create( - @Parameter(description = "생성할 리소스 데이터", required = true) - @RequestBody D dto) { - // ... -} -``` - -### 주요 어노테이션 설명 - -#### 1. `@Tag` - API 그룹화 -```java -@Tag(name = "Product API", description = "제품 관리 API") -public class ProductWebController extends BaseController { - // ... -} -``` - -#### 2. `@Operation` - 엔드포인트 문서화 -```java -@Operation( - summary = "짧은 설명 (목록에 표시)", - description = "상세 설명 (확장 시 표시)", - responses = { /* 응답 정의 */ } -) -``` - -#### 3. `@Parameter` - 파라미터 설명 -```java -@Parameter( - description = "파라미터 설명", - required = true, - example = "예시 값" -) -@PathVariable String id -``` - -#### 4. `@io.swagger.v3.oas.annotations.responses.ApiResponse` - 응답 정의 -```java -@io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "200", - description = "성공 메시지", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ProductDto.class) - ) -) -``` - ---- - -## 🎯 신규 Controller 개발 시 Swagger 적용 가이드 - -### 1. BaseController를 상속하는 경우 - -```java -@RestController -@RequestMapping("/api/myresource") -@RequiredArgsConstructor -@Tag(name = "My Resource API", description = "나의 리소스 관리 API") -public class MyResourceController extends BaseController { - - private final MyResourceService myResourceService; - - @Override - protected BaseService getService() { - return myResourceService; - } - - @Override - protected String getResourceName() { - return "MyResource"; - } - - // BaseController가 제공하는 CRUD 엔드포인트 자동 생성: - // POST /api/myresource - // GET /api/myresource/{id} - // GET /api/myresource - // GET /api/myresource/page - // PUT /api/myresource/{id} - // DELETE /api/myresource/{id} - // GET /api/myresource/{id}/exists - - // 커스텀 엔드포인트 추가 시: - @Operation( - summary = "커스텀 조회", - description = "특정 조건으로 리소스를 조회합니다", - responses = { - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "200", - description = "조회 성공" - ) - } - ) - @GetMapping("/custom/{key}") - public ResponseEntity> customEndpoint( - @Parameter(description = "커스텀 키", required = true) - @PathVariable String key) { - // 구현... - } -} -``` - -### 2. 독립적인 Controller를 작성하는 경우 - -```java -@RestController -@RequestMapping("/api/custom") -@RequiredArgsConstructor -@Slf4j -@Tag(name = "Custom API", description = "커스텀 API") -public class CustomController { - - @Operation( - summary = "커스텀 작업", - description = "특정 작업을 수행합니다", - responses = { - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "200", - description = "작업 성공" - ), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "500", - description = "서버 오류" - ) - } - ) - @PostMapping("/action") - public ResponseEntity> customAction( - @Parameter(description = "액션 파라미터", required = true) - @RequestBody Map params) { - // 구현... - } -} -``` - ---- - -## 🔍 Swagger UI 화면 구성 - -### 메인 화면 -``` -┌─────────────────────────────────────────────────┐ -│ SNP Batch REST API │ -│ Version: v1.0.0 │ -│ Spring Batch 기반 데이터 통합 시스템 REST API │ -├─────────────────────────────────────────────────┤ -│ Servers: │ -│ ▼ http://localhost:8081 (로컬 개발 서버) │ -├─────────────────────────────────────────────────┤ -│ │ -│ ▼ Batch Management API │ -│ POST /api/batch/jobs/{jobName}/execute │ -│ GET /api/batch/jobs │ -│ ... │ -│ │ -│ ▼ Product API (9개 엔드포인트 통합 표시) │ -│ POST /api/products │ -│ GET /api/products/{id} │ -│ GET /api/products │ -│ GET /api/products/page │ -│ PUT /api/products/{id} │ -│ DELETE /api/products/{id} │ -│ GET /api/products/{id}/exists │ -│ GET /api/products/by-product-id/{...} │ -│ GET /api/products/stats/active-count │ -│ │ -│ (Base API 그룹은 표시되지 않음) │ -│ │ -└─────────────────────────────────────────────────┘ -``` - -### API 실행 화면 예시 -각 엔드포인트 클릭 시: -- **Parameters**: 파라미터 입력 필드 -- **Request body**: JSON 요청 본문 에디터 -- **Try it out**: 실제 API 호출 버튼 -- **Responses**: 응답 코드 및 예시 -- **Curl**: curl 명령어 생성 - ---- - -## ⚠️ 문제 해결 - -### 1. Swagger UI 접속 불가 -**증상**: `http://localhost:8081/swagger-ui/index.html` 접속 시 404 오류 - -**해결**: -1. 애플리케이션이 실행 중인지 확인 -2. 포트 번호 확인 (`application.yml`의 `server.port`) -3. 다음 URL 시도: - - `http://localhost:8081/swagger-ui.html` - - `http://localhost:8081/swagger-ui/` - -### 2. API 실행 시 401/403 오류 -**증상**: "Try it out" 클릭 시 인증 오류 - -**해결**: -- 현재 인증이 설정되지 않음 (기본 허용) -- Spring Security 추가 시 Swagger 경로 허용 필요: - ```java - .authorizeHttpRequests(auth -> auth - .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() - .anyRequest().authenticated() - ) - ``` - -### 3. 특정 엔드포인트가 보이지 않음 -**증상**: Controller는 작성했지만 Swagger UI에 표시되지 않음 - -**해결**: -1. `@RestController` 어노테이션 확인 -2. `@RequestMapping` 경로 확인 -3. Controller가 `com.snp.batch` 패키지 하위에 있는지 확인 -4. 애플리케이션 재시작 - ---- - -## 📊 설정 파일 - -### application.yml (Swagger 관련 설정) -```yaml -server: - port: 8081 # Swagger UI 접속 포트 - -# Springdoc OpenAPI 설정 (필요 시 추가) -springdoc: - api-docs: - path: /v3/api-docs # OpenAPI JSON 경로 - swagger-ui: - path: /swagger-ui.html # Swagger UI 경로 - enabled: true - operations-sorter: alpha # 엔드포인트 정렬 (alpha, method) - tags-sorter: alpha # 태그 정렬 -``` - ---- - -## 🎓 추가 학습 자료 - -### Swagger 어노테이션 공식 문서 -- [OpenAPI 3.0 Annotations](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Annotations) -- [Springdoc OpenAPI](https://springdoc.org/) - -### 관련 파일 위치 -``` -src/main/java/com/snp/batch/ -├── common/web/controller/BaseController.java # 공통 CRUD Base -├── global/config/SwaggerConfig.java # Swagger 설정 -├── global/controller/BatchController.java # Batch API -└── jobs/sample/web/controller/ProductWebController.java # Product API -``` - ---- - -## ✅ 체크리스트 - -애플리케이션 실행 전 확인: -- [ ] Maven 빌드 성공 -- [ ] `application.yml` 설정 확인 -- [ ] PostgreSQL 데이터베이스 연결 확인 -- [ ] 포트 8081 사용 가능 여부 확인 - -Swagger 테스트 확인: -- [ ] Swagger UI 접속 성공 -- [ ] Batch Management API 표시 확인 -- [ ] Product API 표시 확인 -- [ ] "Try it out" 기능 동작 확인 -- [ ] API 응답 정상 확인 - ---- - -## 📚 관련 문서 - -### 핵심 문서 -- **[README.md](README.md)** - 프로젝트 개요 및 빠른 시작 가이드 -- **[DEVELOPMENT_GUIDE.md](DEVELOPMENT_GUIDE.md)** - 신규 Job 개발 가이드 및 Base 클래스 사용법 -- **[CLAUDE.md](CLAUDE.md)** - 프로젝트 형상관리 문서 (세션 연속성) - -### 아키텍처 문서 -- **[docs/architecture/ARCHITECTURE.md](docs/architecture/ARCHITECTURE.md)** - 프로젝트 아키텍처 상세 설계 -- **[docs/architecture/PROJECT_STRUCTURE.md](docs/architecture/PROJECT_STRUCTURE.md)** - Job 중심 패키지 구조 가이드 - -### 구현 가이드 -- **[docs/guides/PROXY_SERVICE_GUIDE.md](docs/guides/PROXY_SERVICE_GUIDE.md)** - 외부 API 프록시 패턴 구현 가이드 -- **[docs/guides/SHIP_API_EXAMPLE.md](docs/guides/SHIP_API_EXAMPLE.md)** - Maritime API 연동 실전 예제 - -### 보안 문서 -- **[docs/security/README.md](docs/security/README.md)** - 보안 전략 개요 (계획 단계) - ---- - -**최종 업데이트**: 2025-10-16 -**작성자**: Claude Code -**버전**: 1.1.0