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 @@
## 서비스 소개
-



-
-
+
+

@@ -44,7 +38,7 @@
## 서비스 요청 흐름도
-
+
## 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)
+
+
+### 👉 API 문서
+
+
### 👉 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