diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d41a940 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.gif filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/backend_ci.yml b/.github/workflows/backend_ci.yml index 186fba3..6141285 100644 --- a/.github/workflows/backend_ci.yml +++ b/.github/workflows/backend_ci.yml @@ -41,8 +41,9 @@ jobs: distribution: 'temurin' + # actions/cache@v2 부분을 actions/cache@v3으로 변경 - name: 그래들 캐시 - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | ~/.gradle/caches @@ -51,6 +52,7 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- + - name: Gradle 명령 실행을 위한 권한을 부여합니다 run: chmod +x gradlew @@ -59,8 +61,9 @@ jobs: env: SPRING_PROFILES_ACTIVE: test + # EnricoMi/publish-unit-test-result-action@v1을 최신 버전으로 변경 - name: 테스트 결과를 PR에 코멘트로 등록합니다 - uses: EnricoMi/publish-unit-test-result-action@v1 + uses: EnricoMi/publish-unit-test-result-action@v2 if: always() with: files: '**/build/test-results/test/TEST-*.xml' diff --git a/README.md b/README.md index a50e333..039529a 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,5 @@ +### [AgileHub 바로가기](https://www.agilehub.info) - -## [AgileHub 바로가기](https://www.agilehub.store) - - - -

🚀 애자일 기반 이슈 추적 웹 서비스 🚀 @@ -17,12 +12,11 @@ ## 서비스 소개 - ![image2](https://github.com/AgileHub-DQ/Backend/assets/82764703/1c62b721-3f5c-4ff7-809e-39540faa19af) ![이슈](https://github.com/AgileHub-DQ/Backend/assets/82764703/83517937-9ecb-450e-9e79-9ae8099eb350) ![스프린트](https://github.com/AgileHub-DQ/Backend/assets/82764703/fb6fdd90-acc8-4ae9-bf37-a60e19b91682) -![백로그용GIF](https://github.com/AgileHub-DQ/Backend/assets/82764703/dbb4d960-16e8-41ee-baa2-d6e5abb4a9d5) -![타임라인](https://github.com/AgileHub-DQ/Backend/assets/82764703/7d02b3e1-62d5-48b8-bfae-6197e84f68c4) +![백로그](./backlog.gif) +![타임라인](./timeline.gif) ![멤버](https://github.com/AgileHub-DQ/Backend/assets/82764703/efbe79ed-9f18-4508-9b63-853ac9e217b6) @@ -44,7 +38,7 @@ ## 서비스 요청 흐름도 -스크린샷 2024-01-22 오후 11 44 27 +스크린샷 2024-01-22 오후 11 44 27 ## CI/CD @@ -56,64 +50,96 @@ ## 프로젝트 특징 -### 1. Docker 이미지를 GitHub Actions로 빌드 밎 배포하는 시간 단축 (13분 -> 5분) +### 1. 이슈 생성 API 구현 및 성능 개선 + +[[이슈 #1] 커넥션 풀 고갈 문제를 Redis Atomic 연산으로 개선하기](https://babgeuleus.tistory.com/entry/이슈-1-커넥션-풀-데드락을-Redis-Atomic-연산으로-해결하기) + +- 프로젝트 목표 및 환경 + - 1000명 규모 조직을 가정하여, 100명의 동시 요청 상황에서 TPS 30~40, 응답시간 2초 이내 + - 에러율 0.1% 이하를 목표로 성능테스트 진행 +- 문제 분석 및 원인 파악 + - 테스트 진행 중 20명 동시 요청 시 JPA EntityManager 관련 트랜잭션 오류 발생 + - visualVM 스레드 덤프 분석 결과, handleExisitingTransaction 메소드 호출 시 time Wait 상태 확인 + - 이슈번호 생성 시 REQUIRES_NEW 트랜잭션 분리로 인해, 한 요청당 두 개의 커넥션이 필요해 커넥션 풀이 고갈되는 구조적 문제 발견 +- 개선 전략 및 실행 과정 + - 트랜잭션 구조 변경 + - 기존 비관적 락 기반의 REQUIRES_NEW 분리 방식 대신 단일 커넥션 사용 구조로 통합 + - 단, 비관적 락으로 인해 전체 이슈 생성에 락이 걸려 성능이 저하 + - 락 메커니즘 개선 + - 낙관적 락 + 재시도 전략 적용 + - 하지만 동시 요청 많을 때 충돌과 재시도 증가로 성능 악화 + - Redis 도입 + - Redis INCR 명령어의 단일 스레드/원자성 특징으로 락 없이 동시성 해결 + - Write-behind 전략을 도입하여 업데이트된 이슈번호를 30초마다 스케줄러로 DB에 비동기 반영 + - Redis 장애에 대비해 리플리케이션 센티널 구성으로 고가용성 확보 + - 커넥션 풀 최적화 + - HikaricP connection-timeout 5초 설정 (요청 폭주 시 대기 시간 확보) + - 부하 테스트 결과 기반으로 Connection Pool 200개로 설정 + - MySQL max_connections 500으로 설정 (여유값 확보) + - 최종적으로 목표했던 100 VUser 환경에서 TPS 33-365, 응답시간 2.9초->0.26초 개선 + +### 2. 이메일 초대 시스템 성능 및 안정성 개선 + +[[이슈 #2] 멤버 초대 이메일 발송 설계](https://babgeuleus.tistory.com/entry/%EC%9D%B4%EC%8A%88-2-%EB%A9%A4%EB%B2%84-%EC%B4%88%EB%8C%80-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EB%B0%9C%EC%86%A1-%EC%84%A4%EA%B3%84) + +- 초대 토큰 구현 방식 개선 + - 문제: JWT 사용 시 Base64 디코딩으로 payload 노출 위험 + - 해결: UUID v4 기반 랜덤 토큰 방식 채택 + - 성능 검증: SecureRandom 대비 생성 속도 2배 향상, 100만 건 테스트 시 충돌 없음 +- 이메일 전송 시스템 개선 + - 초기 문제: Gmail SMTP의 낮은 전송 속도, 높은 스팸 필터링율 + - 해결: AWS SES 도입으로 안정적 전송과 도달률 향상 + - 효과: SPF/DKIM 인증으로 스팸 분류 최소화, 네이버메일 지연 문제 해결 +- 초대 코드 저장소 설계 + - Redis 선택 이유 + - TTL 기능으로 10분 유효기간 자동 관리 + - 다중 서버 환경에서 원격 캐시로 활용 + - 메모리 최적화: UUID Base64url 인코딩으로 키-값 크기 256->120바이트 감소 +- 이메일 비동기 발송 및 장애 대응 설계 + - CompletableFuture 기반 비동기 처리로 응답시간 578ms > 130ms로 개선 + - 장애 대응 + - Redis 기반 상태 관리로 중복 전송 방지 (PENDING->SENDING->SENT/FAILED) + - @Retryable로 일시적 장애 시 500ms 간격, 지수 백오프로 재시도 + - ThreadPool 설정 최적화 및 CallerRunsPolicy로 시스템 안정성 확보 + +### 3. Docker 이미지를 GitHub Actions로 빌드 밎 배포하는 시간 단축 (13분 -> 5분) + [배포 하는데 걸리던 시간 13분을 5분으로 줄이기](https://babgeuleus.tistory.com/entry/%EB%B0%B0%ED%8F%AC-%ED%95%98%EB%8A%94%EB%8D%B0-%EA%B1%B8%EB%A6%AC%EB%8D%98-%EC%8B%9C%EA%B0%84-13%EB%B6%84%EC%9D%84-5%EB%B6%84%EC%9C%BC%EB%A1%9C-%EC%A4%84%EC%9D%B4%EA%B8%B0) + - 멀티스테이지 빌드 사용으로 이미지 크기 감소 - Docker 캐싱을 활용하여 빌드 시간 단축 - Gradle 빌드 옵션 최적화 (병렬 빌드 사용 및 테스트 제외) - **성과** - 처음에 이미지 빌드 시 총 700MB였던 것이 적용 후 320MB로 줄어들어, 50% 이상의 용량 최적화달성 - 배포 시간 13분에서 5분으로 단축 (약 62% 감소) - -### 2. GitHub Actions를 활용한 CI/CD 파이프라인 구축 및 보안 문제 해결 -[self-hosted runners를 활용한 CI/CD 파이프라인 구축](https://babgeuleus.tistory.com/entry/CICD-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95-Github-Actions-self-hosted-runners) -
-[GitHub Actions를 활용한 CI/CD 구축](https://babgeuleus.tistory.com/entry/ci-cd) -
-[Jenkins vs GitHub Actions](https://azure-capston.atlassian.net/wiki/x/AYAe) - -- 팀원들의 CI/CD 학습 부담 경감을 위해 GitHub Actions와 Jenkins 비교 및 문서화 -- GitHub Actions의 간편한 워크플로우 구축과 SSH 보안 문제 해결을 위해 self-hosted runners 도입 -### 3. 개발 서버에서 간단한 에러를 신속하게 확인하고 해결 -[Logback 파일 설정](https://babgeuleus.tistory.com/entry/%EC%84%9C%EB%B2%84-%EC%97%90%EB%9F%AC%EB%A5%BC-%EB%B9%A0%EB%A5%B4%EA%B2%8C-%EB%B0%9C%EA%B2%AC%ED%95%98%EA%B3%A0-%ED%95%B4%EA%B2%B0%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-%EB%A1%9C%EA%B7%B8-%EB%B6%84%EC%84%9D%EC%9D%98-%ED%95%B5%EC%8B%AC) -
-[volume경로 잘못된 설정으로 생긴 로그 추출 못하는 문제 해결](https://babgeuleus.tistory.com/entry/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-docker-volume-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%84%A4%EC%A0%95%ED%95%98%EC%9E%90) -
-[모니터링 구축](https://azure-capston.atlassian.net/wiki/x/FoB0Ag) -

-**[문제점]**
-개발 서버에서 발생한 간단한 에러조차 신속하게 확인하고 해결하는 데 어려움
- -**[문제 해결을 위한 접근법]**
-1. **vim nohup.out 실행** - - 로그파일 생성에 예상보다 많은 시간 소요 - - 자주 ssh 세션이 멈추는 문제로 인해 결국 예외가 발생한 부분을 확인하지 못하는 어려움을 겪음 -2. **서버 종료 후 java -jar 명령어로 직접 실행** - - 서버를 종료하고 jar 파일을 직접 실행하여 예외 발생 시 로컬에서 터미널을 통해 바로 확인할 수 있게 해 에러를 해결 -3. **배포를 jar파일에서 docker image로 변경** - - 예외 발생 시 docker logs [컨테이너 ID]를 통해 컨테이너 상태와 로그를 더 쉽게 확인 가능 - - 하지만 이전 에러 기록을 찾는 것이 번거로움 -4. **Logback 파일 설정** - - Prod 서버에서 ERROR 로그 레벨 부터 로그파일 생성 - - 로그 파일의 최대 용량과 보존 기간을 매일 최대 50MB, 최대 7일 보존 기간으로 설정 -5. **로키와 그라파나를 이용한 로그 모니터링** - - 스프링 액추에이터를 이용해 로그와 JVM memory 사용률 추출 - - 프로메테우스로 JVM 메모리 사용률 pull 해서 그라파나로 시각화 - - 프롬테일에서 액추에이터에서 추출한 로그내역 로키에 push, 로키는 pull하여 그라파나로 시각화 - +### 4. DB 구조 리팩토링: 다중 테이블 상속에서 단일 테이블 전략으로 개선 +[[이슈 #3] DB 설계 개선으로 끌어올린 코드 품질과 유지보수성](https://babgeuleus.tistory.com/entry/%EC%9D%B4%EC%8A%88-3-DB-%EC%84%A4%EA%B3%84-%EA%B0%9C%EC%84%A0%EC%9C%BC%EB%A1%9C-%EB%81%8C%EC%96%B4%EC%98%AC%EB%A6%B0-%EC%BD%94%EB%93%9C-%ED%92%88%EC%A7%88%EA%B3%BC-%EC%9C%A0%EC%A7%80%EB%B3%B4%EC%88%98%EC%84%B1) +- 문제사항: 조인 전략을 사용한 이슈 관리 시스템의 복잡성과 성능 저하 문제 해결 +- 해결 방안: 단일 테이블 전략으로 테이블 구조 개선 및 코드 최적화 +- 마이그레이션 과정 + - 데이터 손실 방지를 위한 점진적 트리클 마이그레이션 전략 구현 + - 새 테이블 설계 및 200만 건 이상의 데이터 안전한 이전 + - 읽기 전용 상태 설정으로 데이터 무결성 보장 및 안전한 마이그레이션 +- 성과 + - 마이그레이션 전후 데이터 무결성 검증을 위한 테스트 케이스 구축 + - 코드 복잡성 감소 및 유지보수 용이성 증가 + - 테스트 코드 커버리지 24%에서 42%로 향상

- ## 추가정보 ### 👉 [팀위키](https://azure-capston.atlassian.net/wiki/x/3IAH) -### 👉 [Backend API Swagger Link](https://api.agilehub.store/swagger-ui/index.html) +![img_2.png](img_2.png) + +### 👉 API 문서 + +![img.png](img.png) ### 👉 ERD @@ -132,7 +158,7 @@ ### 👉 멤버 | | | | | -|:-----------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------:| :------------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------------:| -| [BE: 김민상](https://github.com/minsang-alt) | [BE: 최재영](https://github.com/Enble) | [FE: 신승혜](https://github.com/drimh) | [FE: 주원희](https://github.com/wonhee126) | +|:-----------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------------:| +| [BE: 김민상](https://github.com/minsang-alt) | [BE: 최재영](https://github.com/Enble) | [FE: 신승혜](https://github.com/drimh) | [FE: 주원희](https://github.com/wonhee126) | diff --git a/backlog.gif b/backlog.gif new file mode 100644 index 0000000..0317b30 --- /dev/null +++ b/backlog.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8130036730de7060f94471dc9a222e00dd91b93f0817b877e9a942e7d6544bba +size 1529318 diff --git a/img.png b/img.png new file mode 100644 index 0000000..dbfc0c9 Binary files /dev/null and b/img.png differ diff --git a/img_1.png b/img_1.png new file mode 100644 index 0000000..f1c9235 Binary files /dev/null and b/img_1.png differ diff --git a/img_2.png b/img_2.png new file mode 100644 index 0000000..1f377af Binary files /dev/null and b/img_2.png differ diff --git a/src/main/java/dynamicquad/agilehub/dummy/DummyDataLoader.java b/src/main/java/dynamicquad/agilehub/dummy/DummyDataLoader.java index 1346ed7..4ab22bd 100644 --- a/src/main/java/dynamicquad/agilehub/dummy/DummyDataLoader.java +++ b/src/main/java/dynamicquad/agilehub/dummy/DummyDataLoader.java @@ -1,181 +1,133 @@ -//package dynamicquad.agilehub.dummy; -// -//import dynamicquad.agilehub.dummy.bulk.repository.IssueBulkRepository; -//import dynamicquad.agilehub.dummy.bulk.repository.MemberBulkRepository; -//import dynamicquad.agilehub.dummy.bulk.repository.MemberProjectBulkRepository; -//import dynamicquad.agilehub.dummy.bulk.repository.ProjectBulkRepository; -//import dynamicquad.agilehub.issue.domain.Epic; -//import dynamicquad.agilehub.issue.domain.Issue; -//import dynamicquad.agilehub.issue.domain.IssueStatus; -//import dynamicquad.agilehub.issue.domain.Story; -//import dynamicquad.agilehub.issue.domain.Task; -//import dynamicquad.agilehub.member.domain.Member; -//import dynamicquad.agilehub.member.domain.MemberStatus; -//import dynamicquad.agilehub.project.domain.MemberProject; -//import dynamicquad.agilehub.project.domain.MemberProjectRole; -//import dynamicquad.agilehub.project.domain.Project; -//import jakarta.annotation.PostConstruct; -//import java.time.LocalDate; -//import java.util.ArrayList; -//import java.util.List; -//import lombok.RequiredArgsConstructor; -//import lombok.extern.slf4j.Slf4j; -//import org.springframework.context.annotation.Profile; -//import org.springframework.stereotype.Component; -// -//@Component -//@RequiredArgsConstructor -//@Profile("dummy") -//@Slf4j -//public class DummyDataLoader { -// private final ProjectBulkRepository projectBulkRepository; -// private final MemberBulkRepository memberBulkRepository; -// private final MemberProjectBulkRepository memberProjectBulkRepository; -// private final IssueBulkRepository issueBulkRepository; -// -// -// @PostConstruct -// void bulkInsert() { -// -// // project 벌크 실행 -// long startTime = System.currentTimeMillis(); -// -// projectBulk(); -// memberBulk(); -// memberProjectBulk(); -// epicBulk(); -// epicIssueBulk(); -// storyBulk(); -// storyIssueBulk(); -// taskBulk(); -// taskIssueBulk(); -// -// long endTime = System.currentTimeMillis(); -// System.out.println("--------------------"); -// System.out.println("수행시간 : " + (endTime - startTime) + "ms"); -// System.out.println("--------------------"); -// -// } -// -// -// private void projectBulk() { -// List projects = new ArrayList<>(); -// for (long i = 0; i < 10_000L; i++) { -// projects.add(Project.builder() -// .name("프로젝트" + i) -// .key("KEY" + i) -// .build()); -// } -// -// projectBulkRepository.saveAll(projects); -// } -// -// private void memberBulk() { -// List members = new ArrayList<>(); -// for (long i = 0; i < 10_000L; i++) { -// members.add(Member.builder() -// .name("멤버" + i) -// .status(MemberStatus.ACTIVE) -// .build()); -// } -// memberBulkRepository.saveAll(members); -// } -// -// private void memberProjectBulk() { -// // memberProject 벌크 실행 -// List memberProjects = new ArrayList<>(); -// for (long i = 0; i < 10_000L; i++) { -// memberProjects.add(MemberProject.builder() -// .role(MemberProjectRole.ADMIN) -// .build()); -// } -// memberProjectBulkRepository.saveAll(memberProjects); -// } -// -// private void epicBulk() { -// List epics = new ArrayList<>(); -// for (long i = 0; i < 100; i++) { -// epics.add(Epic.builder() -// .title("에픽" + i) -// .content("에픽 내용" + i) -// .number("") -// .status(IssueStatus.DO) -// .startDate(LocalDate.of(2021, 1, 1)) -// .endDate(LocalDate.of(2021, 10, 23)) -// .build()); -// } -// issueBulkRepository.saveEpicAll(epics, 1L, 1L, 1L); -// } -// -// private void epicIssueBulk() { -// // epic-issue 매핑 벌크 실행 -// List epicIssueMappings = new ArrayList<>(); -// for (long i = 0; i < 100; i++) { -// epicIssueMappings.add(Epic.builder() -// .startDate(LocalDate.of(2021, 1, 1)) -// .endDate(LocalDate.of(2021, 10, 23)) -// .build()); -// } -// issueBulkRepository.saveIssueEpicAll(epicIssueMappings); -// } -// -// -// private void storyBulk() { -// List stories = new ArrayList<>(); -// for (long i = 0; i < 100 * 200; i++) { -// stories.add(Story.builder() -// .title("스토리" + i) -// .content("스토리 내용" + i) -// .number("") -// .status(IssueStatus.DO) -// .startDate(LocalDate.of(2021, 1, 1)) -// .endDate(LocalDate.of(2021, 10, 23)) -// .build()); -// } -// issueBulkRepository.saveStoryAll(stories, 1L, 1L, 1L); -// } -// -// private void storyIssueBulk() { -// // story-issue 매핑 벌크 실행 -// for (long i = 0; i < 100; i++) { -// List storiesMappingIssue = new ArrayList<>(); -// for (long j = 0; j < 200; j++) { -// storiesMappingIssue.add(Story.builder() -// .startDate(LocalDate.of(2021, 1, 1)) -// .endDate(LocalDate.of(2021, 10, 23)) -// .build()); -// } -// -// issueBulkRepository.saveIssueStoryAll(storiesMappingIssue, i + 1, 101L + i * 200L); -// } -// } -// -// private void taskBulk() { -// List tasks = new ArrayList<>(); -// for (long i = 0; i < 100 * 200 * 200; i++) { -// tasks.add(Task.builder() -// .title("태스크" + i) -// .content("태스크 내용" + i) -// .number("") -// .status(IssueStatus.DO) -// .build()); -// } -// issueBulkRepository.saveTaskAll(tasks, 1L, 1L, 1L); -// } -// -// private void taskIssueBulk() { -// // task-issue 매핑 벌크 실행 -// for (long i = 0; i < 100 * 200; i++) { -// List tasksMappingIssue = new ArrayList<>(); -// for (long j = 0; j < 200; j++) { -// tasksMappingIssue.add(Task.builder() -// .build()); -// } -// -// issueBulkRepository.saveIssueTaskAll(tasksMappingIssue, 101L + i, 20101L + i * 200L); -// } -// -// } -// -// -//} +package dynamicquad.agilehub.dummy; + +import dynamicquad.agilehub.dummy.bulk.repository.IssueBulkRepository; +import dynamicquad.agilehub.dummy.bulk.repository.MemberBulkRepository; +import dynamicquad.agilehub.dummy.bulk.repository.MemberProjectBulkRepository; +import dynamicquad.agilehub.dummy.bulk.repository.ProjectBulkRepository; +import dynamicquad.agilehub.issue.IssueType; +import dynamicquad.agilehub.issue.domain.Issue; +import dynamicquad.agilehub.issue.domain.IssueStatus; +import dynamicquad.agilehub.member.domain.Member; +import dynamicquad.agilehub.member.domain.MemberStatus; +import dynamicquad.agilehub.project.domain.MemberProject; +import dynamicquad.agilehub.project.domain.MemberProjectRole; +import dynamicquad.agilehub.project.domain.Project; +import jakarta.annotation.PostConstruct; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Profile("dummy") +@Slf4j +public class DummyDataLoader { + private final ProjectBulkRepository projectBulkRepository; + private final MemberBulkRepository memberBulkRepository; + private final MemberProjectBulkRepository memberProjectBulkRepository; + private final IssueBulkRepository issueBulkRepository; + + // 상수 정의 - 더 명확한 이름과 의미 부여 + private static final int PROJECT_COUNT = 10_000; + private static final int PROJECTS_WITH_ISSUES = 10; + private static final int ISSUES_PER_PROJECT = 3_000_000; + private static final LocalDate ISSUE_START_DATE = LocalDate.of(2025, 1, 1); + private static final LocalDate ISSUE_END_DATE = LocalDate.of(2025, 12, 31); + + @PostConstruct + void bulkInsert() { + log.info("더미 데이터 생성 시작"); + long startTime = System.currentTimeMillis(); + + //insertProjects(); + //insertMembers(); + //insertMemberProjects(); + insertIssues(); + + long endTime = System.currentTimeMillis(); + long executionTime = endTime - startTime; + log.info("더미 데이터 생성 완료: 실행 시간 {}ms", executionTime); + } + + private void insertProjects() { + log.info("{}개의 프로젝트 생성 시작", PROJECT_COUNT); + List projects = new ArrayList<>(PROJECT_COUNT); + + for (long i = 0; i < PROJECT_COUNT; i++) { + projects.add(Project.builder() + .name("Project" + i) + .key("KEY" + i) + .build()); + } + + projectBulkRepository.saveAll(projects); + log.info("프로젝트 생성 완료"); + } + + private void insertMembers() { + log.info("{}개의 회원 생성 시작", PROJECT_COUNT); + List members = new ArrayList<>(PROJECT_COUNT); + + for (long i = 0; i < PROJECT_COUNT; i++) { + members.add(Member.builder() + .name("Member" + i) + .status(MemberStatus.ACTIVE) + .build()); + } + + memberBulkRepository.saveAll(members); + log.info("회원 생성 완료"); + } + + private void insertMemberProjects() { + log.info("{}개의 멤버-프로젝트 관계 생성 시작", PROJECT_COUNT); + List memberProjects = new ArrayList<>(PROJECT_COUNT); + + for (long i = 0; i < PROJECT_COUNT; i++) { + memberProjects.add(MemberProject.builder() + .role(MemberProjectRole.ADMIN) + .build()); + } + + memberProjectBulkRepository.saveAll(memberProjects); + log.info("멤버-프로젝트 관계 생성 완료"); + } + + private void insertIssues() { + log.info("{}개 프로젝트에 각 {}개씩 이슈 생성 시작", PROJECTS_WITH_ISSUES, ISSUES_PER_PROJECT); + + insertIssuesForProject(3L); + log.info("프로젝트 ID {}에 대한 이슈 생성 완료", 3L); + + log.info("모든 이슈 생성 완료"); + } + + private void insertIssuesForProject(long projectId) { + + long number = ISSUES_PER_PROJECT; + + for (long i = 0; i < 100; i++) { + List issues = new ArrayList<>(ISSUES_PER_PROJECT); + for (int j = 0; j < number / 100; j++) { + + issues.add(Issue.builder() + .title("Issue" + i) + .content("Issue content" + i) + .number("ISSUE-" + i) + .status(IssueStatus.DO) + .issueType(IssueType.EPIC) + .startDate(ISSUE_START_DATE) + .endDate(ISSUE_END_DATE) + .build()); + } + issueBulkRepository.saveAll(issues, projectId, projectId, projectId); + issues.clear(); // 메모리 사용량 최적화 + } + + + } +} \ No newline at end of file diff --git a/src/main/java/dynamicquad/agilehub/dummy/bulk/repository/IssueBulkRepository.java b/src/main/java/dynamicquad/agilehub/dummy/bulk/repository/IssueBulkRepository.java index d1c87d5..c51c653 100644 --- a/src/main/java/dynamicquad/agilehub/dummy/bulk/repository/IssueBulkRepository.java +++ b/src/main/java/dynamicquad/agilehub/dummy/bulk/repository/IssueBulkRepository.java @@ -1,114 +1,56 @@ -//package dynamicquad.agilehub.dummy.bulk.repository; -// -//import dynamicquad.agilehub.issue.domain.Epic; -//import dynamicquad.agilehub.issue.domain.Issue; -//import dynamicquad.agilehub.issue.domain.Story; -//import dynamicquad.agilehub.issue.domain.Task; -//import java.sql.PreparedStatement; -//import java.util.List; -//import java.util.concurrent.atomic.AtomicLong; -//import lombok.RequiredArgsConstructor; -//import lombok.extern.slf4j.Slf4j; -//import org.springframework.jdbc.core.JdbcTemplate; -//import org.springframework.stereotype.Repository; -//import org.springframework.transaction.annotation.Transactional; -// -//@Repository -//@RequiredArgsConstructor -//@Slf4j -//public class IssueBulkRepository { -// private final JdbcTemplate jdbcTemplate; -// -// @Transactional -// public void saveEpicAll(List issues, Long projectId, Long sprintId, Long memberId) { -// String sql = "INSERT INTO issue (content, issue_type, number, project_id,status, title, member_id) " -// + "VALUES (?,?,?,?,?,?,?)"; -// jdbcTemplate.batchUpdate(sql, issues, issues.size(), -// (PreparedStatement ps, Issue issue) -> { -// ps.setString(1, issue.getContent()); -// ps.setString(2, "EPIC"); -// ps.setString(3, String.valueOf(issue.getNumber())); -// ps.setLong(4, projectId); -// ps.setString(5, String.valueOf(issue.getStatus())); -// ps.setString(6, issue.getTitle()); -// ps.setLong(7, memberId); -// }); -// } -// -// @Transactional -// public void saveIssueEpicAll(List issues) { -// String epicSql = "INSERT INTO epic (issue_id, start_date,end_date) " -// + "VALUES (?, ?,?)"; -// -// AtomicLong index = new AtomicLong(1L); -// jdbcTemplate.batchUpdate(epicSql, issues, issues.size(), -// (PreparedStatement ps, Epic issue) -> { -// ps.setLong(1, index.get()); -// ps.setString(2, String.valueOf(issue.getStartDate())); -// ps.setString(3, String.valueOf(issue.getEndDate())); -// index.getAndIncrement(); -// }); -// } -// -// @Transactional -// public void saveStoryAll(List issues, Long projectId, Long sprintId, Long memberId) { -// String sql = "INSERT INTO issue (content, issue_type, number, project_id, status, title, member_id) " -// + "VALUES (?,?,?,?, ?,?,?)"; -// jdbcTemplate.batchUpdate(sql, issues, issues.size(), -// (PreparedStatement ps, Issue issue) -> { -// ps.setString(1, issue.getContent()); -// ps.setString(2, "STORY"); -// ps.setString(3, String.valueOf(issue.getNumber())); -// ps.setLong(4, projectId); -// ps.setString(5, String.valueOf(issue.getStatus())); -// ps.setString(6, issue.getTitle()); -// ps.setLong(7, memberId); -// }); -// } -// -// @Transactional -// public void saveIssueStoryAll(List issues, Long epicId, Long id) { -// String storySql = "INSERT INTO story (issue_id, epic_id, start_date,end_date) " -// + "VALUES (?,?,?,?)"; -// -// AtomicLong index = new AtomicLong(id); -// jdbcTemplate.batchUpdate(storySql, issues, issues.size(), -// (PreparedStatement ps, Story issue) -> { -// ps.setLong(1, index.get()); -// ps.setLong(2, epicId); -// ps.setString(3, String.valueOf(issue.getStartDate())); -// ps.setString(4, String.valueOf(issue.getEndDate())); -// index.getAndIncrement(); -// }); -// } -// -// @Transactional -// public void saveTaskAll(List issues, Long projectId, Long sprintId, Long memberId) { -// String sql = "INSERT INTO issue (content, issue_type, number, project_id, status, title, member_id) " -// + "VALUES (?,?,?,?,?,?,?)"; -// jdbcTemplate.batchUpdate(sql, issues, issues.size(), -// (PreparedStatement ps, Issue issue) -> { -// ps.setString(1, issue.getContent()); -// ps.setString(2, "TASK"); -// ps.setString(3, String.valueOf(issue.getNumber())); -// ps.setLong(4, projectId); -// ps.setString(5, String.valueOf(issue.getStatus())); -// ps.setString(6, issue.getTitle()); -// ps.setLong(7, memberId); -// }); -// } -// -// @Transactional -// public void saveIssueTaskAll(List issues, Long storyId, Long id) { -// String taskSql = "INSERT INTO task (issue_id, story_id) " -// + "VALUES (?,?)"; -// -// AtomicLong index = new AtomicLong(id); -// jdbcTemplate.batchUpdate(taskSql, issues, issues.size(), -// (PreparedStatement ps, Task issue) -> { -// ps.setLong(1, index.get()); -// ps.setLong(2, storyId); -// index.getAndIncrement(); -// }); -// } -//} +package dynamicquad.agilehub.dummy.bulk.repository; + +import dynamicquad.agilehub.issue.domain.Issue; +import java.sql.PreparedStatement; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +@RequiredArgsConstructor +@Slf4j +public class IssueBulkRepository { + private final JdbcTemplate jdbcTemplate; + + @Transactional + public void saveAll(List issues, Long projectId, Long sprintId, Long memberId) { + String sql = + "INSERT INTO issue_new (content, issue_type, number, project_id, status, title, member_id, created_at,updated_at) " + + "VALUES (?,?,?,?,?,?,?,?,?)"; + jdbcTemplate.batchUpdate(sql, issues, issues.size(), + (PreparedStatement ps, Issue issue) -> { + ps.setString(1, issue.getContent()); + ps.setString(2, issue.getIssueType().name()); + ps.setString(3, issue.getNumber()); + ps.setLong(4, projectId); + ps.setString(5, issue.getStatus().name()); + ps.setString(6, issue.getTitle()); + ps.setLong(7, memberId); + + // created_at 값을 적절히 처리 + LocalDateTime createdAt = issue.getCreatedAt(); + if (createdAt != null) { + ps.setTimestamp(8, Timestamp.valueOf(createdAt)); + } + else { + // 현재 시간으로 설정 + ps.setTimestamp(8, Timestamp.valueOf(LocalDateTime.now())); + } + + // updated_at 값을 적절히 처리 + LocalDateTime updatedAt = issue.getUpdatedAt(); + if (updatedAt != null) { + ps.setTimestamp(9, Timestamp.valueOf(updatedAt)); + } + else { + // 현재 시간으로 설정 + ps.setTimestamp(9, Timestamp.valueOf(LocalDateTime.now())); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/dynamicquad/agilehub/dummy/bulk/repository/MemberProjectBulkRepository.java b/src/main/java/dynamicquad/agilehub/dummy/bulk/repository/MemberProjectBulkRepository.java index e842ccc..dded2ce 100644 --- a/src/main/java/dynamicquad/agilehub/dummy/bulk/repository/MemberProjectBulkRepository.java +++ b/src/main/java/dynamicquad/agilehub/dummy/bulk/repository/MemberProjectBulkRepository.java @@ -16,10 +16,19 @@ public class MemberProjectBulkRepository { @Transactional public void saveAll(List memberProjects) { - String sql = "INSERT INTO member_project (member_id,project_id,role) " - + "VALUES (?, ?,?)"; + // 1. 현재 최대 ID 값을 조회 + Long maxMemberId = jdbcTemplate.queryForObject( + "SELECT COALESCE(MAX(member_id), 0) FROM member_project", Long.class); + Long maxProjectId = jdbcTemplate.queryForObject( + "SELECT COALESCE(MAX(project_id), 0) FROM member_project", Long.class); - AtomicLong index = new AtomicLong(1L); + // 두 ID 중 더 큰 값을 시작점으로 사용 + Long startIndex = Math.max(maxMemberId, maxProjectId) + 1; + + String sql = "INSERT INTO member_project (member_id, project_id, role) " + + "VALUES (?, ?, ?)"; + + AtomicLong index = new AtomicLong(startIndex); jdbcTemplate.batchUpdate(sql, memberProjects, memberProjects.size(), (PreparedStatement ps, MemberProject memberProject) -> { ps.setLong(1, index.get()); @@ -28,4 +37,4 @@ public void saveAll(List memberProjects) { index.getAndIncrement(); }); } -} +} \ No newline at end of file diff --git a/src/main/resources/application-dummy.yml b/src/main/resources/application-dummy.yml index 4341976..d7b4143 100644 --- a/src/main/resources/application-dummy.yml +++ b/src/main/resources/application-dummy.yml @@ -4,12 +4,12 @@ spring: on-profile: dummy datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3307/agilehub?rewriteBatchedStatements=true&profileSQL=true&logger=Slf4JLogger&maxQuerySizeToLog=999999 - username: root - password: test + url: jdbc:mysql://localhost:3306/agilehubdb?rewriteBatchedStatements=true&profileSQL=true&logger=Slf4JLogger&maxQuerySizeToLog=999999 + username: ${MYSQL_USERNAME} + password: ${MYSQL_PASSWORD} jpa: hibernate: - ddl-auto: create + ddl-auto: update properties: hibernate: format_sql: true diff --git a/timeline.gif b/timeline.gif new file mode 100644 index 0000000..8eddd0f --- /dev/null +++ b/timeline.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0117aae7b2b3c58c55a579911789fa7e74169b2d0468fe05221f6558fd3f5686 +size 2299285