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

[인턴 일지] Spring + Amazon S3 이미지 업로드

Jay Tech 2017. 10. 28. 05:27
반응형

처음 프로젝트 요구사항 중 직원들의 특이사항을 그 자리에서 바로 핸드폰으로 찍어서 올리고 싶다는 것이었다.


예를들면 그 직원이 일하는 모습 또는 특정한 문서 사진 또는 기타 필요한 사진들을 말이다. 말처럼 뚝딱 되면 얼마나 좋을까. Project Plan을 줄 때 이 부분을 제일 길게 잡았다.  까다로운 만큼 오래 걸릴것 같았다. 이미지 처리에 있어서 저장소에 실제 파일이 저장되지만 데이터베이스에는 경로와 이름 사이즈 메타데이터 등 정보를 넣어야 하기 때문이다. 사실 조금 꼬인게 처음에 얘기를 나누었을 때 말한 기간안에 이 기능을 구현하지 못할것 같다고 했었다. 그래서 데이터베이스를 설계할 때 이미지 관련해서 테이블을 만들지 않았다. 그런데 할 수 있을 것 같아서 도중에 할 수 있다고하여 기능을 구현 중이다. 데이터베이스에 사진 테이블을 만들지 않아서 지금이라도 만들지 아니면 그냥할지 고민중이다...


그냥하는 경우는 직원들의 ssn넘버를 기준으로 ssn_num + "personal_data" 이런식으로 모든 파일 이름으로 업로드 되게 한 후 조회할 때도 ssn_num으로 조회되게 하는 것이다. 조금 무식한 방법 같지만 좀더 고민해 봐야 겠다. 그렇다면 1명당 업로드할 수 있는 파일 갯수를 limit한다던지의 옵션도 고민해야한다. 무작정 계속 업로드 하게 되면 서버 사이즈를 가늠할 수가 없기 때문이다. 엄청나게 대규모 사진들이 필요한게 아닐 뿐더러 budget 예상이 필요하기 때문에 예측할 수 있는 사이즈가 필요하다고 판단된다. 게다가 내 돈이 아니기 때문에 요금 청구에 있어서 조심스러워 해야한다. 내 돈이었어도 조심해야하는건 마찬가지긴한데 그래도 내가 맡은 일이기 때문에 책임이 있는것은 분명하다.


어찌됬건 Amazon의 S3서비스를 이용하기로 했으므로 java단의 코드를 만들어보았다. Amazon document와 여러 예제들을 일단 다 보았다. 나는 살짝 변형된 framework를 쓰기 때문에 정통 spring boot와는 조금 달라서 딱 맞는 예제를 찾을수가 없었다. 그래서 여러 예제들을 조합해서 내 프로젝트에 어떤 부분에 어떤 코드를 넣어야할 지 흐름을 파악했다. 처음에 어떻게 시작할지부터 애를먹었다. 나는 처음에는 바로 짜는게 아니라 흐름부터 보는 습관이 있다. 난 항상 종이에 그린다. 


이건 처음 프로젝트 하기전에 전체적인 흐름과 어디에서 시간이 많이 소요될지 (issue)로 그려보았다.




근데 개발하면서 물론 내가 체크한 이슈에서 시간이 걸렸지만 다른 부분에서 막히는게 훨씬 많았다..... 그래서 예상치못하게 시간이 더 걸리는 경우도 있었고 아예 하루를 꼬박 날리는 경우도 허다하다. 예를들어 스크립트가 아예 작동이 안되거나 jquery 타겟을 못잡아서 효과가 발동되어야하는 부분이 아예 부동상태거나..ㅋㅋ 프런트엔드에서 시간이 엄청걸렸다... 진짜 1달동안 방에 갇혀서 javascript와 jquery 책을 하루종일 읽고 싶다. 물론 document를 보면서하지만 그래도 어느정도는 안보고 하는게 좀 멋있다.

그리고 환경설정 관련해서도 오래걸렸다. 특히 버젼.... 네임스페이스 xsd를 가져오는데 여러 xsd중 서로 버전이 맞지않으면 아다리가 맞지않아 작동이 되지않았다.  이건 뭐 어떻게 할 수 가 없어서 진짜 노가다로 0.1씩 올리면서 하나씩 다 해보았다. 진짜 웃긴게 3개를 임포트할때 안되는 경우가 있었다. 그럼 경우의 수가 몇가지일까....생각해보다가 앞이 깜깜해진적도 있다.


어쨋든 지금은 s3업로드에 관해서 포스팅을 하는 거니까 과정을 기록해 보겠다.


먼저 해야할게 pom.xml에 등록하여 Maven 우유통을 추가하는 것이다.




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
        <!-- AWS SDK Java -->
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk</artifactId>
            <version>1.11.66</version>
        </dependency>
        
        <!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.2</version>
        </dependency>
 
        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-io -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-io</artifactId>
            <version>1.3.2</version>
        </dependency>
cs




이러면 이렇게 내가 좋아하는 우유통들이 추가가 된다 :)


위에 코드를 보면 첫 번째 dependency는 AWS 아마존 관련 파일들이다. 저거 넣으면 엄청 많은 우유통들을 획득할 수 있다. 그리고 두 번째, 세번 째는 파일 업로드 관련 우유통들이다. 내가 저걸 넣는걸 깜빡해서 다 해놓고 업로드가 아예 안되어서 애를 엄청 먹었다. 분명 컨트롤러 url도 문제가 없었는데 진짜 죽어도 죽어도 안 타는 것이었다. 화면단에는 error occurred만 뜨고 에러 로그도 안찍히는 것이었다. 진짜 몇시간동안 별 삽집을 다하다가 구글에 multipart controller not working 막 생각나는 키워드를 다 입력해도 해결이 되지 않았다. 도저히 안되겠어서 예전 노트북을켜서 예전에 한 다른 프로젝트를 열었다. 설정파일을 하나씩 살펴보던중 갑자기 떠올랐다.... 디펜던시 추가를 안한 것을.... 바로 집어넣고 돌리니까 됬다.... 내 소중한 우유통.....


앞단은 아직 디자인을 못했는데 일단 기능이 되면 시간 문제니까 일단 기본 폼만 넣어놓았다.


1
2
3
4
5
<form name="fileUpload" method="post" action="upload.do?${_csrf.parameterName}=${_csrf.token}" enctype="multipart/form-data">
 <label>Select File</label> <br />
 <input type="file" name="file" />
 <input type="submit" name="submit" value="Upload" />
</form
cs


저거 action안에 csrf토큰은 스프링 시큐리티 때문에 넣어놓은 것이다. 저것에 대해 좀 더 공부하고 포스팅하겠다. multipart로 업로드 하는 폼을 작성하였다. 그냥 인풋에 파일밖에 없다. 저렇게 해놓고 맞는 url을 컨트롤러 단에서 작성하였다.



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
47
48
49
50
51
52
@Controller
public class S3Controller {
    
    @Resource(name="fileProperties")
    private Properties fileProperties;
    
    @Autowired
    private S3Service s3Service;
    
    @RequestMapping(value="upload.do", method=RequestMethod.POST )
    public String initS3(@RequestParam("file") MultipartFile file) throws Exception {
        
        try{
            System.out.println("upload initS3 entered");
            // 저장할 객체 이름
            String downloadKey = fileProperties.getProperty("park.s3.key");
            String uploadFilePath = fileProperties.getProperty("park.s3.uploadfile"); // 전송할 파일 경로
            
            /*if(file.isEmpty()) {
                
            }*/
            
            // custom upload file
            s3Service.imageUpload(downloadKey, file);
            
            System.out.println("aws in");
            System.out.println("---------------- START UPLOAD FILE ----------------");
            //s3Service.uploadFile(downloadKey, uploadFilePath);
            //System.out.println("---------------- START DOWNLOAD FILE ----------------");
            
    
            // 다운로드
            //s3Service.downloadFile(downloadKey);
            
            
            return "";
        } catch(Exception e) {
            e.printStackTrace();
        } 
        
        return "";
    }
 
 
    @RequestMapping(value="uploadPage.do")
    public String uploadFile() throws Exception {
 
        return "upload/upload.tiles";
    }
 
 
}
cs


근데 지금 사실 미완성 상태이다. 기억이 날아갈 까봐 일단 적어본다. 보면 file이 없을 경우 예외처리를 해줘야 하고 (파일 선택안하고 upload눌렀을 시) ... 하여튼 지저분한 상태이다. 근데 사실 매우 궁금한게 일단 Multipart로 file을 받았는데 이걸 가공해서 service로 넘겨야 할지 아니면 그냥 받기만하고 service에서 처리를 시켜야 할지 잘 모르겠다. 어디서 처리를 하든 동작은 되지만 MVC 흐름상 어디에 넣어햐할지 잘 모르겠다. 이럴 때마다 실무에서 경험많으신 개발자 분들의 코드를 보고 싶어진다... 


저기 다운로드는 아직 안한것이다. 물론 저기다가 다운로드 로직을 넣을 것은 아니다.


Service임플을 보자.


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
47
48
 
@Service("s3Service")
public class S3ServiceImpl extends EgovAbstractServiceImpl implements S3Service{
 
    private Logger logger = LoggerFactory.getLogger(S3ServiceImpl.class);
    
    @Resource(name="fileProperties")
    private Properties fileProperties;
    
    @Autowired
    private AmazonS3 s3client;
 
    @Override
    public void imageUpload(String keyName, MultipartFile file) {
        
        String bucketName = fileProperties.getProperty("park.s3.bucket");
        
        ObjectMetadata omd = new ObjectMetadata();
        omd.setContentType(file.getContentType());
        omd.setContentLength(file.getSize());
        omd.setHeader("filename", file.getOriginalFilename());
 
        try {
            //File convFile = new File(file.getOriginalFilename());
            // keyname 안들어갔어!!
            s3client.putObject(new PutObjectRequest(bucketName, "testfile", file.getInputStream(), omd));
            logger.info("============= Upload custom file - Done!! ===============");
        } 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 (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        
    }
 
}
 
cs



저기 맨 꼭대기 Properties아마존 퍼블릭키와 시크릿키 그리고 각종 static한 스트링들을 모아 놓은 것이다.




파일이름은 file.properties로 하였고 저렇게 안에 값을 넣어놓았다. 폴더 위치는 그냥 설정파일있는 곳에 저렇게 안착시켜놓았는데 저기다가 놓는게 맞는지까지는 모르겠다....

그리고 저 프로퍼티를 동작시키기 위해서 스프링 환경 설정 파일에 등록을 시켜줘야 한다.


context-common.xml파일을 열었다.  


1
2
3
4
5
6
7
8
9
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:util="http://www.springframework.org/schema/util" 
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd">
 
<util:properties id="fileProperties" location="classpath:/egovframework/file.properties" />
     
cs


보면 저기 xsd네임스페이스에 util을 쓴다고 명시를 해줘야 한다.

저기밑에 util:properties에 내 설정파일을 등록하는 것이다. id는 fileProperties로 하였다. 다른 태그들은 생략하고 필요하게 넣어야할 것들만 지금 따로 잘라서 기록한다.


그리고 context-multipart를 추가하였다.


1
2
3
4
5
6
7
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="filterMultipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="maxUploadSize" value="100000000" />
    </bean>
</beans>
cs



다시위에 임플파일을 보자.


저렇게 프로퍼티를 등록시켜주면 


String bucketName = fileProperties.getProperty("park.s3.bucket");  이런식으로 이름을 가져올 수 있다.

그리고 ObjectMetadata는 사진의 메타데이터를 설정해주는 것이다.


내가  @Autowired

private AmazonS3 s3client; 로 주입을 시켜놓았기 때문에


s3client.putObject(new PutObjectRequest(bucketName, "testfile", file.getInputStream(), omd)); 로 실제 업로드 기능을 수행할 수 있다. 제일 핵심이 되는 코드이다.


PutObjectRequest는 document를 참고하여 만들었다.

http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/model/PutObjectRequest.html




여러개가 오버라이드 되어있는데 그 중 업로드에 딱 알맞는 친구가 있어서 저것을 사용하였다. 버킷 이름과 키 (키는 파일이름이라고 생각하면 된다) 그리고 inputstream이 필요한데 인자로 넘겨받은 MultipartFile에서 getInputStream으로 스트림을 가져와서 넘겨주면 된다. 마지막으로 오브젝 메타데이타를 넣어준다.

이건 Multipartfile에서 발췌한 내용이다.


그리고 s3client에서 putObject로 밀어 넣어준다. 그러면 업로드가 완료가 된다.





모바일에서도 테스트해보았다. 그런데 한가지 사소한 문제가 생겼다.

nginx서버에서 리퀘스트 용량이 부족하다는 경고와 함께 업로드가 되지 않는 것이다.

413 Request Entity Too Large에러이다. 알고보니 서버 디폴트 용량이 2M이었다. 이것을 수정해야 한다.



엔징스 conf파일을 연다. 




그리고 http안에 client_max_body_size를 늘려주었다. 그리고 꼭 서버를 restart해줘야 한다. 


명령어는 service nginx reload이다. 그러면 무리없이 파일들이 올라가게 된다. 멀티파트로 폼을 만들면 모바일 폰에서 take photo 와 choose in library 둘다 가능하다!


이틀이 걸렸다... 하루는 엄청난 삽질을 했고 둘째 날에 연동을 마쳤다. 이제 시작인 거다. 어떻게 사진 이름 형식을 맞출 것인가, 직원별로 파일을 어떻게 관리할 것인가. 답이 없는 문제로 고민해야한다. 삽질할때는 키보드를 반으로 접고 싶지만 한 순간에 에러로그가 사라지고 정상적으로 돌아간다는 로그가 딱 뜨는 순간 성취감이 느껴진다. 이 맛에 컴퓨터하는것 같다. 



반응형