버킷 만들기
클릭Generate Policy
클릭퍼블릭 액세스 가능
으로 바뀐것 확인 (시간이 조금 걸릴 수 있음)사용자
로 이동AmazonS3FullAccess
권한 추가앞선 과정에 생성한 사용자에 IMA 권한 을 부여하는 작업이 필요하다
IAMUserChangePassword
권한과 이전에 추가한 S3 에 접근이 가능한 AmazonS3FullAccess
권한이 존재한다.액세스 키 만들기
클릭AWS 컴퓨팅 서비스에서 실행되는 애플리케이션
을 선택했다.사용자명_accessKeys.csv
파일은 반드시 다운받도록 하자!자세한 코드는 Github 를 참고해주세요
implementation "com.amazonaws:aws-java-sdk-s3:1.12.281"
# AWS S3 configuration
cloud.aws.region.static=ap-northeast-2
# Disable automatic detection of Spring Cloud AWS stack
cloud.aws.stack.auto-=false
IAM이름_accessKey.csv
파일의 Access key ID
와 Secret access key
를 각각 accessKey
와 secretKey
에 입력# AWS S3 credentials(Key)
cloud.aws.s3.credentials.accessKey=
cloud.aws.s3.credentials.secretKey=
cloud.aws.s3.bucket=wuzuzu-test
# Default image path
upload.path=https://wuzuzu-test.s3.ap-northeast-2.amazonaws.com/
defaultImage.path=https://wuzuzu-test.s3.ap-northeast-2.amazonaws.com/aws.png
@Configuration
public class S3Config {
@Value("${cloud.aws.s3.credentials.accessKey}")
private String accessKey;
@Value("${cloud.aws.s3.credentials.secretKey}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
public AmazonS3 amazonS3Client() {
AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
return AmazonS3ClientBuilder
.standard()
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withRegion(region)
.build();
}
}
@Service
@RequiredArgsConstructor
public class S3Service {
@Value("${cloud.aws.s3.bucket}")
private String bucket;
private final AmazonS3 amazonS3;
/* 파일 업로드 */
public String upload(MultipartFile multipartFile, String s3FileName) throws IOException {
// 업로드할 파일의 메타데이터 생성
ObjectMetadata objMeta = new ObjectMetadata();
objMeta.setContentLength(multipartFile.getInputStream().available()); // 파일의 크기를 가져와 업로드할 파일의 크기 설정
objMeta.setContentType(MediaType.IMAGE_JPEG_VALUE); // 업로드할 파일의 유형을 설정 : JPEG 이미지의 MIME 유형 설정
// putObject(버킷명, 파일명, 파일데이터, 메타데이터)로 S3에 객체 등록
amazonS3.putObject(bucket, s3FileName, multipartFile.getInputStream(), objMeta);
// 등록된 객체의 url 반환
// getUrl : 업로드된 객체의 URL(AWS S3에 업로드된 파일에 대한 고유한 위치) 가져오기
// decode: url 안의 한글 or 특수문자 깨짐 방지
return URLDecoder.decode(amazonS3.getUrl(bucket, s3FileName).toString(), "utf-8");
}
/* 파일 삭제 */
public void delete(String key){
try {
// deleteObject(버킷명, 키값)으로 객체 삭제
amazonS3.deleteObject(bucket, key);
} catch (AmazonServiceException e) {
log.error(e.toString());
}
}
}
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "images")
public class Image extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long imageId;
@Column(nullable = false)
private String imageUrl;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "salePost_id")
private SalePost salePost;
public Image(String imageName, Object object) {
this.imageUrl = imageName;
if(object instanceof SalePost){
this.salePost = (SalePost) object;
}
}
}
@Service
@RequiredArgsConstructor
public class ImageService {
@Value("${defaultImage.path}")
private String defaultImagePath;
@Value("${upload.path}")
private String uploadPath;
private final S3Service s3Service;
private final ImageRepository imageRepository;
@Transactional
public void createImage(MultipartFile file, Object object) throws IOException {
String imageName = getImageName(file);
imageRepository.save(new Image(imageName, object));
}
@Transactional
public void deleteImage(String url, Object object){
Optional<Image> imageUrlToDelete;
// Object 에 속한 이미지 목록에서 해당 URL 을 가진 이미지 탐색
if(object instanceof SalePost salePost){
imageUrlToDelete = salePost.getImageUrl().stream()
.filter(imageUrl -> imageUrl.getImageUrl().equals(uploadPath + url))
.findFirst();
} else {
throw new IllegalArgumentException("Object 가 잘못되었습니다.");
}
if (imageUrlToDelete.isPresent()) {
// 해당 URL 을 가진 이미지가 존재하면 삭제
Image image = imageUrlToDelete.get();
salePost.getImageUrl().remove(image);
imageRepository.delete(image);
}else {
// 해당 URL 을 가진 이미지가 없는 경우 예외 발생
throw new IllegalArgumentException("SalePost 에 해당 URL 을 가진 이미지가 없습니다: " + uploadPath + url);
}
// S3 에 이미지 제거 요청
s3Service.delete(url);
}
private String getImageName(MultipartFile file) throws IOException {
if (file != null) {
// UUID.randomUUID() : UUID 클래스를 이용해 시간과 공간을 기반으로 128비트의 고유한 식별자 생성
// file.getOriginalFilename() : 클라이언트가 업로드한 파일의 원래 파일 이름 반환
String originalFileName = UUID.randomUUID() + file.getOriginalFilename();
return s3Service.upload(file, originalFileName);
}
return defaultImagePath;
}
}
@Getter
@Builder
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "sale_posts")
public class SalePost extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long salePostId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
private User user;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String description;
@Column(nullable = false)
private Long views = 0L;
@Column(columnDefinition = "TINYINT(1) default 0")
private Boolean status = true;
@Column(nullable = false)
private String goods;
@Column(nullable = false)
private Long price;
@Column(nullable = false)
private Long stock;
@ManyToOne
@JoinColumn(name = "category_id", nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
private Category category;
@OneToMany(mappedBy = "salePost")
private List<Image> imageUrl;
public SalePost(User user, SalePostRequest requestDto, Category category) {
this.user = user;
this.title = requestDto.getTitle();
this.description = requestDto.getDescription();
this.goods = requestDto.getGoods();
this.price = requestDto.getPrice();
this.stock = requestDto.getStock();
this.category = category;
}
public void update(SalePostRequest requestDto, Category category) {
this.title = requestDto.getTitle();
this.description = requestDto.getDescription();
this.goods = requestDto.getGoods();
this.price = requestDto.getPrice();
this.stock = requestDto.getStock();
this.category = category;
}
public void increaseViews(){
views++;
}
public void goodsOrder(Long count){
stock -= count;
}
public void delete() {
status = false;
}
}
@Transactional
public void uploadImage(User user, Long salePostId, List<MultipartFile> imageFiles) throws IOException {
SalePost salePost = checkSalePost(user, salePostId);
for (MultipartFile imageFile : imageFiles) {
imageService.createImage(imageFile, salePost);
}
}
public void deleteImage(User user, Long salePostId, String key){
SalePost salePost = checkSalePost(user, salePostId);
imageService.deleteImage(key, salePost);
}
@Getter
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(Include.NON_NULL)
public class SalePostVo {
private Long salePostId;
private String title;
private String description;
private Long views;
private String author;
private String category;
private Boolean status;
private String goods;
private Long price;
private Long stock;
private List<String> imageUrls;
}
@Repository
@RequiredArgsConstructor
public class SalePostQueryRepositoryImpl implements SalePostQueryRepository {
private final JPAQueryFactory jpaQueryFactory;
private final QSalePost salePost = QSalePost.salePost;
private final QImage image = QImage.image;
@Override
public SalePostVo findPostByPostId(Long postId) {
// 출력할 url 을 서브쿼리로 찾기
List<String> imageUrls = jpaQueryFactory
.select(image.imageUrl)
.from(image)
.where(image.salePost.salePostId.eq(postId))
.fetch();
return jpaQueryFactory
.select(Projections.constructor(SalePostVo.class,
salePost.salePostId,
salePost.title,
salePost.description,
salePost.views,
salePost.user.userName,
salePost.category.name,
salePost.status,
salePost.goods,
salePost.price,
salePost.stock,
Expressions.asSimple(imageUrls)
))
.from(salePost)
.where(salePost.salePostId.eq(postId))
.leftJoin(salePost.user) // Fetch Join 으로 N+1 문제 해결
.leftJoin(salePost.category) // Fetch Join 으로 N+1 문제 해결
.fetchOne();
}
}
form-data
로 요청form-data
의 Key 값은 @RequestPart
의 value
값과 일치해야 한다.