Skip to content

[송시연] Sprint 8#247

Open
dstle wants to merge 7 commits into
codeit-bootcamp-spring:송시연from
dstle:sprint8
Open

[송시연] Sprint 8#247
dstle wants to merge 7 commits into
codeit-bootcamp-spring:송시연from
dstle:sprint8

Conversation

@dstle
Copy link
Copy Markdown
Collaborator

@dstle dstle commented Apr 7, 2026

요구사항

기본

  • Dockerfile 작성
  • 이미지 빌드 및 실행 테스트
  • Docker Compose 구성
  • AWS S3 버킷 구성
  • AWS S3 접근을 위한 IAM 구성
  • AWS S3 테스트
  • AWS S3를 활용한 BinaryContentStorage 고도화
  • AWS RDS 구성
  • AWS ECR 구성
  • AWS ECS 구성

심화

  • 이미지 최적화하기
  • GitHub Actions를 활용한 CI/CD 파이프라인 구축

주요 변경사항

스크린샷

RDS

image image

ECR

image

ECS

image image

VPC

image image

IAM

image
image

멘토에게

  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.

@dstle dstle self-assigned this Apr 7, 2026
# Conflicts:
#	.gitignore
#	HELP.md
#	api-docs_1.2.json
#	build.gradle
#	gradle/wrapper/gradle-wrapper.jar
#	gradle/wrapper/gradle-wrapper.properties
#	gradlew
#	gradlew.bat
#	src/main/java/com/sprint/mission/discodeit/DiscodeitApplication.java
#	src/main/java/com/sprint/mission/discodeit/controller/AuthController.java
#	src/main/java/com/sprint/mission/discodeit/controller/BinaryContentController.java
#	src/main/java/com/sprint/mission/discodeit/controller/ChannelController.java
#	src/main/java/com/sprint/mission/discodeit/controller/MessageController.java
#	src/main/java/com/sprint/mission/discodeit/controller/ReadStatusController.java
#	src/main/java/com/sprint/mission/discodeit/controller/UserController.java
#	src/main/java/com/sprint/mission/discodeit/dto/response/PageResponse.java
#	src/main/java/com/sprint/mission/discodeit/entity/BinaryContent.java
#	src/main/java/com/sprint/mission/discodeit/entity/Channel.java
#	src/main/java/com/sprint/mission/discodeit/entity/ChannelType.java
#	src/main/java/com/sprint/mission/discodeit/entity/Message.java
#	src/main/java/com/sprint/mission/discodeit/entity/ReadStatus.java
#	src/main/java/com/sprint/mission/discodeit/entity/User.java
#	src/main/java/com/sprint/mission/discodeit/entity/UserStatus.java
#	src/main/java/com/sprint/mission/discodeit/entity/base/BaseEntity.java
#	src/main/java/com/sprint/mission/discodeit/entity/base/BaseUpdatableEntity.java
#	src/main/java/com/sprint/mission/discodeit/mapper/BinaryContentMapper.java
#	src/main/java/com/sprint/mission/discodeit/mapper/ChannelMapper.java
#	src/main/java/com/sprint/mission/discodeit/mapper/MessageMapper.java
#	src/main/java/com/sprint/mission/discodeit/mapper/PageResponseMapper.java
#	src/main/java/com/sprint/mission/discodeit/mapper/ReadStatusMapper.java
#	src/main/java/com/sprint/mission/discodeit/mapper/UserMapper.java
#	src/main/java/com/sprint/mission/discodeit/mapper/UserStatusMapper.java
#	src/main/java/com/sprint/mission/discodeit/repository/BinaryContentRepository.java
#	src/main/java/com/sprint/mission/discodeit/repository/ChannelRepository.java
#	src/main/java/com/sprint/mission/discodeit/repository/MessageRepository.java
#	src/main/java/com/sprint/mission/discodeit/repository/ReadStatusRepository.java
#	src/main/java/com/sprint/mission/discodeit/repository/UserRepository.java
#	src/main/java/com/sprint/mission/discodeit/repository/UserStatusRepository.java
#	src/main/java/com/sprint/mission/discodeit/service/AuthService.java
#	src/main/java/com/sprint/mission/discodeit/service/BinaryContentService.java
#	src/main/java/com/sprint/mission/discodeit/service/ChannelService.java
#	src/main/java/com/sprint/mission/discodeit/service/MessageService.java
#	src/main/java/com/sprint/mission/discodeit/service/ReadStatusService.java
#	src/main/java/com/sprint/mission/discodeit/service/UserService.java
#	src/main/java/com/sprint/mission/discodeit/service/UserStatusService.java
#	src/main/java/com/sprint/mission/discodeit/service/basic/BasicAuthService.java
#	src/main/java/com/sprint/mission/discodeit/service/basic/BasicBinaryContentService.java
#	src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java
#	src/main/java/com/sprint/mission/discodeit/service/basic/BasicMessageService.java
#	src/main/java/com/sprint/mission/discodeit/service/basic/BasicReadStatusService.java
#	src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java
#	src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserStatusService.java
#	src/main/java/com/sprint/mission/discodeit/storage/BinaryContentStorage.java
#	src/main/resources/application.yaml
#	src/main/resources/schema.sql
#	src/test/java/com/sprint/mission/discodeit/service/basic/BasicBinaryContentServiceTest.java
#	src/test/java/com/sprint/mission/discodeit/service/basic/BasicChannelServiceTest.java
#	src/test/java/com/sprint/mission/discodeit/service/basic/BasicMessageServiceTest.java
#	src/test/java/com/sprint/mission/discodeit/service/basic/BasicUserServiceTest.java
#	src/test/java/com/sprint/mission/discodeit/service/basic/BasicUserStatusServiceTest.java
#	src/test/resources/application-test.yaml
@dstle
Copy link
Copy Markdown
Collaborator Author

dstle commented Apr 8, 2026

env-review.txt

# Spring 애플리케이션
SPRING_PROFILES_ACTIVE=prod
SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/discodeit
SPRING_DATASOURCE_USERNAME=discodeit_user
SPRING_DATASOURCE_PASSWORD=discodeit1234

# PostgreSQL 컨테이너
POSTGRES_DB=discodeit
POSTGRES_USER=discodeit_user
POSTGRES_PASSWORD=discodeit1234

# AWS
STORAGE_TYPE=s3
AWS_S3_ACCESS_KEY=
AWS_S3_SECRET_KEY=
AWS_S3_REGION=ap-northeast-2
AWS_S3_BUCKET=discodeit-binary-content-storage-ssy

@dstle dstle requested a review from itstimi-XD April 8, 2026 08:13
Copy link
Copy Markdown
Collaborator

@itstimi-XD itstimi-XD left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이번 미션도 고생하셨습니다!

Comment thread Dockerfile
RUN ./gradlew --no-daemon dependencies || true

COPY src ./src
RUN ./gradlew --no-daemon clean bootJar -x test
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

멀티 스테이지(amazoncorretto:17 빌더 → amazoncorretto:17-alpine 런타임) 구성이 아주 좋아요. 빌더에서 ./gradlew dependencies를 먼저 실행해 의존성 레이어를 분리하고, 이후 src 복사 및 bootJar를 따로 두신 부분도 의도가 명확해서 캐시 효율이 잘 나올 것 같습니다 👍🏻

Comment thread Dockerfile
ENV PROJECT_VERSION=1.2-M8
ENV JVM_OPTS=""

ENTRYPOINT ["sh", "-c", "java $JVM_OPTS -jar /app/libs/${PROJECT_NAME}-${PROJECT_VERSION}.jar --server.port=80"]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

두 가지 고민 포인트가 있어요.

첫째, JAR 파일명을 ${PROJECT_NAME}-${PROJECT_VERSION}.jar로 조합하고 계신데 build.gradleversion 값(1.2-M8)이 바뀌면 ENV의 PROJECT_VERSION도 같이 바꿔야 깨지지 않아요. 빌드 단계에서 mv로 고정 이름(예: app.jar)으로 통일하거나, 와일드카드(/app/libs/*.jar)로 처리하면 버전 동기화 부담을 덜 수 있을 것 같아요.

둘째, ENTRYPOINT ["sh", "-c", "..."] 쉘 형식이라 컨테이너 PID 1이 sh가 되고 java가 자식 프로세스가 돼요. 이 경우 ECS/Compose가 보내는 SIGTERM이 자바 프로세스로 전달되지 않아 graceful shutdown이 안 될 수 있는데, exec java ... 형태로 감싸거나 exec form + 환경변수 전개 방식을 고민해보시면 좋을 것 같아요.

Comment thread docker-compose.yml
- app-storage:/app/.discodeit/storage
depends_on:
db:
condition: service_healthy
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dbpg_isready healthcheck를 걸고 app에서 depends_on: condition: service_healthy로 받는 구조가 아주 좋아요. 단순 depends_on만 쓰면 컨테이너 시작 순서만 보장되고 DB가 실제로 connection을 받을 준비가 됐는지는 보장되지 않는데, healthcheck 기반으로 묶으신 건 운영 관점에서 정석에 가까운 패턴이에요 👍🏻

ddl-auto: validate
open-in-view: false
profiles:
active:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sprint 7 리뷰에서도 한 번 짚어드렸던 부분인데, 공통 application.yamlspring.profiles.active: dev가 그대로 남아있어요. 이렇게 두면 prod 환경에서 컨테이너를 띄울 때 SPRING_PROFILES_ACTIVE=prod를 명시적으로 넘기지 않으면 의도치 않게 dev로 뜨는 경우가 생길 수 있어요.

Dockerfile/ECS task definition에서 SPRING_PROFILES_ACTIVE를 어떻게 주입하고 계신지, 그리고 이 기본값을 유지하는 의도가 있으신지 한 번 더 확인해보시면 좋을 것 같아요.

secret-key: ${AWS_S3_SECRET_KEY:}
region: ${AWS_S3_REGION:ap-northeast-2}
bucket: ${AWS_S3_BUCKET:}
presigned-url-expiration: ${AWS_S3_PRESIGNED_URL_EXPIRATION:600}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

presigned-url-expiration을 숫자만 받아서 S3BinaryContentStorage에서 Duration.ofSeconds(...)로 변환하고 계시네요. 현재 기본값 600이면 10분이라 합리적이긴 한데, 단위가 코드를 봐야만 드러나는 구조라 헷갈릴 여지가 있어요.

프로퍼티 타입을 Duration으로 받고 yaml에 10m/PT10M처럼 ISO 표기로 적으면 단위가 설정에 명시돼서 더 안전해요. Sprint 7 때 MDC 키 이름 일관성 챙기셨던 것처럼, 설정값 단위도 같은 결의 디테일이라 한 번 고민해보시면 좋을 것 같아요.

import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;

@ConditionalOnProperty(name = "discodeit.storage.type", havingValue = "s3")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ConditionalOnProperty로 local/s3를 깔끔하게 분기한 부분과 BinaryContentStorage 추상화를 통해 Controller가 구현체를 모르고 사용하도록 만든 설계가 좋아요. download가 local은 200 + Resource, s3는 302 redirect를 반환하는 차이도 추상 메서드 시그니처가 ResponseEntity<?>라 자연스럽게 흡수되네요 👍🏻

this.s3Presigner = S3Presigner.builder()
.region(region)
.credentialsProvider(credentialsProvider)
.build();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

한 가지 발전적 제안인데, S3Client/S3Presigner를 생성자 안에서 직접 빌드하고 있어서 단위 테스트에서 mocking하기가 까다로워요. Sprint 7에서 서비스 단위 테스트를 Mock 기반으로 깔끔하게 전환하셨던 것처럼, S3Client/S3Presigner@Bean으로 빼서 생성자 주입을 받으면 테스트 격리와 재사용성 양쪽에서 유리해요.

S3Config가 이미 있으니 거기로 옮기는 흐름이 자연스러울 것 같습니다.

ECS_CLUSTER: ${{ vars.ECS_CLUSTER }}
ECS_SERVICE: ${{ vars.ECS_SERVICE }}
run: |
aws ecs update-service --cluster $ECS_CLUSTER --service $ECS_SERVICE --desired-count 0
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

desired-count 0wait services-stabledesired-count 1 흐름으로 새 task definition을 안전하게 띄우신 점이 좋아요. 프리티어(EC2 launch type, 단일 인스턴스) 환경에서는 rolling update가 어려우니 이 패턴이 현실적이에요.

다만 이 방식은 약 1~2분의 다운타임이 발생하는데, 의도하신 트레이드오프인지 확인차 남겨드려요.

file_name varchar(255) NOT NULL,
size bigint NOT NULL,
content_type varchar(100) NOT NULL
-- ,bytes bytea NOT NULL
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

S3 도입 이후 binary_contents 테이블의 bytes 컬럼을 주석 처리하신 게 보이는데, 의도적으로 "DB에는 메타데이터만, 실제 바이트는 storage(local/s3)로"로 분리하신 거라면 좋은 설계 결정이에요 👍🏻

다만 주석으로 남겨두는 것보다는 깔끔하게 제거하는 편이 schema의 의도를 더 명확하게 전달할 수 있어요. 혹시 롤백/하위 호환을 고려하신 거라면 그 이유를 주석으로 짧게 남겨두면 더 좋을 것 같아요.

@itstimi-XD
Copy link
Copy Markdown
Collaborator

머지 컨플릭트가 나고있네요! 해결하신 뒤 디엠 부탁드립니다 :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants