일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- TiL_1st_0419
- diary
- 회고록
- 내일배움캠프
- Java
- 클래스
- #스파르타내일배움캠프
- 성장기록
- static
- 포맷은 최후의 보루
- GitHub
- 감사기록
- 생성자
- 변수의 다양성
- 스파르타내일배움캠프TIL
- #내일배움캠프
- JVM
- #스파르타내일배움캠프TIL
- 객체지향 언어
- 스파르타내일배움캠프
- 해우소
- 스레드
- Diary 해우소
- Github_token
- 메서드
- Git
- KPT
- Java의 이점
- 인스턴스
- Token
- Today
- Total
몬그로이
팀프로젝트 13일차, 14일차 본문
S3 업로드 기능 구현하기
현재 진행하는 프로젝트에는 이미지를 업로드할 일이 굉장히 많다
아티스트나 소속사의 프로필부터 시작해서
소통을 위한 아티스트 커뮤니티, 공지사항을 생성할 때도 필요하다
확인 필요
enterfeed의 경우
공지에 이미지가 들어가는 건 알겠는데, 스케줄도 이미지 들어가는지?
* 둘 다 들어갈 경우 파일 저장 경로를 다르게 해야하므로 create 메서드 수정 필요
-> 아니오
이미지 최대 용량?
-> 대포카메라 4000*6000 기준 7~8MB 이므로 장 당 10MB,
여러장 받을 경우 최대 용량 100MB
이미지만 받을 건지?
* 그렇다면 이미지파일이 아닌 경우 업로드 불가 안내 (필터링)
-> ok
한 장만 받을 건지, 여러장 받을 건지?
-> 프로필/로고 한 장(10MB), 게시글 여러 장( 100MB ) -> 설정 필요
// 파일 장수 제한 (예: 최대 10장)
if (files != null && files.size() > 10) {
throw new FileUploadException("최대 5개의 파일만 업로드할 수 있습니다.");
}
// 파일 장수 제한 (예: 최대 1장) //프로필이나 로고
if (files != null && files.size() > 10) {
throw new FileUploadException("최대 5개의 파일만 업로드할 수 있습니다.");
}
// 파일 용량 제한 (예: 개별 파일 최대 10MB)
if (files != null) {
for (MultipartFile file : files) {
if (file.getSize() > 10 * 1024 * 1024) {
throw new FileUploadException("파일 크기는 2MB를 초과할 수 없습니다.");
}
}
}
이미지 확장자 어디까지 받을 것인지?
jpg, jpeg, png, webp, gif, bmp, tiff, ppm, pgm, pbm, pnm
-> 모두 받기로 함
절차 요약
메서드상 이미지 받기
-> 객체에 기본정보 저장
-> s3에 이미지 저장
-> 객체에도 그 경로 저장
결정 사항
파일명 중복방지를 위한 방법
파일명을 uuid 를 사용할 것인지 vs. 날짜데이터 등으로 넣을 것인지
-> uuid
파일을 Dto 로 받을 것인지 @RequestPart 로 받을 것인지
-> @RequestPart
저장할 파일 경로
-> 각 패키지명 Dir
더 세부적인 파일 경로 설정필요
enter 의 로고가 Group1, Group2 와 같은 위치에 놓이도록 할지
아니면 enter 끼리만 모아놓도록 할지??
-> 이런 꼴로 만들기로 결정
업로드 원본 이름을 저장할 것인지 말 것인지
-> 저장하기로 함
유저프로필의 경우 디렉토리가 USER 로 시작은 하는데,
모든 유저를 한 폴더에 모아 놓을 수 없는 상황..
어떻게 할 것인지 결정해야 함 (가입 날짜별 / 프로필 사진 등록 날짜 / 기타)
-> 정하기 힘들어서 튜터님께 조언 받음
불러올 때 부하가 있을 거라는 걱정은 안 해도 된다고 하심
20만개까지도 성능상 큰 이슈가 없는 경험이 있다고 하셔서
그대로 User 디렉토리 하나에 다 모아 놓기로 결정함
필수 내용
업로드 파일이름과 저장된 파일이름 구별하여 저장??
-> 특수문자가 들어있을 수도 있어 원본 이름은 삭제해야할지도
이미지 파일인지 다른 파일인지 확인
1. 제목으로 ok
2. contentType 추출로
리스트로 받았을 때, 처리하기
-> 프로필 제외하면 다 리스트로 받기
@RequestPart 로 받을 때 주의점
업로드한 이미지에 대해 읽거나 쓸 수 있는 권한 제한
public enum CannedAccessControlList {
Private("private"), //기본설정. 객체 소유자만 읽기/쓰기 가능
PublicRead("public-read"), //모든 사용자가 읽기 가능, 소유자만 쓰기 가능
PublicReadWrite("public-read-write"), //모든 사용자가 읽기/쓰기 가능 (권장x)
AuthenticatedRead("authenticated-read"), // AWS 계정에 인증된 사용자만 읽기 가능
LogDeliveryWrite("log-delivery-write"), // S3 서버 접근 로그를 버킷에 기록하는 데 사용
BucketOwnerRead("bucket-owner-read"),//객체 소유자 달라도 버킷 소유자가 읽을 수 있음
BucketOwnerFullControl("bucket-owner-full-control"), //버킷 소유자가 객체에 대해 읽기/쓰기 가능
AwsExecRead("aws-exec-read"); //AWS에서 실행권한 갖음
-> publicRead 로 결정
공부가 필요한 사항
인터페이스의 메서드 사용이 그냥 가능한 건지
-> 누군가 만들어둔 결과를 보니 그냥 사용하고 있어서..
예) Multipartfile.getContentType();
Spring Framework는 MultipartFile 인터페이스의 여러 구현체를 제공하며, 가장 일반적으로 사용하는 구현체는 CommonsMultipartFile과 StandardMultipartFile입니다. 이 구현체들은 Spring의 파일 업로드 처리 로직에서 자동으로 생성됩니다.
Spring이 자동으로 MultipartFile 인터페이스를 구현한 인스턴스를 제공합니다. 따라서 file.getContentType() 메서드를 직접 호출할 수 있습니다.
MultipartFile의 getInputStream() 메서드
파일의 데이터에 접근하기 위해 InputStream 객체를 얻는 데 사용됩니다. InputStream은 파일, 네트워크, 메모리 등 다양한 데이터 소스에서 데이터를 바이트 스트림 형태로 읽을 수 있게 해줍니다.
파일의 정보를 읽어서 그 내용을 다른 곳에서 사용(조작)하고자 할 때 사용함
예) 해상도를 줄여서 썸네일을 만들 때 등
Key 란?
putObjectRequest 는 key 를 매개 변수로 받는다
key 는 객체를 버킷 내에서 식별하는 고유한 키로, 객체의 경로와 유사한 역할을 하며, 버킷 내에서 객체를 찾는 데 사용
예를 들어, "folder1/folder2/myfile.txt"와 같이 설정하면, folder1이라는 폴더 안에 folder2라는 하위 폴더가 있고, 그 안에 myfile.txt라는 파일이 있는 구조를 표현할 수 있습니다.
new PutObjectRequest(bucketName, key, file); 의 경우, bucketName 버킷의 key 위치에 file을 업로드 함
public PutObjectRequest(String bucketName, String key, File file) {
super(bucketName, key, file);
}
public PutObjectRequest(String bucketName, String key, String redirectLocation) {
super(bucketName, key, redirectLocation);
}
public PutObjectRequest(String bucketName, String key, InputStream input, ObjectMetadata metadata) {
super(bucketName, key, input, metadata);
PutObjectRequest 생성자는 세 가지가 있다
로컬 파일을 직접 S3 버킷으로 업로드할 때 사용 - 간결함, 파일 스트림의 직접 조작 없을 때
S3 객체를 리디렉션할 때 사용되며, 이 객체를 요청하면 지정된 redirectLocation으로 리디렉션됨 - 리디렉션 할 때
스트림 형태로 파일 내용을 전송하며, 파일의 메타데이터를 설정할 때 사용 - 대용량이나 스트리밍 방식에 유용, 파일 내용을 직접 읽는 대신 스트림을 사용
-> 첫 번째로 결정
그런데 매개변수부분에 오류가 난다
그래서 찾아보니,
pubObject() 메소드에 file을 매개변수로 넘겨주는 경우에는 실제 파일이 존재해야 하기 때문에 file을 create해주어야 합니다. 이 방식은 큰 문제를 가지고 있는데요.
첫째로 파일쓰기 작업이 일어난다는 점입니다. 파일 쓰기 작업은 매우 무거운 작업입니다.
두번째로 업로드할 곳은 S3인데 로컬에도 파일이 저장된다는 점입니다. 이는 쓸데없는 자원을 낭비하며, 이 때문에, 별도로 file을 지우는 작업에 대한 고려를 하게됩니다
라고 한다
그래서 경로 수정
-> 번째로 방식으로 진행하기로 했다
만약 그대로 첫 번째를 이용할 경우에는 임시파일을 이용해야 한다고 한다
//윗부분 생략
File tempFile = convertMultipartFileToFile(file);
try {
amazonS3Client.putObject(new PutObjectRequest(bucket, uploadName, tempFile)
.withCannedAcl(CannedAccessControlList.PublicRead));
} catch (Exception e) {
throw new S3Exception(UPLOAD_ERROR);
} finally {
// 업로드 후 임시 파일 삭제 tempFile.delete();
}
}
private File convertMultipartFileToFile(MultipartFile file) throws IOException {
File tempFile = File.createTempFile("temp", null);
file.transferTo(tempFile); return tempFile;
}
이와같이 임시파일을 삭제하는 로직까지 필요하다
boolean allMatch(Predicate<? super T> predicate); //전부 같을 경우 참
boolean anyMatch(Predicate<? super T> predicate); //하나라도 같을경우 참
@RequestPart + @RequestBody 는 불가. @RequestPart + @RequestPart 로 받아야 함
따라서
@PathVariable final String groupName,
@RequestPart(value = "file", required = false) List<MultipartFile> files, //null 처리 필수
@AuthenticationPrincipal UserDetailsImpl userDetails,
@Valid @RequestBody final CreateFeedRequestDto requestDto
에서
@PathVariable final String groupName,
@RequestPart(value = "file", required = false) List<MultipartFile> files, //null 처리 필수
@AuthenticationPrincipal UserDetailsImpl userDetails, //변경된 부분
@Valid @RequestPart final CreateFeedRequestDto requestDto
로 변경함
난관
이미 feed 가 생성되는 기능을 구현해 놓은 상태이며,
이미지 업로드 기능을 추가하여, feed 를 생성할 때 이미지도 들어가게 하려고 하는 중이다
그런데, feedId 에 맞춰서 feed의 이미지가 저장되는 Dir 경로를 설정해 두었더니
1. 이미지를 먼저 받고자 했더니, feedId가 있어야 이미지를 저장할 수 있고, 그 url을 받을 수가 없다
2. feed 먼저 받고자 했더니, setter 를 feed에 사용해야 url을 넣을 수 있다
-> feedId 가 먼저 생성되는게 여러장의 이미지를 올렸을 때, 한 곳에 모아두기 좋다
feedId 를 먼저 받고싶고, setter 는 사용하고 싶지 않았다
그래서 찾아보다가 객체를 하나 새로 생성하는 방법을 알게 되었다
어떤 패키지에 만들어줄까 하다가
dto 패키지에 support로 집어넣었다
왜냐하면 Dto와 역할이 유사하기 때문이다
=> support 는 보통 service 패키지에서 service에서 분리된 메서드들을 담는 곳으로 쓰이므로
다른 역할을 가진 지금의 것에는 어울리지 않아서
Carrier 라고 이미지url 과 이미지를 넣는 객체의 id를 넣어 전달하는 역할을 하는 객체로 변경함
feed 엔티티에는 List<String> urls 를 만들어주니 에러가 난다
@ElementCollection
private List<String> imageUrls = new ArrayList<>();
그런데 @ElementCollection 어노테이션을 사용하는 것 보다는 일대다 관계 매핑을 하는게 더 낫다고 한다
나중에 고려해보고 일단은 만들어 놓기로 했다
계속 S3 관련하여 구현걸 찾아보다보니
이미지 확장자 확인하는 방법이 두 가지가 있다는 것을 알게되었다
content type을 지정해서 올려주지 않으면 자동으로 "application/octet-stream"으로 고정이 되서 링크 클릭시 웹에서 열리는게 아니라 자동 다운이 시작됨
이라는 글을 읽었기 때문이다
한 가지는 지금 이미 구현해 놓은, 제목에서 확장자를 추출해서 확인하는 방식이고
다른 한 가지는 MIME 를 추출하여 확인하는 방식이다
MIME 는 getContentType()을 이용해서 확인하는 방식이 있는데
만약 contentType 이 image 가 아니라면
그림을 선택했을 때, 브라우저가 어떤 타입인지 모르기때문에 다운을 받는다고 한다
이미지인지 확인되는 위치가 서로 다르므로 두 가지 다 하는 것이 좋을수 있다고 한다
설정이 끝나고, 한 패키지 안의 기본 메서드에 적용을 끝낸 후 AWS S3 bucket 을 생성했다
액세스키와 시크릿키를 발급받아야 하는데
CLI 방식인지 외부Application 방식인지 알 수 없었다
구글링해도 정확하게 설명하는 곳을 찾기 어려워 튜터님께 여쭈러갔다
CLI 방식으로 하는게 맞다고 하셔서 그렇게 설정하고 키를 발급받아 적용했다
키를 환경변수에 다 넣고 실행했는데 오류가 났다
키를 넣었는데도 안 되니까 튜터님을 찾아갔다
알고보니 application-aws.yml 파일을 추가로 만든 것이 문제였다
두 개가 만들어지면 실행할 때 뭔가 문제가 생기는데
현업에서는 나눠서 쓰기도 한다고 하셔서
나중에 따로 조사해 봐야 겠다
지금은 빨리 테스트를 끝내야 하므로
사진 업로드 테스트는 포스트맨으로 한다는 말을 듣고 포스트맨을 켰는데
회원가입시 가입되었다는 안내는 나오지만 MySql 에는 데이터가 들어오지 않는다
테이블을 Drop 하고 재실행시켜서 Feed 를 작성해 보기 위해 아티스트를 가입시킨 후 로그인하는데 401 에러가 떴다
다른 타입의 유저들은 로그인이 잘 되는데 아티스트만 문제였다
새로 풀을 받으면 잘 작동하겠지 싶어서 풀을 받았는데 Redis 를 연결해야 했다
설치하고 레디스 실행파일을 실행시키니 작동이 잘 됐는데 이번엔 회원가입 방법이 이메일로 바뀐것이 문제였다
혼자서 찾아보려고 했지만 그러지 못해서 결국 팀원에게 물어봤고
인증과정에대한 설명을 들은 후 회원가입까지 진행했다
레디스 데이터 삭제 명령어 찾음
한번에 다 지우는 기능은 없다고한다
나중에 시간나면 더 찾아볼 것
Feed 등록시 이메일이 올라가는 부분을 테스트할 차례인데,
포스트맨을 사용하는 방법이 익숙하지 않아서 애먹었다
requestDto 를 requestPart로 받는 것을 적용하는게 잘 안 됐다
알고보니 Body 의 form-data 에 file 과 함께 requestDto 를 json 형식으로 입력하면 되는 거였고,
중요한건 "..." 을 누르면 나오는 ContentType 입력칸에 application/json 이라고 명시해야 한다고 한다
그 에러는 해결되었고, 이번엔 이 에러를 만났다
디버깅을 마저 진행했는데, 이 근방 어딘데~ 싶은 곳은 있어도 어디가 원인인지 찾을 수가 없었다
팀원들이 담당튜터님을 소환하여 프론트엔드에 대하여 이야기 하고 있었고,
다른 튜터님을 찾아갈까 고민하면서 준비를 했다
준비란, 화공 켜는 것과 마이크 켜는 것.. (인데 마이크가 켜있던 것 같음) 이다
그런데 팀원이 어떤 문제가 있는건지 물어봐 주어서 디버깅해도 해결이 안 난다고 말하니
다같이 도와주었고, 금방 해결했다
그렇게 힘들게 만든 원인은.. 피드 생성자 of 메서드에 List<String> imageUrls 필드가 없어서였다!
삭제 테스트
MySql 에는 삭제된 걸로 나오는데, S3에는 파일이 남아있는 문제
@RequestPart 가 아닌 RequestDto 에 MultipartFile 을 담아 보내려고 했더니
생성자를 만들 때 Dto 를 통해서 바로 만들기 어려운 상황이 생겼다
public static Artist of(ArtistRequestDto requestDto, User loginUser) {
return Artist.builder()
.artistName(requestDto.getArtistName())
.artistProfileUrl(url)
.user(loginUser)
.build();
file 타입이 바로 url이 만들어질 수가 없으니 말이다
처음 생성되는 필드는 null을 채우기로 했다
어차피 불러와서 사용하는 일이 없으므로 괜찮기 때문이다
프론트 연결 정보
참고
http://dev-gorany.tistory.com/125
https://galid1.tistory.com/591
https://velog.io/@marbea6282/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C
https://emoney96.tistory.com/258
'Dev입성기' 카테고리의 다른 글
팀프로젝트 17일차, 18일차 (1) | 2024.08.05 |
---|---|
15일차 (0) | 2024.08.02 |
오늘 고민은 여기까지 (0) | 2024.07.28 |
팀프로젝트 10일차 (0) | 2024.07.27 |
아니, 폭탄이 도사리고 있는데..요..? (0) | 2024.07.27 |