1. AWS S3 설정
1) 개요
과제 요구사항에서 AWS S3를 이용해서 이미지를 저장하는 요구사항이 있었다. AWS도 제대로 이해하지 못하고 있어서 조금 곤란하긴 했다. 일단 어떻게 연결하는지 이해를 하기위해 구글링을 시작했다.
"AWS S3 SPRING", "AWS S3 properties", "AWS S3 SPRING 연동방법", "S3 SPRING 이미지 업로드" 등 다양하게 검색하면서 코드를 이해하기 위해 노력했다.
2) 문제상황
하지만 빌드도 제대로 되지 않는 문제가 발생했다. 분명 문제는 없어보이고, 여러 레퍼런스(블로그, 유튜브) 를 보며 문제 없다고 생각했다. 한 5시간 동안 헤맸다.
- Build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.1'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
// AWS S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
// MySQL
implementation 'mysql:mysql-connector-java:8.0.28'
// JDBC Template
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
- application.properties
//application.properties
spring.application.name=testImage
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
spring.cloud.aws.credentials.access-key=${AWS_IAM_ACCESS_KEY}
spring.cloud.aws.credentials.secret-key=${AWS_IAM_SECRET_KEY}
spring.cloud.aws.region.static=us-east-1
spring.cloud.aws.s3.bucket=${AWS_S3_BUCKET_NAME}
- applcation-s3.properties
AWS_IAM_ACCESS_KEY = 개인키
AWS_IAM_SECRET_KEY = 개인키
AWS_S3_BUCKET_NAME = 각 버킷이름
- S3Config.java
// S3Config.java
@Configuration
public class S3Config {
@Value("${spring.cloud.aws.credentials.accesskey}")
private String accessKey;
@Value("${spring.cloud.aws.credentials.secretkey}")
private String secretKey;
@Value("${spring.cloud.aws.region.static}")
private String region;
@Bean
public AmazonS3Client amazonS3() {
AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder
.standard()
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withRegion(region)
.build();
}
}
해결은 정말 쉬웠다.. 이걸 왜 못찾았는지... ....
S3Config.java : spring.cloud.aws.credentials.accesskey
application.properties : spring.cloud.aws.credentials.access-key
변수를 잘못 넣어서 생긴일이였다.. 아까운 내 시간이다. 맵핑이 안되서 생긴 오류였던 것이다. 하지만 이렇게 넣고도 계속 오류가 발생했다.
두번째 오류는 프로퍼티 변수는 읽어올 수 있지만 키가 없다고 뜬다?
spring.cloud.aws.credentials.access-key=${AWS_IAM_ACCESS_KEY}
spring.cloud.aws.credentials.secret-key=${AWS_IAM_SECRET_KEY}
spring.cloud.aws.region.static=us-east-1
spring.cloud.aws.s3.bucket=${AWS_S3_BUCKET_NAME}
applcation.properties에 밑에 구문을 넣어주면 된다. 활성화된 프로파일에 include를 통해 다른 프로파일을 포함시키면 된다. (당연히 공개되면 안되기에 applcation-s3.properties 는 .gitignore 처리 해줄것)
spring.profiles.include=s3
ImageService.java
public void uploadtest(MultipartFile multipartFile) throws IOException {
String s3FileName = multipartFile.getOriginalFilename();
ObjectMetadata objMeta = new ObjectMetadata();
objMeta.setContentLength(multipartFile.getInputStream().available());
amazonS3.putObject(bucket+"/test", s3FileName, multipartFile.getInputStream(), objMeta);
}
이렇게 하면 S3 버킷에 잘 업로드된다. 하지만
저기 ObjectUrl을 눌러도 접근이 안된다...
AccessDenied가 뜬다... 뭘까
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Stmt1405592139000",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::YOUR-BUCKET-ARN/*",
"arn:aws:s3:::YOUR-BUCKET-ARN"
]
}
]
}
Bucket 정책을 정하지 않아서 접근을 못했다.
3) 해결
- 변수명 실수
-> 변수명 정정
- AWS Properties 포함 안 시킴
-> spring.profiles.include=s3 해결
- AccessDenied
-> 버킷 정책 설정
4) 결론
- AWS S3는 설정이 어렵지, 기본적인 사용방법은 간단해보인다. 하지만 실제 프로젝트에선 예외처리에 대해서 한 번 더 고민해보아야 할 것 이다. 또한, 대용량 이미지, 이미지 최적화 에 대해서 고민을 해보아야겠다.
2. 특정 메소드에 다양한 dto 넣기 -> @ModelAttribute와 생성자
1) 개요
특정 서비스에서 이미지와 기본 정보들을 같이 넣어야할 때가 있어서 미리 이미지 관련 서비스를 정리해놓고 기다리고 있었다. 하지만 이미지 생성만 생각하고 수정을 따로 생각하지 못해서 dto 하나만 맵핑 시켜놓고 있었다.
// S3Service
public interface S3Service {
String uploadImage(BoardDto.Request boardDto, Long boardId) throws IOException;
}
// BoardDto
public class BoardDto {
@Getter
@AllArgsConstructor
public static class Request {
private MultipartFile[] image;
@NotBlank(message = "title 은 필수입니다.")
private String title;
private String description;
@NotBlank(message = "배경색 은 필수입니다.")
private String backgroundColor;
@Min(0)
@Max(1)
private int imageActivated;
}
}
@RestController
@RequestMapping("workspaces/{workspaceId}/boards")
@RequiredArgsConstructor
public class BoardController {
private final BoardService boardService;
@PostMapping
public ResponseEntity<BoardDto.ResponseBaseDto> createBoard(@PathVariable Long workspaceId,
@Valid @ModelAttribute BoardDto.Request boardDto,
@AuthenticationPrincipal User user) {
BoardDto.ResponseBaseDto boardResponse = boardService.createBoard(user, boardDto, workspaceId);
return ResponseEntity.status(HttpStatus.CREATED).body(boardResponse);
}
}
2) 문제상황
그렇다면 문제는 무엇인가?
- 생성은 @Valid으로 제목, 배경색, 이미지 활성화 여부를 값을 검증하고있다.
- 수정은 원하는 필드만 변경해주도록 하기위해 이미지 활성화 여부만 필수값으로 검증하고 있다.
@AllArgsConstructor
@Getter
public static class UpdateRequest {
private MultipartFile[] image;
private String title;
private String description;
private String backgroundColor;
@Min(0)
@Max(1)
private Integer imageActivated;
}
이런 구조를 가진다.
그렇다면 이미지 업로드에 대한 메소드를 재활용할 수 없을까 란 생각을 해보았다.
그러면 아까 S3Service에서 dto가 여러개 들어가게 구현을 해야하는데 "추상클래스"가 생각이 났다.
추상클래스에 둘다 상속을 시키면 다형성에 의해 들어가게 할 수 있을 것이다.
하지만 S3Service에서 이 클래스가 어떤 클래스 인지 판별해야 정확한 서비스를 구현할 수 있을 것이다.
당장 시간도 없어서 일단은 updateRequest를 저렇게 dto를 변경해주었다.
@Getter
@AllArgsConstructor
public static class Request {
private MultipartFile[] image;
@NotBlank(message = "title 은 필수입니다.")
private String title;
private String description;
@NotBlank(message = "배경색 은 필수입니다.")
private String backgroundColor;
@Min(0)
@Max(1)
private int imageActivated;
public Request(UpdateRequest updateRequest) {
this.image = updateRequest.getImage();
this.title = updateRequest.getTitle();
this.description = updateRequest.getDescription();
this.backgroundColor = updateRequest.getBackgroundColor();
this.imageActivated = updateRequest.getImageActivated();
}
}
해결했다고 생각했지만 CreateBoard가 되지 않는다...
@RestController
@RequestMapping("workspaces/{workspaceId}/boards")
@RequiredArgsConstructor
public class BoardController {
private final BoardService boardService;
@PostMapping
public ResponseEntity<BoardDto.ResponseBaseDto> createBoard(@PathVariable Long workspaceId,
@Valid @ModelAttribute BoardDto.Request boardDto,
@AuthenticationPrincipal User user) {
BoardDto.ResponseBaseDto boardResponse = boardService.createBoard(user, boardDto, workspaceId);
return ResponseEntity.status(HttpStatus.CREATED).body(boardResponse);
}
@ModelAttribute 관련해서 내가 모르는 무언가가 있다. 생성자 관련해서 자꾸 오류가 뜬다.
정확히는 모르겠지만 생성자 충돌이 나는 것 같다. 그래서 구글링을 통해 찾아 보았는데
- @ModelAttribute는 생성자가 1개이면 생성자를 통해 객체를 생성
아직까지 이해가 되지않는다?
나는 생성자가 총 두개가 있다.
1. @AllArgsConstructor
2.
public Request(UpdateRequest updateRequest) {
this.image = updateRequest.getImage();
this.title = updateRequest.getTitle();
this.description = updateRequest.getDescription();
this.backgroundColor = updateRequest.getBackgroundColor();
this.imageActivated = updateRequest.getImageActivated();
}
3) 해결
그렇다면 무엇이 문제?
- @ModelAttribute는 생성자가 2개 이상이면 매개변수 없는 생성자를 통해 객체를 생성하고
Setter를 통해 값을 셋팅
public class BoardDto {
@Getter
@NoArgsConstructor
@Setter
public static class Request {
private MultipartFile[] image;
@NotBlank(message = "title 은 필수입니다.")
private String title;
private String description;
@NotBlank(message = "배경색 은 필수입니다.")
private String backgroundColor;
@Min(0)
@Max(1)
private int imageActivated;
public Request(UpdateRequest updateRequest) {
this.image = updateRequest.getImage();
this.title = updateRequest.getTitle();
this.description = updateRequest.getDescription();
this.backgroundColor = updateRequest.getBackgroundColor();
this.imageActivated = updateRequest.getImageActivated();
}
}
@AllArgsConstructor -> @NoArgsConstructor + @Setter
잘 작동한다. 물론 @Setter 가 맘에 들지는 않지만 좀 고민해보아야할 것이다.
또한, dto 상속을 통해 여러개를 넣을 수 있게 프로젝트가 끝나고 혼자 테스트를 해보았다.
// Basedto
@Getter
public abstract class Basedto {
@Min(0)
@Max(1)
private int imageActivated;
private String description;
public Basedto(int imageActivated, String description) {
this.imageActivated = imageActivated;
this.description = description;
}
}
// TestDto1
@Getter
public class TestDto1 extends Basedto {
private MultipartFile[] image;
@NotBlank(message = "title 은 필수입니다.")
private String title;
@NotBlank(message = "배경색 은 필수입니다.")
private String backgroundColor;
public TestDto1(MultipartFile[] image, String title, String description, String backgroundColor, int imageActivated) {
super(imageActivated, description);
this.image = image;
this.title = title;
this.backgroundColor = backgroundColor;
}
}
// TestDto2
@Getter
public class TestDto2 extends Basedto{
private MultipartFile[] image;
private String title;
private String backgroundColor;
public TestDto2(MultipartFile[] image, String title, String description, String backgroundColor, int imageActivated) {
super(imageActivated, description);
this.image = image;
this.title = title;
this.backgroundColor = backgroundColor;
}
}
@Service
public class TestService {
public void testMethod(Basedto basedto) {
if (basedto instanceof TestDto1) {
System.out.println("123");
}
if (basedto instanceof TestDto2) {
System.out.println("456");
}
if (basedto.getClass().equals(TestDto1.class)){
System.out.println("19199");
}
if (basedto.getClass().equals(TestDto2.class)){
System.out.println("789");
}
}
}
이런식으로 어떤 dto인지 알아낼 수 있을 것이다. 하지만 찾아보니 instance of, equals 는 지양해야할 것이다.
SOLID 5대 원칙인 SRP, OCP를 위배한다.
OCP: 만약 BaseDto를 상속 받는 dto가 추가된다면 조건문이 추가 되어야하며, 삭제 할 때도, 코드 수정이 필요
SRP: 각 dto 마다 어떻게 구현을 하는지 TestService가 부가 책임을 가진다.
4) 결론
간단한 문제인지 알았는데 생각보다 많은 것들이 얽혀있어서 푸는데 조금 시간이 걸렸다. 실제 학습이라면 모르지만 프로젝트 개발 시에는 조금 지양을 해보아야 할 것이다. 상속도 잘 쓰면 편하지만 저런 SOLID 원칙에 대해서 한 번 더 공부하고 확인해보아야 할 것이다.
'스파르타(부트캠프) > Troubleshooting' 카테고리의 다른 글
[내일배움캠프] 플러스주차(스프링 심화)에 대한 트러블슈팅 (0) | 2024.12.19 |
---|---|
[내일배움캠프] 뉴스피드 프로젝트에 대한 트러블슈팅 (0) | 2024.11.22 |
[내일배움캠프] 일정관리 앱(develop)에 대한 트러블슈팅 (4) | 2024.11.15 |
[내일배움캠프] 일정관리 앱에 대한 트러블슈팅 (1) | 2024.11.08 |