๐ ํ์ฌ ์ํฉ ๋ฐ ๋ฐฐ๊ฒฝ ์ค๋ช
- ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ตฌํํ๋ ๊ณผ์ ์์ ์ด๋ฏธ์ง๋ฅผ ์ ์ฅํด์ผ ํ๋ ์ํฉ์ด ์๊ฒผ๋ค.
- AWS S3 Bucket์ ์ฌ์ฉํ์ฌ ์ด๋ฏธ์ง๋ S3์ ์ ์ฅํ๊ณ , ํด๋น ์ด๋ฏธ์ง์ URL์ ์๋ฒ์ DB์ ์ ์ฅํ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํํ๋ค.
- ์ด ๋ฐฉ์์ ์คํ ๋ฆฌ์ง์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ญํ ์ ๋ถ๋ฆฌํจ์ผ๋ก์จ, ์ด๋ฏธ์ง๋ ํด๋ผ์ฐ๋ ์คํ ๋ฆฌ์ง์ ์์ ํ๊ฒ ๋ณด๊ดํ๊ณ , ๊ด๋ จ ๋ฉํ๋ฐ์ดํฐ๋ DB์ ์ ์ฅํ์ฌ ์๋ฒ์ ๋ถ๋ด์ ์ค์ด๊ณ ์ด๋ฏธ์ง ์ ์ฅ ๋ฐ ์ ๊ณต ์๋๋ฅผ ๋์ผ ์ ์๋ ์ฅ์ ์ด ์๋ค.
โถ ๊ตฌํ ์ฝ๋
๐ฝ S3์ ์ด๋ฏธ์ง๋ฅผ ์ ๋ก๋ ๋ฐ ์ญ์ ํ๋ ์๋น์ค ํด๋์ค์ธ S3ImageService
/**
* S3์ ์ด๋ฏธ์ง๋ฅผ ์
๋ก๋ ๋ฐ ์ญ์ ํ๋ ์๋น์ค ํด๋์ค์
๋๋ค.
*/
@Service
@RequiredArgsConstructor
public class S3ImageService {
@Value("${cloud.aws.s3.bucket}")
private String bucketName;
public final AmazonS3 amazonS3;
/**
* S3์ ํ์ผ ์
๋ก๋
*/
public String uploadImage(MultipartFile file) {
String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();
try {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
metadata.setContentType(file.getContentType());
amazonS3.putObject(bucketName, fileName, file.getInputStream(), metadata);
return amazonS3.getUrl(bucketName, fileName).toString();
} catch (IOException e) {
throw new S3Exception(S3ErrorCode.FILE_UPLOAD_ERROR);
}
}
/**
* S3์์ ์ด๋ฏธ์ง ์ญ์
*/
public void deleteImage(String imageUrl) {
String fileName = imageUrl.substring(imageUrl.lastIndexOf("/") + 1);
amazonS3.deleteObject(bucketName, fileName);
}
}
๐ฝ ์ฌ์ง์ ์ ๋ฐ์ดํธํ๋ Service ํด๋์ค ๋ฉ์๋
/**
* ๊ฐ๊ฒ ์ฌ์ง์ ์
๋ฐ์ดํธํฉ๋๋ค.
*/
@Transactional
protected void updateMarketImages(Market market, List<MultipartFile> updatedImages) {
// ๊ธฐ์กด ์ฌ์ง ์กฐํ
List<MarketImage> marketImageList = marketImageRepository.findAllByMarketId(market.getId());
// S3์์ ๊ธฐ์กด ์ฌ์ง ์ญ์
for (MarketImage marketImage : marketImageList) {
s3ImageService.deleteImage(marketImage.getImageUrl());
}
// DB์์ ๊ธฐ์กด ์ฌ์ง URL ์ญ์
marketImageRepository.deleteAll(marketImageList);
for (MultipartFile imageFile : updatedImages) {
// S3์ ์๋ก์ด ์ฌ์ง ์
๋ก๋
String imageUrl = s3ImageService.uploadImage(imageFile);
MarketImage updateImage = MarketImage.builder()
.market(market)
.imageUrl(imageUrl)
.build();
// DB์ ์๋ก์ด ์ฌ์ง URL ์ ์ฅ
marketImageRepository.save(updateImage);
}
}
๐จ ๋ฌธ์ ์ํฉ
- S3 Bucket์ ์ด๋ฏธ์ง๋ฅผ ์ ๋ก๋(amazonS3.putObject)ํ๊ฑฐ๋ ์ญ์ (amazonS3.deleteObject)ํ๋ ์์ ์ ํธ๋์ญ์ ์ด ์ ์ฉ๋์ง ์๋๋ค.
- ์ฌ์ง์ ์ ๋ฐ์ดํธํ๋ ๋ฉ์๋์์ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ฌ ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋ ๊ฒฝ์ฐ, DB์์ URL์ ์ถ๊ฐํ๊ฑฐ๋ ์ญ์ ํ๋ ์์ ์ ๋กค๋ฐฑ๋์ง๋ง, S3 ๋ฒํท์ ๋ํ ์์ ์ ๋กค๋ฐฑ๋์ง ์๋๋ค. ์ด๋ S3 ๋ฒํท์ ๊ณ ์ ๊ฐ์ฒด๊ฐ ๋จ์ ๋ฐ์ดํฐ ์ ํฉ์ฑ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๋ค๋ ์๋ฏธ๋ค.
๊ณ ์ ๊ฐ์ฒด: ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๊ฐ์ ๊ด๋ฆฌ ์์คํ ์์ ์ฐธ์กฐ๋์ง ์๊ฑฐ๋ ์ฐ๊ด๋ ์ํฐํฐ๊ฐ ์ญ์ ๋ ์ดํ์๋ ๋จ์ ์๋ ๋ถํ์ํ ๋ฐ์ดํฐ๋ฅผ ์๋ฏธํ๋ค. ์๋ฅผ ๋ค์ด, S3์ ์ ์ฅ๋ ์ด๋ฏธ์ง๊ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ ์ด์ ์ฐธ์กฐ๋์ง ์์ ๊ฒฝ์ฐ ํด๋น ์ด๋ฏธ์ง๋ ๊ณ ์ ๊ฐ์ฒด๊ฐ ๋๋ค.
๋ฐ์ดํฐ ์ ํฉ์ฑ: ์์คํ ์์ ๋ฐ์ดํฐ๊ฐ ์ผ๊ด๋๊ณ ์ ํํ๊ฒ ์ ์ง๋๋ ์ํ๋ฅผ ์๋ฏธํ๋ค. ๋ฐ์ดํฐ ์ ํฉ์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ฉด ์๋ก ๋ค๋ฅธ ์์คํ ๊ฐ์ ๋ฐ์ดํฐ ๋ถ์ผ์น๊ฐ ์๊ธธ ์ ์๋ค. ์ด๋ ๋ฐ์ดํฐ์ ์ ๋ขฐ์ฑ์ ๋จ์ด๋จ๋ฆฌ๋ ๋ฌธ์ ๋ฅผ ์ด๋ํ๋ค. ์๋ฅผ ๋ค์ด, ๋ฐ์ดํฐ๋ฒ ์ด์ค์๋ ์ญ์ ๋์๋ค๊ณ ํ์๋์ด ์์ง๋ง S3์๋ ์ฌ์ ํ ์ด๋ฏธ์ง๊ฐ ๋จ์ ์๋ ๊ฒฝ์ฐ๊ฐ ์ด์ ํด๋นํ๋ค.
โ๏ธ ์์ธ ๋ถ์
S3 Bucket ๊ด๋ จ ์์ ์ ํธ๋์ญ์ ์ด ์ ์ฉ๋์ง ์๋ ์ด์ ๋ S3๊ฐ ์ธ๋ถ์ ๋ ๋ฆฝ๋ ์คํ ๋ฆฌ์ง ์๋น์ค๋ก ๋์ํ๊ธฐ ๋๋ฌธ์ด๋ค. ํธ๋์ญ์ ์ ์ผ๋ฐ์ ์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๊ฐ์ ํธ๋์ญ์ ์ ์ง์ํ๋ ์์คํ ์์๋ง ๋์ํ๋ค.
- S3๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ์๋๋ค.
- S3๋ ๋จ์ํ ํ์ผ ์ ์ฅ์๋ก, ๋ฐ์ดํฐ๋ฒ ์ด์ค์ฒ๋ผ ํธ๋์ญ์ ๊ด๋ฆฌ ๊ธฐ๋ฅ์ด ํฌํจ๋์ด ์์ง ์๋ค. ์ฆ, S3๋ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ฑฐ๋ ์ญ์ ํ ๋ ์ฆ์ ๋ณ๊ฒฝ ์ฌํญ์ ๋ฐ์ํ๋ฉฐ, ์ด๋ฅผ ๋๋๋ฆฌ๋ ๋กค๋ฐฑ ๊ธฐ๋ฅ์ด ์๋ค.
- HTTP ๊ธฐ๋ฐ์ ์์ฒญ
- S3์์ ์ํธ์์ฉ์ HTTP ์์ฒญ์ ํตํด ์ด๋ฃจ์ด์ง๋ฉฐ, ๊ฐ ์์ฒญ์ด ๋ ๋ฆฝ์ ์ผ๋ก ์คํ๋๋ค. HTTP ์์ฒญ์ stateless ํน์ฑ์ ๊ฐ ์์ ์ ์๋ฃ๋ ํ ๋๋๋ฆด ์ ์์ผ๋ฉฐ, ๋ฐ์ดํฐ๋ฒ ์ด์ค ํธ๋์ญ์ ๊ณผ ๊ฐ์ ์ผ๊ด๋ ์ํ๋ฅผ ๋ณด์ฅํ ์ ์๋ค.
๋ฐ๋ผ์ S3์ ๊ฐ์ ์ธ๋ถ ์คํ ๋ฆฌ์ง ์๋น์ค๋ ํธ๋์ญ์ ์ด ์ ์ฉ๋์ง ์์ผ๋ฉฐ, S3์์์ ์์ ์ ์ฑ๊ณต ์ฌ๋ถ์ ๊ด๊ณ์์ด ์ฆ์ ๋ฐ์๋๊ณ ๋กค๋ฐฑ๋์ง ์๋๋ค.
๐จ ํด๊ฒฐ ๋ฐฉ๋ฒ
โถ 1. ์คํจ: try-catch๋ฅผ ํ์ฉํ ์์ธ ์ฒ๋ฆฌ
๐ฝ ๊ตฌํ ์ฝ๋
/**
* ๊ฐ๊ฒ ์ฌ์ง์ ์
๋ฐ์ดํธํฉ๋๋ค.
*/
@Transactional
protected void updateMarketImages(Market market, List<MultipartFile> updatedImages) {
// ๊ธฐ์กด ์ฌ์ง ์กฐํ
List<MarketImage> marketImageList = marketImageRepository.findAllByMarketId(market.getId());
// S3์์ ๊ธฐ์กด ์ฌ์ง ์ญ์
try {
for (MarketImage marketImage : marketImageList) {
s3ImageService.deleteImage(marketImage.getImageUrl());
}
} catch (Exception e) {
throw new RuntimeException("๊ธฐ์กด ์ด๋ฏธ์ง๋ฅผ S3์์ ์ญ์ ํ๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.");
}
// DB์์ ๊ธฐ์กด ์ฌ์ง URL ์ญ์
try {
marketImageRepository.deleteAll(marketImageList);
} catch (Exception e) {
throw new RuntimeException("๊ธฐ์กด ์ด๋ฏธ์ง๋ฅผ DB์์ ์ญ์ ํ๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.");
}
// ์๋ก์ด ์ฌ์ง ์
๋ก๋ ๋ฐ ์ ์ฅ
for (MultipartFile imageFile : updatedImages) {
try {
// S3์ ์๋ก์ด ์ฌ์ง ์
๋ก๋
String imageUrl = s3ImageService.uploadImage(imageFile);
MarketImage updateImage = MarketImage.builder()
.market(market)
.imageUrl(imageUrl)
.build();
// DB์ ์๋ก์ด ์ฌ์ง URL ์ ์ฅ
marketImageRepository.save(updateImage);
} catch (Exception e) {
throw new RuntimeException("์๋ก์ด ์ด๋ฏธ์ง๋ฅผ ์
๋ก๋ํ๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.");
}
}
}
๐ฝ ๋ฌธ์ ์
- try-catch๋ก S3 ์์ ์์ ๋ฐ์ํ๋ ์์ธ๋ฅผ ์ฒ๋ฆฌํ๊ณ ์์ง๋ง, S3์ ๋ฐ์๋ ๋ณ๊ฒฝ ์ฌํญ์ ๋กค๋ฐฑ๋์ง ์์ ํด๊ฒฐ์ฑ ์ด ๋์ง ์๋๋ค.
โถ 2. ์คํจ: ๋ก์ง ๋ถ๋ฆฌ ๋ฐ ์์ ๋ณ๊ฒฝ
๐ฝ ๋ฌธ์ ์
- DB ์ญ์ ํ S3 ์ญ์ , DB ์ ๋ก๋ ํ S3 ์ ๋ก๋์ ๊ฐ์ด ๋ก์ง์ ๋ถ๋ฆฌํ๊ณ ์์๋ฅผ ๋ณ๊ฒฝํ๋ฉด ๋ฐ์ดํฐ ์ ํฉ์ฑ์ ์ด๋ ์ ๋ ์ ์งํ๋ ๋ฐ ๋์์ด ๋์ง๋ง, ํธ๋์ญ์ ๋์ค์ ์๋ฒ๊ฐ ๋ค์ด๋๊ฑฐ๋ ์๊ธฐ์น ์์ ์ค๋ฅ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ ์ฌ์ ํ ์๋ฒฝํ ํด๊ฒฐ์ฑ ์ด ๋์ง ์๋๋ค.
โถ 3. ์คํจ: ์์ธ ์ฒ๋ฆฌ๋ฅผ ํตํด ์๋์ผ๋ก ํ์ผ ์ญ์
- ์ฌ์ง ์ ๋ก๋ ์คํจ ์ ์ ๋ก๋์ ์คํจํ ์ฌ์ง์ ์๋์ผ๋ก ์ญ์ ํ๋ค.
- ์ฌ์ง ์ญ์ ์คํจ ์ ์ญ์ ์ ์คํจํ ์ฌ์ง์ ์๋์ผ๋ก ๋ค์ ์ ๋ก๋ํ๋ค.
๐ฝ ๊ตฌํ ์ฝ๋
/**
* ๊ฐ๊ฒ ์ฌ์ง์ ์
๋ฐ์ดํธํฉ๋๋ค.
*/
@Transactional
protected void updateMarketImages(Market market, List<MultipartFile> updatedImages) {
// ๊ธฐ์กด ์ฌ์ง ์กฐํ
List<MarketImage> marketImageList = marketImageRepository.findAllByMarketId(market.getId());
// S3์์ ๊ธฐ์กด ์ฌ์ง ์ญ์ ๋ฐ ๋ณด์ ๋ก์ง ์ฒ๋ฆฌ
for (MarketImage marketImage : marketImageList) {
try {
// S3์์ ๊ธฐ์กด ์ฌ์ง ์ญ์
s3ImageService.deleteImage(marketImage.getImageUrl());
} catch (Exception e) {
// ์ญ์ ์คํจ ์ ๋ณต๊ตฌ ๋ณด์ ๋ก์ง
System.err.println("S3์์ ๊ธฐ์กด ํ์ผ ์ญ์ ์ค ์ค๋ฅ ๋ฐ์: " + e.getMessage());
try {
// S3์ ๋ค์ ์
๋ก๋ ์๋ (์คํจํ ํ์ผ ๋ณต๊ตฌ)
s3ImageService.uploadImageFromUrl(marketImage.getImageUrl());
} catch (Exception restoreException) {
System.err.println("S3์ ๊ธฐ์กด ํ์ผ ๋ณต๊ตฌ ์ค ์ค๋ฅ ๋ฐ์: " + restoreException.getMessage());
throw new RuntimeException("๊ธฐ์กด ์ด๋ฏธ์ง๋ฅผ ๋ณต๊ตฌํ๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.", restoreException);
}
throw new RuntimeException("๊ธฐ์กด ์ด๋ฏธ์ง๋ฅผ ์ญ์ ํ๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.", e);
}
}
// DB์์ ๊ธฐ์กด ์ฌ์ง URL ์ญ์
marketImageRepository.deleteAll(marketImageList);
// ์๋ก์ด ์ฌ์ง ์
๋ก๋ ๋ฐ ์ ์ฅ
for (MultipartFile imageFile : updatedImages) {
String imageUrl = null;
try {
// S3์ ์๋ก์ด ์ฌ์ง ์
๋ก๋
imageUrl = s3ImageService.uploadImage(imageFile);
MarketImage updateImage = MarketImage.builder()
.market(market)
.imageUrl(imageUrl)
.build();
// DB์ ์๋ก์ด ์ฌ์ง URL ์ ์ฅ
marketImageRepository.save(updateImage);
} catch (Exception e) {
// ์
๋ก๋ ์คํจ ์, ์
๋ก๋๋ ํ์ผ ์ญ์
if (imageUrl != null) {
try {
s3ImageService.deleteImage(imageUrl);
} catch (Exception deleteException) {
System.err.println("S3์ ์
๋ก๋๋ ํ์ผ ์ญ์ ์ค ์ค๋ฅ ๋ฐ์: " + deleteException.getMessage());
}
}
throw new RuntimeException("์๋ก์ด ์ด๋ฏธ์ง๋ฅผ ์
๋ก๋ํ๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.", e);
}
}
}
๐ฝ ๋ฌธ์ ์
- ํ๋์ ๋ด๋ ๋ก์ง์ด ๋ณต์กํด์ ธ ๊ฐ๋ ์ฑ์ด ๋งค์ฐ ๋จ์ด์ง๋ค.
- ์๋์ผ๋ก ์ ๋ก๋ํ๊ฑฐ๋ ์ญ์ ํ๋ ๊ณผ์ ์์ ๋๋ค์ ์ค๋ฅ๊ฐ ๋ฐ์ํ ๊ฐ๋ฅ์ฑ์ด ์๋ค.
- ์ญ์ ์์๋ ์ฑ๊ณตํ์ผ๋ ์
๋ก๋์์ ์คํจํ๊ฒ ๋๋ค๋ฉด ๋๋ค์ ๋ฐ์ดํฐ ์ ํฉ์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๊ฒ ๋๋ค.
- ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ก์ง์ ๋ถ๋ฆฌํ๋ ค ํ์ผ๋, ์ญ์ ๋ ๊ธฐ์กด ์ฌ์ง ์กฐํ ๋ฆฌ์คํธ๋ฅผ ์ฌ์ฉํ๊ณ , ์ ๋ก๋๋ ์์ฒญ๋ฐ์ Multipart ๋ฆฌ์คํธ๋ฅผ ์ฌ์ฉํ์ฌ ์ฒ๋ฆฌํ๊ธฐ ๋๋ฌธ์ ๋ก์ง์ด ๋งค์ฐ ๋ณต์กํด์ง๋ค.
โถ 4. ๋ณด๋ฅ: Spring Scheduler๋ฅผ ์ฌ์ฉํ์ฌ S3 Bucket์ ์ฃผ๊ธฐ์ ์ผ๋ก ์ ๋ฆฌํ๋ ๋ณด์ ํธ๋์ญ์
- ์ค์ผ์ค๋ฌ๋ฅผ ํตํด ๋งค์ผ ์์ ์ S3 Bucket์ ๊ณ ์ ๊ฐ์ฒด๋ฅผ ์ญ์ ํ๋ค.
๐ฝ ๊ตฌํ ์ฝ๋
/**
* ์ฃผ๊ธฐ์ ์ผ๋ก S3์์ ๊ณ ์ ๊ฐ์ฒด ์ญ์ (๋งค์ผ ์์ ์คํ)
*/
@Scheduled(cron = "0 0 0 * * ?")
public void cleanUpOrphanImages() {
// 1. S3 ๋ฒํท์ ๋ชจ๋ ํ์ผ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
ObjectListing objectList = amazonS3.listObjects(bucketName);
List<S3ObjectSummary> s3Objects = objectList.getObjectSummaries();
// 2. ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ๋ ๋ชจ๋ ์ด๋ฏธ์ง URL ๊ฐ์ ธ์ค๊ธฐ
Set<String> dbImageUrls = marketImageRepository.findAll().stream()
.map(MarketImage::getImageUrl)
.collect(Collectors.toSet());
// 3. S3์ ์กด์ฌํ์ง๋ง ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์๋ ํ์ผ(๊ณ ์ ๊ฐ์ฒด) ์ญ์
for (S3ObjectSummary s3Object : s3Objects) {
String imageUrl = amazonS3.getUrl(bucketName, s3Object.getKey()).toString();
if (!dbImageUrls.contains(imageUrl)) {
amazonS3.deleteObject(bucketName, s3Object.getKey());
}
}
}
๐ฝ ๋ฌธ์ ์
- S3์์ ๊ฐ์ฒด๋ฅผ ์ญ์ ํ๋ ๊ณผ์ ๊ณผ ๋์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ํ๊ฐ ๋ณ๊ฒฝ๋๊ฑฐ๋ ์์คํ ์ ๋ค๋ฅธ ๋ณํ๊ฐ ์๊ธธ ์ ์๋ค. ์์ ์ ์ค์ผ์ค๋ฌ๊ฐ S3์ ๊ณ ์ ๊ฐ์ฒด๋ฅผ ์ญ์ ํ๋ ๋์ ์๋ก์ด ์ฌ์ง์ด ์ ๋ก๋๋๊ฑฐ๋ ๊ธฐ์กด ์ฌ์ง์ด ์ญ์ ๋๋ ๊ฒฝ์ฐ๋ฅผ ์๋ฏธํ๋ค.
- s3Objects์ dbImageUrls๋ฅผ ๋ชจ๋ ๋ฉ๋ชจ๋ฆฌ์ ๋ก๋ํ๊ธฐ ๋๋ฌธ์ ํ์ผ์ ์๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ด๋ฏธ์ง URL ์๊ฐ ๋ง์ ๊ฒฝ์ฐ ๋ฉ๋ชจ๋ฆฌ ๋ถ์กฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๋ค. ํนํ ๋๊ท๋ชจ ํ์ผ ๊ด๋ฆฌ ํ๊ฒฝ์์๋ ์ด๋ฌํ ๋ฐฉ์์ผ๋ก ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ํ๊บผ๋ฒ์ ๋ฉ๋ชจ๋ฆฌ์ ์ฌ๋ฆฌ๋ ๊ฒ์ ๋นํจ์จ์ ์ด๋ค.
โถ 5. ํด๊ฒฐ1: S3 Bucket ์ ๋ก๋, S3 Bucket ์ญ์ , DB ์ ๋ก๋ ๋ฐ ์ญ์ ๋ก์ง 3๊ฐ๋ก ๋ถ๋ฆฌ
- ํด๋ผ์ด์ธํธ ์ฑ์์ ์ฌ์ง์ ์ ๋ก๋ ๋ฐ ์ญ์ ํ๋ ์ฐฝ์ ๋ฐ๋ก ๋ง๋ค์ด, S3 ๋ฒํท์ ์ฌ์ง์ ์ ๋ก๋ํ๊ณ URL์ ๋ฐํํ๋ค. X ๋ฒํผ์ ๋๋ฅด๋ฉด S3 ๋ฒํท์์ ํด๋น ์ฌ์ง์ ์ญ์ ํ๋ค. ์ดํ ์ฌ์ง ์ ๋ฐ์ดํธ ๋ฒํผ์ ๋๋ฅด๋ฉด DB์ ์ ์ฅ๋๋๋ก ํ๋ค. ์ด ๋ก์ง์ 3๊ฐ๋ก ๋ถ๋ฆฌํ๋ค.
![](https://blog.kakaocdn.net/dn/NnB6O/btsJVpPXWO8/IS7XMhnVSxuaXmubEsNCDK/img.png)
๐ฝ ๊ตฌํ ์ฝ๋
/**
* S3์์ ๊ฐ๊ฒ ์ฌ์ง์ ์ญ์ ํฉ๋๋ค.
*/
public void deleteMarketImage(String imageUrl) {
s3ImageService.deleteImage(imageUrl);
}
/**
* S3์ ๊ฐ๊ฒ ์ฌ์ง์ ์
๋ก๋ํฉ๋๋ค.
*/
public MarketImageUrlResponse uploadMarketImage(MultipartFile uploadImage) {
return MarketImageUrlResponse.builder()
.imageUrl(s3ImageService.uploadImage(uploadImage))
.build();
}
/**
* ๊ฐ๊ฒ ์ฌ์ง์ ์
๋ฐ์ดํธํฉ๋๋ค.
*/
@Transactional
protected void updateMarketImages(Market market, List<String> imageUrls) {
// ๊ธฐ์กด ์ฌ์ง ์กฐํ
List<MarketImage> marketImageList = marketImageRepository.findAllByMarketId(market.getId());
// DB์์ ๊ธฐ์กด ์ฌ์ง URL ๋ชจ๋ ์ญ์
marketImageRepository.deleteAll(marketImageList);
// DB์ ์๋ก์ด ์ฌ์ง URL ๋ชจ๋ ์ ์ฅ
for (String imageUrl : imageUrls) {
MarketImage updateImage = MarketImage.builder()
.market(market)
.imageUrl(imageUrl)
.build();
marketImageRepository.save(updateImage);
}
}
๐ฝ ๋ฌธ์ ์
- ๋ก์ง์ ๋ถ๋ฆฌํ์ฌ ๊ฐ ์์
์ ์ฑ
์์ ๋๊ณ ์ค๋ฅ ๋ฐ์ ๋ฒ์๋ฅผ ์ค์์ง๋ง, ์ฌ์ฉ์๊ฐ ์ฌ์ง ์
๋ฐ์ดํธ๋ฅผ ๋๋ฅด์ง ์์ผ๋ฉด S3 ๋ฒํท์ ๊ณ ์ ๊ฐ์ฒด๊ฐ ์์ฑ๋์ด ์ฌ์ ํ ๋ฐ์ดํฐ ์ ํฉ์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๊ฒ ๋๋ค.
- ์ด ๋ฌธ์ ๋ฅผ 6๋ฒ์์ ์ด์ด์ ํด๊ฒฐํ๋ค.
- ์ฌ์ง ์ ๋ก๋์ ์ญ์ ๊ฐ ํด๋ผ์ด์ธํธ์ ์ํด ์๋์ผ๋ก ์ํ๋๋๋ก ๋๋์ด์ก๊ธฐ ๋๋ฌธ์, ์ฌ์ฉ์๊ฐ ์๋ฒ์์ ๋ชจ๋ ์์ ์ ์ฌ๋ฐ๋ฅด๊ฒ ์๋ฃํด์ผ ๋ฐ์ดํฐ ์ ํฉ์ฑ์ด ์ ์ง๋๋ค. ๋ฐ๋ผ์ ํด๋ผ์ด์ธํธ์ ์๋ฒ ๊ฐ์ ์์กด์ฑ์ด ์๊ธด๋ค.
โถ 6. ํด๊ฒฐ2: Lifecycle ๊ท์น์ ์ฌ์ฉํ์ฌ ๊ฐ์ฒด์ ์๋ช ์ฃผ๊ธฐ๋ฅผ ๊ด๋ฆฌ
- 5๋ฒ ๋ฐฉ๋ฒ์์ ๋ฐ์ํ ๋ฌธ์ ์ธ, ์ฌ์ฉ์๊ฐ ์ฌ์ง ์ ๋ฐ์ดํธ ๋ฒํผ์ ๋๋ฅด์ง ์์ผ๋ฉด S3 ๋ฒํท์ ๊ณ ์ ๊ฐ์ฒด๊ฐ ๋จ๋ ์ํฉ์ ํด๊ฒฐํ๊ธฐ ์ํด S3 Lifecycle ๊ท์น์ ๋์ ํ๋ค.
- ์ฌ์ฉ์๊ฐ ์ฌ์ง์ ์ ๋ก๋ํ ๋ temp ํด๋์ ์์ ์ ์ฅํ๊ณ , ์ฌ์ง ์ ๋ฐ์ดํธ ๋ฒํผ์ ๋๋ฅด๋ฉด main ํด๋๋ก ์ด๋ฏธ์ง๋ฅผ ์ด๋์ํค๋ ๋ฐฉ์์ด๋ค.
๐ฝ S3 Bucket ์ค์
- S3 Bucket์์ ์๋ช ์ฃผ๊ธฐ ๊ท์น ์์ฑ์ ํด๋ฆญํ๋ค.
- ์์๋ก ํ์ผ์ ์ ์ฅํ ํด๋ ์ด๋ฆ์ ์ ๋์ฌ๋ก ์ค์ ํ๋ค.
- ๊ฐ์ฒด์ ๋ง๋ฃ ์ผ์๋ฅผ ์ค์ ํ๋ค.
- ์ค์ ์๋ฃ ์ดํ ํ๋ฉด์ด๋ค.
๐ฝ ๊ตฌํ ์ฝ๋
1. tmp ํด๋์ ์ด๋ฏธ์ง ์์ ์ ์ฅ
์ฌ์ฉ์๊ฐ ์ด๋ฏธ์ง๋ฅผ ์ ๋ก๋ํ ๋, ์์ง ์ต์ข ์ ์ผ๋ก ์ ๋ฐ์ดํธํ์ง ์์์ผ๋ฏ๋ก ์ด๋ฏธ์ง๋ฅผ ์ต์ข ์ ์ฅ์๊ฐ ์๋ S3์ temp ํด๋์ ์์๋ก ์ ์ฅํ๋ค.
/**
* S3 temp ํด๋์ ํ์ผ ์
๋ก๋
*/
public String uploadImageToTemp(MultipartFile file) {
String fileName = "temp/" + UUID.randomUUID() + "_" + file.getOriginalFilename();
try {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
metadata.setContentType(file.getContentType());
amazonS3.putObject(bucketName, fileName, file.getInputStream(), metadata);
return amazonS3.getUrl(bucketName, fileName).toString();
} catch (IOException e) {
throw new S3Exception(S3ErrorCode.FILE_UPLOAD_ERROR);
}
}
2. ์ฌ์ง ์ ๋ฐ์ดํธ ๋ฒํผ ํด๋ฆญ ์ ์ด๋ฏธ์ง ์ด๋
์ฌ์ฉ์๊ฐ ์ฌ์ง ์ ๋ฐ์ดํธ ๋ฒํผ์ ํด๋ฆญํ๋ฉด, temp ํด๋์ ์ ์ฅ๋ ์ด๋ฏธ์ง๋ฅผ main ํด๋๋ก ์ด๋์ํจ๋ค.
/**
* S3 temp ํด๋์์ main ํด๋๋ก ํ์ผ ์ด๋
*/
public void moveImagesToMain(List<String> imageUrls) {
for (String imageUrl : imageUrls) {
String decodedFileName = getDecodedFileName(imageUrl);
String tempFileName = "temp/" + decodedFileName;
String mainFileName = "main/" + decodedFileName;
if (!doesDecodedImageExist(tempFileName)) {
throw new S3Exception(S3ErrorCode.IMAGE_NOT_FOUND_ERROR);
}
// ํ์ผ ์ด๋
amazonS3.copyObject(bucketName, tempFileName, bucketName, mainFileName);
// ์๋ณธ temp ํ์ผ ์ญ์
amazonS3.deleteObject(bucketName, tempFileName);
}
}
/**
* ๊ฐ๊ฒ ์ ๋ณด๋ฅผ ์
๋ฐ์ดํธํฉ๋๋ค.
*/
@Transactional
public void updateMarket(Long marketId, MarketUpdateRequest marketUpdateRequest) {
// ๊ฐ๊ฒ ์กฐํ
Market market = marketRepository.findById(marketId)
.orElseThrow(() -> new MemberException(MarketErrorCode.NOT_FOUND_MARKET_ID));
// ํ ์ค ์๊ฐ ์
๋ฐ์ดํธ
market.updateSummary(marketUpdateRequest.getSummary());
// ์์
์๊ฐ ์
๋ฐ์ดํธ
market.updateBusinessHours(marketUpdateRequest.getOpenAt(), marketUpdateRequest.getCloseAt());
// ํฝ์
์๊ฐ ์
๋ฐ์ดํธ
market.updatePickUpHours(marketUpdateRequest.getPickupStartAt(), marketUpdateRequest.getPickupEndAt());
// ๊ฐ๊ฒ ์ฌ์ง ์
๋ฐ์ดํธ
updateMarketImages(market, marketUpdateRequest.getImageUrls());
s3ImageService.moveImagesToMain(marketUpdateRequest.getImageUrls()); // S3 Bucket: temp -> main
}
3. S3์์ ํ์ผ ์ญ์ ๋ฉ์๋ ์์
temp ํด๋์ main ํด๋๊ฐ ๋๋์๊ธฐ ๋๋ฌธ์, S3์์ ํ์ผ์ ์ญ์ ํ๋ ๋ก์ง๋ ์ด์ ๋ง๊ฒ ๋ณ๊ฒฝํด์ผ ํ๋ค. temp ํด๋์ ์๋ ํ์ผ์ S3 Lifecycle ๊ท์น์ ์ํด ์๋์ผ๋ก ์ญ์ ๋๋ฏ๋ก, main ํด๋์ ์์ ๊ฒฝ์ฐ์๋ง ์ญ์ ํ๋๋ก ์์ ํ๋ค.
/**
* S3์์ ํ์ผ ์ญ์
* main ํด๋์ ์์ ๊ฒฝ์ฐ์๋ง ์ญ์ ํฉ๋๋ค.
*/
public void deleteImage(String imageUrl) {
String decodedFileName = getDecodedFileName(imageUrl);
String mainFileName = "main/" + decodedFileName;
if (doesDecodedImageExist(mainFileName)) {
amazonS3.deleteObject(bucketName, mainFileName);
}
}
๐ ๊ฒฐ๊ณผ ๊ด์ฐฐ
5๋ฒ๊ณผ 6๋ฒ ๋ฐฉ๋ฒ์ ๊ฒฐํฉํ์ฌ ๋ฌธ์ ๋ฅผ ์ฑ๊ณต์ ์ผ๋ก ํด๊ฒฐํ๋ค.
- ์ฌ์ง์ ์ ๋ก๋ํ๋ API๋ฅผ ์คํํ๋ฉด ์ด๋ฏธ์ง๊ฐ ์ผ์์ ์ผ๋ก temp ํด๋์ ์ ์ฅ๋๋ค.
![](https://blog.kakaocdn.net/dn/ulzrb/btsJ34jCr2G/m0dUWsKl8he6p9cC8KXvg1/img.png)
- ์ดํ, ์ฌ์ฉ์๊ฐ ์ฌ์ง ์ ๋ฐ์ดํธ ๋ฒํผ์ ๋๋ฅด๋ฉด ์ด๋ฏธ์ง๊ฐ main ํด๋๋ก ์ด๋๋๋ค.
![](https://blog.kakaocdn.net/dn/bYHQFL/btsJ3YcsOfV/cDJnLoRktcN0QgPJLRR7KK/img.png)
- temp ํด๋์ main ํด๋๊ฐ ๊ฐ๊ฐ ๋ช ํํ๊ฒ ๋ถ๋ฆฌ๋์ด ์ ์์ ์ผ๋ก ์๋ํ๋ค.
![](https://blog.kakaocdn.net/dn/c1LhsU/btsJ4MP3na2/Ru6VKe3Eda4q7lEGwrIrF0/img.png)
๋ก์ง์ ๋ถ๋ฆฌํจ์ผ๋ก์จ ๊ฐ ์์ ์ ์ฑ ์์ ๋ช ํํ ํ ์ ์์๊ณ , S3 Lifecycle ๊ท์น์ ํตํด ๊ณ ์ ๊ฐ์ฒด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ค. ์ด๋ก์จ ๋ฐ์ดํฐ ์ ํฉ์ฑ์ ํจ๊ณผ์ ์ผ๋ก ์ ์งํ ์ ์์๋ค.
๐ก ๊ณ ์ฐฐ
์ด๋ฒ ํด๊ฒฐ์ฑ ์ S3 ๋ฒํท์ ๋จ๋ ๊ณ ์ ๊ฐ์ฒด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๋ฐ ํจ๊ณผ์ ์ด์์ง๋ง, S3์ PutObject, deleteObject, copyObject ์์ ์ ๋ฐ๋ณตํ๋ฉด์ ๋ฐ์ํ๋ ํธ๋ํฝ ๋น์ฉ์ ๊ณ ๋ คํ ํ์๊ฐ ์๋ค. S3๋ ์์ฒญ์ ๋น๋กํด ๋น์ฉ์ด ๋ฐ์ํ๋ฏ๋ก, ๋น๋ฒํ ์ด๋ฏธ์ง ์ ๋ก๋ ๋ฐ ์ญ์ ์์ ์ด ์ด๋ฃจ์ด์ง ๊ฒฝ์ฐ ์์์น ๋ชปํ ๋น์ฉ ์ฆ๊ฐ๊ฐ ๋ฐ์ํ ์ ์๊ธฐ ๋๋ฌธ์ด๋ค.
- ์๋ฒ ์ ์ฅ์๋ฅผ ํ์ฉํ ๋น์ฉ ์ ๊ฐ: ํ์ฌ๋ ์ด๋ฏธ์ง๋ฅผ ์ฆ์ temp ํด๋์ ์ ์ฅํ๊ณ ์์ผ๋, ์ด๋ฅผ ์๋ฒ์ ๋ก์ปฌ ์ ์ฅ์๋ ์บ์์ ์์๋ก ์ ์ฅํ ํ, ํ์ํ ๋ S3๋ก ์ ๋ก๋ํ๋ ๋ฐฉ์์ผ๋ก ๋ณ๊ฒฝํ ์ ์๋ค. ์ด ๋ฐฉ๋ฒ์ ํตํด S3์ ๋ฐ์ดํฐ ์ ์ก ํ์๋ฅผ ์ค์ด๊ณ , 4๋ฒ ๋ฐฉ์์์ ์ค๋ช ํ ์ค์ผ์ค๋ฌ๋ฅผ ํ์ฉํด ์ฃผ๊ธฐ์ ์ผ๋ก ์ด๋ฏธ์ง๋ฅผ S3๋ก ์ ๋ก๋ํ๋ ๋ฐฉ์์ผ๋ก ์ ํํ๋ฉด ํจ์จ์ฑ์ ๋์ผ ์ ์๋ค.
- AWS Lambda ํ์ฉ: AWS Lambda๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋ฒคํธ ๊ธฐ๋ฐ์ผ๋ก ์ด๋ฏธ์ง๋ฅผ ๊ด๋ฆฌํ๋ ๋ฐฉ์๋ ๊ณ ๋ คํด ๋ณผ ์ ์๋ค. Lambda๋ ํน์ ์ด๋ฒคํธ(ํ์ผ ์ ๋ก๋, ํ์ผ ๋ฏธ์ฌ์ฉ ๊ธฐ๊ฐ ๊ฒฝ๊ณผ ๋ฑ)๊ฐ ๋ฐ์ํ ๋ ์๋์ผ๋ก ์คํ๋๋ฏ๋ก, S3 ๋ฒํท์ ์ ์ฅ๋ ํ์ผ๋ค์ ์ฃผ๊ธฐ์ ์ผ๋ก ๊ด๋ฆฌํ ์ ์๋ค. ์ด๋ฅผ ํตํด ๋ถํ์ํ ๊ณ ์ ๊ฐ์ฒด๋ฅผ ์๋์ผ๋ก ์ ๋ฆฌํ๊ณ , ํ์ํ ๋์๋ง S3 ์์ ์ ์ํํ๋ ํจ์จ์ ์ธ ๋ฐฉ๋ฒ์ ๋์ ํ ์ ์๋ค.
์ถ๊ฐ์ ์ธ ๊ณ ์ฐฐ๋ก, ํ์ฌ ์ฌ์ง์ ์ ๋ฐ์ดํธํ S3 ๋ฒํท์ ๊ธฐ์กด ์ฌ์ง๊ณผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ๋ URL์ ๋ชจ๋ ์ญ์ ํ ํ, ์๋ก์ด ์ฌ์ง์ S3 ๋ฒํท์ ์ ๋ก๋ํ ํ DB์ URL์ ์ ์ฅํ๋ ๋ฎ์ด์ฐ๊ธฐ ๋ฐฉ์์ ์ฌ์ฉํ๊ณ ์๋ค. ์ด ๋ฐฉ์์ ๋ถํ์ํ๊ฒ ๋ง์ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ฏ๋ก, ๊ฐ์ ์ด ํ์ํ๋ค๊ณ ์๊ฐํ๋ค.
์ด์ ๊ด๋ จ๋ ์ฌํญ์ ๊ฐ์ ํ์ผ๋ฉฐ, ์์ธํ ๊ฐ์ ๋ด์ฉ์ ์ฌ๊ธฐ๋ฅผ ์ฐธ๊ณ ํ๋ฉด ์ข์ ๊ฒ ๊ฐ๋ค.