Springboot, AWS S3를 이용해서 이미지 처리하기 - (2)

kindof

·

2022. 2. 7. 14:35

0. 들어가면서

이번 시간에는 지난 포스팅에 이어 Springboot와 AWS S3를 이용해서 이미지를 업로드하는 과정을 코드로 살펴보도록 하겠습니다.

 

지난 포스팅에서 S3 버킷 생성과 IAM 사용자 생성을 다뤘으니 이 부분이 궁금하신 분은 아래 글을 참고해주세요. 그리고 이전 글과 마찬가지로 이동욱님의 블로그에서 많은 도움을 받았습니다.

 

Springboot, AWS S3를 이용해서 이미지 처리하기 - (1)

0. 들어가면서 이번 포스팅에서는 Springboot와 AWS S3를 이용해서 이미지를 처리하는 과정을 기록해보려고 합니다. 내용이 길어질 것 같아 두 편으로 나누어서 글을 쓰려고 하는데요. 이번 편에서는

studyandwrite.tistory.com

 

그럼 이제 의존성을 추가하는 과정부터 시작해보겠습니다.

 

1. 의존성 추가(Gradle)

AWS S3를 사용하기 위해서 아래와 같이 build.gradle에 의존성을 추가합니다.

build.gradle

...

repositories {
	mavenCentral()
	maven { url 'https://repo.spring.io/libs-milestone'}
}


dependencies {
	...
	implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.0.1.RELEASE' // s3 설정
    ...
}

...

 

 

2. yml, Config 파일 작성 및 Application 파일 수정

2-1) 먼저 아래와 같이 aws.yml 파일을 생성합니다. credentials에서 accessKey, secretKey는 이전 포스팅에서 csv 파일로 다운받았던 IAM 사용자의 키값입니다.

 

aws.yml

cloud:
  aws:
    s3:
      bucket: S3 버킷 이름
    region:
      static: ap-northeast-2
    credentials:
      accessKey: csv 파일 참고 accessKey
      secretKey: csv 파일 참고 secretKey

 

 

2-2) aws.yml의 내용을 사용하기 위해 Application.java 파일을 아래와 같이 수정합니다.

Application.java

@SpringBootApplication
@EnableJpaAuditing
public class Application {

	public static final String APPLICATION_LOCATIONS = "spring.config.location="
			+ "classpath:application.yml,"
			+ "classpath:aws.yml,"
			+ "classpath:application-oauth.yml";

	public static void main(String[] args) {
		new SpringApplicationBuilder(Application.class)
				.properties(APPLICATION_LOCATIONS)
				.run(args);
	}
}

저같은 경우에는 현재 application.yml과 aws.yml 그리고 oauth 사용을 위한 application-oauth.yml 파일이 존재합니다.

 

 

2-2) S3Config.java 파일을 생성하고, 전역적으로 사용할 변수들을 정의합니다. 그리고 AmazonS3Client를 Bean으로 등록합니다. 이는 아래 코드에서 의존성 주입을 할 때 사용됩니다.

S3Config.java

@Configuration
public class S3Config {

    @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();
    }
}

AmazonS3Client는 aws.yml에서 정의한 accessKey, secretKey, region 등을 이용하여 만든 클라이언트 객체입니다. 이 객체를 통해 S3 버킷에 접근할 수 있습니다.

 

 

 

3. 코드 작성

S3Uploader.java

S3에 정적 파일을 올리는 역할을 하는 S3Uploader.java 파일입니다. 이 클래스를 Service 영역에 둘 지, 아니면 다른 영역(utils, handlers 등)에 두고 사용할 지는 개인이 판단해주시면 될 것 같습니다. 저 같은 경우에는 handler라는 디렉토리를 따로 생성해서 보관했습니다.

@Slf4j
@RequiredArgsConstructor
@Component
public class S3Uploader {

    private final AmazonS3Client amazonS3Client;

    @Value("${cloud.aws.s3.bucket}")
    private String bucket;


    public String upload(MultipartFile multipartFile, String dirName) throws IOException {
        File uploadFile = convert(multipartFile)
                .orElseThrow(() -> new IllegalArgumentException("MultipartFile -> File로 전환이 실패했습니다."));

        return upload(uploadFile, dirName);
    }

    private String upload(File uploadFile, String dirName) {
        String fileName = dirName + "/" + uploadFile.getName();
        String uploadImageUrl = putS3(uploadFile, fileName);
        removeNewFile(uploadFile);
        return uploadImageUrl;
    }

    private String putS3(File uploadFile, String fileName) {
        amazonS3Client.putObject(new PutObjectRequest(bucket, fileName, uploadFile).withCannedAcl(CannedAccessControlList.PublicRead));
        return amazonS3Client.getUrl(bucket, fileName).toString();
    }

    private void removeNewFile(File targetFile) {
        if (targetFile.delete()) {
            log.info("파일이 삭제되었습니다.");
        } else {
            log.info("파일이 삭제되지 못했습니다.");
        }
    }

    private Optional<File> convert(MultipartFile file) throws IOException {
        File convertFile = new File(file.getOriginalFilename());
        System.out.println("convertFile = " + convertFile);
        if(convertFile.createNewFile()) {
            try (FileOutputStream fos = new FileOutputStream(convertFile)) {
                fos.write(file.getBytes());
            }
            return Optional.of(convertFile);
        }
        return Optional.empty();
    }
}

코드를 보면 맨 처음에 AmazonS3Client를 @RequiredArgsConstructor 어노테이션으로 의존성 주입을 받습니다. bucket의 이름으로 @Value("${cloud.aws.s3.bucket}")을 사용합니다. 이는 위에서 설명했듯이 S3Config.java파일에서 가져옵니다.

 

각 코드 부분에 대한 이해는 어렵지 않습니다. MultipartFile을 전달받아서 S3에 전달가능한 형태인 File을 생성하고, 전환된 File을 S3에 public 읽기 권한으로 전송합니다. 그리고 로컬에 생성된 File을 삭제한 뒤, 업로드된 파일의 S3 URL 주소를 반환합니다.

 

위 코드에서 dirName은 AWS S3 버킷 내 디렉토리를 의미합니다. 저같은 경우 아래와 같이 버킷 안에 static이라는 디렉토리를 생성해두었습니다.

S3 디렉토리

 


ItemPostController.java

이 부분부터는 제가 진행하고 있는 프로젝트의 코드이기 때문에 똑같이 따라하시기보다는 어떻게 사용하는지를 보시면 될 것 같습니다.

@RestController
@RequiredArgsConstructor
@RequestMapping("/item-posts")
public class ItemPostController {
    private final ItemPostService itemPostService;
    private final MemberService memberService;
    private final S3Uploader s3Uploader;

	...
    
	// 생성
    @PostMapping("")
    public ResponseEntity<Message> createItemPost(@ModelAttribute ItemPostFileVO itemPostFileVO) throws IOException{
        Member member = memberService.findById(Long.parseLong(itemPostFileVO.getMemberId()));
        ItemPostSaveDto itemPostSaveDto = ItemPostSaveDto.builder()
                .writer(member.getNickname())
                .title(itemPostFileVO.getTitle())
                .description(itemPostFileVO.getDescription())
                .price(itemPostFileVO.getPrice())
                .itemCategory(itemPostFileVO.getItemCategory())
                .build();

        itemPostService.save(itemPostSaveDto, itemPostFileVO.getFiles());
        return new ResponseEntity<>(Message.builder().status(StatusEnum.OK).message("게시물이 등록되었어요.").data(itemPostSaveDto).build(), HttpStatus.OK);
    }
}

컨트롤러에서는 클라이언트로부터 게시물의 작성자와 제목, 내용 등의 정보를 ItemPostFileVO를 통해 받아옵니다.

 

이 때, ItemPostFileVO는 MultipartFile을 아래와 같이 리스트 형태로 가지고 있는데요. 이는 사용자가 게시물에 여러 장의 이미지를 업로드할 수 있기 때문입니다.

ItemPostFileVO.java

@Data
public class ItemPostFileVO {
    private String memberId;
    private String title;
    private String description;
    private Integer price;
    private ItemCategory itemCategory;
    private List<MultipartFile> files;
}

따라서, 위 컨트롤러에서는 ItemPost를 저장하는 Dto(ItemPostSaveDto)를 Build한 뒤 Service 영역에 이미지 처리에 대한 역할을 넘기고 있습니다.

 

ItemPostService.java

@Service
@RequiredArgsConstructor
public class ItemPostService {
    private final ItemPostRepository itemPostRepository;
    private final MemberRepository memberRepository;
    private final PhotoRepository photoRepository;
    private final S3Uploader s3Uploader;

    // 생성
    public void save(ItemPostSaveDto itemPostSaveDto, List<MultipartFile> files) throws IOException {
        Member member = memberRepository.findByNickname(itemPostSaveDto.getWriter());
        ItemPost itemPost = ItemPost.builder()
                .member(member)
                .title(itemPostSaveDto.getTitle())
                .description(itemPostSaveDto.getDescription())
                .price(itemPostSaveDto.getPrice())
                .itemCategory(itemPostSaveDto.getItemCategory())
                .viewCount(0)
                .build();

        files.forEach((f) -> {
            try {
                String S3Url = s3Uploader.upload(f, "static");
                itemPost.addPhoto(photoRepository.save(Photo.builder().path(S3Url).build()));
                itemPostSaveDto.getPhotoList().add(S3Url);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        itemPost.setMember(member);
        itemPostRepository.save(itemPost);
    }
    ...
    ...
}

코드를 보면 Dto를 통해 넘어온 값을 바탕으로 ItemPost 엔티티를 생성합니다. 그리고 MultipartFile 여러 개에 대해 각각 forEach()문을 수행하면서 s3Uploader를 통해 S3 버킷에 이미지를 업로드합니다. 위에서 말씀드렸듯이, 버킷 내 디렉토리 이름이 "static"이기 때문에 dirName에 "static"을 넣어서 넘겨주고 있습니다. 

 

그리고 s3Uploader에서 리턴해주는 S3의 URL 값을 엔티티와 Dto에 넣어주고, 마지막으로 연관관계에 따라 ItemPost에 게시물의 작성자를 저장해준 뒤, itemPostRepository에도 저장해줍니다.

 

* 이 부분에 대한 로직은 코드를 짜는 사람마다 다를 수 있고, 제 코드가 완벽하지 않을 수 있습니다.

 

 

4. Postman으로 결과 확인하기

아래와 같이 Postman으로 사진 데이터와 몇 가지 값을 넣어서 테스트를 해보겠습니다.

Postman 테스트

의도했던대로 결과가 나오는 것을 확인할 수 있습니다.

 

그리고 S3 버킷에 가서 실제로 이미지가 업로드되어 있는지 확인해보겠습니다.

S3 업로드 성공

위에서 요청을 보낸 사진 파일이 성공적으로 버킷 안에 들어가는 것을 볼 수 있습니다.

 

 

5. 나가면서

지금까지 Springboot와 AWS S3를 이용해서 이미지를 처리하는 방법에 대해 살펴보았습니다.

 

위의 Controller와 Service 코드를 보시면 알 수 있는 것처럼, 이미지를 업로드한 뒤에는 엔티티에 S3 URL을 저장하고 있습니다. 이는 나중에 이미지를 로드할 때도 간편하게 URL을 통해 src를 입력할 수 있다는 뜻이니, 이 부분을 참고해서 작성해보시면 좋을 것 같습니다.

 

감사합니다.