Lv 0 AWS Budget 설정
1. AWS Budgets에서 월 예산을 $100 로 설정

2. 예산의 80% 도달 시 이메일 알림이 오도록 설정

3. 설정 완료


LV 1 네트워크 구축 및 핵심 기능 배포
1. 인프라 구축 (VPC & EC2)
- VPC을 설정하여 Public/Private Subnet 분리


- Public Subnet에 EC2를 생성



2. 팀원 정보 저장 및 조회 API 개발
- Mbti enum: Mbti
더보기
더보기
public enum Mbti {
INTJ, INTP, ENTJ, ENTP,
INFJ, INFP, ENFJ, ENFP,
ISTJ, ISFJ, ESTJ, ESFJ,
ISTP, ISFP, ESTP, ESFP
}
- MBTI를 문자열로 받으면 아무 문자열이나 입력받기 때문에 enum을 사용하여 정해진 MBTI 값만 받게 설정
- Entity: Member
더보기
더보기
@Entity
@Getter
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
private String name;
@NotNull
private Integer age;
@NotNull
@Enumerated(EnumType.STRING)
private Mbti mbti;
protected Member(){
} // JPA가 DB 조회할 때
public Member(String name, Integer age, Mbti mbti) {
this.name = name;
this.age = age;
this.mbti = mbti;
}
}
- @GeneratedValue(strategy = GenerationType.IDENTITY)
: DB가 1, 2, 3처럼 자동으로 ID 값 생성 - @Enumerated(EnumType.STRING)
: enum 값을 DB에 숫자가 아니라 문자열로 저장 - protected Member() {}
: JPA는 DB에서 데이터를 꺼내와 객체로 만들 때 기본 생성자가 필요 > JPA를 위해 기본 생성자를 하나 설정
- Repository: MemberRepository
더보기
더보기
public interface MemberRepository extends JpaRepository<Member, Long> {
}
- JpaRepository<Member, Long> : 저장, 조회, 전체조회, 삭제 기능
- Service: MemberService
더보기
더보기
@Service
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// 팀원 생성
public Member create(String name, Integer age, Mbti mbti){
Member member = new Member(name, age, mbti);
return memberRepository.save(member);
}
// 팀원 조회
public Member getById(Long id){
return memberRepository.findById(id)
.orElseThrow(() -> new RuntimeException("해당 팀원을 찾을 수 없습니다. id="+id));
}
}
- Controller: MemberController
더보기
더보기
@RestController
@RequestMapping("/api/members")
public class MemberController {
private final MemberService memberService;
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
@PostMapping
public Member createMember(@Valid @RequestBody Member request){
return memberService.create(
request.getName(),
request.getAge(),
request.getMbti()
);
}
@GetMapping("/{id}")
public Member getMember(@PathVariable Long id){
return memberService.getById(id);
}
}
3. Profile 분리 : 로컬 개발 환경 + 서버 운영 환경을 분리하는 것
1) 설정파일 분리

2) application.yml
spring:
application:
name: member
profiles:
active: local
server:
port: 8080
management:
endpoint:
web:
exposure:
include: health
- 모든 환경에서 공통으로 쓰는 설정
- 서버설정: localhost:8080
- health라는 기능을 밖에서 볼 수 있게 함: 서버 상태 확인
2) application-local.yml
// local 프로필일 때 사용
spring:
config:
activate:
on-profile: local
// 데이터베이스 연결 정보
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
// H2 콘솔 열기
h2:
console:
enabled: true
jpa:
hibernate:
ddl-duto: update
show-sql: true
- 내 컴퓨터에서 실행할 때만 쓰는 설정
- 데이터 베이스 연결 정보
- url: jdbc:h2:mem:testdb : H2라는 연습용 DB를 메모리 안에서 쓰겠다
- driver-class-name: org.h2.Driver : H2 DB랑 대화하려면 H2용 통역사를 쓰겠다
- ddl-auto: update : 자바 클래스가 바뀌면 DB 테이블도 알아서 비슷하게 맞춰줌
- show-sql: true : 장하거나 조회할 때 뒤에서 어떤 SQL이 나가는지 볼 수 있음
- 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
3) application-prod.yml
// prod 모드일 때만 이 파일 사용
spring:
config:
activate:
on-profile: prod
// MySQL 연결 설정
datasource:
url: jdbc:mysql://localhost:3306/memberdb
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 12345678
jpa:
hibernate:
ddl-auto: update
show-sql: false
- 진짜 서버에서 실행할 때 쓰는 설정
- show-sql: false
: 운영 서버에선 보기 X = 많아서 보기 불편하고 정보 노출 위험
4. 로그 전략
- MemberController
더보기
더보기
@Slf4j
public class MemberController {
@PostMapping
public Member createMember(@Valid @RequestBody Member request) {
log.info("[API - LOG] 회원 생성 요청 name={}, age={}, mbti={}",
request.getName(),
request.getAge(),
request.getMbti()
);
return memberService.create(
request.getName(),
request.getAge(),
request.getMbti()
);
}
@GetMapping("/{id}")
public Member getMember(@PathVariable Long id) {
log.info("[API - LOG] 회원 조회 요청 id={}", id);
return memberService.getById(id);
}
- @Slf4j 어노테이션 추가
- @RequiredArgsConstructor : 기본생성자 자동 생성
- log.info : INFO 레벨로 로그 생성
- GlobalExceptionHandler
더보기
더보기
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<String> handleRuntimeException(RuntimeException e) {
log.error("[ERROR] 요청 처리 실패", e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(e.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception e) {
log.error("[ERROR] 서버 오류 발생", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("서버 오류가 발생했습니다.");
}
}
- e : 에러메시지 + 스택 트레이스 함께 남음
5. 상태 모니터링 (Actuator) 추가
- 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-actuator'
- 설정파일에 헬스 체크 엔드포인트를 노출
management:
endpoints:
web:
exposure:
include: health
6. 배포 및 검증
1) 프로젝트 JAR 파일 만들기 (빌드)
- 프로젝트를 EC2에서 실행할 수 있게 .jar 파일로 포장
./gradlew build
- JAR 파일 생성

2) EC2 서버 접속
- AWS에서 생성한 EC2 인스턴스에 SSH를 통해 접속
ssh -i /c/Users/User/Downloads/ec2-key.pem ec2-user@54.180.158.20
- 접속 성공!
[ec2-user@ip-10-0-14-164 ~]$
3) JAR 파일 EC2 서버로 업로드
- 로컬 환경에서 생성한 JAR 파일을 EC2 서버로 복사
scp -i /c/Users/User/Downloads/ec2-key.pem build/libs/cloud-0.0.1-SNAPSHOT.jar ec2-user@54.180.158.20:/home/ec2-user
- 업로드 여부를 확인
[ec2-user@ip-10-0-14-164 ~]$ ls
cloud-0.0.1-SNAPSHOT.jar web
4) EC2에서 실행
- EC2 서버에 Java 설치
sudo yum install java-17-amazon-corretto -y
- 업로드한 JAR 파일을 실행하여 서버에서 애플리케이션을 실행
java -jar cloud-0.0.1-SNAPSHOT.jar --spring.profiles.active=local
5) 외부 접속 확인
- http://54.180.158.20:8080/actuator/health

Lv2 DB 분리 및 보안 연결하기
1. 인프라 요구사항
1) RDS 생성

- 엔드포인트
member-db.cp26aqmkiuti.ap-northeast-2.rds.amazonaws.com
- DB URL
jdbc:mysql://member-db.cp26aqmkiuti.ap-northeast-2.rds.amazonaws.com:3306/memberdb?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
2) 보안 그룹 체이닝
- 보안 그룹 인바운드 규칙 추가

3) Parameter Store
- 파라미터 생성

2. 애플리케이션 요구사항 + 검증
1) Parameter Store 값을 주입받기
더보기
더보기
- application-prod.yml
spring:
config:
activate:
on-profile: prod
import: "aws-parameterstore:/member-service/prod/"
datasource:
url: ${db.url}
driver-class-name: com.mysql.cj.jdbc.Driver
username: ${db.username}
password: ${db.password}
jpa:
hibernate:
ddl-auto: update
show-sql: false
management:
info:
env:
enabled: true
endpoints:
web:
exposure:
include: "*"
cloud:
aws:
region:
static: ap-northeast-2
s3:
bucket: camp-health-eunjiom-files
- application.yml
spring:
application:
name: member
profiles:
active: prod
server:
port: 8080
2) 트러블 슈팅
- 검증했을 때 에러 발생

- IAM 역할 만들기

- yml 파일 코드 수정
spring:
application:
name: member
profiles:
active: prod
server:
port: 8080
info:
team-name: ${team-name}
- 재빌드 후 실행

Lv 3 프로필 사진 기능 추가와 권한 관리
1. S3 버킷 생성
- 버킷 생성

2. IAM Role, IAM Policy
- IAM Role을 생성

- EC2에 연결

- application-prod.yml 설정 추가
cloud:
aws:
region:
static: ap-northeast-2
s3:
bucket: camp-health-eunjiom-files
- 의존성 추가
implementation 'software.amazon.awssdk:s3'
implementation 'software.amazon.awssdk:s3-presigner'
3. POST /api/members/{id}/profile-image
- MultipartFile로 이미지를 받아 S3 버킷에 업로드하고, 이미지 URL을 DB에 업데이트하는 기능
- Entity에 프로필 이미지 필드 추가
// S3 이미지 key 저장
private String profileImageKey;
.
.
.
public void updateProfileImageKey(String profileImageKey){
this.profileImageKey = profileImageKey;
}
}
- S3Service 생성
@Service
public class S3Service {
@Value("${cloud.aws.s3.bucket}")
private String bucket;
@Value("${cloud.aws.region.static}")
private String region;
public String uploadFile(Long memberId, MultipartFile file)
throws IOException{
if(file == null || file.isEmpty()){
throw new IllegalArgumentException("업로드할 파일이 없습니다.");
}
String originalFilename = file.getOriginalFilename();
String extension = extractExtension(originalFilename);
String key = "profiles/" + memberId + "/" + UUID.randomUUID()
+ "." + extension;
S3Client s3Client = S3Client.builder()
.region(Region.of(region))
.build();
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucket)
.key(key)
.contentType(file.getContentType())
.build();
s3Client.putObject(putObjectRequest, RequestBody.fromBytes(file.getBytes()));
return key;
}
private String extractExtension(String filename) {
if (filename == null || !filename.contains(".")) {
throw new IllegalArgumentException("파일 확장자가 없습니다.");
}
return filename.substring(filename.lastIndexOf(".") + 1);
}
}
- MemberService 수정
private final S3Service s3Service;
// 프로필 이미지 업로드
@Transactional
public void uploadProfileImage(Long id, MultipartFile file)
throws IOException{
Member member = memberRepository.findById(id)
.orElseThrow(() -> new RuntimeException("해당 팀원을 찾을 수 없습니다" + id));
String key = s3Service.uploadFile(id, file);
member.updateProfileImageKey(key);
}
- MemberController
// 이미지 업로드
@PostMapping(value = "/{id}/profile-image",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String uploadProfileImage(@PathVariable Long id,
@RequestPart("file")MultipartFile file)
throws IOException{
log.info("[API - LOG] 프로필 이미지 업로드 요청 memberId={}", id);
memberService.uploadProfileImage(id, file);
return "프로필 이미지 업로드 완료";
}
- config 생성
@Configuration
public class S3Config {
@Bean
public S3Client s3Client() {
return S3Client.builder()
.region(Region.AP_NORTHEAST_2)
.build();
}
@Bean
public S3Presigner s3Presigner() {
return S3Presigner.builder()
.region(Region.AP_NORTHEAST_2)
.build();
}
}
- 이미지 업로드 테스트 실행

4. 트러블 슈팅

- Entity 이미지키 필드 위에 @JsonIgnore 어노테이션 사용

5. GET /api/members/{id}/profile-image
- S3 Presigned URL 만드는 서비스
public String createPresignedUrl(String key) {
S3Presigner presigner = S3Presigner.builder()
.region(Region.of(region))
.build();
GetObjectRequest getObjectRequest = GetObjectRequest.builder()
.bucket(bucket)
.key(key)
.build();
GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
.signatureDuration(Duration.ofDays(7))
.getObjectRequest(getObjectRequest)
.build();
URL url = presigner.presignGetObject(presignRequest).url();
presigner.close();
return url.toString();
}
- MemberService
// 프로필 이미지 Presigned URL 조회
@Transactional(readOnly = true)
public String getProfileImagePresignedUrl(Long id) {
Member member = memberRepository.findById(id)
.orElseThrow(() -> new RuntimeException("해당 팀원을 찾을 수 없습니다. id=" + id));
if (member.getProfileImageKey() == null || member.getProfileImageKey().isBlank()) {
throw new RuntimeException("해당 팀원의 프로필 이미지가 없습니다.");
}
return s3Service.createPresignedUrl(member.getProfileImageKey());
}
- Controller GET API
@GetMapping("/{id}/profile-image")
public String getProfileImage(@PathVariable Long id) {
log.info("[API - LOG] 프로필 이미지 조회 요청 memberId={}", id);
return memberService.getProfileImagePresignedUrl(id);
}
- 테스트 실행

- 에러 테스트 실행

'🔌 SPARTA > Assignments' 카테고리의 다른 글
| [CH 6 실전] 서버 개발 과제 (0) | 2026.05.11 |
|---|---|
| [플러스 Spring] 코드 개선 과제 (0) | 2026.04.03 |
| [Spring 과제] 코드 개선 (1) | 2026.03.04 |
| [Spring 과제] 일정 관리 앱 Develop (0) | 2026.02.13 |
| [Spring 과제] 일정 관리 앱 만들기 (0) | 2026.02.05 |