[한성재] 스프린트 미션8#191
Hidden character warning
Conversation
기본 요구사함 프로파일 기반 설정 관리에 맞춘 프로파일 추가
Spring Validation 의존성을 추가
joonfluence
left a comment
There was a problem hiding this comment.
전체 요약
Docker 컨테이너화, AWS S3 BinaryContent 고도화, ECS/ECR 기반 배포, GitHub Actions CI/CD 파이프라인을 구축한 PR입니다. 전반적인 구조는 잘 설계되어 있으나, IAM Role 환경에서 presigned URL 생성 불일치, 사용자 삭제 시 NPE, 배포 중 다운타임 등 수정이 필요한 이슈가 있습니다.
[P4] 커밋에 포함된 불필요한 파일
logs/discodeit.log(768줄) — 런타임 로그 파일이 커밋되었습니다..gitignore에logs/가 추가되었으므로git rm --cached logs/discodeit.log로 추적에서 제외해 주세요.discodeit/.gradle/buildOutputCleanup/— Gradle 빌드 바이너리 파일이 커밋되었습니다..gitignore에 추가하고 제거가 필요합니다.
⚠️ 보안 참고: PR 설명에 실제 RDS 비밀번호(discodeit1234)와 EC2 퍼블릭 IP가 노출되어 있습니다. 해당 자격증명이 실제 운영 환경에서 사용 중이라면 즉시 변경을 권장합니다.
| private String generatePresignedUrl(String key, String contentType) { | ||
| try (S3Presigner presigner = S3Presigner.builder() | ||
| .region(Region.of(region)) | ||
| .credentialsProvider(StaticCredentialsProvider.create( |
There was a problem hiding this comment.
[P1] IAM Role 환경에서 presigned URL 생성 실패 위험
generatePresignedUrl()에서 S3Presigner가 항상 StaticCredentialsProvider(하드코딩 자격증명)를 사용합니다. 그러나 S3Config는 access key가 비어있으면 DefaultCredentialsProvider(IAM Role 등)를 사용하도록 설계되어 있습니다.
ECS에서 IAM Role로 실행 시 awsProperties.getAccessKey()가 빈 값이 되어 presigned URL 생성이 실패합니다. S3Config와 동일한 자격증명 결정 로직을 적용해야 합니다.
| .credentialsProvider(StaticCredentialsProvider.create( | |
| private String generatePresignedUrl(String key, String contentType) { | |
| software.amazon.awssdk.auth.credentials.AwsCredentialsProvider credProvider; | |
| if (accessKey != null && !accessKey.isBlank()) { | |
| credProvider = StaticCredentialsProvider.create( | |
| AwsBasicCredentials.create(accessKey, secretKey)); | |
| } else { | |
| credProvider = software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider.create(); | |
| } | |
| try (S3Presigner presigner = S3Presigner.builder() | |
| .region(Region.of(region)) | |
| .credentialsProvider(credProvider) | |
| .build()) { |
| private final S3Client s3Client; | ||
| private final AwsProperties awsProperties; | ||
|
|
||
| private String accessKey; |
There was a problem hiding this comment.
[P3] AwsProperties 중복 복사 및 미사용 메서드
AwsProperties가 이미 주입되어 있는데, accessKey, secretKey, region, bucket을 별도 인스턴스 필드로 복사하고 있습니다. 또한 68번 줄의 getS3Client() 메서드도 실제로 사용되지 않습니다.
awsProperties.getXxx()를 직접 참조하거나, 불필요한 필드와 메서드를 제거하면 코드가 단순해집니다.
| binaryContentRepository.deleteById(removeUser.getProfile().getId()); | ||
| userRepository.deleteById(removeUser.getId()); | ||
| userStatusRepository.deleteById(userStatus.getId()); | ||
| binaryContentRepository.deleteById(removeUser.getProfile().getId()); |
There was a problem hiding this comment.
[P1] 프로필 없는 사용자 삭제 시 NullPointerException
프로필 이미지가 없는 사용자(profile == null)를 삭제하면 removeUser.getProfile().getId()에서 NPE가 발생합니다. 프로필은 선택적(optional)이므로 null 체크가 필요합니다.
| binaryContentRepository.deleteById(removeUser.getProfile().getId()); | |
| if (removeUser.getProfile() != null) { | |
| binaryContentRepository.deleteById(removeUser.getProfile().getId()); | |
| } |
| container-name: discodeit-app | ||
| image: ${{ vars.ECR_REPOSITORY_URI }}:latest | ||
|
|
||
| - name: Stop and Wait for Task to Terminate |
There was a problem hiding this comment.
[P2] 배포 중 다운타임 발생
desired-count 0으로 서비스를 완전히 중단한 뒤 새 태스크를 배포하므로, "중단 → 배포 → Restart" 사이 구간에 다운타임이 발생합니다.
Stop and Wait 및 Restart Service 스텝을 제거하고 amazon-ecs-deploy-task-definition 액션만으로 롤링 업데이트를 처리하면 무중단 배포가 가능합니다. 단일 태스크 환경이라면 ECS 서비스의 minimumHealthyPercent: 0, maximumPercent: 100 설정으로 제어할 수 있습니다.
| task-definition: ${{ steps.task-def.outputs.task-definition }} | ||
| service: ${{ vars.ECS_SERVICE }} | ||
| cluster: ${{ vars.ECS_CLUSTER }} | ||
| wait-for-service-stability: false |
There was a problem hiding this comment.
[P3] 배포 성공 여부 미확인
배포 후 ECS 서비스 안정화를 기다리지 않으므로, 새 태스크 시작이 실패해도 파이프라인이 성공으로 표시됩니다. true로 변경하면 배포 실패를 즉시 감지할 수 있습니다.
| wait-for-service-stability: false | |
| wait-for-service-stability: true |
| ApiError error = new ApiError("SERVER ERROR", "IllegalArgumentException - " + e.getMessage()); | ||
| return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); | ||
| @ExceptionHandler(Exception.class) | ||
| public ResponseEntity<ErrorResponse> handleException(Exception e) { |
There was a problem hiding this comment.
[P3] catch-all 핸들러에서 예외 미로깅
handleException(Exception e) catch-all 핸들러에서 log.error()가 없어, 예상치 못한 예외 발생 시 스택 트레이스가 로깅되지 않습니다. 운영 환경에서 디버깅이 매우 어려워질 수 있습니다.
| public ResponseEntity<ErrorResponse> handleException(Exception e) { | |
| public ResponseEntity<ErrorResponse> handleException(Exception e) { | |
| log.error("처리되지 않은 예외 발생", e); |
| .body(response); | ||
| } | ||
| // User | ||
| @ExceptionHandler(UserNotFoundException.class) |
There was a problem hiding this comment.
[P3] DiscodeitException 하위 클래스 핸들러 중복
UserNotFoundException, UserAlreadyExistsException, ChannelNotFoundException, PrivateChannelUpdateException, MessageNotFoundException 핸들러가 모두 83번 줄의 handleDiscodeitException과 동일한 로직(ErrorResponse.of(e, status.value()))을 수행합니다.
Spring은 @ExceptionHandler에서 가장 구체적인 타입을 우선 적용하지만, 이 하위 클래스들은 이미 DiscodeitException 핸들러로 처리됩니다. 중복 핸들러를 제거하면 새로운 예외 클래스 추가 시 일관성을 유지하기 쉬워집니다.
요구사항
기본
심화
스크린샷
멘토에게