본문 바로가기
SpringBoot

aws 인스턴스 서버에 [처음]Spring Boot jar를 올린다면 알아야할 주의 사항

by devebucks 2020. 10. 16.
728x90

 스프링부트 애플리케이션을 prod.properties로 docker 이미지로 빌드하고, aws-lightsail에 docker 이미지를 올려서 컨테이너를 실행해서 앱을 서비스하는 작업을 끝냈습니다.

 

실제로 제가 서버에 올리는 작업을 하면서 만든 구성도입니다. 이대로 작업했습니다.

 

IDE에서는 잘 동작했던 앱이 패키징한 jar로 실행하니 많은 문제가 발생했습니다. 실무에서 이런 일이 터졌다면, 정말.. 상상도 하기 싫습니다.

 

이번 포스팅에서는 이번 작업을 수행하면서 발생한 문제와 해결방법들을 얘기해보려고 합니다.

 

제가 마주한 문제는 다섯 가지였습니다.

1. spring boot application-prod.properties로 설정해서 어떻게 앱을 jar패키징하고 도커에 빌드해서 서버에 올릴 것인지 [해결]

2. emailService를 못 찾는 문제[해결]

3. jar로 앱을 실행할 경우, static resources인 csv파일을 못 찾아서 java.io.FileNotFoundException 에러 발생[해결]

4. template exception 문제 [해결]

5. 로컬 환경에서 jar로 실행한 앱에서 회원가입이 정상적으로 되는데, aws서버에 올린 앱에서 회원가입을 하면, 디비에 저장을 못함. [해결]

 

 

첫 번째 문제

 Spring Boot를 처음 운영 서버에 올리다 보니 properties를 운영에서 사용할 프로퍼티를 어떻게 지정해서 빌드할 지를 모르는 상황이었습니다. (application-prod.properties)

현재 제가 서버에 올리려는 프로젝트는 3개의 properties가 존재합니다.

해결방법

-  jar로 application-prod.properties를 지정해서 실행하는 방법

java -jar 자르파일이름.jar -Dspring.profiles.active=prod

1. Dockerfile을 다음 코드와 같이 작성해서 빌드 옵션이 적용될 수 있도록 합니다.

ARG ENVIRONMENT
ENV SPRING_PROFILES_ACTIVE=${ENVIRONMENT}

2. prod 옵션으로 도커 이미지를 빌드합니다.

docker build --build-arg ENVIRONMENT=prod -t 도커허브계정_아이디/도커허브레포지토리_이름 . 
//ex) docker build --build-arg ENVIRONMENT=prod -t devebucks/app . 

3. 로컬 도커 이미지를 도커 허브로 push 합니다.

docker push 도커허브계정_아이디/도커허브레포지토리_이름
//ex docker push devebucks/app

 

두 번째 문제

2번 문제는 jar로 패키징 된 애플리케이션을 실행했을 때, 개발자가 작성한 소스에서 Resources를 로딩할 때, java.io.FileNotFoundException이 발생하는 것이었습니다.

 

제가 기존에 사용했던 코드는 다음과 같습니다.

csv파일을 ClassPathResource()라는 메서드로 객체를 가져와서, getFile()로 파일 객체를 만들었습니다. 그리고 그 파일객체를 다시 Path객체로 파싱 해서 한 줄씩 읽으면서 데이터베이스에 저장하는 작업을 반복시켰습니다.

@PostConstruct
    public List<Zone> initZoneData() throws IOException {
        if(zoneRepository.count() == 0 ) {
            Resource resource = new ClassPathResource("zones_kr.csv");
            List<Zone> zoneList = Files.readAllLines(resource.getFile().toPath(), StandardCharsets.UTF_8).stream()
                    .map(line -> {
                        String[] split = line.split(",");
                        return Zone.builder().city(split[0]).localNameOfCity(split[1]).province(split[2]).build();
                    }).collect(Collectors.toList());
                zoneRepository.saveAll(zoneList);
        }
        return zoneRepository.findAll();
    }

문제 원인

여기서 jar가 문제를 일으킨 부분은 classpath가 아닌 resources의 경로를 그대로 사용하고 있다는 점이었습니다.

compiled code의 경로와 compile전의 Source Code의 resources의 경로가 다르고, jar는 compiled Code로 실행되므로 fileNotFindException이 발생하는 건 당연한 것이었습니다.

해결방법

해법은 ClassLoader.getResourceAsStream()를 사용하는 것이었습니다. 해당 메서드는 찾고자 하는 resource를 classpath에서 찾아줍니다.

 

이를 회피하는 방법을 이 링크가 제시해 주었습니다.

여러 블로그의 해법을 보고 적용도 해봤지만, 뭔가 다 안 맞는 것 같아서 어물쩡거리고 있었는데, 우연히 baeldung이라는 스프링 사이트에 해당 케이스와 딱 들어맞는 경우의 해법을 제시하고 있었습니다. 해당 문서를 읽고, 적용해서 문제를 해결할 수 있었습니다.

 

 

세 번째 문제

 prod.properties로 실행하면 EmailService 클래스를 못 찾습니다.

문제 원인

jar파일을  spring.profiles.active=prod 로 실행했는데,  @profile("dev") 로 설정되어 있는 해당 메서드를 찾지 못해서 발생한 오류였습니다.

해결방법

EmailService를 상속받는 HtmlEmailService클래스에서  @profile("dev") 에 "prod"를 추가했습니다.

 

네 번째 문제

 회원의 정보를 수정하는 페이지로 넘어가는 요청을 보냈는데 다음과 같은 에러가 발생했다.

역시나 로컬에서 정상적으로 동작하던 기능이 jar로 패키징을 해서 jar로 실행하니 동작하지 않았다..

2020-10-16 23:30:57.368 ERROR 27392 --- [nio-8085-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.thymeleaf.exceptions.TemplateInputException: Error resolving template [/settings/profile], template might not exist or might not be accessible by any of the configured Template Resolvers] with root cause

문제의 원인

내가 Controller로 작성했던 요청을 처리하는 핸들러들의 리턴 경로로 "/"로 시작하는 경로도 있고, 없이 시작하는 경로도 있었다. "/"로 시작하는 경로를 반환하는 핸들러에서 에러가 발생하였다.

다음처럼 Controller의 메서드 반환 경로가 "/"로 시작되면 에러가 발생하였다.

"/"를 제거하고 jar로 패키징 해서 jar를 실행시키면 오류 없이 정상적으로 작동하였다.

myserena.tistory.com/155

주의하실 점은 테스트 코드도 "/"에 관련된 수정을 해줘야 한다는 겁니다. 제가 작성한 테스트 코드를 아래 그림으로 보시면, 기존에 하드코딩 되어 있는 경로들도 역시 "/"을 없애야 하고, MockMvcRequestBuilders.get() 이나  MockMvcRequestBuilders.post() 에 입력할 때, static final로 입력된 경로를 호출한다면, 호출 경로 앞에 "/"를 붙여줘야 합니다.

테스트 코드 Junit5

 

 이번 문제를 고민하면서, 이러다가 IDE에서 앱이 정상적으로 실행되지 않는거 아닌가 생각도 했지만, 문제 없이 정상적으로 테스트들이 동작하는 것을 확인해서 안심하고 개발했습니다.

 

다섯 번째 문제

상황은 이랬습니다.

- 회원가입 요청은 들어왔지만, bad request가 있었고, 메일이 보내지지 않아서 저장되려던 회원가입 정보까지 롤백되고 결국 Connection Timed Out으로 프로세스가 종료되었습니다.

- 로컬과 서버 둘 다 동일한 데이터베이스(aws-lightsail-database)를 바라보고 있음.

- 서버 쪽이 데이터베이스와 연결이 안 돼있나 싶었지만, 로컬 앱에서 회원 가입한 정보로 서버 쪽 앱에서 로그인이 됩니다.

- 회원가입 기능만 안될까 싶어서, 데이터베이스에 저장되는 다른 기능을 사용해 보니 정상적으로 동작했습니다.

- 이메일을 보내는 기능이 있는 프로세스에서 timed out이 발생하고 있었습니다.

문제 원인

일단, 로그를 확인했습니다.

에러 내용

nested exception is: java.net.ConnectException: Connection timed out (Connection timed out). Failed messages: com.sun.mail.util.MailConnectException: Couldn't connect to host, port: sm tp.gmail.com, 25; timeout -1;

ssh 터미널 에러 내용

 

 저는 gmail smtp를 사용해서 메일을 보내도록 기능을 구현했습니다. 알아보니, 방화벽 문제였습니다. aws에서 smtp로 요청과 응답을 주고받은 port는 25번 포트입니다. 25번 포트를 사용할 수 없어서 MailConnectionException이 발생하고 있었습니다.

aws에서는 모든 인스턴스를 대상으로 25번 포트를 기본적으로 block 한다고 aws 가이드 페이지에 나와있습니다. 

 

해결방법

블로그 참조

아마존 : EC2 인스턴스에서 포트 25에 대한 제한을 제거하려면 어떻게 해야 합니까?

 

EC2 인스턴스에서 포트 25 제한 제거

Amazon EC2(Amazon Elastic Compute Cloud) 인스턴스의 포트 25를 통해 이메일을 보내는 데 문제가 있거나 시간 초과 오류가 자주 발생합니다. EC2 인스턴스에서 포트 25 제한을 제거하려면 어떻게 해야 합니��

aws.amazon.com

일단, 아마존에 제 앱이 설치된 aws-lightsail 인스턴스 IP와 smtp.gmail.com의 도메인 사이에 25번 포트를 열도록 요청을 보냈습니다.

 

미국 영업일 기준으로 48시간 안에 메일이 온다고 나와있습니다. 실제로 요청에 대한 메일이 왔고, 처음에는 거절당했는데, 서버 사용료 미납금 때문에 거절된 것이었고, 돈을 지불하고 다시 요청한 결과, 다음과 같이 승인이 났고, 정상적으로 메일이 보내지는 것을 확인했습니다.

aws에서 승인 메일

느낀점.

처음으로 springboot 앱을 aws에 배포하면서 정말 많은 오류가 발생했고, 이를 해결했습니다. 

느낀 점은 개발을 하면서, jar로 패키징을 해서 동작 테스트를 해야 한다는 것이었습니다. IDE에서 앱을 실행하는 것과 jar로 패키징 해서 jar를 실행하는 것은 정말 많은 차이(classpath)가 있다는 것을 알게 되었고, jar로 실행한다는 것에 초점을 맞춰서 개발을 진행해야겠다고 생각했습니다.

 

힘든 점이 많았지만, 실제로 스프링 부트 앱을 docker를 통해서 올리는 과정에서 docker를 잘 사용할 수 있게 되었고, 스프링부트 운영 프로퍼티를 적용해서 docker 이미지를 빌드하는 방법도 학습할 수 있었습니다. 

 

점점 프로덕트를 만드는 역량이 강해지는 것을 느낄 수 있었습니다. 이번 경험을 양식 삼아, 다음에 서버에 올릴 때는 더 빠르고 능숙하게 할 수 있을 것 같습니다.

728x90

댓글