diff --git a/README.md b/README.md index 6b68bbf..4b29724 100644 --- a/README.md +++ b/README.md @@ -355,14 +355,20 @@ public class Member extends BaseEntity { ### 11. 데이터베이스 스키마 -**데이터베이스 테이블 구조는 `ddl/schema.sql`에 정의되어 있습니다.** +**데이터베이스 테이블 구조는 `ddl/schema_entity.sql`에 정의되어 있습니다.** #### 스키마 파일 -- **위치**: `ddl/schema.sql` -- **내용**: 모든 테이블의 CREATE TABLE DDL 스크립트 +- **위치**: `ddl/schema_entity.sql` +- **내용**: 모든 엔티티 테이블의 CREATE TABLE DDL 스크립트 + +#### 초기화 스크립트 + +- **위치**: `src/main/resources/schema_initial.sql` +- **내용**: 서버 부팅 시 자동 실행되는 초기화 스크립트 (예: shedlock 테이블 등) #### 사용 방법 -- **자동 생성**: 애플리케이션 시작 시 `schema.sql`로 테이블 자동 생성 +- **자동 생성**: 애플리케이션 시작 시 `schema_entity.sql`로 엔티티 테이블 자동 생성 +- **초기화**: 서버 부팅 시 `schema_initial.sql`로 시스템 테이블 자동 생성 - **설정**: `spring.jpa.hibernate.ddl-auto=none`으로 Hibernate 자동 스키마 생성 비활성화 diff --git a/build.gradle b/build.gradle index 4d6c488..b4e282f 100644 --- a/build.gradle +++ b/build.gradle @@ -73,6 +73,10 @@ dependencies { // SpringDoc OpenAPI (Swagger) implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9' + + // ShedLock for distributed scheduling + implementation 'net.javacrumbs.shedlock:shedlock-spring:5.10.2' + implementation 'net.javacrumbs.shedlock:shedlock-provider-jdbc-template:5.10.2' } tasks.named('test') { diff --git a/ddl/schema.sql b/ddl/schema_entity.sql similarity index 100% rename from ddl/schema.sql rename to ddl/schema_entity.sql diff --git a/src/main/java/com/bio/bio_backend/domain/base/member/scheduler/MemberSyncScheduler.java b/src/main/java/com/bio/bio_backend/domain/base/member/scheduler/MemberSyncScheduler.java new file mode 100644 index 0000000..6942360 --- /dev/null +++ b/src/main/java/com/bio/bio_backend/domain/base/member/scheduler/MemberSyncScheduler.java @@ -0,0 +1,105 @@ +package com.bio.bio_backend.domain.base.member.scheduler; + +import com.bio.bio_backend.domain.base.member.dto.MemberDto; +import com.bio.bio_backend.domain.base.member.service.MemberService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.concurrent.ThreadLocalRandom; + +/** + * 멤버 동기화 스케줄러 + * ShedLock을 사용하여 분산 환경에서 중복 실행을 방지합니다. + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class MemberSyncScheduler { + + private final MemberService memberService; + + /** + * 1시간마다 멤버 동기화를 실행합니다. + * 현재는 더미 데이터를 생성하지만, 추후 실제 조직도 연동으로 변경 예정입니다. + */ + @Scheduled(cron = "0 0 * * * *") // 1시간마다 + @SchedulerLock(name = "memberSync", lockAtMostFor = "50m", lockAtLeastFor = "5m") + public void syncMembersAtTopOfHour() { + log.info("1시간마다 멤버 동기화 시작"); + try { + int createdCount = createDummyMembers(5); // 1시간마다 5명씩 생성 + log.info("1시간마다 멤버 동기화 완료: {}명 생성", createdCount); + } catch (Exception e) { + log.error("1시간마다 멤버 동기화 실패: {}", e.getMessage(), e); + } + } + + /** + * 매일 새벽 2시에 대량 동기화를 실행합니다. + */ + @Scheduled(cron = "0 0 2 * * *") // 매일 새벽 2시 + @SchedulerLock(name = "memberBulkSync", lockAtMostFor = "30m", lockAtLeastFor = "5m") + public void bulkSyncMembers() { + log.info("일일 대량 멤버 동기화 시작"); + try { + int createdCount = createDummyMembers(20); // 새벽에 20명씩 생성 + log.info("일일 대량 멤버 동기화 완료: {}명 생성", createdCount); + } catch (Exception e) { + log.error("일일 대량 멤버 동기화 실패: {}", e.getMessage(), e); + } + } + + /** + * 더미 멤버를 생성합니다. + * 추후 실제 조직도 API 연동으로 변경될 예정입니다. + * + * @param count 생성할 멤버 수 + * @return 실제 생성된 멤버 수 + */ + private int createDummyMembers(int count) { + int createdCount = 0; + + for (int i = 0; i < count; i++) { + try { + String randomSuffix = String.valueOf(System.currentTimeMillis() + i); + + MemberDto memberDto = MemberDto.builder() + .userId("user_" + randomSuffix) + .password("password123") // 실제로는 더 복잡한 패스워드 생성 + .name(generateRandomName()) + .email("user_" + randomSuffix + "@company.com") + .build(); + + memberService.createMember(memberDto); + createdCount++; + + log.debug("더미 멤버 생성 완료: {}", memberDto.getUserId()); + + } catch (Exception e) { + log.warn("더미 멤버 생성 실패 ({}번째): {}", i + 1, e.getMessage()); + // 개별 멤버 생성 실패는 전체 프로세스를 중단하지 않음 + } + } + + return createdCount; + } + + /** + * 랜덤한 이름을 생성합니다. + * 실제 환경에서는 조직도에서 가져온 실제 이름을 사용합니다. + * + * @return 생성된 랜덤 이름 + */ + private String generateRandomName() { + String[] surnames = {"김", "이", "박", "최", "정", "강", "조", "윤", "장", "임"}; + String[] givenNames = {"민수", "지영", "현우", "수진", "태현", "은지", "동훈", "예린", "준호", "서연"}; + + String surname = surnames[ThreadLocalRandom.current().nextInt(surnames.length)]; + String givenName = givenNames[ThreadLocalRandom.current().nextInt(givenNames.length)]; + + return surname + givenName; + } +} diff --git a/src/main/java/com/bio/bio_backend/global/config/SchedulerConfig.java b/src/main/java/com/bio/bio_backend/global/config/SchedulerConfig.java new file mode 100644 index 0000000..f0cc578 --- /dev/null +++ b/src/main/java/com/bio/bio_backend/global/config/SchedulerConfig.java @@ -0,0 +1,38 @@ +package com.bio.bio_backend.global.config; + +import net.javacrumbs.shedlock.core.LockProvider; +import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider; +import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.scheduling.annotation.EnableScheduling; + +import javax.sql.DataSource; + +/** + * 스케줄러 및 분산 락 설정을 위한 Configuration 클래스 + * ShedLock을 사용하여 분산 환경에서 스케줄러 중복 실행을 방지합니다. + */ +@Configuration +@EnableScheduling +@EnableSchedulerLock(defaultLockAtMostFor = "10m") +public class SchedulerConfig { + + /** + * ShedLock용 LockProvider Bean을 생성합니다. + * JDBC 기반으로 분산 락을 관리합니다. + * + * @param dataSource 데이터소스 + * @return JdbcTemplateLockProvider 인스턴스 + */ + @Bean + public LockProvider lockProvider(DataSource dataSource) { + return new JdbcTemplateLockProvider( + JdbcTemplateLockProvider.Configuration.builder() + .withJdbcTemplate(new JdbcTemplate(dataSource)) + .usingDbTime() // DB 시간 사용으로 서버 간 시간차 문제 해결 + .build() + ); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 56a0bd8..f888bc0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -15,7 +15,7 @@ spring.devtools.restart.additional-paths=src/main/java # ======================================== # 데이터베이스 설정 # ======================================== -spring.datasource.url=jdbc:postgresql://stam.kr:15432/imas +spring.datasource.url=jdbc:postgresql://stam.kr:15432/imas?options=-c%20TimeZone=Asia/Seoul spring.datasource.username=imas_user spring.datasource.password=stam1201 spring.datasource.driver-class-name=org.postgresql.Driver @@ -24,6 +24,10 @@ spring.datasource.driver-class-name=org.postgresql.Driver # spring.datasource.username=${DB_USERNAME:} # spring.datasource.password=${DB_PASSWORD:} +# 항상 schema_initial.sql, data.sql 실행 +spring.sql.init.mode=always +spring.sql.init.schema-locations=classpath:schema_initial.sql + # ======================================== # JPA/Hibernate 설정 # ======================================== @@ -43,7 +47,7 @@ spring.jpa.properties.hibernate.order_updates=true # 스키마 생성 설정 spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create -spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=ddl/schema.sql +spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=ddl/schema_entity.sql spring.jpa.properties.hibernate.hbm2ddl.schema-generation.script.append=false # ======================================== diff --git a/src/main/resources/schema_initial.sql b/src/main/resources/schema_initial.sql new file mode 100644 index 0000000..c3bd1e4 --- /dev/null +++ b/src/main/resources/schema_initial.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS shedlock ( + name varchar(64) NOT NULL, + lock_until timestamptz(3) NOT NULL, + locked_at timestamptz(3) NOT NULL, + locked_by varchar(255) NOT NULL, + CONSTRAINT shedlock_pkey PRIMARY KEY (name) +); \ No newline at end of file