AWS RDS 클라우드 DB
AWS RDS(Relational Database Service)란?
애플리케이션에 필요한 빠른 성능, 고가용성, 보안 및 호환성을 제공하는 클라우드 환경 관계형 데이터베이스
기존에는 원활한 테스트를 위해 조원 각자의 로컬에 MySQL을 설치하고 사용했으나,
어느 정도 프로젝트가 완성되면서 데이터베이스를 공용으로 써야 할 필요성이 느껴졌다.
그래서 AWS에서 제공하는 RDS 서비스를 이용하기로 했다.
Spring boot 프로젝트에 RDS 적용하기
- AWS에 로그인 후, RDS 구매(프리티어)
- 퍼블릭 액세스 기능을 "예"로 설정해야 로컬과 RDS가 연결 가능 - RDS 포트 열어주기
- 보안 그룹 → 인바운드 규칙 편집 → 소스를 위치무관으로 설정 - 인텔리제이(IntelliJ)에서 확인하기
- 해당 RDS의 엔드포인트 복사
- Database 탭에서 Data Source → MySQL 선택
- Host에는 복사한 엔드포인트, User와 Password 채워서 Test Connection - 스프링 부트를 MySQL과 연결하기
- application.properties에 다음과 같이 설정
spring.datasource.url=jdbc:mysql://나의엔드포인트:3306/초기데이터베이스이름
spring.datasource.username=나의USERNAME
spring.datasource.password=나의패스워드
spring.jpa.hibernate.ddl-auto=update
AWS S3를 이용한 다중 이미지 업로드
단일 이미지까지는 성공했지만,
우리의 서비스는 여러 장의 이미지가 필요하기 때문에 다중 이미지 업로드에 도전했다.
다중 이미지 업로드를 구현하면서 내가 신경 쓴 포인트는 다음과 같다.
- 파일 이름은 UUID로 변경
- 중복된 파일이름을 막기 위해 - S3뿐만 아니라 DB에도 저장
- 더 원활한 관리를 위해 파일 이름, 경로, 크기 등을 저장 - 상품 정보와 이미지를 동시에 받아서 저장
- 컨트롤러에서 ReqeustPart로 처리 - 컨트롤러에서의 반환값은 ResponseEntity로 통일
AwsS3Config
@Configuration
public class AwsS3Config {
@Value("${cloud.aws.credentials.accessKey}")
private String accessKey;
@Value("${cloud.aws.credentials.secretKey}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
public AmazonS3Client amazonS3Client() {
BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCreds))
.build();
}
}
AwsS3Service
@Slf4j
@Service
@RequiredArgsConstructor
public class AwsS3Service {
@Value("${cloud.aws.s3.bucket}")
private String bucket;
private final AmazonS3 amazonS3;
private final ImageFileRepository imageFileRepository;
// 이미지 파일 s3에 저장 후 Dto 리스트에 담아서 반환
public List<ImageFileRequestDto> uploadFile(List<MultipartFile> multipartFile, String dirName) {
List<ImageFileRequestDto> reponseDto = new ArrayList<>();
// forEach 구문을 통해 multipartFile로 넘어온 파일들 하나씩 reponseDto에 추가
multipartFile.forEach(file -> {
String fileName = createFileName(file.getOriginalFilename(), dirName);
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(file.getSize());
objectMetadata.setContentType(file.getContentType());
try(InputStream inputStream = file.getInputStream()) {
amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
log.info("a3 업로드 성공");
} catch(IOException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "파일 업로드에 실패했습니다.");
}
// file에 관한 내용을 Dto로 변환 후 list에 담아 return
reponseDto.add(new ImageFileRequestDto(file.getOriginalFilename(), fileName, file.getSize()));
});
return reponseDto;
}
public void deleteFile(String fileName, String dirName) {
amazonS3.deleteObject(new DeleteObjectRequest(bucket, dirName + "/" + fileName));
}
// 먼저 파일 업로드 시, 파일명을 난수화하기 위해 UUID를 붙여준다.
private String createFileName(String fileName, String dirName) {
return dirName + "/" + UUID.randomUUID().toString().concat(getFileExtension(fileName));
}
// file 형식이 잘못된 경우를 확인하기 위해 만들어진 로직이며, 파일 타입과 상관없이 업로드할 수 있게 하기 위해 .의 존재 유무만 판단하였다.
private String getFileExtension(String fileName) {
try {
return fileName.substring(fileName.lastIndexOf("."));
} catch (StringIndexOutOfBoundsException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "잘못된 형식의 파일(" + fileName + ") 입니다.");
}
}
}
ProductService
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
private final ImageFileRepository imageFileRepository;
//게시글 생성하기
public ResponseDto addProduct(ProductRequestDto productRequestDto, User user, List<ImageFileRequestDto> imageRequestDtoList){
Product product = productRepository.save(new Product(productRequestDto,user)); // 저장소에 입력 받은 데이터 저장 // save()때문에 @Transactional 을 사용하지 않아도 됨
// 전달받은 이미지파일 Dto 리스트를 DB에 저장
for (ImageFileRequestDto imageFileRequestDto : imageRequestDtoList) {
ImageFile imageFile = imageFileRepository.save(new ImageFile(imageFileRequestDto, product));
}
return new ResponseDto(HttpStatus.OK.value(), "게시글 업로드 성공");
}
ProductController
@Controller
@RequiredArgsConstructor
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
private final AwsS3Service awsS3Service;
private static String dirName = "product-img";
//게시글 작성 + S3 이미지 업로드(+ DB저장)
@PostMapping
public ResponseEntity<ResponseDto> addProduct(
@RequestPart(value = "key") ProductRequestDto productRequestDto,
@RequestPart(value = "multipartFile") List<MultipartFile> multipartFile,
@AuthenticationPrincipal UserDetailsImpl userDetails){
// AwsS3Service의 uploadFile 메소드를 호출하여 S3에 저장하고,
// 그 반환값인 이미지파일 Dto 리스트를 ProductService의 addProduct 메소드의 매개변수로 전달하여
// 유저가 작성한 내용과 함께 이미지 파일 DB에 저장
return ResponseEntity.ok(productService.addProduct(productRequestDto,userDetails.getUser(), awsS3Service.uploadFile(multipartFile, dirName)));
}
📚 참고자료
yml 파일 분리
application 설정 파일들이 많아지면서 key-value 형식을 사용하는 properties 형식보다
계층적 구조를 가진 yml 형식을 사용하는 것이 더 효율적이라는 판단이 들었다.
설정 파일을 분리해서 민감한 key가 들어있는 파일은 gitignore 설정 후 조원들과 따로 공유하여
aws 과금사태가 일어나지 않도록(...) 대비하였다.
application.yml
spring:
profiles:
active: rds
include:
- aws
- credentials
servlet:
multipart:
enabled: true
max-file-size: 20MB
max-request-size: 20MB
application-aws.yml
cloud:
aws:
s3:
bucket: S3 버킷이름
region:
static: S3 지역
stack:
auto: false
application-credentials.yml
cloud:
aws:
credentials:
accessKey: S3 엑세스 키
secretKey: S3 시크릿 키
application-rds.yml
server:
port: 8080
spring:
config:
activate:
on-profile: rds
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://나의엔드포인트:3306/초기데이터베이스이름?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul
username: 나의 USERNAME
password: 나의 패스워드
jpa:
show-sql: true
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: true
멘토링 질문사항
프로젝트를 진행하면서 궁금했던 부분들을 기술 매니저님께 여쭤보기 위해
조원들과 미리 질문을 작성하여 중간 멘토링 시간을 가졌다.
- 주석을 달 때 한 줄 한 줄 기본적인 코드 모두 주석을 달아야 하는지 아니면 이해하기 어려운 부분만 주석을 달아도 되는지?
- 논란의 여지가 있음 코드는 수정했는데 주석은 수정하지 않았다면?ㄷㄷ 유지보수 측면에서 권장하지 않음 (클린코드 책 추천)
- 그러나 현재는 공부하는 단계. 한 줄 한 줄 정리하는 것도 해볼 만함
- 이후에는 다른 사람의 이해를 위한 주석
- 사실 주석 없이도 이해가 가는 코드가 좋은 코드!
- 댓글과 대댓글 entity를 하나로 통합해서 사용하는 게 효율적 일지 따로 분리해서 사용하는 게 효율적 일지?
- entity는 설계하기 나름 답은 없다
- 굳이 분리할 필요성을 못 느낀다면 entity 수를 줄이는 게 낫다
- success code도 error code처럼 enum으로 분리해서 반환하는 게 좋은 지?
- 에러 코드를 enum으로 나눈 이유는? 에러코드가 다양하기 때문에 한 번에 관리하기 위해서
- HttpStatus가 분리돼서 나갈 경우가 많은가? 그렇지 않다
- msg 자체도 보낼 필요성이 있는지 생각해 볼 것
- dept라는 건? 가령 대댓글이라면 products/3/comments/2/reply/1 이런 식으로 가야 한다는 의미인지? RESTful API에 따르면 Document는 단수, Collection은 복수로 표현해야 한다고 하는데 어떤 부분이 Document이고, Collection인지 어떻게 적용시켜야 할지 궁금하다.
- 게시글은 여러 개가 있을 수 있고, 댓글도 여러 개가 있을 수 있고, 그렇다면 대댓글은 여러 개?
- 같은 계층에서 여러 개라면 replies가 맞을지도! 정답은 없다! 구현하기 나름~
- 이미지 업로드 방식에서 흐름을 어떻게 가져가는 게 좋을지?
- s3에 먼저 이미지를 업로드하고(+DB저장) 받아온 파일 경로를 product 엔티티에 저장
이미지를 DB에 저장할 때 product는? product create를 하지 않는다면? - product create를 요청할 때 파일도 같이 받아서 한 번에 저장
form-data형식으로 json과 file을 같이 받는 게 옳은 방향일지? 그렇다면 product의 service단에서 한 번에 처리해주는 게 맞는 건지?- reuqest param으로 list로 묶어서? json과 동시에? 각각 request param으로 받는 방법도 있음
- 서비스에서 서비스를 불러도 괜찮다!
- s3에 먼저 이미지를 업로드하고(+DB저장) 받아온 파일 경로를 product 엔티티에 저장
- 게시글 작성이나, 수정 요청에도 성공 여부와 메시지뿐만 아니라 데이터를 함께 반환하는 이유가 있는지?
- 반드시 반환할 필요는 없음
- 프론트 상황에 따라 다르게 적용
너무 유용했던 시간!
따로 프로젝트 진행 관련 조언도 해주셨다.
- api 수정사항은 바로바로 수정하여 프론트와 공유!
- service단이 커지므로 메소드 분리를 고려할 것 → 코드 분리도 많이 해보기! (네이밍, 접근제어자)
- 조원들과 코드 리뷰 해보기!
오늘의 나는
오늘 프론트 조원 한 분이 항해99를 하차하셨다.
프로젝트 도중에 하차해버린 조원 분이 야속하다가도,
이 압박감이 얼마나 힘들었을지 알기에 이해할 수밖에 없었다.
어떻든 간에 프로젝트는 진행되어야 한다.
멘탈 다 잡고, 고군분투하고 있는 조원들의 노력이 헛되지 않게
내가 할 수 있는 일들을 기꺼이 해내자.
'📝 TIL' 카테고리의 다른 글
[TIL] 6주차 미니 프로젝트ㅣReact + Spring boot 연동 (0) | 2022.12.21 |
---|---|
[TIL] 6주차 미니 프로젝트ㅣAWS EC2 서버 배포 (0) | 2022.12.21 |
[TIL] 5주차 미니 프로젝트ㅣAWS S3를 이용한 이미지 업로드 (0) | 2022.12.17 |
[TIL] 5주차 미니 프로젝트ㅣ주간 시작 (0) | 2022.12.16 |
[TIL] 5주차 주특기 심화ㅣ과제 제출과 코드 리뷰 (0) | 2022.12.15 |