[박정현] Sprint8#258
Conversation
# Conflicts: # src/main/java/com/sprint/mission/discodeit/config/OpenApiConfig.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/binarycontent/response/BinaryContentResponse.java # src/main/java/com/sprint/mission/discodeit/dto/channel/request/PublicChannelCreateRequest.java # src/main/java/com/sprint/mission/discodeit/dto/channel/request/PublicChannelUpdateRequest.java # src/main/java/com/sprint/mission/discodeit/dto/channel/response/ChannelResponse.java # src/main/java/com/sprint/mission/discodeit/dto/message/request/MessageCreateRequest.java # src/main/java/com/sprint/mission/discodeit/dto/message/response/MessageResponse.java # src/main/java/com/sprint/mission/discodeit/dto/readstatus/response/ReadStatusResponse.java # src/main/java/com/sprint/mission/discodeit/dto/user/request/UserCreateRequest.java # src/main/java/com/sprint/mission/discodeit/dto/user/request/UserUpdateRequest.java # src/main/java/com/sprint/mission/discodeit/dto/user/response/UserResponse.java # src/main/java/com/sprint/mission/discodeit/dto/userstatus/UserStatusDto.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/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/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/repository/file/FileMessageRepository.java # src/main/java/com/sprint/mission/discodeit/repository/file/FileReadStatusRepository.java # src/main/java/com/sprint/mission/discodeit/repository/file/FileUserRepository.java # src/main/java/com/sprint/mission/discodeit/repository/file/FileUserStatusRepository.java # src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFMessageRepository.java # src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFReadStatusRepository.java # src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFUserRepository.java # src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFUserStatusRepository.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/resources/static/index.html
- 로깅 패턴과 출력 방식 - 저장 위치
…on`을 잡는 예외 핸들러 추가
- 추후 커스텀 예외로 수정 예정
- 성공/생성 완료 기록은 "시스템에 무슨 일이 발생했는지"를 보여주는 기록이라서 `INFO`가 적합
sprint8 작업 내용 반영 및 GitHub Actions CI 설정 추가
joonfluence
left a comment
There was a problem hiding this comment.
Docker 컨테이너화, AWS S3 BinaryContentStorage 고도화, ECS 배포, GitHub Actions CI/CD 파이프라인 구축이 잘 완성되었습니다. 커스텀 예외 계층 정비와 로깅 개선도 체계적으로 이루어졌습니다.
아래 P2 이슈(HTTP 상태 코드 불일치, Gradle 캐시 step 누락) 수정을 요청드립니다.
| # GitHub Actions가 Gradle build를 더 빠르게 하도록 Gradle 관련 캐시를 설정 | ||
| # 매번 CI/CD 실행 시 의존성이나 Gradle 관련 상태를 처음부터 다시 받지 않도록, | ||
| # 이전에 내려받은 것들을 저장했다가 다음 실행에서 재사용 | ||
| - name: 실행 권한 부여 |
There was a problem hiding this comment.
[p2] Gradle 캐시 step 누락
주석에 "3) Gradle 캐시"라고 명시되어 있으나 실제 캐시 step(uses: gradle/actions/setup-gradle@v5)이 빠져 있습니다. deploy.yml에는 해당 step이 있는데 여기서는 누락되어 매 실행마다 Gradle 의존성을 새로 다운로드하게 됩니다.
| - name: 실행 권한 부여 | |
| - name: Gradle 캐시 | |
| uses: gradle/actions/setup-gradle@v5 | |
| - name: 실행 권한 부여 | |
| run: chmod +x gradlew |
| return switch (errorCode) { | ||
| case INVALID_INPUT, NO_CHANGE_VALUE, LOGIN_FAILED, | ||
| PROFILE_UPLOAD_FAILED, ATTACHMENTS_UPLOAD_FAILED, | ||
| PRIVATE_CHANNEL_PARTICIPANT_REQUIRED, PRIVATE_CHANNEL_CANNOT_BE_UPDATED, |
There was a problem hiding this comment.
[p2] PRIVATE_CHANNEL_CANNOT_BE_UPDATED → 403 FORBIDDEN이 더 적절합니다
이 에러는 요청 형식의 문제가 아니라 권한(인가) 문제입니다. 400 BAD_REQUEST로 처리하면 클라이언트가 "내 요청이 잘못됐구나"로 해석하지만, 실제 의미는 "해당 리소스에 대한 권한이 없다"입니다. 아래 주석처리된 FORBIDDEN 케이스를 활성화하는 것을 권장합니다.
| PRIVATE_CHANNEL_PARTICIPANT_REQUIRED, PRIVATE_CHANNEL_CANNOT_BE_UPDATED, | |
| case INVALID_INPUT, NO_CHANGE_VALUE, LOGIN_FAILED, | |
| PROFILE_UPLOAD_FAILED, ATTACHMENTS_UPLOAD_FAILED, | |
| PRIVATE_CHANNEL_PARTICIPANT_REQUIRED, | |
| DUPLICATED_USER_STATUS, DUPLICATED_USERNAME, DUPLICATED_EMAIL, | |
| DUPLICATED_READ_STATUS -> HttpStatus.BAD_REQUEST; | |
| case PRIVATE_CHANNEL_CANNOT_BE_UPDATED -> HttpStatus.FORBIDDEN; |
| PROFILE_UPLOAD_FAILED, ATTACHMENTS_UPLOAD_FAILED, | ||
| PRIVATE_CHANNEL_PARTICIPANT_REQUIRED, PRIVATE_CHANNEL_CANNOT_BE_UPDATED, | ||
| DUPLICATED_USER_STATUS, DUPLICATED_USERNAME, DUPLICATED_EMAIL, | ||
| DUPLICATED_READ_STATUS -> HttpStatus.BAD_REQUEST; |
There was a problem hiding this comment.
[p3] DUPLICATED_* 에러는 409 CONFLICT가 REST 관례에 더 부합합니다
중복(Duplicated) 에러는 요청 자체가 잘못된 것이 아니라 리소스 충돌 상황입니다. 주석처리된 CONFLICT 케이스가 이미 준비되어 있으니 활성화를 고려해 주세요.
| # `+x` : `chmod` 와 함께 쓰이며, 실행 권한을 추가하는 명령어 | ||
| - name: 실행 권한 부여 | ||
| run: chmod +x gradlew | ||
|
|
There was a problem hiding this comment.
[p3] CI에서 JAR 빌드 후 Docker 내 멀티스테이지에서 또 빌드됩니다
./gradlew clean bootjar로 JAR를 빌드하지만, 이후 docker build가 멀티스테이지 Dockerfile을 실행하므로 Docker 내부에서 소스코드부터 다시 빌드합니다. CI에서 빌드한 JAR는 사용되지 않습니다.
두 가지 방법 중 하나를 선택하세요:
- 방법 A: CI의
build JARstep을 제거 (Docker 멀티스테이지가 알아서 빌드) - 방법 B: Dockerfile을 싱글 스테이지로 변경하고 CI 빌드 JAR만 COPY
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||
| import org.springframework.context.annotation.Configuration; | ||
|
|
||
| @Configuration |
There was a problem hiding this comment.
[p3] @Configuration이 불필요합니다
@Configuration과 @ConfigurationProperties를 함께 사용하면 CGLIB 프록시 대상이 됩니다. @ConfigurationProperties만 사용하고 S3Config에 @EnableConfigurationProperties(AwsProperties.class)를 추가하거나 메인 클래스에 @ConfigurationPropertiesScan을 사용하는 방식이 권장됩니다.
| @Configuration | |
| @ConfigurationProperties(prefix = "discodeit.storage.s3") | |
| @Getter | |
| @Setter | |
| public class AwsProperties { |
| # - name: image tag 확인 | ||
| # run: | | ||
| # echo "IMAGE TAG = [${{ needs.docker-image-build-and-push.outputs.image_tag }}]" | ||
|
|
There was a problem hiding this comment.
[p4] 배포 시 다운타임 발생
desired-count 0으로 기존 태스크를 먼저 종료한 뒤 새 버전을 배포하므로, 그 사이에 서비스 다운타임이 발생합니다. 프리티어 리소스 제약을 고려한 선택으로 이해하지만, 실무에서는 Rolling Update(minimum-healthy-percent: 0, maximum-percent: 100) 방식을 고려할 수 있습니다.
| - "5433:5432" | ||
| volumes: # PostgreSQL 볼륨 구성 | ||
| # 컨테이너 재시작되어도 PostgreSQL 데이터가 유지되도록 설정 | ||
| # - postgres-data:/var/lib/postgresql |
There was a problem hiding this comment.
[p5] 이전 경로 주석 제거 권장
/var/lib/postgresql 경로가 주석으로 남아 있어 혼란을 줄 수 있습니다. 제거를 권장합니다.
Sprint mission 8
1. 프로젝트 마일스톤
BinaryContentStorage고도화 (AWS S3)AWS ECS,RDS)GitHub Actions)1-01. 주요 변경 사항
1) 원활한 실습 진행을 위해
Spring Boot Admin관련 코드는 삭제세부 사항
admin모듈IntelliJ Gradle툴 윈도우에서도 삭제해주세요.spring boot admin의존성 삭제2) 프로젝트 버전이 변경되었습니다.
v1.2-M8세부 사항
1.2:api-doc버전을 따릅니다.M8: 미션 8을 의미합니다.1-02. 과금 안내사항
미션 진행 중 유의사항
이 미션은 AWS 프리티어 할당량 내에서 충분히 수행 가능하도록 설계되었습니다. 불필요한 비용이 발생하지 않도록 미션 요구사항을 사전에 꼼꼼히 확인해 주시기 바랍니다.
미션 종료 후 안내사항
멘토님들의 리뷰가 완료된 후에는 추후에 진행할 실습(미션, 중급 프로젝트 등)을 위해, 이번 미션에서 생성했던 모든 AWS 리소스(EC2, RDS, S3)를 반드시 삭제해 주세요. 리소스를 '중지'하는 것이 아닌 '삭제'해야 추가 비용이 발생하지 않습니다.
2. 기본 요구사항
2-01. 애플리케이션 컨테이너화
Dockerfile 작성
/app).dockerignore를 활용해 제외하세요.80포트를 노출하도록 설정하세요.PROJECT_NAME: discodeitPROJECT_VERSION: 1.2-M8JVM_OPTS: 기본값은 빈 문자열로 정의이미지 build 및 실행 테스트
local)를 지정하세요.prod프로필로 실행하세요.http://localhost:8081로 접속 가능하도록 포트를 매핑하세요.Docker Compose 구성
docker-compose.yml파일을 작성합니다..env파일을 활용하되,.env는 형상관리에서 제외하여 보안을 유지하세요.BinaryContentStorage데이터가 유지되도록 하세요.schema.sql이 자동으로 실행되도록 구성하세요.depends_on).--build플래그를 사용하여 서비스 시작 전에 이미지를 build하도록 합니다.2-02. BinaryContentStorage 고도화 (AWS S3)
AWS S3 버킷 구성
discodeit-binary-content-storage-(사용자 이니셜)형식으로 지정하세요.AWS S3 접근을 위한 IAM 구성
S3 버킷에 접근하기 위한 IAM 사용자(
discodeit)를 생성하세요.AmazonS3FullAccess권한을 할당하고, 사용자 생성을 완료하세요.생성된 사용자에 엑세스 키를 생성하세요.
발급받은 키를 포함해서 AWS 관련 정보는
.env파일에 추가합니다..env파일은 리뷰를 위해 PR에 별도로 첨부해주세요. 단, 엑세스 키와 시크릿 키는 제외하세요.AWS S3 테스트
AWS S3 SDK 의존성을 추가하세요.
implementation 'software.amazon.awssdk:s3:2.31.7'S3 API를 간단하게 테스트하세요.
com.sprint.mission.discodeit.stoarge.s3AWSS3TestProperties클래스를 활용해서.env에 정의한 AWS 정보를 로드하세요.AWS S3를 활용한
BinaryContentStroage고도화앞서 작성한 테스트 메서드를 참고해
S3BinaryContentStorage를 구현하세요.클래스 다이어그램
discodeit.storage.type값이s3인 경우에만 Bean으로 등록되어야 합니다.S3BinaryContentStorageTest를 함께 작성하면서 구현하세요.BinaryContentStorage설정을 유연하게 제어할 수 있도록application.yaml을 수정하세요..env파일에 작성된 값을 임포트하는 방식으로 설정하세요.download메서드는PresignedUrl을 활용해 리다이렉트하는 방식으로 구현하세요.2-03. AWS를 활용한 배포 (AWS RDS, ECR, ECS)
AWS RDS 구성
AWS RDS PostgreSQL 인스턴스를 생성하세요.
과금이 발생할 수 있으니 다음 항목은 한번 더 확인해주세요.
프리티어아니오7일모두 체크 해제비활성화SSH 터널링을 통해 개발 환경에서 접근할 수 있도록 EC2를 구성하세요.
EC2 인스턴스를 생성하세요.
보안 그룹에서 인바운드 규칙을 편집하세요.
SSH내 IPDataGrip을 통해 연결 후 데이터베이스와 사용자, 테이블을 초기화하세요.
데이터 소스 추가 시
SSH/SSL > Use SSH tunnel설정을 활성화하세요. 이때 이전에 다운로드한.pem파일을 활용하세요.연결이 성공하면 데이터베이스와 사용자, 테이블을 초기화하세요.
구성이 완료되면
rds-ssh인스턴스는 완전히 삭제하여 과금에 유의하세요.AWS ECR 구성
이미지를 배포할 퍼블릭 레포지토리(
discodeit)를 생성하세요.AWS CLI를 설치하세요.
aws configure실행 후 앞서 생성한discodeitIAM 사용자 정보를 입력하세요.ap-northeast-2jsondiscodeitIAM 사용자가 ECR에 접근할 수 있도록 다음 권한을 부여하세요.AmazonElasticContainerRegistryPublicFullAccessDocker 클라이언트를 배포할 레지스트리에 대해 인증합니다.
AWS 콘솔을 통해 생성한 레포지토리 페이지로 이동 후 우측 상단
푸시 명령 보기를 클릭하면 관련 명령어를 확인할 수 있습니다.멀티플랫폼을 지원하도록 애플리케이션 이미지를 빌드하고,
discodeit레포지토리에 push 하세요.latest,1.2-M8linux/amd64,linux/arm64AWS 콘솔에서 푸시된 이미지를 확인하세요.
AWS ECS 구성
배포 환경에서 컨테이너 실행 간 사용할 환경 변수를 정의하고, S3에 업로드하세요.
discodeit.env파일을 만들어 다음의 내용을 작성하세요.이 파일을 S3에 업로드하세요.
이 파일은 형상관리되지 않도록 주의하세요.
AWS ECS 콘솔에서 클러스터를 생성하세요.
태스크를 정의하세요.
태스크 실행 역할에 S3 관련 권한을 추가하세요.discodeit클러스터 상세 화면에서 서비스를 생성하세요.태스크의 EC2 보안 그룹의 인바운드 규칙을 설정하여 어디서든 접근할 수 있도록 하세요.
HTTP를 선택하세요.Anywhere-IPv4를 선택하여 모든 IP를 허용하세요.태스크 실행이 완료되면 해당 EC2의 퍼블릭 IP에 접속해보세요.
3. 심화 요구사항
3-01. 이미지 최적화하기
빌드,런타임) 빌드를 활용해 이미지의 크기를 줄여보세요.local-slim1.2-M8또는local)와 크기를 비교해보세요.3-02. GitHub Actions를 활용한 CI/CD 파이프라인 구축
.github/workflows/test.yml파일을 생성하세요.main브랜치에 PR이 생성되면 실행되도록 설정하세요..github/workflows/deploy.yml파일을 생성하세요.release브랜치에 코드가 푸시되면 실행되도록 설정하세요.AWS_ACCESS_KEY: IAM 사용자의 액세스 키AWS_SECRET_KEY: IAM 사용자의 시크릿 키AWS_REGION: AWS 리전(ap-northeast-2)ECR_REPOSITORY_URI: ECR 레포지토리 URIECS_CLUSTER: ECS 클러스터 이름(discodeit-cluster)ECS_SERVICE: ECS 서비스 이름(discodeit-service)ECS_TASK_DEFINITION: ECS 태스크 정의 이름(discodeit-task)us-east-1으로 설정해야합니다.x86_64입니다.latest와 GitHub 커밋 해시를 사용하도록 설정하세요.AWS_REGION으로 설정해야합니다.aws ecs update-service --desired-count옵션을 활용하세요.4. PR에 포함해야할 정보
.env파일 (AWS 키는 제외)RDS
AWS 콘솔 인스턴스 상세 페이지 스크린샷 이미지

SSH 터널링을 통해 연결한 DataGrip 스크린샷 이미지
ECR
ECS
실행 중인 태스크 구성정보가 표시된 AWS 콘솔 페이지 스크린샷 이미지

배포된 EC2 엔드포인트

VPC
IAM