728x90
게시판 CRUD
- TEXT 만으로 간단하게 게시판을 구현해보았다.
- Data JPA를 사용했기에 List Page에서의 Pagination까지 간단하게 완료할 수 있었다.
- 여기에 AWS S3 이용해서 File Upload를 구현하고자 했다.
- 한 파일첨부는 쉽다. post 테이블에 url만 넣어주면되니까.
- 근데 여러 파일 첨부는 진짜 어려웠다. 데이터 중심적 설계로 접근하면 어렵지 않다.
- 단순히 upload_files 테이블을 만들고 FK관계로 여러 file들을 insert하는데 이 때 post_id만 잘 넣어주면된다.
- 근데 이걸 JPA로 == 객체 지향 중심적 설계 관점으로 접근하려다 보니 너무 어려웠다.
- 결국 김영한님의 `<자바 ORM 표준 JPA 프로그래밍>` 책을 절반 가량 읽어서 연관관계에 대한 개념을 잡은 뒤, 다른 사람들의 코드를 참고해가며 겨우겨우 성공했다...
- 이번 파일 첨부 개발 건을 통해 겸손에 대해 제대로 배운 것 같다. 대단한 실력이 없다는건 알고 있었지만, 이력서에 적을 만한 내용이 없는거지 개발 자체가 안 맞거나 못하는 사람은 아니라고 생각했는데, 간단해 보이는 이 작업을 이틀이나 붙잡고 있었다는게 조금 민망하기도 하고.. 그렇더라.. 겸손을 제대로 배웠고, 진짜 실력을 많이 키우고 싶다는 생각을 하는 중이다...!
AWS S3 파일 업로드
- pom.xml 세팅
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-context</artifactId>
<version>2.3.5</version>
</dependency>
- application.properties 세팅
# AWS Account Credentials
cloud.aws.credentials.accessKey=[액세스키]
cloud.aws.credentials.secretKey=[시크릿키]
# AWS S3 Service bucket
cloud.aws.s3.bucket=[버킷이름]
cloud.aws.region.static=ap-northeast-2
cloud.aws.stack.auto=false
# file upload max size
spring.servlet.multipart.max-file-size=20MB
spring.servlet.multipart.max-request-size=20MB
# AWS S3 Bucket URL
cloud.aws.s3.bucket.url=https://s3.ap-northeast-2.amazonaws.com/[버킷이름]
- config
@Configuration
public class AWSConfig {
@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 BasicAWSCredentials basicAWSCredentials() {
return new BasicAWSCredentials(accessKey, secretKey);
}
@Bean
public AmazonS3Client amazonS3Client(AWSCredentials awsCredentials) {
AmazonS3Client amazonS3Client = new AmazonS3Client(awsCredentials);
amazonS3Client.setRegion(Region.getRegion(Regions.fromName(region)));
return amazonS3Client;
}
}
- Service Code - (다 적고 보니 S3 업로드 로직은 Service 가 아니라 Utils로 가도 될것같다..?)
@Service
public class AwsS3ServiceImpl {
private final AmazonS3Client amazonS3Client;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
@Autowired
public AwsS3ServiceImpl(AmazonS3Client amazonS3Client) {
this.amazonS3Client = amazonS3Client;
}
private String upload(InputStream inputStream, String uploadKey) {
PutObjectRequest putObjectRequest = new PutObjectRequest(bucket, uploadKey, inputStream, new ObjectMetadata());
PutObjectResult putObjectResult = amazonS3Client.putObject(putObjectRequest);
IOUtils.closeQuietly(inputStream, null);
return amazonS3Client.getUrl(bucket, uploadKey).toString();
}
public List<String> upload(MultipartFile[] multipartFiles) {
List<String> storeUrlList = new ArrayList<>();
Arrays.stream(multipartFiles)
.filter(multipartFile -> !StringUtils.isEmpty(multipartFile.getOriginalFilename()))
.forEach(multipartFile -> {
try {
storeUrlList.add(upload(multipartFile.getInputStream(), createStoreFileName(multipartFile.getOriginalFilename())));
} catch (IOException e) {
e.printStackTrace();
}
});
return storeUrlList;
}
public ResponseEntity<byte[]> download(String key) throws IOException {
GetObjectRequest getObjectRequest = new GetObjectRequest(bucket, key);
S3Object s3Object = amazonS3Client.getObject(getObjectRequest);
S3ObjectInputStream objectInputStream = s3Object.getObjectContent();
byte[] bytes = IOUtils.toByteArray(objectInputStream);
String fileName = URLEncoder.encode(key, "UTF-8").replaceAll("\\+", "%20");
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
httpHeaders.setContentLength(bytes.length);
httpHeaders.setContentDispositionFormData("attachment", fileName);
return new ResponseEntity<>(bytes, httpHeaders, HttpStatus.OK);
}
public List<S3ObjectSummary> list() {
ObjectListing objectListing = amazonS3Client.listObjects(new ListObjectsRequest().withBucketName(bucket));
List<S3ObjectSummary> s3ObjectSummaries = objectListing.getObjectSummaries();
return s3ObjectSummaries;
}
private String createStoreFileName(String originalFileName) {
String ext = extractExt(originalFileName);
String uuid = UUID.randomUUID().toString();
return uuid + "." + ext;
}
private String extractExt(String originalFileName) {
int pos = originalFileName.lastIndexOf(".");
return originalFileName.substring(pos + 1);
}
}
- PostService Code
@Override
public PostResponseDTO insertPost(PostRequestDTO postRequestDTO, MultipartFile[] multipartFiles) {
/**
* 1. Post 엔티티 생성
*/
Post post = Post.builder()
.postTitle(postRequestDTO.getPostTitle())
.postContent(postRequestDTO.getPostContent())
.userId(postRequestDTO.getUserId())
.uploadFiles(new ArrayList<>())
.build();
/**
* 2. S3 Upload
* 2.1. UploadFile 엔티티 생성
* 2.2. post <-> uploadFiles 양방향 연관관계 매핑
*/
awsS3Service.upload(multipartFiles).stream()
.forEach(url -> {
UploadFile uploadFile = UploadFile.builder()
.fileUrl(url)
.build();
post.getUploadFiles().add(uploadFile);
uploadFile.setPost(post);
});
return new PostResponseDTO(postRepository.save(post));
}
진짜 이 부분이 잘 안되서 고생을 엄청했다. 이 부분을 잘 이해하려면 일단 김영한님의 JPA 책을 읽고 시작하는게 맞는 순서라고 본다. 그 책을 읽고 나름대로 연관관계에 대해 이해도가 생겼다고 생각했음에도 이 부분을 너무 해멨다...
'BackEnd > Spring-boot' 카테고리의 다른 글
[CodeDeploy] 에러 로그 경로 (0) | 2023.07.27 |
---|---|
[EC2] 배포 환경에서 Gradle Error - "Could not find or load main class {클래스이름}" 에러 (0) | 2023.07.27 |
[EC2] gradlew test 실행 시 ec2 hang -> 이후 연결 안됨 (0) | 2023.07.26 |
[Redis] Spring-boot-data-redis 캐싱 (0) | 2023.07.09 |
댓글