Skip to content

[한성재] 스프린트 미션8#191

Open
seonghj wants to merge 58 commits into
codeit-bootcamp-spring:한성재from
seonghj:한성재_release

Hidden character warning

The head ref may contain hidden characters: "\ud55c\uc131\uc7ac_release"
Open

[한성재] 스프린트 미션8#191
seonghj wants to merge 58 commits into
codeit-bootcamp-spring:한성재from
seonghj:한성재_release

Conversation

@seonghj
Copy link
Copy Markdown
Collaborator

@seonghj seonghj commented Apr 19, 2026

요구사항

기본

  • 애플리케이션 컨테이너화
  • BinaryContentStorage 고도화
  • AWS를 활용한 배포

심화

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

스크린샷

  • RDS
1 2
  • ECR
3
  • ECS
4
  • 보안 그룹
5
  • IAM 권한 정책
6
  • .env 파일
# Spring Configuration
SPRING_PROFILES_ACTIVE=prod
PORT=80

# Application Configuration
STORAGE_TYPE=s3
AWS_S3_ACCESS_KEY=
AWS_S3_SECRET_KEY=
AWS_S3_REGION=ap-northeast-2
AWS_S3_BUCKET=discodeit-binary-content-storage-hsj
AWS_S3_PRESIGNED_URL_EXPIRATION=600

# DataSource Configuration
RDS_ENDPOINT=discodeit-db.cheuy484ure1.ap-northeast-2.rds.amazonaws.com:5432
SPRING_DATASOURCE_URL=jdbc:postgresql://${RDS_ENDPOINT}/discodeit
SPRING_DATASOURCE_USERNAME=discodeit_user
SPRING_DATASOURCE_PASSWORD=discodeit1234

# JVM Configuration
JVM_OPTS=-Xmx256m -Xms256m -XX:MaxMetaspaceSize=192m -XX:+UseSerialGC

멘토에게

Copy link
Copy Markdown

@joonfluence joonfluence left a comment

Choose a reason for hiding this comment

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

전체 요약

Docker 컨테이너화, AWS S3 BinaryContent 고도화, ECS/ECR 기반 배포, GitHub Actions CI/CD 파이프라인을 구축한 PR입니다. 전반적인 구조는 잘 설계되어 있으나, IAM Role 환경에서 presigned URL 생성 불일치, 사용자 삭제 시 NPE, 배포 중 다운타임 등 수정이 필요한 이슈가 있습니다.


[P4] 커밋에 포함된 불필요한 파일

  • logs/discodeit.log (768줄) — 런타임 로그 파일이 커밋되었습니다. .gitignorelogs/가 추가되었으므로 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(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P1] IAM Role 환경에서 presigned URL 생성 실패 위험

generatePresignedUrl()에서 S3Presigner항상 StaticCredentialsProvider(하드코딩 자격증명)를 사용합니다. 그러나 S3Config는 access key가 비어있으면 DefaultCredentialsProvider(IAM Role 등)를 사용하도록 설계되어 있습니다.

ECS에서 IAM Role로 실행 시 awsProperties.getAccessKey()가 빈 값이 되어 presigned URL 생성이 실패합니다. S3Config와 동일한 자격증명 결정 로직을 적용해야 합니다.

Suggested change
.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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[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());
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P1] 프로필 없는 사용자 삭제 시 NullPointerException

프로필 이미지가 없는 사용자(profile == null)를 삭제하면 removeUser.getProfile().getId()에서 NPE가 발생합니다. 프로필은 선택적(optional)이므로 null 체크가 필요합니다.

Suggested change
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P2] 배포 중 다운타임 발생

desired-count 0으로 서비스를 완전히 중단한 뒤 새 태스크를 배포하므로, "중단 → 배포 → Restart" 사이 구간에 다운타임이 발생합니다.

Stop and WaitRestart 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P3] 배포 성공 여부 미확인

배포 후 ECS 서비스 안정화를 기다리지 않으므로, 새 태스크 시작이 실패해도 파이프라인이 성공으로 표시됩니다. true로 변경하면 배포 실패를 즉시 감지할 수 있습니다.

Suggested change
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) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P3] catch-all 핸들러에서 예외 미로깅

handleException(Exception e) catch-all 핸들러에서 log.error()가 없어, 예상치 못한 예외 발생 시 스택 트레이스가 로깅되지 않습니다. 운영 환경에서 디버깅이 매우 어려워질 수 있습니다.

Suggested change
public ResponseEntity<ErrorResponse> handleException(Exception e) {
public ResponseEntity<ErrorResponse> handleException(Exception e) {
log.error("처리되지 않은 예외 발생", e);

.body(response);
}
// User
@ExceptionHandler(UserNotFoundException.class)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P3] DiscodeitException 하위 클래스 핸들러 중복

UserNotFoundException, UserAlreadyExistsException, ChannelNotFoundException, PrivateChannelUpdateException, MessageNotFoundException 핸들러가 모두 83번 줄의 handleDiscodeitException과 동일한 로직(ErrorResponse.of(e, status.value()))을 수행합니다.

Spring은 @ExceptionHandler에서 가장 구체적인 타입을 우선 적용하지만, 이 하위 클래스들은 이미 DiscodeitException 핸들러로 처리됩니다. 중복 핸들러를 제거하면 새로운 예외 클래스 추가 시 일관성을 유지하기 쉬워집니다.

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