프로젝트 구현 목표
1. 제목, 내용, 이미지를 첨부하여 게시글 작성(Create) API 구현
2. DB에는 제목, 내용, 이미지 URL 저장
3. 이미지는 S3에 저장
S3 버킷 생성 방법 관련 글
AWS S3 활용한 파일 및 이미지 업로드
S3란 무엇인가?S3는 AWS에서 제공하는 '클라우드 스토리지 서비스'이다. 쉽게 말하면 AWS에서 제공하는 파일 저장 서비스이다. S3의 사용 용도이미지나 동영상 저장 및 제공정적 웹사이트 호스팅CD
developerwoohyeon.tistory.com
Post 엔티티 생성
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
private String image;
}
<PostRepository>
import S3_9oormthonUNIV.demo.domain.entity.Post;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PostRepository extends JpaRepository<Post, Long> {
}
<S3Config>
@Configuration
public class S3Config {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
public AmazonS3 amazonS3() {
AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
return AmazonS3ClientBuilder
.standard() //표준 설정을 기반으로 빌더 인스턴스를 생성
.withCredentials(new AWSStaticCredentialsProvider(credentials)) //이 AmazonS3 객체는 accessKey, secretKey를 사용해서 인증을 하게 된다.
.withRegion(region)
.build();
}
}
AmazonS3 객체를 생성해서 Spring Bean으로 등록하는 S3Config클래스를 생성해줘야 한다.
<PostService>
이 클래스는 S3 버킷에 객체를 업로드하는 동작을 구현한 클래스이다.
@Slf4j
@RequiredArgsConstructor
@Component
public class PostService {
private final AmazonS3 amazonS3;
private final PostRepository postRepository;
@Value("${cloud.aws.s3.bucketName}")
private String bucketName;
public String upload(MultipartFile image) {
if(image.isEmpty() || Objects.isNull(image.getOriginalFilename())){
throw new S3Exception(ErrorCode.EMPTY_FILE_EXCEPTION);
}
return this.uploadImage(image);
}
private String uploadImage(MultipartFile image) {
this.validateImageFileExtention(image.getOriginalFilename());
try {
return this.uploadImageToS3(image);
} catch (IOException e) {
throw new S3Exception(ErrorCode.IO_EXCEPTION_ON_IMAGE_UPLOAD);
}
}
private void validateImageFileExtention(String filename) {
int lastDotIndex = filename.lastIndexOf(".");
if (lastDotIndex == -1) {
throw new S3Exception(ErrorCode.NO_FILE_EXTENTION);
}
String extention = filename.substring(lastDotIndex + 1).toLowerCase();
List<String> allowedExtentionList = Arrays.asList("jpg", "jpeg", "png", "gif");
if (!allowedExtentionList.contains(extention)) {
throw new S3Exception(ErrorCode.INVALID_FILE_EXTENTION);
}
}
private String uploadImageToS3(MultipartFile image) throws IOException {
String originalFilename = image.getOriginalFilename(); //원본 파일 명
String extention = originalFilename.substring(originalFilename.lastIndexOf(".")); //확장자 명
String s3FileName = UUID.randomUUID().toString().substring(0, 10) + originalFilename; //UUID를 통해 중복없이 변경된 파일 명
log.info("[S3 업로드 시작] 원본파일명: {}, 확장자: {}, 저장될 S3 파일명: {}", originalFilename, extention, s3FileName);
InputStream is = image.getInputStream();
byte[] bytes = IOUtils.toByteArray(is);
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType("image/" + extention); // OK: image/png
metadata.setContentLength(bytes.length);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
try{
PutObjectRequest putObjectRequest =
new PutObjectRequest(bucketName, s3FileName, byteArrayInputStream, metadata);
//.withCannedAcl(CannedAccessControlList.PublicRead); ACL (Access Control List)말고 정책으로 명시 했기 때문에 이 코드는 필요 없다.
amazonS3.putObject(putObjectRequest); // put image to S3
}catch (Exception e){
log.error("[S3 업로드 실패] 에러 메시지: {}", e.getMessage(), e);
throw new S3Exception(ErrorCode.PUT_OBJECT_EXCEPTION);
}finally {
byteArrayInputStream.close();
is.close();
}
return amazonS3.getUrl(bucketName, s3FileName).toString();
}
public void deleteImageFromS3(String imageAddress){
String key = getKeyFromImageAddress(imageAddress);
try{
amazonS3.deleteObject(new DeleteObjectRequest(bucketName, key));
}catch (Exception e){
throw new S3Exception(ErrorCode.IO_EXCEPTION_ON_IMAGE_DELETE);
}
}
private String getKeyFromImageAddress(String imageAddress){
try{
URL url = new URL(imageAddress);
String decodingKey = URLDecoder.decode(url.getPath(), "UTF-8");
return decodingKey.substring(1); // 맨 앞의 '/' 제거
}catch (MalformedURLException | UnsupportedEncodingException e){
throw new S3Exception(ErrorCode.IO_EXCEPTION_ON_IMAGE_DELETE);
}
}
public void savePost(String title, String content, MultipartFile imageFile) {
// 1. 이미지 S3 업로드
String imageUrl = this.upload(imageFile);
// 2. Post 객체 생성 및 저장
Post post = Post.builder()
.title(title)
.content(content)
.image(imageUrl)
.build();
postRepository.save(post);
}
@Transactional
public void deleteImageAndUpdatePost(Long postId) {
// 1. 게시글 조회
Post post = postRepository.findById(postId)
.orElseThrow(() -> new S3Exception(ErrorCode.POST_NOT_FOUND));
// 2. 기존 이미지 주소 가져오기
String imageUrl = post.getImage();
if (imageUrl != null && !imageUrl.isEmpty()) {
// 3. S3에서 이미지 삭제
deleteImageFromS3(imageUrl);
// 4. DB에서 이미지 필드 초기화
post.setImage(null);
postRepository.save(post);
}
}
}
1. 컨트롤러에서 savePost(title,content,imageFile) 호출
2. 이미지 파일을 S3 버킷에 업로드 하기 위해 upload(imageFile) 호출 ( 이미지가 비어있거나 파일명이 없는 경우 예외 발생) -> uploadImage() 호출
3. validateImageFileExtention()을 통해 확장자가 .jpg, .jpeg, .png, .gif 중 하나인지 확인 -> uploadImageToS3() 호출
4. uploadImageToS3() 호출
- 파일 이름 생성 -> UUID + 원본 파일명 (파일명 중복 방지)
- 이미지를 byte[]로 읽음
- S3 업로드용 메타데이터 생성
- PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, s3FileName, byteArrayInputStream, metadata);를 통해 업로드용 요청 객체 생성
- amazonS3.putObject() => S3 버킷으로 업로드!
- 이미지 URL을 리턴
5. 이미지 URL과 함께 게시글 DB에 저장
'9oormthonUNIV 스터디 프로젝트' 카테고리의 다른 글
Jenkins + Docker를 활용한 기본 CI/CD 파이프라인 구축 (0) | 2025.05.10 |
---|---|
Spring Boot + Kafka + Docker로 비동기 메시징 구현하기 (0) | 2025.05.05 |
Spring Boot 프로젝트를 AWS EC2에 Docker로 배포하기 (0) | 2025.04.30 |