프로젝트 설정
- Window 10
- Java 11
- Spring boot 2.4.5
- Gradle
- MySql
- Intellj IDEA 2021.1
의존성 추가 (Gradle)
implementation group: 'org.springframework.cloud', name: 'spring-cloud-aws', version: '2.2.6.RELEASE', ext: 'pom'
implementation group: 'com.amazonaws', name: 'aws-java-sdk-s3', version: '1.11.1021'
spring-cloud-aws 와 aws-java-sdk-s3에 대한 의존성 추가합니다.
프로젝트 구조
AWS 설정
aws 설정은 applcation.yml 파일에 작성합니다.
cloud:
aws:
credentials:
accessKey: YOUR_ACCESS_KEY
secretKey: YOUR_SECRET_KEY
s3:
bucket: YOUR_BUCKET_NAME
region:
static: YOUR_REGION
stack:
auto: false
- accessKey,secretKey : aws 계정에 부여된 key 값을 입력합니다.
- s3.buket : S3 서비스에서 생성한 버킷 이름을 작서합니다.
- region.static : 서울은 ap-northeast-2를 작성하면 됩니다.
- stack.auto
stack.auto 는 Spring Cloud 실행 시, 서버 구성을 자동화하는 CloudFormation이 자동으로 실행되는데 이를 사용하지 않겠다는 설정입니다. 해당 설정을 안해주면 아래의 에러가 발생합니다.
org.springframework.beans.factory.BeanCreationException: Error creating bean with name
'org.springframework.cloud.aws.core.env.ResourceIdResolver.BEAN_NAME': Invocation of init method failed;
nested exception is org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'stackResourceRegistryFactoryBean'
defined in class path resource [org/springframework/cloud/aws/autoconfigure/context/ContextStackAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.cloud.aws.core.env.stack.config.StackResourceRegistryFactoryBean]: Factory method 'stackResourceRegistryFactoryBean' threw exception;
nested exception is java.lang.IllegalArgumentException: No valid instance id defined
※ application.yml 파일은 꼭 .gitignore가 되어야 합니다. AWS키가 노출되면 엄청난 과금을 맛볼수 있습니다.
.gitignore
### my ignore ###
src/main/resources/application.yml
GalleryController.java
@Controller
@AllArgsConstructor
public class GalleryController {
private final S3Service s3Service;
private final GalleryService galleryService;
@GetMapping("/gallery")
public String dispWrite(Model model) {
List<GalleryDto> galleryDtoList = galleryService.getList();
model.addAttribute("galleryList", galleryDtoList);
return "/gallery";
}
@PostMapping("/gallery")
public String execWrite(GalleryDto galleryDto, MultipartFile file) throws IOException {
String imgPath = s3Service.upload(galleryDto.getFilePath(), file);
galleryDto.setFilePath(imgPath);
galleryService.savePost(galleryDto);
return "redirect:/gallery";
}
}
s3Service.upload(file) : s3Service는 AWS S3의 비즈니스 로직을 담당하며, 파일을 조작합니다.
galleryService.savePost(galleryDto) : galleryService는 DB에 데이터를 조작하기 위한 서비스입니다.
S3Service
@Service
@NoArgsConstructor
public class S3Service {
private AmazonS3 s3Client;
public static final String CLOUD_FRONT_DOMAIN_NAME ="d2endhb8pwljj4.cloudfront.net";
@Value("${cloud.aws.credentials.accessKey}")
private String accessKey;
@Value("${cloud.aws.credentials.secretKey}")
private String secretKey;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
@Value("${cloud.aws.region.static}")
private String region;
@PostConstruct
public void setS3Client() {
AWSCredentials credentials = new BasicAWSCredentials(this.accessKey, this.secretKey);
s3Client = AmazonS3ClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withRegion(this.region)
.build();
}
public String upload(String currentFilePath, MultipartFile file) throws IOException {
// 고유한 key 값을 갖기위해 현재 시간을 postfix로 붙여줌
SimpleDateFormat date = new SimpleDateFormat("yyyymmddHHmmss");
String fileName = file.getOriginalFilename() + "-" + date.format(new Date ());
// key가 존재하면 기존 파일은 삭제
if ("".equals(currentFilePath) == false && currentFilePath != null) {
boolean isExistObject = s3Client.doesObjectExist(bucket, currentFilePath);
if (isExistObject == true) {
s3Client.deleteObject(bucket, currentFilePath);
}
}
// 파일 업로드
s3Client.putObject(new PutObjectRequest(bucket, fileName, file.getInputStream(), null)
.withCannedAcl(CannedAccessControlList.PublicRead));
return fileName;
}
}
@Value("${cloud.aws.credentials.accessKey}")
- lombok 패키지가 아닌, org.springframework.beans.factory.annotation 패키지임에 유의합니다.
- 해당 값은 application.yml에서 작성한 cloud.aws.credentials.accessKey 값을 가져옵니다.
@PostConstruct
- 의존성 주입이 이루어진 후 초기화를 수행하는 메서드이며, bean이 한 번만 초기화 될수 있도록 해줍니다.
- 이렇게 해주는 목적은 AmazonS3ClientBuilder를 통해 S3 Client를 가져와야 하는데, 자격증명을 해줘야 S3 Client를 가져올 수 있기 때문입니다.
s3Client.putObject(new PutObjectRequest(bucket, fileName, file.getInputStream(), null)
업로드를 하기 위해 사용되는 함수입니다.
s3Client.getUrl(bucket, fileName).toString()
업로드를 한 후, 해당 URL을 DB에 저장할 수 있도록 컨트롤러로 URL을 반환합니다.
GalleryService
@Service
@AllArgsConstructor
public class GalleryService {
private GalleryRepository galleryRepository;
private S3Service s3Service;
public void savePost(GalleryDto galleryDto) {
galleryRepository.save(galleryDto.toEntity());
}
public List<GalleryDto> getList() {
List<GalleryEntity> galleryEntityList = galleryRepository.findAll();
List<GalleryDto> galleryDtoList = new ArrayList<> ();
for (GalleryEntity galleryEntity : galleryEntityList) {
galleryDtoList.add(convertEntityToDto(galleryEntity));
}
return galleryDtoList;
}
private GalleryDto convertEntityToDto(GalleryEntity galleryEntity) {
return GalleryDto.builder()
.id(galleryEntity.getId())
.title(galleryEntity.getTitle())
.filePath(galleryEntity.getFilePath())
.imgFullPath ("https://" + s3Service.CLOUD_FRONT_DOMAIN_NAME+"/"+ galleryEntity.getFilePath())
.build();
}
}
DB에 저장하는 로직입니다.
GalleryRepository
public interface GalleryRepository extends JpaRepository<GalleryEntity, Long> {
@Override
List<GalleryEntity> findAll();
}
GalleryEntity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
@Table(name = "gallery")
public class GalleryEntity {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
private Long id;
@Column(length = 10, nullable = false)
private String title;
@Column(columnDefinition = "TEXT")
private String filePath;
@Builder
public GalleryEntity(Long id, String title, String filePath) {
this.id = id;
this.title = title;
this.filePath = filePath;
}
}
filePath 필드로 AWS S3에 저장된 파일 경로를 DB에 저장합니다.
GalleryDto
@Getter
@Setter
@ToString
@NoArgsConstructor
public class GalleryDto {
private Long id;
private String title;
private String filePath;
private String imgFullPath;
public GalleryEntity toEntity(){
GalleryEntity build = GalleryEntity.builder()
.id(id)
.title(title)
.filePath(filePath)
.build();
return build;
}
@Builder
public GalleryDto(Long id, String title, String filePath, String imgFullPath) {
this.id = id;
this.title = title;
this.filePath = filePath;
this.imgFullPath = imgFullPath;
}
}
서비스 구현- 퍼블리싱
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>S3 파일 업로드 테스트</title>
</head>
<body>
<h1>파일 업로드</h1> <hr>
<form th:action="@{/gallery}" method="post" enctype="multipart/form-data">
제목 : <input type="text" name="title"> <br>
파일 : <input type="file" name="file"> <br>
<button>등록하기</button>
</form>
<hr>
<div th:each="gallery : ${galleryList}">
<div style="margin-bottom: 30px">
<p th:inline="text">제목 : [[${gallery.title}]]</p>
<img th:src="${gallery.imgFullPath}" style="width: 500px; height: 300px;">
</div>
<form th:action="@{/gallery}" method="post" enctype="multipart/form-data">
<input type="hidden" name="id" th:value="${gallery.id}">
<input type="hidden" name="title" th:value="${gallery.title}">
<input type="hidden" name="filePath" th:value="${gallery.filePath}">
파일 : <input type="file" name="file"> <br>
<button>수정하기</button>
</form>
<hr>
</div>
</body>
</html>
'Spring' 카테고리의 다른 글
JpaReporitory vs EntityManager (0) | 2021.11.23 |
---|---|
엔티티 설계 주의점 (0) | 2021.11.21 |
Maven vs Gradle (0) | 2021.11.05 |
Spring 이란? (0) | 2021.07.14 |
JAR vs WAR (0) | 2021.06.18 |