[송시연] Sprint 8#247
Conversation
# 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
|
| RUN ./gradlew --no-daemon dependencies || true | ||
|
|
||
| COPY src ./src | ||
| RUN ./gradlew --no-daemon clean bootJar -x test |
There was a problem hiding this comment.
멀티 스테이지(amazoncorretto:17 빌더 → amazoncorretto:17-alpine 런타임) 구성이 아주 좋아요. 빌더에서 ./gradlew dependencies를 먼저 실행해 의존성 레이어를 분리하고, 이후 src 복사 및 bootJar를 따로 두신 부분도 의도가 명확해서 캐시 효율이 잘 나올 것 같습니다 👍🏻
| 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"] |
There was a problem hiding this comment.
두 가지 고민 포인트가 있어요.
첫째, JAR 파일명을 ${PROJECT_NAME}-${PROJECT_VERSION}.jar로 조합하고 계신데 build.gradle의 version 값(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 + 환경변수 전개 방식을 고민해보시면 좋을 것 같아요.
| - app-storage:/app/.discodeit/storage | ||
| depends_on: | ||
| db: | ||
| condition: service_healthy |
There was a problem hiding this comment.
db에 pg_isready healthcheck를 걸고 app에서 depends_on: condition: service_healthy로 받는 구조가 아주 좋아요. 단순 depends_on만 쓰면 컨테이너 시작 순서만 보장되고 DB가 실제로 connection을 받을 준비가 됐는지는 보장되지 않는데, healthcheck 기반으로 묶으신 건 운영 관점에서 정석에 가까운 패턴이에요 👍🏻
| ddl-auto: validate | ||
| open-in-view: false | ||
| profiles: | ||
| active: |
There was a problem hiding this comment.
Sprint 7 리뷰에서도 한 번 짚어드렸던 부분인데, 공통 application.yaml에 spring.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} |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
@ConditionalOnProperty로 local/s3를 깔끔하게 분기한 부분과 BinaryContentStorage 추상화를 통해 Controller가 구현체를 모르고 사용하도록 만든 설계가 좋아요. download가 local은 200 + Resource, s3는 302 redirect를 반환하는 차이도 추상 메서드 시그니처가 ResponseEntity<?>라 자연스럽게 흡수되네요 👍🏻
| this.s3Presigner = S3Presigner.builder() | ||
| .region(region) | ||
| .credentialsProvider(credentialsProvider) | ||
| .build(); |
There was a problem hiding this comment.
한 가지 발전적 제안인데, 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 |
There was a problem hiding this comment.
desired-count 0 → wait services-stable → desired-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 |
There was a problem hiding this comment.
S3 도입 이후 binary_contents 테이블의 bytes 컬럼을 주석 처리하신 게 보이는데, 의도적으로 "DB에는 메타데이터만, 실제 바이트는 storage(local/s3)로"로 분리하신 거라면 좋은 설계 결정이에요 👍🏻
다만 주석으로 남겨두는 것보다는 깔끔하게 제거하는 편이 schema의 의도를 더 명확하게 전달할 수 있어요. 혹시 롤백/하위 호환을 고려하신 거라면 그 이유를 주석으로 짧게 남겨두면 더 좋을 것 같아요.
|
머지 컨플릭트가 나고있네요! 해결하신 뒤 디엠 부탁드립니다 :) |
요구사항
기본
심화
주요 변경사항
스크린샷
RDS
ECR
ECS
VPC
IAM
멘토에게