diff --git a/.github/workflows/release-code-deploy.yml b/.github/workflows/release-code-deploy.yml deleted file mode 100644 index 7e45fb48..00000000 --- a/.github/workflows/release-code-deploy.yml +++ /dev/null @@ -1,111 +0,0 @@ -name: Release - Code Deploy with Github Actions - -on: - push: - tags: - - 'v*' - -env: - RESOURCE_DIR: src/main/resources - GCR_PACKAGE_NAME: prod-pfplay-backend-java -jobs: - deploy: - name: Build and dockerize & deploy - runs-on: ubuntu-latest - - defaults: - run: - working-directory: api - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Set env - run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/v}" >> $GITHUB_ENV - - - name: Create directory for resources - run: mkdir -p $RESOURCE_DIR/key - - - name: Set application.yml - env: - PROPERTY_FILE: ${{ secrets.PROD_PROFILE }} - PROPERTY_FILE_NAME: application.yml - run: echo $PROPERTY_FILE | base64 --decode > $RESOURCE_DIR/$PROPERTY_FILE_NAME - - - name: Set JWT private key - env: - JWT_PRIVATE_KEY_FILE: ${{ secrets.JWT_PRIVATE_KEY }} - JWT_PRIVATE_KEY_FILE_NAME: private_key.pem - run: echo $JWT_PRIVATE_KEY_FILE | base64 --decode > $RESOURCE_DIR/key/$JWT_PRIVATE_KEY_FILE_NAME - - - name: Set JWT public key - env: - JWT_PUBLIC_KEY_FILE: ${{ secrets.JWT_PUBLIC_KEY }} - JWT_PUBLIC_KEY_FILE_NAME: public_key.pem - run: echo $JWT_PUBLIC_KEY_FILE | base64 --decode > $RESOURCE_DIR/key/$JWT_PUBLIC_KEY_FILE_NAME - - - name: Set up JDK - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - - name: Set up Git Actions cache - uses: actions/cache@v3 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: Grant execute permission for gradlew - run: chmod +x ./gradlew - - - name: Build with Gradle - run: ./gradlew build -x test - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v1 - with: - registry: ghcr.io - username: JeekLee - password: ${{ secrets.PACKAGE_ACCESS_TOKEN }} - - - name: Build, tag and push image to Github Container Registry - uses: docker/build-push-action@v2 - with: - context: . - file: ./api/Dockerfile-deploy - push: true - tags: | - ghcr.io/pfplay/${{ env.GCR_PACKAGE_NAME }}:latest - ghcr.io/pfplay/${{ env.GCR_PACKAGE_NAME }}:${{ env.RELEASE_VERSION }} - cache-from: type=gha # Refer: https://docs.docker.com/build/ci/github-actions/cache/ - cache-to: type=gha,mode=max - - - name: Pull image from Github registry to GCP VM - uses: appleboy/ssh-action@master - env: - PACKAGE_ACCESS_TOKEN: ${{ secrets.PACKAGE_ACCESS_TOKEN }} - GCR_PACKAGE_NAME: ${{ env.GCR_PACKAGE_NAME }} - with: - host: ${{ secrets.GCP_VM_INSTANCE }} - username: gm - port: 22 - key: ${{ secrets.GCP_VM_SSH_SECRET}} - passphrase: ${{ secrets.GCP_VM_SSH_PASSPHRASE }} - envs: PACKAGE_ACCESS_TOKEN, GCR_PACKAGE_NAME - script: | - docker stop pfplay-api-server && docker rm pfplay-api-server - sudo docker rmi $(docker images | grep "prod-pfplay-backend-java") -f - echo $PACKAGE_ACCESS_TOKEN | docker login ghcr.io -u JeekLee --password-stdin - docker pull ghcr.io/pfplay/$GCR_PACKAGE_NAME:latest - docker run -d --name pfplay-api-server -p 8088:8080 --restart unless-stopped ghcr.io/pfplay/$GCR_PACKAGE_NAME:latest - docker network connect api_backend pfplay-api-server - diff --git a/.gitignore b/.gitignore index 3976554f..ae3166f5 100644 --- a/.gitignore +++ b/.gitignore @@ -33,15 +33,9 @@ build/ # docker db/ -api/src/main/resources/key/private_key.pem -api/src/main/resources/key/public_key.pem -api/src/main/resources/application.yml -api/*.env .env .env.* !.env.example -api/src/main/resources/private_key.pem -api/src/main/resources/public_key.pem # Claude Code .claude/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..fe831fe1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM eclipse-temurin:21-jre-alpine + +COPY app/build/libs/*-SNAPSHOT.jar app.jar + +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/app/Dockerfile b/app/Dockerfile deleted file mode 100644 index fd6e996e..00000000 --- a/app/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -#FROM eclipse-temurin:17-jdk-alpine as build -##FROM --platform=linux/amd64 eclipse-temurin:17-jdk-alpine as build -#WORKDIR /opt/pfplay -#COPY . /opt/pfplay -#RUN --mount=type=cache,target=/root/.gradle ./gradlew clean build -x test --no-daemon -##RUN ./gradlew clean build -x test --refresh-dependencies -#RUN mkdir -p build/dependency && (cd build/dependency; jar -xf ../libs/*-SNAPSHOT.jar) -# -#FROM eclipse-temurin:17-jre-alpine -#WORKDIR /opt/api -#ARG JAR_FILE=/opt/pfplay/build/libs/*.jar -#ENV JAR_NAME=pfplay-api-v1.jar -#COPY --from=build ${JAR_FILE} ${JAR_NAME} -#ENTRYPOINT ["sh", "-c", "java -jar ${JAR_NAME}"] -FROM eclipse-temurin:17-jdk-alpine as build - -ARG SPRING_PROFILES_ACTIVE -ENV SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE} -RUN echo ${SPRING_PROFILES_ACTIVE} - -WORKDIR /opt/pfplay -COPY . . -RUN ./gradlew clean build -x test --refresh-dependencies -#ENTRYPOINT ["sh", "-c", "java -jar ./build/libs/*-SNAPSHOT.jar"] -ENTRYPOINT ["sh", "-c", "java -jar -Dspring.profiles.active=$SPRING_PROFILES_ACTIVE ./build/libs/*-SNAPSHOT.jar"] - diff --git a/app/Dockerfile-deploy b/app/Dockerfile-deploy deleted file mode 100644 index 10521318..00000000 --- a/app/Dockerfile-deploy +++ /dev/null @@ -1,5 +0,0 @@ -FROM eclipse-temurin:17-jdk - -COPY api/build/libs/*-SNAPSHOT.jar app.jar - -ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=dev", "app.jar"] \ No newline at end of file diff --git a/app/src/main/resources/application.yml b/app/src/main/resources/application.yml index df231751..eca6ec76 100644 --- a/app/src/main/resources/application.yml +++ b/app/src/main/resources/application.yml @@ -36,8 +36,8 @@ spring: data: redis: - host: localhost - port: 6379 + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} repositories: enabled: false @@ -59,7 +59,7 @@ springdoc: disable-swagger-default-url: true use-root-path: false path: /spec/api - show-actuator: true + show-actuator: false logging: level: @@ -122,12 +122,40 @@ app: --- -# ๐ŸŸก ๊ฐœ๋ฐœ ํ™˜๊ฒฝ (์ถ”ํ›„ ํ•„์š” ์‹œ ํ™•์žฅ) +# ๐ŸŸก ๊ฐœ๋ฐœ ํ™˜๊ฒฝ spring: config: activate: on-profile: dev + jpa: + show-sql: false + hibernate: + ddl-auto: create + properties: + hibernate: + format_sql: false + + sql: + init: + mode: never + +logging: + level: + org.springframework.security: INFO + org.springframework.web.reactive.function.client: INFO + +server: + error: + include-stacktrace: never + +app: + jwt: + cookie: + domain: ${COOKIE_DOMAIN} + secure: ${COOKIE_SECURE:true} + same-site: ${COOKIE_SAME_SITE:Lax} + --- # ๐Ÿ”ด ์šด์˜ ํ™˜๊ฒฝ @@ -136,14 +164,37 @@ spring: activate: on-profile: prod + jpa: + show-sql: false + hibernate: + ddl-auto: validate + properties: + hibernate: + format_sql: false + + sql: + init: + mode: never + +server: + error: + include-message: never + include-binding-errors: never + include-stacktrace: never + include-exception: false + +logging: + level: + org.springframework.security: WARN + org.springframework.web.reactive.function.client: WARN + app: jwt: cookie: + domain: ${COOKIE_DOMAIN} secure: true same-site: Strict - domain: ${PRODUCTION_DOMAIN} springdoc: - # ์šด์˜์—์„œ๋Š” Swagger ๋น„ํ™œ์„ฑํ™” ๊ถŒ์žฅ swagger-ui: enabled: false diff --git a/build.gradle b/build.gradle index 6e6ad16e..4b37134a 100644 --- a/build.gradle +++ b/build.gradle @@ -43,9 +43,9 @@ subprojects { excludeTags 'integration' } } - maxParallelForks = Math.max(1, Runtime.runtime.availableProcessors().intdiv(4)) + maxParallelForks = 1 // single JVM to maximize Spring Context cache reuse forkEvery = 0 // reuse JVM across tests - jvmArgs '-XX:+UseParallelGC' + jvmArgs '-XX:+UseParallelGC', '-XX:TieredStopAtLevel=1' } tasks.register('integrationTest', Test) { diff --git a/common/src/main/java/com/pfplaybackend/api/common/config/security/SecurityConfig.java b/common/src/main/java/com/pfplaybackend/api/common/config/security/SecurityConfig.java index 6391e467..a8e0528a 100644 --- a/common/src/main/java/com/pfplaybackend/api/common/config/security/SecurityConfig.java +++ b/common/src/main/java/com/pfplaybackend/api/common/config/security/SecurityConfig.java @@ -42,7 +42,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/actuator/health").permitAll() .requestMatchers("/api/v1/admin/**").permitAll() // Admin API - no auth required (temporary) .requestMatchers("/api/**").authenticated() - .anyRequest().permitAll() + .requestMatchers("/ws/**").permitAll() + .requestMatchers("/spec/**").permitAll() + .anyRequest().denyAll() ) .oauth2ResourceServer(oauth2 -> oauth2 .bearerTokenResolver(customBearerTokenResolver) diff --git a/docs/TEST_SPEED_ANALYSIS.md b/docs/TEST_SPEED_ANALYSIS.md new file mode 100644 index 00000000..5ee7fc76 --- /dev/null +++ b/docs/TEST_SPEED_ANALYSIS.md @@ -0,0 +1,272 @@ +# Test Speed Analysis + +## ์ธก์ • ํ™˜๊ฒฝ + +| ํ•ญ๋ชฉ | ๊ฐ’ | +|------|-----| +| OS | Windows 11 Pro | +| CPU ์ฝ”์–ด | 18 | +| JDK | Amazon Corretto 17.0.11 | +| Gradle | ๋นŒ๋“œ ์บ์‹œ + ๋ฐ๋ชฌ + ๋ณ‘๋ ฌ ๋นŒ๋“œ ํ™œ์„ฑํ™” | +| Spring Boot | 3.2.3 | +| ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค ์ˆ˜ | 152๊ฐœ (app 95, user 35, playlist 17, common 5) | + +## ์ธก์ • ๊ฒฐ๊ณผ + +**์ „์ฒด ๋นŒ๋“œ ์‹œ๊ฐ„: ~2๋ถ„** (`clean test --rerun` ๊ธฐ์ค€) + +### ๋ชจ๋“ˆ๋ณ„ `:test` ํƒœ์Šคํฌ ์†Œ์š” ์‹œ๊ฐ„ + +| ๋ชจ๋“ˆ | ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค ์ˆ˜ | `:test` ์‹œ๊ฐ„ | ๋น„๊ณ  | +|------|-----------------|-------------|------| +| **:app** | 95 | **1m 23s** | WebMvcTest 3์ข… + ์ˆœ์ˆ˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ | +| **:user** | 35 | **1m 26s** | WebMvcTest 1์ข… + ์ˆœ์ˆ˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ | +| **:playlist** | 17 | **1m 12s** | WebMvcTest 1์ข… + ์ˆœ์ˆ˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ | +| **:common** | 5 | **38s** | ์ˆœ์ˆ˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋งŒ (Spring Context ์—†์Œ) | + +> ๋ชจ๋“ˆ ๊ฐ„ ์˜์กด ์ˆœ์„œ(common โ†’ realtime/playlist/user โ†’ app)์— ์˜ํ•ด common์ด ๋๋‚œ ํ›„ ๋‚˜๋จธ์ง€๊ฐ€ ์‹คํ–‰๋œ๋‹ค. +> `org.gradle.parallel=true` ๋•๋ถ„์— playlist, user, app์€ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋˜์–ด wall-clock ๊ธฐ์ค€ ~2๋ถ„. + +### ๋А๋ฆฐ ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค Top 10 + +| ์‹œ๊ฐ„ | ๋ชจ๋“ˆ | ํด๋ž˜์Šค | ํ…Œ์ŠคํŠธ ์ˆ˜ | ์œ ํ˜• | +|------|------|--------|----------|------| +| 21.7s | common | ExceptionCreatorTest | 5 | ์ˆœ์ˆ˜ ๋‹จ์œ„ | +| 15.3s | app | AdminDemoServiceTest | 4 | ์ˆœ์ˆ˜ ๋‹จ์œ„ | +| 6.7s | common | GlobalExceptionHandlerTest | 6 | ์ˆœ์ˆ˜ ๋‹จ์œ„ | +| 4.8s | playlist | PlaylistQueryControllerTest | 2 | WebMvcTest | +| 3.7s | playlist | PlaylistCommandControllerTest | 3 | WebMvcTest | +| 3.5s | user | AvatarResourceQueryServiceTest | 4 | ์ˆœ์ˆ˜ ๋‹จ์œ„ | +| 3.3s | playlist | TrackCommandControllerTest | 4 | WebMvcTest | +| 2.7s | user | UserAvatarCommandControllerTest | 2 | WebMvcTest | +| 2.6s | playlist | TrackCommandServiceTest | 20 | ์ˆœ์ˆ˜ ๋‹จ์œ„ | +| 2.4s | user | GuestSignServiceTest | 2 | ์ˆœ์ˆ˜ ๋‹จ์œ„ | + +> "์ˆœ์ˆ˜ ๋‹จ์œ„"๋กœ ๋ถ„๋ฅ˜๋œ ํ…Œ์ŠคํŠธ๊ฐ€ ์ˆ˜ ์ดˆ~20์ดˆ ๊ฑธ๋ฆฌ๋Š” ๊ฒƒ์€ ํ…Œ์ŠคํŠธ ๋กœ์ง ์ž์ฒด๊ฐ€ ์•„๋‹ˆ๋ผ **JVM fork์˜ ์ฒซ ํด๋ž˜์Šค ๋กœ๋”ฉ ๋น„์šฉ**์ด ํฌํ•จ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. + +--- + +## ๋ฌธ์ œ ์›์ธ + +### 1. ๊ณผ๋„ํ•œ JVM Fork โ€” 17๊ฐœ ํ”„๋กœ์„ธ์Šค ์ƒ์„ฑ + +```groovy +// build.gradle (subprojects ๋ธ”๋ก) +maxParallelForks = Math.max(1, Runtime.runtime.availableProcessors().intdiv(4)) +// โ†’ 18์ฝ”์–ด / 4 = maxParallelForks = 4 (๋ชจ๋“ˆ๋‹น) +``` + +`maxParallelForks = 4`์ด๋ฉด Gradle์ด ๋ชจ๋“ˆ๋‹น ์ตœ๋Œ€ 4๊ฐœ์˜ **๋…๋ฆฝ JVM ํ”„๋กœ์„ธ์Šค**๋ฅผ forkํ•œ๋‹ค. +4๊ฐœ ๋ชจ๋“ˆ ร— ์ตœ๋Œ€ 4 fork = **์ตœ๋Œ€ 16๊ฐœ JVM**์ด ๋™์‹œ์— ์ƒ์„ฑ๋˜๋ฉฐ, ์‹ค์ธก 17๊ฐœ๊ฐ€ ๊ด€์ฐฐ๋˜์—ˆ๋‹ค. + +**ํ•ต์‹ฌ ๋ฌธ์ œ**: Spring์˜ ApplicationContext ์บ์‹œ๋Š” JVM ํ”„๋กœ์„ธ์Šค ๋‚ด๋ถ€์—์„œ๋งŒ ์ž‘๋™ํ•œ๋‹ค. +fork๊ฐ€ ๋Š˜์–ด๋‚ ์ˆ˜๋ก ๋™์ผํ•œ `@WebMvcTest` ์„ค์ •์ด๋ผ๋„ **๊ฐ JVM๋งˆ๋‹ค ๋…๋ฆฝ์ ์œผ๋กœ Spring Context๋ฅผ ๋กœ๋”ฉ**ํ•œ๋‹ค. + +์˜ˆ์‹œ: playlist ๋ชจ๋“ˆ(17๊ฐœ ํ…Œ์ŠคํŠธ, `maxParallelForks=4`) +- fork 1: PlaylistQueryControllerTest โ†’ **Context ๋กœ๋”ฉ ~4.8s** + ํ…Œ์ŠคํŠธ ์‹คํ–‰ +- fork 2: TrackCommandControllerTest โ†’ **๋™์ผ Context ์žฌ๋กœ๋”ฉ ~3.3s** + ํ…Œ์ŠคํŠธ ์‹คํ–‰ +- fork 3: PlaylistCommandControllerTest โ†’ **๋™์ผ Context ์žฌ๋กœ๋”ฉ ~3.7s** + ํ…Œ์ŠคํŠธ ์‹คํ–‰ +- fork 4: ๋‚˜๋จธ์ง€ ํ…Œ์ŠคํŠธ + +๋ชจ๋‘ `AbstractPlaylistWebMvcTest`๋ฅผ ๊ณต์œ ํ•˜์ง€๋งŒ, JVM์ด ๋‹ค๋ฅด๋ฏ€๋กœ ์บ์‹œ๊ฐ€ ์ž‘๋™ํ•˜์ง€ ์•Š๋Š”๋‹ค. + +### 2. Spring Context ์„ค์ •์ด 8์ข…์œผ๋กœ ๋ถ„์‚ฐ + +๋ชจ๋“ˆ ์ „์ฒด์—์„œ 8๊ฐœ์˜ ์„œ๋กœ ๋‹ค๋ฅธ Spring ApplicationContext๊ฐ€ ์กด์žฌํ•œ๋‹ค. + +| # | ์œ„์น˜ | ์œ ํ˜• | Context ํ‚ค๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ์„ค์ • | +|---|------|------|--------------------------| +| 1 | app | `@WebMvcTest` | AbstractAdminWebMvcTest โ€” 3๊ฐœ ์ปจํŠธ๋กค๋Ÿฌ, MockBean 5๊ฐœ | +| 2 | app | `@WebMvcTest` | AbstractPartyCommandWebMvcTest โ€” 9๊ฐœ ์ปจํŠธ๋กค๋Ÿฌ, MockBean 9๊ฐœ | +| 3 | app | `@WebMvcTest` | AbstractPartyQueryWebMvcTest โ€” 7๊ฐœ ์ปจํŠธ๋กค๋Ÿฌ, MockBean 7๊ฐœ | +| 4 | app | `@WebMvcTest` | AuthControllerTest โ€” **๋…๋ฆฝ context**, ์ปจํŠธ๋กค๋Ÿฌ 1๊ฐœ | +| 5 | app | `@WebMvcTest` | PartyroomAccessQueryControllerTest โ€” **๋…๋ฆฝ context**, ์ปค์Šคํ…€ SecurityFilterChain | +| 6 | playlist | `@WebMvcTest` | AbstractPlaylistWebMvcTest โ€” 5๊ฐœ ์ปจํŠธ๋กค๋Ÿฌ, MockBean 6๊ฐœ | +| 7 | user | `@WebMvcTest` | AbstractUserWebMvcTest โ€” 8๊ฐœ ์ปจํŠธ๋กค๋Ÿฌ, MockBean 12๊ฐœ | +| 8 | app | `@SpringBootTest` | AbstractIntegrationTest โ€” ์ „์ฒด Context + Testcontainers | + +**Context #4, #5**: ๋‹จ 1๊ฐœ์˜ ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค๋ฅผ ์œ„ํ•ด ๋ณ„๋„ Spring Context๋ฅผ ๋กœ๋”ฉํ•œ๋‹ค. +์ด ๋‘ ๊ฐœ๋งŒ์œผ๋กœ ์ถ”๊ฐ€ ~3~5์ดˆ๊ฐ€ ๋‚ญ๋น„๋œ๋‹ค. + +> ์ฐธ๊ณ : **Context #8 (ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ)**๋Š” `@Tag("integration")`์œผ๋กœ ๊ธฐ๋ณธ `test` ํƒœ์Šคํฌ์—์„œ ์ œ์™ธ๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ +> ์ปค๋ฐ‹ ์ „ ์ผ๋ฐ˜ ํ…Œ์ŠคํŠธ์—๋Š” ์˜ํ–ฅ ์—†์Œ. + +### 3. ์ˆœ์ˆ˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์˜ JVM ์ดˆ๊ธฐํ™” ์˜ค๋ฒ„ํ—ค๋“œ + +common ๋ชจ๋“ˆ์˜ 5๊ฐœ ํ…Œ์ŠคํŠธ๋Š” `@ExtendWith(MockitoExtension.class)`๋งŒ ์‚ฌ์šฉํ•˜๋Š” ์ˆœ์ˆ˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋‹ค. +Spring Context๋ฅผ ๋กœ๋”ฉํ•˜์ง€ ์•Š์Œ์—๋„ **ํ•ฉ๊ณ„ 38์ดˆ**๊ฐ€ ์†Œ์š”๋œ๋‹ค. + +์›์ธ: +- `maxParallelForks=4`์— ์˜ํ•ด 5๊ฐœ ํ…Œ์ŠคํŠธ๊ฐ€ ์ตœ๋Œ€ 4๊ฐœ JVM์— ๋ถ„์‚ฐ โ†’ **JVM ๊ธฐ๋™ ๋น„์šฉ ร— 4** +- ๊ฐ JVM์€ classpath์— Spring Boot + JPA + QueryDSL ๋“ฑ ์ „์ฒด ์˜์กด์„ฑ์„ ๋กœ๋”ฉ +- `common/build.gradle`์˜ `testImplementation 'spring-boot-starter-test'`๋กœ ์ธํ•ด classpath๊ฐ€ ๋ฌด๊ฑฐ์›€ +- ์ฒซ ๋ฒˆ์งธ ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค ์‹คํ–‰ ์‹œ classpath scanning + class loading์— 5~20์ดˆ ์†Œ์š” + +### 4. ๋ชจ๋“ˆ ๊ฐ„ ์ˆœ์ฐจ ์˜์กด์„ฑ + +``` +common (38s) โ†’ [์™„๋ฃŒ ๋Œ€๊ธฐ] โ†’ playlist (1m12s) + โ†’ user (1m26s) โ† ๋ณ‘๋ ฌ + โ†’ app (1m23s) โ† ๋ณ‘๋ ฌ +``` + +`org.gradle.parallel=true`๋กœ ๋ชจ๋“ˆ ๊ฐ„ ๋ณ‘๋ ฌ ์‹คํ–‰์€ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์œผ๋‚˜, +common์ด ์™„๋ฃŒ๋˜์–ด์•ผ ๋‚˜๋จธ์ง€๊ฐ€ ์‹œ์ž‘๋˜๋ฏ€๋กœ **์ตœ์†Œ 38์ดˆ์˜ ์ง๋ ฌ ๋Œ€๊ธฐ**๊ฐ€ ์กด์žฌํ•œ๋‹ค. + +--- + +## ์‹œ๊ฐ„ ๊ตฌ์„ฑ ๋ถ„์„ (์ถ”์ •) + +์ „์ฒด 2๋ถ„์˜ ์‹œ๊ฐ„์ด ์–ด๋””์— ์“ฐ์ด๋Š”์ง€ ๋ถ„ํ•ดํ•œ ์ถ”์ •์น˜: + +| ๊ตฌ๊ฐ„ | ์‹œ๊ฐ„ | ๋น„์œจ | +|------|------|------| +| Gradle ๊ธฐ๋™ + ์˜์กด์„ฑ ํ•ด์„ | ~1s | 1% | +| ์ปดํŒŒ์ผ (์บ์‹œ ํžˆํŠธ) | ~5s | 4% | +| **JVM fork ๊ธฐ๋™ (17๊ฐœ)** | **~50s** | **42%** | +| **Spring Context ๋กœ๋”ฉ (8์ข… ร— fork ์ค‘๋ณต)** | **~40s** | **34%** | +| ์‹ค์ œ ํ…Œ์ŠคํŠธ ๋กœ์ง ์‹คํ–‰ | ~20s | 17% | +| ๊ธฐํƒ€ (jar ํŒจํ‚ค์ง•, I/O) | ~2s | 2% | + +> **ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž์ฒด์˜ ์‹คํ–‰ ์‹œ๊ฐ„์€ ์ „์ฒด์˜ ~17%์— ๋ถˆ๊ณผํ•˜๋‹ค.** +> ๋‚˜๋จธ์ง€ 83%๋Š” JVM ๊ธฐ๋™, classpath ๋กœ๋”ฉ, Spring Context ์ดˆ๊ธฐํ™”์— ์†Œ๋ชจ๋œ๋‹ค. + +--- + +## ํ•ด๊ฒฐ ๋ฐฉ์•ˆ + +### A. maxParallelForks ์กฐ์ • โ€” ์ฆ‰์‹œ ์ ์šฉ ๊ฐ€๋Šฅ, ํšจ๊ณผ ๋†’์Œ + +**ํ˜„์žฌ**: `maxParallelForks = availableProcessors / 4 = 4` +**๊ถŒ์žฅ**: `maxParallelForks = 1` + +```groovy +tasks.named('test') { + maxParallelForks = 1 +} +``` + +**์ด์œ **: fork๊ฐ€ 1๊ฐœ์ด๋ฉด ๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ๋‹จ์ผ JVM์—์„œ ์‹คํ–‰๋˜์–ด: +- JVM ๊ธฐ๋™ ๋น„์šฉ: 17ํšŒ โ†’ 4ํšŒ (๋ชจ๋“ˆ๋‹น 1ํšŒ) +- Spring Context ์บ์‹œ๊ฐ€ JVM ๋‚ด์—์„œ ์™„์ „ํžˆ ์žฌ์‚ฌ์šฉ๋จ +- ๋™์ผ WebMvcTest ์„ค์ •์„ ๊ณต์œ ํ•˜๋Š” ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค๋“ค์ด context๋ฅผ ํ•œ ๋ฒˆ๋งŒ ๋กœ๋”ฉ + +**์˜ˆ์ƒ ํšจ๊ณผ**: JVM fork ๊ธฐ๋™ 50์ดˆ โ†’ ~10์ดˆ, Context ์ค‘๋ณต ๋กœ๋”ฉ 40์ดˆ โ†’ ~15์ดˆ +์ „์ฒด ์‹œ๊ฐ„: **~2๋ถ„ โ†’ ~50์ดˆ** (์•ฝ 50% ๋‹จ์ถ• ์ถ”์ •) + +> `maxParallelForks > 1`์ด ์œ ๋ฆฌํ•œ ๊ฒฝ์šฐ๋Š” ํ…Œ์ŠคํŠธ ์ˆ˜๊ฐ€ ์ˆ˜๋ฐฑ ๊ฐœ์ด๊ณ  ๊ฐœ๋ณ„ ํ…Œ์ŠคํŠธ๊ฐ€ CPU-bound๋กœ ์˜ค๋ž˜ ๊ฑธ๋ฆด ๋•Œ๋ฟ์ด๋‹ค. +> ์ด ํ”„๋กœ์ ํŠธ์ฒ˜๋Ÿผ ํ…Œ์ŠคํŠธ ์ž์ฒด๋Š” ๊ฐ€๋ณ๊ณ  context ๋กœ๋”ฉ์ด ๋ฌด๊ฑฐ์šด ๊ฒฝ์šฐ์—๋Š” fork๋ฅผ ์ค„์ด๋Š” ๊ฒƒ์ด ์œ ๋ฆฌํ•˜๋‹ค. + +### B. ๋…๋ฆฝ Context ํ†ตํ•ฉ โ€” ์ค‘๊ฐ„ ๋…ธ๋ ฅ, ํšจ๊ณผ ์ค‘๊ฐ„ + +`AuthControllerTest`์™€ `PartyroomAccessQueryControllerTest`๊ฐ€ ๊ฐ๊ฐ ๋…๋ฆฝ๋œ `@WebMvcTest`๋ฅผ ์„ ์–ธํ•˜์—ฌ ๋ณ„๋„ context๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. + +**๋ฐฉ์•ˆ**: ์ด ๋‘ ํ…Œ์ŠคํŠธ๋ฅผ ๊ธฐ์กด abstract base์— ํ†ตํ•ฉํ•˜๊ฑฐ๋‚˜, ๊ณตํ†ต abstract base๋ฅผ ๋งŒ๋“ค์–ด context ์ˆ˜๋ฅผ 8 โ†’ 6์œผ๋กœ ์ค„์ธ๋‹ค. + +- `AuthControllerTest` โ†’ `AbstractAuthWebMvcTest` ๋ถ„๋ฆฌ ํ›„ ํ–ฅํ›„ auth ๊ด€๋ จ ์›น ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•  ๊ธฐ๋ฐ˜์œผ๋กœ ํ™œ์šฉ +- `PartyroomAccessQueryControllerTest` โ†’ `AbstractPartyQueryWebMvcTest`์— ์ปค์Šคํ…€ SecurityFilterChain์„ ์กฐ๊ฑด๋ถ€๋กœ ์ ์šฉ + +**์˜ˆ์ƒ ํšจ๊ณผ**: context 2๊ฐœ ์ œ๊ฑฐ โ†’ ~3~5์ดˆ ๋‹จ์ถ• + +### C. JVM ๊ธฐ๋™ ์ตœ์ ํ™” โ€” ์ฆ‰์‹œ ์ ์šฉ ๊ฐ€๋Šฅ, ํšจ๊ณผ ์ค‘๊ฐ„ + +```groovy +tasks.named('test') { + jvmArgs '-XX:+UseParallelGC', + '-XX:TieredStopAtLevel=1', // JIT ์ปดํŒŒ์ผ ์ตœ์†Œํ™” (ํ…Œ์ŠคํŠธ๋Š” ์žฅ๊ธฐ ์‹คํ–‰ ์•„๋‹˜) + '-Xverify:none' // ๋ฐ”์ดํŠธ์ฝ”๋“œ ๊ฒ€์ฆ ์ƒ๋žต (ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ํ•œ์ •) +} +``` + +**์˜ˆ์ƒ ํšจ๊ณผ**: JVM ๊ธฐ๋™ ์‹œ๊ฐ„ ~20% ๋‹จ์ถ• + +> ์ฃผ์˜: `-Xverify:none`์€ Java 13+์—์„œ deprecated. `-XX:TieredStopAtLevel=1`์€ ์•ˆ์ „ํ•˜๊ฒŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ. + +### D. Spring Context ์บ์‹ฑ ๊ทน๋Œ€ํ™”๋ฅผ ์œ„ํ•œ ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์ˆœ์„œ ์ œ์–ด + +Gradle์˜ JUnit Platform์—์„œ๋Š” ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค ์‹คํ–‰ ์ˆœ์„œ๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์—†์œผ๋‚˜, +`forkEvery = 0` (ํ˜„์žฌ ์„ค์ •)์ด context ์บ์‹œ๋ฅผ ๋ณด์กดํ•˜๋ฏ€๋กœ ์ด ์„ค์ •์€ ์œ ์ง€ํ•ด์•ผ ํ•œ๋‹ค. + +> `forkEvery > 0`์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋ฉด N๊ฐœ ํ…Œ์ŠคํŠธ๋งˆ๋‹ค JVM์„ ์žฌ์‹œ์ž‘ํ•˜์—ฌ context ์บ์‹œ๊ฐ€ ํŒŒ๊ดด๋œ๋‹ค. ์ ˆ๋Œ€ ๋ณ€๊ฒฝํ•˜์ง€ ๋ง ๊ฒƒ. + +### E. ํ…Œ์ŠคํŠธ ํƒœ๊ทธ ๊ธฐ๋ฐ˜ ์„ ํƒ์  ์‹คํ–‰ โ€” ์žฅ๊ธฐ ๊ฐœ์„  + +์ปค๋ฐ‹ ์‹œ ์ „์ฒด ํ…Œ์ŠคํŠธ ๋Œ€์‹  ๋ณ€๊ฒฝ๋œ ๋ชจ๋“ˆ์˜ ํ…Œ์ŠคํŠธ๋งŒ ์‹คํ–‰: + +```bash +# ์˜ˆ: playlist ๋ชจ๋“ˆ๋งŒ ๋ณ€๊ฒฝ๋œ ๊ฒฝ์šฐ +./gradlew :playlist:test +``` + +๋˜๋Š” Gradle์˜ `--tests` ์˜ต์…˜์œผ๋กœ ํŠน์ • ํŒจํ„ด๋งŒ ์‹คํ–‰: + +```bash +./gradlew :app:test --tests "*CommandServiceTest" +``` + +Git hook์ด๋‚˜ CI ์Šคํฌ๋ฆฝํŠธ์—์„œ `git diff --name-only`๋กœ ๋ณ€๊ฒฝ๋œ ๋ชจ๋“ˆ์„ ๊ฐ์ง€ํ•˜์—ฌ ์ž๋™ํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค. + +### F. Gradle Build Cache ํ™œ์šฉ ๊ทน๋Œ€ํ™” โ€” ์ด๋ฏธ ํ™œ์„ฑํ™”๋จ + +```properties +# gradle.properties (ํ˜„์žฌ ์„ค์ •) +org.gradle.caching=true +``` + +๋ณ€๊ฒฝ์ด ์—†๋Š” ๋ชจ๋“ˆ์˜ ํ…Œ์ŠคํŠธ๋Š” ์บ์‹œ์—์„œ `FROM-CACHE`/`UP-TO-DATE`๋กœ ์Šคํ‚ต๋œ๋‹ค. +๋‹จ, `clean test`๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ์บ์‹œ๊ฐ€ ๋ฌดํšจํ™”๋˜๋ฏ€๋กœ **`clean` ์—†์ด `test`๋งŒ ์‹คํ–‰**ํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค. + +--- + +## ๊ถŒ์žฅ ์ ์šฉ ์ˆœ์„œ + +| ์ˆœ์„œ | ๋ฐฉ์•ˆ | ๋…ธ๋ ฅ | ์˜ˆ์ƒ ๋‹จ์ถ• | ์œ„ํ—˜๋„ | +|------|------|------|----------|--------| +| 1 | **A. maxParallelForks=1** | 1์ค„ ๋ณ€๊ฒฝ | ~60์ดˆ (50%) | ๋‚ฎ์Œ | +| 2 | **C. JVM ๊ธฐ๋™ ์ตœ์ ํ™”** | 1์ค„ ๋ณ€๊ฒฝ | ~5~10์ดˆ | ๋‚ฎ์Œ | +| 3 | **B. ๋…๋ฆฝ Context ํ†ตํ•ฉ** | ํ…Œ์ŠคํŠธ ๋ฆฌํŒฉํ† ๋ง | ~3~5์ดˆ | ์ค‘๊ฐ„ | +| 4 | **E. ์„ ํƒ์  ์‹คํ–‰** | ์Šคํฌ๋ฆฝํŠธ ์ž‘์„ฑ | ์ƒํ™ฉ์— ๋”ฐ๋ผ | ๋‚ฎ์Œ | + +--- + +## ์ ์šฉ ๊ฒฐ๊ณผ + +### ์ ์šฉํ•œ ๋ฐฉ์•ˆ: A + C + +```groovy +// build.gradle โ€” ๋ณ€๊ฒฝ ์ „ +maxParallelForks = Math.max(1, Runtime.runtime.availableProcessors().intdiv(4)) +jvmArgs '-XX:+UseParallelGC' + +// build.gradle โ€” ๋ณ€๊ฒฝ ํ›„ +maxParallelForks = 1 // single JVM to maximize Spring Context cache reuse +jvmArgs '-XX:+UseParallelGC', '-XX:TieredStopAtLevel=1' +``` + +> `-Xverify:none`์€ Java 13+์—์„œ deprecated์ด๋ฏ€๋กœ ์ ์šฉํ•˜์ง€ ์•Š์•˜๋‹ค. + +### ์ ์šฉํ•˜์ง€ ์•Š์€ ๋ฐฉ์•ˆ: B (๋…๋ฆฝ Context ํ†ตํ•ฉ) + +`AuthControllerTest`๋Š” MockBean ๊ตฌ์„ฑ์ด ๊ธฐ์กด abstract base์™€ ๋‹ค๋ฅด๊ณ , +`PartyroomAccessQueryControllerTest`๋Š” ์ปค์Šคํ…€ `SecurityFilterChain`(permitAll)์ด ํ•„์š”ํ•˜์—ฌ +๊ธฐ์กด base์— ํ†ตํ•ฉํ•˜๋ฉด ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ์˜ ๋ณด์•ˆ ์„ค์ •์— ์˜ํ–ฅ์„ ์ค€๋‹ค. + +A ์ ์šฉ์œผ๋กœ fork๊ฐ€ ๋ชจ๋“ˆ๋‹น 1๊ฐœ๊ฐ€ ๋˜๋ฉด context ์บ์‹œ๊ฐ€ JVM ๋‚ด์—์„œ ์™„์ „ํžˆ ์žฌ์‚ฌ์šฉ๋˜๋ฏ€๋กœ, +๋…๋ฆฝ context๊ฐ€ ์žˆ๋”๋ผ๋„ **๋ชจ๋“ˆ๋‹น 1ํšŒ๋งŒ ๋กœ๋”ฉ**๋œ๋‹ค. ๋ฌด๋ฆฌํ•œ ํ†ตํ•ฉ ์—†์ด๋„ ์ถฉ๋ถ„ํ•œ ํšจ๊ณผ๋ฅผ ์–ป์—ˆ๋‹ค. + +### Before / After ๋น„๊ต + +| ์ง€ํ‘œ | Before | After | ๋ณ€ํ™” | +|------|--------|-------|------| +| **์ „์ฒด ๋นŒ๋“œ ์‹œ๊ฐ„** | **~2๋ถ„** | **~32์ดˆ** | **-73%** | +| JVM fork ์ˆ˜ | 17๊ฐœ | 4๊ฐœ (๋ชจ๋“ˆ๋‹น 1๊ฐœ) | -76% | +| Task Execution (ํ•ฉ์‚ฐ) | 5m 50s | 1m 8s | -81% | + +### ๋ชจ๋“ˆ๋ณ„ `:test` ํƒœ์Šคํฌ Before / After + +| ๋ชจ๋“ˆ | Before | After | ๋ณ€ํ™” | +|------|--------|-------|------| +| **:app** | 1m 23s | **25s** | -70% | +| **:user** | 1m 26s | **13s** | -85% | +| **:playlist** | 1m 12s | **12s** | -83% | +| **:common** | 38s | **5s** | -87% |