๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

๊ฐœ๋ฐœ๊ณต๋ถ€/AWS ๐Ÿ›ฐ๏ธ

AWS S3๋ฅผ ํ™œ์šฉํ•ด ํ”„๋กœ์ ํŠธ ๊ตฌํ˜„ : ์ƒํ’ˆ & ํ”„๋กœํ•„ ์„œ๋น„์Šค ์ด๋ฏธ์ง€ ์Šคํ† ๋ฆฌ์ง€๋กœ ํ™œ์šฉํ•˜๊ธฐ

728x90

๋„์ž… ์ทจ์ง€

ํŒŒ์ผ ์Šคํ† ๋ฆฌ์ง€๋กœ ์‚ฌ์šฉํ•  ์„œ๋น„์Šค๋ฅผ ์ฐพ๋˜ ๋„์ค‘, ์ด๋ฏธ์ง€ ์Šคํ† ๋ฆฌ์ง€๋กœ ๋งŽ์ด ํ™œ์šฉํ•œ๋‹ค๋Š” AWS S3 ์„œ๋น„์Šค์— ๋Œ€ํ•ด ์•Œ๊ฒŒ ๋˜์—ˆ๊ณ , ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์žฅ์ ์„ ๊ทผ๊ฑฐ๋กœ ๋„์ž…ํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค

 

์žฅ์ 

  1. ํ™•์žฅ์„ฑ: S3๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ฑฐ์˜ ๋ฌด์ œํ•œ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ  ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๋น ๋ฅด๊ฒŒ ํ™•์žฅํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์ ํ•ฉํ•˜๋‹ค.
  2. ๋ณด์•ˆ: S3๋Š” ์„œ๋ฒ„ ์ธก ์•”ํ˜ธํ™”, ACL(์•ก์„ธ์Šค ์ œ์–ด ๋ชฉ๋ก) ๋ฐ ๋ฒ„ํ‚ท ์ •์ฑ…์„ ๋น„๋กฏํ•œ ๋‹ค์–‘ํ•œ ๋ณด์•ˆ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณดํ˜ธํ•˜๋‹ค.
  3. ์‚ฌ์šฉ ์šฉ์ด์„ฑ: S3๋Š” ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฝ๊ณ  ๋‹ค๋ฅธ AWS ์„œ๋น„์Šค์™€ ์›ํ™œํ•˜๊ฒŒ ํ†ตํ•ฉ๋˜๋ฏ€๋กœ ํ™•์žฅ ๊ฐ€๋Šฅํ•˜๊ณ  ์•ˆ์ •์ ์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ์ถ•ํ•˜๋ ค๋Š” ๊ฐœ๋ฐœ์ž์—๊ฒŒ ์ ํ•ฉํ•˜๋‹ค.

๊ตฌํ˜„ ๊ธฐ๊ฐ„

02. 28 ~ 03. 03

 

์‹คํ–‰ ํ™˜๊ฒฝ

- Spring boot 3.02

- Java 17

- Gradle

- Spring Cloud Starter aws 2.2.6

 

๊ตฌํ˜„ํ•œ ์ฝ”๋“œ

1. Backend

0) AmazonS3Config

@Configuration
public class AmazonS3Config {


  @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 AmazonS3Client amazonS3Client() {
    BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey);
    return (AmazonS3Client) AmazonS3ClientBuilder.standard()
        .withRegion(region)
        .withCredentials(new AWSStaticCredentialsProvider(awsCreds))
        .build();
  }

}

 

0) application.properties

# multipart
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
spring.file-dir=/home/ubuntu/app/src/main/resources
# aws
cloud.aws.credentials.access-key=ENC(SLFPdcj89oNQLKpRBGoFh/uaCmu7/41Z59rnsX6lMdg=)
cloud.aws.credentials.secret-key=ENC(hBKshx6HaHSeeXsGvqYxXC/MNBAQWZNjk5Jrau0+OnTchrj7HL2Pmm3vGkOq35W9twvLVmFqrKE=)
# S3 bucket region
cloud.aws.region.static=ap-northeast-2
cloud.aws.s3.bucket=knock-knock-image-bucket-1

 

1) S3 Controller

@RestController
@RequiredArgsConstructor
public class S3Controller {

  private final S3ServiceImpl s3Service;

  // ์ด๋ฏธ์ง€ ํ”„๋กœํ•„์šฉ
  @PostMapping("/api/images/upload/profiles")
  public String imageUploadProfile(@RequestParam("profile") MultipartFile multipartFile) throws IOException {
    String s = s3Service.upload(multipartFile, "knock-knock-image-bucket-1", "profile");
    System.out.println(s);
    return s;
  }

  // ์ด๋ฏธ์ง€ ์ƒํ’ˆ์šฉ
  @PostMapping("/api/images/upload/products")
  public String imageUploadProduct(@RequestParam("product") MultipartFile multipartFile) throws IOException {
    String s = s3Service.upload(multipartFile, "knock-knock-image-bucket-1", "product");
    System.out.println(s);
    return s;
  }

}

 

2) S3 Service

@Service
@RequiredArgsConstructor
public class S3ServiceImpl implements S3Service{

  private final AmazonS3Client amazonS3Client;

  @Value("${spring.file-dir}")
  private String fileDirectory;

  // ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง
  @Override
  public String upload(MultipartFile multipartFile, String bucket, String dirName) throws IOException {
    File uploadFile = convert(multipartFile).orElseThrow(
        () -> new IllegalArgumentException("ํŒŒ์ผ ๋ณ€ํ™˜์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค")
    );
    String fileName = dirName + "/" + UUID.randomUUID() + uploadFile.getName();
    String putS3 = putS3(uploadFile, bucket, fileName);
    removeFile(uploadFile);
    return putS3;
  }

  // S3๋กœ ์—…๋กœ๋“œ ~ PutObjectFile ํƒ€์ž…: ๋ฒ„ํ‚ท ์—…๋กœ๋“œ ๋ชฉ์ ์œผ๋กœ ๋งŒ๋“œ๋Š” ๊ฐ์ฒด์˜ URL์„ ๋ฐ˜ํ™˜
  // cannedAcl : ๋ฒ„ํ‚ท ๊ฐ์ฒด๋ฅผ ๋ฒ„ํ‚ท ์ •์ฑ…์— ๋งž๊ฒŒ ์„ค์ •
  @Override
  public String putS3(File uploadFile, String bucket, String fileName) {
    amazonS3Client.putObject(new PutObjectRequest(bucket, fileName, uploadFile)
        .withCannedAcl(CannedAccessControlList.PublicRead));
    return amazonS3Client.getUrl(bucket, fileName).toString();
  }

  // ๋กœ์ปฌ์— ์ €์žฅ๋œ ์ด๋ฏธ์ง€ ์ง€์šฐ๊ธฐ
  @Override
  public void removeFile(File savedLocalFile) {
    if (savedLocalFile.delete()) {
      System.out.println(savedLocalFile.getName() + "์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค");
    } else {
      System.out.println(savedLocalFile.getName() + "์ด ์‚ญ์ œ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค");
    }
  }

  // ์ด๋ฏธ์ง€๋ฅผ File ํƒ€์ž…์œผ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ Optional๋กœ ๋ฐ˜ํ™˜
  @Override
  public Optional<File> convert(MultipartFile multipartFile) throws IOException {
    if(multipartFile.isEmpty()) return Optional.empty();

    // ํŒŒ์ผ ์ด๋ฆ„ ์กฐํšŒ
    String originalFilename = multipartFile.getOriginalFilename();

    // ์ด๋ฆ„๋ณ€๊ฒฝ, ํ˜•์‹์„ file๋กœ ๋ณ€๊ฒฝ ํ›„ ํŒŒ์ผ ํƒ€์ž… ๋ณ€ํ™˜
    File file = new File(fileDirectory + createStoreFileName(originalFilename));
    multipartFile.transferTo(file);

    return Optional.of(file);
  }

  // ํŒŒ์ผ ์ด๋ฆ„์„ ๊ธฐ์กด ํŒŒ์ผ๊ณผ ๊ฒน์น˜์ง€ ์•Š๋„๋ก UUID๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋งŒ๋“ค๊ณ  ํ™•์žฅ์ž๋ฅผ ๋’ค์— ๋ถ™์—ฌ ์ด๋ฏธ์ง€ ํ™•์žฅ์ž๋ฅผ ์œ ์ง€ํ•œ๋‹ค
  @Override
  public String createStoreFileName(String originalFilename) {
    return UUID.randomUUID() + "." + extractExt(originalFilename);
  }

  // ์‚ฌ์šฉ์ž๊ฐ€ ์—…๋กœ๋“œํ•œ ํŒŒ์ผ์—์„œ ํ™•์žฅ์ž๋ฅผ ์ถ”์ถœ
  @Override
  public String extractExt(String originalFilename) {
    int pos = originalFilename.lastIndexOf(".");
    return originalFilename.substring(pos + 1);
  }
}

 

2. Frontend

1) product.js

// ์‚ฌ์ง„ ๋“ฑ๋กํ•˜๊ธฐ
$('#uploadPhoto').click(function(event) {
  event.preventDefault();
  var fileInput = document.getElementById("fileInput");
  var file = fileInput.files[0];
  var formData = new FormData();
  formData.append("product", file);
  
  $.ajax({
    url: URL_VARIABLE + "api/images/upload/products",
    type: 'POST',
    data: formData,
    headers: {
      Authorization: userToken
    },
    processData: false,
    contentType: false,
    success: function(response) {
      console.log(response)
      document.getElementById("image-url").value = response
      let image_preview = response 
      let temp_html = `<img src=${image_preview} style="width: 300px; height: 300px;">`
      
      $('#image-preview').append(temp_html);
    },
    error: function() {
      alert("An error occurred while uploading the file.");
    }
  });
});

 

2) addProduct.html

<tr>
        <th><label for="productPhoto">์ƒํ’ˆ์‚ฌ์ง„</label></th>
        <td><input type="file" id="fileInput" name="fileInput" /></td>
        <td><button class="form-control" id="uploadPhoto">์‚ฌ์ง„ ์—…๋กœ๋“œ</button></td>
      </tr>

 

Sequence Diagram์„ ํ™œ์šฉํ•œ ๊ธฐ๋Šฅ ํ๋ฆ„

 

ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

- ํŒŒ์ผ๋ช…์„ ์ค‘๋ณต๋˜์ง€ ์•Š๊ฒŒ ๋ณ€ํ™˜ํ•˜๊ณ  ์ด๋ฅผ S3์— ์—…๋กœ๋“œํ•˜๊ธฐ ์ „์— ์ €์žฅํ•˜๋Š” ๋กœ์ง์ธ๋ฐ, ์ด๋ฅผ ๋ฐฐํฌํ•˜๊ธฐ ์œ„ํ•ด์„œ ์šฐ๋ถ„ํˆฌ๋ฅผ ํ™œ์šฉํ•ด์•ผ ํ–ˆ๋‹ค. ์ตœ์ข… ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์šฐ๋ถ„ํˆฌ๋กœ ์—ฐ๊ฒฐํ•ด์„œ ๋ฐฐํฌํ•˜๋Š” ๋ถ€๋ถ„์„ ๋‚ด๊ฐ€ ์ง์ ‘ ๊ตฌํ˜„ํ•œ ๊ฒƒ์€ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ดํ›„ ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ์— ์ถ”๊ฐ€๋กœ ์—ฐ์Šตํ•ด๋ด์•ผ๊ฒ ๋‹ค.

์†Œ๊ฐ

๊ฐ„๋‹จํ•˜๋‹ค๋ฉด ๊ฐ„๋‹จํ•˜๊ณ , ๊นŒ๋‹ค๋กญ๋‹ค๋ฉด ๊นŒ๋‹ค๋กœ์šธ ์ˆ˜๋„ ์žˆ๋Š” ๊ธฐ๋Šฅ ๊ตฌํ˜„์ด์—ˆ๋‹ค. ๋‹ค๋งŒ ์ด๋ฅผ ํ†ตํ•ด AWS์™€ ๋”์šฑ ์นœ์ˆ™ํ•ด์งˆ ์ˆ˜ ์žˆ์—ˆ๋‹ค. ํด๋ผ์šฐ๋“œ๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ ํ™œ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ ์  ๋” ์ž์—ฐ์Šค๋Ÿฌ์›Œ์ง€๊ณ  ๋Œ€์„ธ๊ฐ€ ๋˜๊ณ  ์žˆ๋Š” ํ™˜๊ฒฝ์˜ ์•ž์„  ๊ธฐ์ˆ  ์ค‘ ํ•˜๋‚˜๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋˜ ๊ธฐํšŒ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์–ด ๊ฐ’์ง„ ์‹œ๊ฐ„์ด์—ˆ๋‹ค.

728x90

'๊ฐœ๋ฐœ๊ณต๋ถ€ > AWS ๐Ÿ›ฐ๏ธ' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

๊ฐ•์˜ ์ •๋ฆฌ : AWS ์‹ค๋ฌด ๊ธฐ์ดˆ (1)  (0) 2023.02.15
CI/ CD + Github Actions & AWS EC2  (0) 2023.01.31