프로그래밍/해외인턴 개발일지

[인턴 일지] Amazon S3 이미지 업로드, 다운로드

Jay Tech 2017. 12. 1. 07:16
반응형

원래는 각 직원 한 명당 사진 1개씩으로 정해졌지만 갑자기 한 사람당 여러 사진들을 업로드하는 기능이 있었으면 좋겠다고 한다. 자꾸 말 바꾸면 기간만 길어지고 개발진행에 방해가 될거같다고 했다.


예상은 하고 있었다. 결국 알겠다고 바꾸기로 했다. 그러면 아마존 버킷에 각 직원별로 폴더가 생겨야 할것이고 각 폴더에 동적으로 원하는 만큼의 이미지 또는 파일들이 올라가게 될 것이다. 


오래 걸릴거 같다고 했는데 2시간만에 끝내버렸다.


제일 먼저 규칙을 정했다. 



버킷내에 폴더 명 규칙Last Name + First Name + SSN Number (소셜넘버) 으로 정했다. 처음에는 그냥 소셜 넘버만으로 폴더를 만들기로했는데 생각해보니까 버킷을 열었을 때 가독성이 좀 떨어질 것 같아서 이름까지 넣기로했다. 이름만으로 폴더이름을 지으면 동명이인이 있기 때문에 안되는 것은 당연하다.







먼저 사진 업로드 폼이다.


1
2
3
4
5
6
    <form method="POST" enctype="multipart/form-data" id="fileUploadForm">
        <input type="file" name="files" multiple/><br/><br/>
        <input type="hidden" name="ssn_num" id="ssn_num"/>
        <input type="submit" value="Submit" id="btnSubmit"/>
    </form>
    
cs



스크립트이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
$("#btnSubmit").click(function (event) {
 
            //stop submit the form, we will post it manually.
            event.preventDefault();
 
            // Get form
            var form = $('#fileUploadForm')[0];
 
            // Create an FormData object
            var data = new FormData(form);
 
            data.append("frst_nm""${frst_nm}");
            data.set("last_nm""${last_nm}");
 
            // disabled the submit button
            $("#btnSubmit").prop("disabled"true);
 
            $.ajax({
                type: "POST",
                enctype: 'multipart/form-data',
                url: "uploadImages.do?${_csrf.parameterName}=${_csrf.token}",
                data: data,
                processData: false,
                contentType: false,
                cache: false,
                timeout: 600000,
                success: function (data) {
 
                    $("#result").text(data);
                    console.log("SUCCESS : ", data);
                    $("#btnSubmit").prop("disabled"false);
 
                },
                error: function (e) {
 
                    $("#result").text(e.responseText);
                    console.log("ERROR : ", e);
                    $("#btnSubmit").prop("disabled"false);
 
                }
            });
 
        });
 
cs


FormData라는 것을 이용하였다. 파일을 업로드할 때 스크립트단에서 처리하고 넘길수가 있다. 


line 12, 13 : 폼 데이터가 데이터를 더 물고 갈 수가 있는데 2가지 메소드가 있었다.


모질라 공식 문서에서 찾아보았다.

https://developer.mozilla.org/en-US/docs/Web/API/FormData/set



The difference between set() and FormData.append is that if the specified key does already exist, set() will overwrite all existing values with the new one, whereas FormData.append will append the new value onto the end of the existing set of values.


set은 key가 존재한다면 원래 value를 지우고 덮어쓰는 것이고 append는 key가 존재한다면 뒤에다가 갖다 붙이는 것이다. 그냥 테스트로 둘 다 해보았다. 어차피 key가 다 새로 만들어지는 것이므로 내가 하려는 동작에는 둘 중 무엇을 써도 상관이 없다. 그래도 set이 make sense하므로 set으로 다 바꿨다.



이거는 요구사항이 바뀌어서 추가한 부분이다.


1
2
3
4
5
<form id="image" name="image">
    <input type="hidden" id="ssn_num" name="ssn_num" value="${empAllData.SSN_NUM}"/>
    <input type="hidden" id="frst_nm" name="frst_nm" value="${empAllData.FRST_NM}"/>
    <input type="hidden" id="last_nm" name="last_nm" value="${empAllData.LAST_NM}"/>
</form>
cs


hidden으로 ssn number, lastname, firstname을 넘긴다.



받는 컨트롤러 쪽이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 
    /**
     * Upload multiple images to s3
     *
     * @author Jun Ho Park
     *
     * @param files
     * @param ssn_num
     * @param request
     * @return
     * @throws Exception
     */
    @RequestMapping(value="uploadImages.do", method=RequestMethod.POST )
    public     @ResponseBody String initUpload2(@RequestParam("files") MultipartFile[] files, 
            @RequestParam("ssn_num"String ssn_num, @RequestParam String frst_nm, @RequestParam String last_nm, HttpServletRequest request) throws Exception {
 
        try{
 
            AWSCredentials credentials = new BasicAWSCredentials("xxx""xxx");
            AmazonS3 s3Client = new AmazonS3Client(credentials);
            
            logger.info("ssn_num : " + ssn_num);
            logger.info("first_name : " + frst_nm);
            logger.info("last_name  : " + last_nm);
 
            s3Service.multiImagesUpload(ssn_num, last_nm + "_" + frst_nm, files, s3Client);
                        
            System.out.println("---------------- START UPLOAD FILE ----------------");
            
            return "completed! select another files you want";
        } catch(Exception e) {
            e.printStackTrace();
        } 
        
        return "completed! select another files you want";
    }
 
cs


line 14 : 내가 ajax로 넘기고 Responsebody를 썼다. 자바 객체를 HTTP응답몸체로 전송한다. 그래서 url상에 정보가 표시되어버린다. (이 부분을 나중에 개선해야 한다. 개인정보가 그대로 노출되기 때문이다)


line 19 : 저거는 아마존 public private 키인데 보안상 생략했다.


line 20 : 생성한 credential로 아마존 s3객체를 만든다.


line 26 : 서비스 쪽으로 위임한다. 


line 30 : ajax success 시 반환되는 스트링인데 ui상에 그냥 저 텍스트로 뿌려주려고 리턴타입을 string으로 했다.




서비스 단이다.


1
2
3
4
5
6
7
8
9
10
11
12
/**
     * upload multiple images to s3 bucket
     *
     * @author Jun Ho Park
     *
     * @param ssn_num
     * @param file
     * @param s3client
     * @throws Exception
     */
public void multiImagesUpload(String ssn_num, String folderName, MultipartFile[] file, AmazonS3 s3client) throws Exception;
 
cs



여러 장의 이미지를 올리는 함수머리를 작성하고 멀팟은 배열로 넘겼다. 하나여도 상관없고 여러장도 상관없다.


그리고 로직을 처리하는 유틸 클래스를 따로 만들었다. util 폴더를 만들고 업로드와 다운로드를 관장하는 클래스를 만들었다.






그리고 서비스 임플 단이다. 이 함수안에서 유틸 클래스를 부른다. 그래서 static으로 정의하여 놓았다. 

(근데 이상하게 유틸에서 Properties를 부르는게 계속 안되서 임플안에서 Properties에서 버킷이름을 가지고 왔다. 그래서 좀 어설프게 나눠졌다....ㅠ)


1
2
3
4
5
6
@Override
    public void multiImagesUpload(String ssn_num, String folderName, MultipartFile[] file, AmazonS3 s3client) throws Exception {
        String bucketName = fileProperties.getProperty("park.s3.bucket");
        
        ImageUploadUtil.multiImagesUpload(ssn_num, folderName, file, s3client, bucketName);
    }
cs




이미지 업로드를 담당하는 유틸 클래스이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
 
public final class ImageUploadUtil {
    
    private static Logger logger = LoggerFactory.getLogger(ImageUploadUtil.class);
 
    public static void multiImagesUpload(String ssn_num, String folderName, MultipartFile[] file, AmazonS3 s3client, String bucketName) {
 
        ObjectMetadata omd = new ObjectMetadata();
        
        try {            
            // 사진 여러개 업로드
            for(int i=0; i<file.length; i++) {
                omd.setContentType(file[i].getContentType());
                omd.setContentLength(file[i].getSize());
                omd.setHeader("filename", file[i].getOriginalFilename());
                
                PutObjectRequest putObjectRequest = 
                        new PutObjectRequest(bucketName, folderName + "_" + ssn_num + "/" + folderName + ssn_num + "_" + System.currentTimeMillis(), file[i].getInputStream(), omd);
                
                putObjectRequest.setCannedAcl(CannedAccessControlList.Private);
                s3client.putObject(putObjectRequest);
                logger.info("======== Upload "+i+1+" completed !!!! =======");
            }
 
        } catch (AmazonServiceException ase) {
            logger.info("Caught an AmazonServiceException from PUT requests, rejected reasons:");
            logger.info("Error Message:    " + ase.getMessage());
            logger.info("HTTP Status Code: " + ase.getStatusCode());
            logger.info("AWS Error Code:   " + ase.getErrorCode());
            logger.info("Error Type:       " + ase.getErrorType());
            logger.info("Request ID:       " + ase.getRequestId());
        } catch (AmazonClientException ace) {
            logger.info("Caught an AmazonClientException: ");
            logger.info("Error Message: " + ace.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
        }
        
    }
    
cs



line 8 : 오브젝메타데이터 객체를 만든다.


com.amazonaws.services.s3.model

Class ObjectMetadata

아마존 도큐먼트에서 발췌하였다. s3에 저장되는 메타데이터를 설정하는 기능을 한다.


line 13 부터 파일의 메타데이터들을 설정한다. 크기 이름 등등을 말이다. 이게 멀팟이 여러개가 들어왔으므로 배열로 돌린다.


line 17 : 이제 실제 올리는 기능을 하는 객체를 생성한다.


Class PutObjectRequest

특정 s3 버킷에 올리는 기능을 한다. 메타데이터와 canned access control 정책을 적용한다고 한다. 즉 올라가는 s3 객체의 권한을 지정한다. 보안상 외부에 공개되면 안되므로 private으로 설정을 할 것이다.


line 18 : 이제 버킷이름, 폴더이름, 파일스트림, 메타데이터 순으로 초기화를 한다. 멀팟에서 getInputStream()으로 스트림을 연다. 그리고 파일이름 (s3 에서는 KeyName이라고 한다)이 서로 겹치면 안되므로 밀리세컨단위를 이용하여 파일 이름을 지었다. 

System.currentTimeMillies() 로 밀리초 단위로 이름을 짓는다. 이렇게 이름을 지으면 겹칠 일이 없다.


그리고 버킷내에 폴더는 / (슬래쉬) 로 구분한다. 즉 [폴더이름]/[파일이름] 형식으로 던지면 된다. 폴더이름이 존재하지 않으면 생성하게 되고 존재한다면 그냥 그 안으로 쏙 들어가게 된다.


line 20 : 보안 정책을 Private으로 설정한다.


line 21 : 실제 업로드를 진행한다! 아마존 콘솔을 열어서 확인해 보면 정상적으로 잘 올라가있는 것을 볼 수 있다.





----이제 다운로드 부분이다----


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
     * Multiple images upload & Show multiple images page
     *
     * @author Jun Ho Park
     *
     * @param ssn_num
     * @param frst_nm
     * @param last_nm
     * @param model
     * @return
     * @throws Exception
     */
    @RequestMapping(value="image.do")
    public String initImage(@RequestParam String ssn_num, @RequestParam String frst_nm, @RequestParam String last_nm, ModelMap model) throws Exception {
    
        System.out.println("image.do");
        
        String bucketName = fileProperties.getProperty("park.s3.bucket");
        
        List<String> employeeImageList = new ArrayList<>();
        
        // 버킷 열어서 검사해야 함
        
        try{
            
            AWSCredentials credentials = new BasicAWSCredentials("xxx""xxx");
            AmazonS3 s3Client = new AmazonS3Client(credentials);
            
            logger.info("bucketName : " + bucketName);
            
            employeeImageList = ImageDownloadUtil.getEmployeeImagesList(s3Client, bucketName, last_nm, frst_nm, ssn_num);
 
        } catch(Exception e) {
            e.printStackTrace();
        } 
        
        
        model.addAttribute("frst_nm", frst_nm);
        model.addAttribute("last_nm", last_nm);
        model.addAttribute("ssn_num", ssn_num); 
        
        model.addAttribute("employeeImageList", employeeImageList);
        
        return "employeeImage/employeeImage.tiles";
    }
 
cs



line 20 : 각 직원들 파일의 이름을 담기 위해 list 를 생성한다.


line 31 : 다운 로드 유틸을 열어서 리스트를 가져온다. 이때 필요한 것은 버킷이름, 파일이름(키 네임) 이다.


line 42 : View 단에 list를 내려주기 위해 ModelMap을 이용한다.




다운로드 유틸이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public final class ImageDownloadUtil {
    private static Logger logger = LoggerFactory.getLogger(ImageDownloadUtil.class);
 
    public static List<String> getEmployeeImagesList(AmazonS3 s3Client, String bucketName, String last_nm, String frst_nm, String ssn_num) throws Exception {
        
        List<String> employeeImageList = new ArrayList<String>();
        
        ListObjectsRequest listObjectsRequest = new ListObjectsRequest();
        listObjectsRequest.setBucketName(bucketName);
        listObjectsRequest.setPrefix(last_nm + "_" + frst_nm + "_" + ssn_num);
        
        ObjectListing objects = s3Client.listObjects(listObjectsRequest);
        
        do {
            objects = s3Client.listObjects(listObjectsRequest);
            
            for(S3ObjectSummary objectSummary : objects.getObjectSummaries()) {
                logger.info("keyname : " + objectSummary.getKey());
                employeeImageList.add(objectSummary.getKey());
            }
            
            listObjectsRequest.setMarker(objects.getNextMarker());
        } while(objects.isTruncated());
        
        return employeeImageList;
    }
}
cs


line 8 : ListObjectRequest를 생성한다.


Class ListObjectsRequest

  • All Implemented Interfaces:
    HandlerContextAwareReadLimitInfoSerializableCloneable


    public class ListObjectsRequest
    extends AmazonWebServiceRequest
    implements Serializable

    Contains options to return a list of summary information about the objects in the specified bucket. Depending on the request parameters, additional information is returned, such as common prefixes if a delimiter was specified. List results are always returned in lexicographic (alphabetical) order.

    Buckets can contain a virtually unlimited number of keys, and the complete results of a list query can be extremely large. To manage large result sets, Amazon S3 uses pagination to split them into multiple responses. Always check the ObjectListing.isTruncated() method to see if the returned listing is complete, or if callers need to make additional calls to get more results. Alternatively, use the AmazonS3Client.listNextBatchOfObjects(ObjectListing) method as an easy way to get the next page of object listings.

특정 버킷에 대한 정볼르 얻기 위해 사용한다. 버킷은 들어가는 파일이 무한대 이므로 검색시간이 오래 걸릴 것을 대비해 자체적으로 페이징을 적용했다고 나와있다. 그리고 항상 isTruncated로 끝을 검사하라고 한다.



line 9 : 특정 버킷 이름을 설정한다.


line 10 : prefix 를 설정한다. 즉 내가 찾을 폴더이름을 설정한다.


line 12 : ObjectListing을 설정한다. 


com.amazonaws.services.s3.model

Class ObjectListing

  • All Implemented Interfaces:
    Serializable


    public class ObjectListing
    extends Object
    implements Serializable
    Contains the results of listing the objects in an Amazon S3 bucket. This includes a list of S3ObjectSummary objects describing the objects stored in the bucket, a list of common prefixes if a delimiter was specified in the request, information describing if this is a complete or partial listing, and the original request parameters.

버킷안에 객체의 리스트를 가져온다. 오브젝Summary객체를 내장하고 있다.


그리고 AmazonS3 객체로 리스트를 가져온것을 담는다.


line 17 : 각 객체의 이름을 가져오기 위해 summary에서 정보를 가져온다. 여러개 이므로 반복문을 돌린다.

Class S3ObjectSummary

  • All Implemented Interfaces:
    Serializable


    public class S3ObjectSummary
    extends Object
    implements Serializable
    Contains the summary of an object stored in an Amazon S3 bucket. This object doesn't contain contain the object's full metadata or any of its contents.

line 19 : 아까 만들었던 리스트에 이 이름들을 담는다.


line 25 : 리스트를 반환한다.





---- 뷰단 -----


s3 객체 이름들의 배열을 가지고 있으므로 뷰단에서 jstl로 뜯는다.


1
2
3
4
5
<div class="personal">
        <c:forEach items="${employeeImageList}" var="employeeImageList" varStatus="status">
            <img src="xxx/${employeeImageList}" alt="employeeimages" class="img-responsive"/>               
        </c:forEach>
       </div>
cs


img source url은 보안상 생략했다. 내가 employeeImageList로 뷰에 내렸으므로 forEach구문에서도 $와 함께 저 이름을 써주는 것이다.

그러면 페이지에서 동적으로 내가 업로드한 이미지들이 붙게된다.

Ajax를 이용했으므로 사진들을 눌렀을 때는 텍스트만 바뀐다. 그리고 새로고침을 누르거나 다른곳을 갔다왔을 때 저 페이지에 사진들이 뜨게 된다.


이상 이미지 처리 개발 기록이었다.


궁금한 점이나 개선 부분이 보이면 댓글 남겨주시면 감사하겠습니다.


반응형