spring(gradle) + vue3 + mysql + docker-compose + AWS 배포 + Travis CI/CD
by AMAD
해당 프로젝트는 거창한 백엔드 로직과 예쁘고 멋스러운 UI를 담은 프론트 로직이 포함 되어있지 않음을 서두에서 밝힙니다.
하지만, spring + vue를 이용하여 백엔드와 프론트를 연동 하는 방법,
gradle을 이용하여 jar파일로 빌드하는 방법,
dockerfile 작성하여 mysql(database) 컨테이너를 생성하는 방법,
build 된 jar파일을 실행할 수 있는 docker 컨테이너를 생성하는 방법,
docker-compose를 이용하여 각각의 컨테이너들을 연동하는 방법,
도커로 배포한 컨테이너를 AWS에 배포하는 방법,
심화 과정으로 Travis를 이용하여 CI/CD 하는 방법
그리고 해당 과정을 수행하면서 겪었던 에러를 해결한 과정을 정리한 글 입니다.
개발자라면 반드시 알아야 할(소스코드 빌드, 백엔드-프론트엔드 통신, 도커를 이용한 APP 실행 및 배포, 컨테이너간의 연동을 위한 docker-compose, AWS EC2를 이용한 프로젝트 배포, Travis를 이용한 CI/CD 등)흐름을 담았다고 보시면 될 것 같습니다.
아키텍쳐

UI

구성
- 백엔드: spring (java 11)
- 프론트: vue
- UI framework: vuetify
- 빌드도구: gradle
- 데이터베이스: mysql
- 컨테이너: docker
목차
- Spring Initializr
- Vue3 + vuetify3
- spring + vue 연동 (proxy)
- dockerfile 작성 (mysql)
- gradle로 jar 빌드 (gradlew)
- vue 간단한 컴포넌트 구성하기
- dockerfile 작성 (jar)
- docker depends_on 옵션의 한계
- docker 배포 최종 결과물
- AWS 배포
1. Spring Initializr
스프링 부트 설정 및 디펜던시를 편리하게 선택, 추가 해줄수 있는 spring initializr 웹사이트(https://start.spring.io/)를 이용하여 스프링 부트 소스코드를 다운받는다.

빌드 툴은 gradle선택
java 버전은 11 스프링 부트 2.7 선택
간단한 프로젝트이기에 디펜던시에는 정말 필요한 것들만 넣었다.
(바로 db에 연결하실 분들은 H2 Database 추가할 필요 없음 - 연결 하지 않고 바로 스프링 실행부터 하실분들은 H2 Database 필요!)
다운 받은 zip파일을 압축 해제 후 vscode를 이용하여 소스코드를 오픈한다.

application파일의 확장자를 변경한다.

요즘은 properties보다는 yml을 많이 사용하는 추세이다.
(yml파일은 들여쓰기를 통하여 어플리케이션의 설정을 해줄 수 있어 가독성이 좋다. - 하지만 자신이 속한 팀의 규칙을 따를 것!)

확장자를 변경하였다면 yml파일에 설정을 하나 추가해준다.

기본 포트는 이미 다른 프로젝트에서 사용하고 있어 포트 넘버를 변경해주었다.
다음으로 원하는 yml파일을 선택하여 스프링을 실행시킬 수 있도록 launch.json파일을 생성한다.

json파일 작성

create a launch.json file만 클릭하여도 자동으로 메인 메소드를 name으로 한 설정 파일이 만들어 진다.
자신이 원하는 옵션 및 args도 추가,수정 가능하다.
나는 보통 console은 internalConsole을 사용하여 추가해주었다.
(에러 발생시 원하는 단어를 찾아서 나열 해주는 기능 등 편리한 기능이 많아 인터널콘솔로 선택하는 편이 좋다.)
args는 스프링 실행시 어떤 yml파일을 읽어들여 실행할 것인지에 대한 옵션이다. 아무런 값도 넣지 않고 ""로 해주면 디폴트 yml(application.yml)을 실행한다.
실무에서는 application-dev.yml, application-prod.yml 등
테스트를 위하여 여러개의 yml파일을 운영하는 경우가 많은데 이때 args의 값으로
yml파일의 name중 하이픈(-) 뒤에 쓰여있는 name을 값으로 넣어주면 해당 yml파일로 스프링부트를 실행시켜준다.
ex) "args":"dev" 이렇게 하면 application-dev.yml 파일을 읽어들여 실행한다.
여기까지 완료했다면 스프링 실행해준다.

변경한 포트넘버(1101)로 스프링 정상 실행 되는 것 확인

localhost:1101로 접속하여 UI까지 확인하면 1단계 성공!

아직 resources 하위 디렉토리에 index.html 파일이 없기때문에 whitelabel error page 나오면 정상!
2. Vue3 + Vuetify3
사전준비 : node 설치
(참고로 해당 프로젝트는 v16.16.0 버전으로 진행되었으며 이 포스팅에서 node 설치는 생략합니다.)
vue3로 버전업이 되면서 vue cli는 현재 유지 관리 모드에 들어갔다.
따라서 vite를 사용하여 vue3 설치를 권장한다. (by 공식문서)
vite를 따로 설치할 필요는 없고 node 설치후 npm init vue 명령어를 사용하면 vite로 설치가 된다.
vue3 설치는 spring 프로젝트와 동일한 경로에 설치한다. (toyproject 경로에서 설치 명령어 실행)

project name은 frontend로 하였고 기타 라이브러리들은 전부 no로 설치

설치 후 디렉토리 구조 (frontend와 spring(src)은 동일한 경로인 toyproject 하위이다.)

vue3 설치 후 아래 명령어 실행
- cd frontend
- npm install
- npm run dev

서버 실행 후 localhost:5173 접속

frontend 서버 실행 까지 완료!
이제 vuetify를 설치해보자.
디렉토리 위치는 계속해서 frontend 에서 아래 명령어 실행
- vue add vuetify

vuetify 3 - vite(preview)를 선택한다.

y선택 하면 인스톨 진행된다.
npm run dev로 서버 실행 후 접속하면

vue2 + vuetify2를 써보신 분들은 화면이 뭔가 이상하다는(?) 느낌을 받을것이다.
정중앙의 문구 역시 Vuetify3 Beta 라고 쓰여있다.
현재(23.10.05) vuetify3는 vue3에 완전 최적화 되어있지 않고 중요한 몇 개의 component들만 사용 가능하다고 공식 문서에도 나와있다.
한참을 구글링해서 vue3에 vuetify3설치했지만 vuetify2에서 사용하던 모든 컴포넌트를 사용 할 수 있는 방법을 찾았다.
vite.config.js파일만 조금 수정해주면 된다.

위 이미지처럼 config파일에서 //추가 라고 주석처리해준 부분들을 자신의 config에 추가 하면 된다.
수정 후 다시 서버 실행 하면

vuetify3 적용 된 것을 볼 수 있다.
3. Spring + Vue 연동 (proxy)
백엔드는 spring으로 개발 하고 프론트는 vue로 개발 후 이 빌드 결과물을 연동을 할 수 있다.
연동 방법은 여러가지 방법이 있다. spring으로 개발한 백엔드의 결과물인 jar파일과 vue로 개발한 프론트의 결과물인 html, css 등이 담겨있는 dist 디렉토리 등이 그것이다.
배포할 서버에 이 결과물을 따로 배포하여 각각 실행 시킨 후,
백엔드와 프론트 중간에 nginx같은 web server를 두어 proxy를 통해 연동 할 수도 있다.
하지만 각각의(프,백) 빌드 결과물을 서버에 올려주고, 실행하는것보다는 스프링부트의 정적 리소스를 관리하는 디렉토리 안에 프론트의 빌드 결과물을 옮겨준 후, 백엔드만 빌드하여 이를 하나의 jar파일 안에 패키징 할 수 있다. 이렇게 하면 프,백 각각의 빌드 결과물을 운영하는 것 보다 백엔드 하나의 결과물(jar파일)만 서버로 옮겨 실행해주면 되기 때문에 이 방법이 훨씬 간편하다.
spring에는 정적 자원을 관리하는 디렉토리가 있다. (main/resources/static)
(디폴트 경로일뿐 다른 경로로 바꿀수 있다. 허나 이 포스팅에서 그것까지 다루지는 않는다.)
해당 디렉토리에 정적 자원이 존재하면 클라이언트의 요청에 따라 그에 맞는 정적 자원(정적 페이지)을 응답한다.
vue로 개발한 코드를 빌드하면 html, css, 이미지 등이 담겨있는 디렉토리가 생긴다. -> 이는 vue의 빌드 결과물이 정적 자원이라는 것을 알 수 있다.
그렇다면 main/resources/static 안에 vue의 빌드 결과물을 이동 시킨다면 node서버를 실행 하지 않아도(vue 서버 실행 하지 않는 다는 것을 말한다. - npm run dev) spring 서버만 실행 한 후 해당 url로 접속 했을때 vue로 개발한 UI가 표출된다.
먼저 경로를 지정하지 않고 빌드를 해보자

dist라는 디렉토리가 생기며 그곳에 html, css, js, 각종 이미지등이 빌드되는 것을 알 수 있다.

dist 하위의 결과물들을 스프링 resources/static으로 수동으로 옮겨줘도 상관은 없다.
하지만, 매번 빌드 할때마다 수동으로 드래그해서 옮겨주는 것은 비효율적이다.
config 파일을 수정하여 build 결과물을 내가 원하는 곳으로 생성 될 수 있게 해준다.

vite.config.js 파일에 // build 경로 설정 이하를 추가해준다. (위 경로 설정 코드는 vue3에서만 유효하다. vue2에서는 약간 달라지므로 구글링 할 것!)
frontend 하위에 생긴 dist 디렉토리는 수동으로 삭제 시켜준 후 다시 npm run build한다.

config에 수정한대로 resources/static/ 하위에 결과물이 생성되는것을 확인 할 수 있다.

이후 스프링 실행

localhost:1101로 접속하면?!

npm run dev를 하여 front server 실행 후 UI 띄운것과 동일한 화면이 스프링 서버 실행 후 스프링 포트넘버로 접속 했을때 나온다. (일부러 url까지 보이게 캡쳐하였다 - 당연히 node 서버는 실행 시키지 않고 스프링만 실행시킨 상태이다)
자 이제 그럼 연동 되었으니 vue를 이용하여 front를 개발하고 spring으로는 서비스 로직을 개발하면 되는 것일까??
아니다!
현재는 단순히 정적 리소스 파일들만 스프링내로 옮겨와 UI만 연동 된 것이다.
UI에서 이벤트가 일어났을때(ex: 클릭 이벤트) API 요청이 백엔드 서버로 가야하는데 아직 이 설정등이 남아있다. 해당 설정은 proxy설정을 통하여 이루어 질 것이고, 이 설정을 통해 클라이언트와 서버가 서로 통신 할 수 있게 된다.
vite.config.js 파일에 // proxy 설정 이하의 코드를 추가한다. (아래의 프록시 설정 코드 또한 vue3에서만 작동된다.)

/api 라는 엔드포인트로 요청이 들어오면 target으로 요청을 돌려준다. (백엔드 서버 주소 및 포트넘버를 적용 하면 된다.)
/api는 예시의 엔드포인트 일 뿐 각자 본인 프로젝트에 맞는 백엔드 API의 엔드포인트를 적어주면된다.
그 아래 밑줄에 가려 잘 보이지 않는데 changeOrigin: true 역시 빼먹지 말고 설정해야 한다 (cors 에러 방지용)
프록시 설정이 완료됐으면 axios라이브러리를 설치 한다.

해당 에러 뜬다면 --force 옵션을 주어 설치 시도한다.

axios까지 설치 완료했다면 백엔드와 통신이 잘 되는지 테스트 하기 위해 간단한 test용 컨트롤러를 만들어준다.

@RequestMapping 어노테이션 으로 엔드포인트 /api를 적용

@GetMapping 어노테이션으로 /api/test 엔드포인트로 요청들어왔을때 test() 메소드 작동 하도록 해준다.
(vue 프록시 설정에서 /api/test로 해야하는건가? 하고 착각 할 수도 있지만 그게 아니다! /api 로 설정해주면 그 이하의 엔드 포인트는 어떤 것이 되었든 target으로 요청을 돌려준다. 단! 다른 컨트롤러에서 다른 메소드를 호출할때 RequestMapping 으로 정의한 엔드포인트가 /api2 라면 vue 프록시 설정에서 이부분을 추가 해주면 된다.)
예시)
server: {
proxy: {
"/api": {
target: "http://localhost:1101",
changeOrigin: true
},
"/api2": {
target: "http://localhost:1101",
changeOrigin: true
}
}
}
컨트롤러 작성이 완료됐다면 해당 api로 요청을 보낼 버튼을 하나 만들어 준다.

버튼에 클릭이벤트로 test메소드가 작동하게 한다.
axios.get("/api/test") --> 컨트롤러에서 작성한 엔드포인트로 요청하겠다는 코드를 작성한다.
리턴값을 콘솔에 찍어준다.
여기까지 완료됐다면 스프링 서버를 실행한다. (백엔드로 요청 보내는 것이기 때문에 당연히 백엔드 서버도 실행중인 상태여야 한다.)
그리고 vue 서버 실행 후 버튼을 클릭한다 (포트넘버가 5173인것에 주목! - 5173에서 버튼을 눌렀지만 프록시 설정을 통해 백엔드 서버인 1101의 컨트롤러에 정의해 둔 메소드가 작동한다.)

개발자 도구를 열어 콘솔창을 열어준다.

test Success! 라는 문구 출력 확인!
4.dockerfile 작성 (mysql)
프로젝트 최상위 경로에(frontend, src가 위치한 곳) docker 라는 디렉토리를 생성한 후, 그 하위에 mysql 디렉토리 생성, 다시 그 하위에 init 디렉토리 생성 및 Dockerfile 을 생성한다.

docker 디렉토리는 소문자로 하든 대문자로 하든 심지어 오타가 있더라도 상관없다. (개발자에게 해당 디렉토리에 도커관련된 파일이 모아진 곳이라는 걸 알려주기만 하면 되기 때문) 하지만 Dockerfile 은 오타가 있으면 안되고 무조건 대문자 'D'로 시작해야 도커 시스템 내에서 해당 파일이 도커파일 이라는 것을 인지한다. 따라서 오타 주의 하여 생성해준다.
init 디렉토리에는 해당 컨테이너가 생성 될 때 수행 될 쿼리문을 미리 작성해두는 곳이다. (보통 초기에 생성될 테이블들의 DDL 및 디폴트로 생성되어있어야 할 데이터들을 insert 하는 쿼리등을 작성해놓는다.)

위 이미지와 같이 test_table 을 하나 만들어주고 row 3개를 insert 시켜주는 DDL을 작성하였다.
init.sql 파일의 경로는 아래와 같다.

그리고 다음은 mysql 도커파일을 작성 한다.

포트넘버는 현재 사용 하고 있는 mysql 포트가 있어서 3300으로 설정 하였다.
docker 디렉토리 하위에 컴포즈 파일을 생성한다.

보통 2개이상의 컨테이너를 동시에 생성하고 제어 할 때 컴포즈 파일을 작성한다고 생각 할 수 있지만,
docker run 명령어로 많은 옵션을 주어야 할 때 제어를 편리하게 하기 위하여 1개의 컨테이너만 생성할 때에도 도커 컴포즈 파일을 작성하여 컨테이너를 생성한다.
도커 컴포즈 파일을 다음과 같이 작성한다.

context: mysql 도커파일이 있는 경로를 적어준다. 해당 경로를 찾아가서 도커파일을 빌드 시킨다.
ports: 포트포워딩 (왼쪽: 외부에서 접속할 포트넘버, 오른쪽: 도커 내부 포트넘버) - 왼쪽 포트넘버로 접속했을때 오른쪽 포트넘버로 포워딩 해준다는 의미이다. 포트포워딩 해주지 않을 경우 외부에서 DB툴 등을 이용하여 mysql컨테이너에 접속이 불가하다.
volumes: mysql 하위의 init 디렉토리에 작성한 init.sql 파일을 실행시켜 docker-entrypoint-initdb.d에 볼륨 마운트 시킨다.
도커 컴포즈 파일까지 작성 완료했으면 이를 기반으로 컨테이너를 생성한다.
도커 컴포즈 파일이 있는 경로에서 docker-compose up -d --build 명령어 실행

명령어 실행 후 도커 데스크탑으로 확인 해준다.

컴포즈 파일에 명시한대로 컨테이너 이름 만들어졌고 port에 3300:3306 으로 컴포즈 파일에서 작성한대로 컨테이너 생성 확인 완료
이제 생성한 컨테이너에 DB툴로 접속을 시도 한다. (디비버를 이용하였다.)

도커 파일에 명시한 포트넘버 및 username database 이름 등 정보를 입력후 테스트 커넥션 하면

위와 같은 오류가 발생 할 것이다.

드라이버 설정에 가서 allowPublicKeyRetrieval 옵션을 TRUE로 주면 해결 된다.

접속 성공 했으면 init.sql 파일이 제대로 동작하였는지 확인!

init.sql 에 작성한대로 3개의 row 생성되어있는것 까지 확인했다면 mysql docker로 띄우기 완료!
5. gradle로 jar 빌드 (gradlew)
3번 (spring + vue 연동)항목 에서 test Success! 를 리턴 하는 테스트 컨트롤러를 만들었었다.
도커를 이용하여 DB를 띄웠으니 하드코딩 말고 실제로 테이블(test_table)에 저장되어있는 데이터를 불러오는 간단한 로직을 하나 만들어보겠다.
먼저 컨트롤러를 만들어준다.

컨트롤러 디렉토리에 컨트롤러로 사용할 java 파일 생성
생성자를 이용한 의존성 주입을 위한 RequiredArgsConstructor 어노테이션
객체 불변 위한 final 키워드
컨트롤러 실행을 알리는 log 남기기
다음으로 서비스를 만든다.

서비스 디렉토리 하위에 서비스 자바 파일 생성
test_table에 저장된 데이터를 select 해오는 getAll() 메소드 정의
다음으로 매퍼를 만든다.

mybatis 디렉토리 생성하고 그 하위에 매퍼 자바 파일 생성
타입은 인터페이스, 서비스에서 호출하는 getAll() 메소드 작성
다음으로 매퍼xml 파일을 만든다.

경로 주의!!
resources 디렉토리 하위에 mapper 디렉토리를 생성 후 그 하위에 xml파일 생성
getAll() 메소드가 호출 되었을 때 적용할 sql문을 정의
다음은 yml파일을 작성한다.

datasource.url 및 username, password는 4단계에서 생성한 도커로 띄운 mysql의 접속 정보 입력
카멜케이스 옵션 true
매퍼 xml 파일의 경로 지정 (mapper 디렉토리 하위의 .xml로 끝나는 파일을 해당 매퍼의 xml파일로 인식한다는 뜻)
모든 파일 작성 완료했다면 스프링 실행!
그리고 포스트맨으로 API 요청 보내기

API 엔드포인트 입력 후 send 버튼 클릭하면 db에 저장되어있는 id:111, 222, 333 3개의 데이터 get
log도 잘 찍혔나 확인!

log 까지 잘 찍혔다면 내가 작성한 소스코드가 정상적으로 동작한다는 것을 의미
이제 이 소스코드를 gradle을 이용하여 jar파일로 빌드 해보자.
스프링 빌드 도구에는 maven과 gradle이 있다.
이 포스팅에서는 서두에서 밝힌대로 gradle로 빌드를 진행 할 것이다.
기존에는 빌드 도구를 직접 설치 후에 환경변수를 설정 하는 등의 과정을 거쳐야 빌드를 할 수 있다. 하지만 이런 방식으로 빌드를 하게 되면 버전이 다르면 사용할 수 없다는 치명적인 문제가 있다. 만약 gradle ver 6.x.x 가 설치된 시스템에서 프로젝트는 ver 7점대를 채택한다면 빌드시 에러가 발생한다.
이는 매번 프로젝트를 수행할 때마다 시스템에 해당 프로젝트가 채택하는 버전을 다시 설치하는 등의 불필요한 리소스 낭비가 들어갔다.
이런 문제를 해결하기 위해 wrapper 가 등장했다. 각 프로젝트마다 시스템에 설치된 빌드 도구를 사용하는 것이 아닌, 프로젝트 내에 내장된 gradle wrapper 또는 maven wrapper를 사용하여 빌드를 할 수 있게 됐다.
spring initializr로 프로젝트 생성 시

gradle을 선택하면 gradle wrapper가 maven을 선택하면 maven wrapper가 자동으로 프로젝트에 내장되어 생성 된다.
프로젝트 경로 하위의 gradlew 와 gradle.bat이 그것이다.

gradlew: 유닉스용 wrapper 실행 스크립트이다.
gradlew.bat: 윈도우용 wrpper 실행 스크립트이다.
터미널을 열고 gradlew가 있는 경로로 이동한다.

해당 경로에서 ./gradlew clean build 하면 소스코드가 빌드 된다. (clean명령어를 쓰면 과거 빌드 경로에 존재하던 jar파일을 삭제 처리 후 새롭게 빌드된 jar파일이 빌드 경로에 생선된다.)
빌드 결과물인 jar파일은 아래 경로에 생성된다.

빌드 전에는 프로젝트에 build 디렉토리는 존재하지 않는다. 해당 명령어로 빌드시 자동으로 생성된다. 빌드 하위의 libs 디렉토리에 jar파일이 2개 생성 된 것을 볼 수 있다. 여기서 plain이 붙은 jar파일 말고 toyproject-0.0.1-SNAPSHOT.jar 파일이 executable jar로 모든 의존성을 포함하여 빌드 된 것이기 때문에 java만 설치 되어있는 pc 라면 java -jar 명령어로 실행 가능하다.

해당 jar파일을 터미널을 열고 실행 시켜 보자.
jar 파일이 존재하는 경로로 이동 후 java -jar "jar파일이름.jar" (실제 입력시 "" 입력 하지 말고) 명령어 실행시 vscode에서 실행 시켰던 것처럼 스프링이 실행되는 것을 볼 수 있다.
실행까지 정상적으로 된다면 다시 포스트맨으로 API 요청 해보기!

엔드포인트 작성 후 send 클릭하면 데이터 select 해오고

로그도 실행 되는 것을 볼 수 있다.
gradle로 jar 빌드 완료!
6. vue 간단한 컴포넌트 구성하기
백엔드는 모두 완성했다. vue를 이용하여 백엔드로직을 거쳐 가져올 데이터를 화면에 뿌려보는 간단한 컴포넌트를 구성해보자.
3번 항목에서 작성한 HelloWorld.vue 파일을 수정한다.

간단하게 구성하는 것이므로 카드 형태로 화면을 구성한다.
v-btn: 버튼을 하나 만들어서 클릭 이벤트 발생 시 정의한 test() 메소드가 실행 되게 한다.
v-for: for문 이용하여 db에 저장되어있는 3개의 데이터를 가져와서 li 태그에 뿌려준다.
item.mb_id: id는 key값이므로 굳이 화면에 보여 줄 필요 없는 데이터 이므로 select 해온 데이터 중 mb_id만 출력한다.
setup(): vue3에서 지원하는 composition 함수를 사용한다.
컴포넌트 작성 완료했으면 npm run dev 하여 서버 실행

localhost:5173 접속

스타일링한 카드 형태의 UI가 표출 되면 성공! 버튼을 클릭한다!

test() 메소드 작동하여 백엔드 AmadController의 getAll메소드 실행 하여 db에 저장되어있는 값을 select 해온 후 화면에 표출한다.
임의로 db의 데이터 변경 후 버튼 클릭했을때, 변경된 데이터를 가지고 오는지 확인 해보자.

3번 로우의 mb_id의 값을 ccc -> test로 변경하였다.
UI 새로 고침 후 다시 버튼 클릭하면?!

세번째 값이 변경한대로 test를 출력한다.
이제 front를 빌드하여 스프링 서버만 실행 해보자.
3번 항목에서 처럼 npm run build 하여 스프링 resources 디렉토리 하위에 front 빌드 결과물이 떨어지게 해준다.
빌드 완료 했다면 스프링 실행!

백엔드 포트넘버인 1101로 접속 했을때 종전의 front(5173) 로 접속 했을 때와 같은 UI 나온다면 제대로 빌드 되어 스프링 resources 디렉토리에 결과물이 생겼다는 뜻이다.
다시 한번 버튼을 클릭하면,

데이터를 잘 가져오는 것을 볼 수 있다.
7. dockerfile 작성 (jar)
jar 파일 빌드 전 application-prod.yml 을 먼저 생성한다.

1번 항목에서 설명했듯이 실무에서는 yml파일을 여러개 나눠서 개발한다.
이는 로컬에서 테스트 할 때 필요한 설정과 실제로 서버에 배포할 때 필요한 설정등이 달라서이다.
db를 예를들어 쉽게 설명 하자면, 개발자가 로컬환경에서 개발할 때에는 본인 pc에 설치한 db를 사용 할 것이다. 따라서 스프링 실행시 로컬에 설치한 db에 접속 하여야 한다.
하지만, 실제 운영되고 있는 서버에 배포 할 때에는 로컬에 설치한 db에 접속하는것이 아닌 운영 중인 db에 접속 해야 한다.
이 프로젝트에서는 어차피 같은 db를 사용 하므로 굳이 yml파일을 나눌 필요는 없다. (default yml 파일을 수정 하면 된다.)하지만 잠시 아키텍쳐로 돌아가보면

jar 파일도 도커 컨테이너 위에서 실행되고 mysql도 도커 컨테이너 위에서 실행되는 형태이다.
각각 서로 다른 컨테이너라는 뜻이다. 이처럼 A컨테이너에서 B컨테이너의 db에 접속 하기 위해서는 로컬에서 실행할때와 달리 접속 정보를 수정해줘야 한다. 접속 정보를 수정한다는 뜻은 로컬 yml 파일과 prod yml파일을 나눌 필요가 있다는 뜻이고 이는 현업에서도 적용하는 부분이므로 이 프로젝트에서도 적용해 보고자 application-prod.yml 파일을 생성 하였다.
prod.yml 파일의 url 부분을 살펴보자.

local db에 접속 할때에는 localhost:3300 이었다.
하지만 여기서는 locahost가 아닌, mysql-byAMAD이고, 포트넘버는:3300이 아닌 3306이다.
도커 컨테이너 끼리 통신을 할 때에는 호스트 주소로 컨테이너 네임(mysql-byAMAD)을 호출 하면 된다.
포트 넘버가 3306인 이유는 외부에서3300으로 접속해서 3306으로 포트포워딩을 하는 형태가 아닌, 도커 컴포즈 내에서 서로 통신이므로 실제 mysql컨테이너의 포트넘버인 3306을 설정해야 에러 없이 작동한다.
그리고 그 뒤에 옵션들은 접속 불가 이슈를 해결하기 위한 옵션이다.
prod.yml 파일을 작성 완료했다면, 도커 파일을 작성하기 전에 소스코드를 빌드 하여 jar 파일을 생성한다. (jar 빌드는 5번 항목에서 다루었으므로 과정은 생략한다.) - jar빌드 전 prod.yml 파일을 반드시 먼저 생성할 것!(그렇지 않으면 빌드된 jar 파일에 prod.yml파일이 누락된다.)
빌드를 완료 하면 아래 경로에 jar파일이 생성 될 것이다.

빌드 완료 후 도커 디렉토리 하위에 project(이름은 아무거나해도 상관없음)라고 디렉토리를 생성해주고 하위에 Dockerfile을 생성한다.

생성한 project 하위에 빌드결과물인 toyproject-0.0.1-SNAPSHOT.jar 파일을 복붙한다.

여기 까지 완료 했으면 Dockerfile을 작성해보자

FROM: 베이스 이미지는 openjdk:11 (빌드한 jar파일이 java11이 설치되어있어야 실행가능하므로)
ARG: JAR_FILE이라는 변수 생성 후 해당 도커파일 경로와 같은 경로에있는 *.jar 파일을 담는다. (해당 도커파일과 같은 경로에 jar파일을 복붙한 이유)
COPY: 컨테이너 생성시 JAR_FILE을 컨테이너 내부로 복사한다.
ENV: 스프링 실행 시 읽어올 profile을 환경변수로 저장.
ENRTYPOINT: 컨테이너 생성시 시작 할 명령어이다. java -Dspring.profiles.active=prod -jar *.jar 이것이 해당 명령어로 active=prod 이 부분이 읽어올 yml파일을 지정 하는 부분이다. 이 부분은 위에서 생성한 환경변수 PROFILE로 치환하여 prod가 들어갈 수 있게 했다.
도커 파일 작성을 완료했으면 도커 컴포즈 파일을 수정한다.

빌드 시킬 도커파일의 경로 및 포트포워딩등 의 옵션을 준다.
depends_on: mysql 은 mysql이 먼저 실행 되어야 스프링 실행시 에러가 없으므로 depends_on 옵션을 주어 mysql이 먼저 실행 되게 한다.
restart: always 옵션을 주어 컨테이너가 다운되면 다시 재실행 되게 한다.
컴포즈 파일 작성 완료했다면 docker-compose up -d --build 하여 컨테이너를 생성한다. (도커 컴포즈로 mysql만 생성했던 컨테이너는 삭제 후에 진행 하도록 한다.)

컨테이너가 started 되었다고 나오고 데스크탑으로 확인해보면

컨테이너가 잘 생성되어 Running 중인 것을 볼 수 있다.
그리고 localhost:1101로 접속하면?!

빌드 된 jar파일을 실행시킨것도 아니고, vscode로 스프링서버를 실행시킨것도 아니다. 도커로만 컨테이너 생성후 띄운 상태로 종전의 jar파일을 실행시킨 후 보았던 UI가 그대로 나온다!
버튼을 클릭해보자!

데이터 또한 잘 가져오는것을 볼 수 있다.
서버 실행 없이 컨테이너만 띄워놓고 이런 결과를 가져왔다는 것은, 아키텍쳐대로 스프링 컨테이너에서 mysql 컨테이너에 통신을 하였고(select 쿼리문) 값을 받아와서 UI로 표출 시켰다는 것을 의미한다.
8. docker depends_on 옵션의 한계
해당 프로젝트를 진행하며 컴포즈 파일까지 작성 완료후 컴포즈 업 하였지만 스프링 컨테이너가 다운되는 에러가 있었다.
에러 로그를 보니 mysql에 연결 하지 못하였다는 로그가 남겨있었다. 하지만 도커 데스크탑으로 확인 결과 mysql 컨테이너는 실행 중이었다. 이를 해결 하기 위해 애꿎은 docker-compose파일과 yml파일에서 내가 오타를 낸것이 있는지, 설정을 잘못잡은게 있는지 한참을 확인하였지만 해당파일들은 정상적으로 작성되었다.
계속해서 왜 안되지 하면서 docker-compose down <-> docker-compose up -d --build 명령어를 왔다갔다 입력하며 컨테이너만 생성했다가, 삭제했다가를 반복하였다.
온갖 구글링, chatGPT 동원하여도 해결이 안될 쯔음 docker-compose 다운 하지 말고 그냥 죽어있는 스프링 컨테이너만 재 실행 해보았다. 결과는 정상 작동!......??
왜 되지..? 를 연신 외쳐 대다가 머릿속을 지나가는 하나의 생각이 떠올랐다.
혹시 mysql컨테이너가 완전히 실행되기도 전에 spring 컨테이너가 실행되서 jdbc 연결이 불가해서 서비스가 다운되었나?? 하는 생각이었다.
바로 구글에 depends_on 옵션을 검색해봤고 결국 원인은 여기에 있었다는 걸 알게되었다.
공식문서에서도 명시 되어있는데,
depends_on 옵션은 단지 컨테이너의 실행 순서만 제어 할 뿐, 먼저 실행 된 컨테이너가 완전히 서비스가 올라 갈 때까지 기다렸다가 후순위 컨테이너를 실행하는것이 아니었다.
따라서 내 도커 컴포즈 파일이 실행되는 순서는 이랬다.
1. depends_on 옵션에 의해 mysql 컨테이너가 실행된다. (완전히 서비스가 올라가기 전에)
2. spring 컨테이너가 실행된다.
3. mysql 컨테이너 서비스가 완전히 작동한다.
이 순서대로 올라갔을 것이다.
그래서 jdbc에서 db 접속을 위해 mysql에 연결 시도 하였지만 아직 mysql서비스가 완전 작동 하는 상태가 아니었기에 에러를 뱉으며 스프링서버가 죽었던 것이다.
하여 나중에 그냥 한번 스프링 컨테이너만 실행해볼까? 하고 다시 실행했을때는 순서상 3번 이후에 스프링 컨테이너를 재실행 하였기에 에러 없이 서비스가 올라갔던 것이다.
(이 포스팅을 보며 차근차근 따라해 보실 분들은 마지막에 스프링 컨테이너가 다운된다고 좌절하지 말고 조금 기다렸다가 다시 스프링 컨테이너를 재실행 해볼것을 권장합니다.)
하지만 도커 컴포즈 빌드시 무조건 스프링 컨테이너가 다운되는 것은 아니었다. (몇차례 더 테스트를 진행했었고, 그때 그때 다른 결과를 도출한다는 것을 발견했다.) 당시 리소스 상황에 따라서 mysql 컨테이너가 엄청 빠르게 서비스 실행이 완료가 될때에는 재실행 없이 docker-compose up -d --build로 스프링 컨테이너도 정상 작동한다.
9. docker 배포 최종 결과물
이제 프로젝트를 도커로 배포해보았으니 해당 도커파일을 AWS 인스턴스에 배포 하는 것까지 들어가보겠다.
10. AWS 배포
AWS 가입 및 인스턴스 생성 과정은 해당 포스팅에서 다루지 않습니다. 구글링으로 아주 쉽게, 그리고 많은 자료가 있으므로 인스턴스 생성 및 탄력적 IP 연동 까지는 다 되었다는 가정하에 진행합니다.
인스턴스 생성 및 해당 인스턴스에 SSH 접속을 완료했다면 가장 먼저 타임 설정을 해준다. (인스턴스 생성시 디폴트로 UTC로 타임존 설정되어있다. 이럴 경우 프로젝트에서 현재시간을 부여하는 로직이 있을시에 미국 시간 기준으로 작성되기 때문에 한국으로 바꿔준다.)

아래의 명령어 입력
sudo rm /etc/localtime
sudo ln -s /usr/share/zoneinfo/Asia/Seoul /etc/lacaltime
date

date 입력했을때 KST로 나온다면 성공!
다음으로 호스트네임을 변경한다. (인스턴스가 여러개일시에는 ip주소만으로는 어떤 서버에 접속했는지 알기 어렵기 때문)
sudo vim /etc/hosts

vim 열리면 가장 아래에 127.0.0.1 호스트 네임 추가 해준다.

서버를 재부팅 시킨다.
sudo reboot

재접속 하여 호스트 네임 변경을 확인한다.

변경 완료했으면 curl test 진행한다.

위와 같이 80포트로는 커넥트 페일이 뜰것인데 이는 정상 작동! (아직 80포트가 열려있지 않아서 그렇다.)
여기 까지 설정 완료했다면 도커를 설치해준다. (jar파일 자체를 깃클론, SCP로 파일을 옮겨와서 배포 할 수도 있지만 해당 프로젝트는 도커로 배포한 것을 AWS서버에 배포하는 것이 목표이다.)
sudo yum install docker -y

도커 서비스를 실행 시킨다.
sudo service docker start

인스턴스 접속 유저는 따로 생성 하지 않을 것이므로 ec2-user에게 도커 실행 권한을 부여한다.
sudo usermod -aG docker ec2-user

인스턴스 재실행 후에도 자동으로 도커 서비스가 실행 될 수 있게 해준다.
sudo chkconfig docker on

제대로 적용 되었는지 서버를 재실행 한 후 도커 명령어 사용해본다.

다음으로 도커 컴포즈를 설치한다.
sudo curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose

다운 받은 도커 컴포즈에 권한을 부여한다.
sudo chmod +x /usr/local/bin/docker-compose

설치 완료 후 도커 컴포즈의 버전을 확인한다.
docker-compose version

도커 컴포즈 까지 설치 완료했다면 로컬 소스코드중 docker 관련 파일을 모아 놓았던 docker 디렉토리를 zip으로 압축 후 scp를 이용하여 ec2 인스턴스 내부로 파일을 이동해준다.

위의 디렉토리를 말한 것이다.
다음과 같이 명령어 이용하여 파일을 옮겨준다. 편의상 중괄호를 사용하였지만 실제 명령어 사용시에는 중괄호 제거 후 입력!

인스턴스에 접속하여 파일이 제대로 옮겨졌는지 확인한다.

압축을 해제해준다.
unzip docker.zip

docker-compose.yml 파일이 있는 경로로 이동하여 도커 컴포즈 업 빌드 해준다.
docker-compose up -d --build

정상 실행 되었는지 확인해준다.

정상 실행까지 확인 했다면 AWS홈페이지에서 보안그룹을 설정해준다.

네트워크 및 보안 탭에서 보안 그룹 클릭

보안 그룹 생성 클릭

위 이미지와 같이 보안 그룹의 이름을 입력해주고 인바운드 규칙에 SSH, HTTP, HTTPS, 사용자 지정 TCP(스프링부트 포트넘버) 를 추가 해준다.
다시 인스턴스로 이동한다.

보안그룹을 변경한다.

생성한 보안 그룹을 선택한다.

보안 그룹 선택 후 보안 그룹 추가를 눌러준다.
기존에 생성되어있던 디폴트 보안그룹은 제거 해준 후에 저장 버튼 클릭

이렇게 보안 그룹에서 스프링부트 포트넘버까지 추가 해주면 배포 완료이다!
탄력적IP:{포트넘버} 또는 퍼플릭DNS:{포트넘버} 로 접속하면 로컬 도커 배포와 같은 화면이 나온다.

완료!.... 인 줄 알았지만 버튼을 눌렀을때 db에서 가져온 값을 뿌려주지 못했다... 잉..??
docker ps 하여 서비스가 죽었나 확인해보았다.

지금 보니 status가 up 이 아니고 up less than a second 였다.
이상해서 docker log를 찍어보았고
cannot open directory "/docker-entrypoint-initdb.d/": permission denied 에러를 뱉으며 mysql 서비스가 실행되지 않고 있음을 확인 하였다.
이는 다른 포스팅에서 해결법을 작성하겠다 --> (cannot open directory '/docker-entrypoint-initdb.d/': permission denied)
에러 해결 후 다시 탄력적IP:포트넘버 로 접속

버튼 클릭 하면?!

db에 저장된 값을 잘 가져오는 것을 확인 할 수 있다!!
이제 로컬호스트에서만 내 프로젝트에 접속 할 수 있는 것이 아닌 인터넷이 되는 환경이라면 어디에서든 내 프로젝트에 접속 할 수 있다.
AWS 배포 성공!!
다음의 과정은 심화 과정으로 옮겨서 포스팅 하겠습니다.
(Travis를 이용한 CI/CD -> 심화과정 보러가기!)
'Toy project' 카테고리의 다른 글
| Travis를 이용한 CI/CD (0) | 2023.12.20 |
|---|---|
| 가위, 바위, 보 게임 만들기 (2) | 2022.11.27 |
| 로또 번호 만들기 (0) | 2022.11.27 |
블로그의 정보
성장 하고 싶은 개발자
AMAD