Spring

Spring boot S3 업로드, 수정, 삭제

JUNGKEUNG 2021. 6. 18. 17:51

프로젝트 설정


  • 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-awsaws-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