HyeLog
[Spring Boot + Amazon S3] - μ΄λ―Έμ§ μ λ‘λ, μ‘°ν λ°©λ² λ³Έλ¬Έ
[Spring Boot + Amazon S3] - μ΄λ―Έμ§ μ λ‘λ, μ‘°ν λ°©λ²
shj718 2023. 7. 6. 22:49π κ°λ° νκ²½
μ΄λ² νλ‘μ νΈμμ μ΄λ―Έμ§ νμΌμ Amazon S3μ μ λ‘λ(μ μ₯) / μ‘°ν / μμ νλ κΈ°λ₯μ κ°λ°νκ² λμλ€.
μ°Έκ³ λ‘ νλ‘ νΈμλμμλ Next.js λ₯Ό μ¬μ©νκ³ , λ°±μλμμλ Spring Boot λ₯Ό μ¬μ©νμ¬ κ°λ°μ μ§ννκ³ μλ€.
λ°λΌμ Spring Boot, Amazon S3 λ₯Ό μ¬μ©νμ¬ μ΄λ―Έμ§ νμΌμ μ λ‘λ(μ μ₯)νκ³ λ€μ μ‘°ν λ° μμ νλ λ°©λ²μ λν΄ μ°¨κ·Όμ°¨κ·Ό μ μ΄λ³΄λ € νλ€.
λΉμ·ν νκ²½μμ κ°λ°νλ μ¬λλ€μκ² λμμ΄ λμμΌλ©΄ νλ λ§μμ΄λ€!
π₯ μ 체μ μΈ Flow
1. νλ‘ νΈμλμμ μ΄λ―Έμ§ νμΌμ Form data λ‘ ν¬μ₯νμ¬ λ°±μλμκ² μ λ¬
2. λ°±μλμμ ν΄λΉ μ΄λ―Έμ§λ₯Ό Amazon S3 μ μ λ‘λ, μ λ‘λλ μ΄λ―Έμ§ μ 보λ₯Ό DBμ μ μ₯
3. νλ‘ νΈμλμμ μ΄λ―Έμ§ μ‘°νμ, λ°±μλμμλ S3μ μ μ₯λ ν΄λΉ μ΄λ―Έμ§μ URL μ μμ±
4. μμ±λ μ΄λ―Έμ§ URLμ νλ‘ νΈμλμκ² μ λ¬
5. νλ‘ νΈμλμμ ν΄λΉ URLλ‘ μ΄λ―Έμ§λ₯Ό νμνκ±°λ λ€μ΄λ‘λ
μ°Έκ³ λ‘ κ΅¬νμ μμ S3 λ²ν· μμ±κ³Ό IAM μ¬μ©μ μ€μ μ ν΄μΌ νλ€. νμλ μ΄ λ§ν¬λ₯Ό μ°Έκ³ νλ€.
λ€λ§, νμ¬λ IAM μ¬μ©μ μμ±μ μλ£ν μ΄νμ μΆκ°λ‘ μ‘μΈμ€ ν€λ₯Ό λ§λ€μ΄μΌ νλ€.
μ€μν κ²μ λ²ν·μ μ κ·ΌνκΈ° μν΄ νμν μ‘μΈμ€ ν€(.csv νμΌ)λ₯Ό λ°λμ μ μ₯ν΄λμ΄μΌ νλ€λ μ μ΄λ€!!
μΆκ°λ‘ S3 λ²ν·μ Cors μ€μ μ μ΄ λ§ν¬μ AWS 곡μ λ¬Έμλ₯Ό μ°Έκ³ νλ€.
μ΄μ , 본격μ μΌλ‘ Spring Boot μ ꡬνν΄λ³΄μ.
π build.gradle
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
π application-aws.yaml
μ°λ¦¬ νλ‘μ νΈμμλ μ¬λ¬κ°μ μ€μ νμΌ(application-database.yaml λ±)μ μ¬μ©νκ³ μκΈ° λλ¬Έμ S3 κ΄λ ¨ μ€μ μ application-aws.yaml νμΌμ λ§λ€μ΄μ ν΄μ£Όμλ€.
μ΄λ¬ν μ€μ νμΌμ μ λ! κΉνλΈμ λ ΈμΆλλ©΄ μλκΈ° λλ¬Έμ .gitignore μ μΆκ°ν΄μ£Όκ³ , κΉνλΈ Actions μ Secrets μ application-aws.yaml νμΌ λ΄μ©μ μΆκ°νλ€.
spring:
servlet:
multipart:
max-request-size: 10MB
max-file-size: 10MB
cloud:
aws:
credentials:
access-key: {μ‘μΈμ€ ν€}
secret-key: {μν¬λ¦Ώ ν€}
s3:
bucket: {λ²ν· μ΄λ¦}
region:
static: ap-northeast-2
stack:
auto: false
π IntelliJ μ€μ
Spring Cloud AWS μμ‘΄μ±μ μΆκ°ν ν μ ν리μΌμ΄μ μ μ€ννλ©΄ μ½μμ μλ¬λ©μΈμ§κ° μΆλ ₯λλ€.
μ΄λ₯Ό λ°©μ§νκΈ° μν΄ [Run] > [Edit Configurations] μμ VM μ΅μ μ μλ λ΄μ©μ μΆκ°νμ.
-Dcom.amazonaws.sdk.disableEc2Metadata=true
(μ°Έκ³ : https://thalals.tistory.com/289 , https://kim-jong-hyun.tistory.com/79)
π AwsS3Config μμ±
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AwsS3Config {
@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 BasicAWSCredentials basicAWSCredentials() {
return new BasicAWSCredentials(accessKey, secretKey);
}
@Bean
public AmazonS3 amazonS3(BasicAWSCredentials basicAWSCredentials) {
return AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(basicAWSCredentials))
.build();
}
}
π AwsS3Controller μμ±
awsS3Service.upload() μ λλ²μ§Έ μΈμλ λ²ν· νμμ μμ±ν ν΄λ μ΄λ¦μ΄λ€.
μ¦, μλ‘μ΄ μ΄λ―Έμ§ νμΌμ μ λ‘λ νλ©΄ {λ²ν· μ΄λ¦}/test/ ν΄λ μμ μμ±λλ€.
import repick.repickserver.domain.product.domain.AwsS3;
import repick.repickserver.domain.product.application.AwsS3Service;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
@RestController
@RequestMapping("/s3")
@RequiredArgsConstructor
public class AwsS3Controller {
private final AwsS3Service awsS3Service;
@PostMapping("/resource")
public AwsS3 upload(@RequestPart("file") MultipartFile multipartFile) throws IOException {
return awsS3Service.upload(multipartFile,"test");
}
@DeleteMapping("/resource")
public void remove(AwsS3 awsS3) {
awsS3Service.remove(awsS3);
}
}
π AwsS3Service μμ±
S3 μ μ΄λ―Έμ§ νμΌμ μ λ‘λ νλ Flow λ λ€μκ³Ό κ°λ€.
- MultipartFile μ File λ‘ λ³νν΄μ νμ¬ νλ‘μ νΈ κ²½λ‘μ μ λ‘λ νλ€.
- λ³νλ File μ S3μ μ λ‘λ νλ€. (μ΄λ νμΌλͺ μ UUIDλ₯Ό λΆμ¬ μλ‘κ² μ ν΄μ€λ€.)
- S3μ μ λ‘λ λ νμΌμ key μ path λ₯Ό λ°ννλ€. (μλμμ μ€λͺ )
- λ³ν κ³Όμ μμ νμ¬ νλ‘μ νΈ κ²½λ‘μ μμ±λ νμΌμ μ κ±°νλ€.
μ¬κΈ°μ key λ κ°μ²΄ μ΄λ¦(νμΌλͺ ) μ΄κ³ , path λ κ°μ²΄ URL μ΄λ€.
μλ₯Ό λ€μ΄, key λ "example.jpg" μ΄κ³ , path λ "https://my-s3-bucket.s3.ap-northeast-2.amazonaws.com/example.jpg" μ΄λ€.
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.AmazonS3Exception;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.PutObjectRequest;
import repick.repickserver.domain.product.domain.AwsS3;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
@Service
@RequiredArgsConstructor
public class AwsS3Service {
private final AmazonS3 amazonS3;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
public AwsS3 upload(MultipartFile multipartFile, String dirName) throws IOException {
File file = convertMultipartFileToFile(multipartFile)
.orElseThrow(() -> new IllegalArgumentException("MultipartFile -> File λ³ν μ€ν¨"));
return upload(file, dirName);
}
private AwsS3 upload(File file, String dirName) {
String key = randomFileName(file, dirName);
String path = putS3(file, key);
removeFile(file); // λ‘컬μ μμ±λ File μμ (MultipartFile -> File λ³ν ν λ λ‘컬μ νμΌ μμ±λ¨)
return AwsS3
.builder()
.key(key)
.path(path)
.build();
}
private String randomFileName(File file, String dirName) {
return dirName + "/" + UUID.randomUUID() + file.getName();
}
private String putS3(File uploadFile, String fileName) {
amazonS3.putObject(new PutObjectRequest(bucket, fileName, uploadFile)
.withCannedAcl(CannedAccessControlList.PublicRead));
return getS3(bucket, fileName);
}
private String getS3(String bucket, String fileName) {
return amazonS3.getUrl(bucket, fileName).toString();
}
private void removeFile(File file) {
file.delete();
}
public Optional<File> convertMultipartFileToFile(MultipartFile multipartFile) throws IOException {
File file = new File(System.getProperty("user.dir") + "/" + multipartFile.getOriginalFilename());
if (file.createNewFile()) {
try (FileOutputStream fos = new FileOutputStream(file)){
fos.write(multipartFile.getBytes());
}
return Optional.of(file);
}
return Optional.empty();
}
public void remove(AwsS3 awsS3) {
if (!amazonS3.doesObjectExist(bucket, awsS3.getKey())) {
throw new AmazonS3Exception("κ°μ²΄ " +awsS3.getKey()+ " μ΄/κ° μ‘΄μ¬νμ§ μμ΅λλ€.");
}
amazonS3.deleteObject(bucket, awsS3.getKey());
}
}
π AwsS3 μμ±
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class AwsS3 {
private String key; // νμΌλͺ
private String path; // νμΌ κ²½λ‘ (κ°μ²΄ URL)
public AwsS3() {
}
@Builder
public AwsS3(String key, String path) {
this.key = key;
this.path = path;
}
}
π ν μ€νΈ
μ΄μ Postman μΌλ‘ ν μ€νΈ ν΄λ³΄μ.
μ΄λ―Έμ§ μ λ‘λλ₯Ό μν POST μμ²μ 보λ΄κ³ ,
AWS μ κ°μ λ²ν·μ νμΈν΄λ³΄λ©΄ μ§μ! test ν΄λ μμ μ΄λ―Έμ§κ° μ μ
λ‘λ λμλ€. π
κ°μ²΄ μ΄λ¦μ μ νν΄μ κ°μ²΄ URLμ λλ₯΄λ©΄,
μ΄λ κ² λ°©κΈ μ¬λ¦° μ¬μ§μ΄ μ 보μΈλ€ π½
Postman μλ΅μΌλ‘λ λ°©κΈ μ λ‘λν μ΄λ―Έμ§ νμΌμ key μ path κ° μ μ€λ κ²μ νμΈν μ μλ€!!
μ°Έκ³ :
'μΉ κ°λ° > Spring Boot' μΉ΄ν κ³ λ¦¬μ λ€λ₯Έ κΈ
[Spring Boot] λ€μ΄λ² SENS λ‘ λ¬Έμ(SMS) μ μ‘νκΈ° (0) | 2023.07.28 |
---|---|
[QueryDSL] No Offset νμ΄μ§, @QueryProjection μΌλ‘ DTO λ°ννκΈ° (0) | 2023.07.17 |
[Spring Data JPA] N+1 λ¬Έμ , Fetch Join λ° Entity Graph λ‘ ν΄κ²° (0) | 2023.07.05 |
[Spring Data JPA] SQLμ IN - 컬λ μ νλΌλ―Έν° λ°μΈλ© (0) | 2023.07.04 |
[Spring Data JPA] @Query μ‘°ν κΈ°λ₯ (0) | 2023.07.04 |