Super Kawaii Cute Cat Kaoani 스프링 부트와 AWS로 혼자 구현하는 웹 서비스(10)

스프링 부트와 AWS로 혼자 구현하는 웹 서비스(10)

2024. 1. 22. 01:44
728x90
SMALL

📌 24시간 365일 중단없는 서비스를 만들자

  • 새로운 Jar가 실행되기 전까지 기존 Jar를 종료시켜 놓기 때문에 서비스가 중단됨
  • 카톡이나 네이버같이 정지되지 않는 서비스를 만들자

 

⚡ 무중단 배포 소개

과거에는 다 같이 코드를 합치는 날 배포하는 날을 정하고 진행했음.
특히 배포일에는 사용자가 적은 새벽 시간에 개발자들이 모두 남아 배포를 준비하곤 했음
-> 배포를 하고 문제가 생기면 긴급 점검 공지를 하고 수정을 해야 했음
-> 무중단 배포를 통해 이를 해결할 수 있음!

 

 

✔ AWS에서 블루 그린(Blue-Grean) 무중단 배포
✔ 도커를 이용한 웹서비스 무중단 배포

 

엔진엑스(Nginx)

  • 무중단 배포 방법 중 하나(가장 저렴하고 쉬움)
  • 웹 서버, 리버스 프록시, 캐싱, 로드 밸런싱, 미디어 스트리밍 등을 위한 오픈소스 소프트웨어
    • 리버스 프록시 : 엔진엑스가 외부의 요청을 받아 백앤드 서버로 요청을 전달하는 행위
  • 기존에 쓰던 EC2에 그대로 적용하면 되므로 배포를 위해 AWS EC2 인스턴스가 하나 더 필요하지 않음
  • 꼭 AWS와 같은 클라우드 인프라가 구축되어 있지 않아도 사용할 수 있는 범용적인 방법
    • 개인 서버 혹은 사내 서버에서도 동일한 방식으로 구축할 수 있으므로 사용처가 많음

 

✔ 하나의 EC2 혹은 리눅스 서버에 엔진엑스 1대와 스프링 부트 Jar를 2대 사용

  • 엔진엑스는 80(http), 443(https)포트를 할당
  • 스프링 부트 1는 8082포트로 실행
  • 스프링 부트 2는 8083포트로 실행

<엔진엑스 무중단 배포1 구조>(나는 8082, 8083으로 함)

  1. 사용자는 서비스 주소로 접속(80 or 443)
  2. 엔진엑스는 사용자의 요청을 받아 현재 연결된 스프링 부트로 요청을 전달
    • 스프링 부트1 즉 8082 포트로 요청을 전달한다고 가정
  3.  스프링 부트2는 엔진엑스와 연결된 상태가 아니니 요청받지 못함

 

✔ 1.1 버전으로 신규 배포가 필요하면, 엔진엑스와 연결되지 않은 스프링 부트 2(8083)으로 배포함

<엔진엑스 무중단 배포2> (8082, 8083)

  1. 배포하는 동안에도 서비스는 중단되지 않음
    • 엔진엑스는 스프링 부트1을 바라보기 때문
  2. 배포가 끝나고 정상적으로 스프링 부트2가 구동 중인지 확인함
  3. 스프링 부트2가 정상 구동 중이면 nginx reload 명령어를 통해 8082 대신에 8083을 바라보도록 함
  4. nginx reload는 0.1초 이내 완료됨

 

✔ 1.2 버전 배포가 필요하면 이번에는 스프링 부트1로 배포함

<엔진엑스 무중단 배포3> (8082, 8083)

  1. 현재는 엔진엑스와 연결된 것이 스프링 부트2임
  2. 스프링 부트1의 배포가 끝났다면 엔진엑스가 스프링 부트1을 바라보고 변경하고 nginx reload를 실행함
  3. 이후 요청부터는 엔진엑스가 스프링 부트1로 요청 전달

 

 

 

⚡ 엔진엑스 설치와 스프링 부트 연동하기

✔ 엔진엑스 설치

  • EC2에 접속하여 명령어 입력
sudo yum install nginx

 

엔진엑스 실행

sudo service nginx start

 

✔ 잘 실행되었다면 아래 메시지가 뜸

Starting nginx: [ ok ]

 

나는 이 메시지가 뜸,, 다른 사람들 보면 이것도 잘 실행된거라곤 하는데 일단 넘어가본다

Redirecting to /bin/systemctl start nginx

 

 

✔ 보안 그룹 추가

  • 엔진엑스의 포트번호를 보안 그룹에 추가
  • 엔진엑스의 포트번호 : 80
  • EC2 -> 보안그룹 -> EC2 보안그룹 선택 -> 인바운드 편집
    • 80으로 0.0.0.0/0, ::/0 선택

 

✔ 리다이렉션 주소 추가

  • 8081이 아닌 80포트로 주소가 변경되니 구글과 네이버 로그인에도 변경된 주소를 등록해야만 함
  • 기존에 등록된 리다이렉션 주소에서 8080부분을 제거하여 추가 등록
  • EC2의 도메인으로 접근하되, 포트번호 없이 도메인만 입력해서 접속하면 엔진엑스 웹페이지를 볼 수 있음

 

✔ 엔진엑스와 스프링 부트 연동

  • 엔진엑스 설정파일 오픈
sudo vim /etc/nginx/nginx.conf

 

 설정 내용 중 server 아래의 location / 부분을 찾아서 다음과 같이 추가

location / {
    proxy_pass http://localhost:8080; # 1
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 2
    proxy_set_header Host $http_host;
}

 

🔶 proxy_pass

  • 엔진엑스로 요청이 오면 http://localhost:8081로 전달

🔶 proxy_set_header XXX

  • 실제 요청 데이터를 header의 각 항목에 할당
  • ex) proxy_set_header X-Real-IP $remote_addr: Request Header의 X-Real-IP에 요청자 IP를 저장

 엔진엑스 재시작

sudo service nginx restart

브라우저로 접속해 엔진엑스 시작 페이지가 보이면 새로고침

  •  엔진엑스가 스프링 부트 프로젝트를 프록시하는게 보이면 성공!

 

 

⚡ 무중단 배포 스크립트 만들기

✔ 무중단 배포 스크립트 작업 전에 API를 하나 추가

  • 이 API는 이후 배포 시에 8082를 쓸지, 8083을 쓸지 판단하는 기준이 됨!

 

profile API 추가

 

✔ ProfileController 생성

🔶 main/.../web 아래에 생성

package com.yerin.book.springbootwebservice.web;

import lombok.RequiredArgsConstructor;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.List;

@RequiredArgsConstructor
@RestController
public class ProfileController {
    private final Environment env;

    @GetMapping("/profile")
    public String profile(){
        List<String> profiles = Arrays.asList(env.getActiveProfiles());
        List<String> realProfiles = Arrays.asList("real", "real1", "real2");
        String defaultProfile = profiles.isEmpty()? "default" : profiles.get(0);

        return profiles.stream().filter(realProfiles::contains).findAny().orElse(defaultProfile);
    }
}

🔶 env.getActiveProfiles()

  • 현재 실행 중인 ActiveProfile을 모두 가져옴
    • 즉, real, oauth, real-db 등이 활성화되어 있다면(active) 3개가 모두 담겨있음
    • 여기서 real, real1, real2는 모두 배포에 사용될 profile이라 이 중 하나라도 있으면 그 값을 반환하도록 함
  • 실제로 이번 무중단 배포에서는 real1과 real2만 사용되지만, step2를 다시 사용해볼 수 있으니 real도 남겨둠

 

✔ ProfileControllerUnitTest 코드 생성

🔶 test/.../web 아래에 생성

package com.yerin.book.springbootwebservice.web;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.mock.env.MockEnvironment;

import static org.junit.jupiter.api.Assertions.*;

class ProfileControllerUnitTest {

    @Test
    public void real_profile이_조회된다(){
        //given
        String expectedProfile = "real";
        MockEnvironment env = new MockEnvironment();
        env.addActiveProfile(expectedProfile);
        env.addActiveProfile("oauth");
        env.addActiveProfile("real-db");

        ProfileController controller = new ProfileController(env);

        //when
        String profile = controller.profile();

        //then
        Assertions.assertThat(profile).isEqualTo(expectedProfile);
    }

    @Test
    public void real_profile이_없으면_첫_번째가_조회된다(){
        //given
        String expectedProfile = "ouath";
        MockEnvironment env = new MockEnvironment();

        env.addActiveProfile(expectedProfile);
        env.addActiveProfile("real-db");

        ProfileController controller = new ProfileController(env);

        //when
        String profile = controller.profile();

        //then
        Assertions.assertThat(profile).isEqualTo(expectedProfile);
    }

    @Test
    public void active_profile이_없으면_default가_조회된다(){
        //given
        String expectedProfile = "default";
        MockEnvironment env = new MockEnvironment();
        ProfileController controller = new ProfileController(env);

        //when
        String  profile = controller.profile();

        //then
        Assertions.assertThat(profile).isEqualTo(expectedProfile);
    }
}

🔶 ProfileControllerEnvironment 모두 자바 클래스(인터페이스)이기 때문에 쉽게 테스트 가능

  • Environment : 인터페이스라 가짜 구현체인 MockEnvironment(스프링에서 제공)을 사용해서 테스트
  • 생성자 DI가 얼마나 유용한지 알 수 있음
  • 만약 Environment@Autowired로 DI 받았다면 이런 테스트 코드를 작성하지 못했음

 

 

SecurityConfig 코드 수정

AntPathRequestMatcher.antMatcher("/profile")

추가해줌

 

SecurityConfig 관련 Test 코드

🔶 test/.../web 아래에 ProfileControllerTest 클래스 생성

package com.yerin.book.springbootwebservice.web;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit.jupiter.SpringExtension;


@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ProfileControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void profile은_인증없이_호출된다() throws Exception {
        String expected = "default";

        ResponseEntity<String>response = restTemplate.getForEntity("/profile", String.class);
        Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        Assertions.assertThat(response.getBody()).isEqualTo(expected);
    }
}
  • 성공했다면 깃허브로 푸시하여 배포
  • 배포가 끝나면 /profile로 접속해서 잘 나오는지 확인

해당 화면이 뜸

 

 

real1, real2 profile 생성

  • 현재 EC2 환경에서 실행하는 profile은 real밖에 없음
  • 해당 profile은 Travis CI 배포 자동화를 위한 profile이니 무중단 배포를 위한 profile 2개(real1, real2)을 src/main/resources 아래에 추가

 

application-real1.yml

server:
  port: 8082
spring:
  profiles:
    include:
      - oauth
      - real-db
  jpa:
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL57Dialect
        dialect.storage_engine: innodb
  session:
    store-type: jdbc

application-real2.yml

server:
  port: 8083
spring:
  profiles:
    include:
      - oauth
      - real-db
  jpa:
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL57Dialect
        dialect.storage_engine: innodb
  session:
    store-type: jdbc

 

엔진엑스 설정 수정(무중단 배포의 핵심)

  • 배포 때마다 엔진엑스의 프록시설정(스프링부트로 요청을 흘려보내는)이 순식간에 교체됨
    • 엔진엑스 설정이 모여있는 /etc/nginx/conf.d/service-url.inc라는 파일을 하나 생성

 

 

sudo vim /etc/nginx/conf.d/service-url.inc
  • 다음 코드 입력 후 저장 종료

 

set $service_url http://127.0.0.1:8081;
  • 해당 파일은 엔진엑스가 사용할 수 있게 설정
  • nginx.conf파일 오픈
sudo vim /etc/nginx/conf.d/service-url.inc;
  • location / 부분 코드 수정

 

 

 

 

 

✔ 재시작

sudo service nginx restart

 

 

배포 스크립트들 작성

 

✔ step2와 중복되지 않기 위해 EC2에 step3 디렉토리 생성

mkdir ~/app/step3 && mkdir ~/app/step3/zip

 

✔ appspec.yml -> step3으로 배포되도록 수정

version: 0.0
os: linux
files:
  - source: /
    destination: /home/ec2-user/app/step3/zip/
    overwrite: yes

 

무중단 배포를 진행할 스크립트는 총 5개

  • stop.sh : 기존 엔진엑스에 연결되어 있진 않지만, 실행 중이던 스프링 부트 종료
  • start.sh : 배포할 신규 버전 스프링 부트 프로젝트를 stop.sh로 종료한 'profile'로 실행
  • health.sh : 'start.sh'로 실행시킨 프로젝트가 정상적으로 실행됐는지 체크
  • switch.sh : 엔진엑스가 바라보는 스프링 부트를 최신 버전으로 변경
  • profile.sh : 앞선 4개 스크립트 파일에서 공용으로 사용할 'profile'과 포트 체크 로직

 

✔ appespec.yml에 앞선 스크립트 사용

hooks:
  AfterInstall:
    - locaion: stop.sh #엔진엑스와 연결되어 있지 않은 스프링 부트를 종료
      timeout: 60
      runas: ec-user
  ApplicationStart:
    - location: start.sh # 엔진엑스와 연결되어 있지 않은 Port로 새 버전의 스프링 부트를 시작함
      timeout: 60
      runas: ec-user
  ValidateService:
    - location: health.sh # 새 스프링 부트가 정상적으로 실행됐는지 확인
      timeout: 60
      runas: ec-user

✔ Jar파일이 복사된 이후부터 차례로 앞선 스크립트들이 실행된다고 보면 됨

  • scripts 디렉토리 안에 각 스크립트들 추가

 

 

✔ profile.sh

#!/usr/bin/env bash

# 쉬고 있는 profile 찾기: real1이 사용 중이면 real2가 쉬고 있고, 반대면 real1이 쉬고 있음

function find_idle_profile() {
  # 현재 엔진엑스가 바라보고 있는 스프링 부트가 정상적으로 수행 중인지 확인. 정상이면 200, 오류면 400~503
  RESPONSE_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/profile)

  if [ ${RESPONSE_CODE} -ge 400 ] # 400보다 크면 (즉, 40x/50x 에러 포함)
  then
    CURRENT_PROFILE=real2
  else
    CURRENT_PROFILE=$(curl -s http://localhost/profile)
  fi

  if [ ${CURRENT_PROFILE} == real1 ]
  then
    IDLE_PROFILE=real2
  else
    IDLE_PROFILE=real1
  fi

  # 스프링 부트 프로젝트를 이 profile로 연결하기 위해 반환
  echo "${IDLE_PROFILE}"
}



# 쉬고 있는 profile의 port 찾기
function find_idle_port() {
  IDLE_PROFILE=$(find_idle_profile)

  if [ ${IDLE_PROFILE} == real1 ]
      then
        echo "8082"
      else
        echo "8083"
      fi
}

🔶 $(curl -s -o /dev/null -w "%{http_code}" http://localhost/profile)

  • 현재 엔진엑스가 바라보고 있는 스프링 부트가 정상적으로 수행 중인지 확인
  • 응답값을 HttpStatus로 받음
  • 정상이면 200, 오류가 발생한다면 400~503 사이로 발생하니 400 이상은 모두 예외로 보고 real2를 현재 profile로 사용

🔶 IDLE_PROFILE

  • 엔진엑스와 연결되지 않은 profile
  • 스프링 부트 프로젝트를 이 profile로 연결하기 위해 반환함

🔶 echo "${IDLE_PROFILE}"

  • bash라는 스크립트는 값을 반환하는 기능은 없음
    • 제일 마지막 줄에 echo로 결과를 출력 후, 클라이언트에서 그 값을 잡아서 ($(find_idle_profile))사용
  • 중간에 echo를 사용하면 안됨

 

 

stop.sh

#!/usr/bin/env bash

ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh

IDLE_PORT=$(find_idle_port)

echo "> $IDLE_PORT 에서 구동 중인 애플리케이션 pid 확인"
IDLE_PID=$(lsof -ti tcp:${IDLE_PORT})

if [ -z ${IDLE_PID} ]
then
  echo "> 현재 구동 중인 애플리케이션이 없으므로 종료하지 않습니다."
else
  echo "> kill -15 $IDLE_PID"
  kill -15 ${IDLE_PID}
  sleep 5
fi

🔶 ABSDIR=$(dirname $ABSPATH)

  • 현재 stop.sh가 속해 있는 경로를 찾음
  • 하단의 코드와 같이 profile.sh의 경로를 찾기 위해 사용됨

🔶 source ${ABSDIR}/profile.sh

  • 자바로 보면 일종의 import 구문
  • 해당 코드로 인해 stop.sh에서도 profile.sh의 여러 function을 사용할 수 있게 됨

 

 

start.sh

#!/usr/bin/env bash

ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh

REPOSITORY=/home/ec2-user/app/step3
PROJECT_NAME=springboot-webservice

echo "> Build 파일 복사"
echo "> cp $REPOSITORY/zip/*.jar $REPOSITORY/"

cp $REPOSITORY/zip/*.jar $REPOSITORY/

echo "> 새 애플리케이션 배포"
JAR_NAME=$(ls -tr $REPOSITORY/*.jar | tail -n 1)

echo "> JAR Name: $JAR_NAME"

echo "> $JAR_NAME 에 실행 권한 추가"

chmod +x $JAR_NAME

echo "> $JAR_NAME 실행"

IDLE_PROFILE=$(find_idle_profile)

echo "> $JAR_NAME 를 profile=$IDLE_PROFILE 로 실행합니다."

nohup java -jar \
  -Dspring.config.location="classpath:/application.yml","classpath:/application-$IDLE_PROFILE.yml","/home/ec2-user/app/application-oauth.yml","/home/ec2-user/app/application-real-db.yml" \
  -Dspring.profiles.active=$IDLE_PROFILE \
  $JAR_NAME > $REPOSITORY/nohup.out 2>&1 &
  • 기본적인 스크립트는 step2의 deploy.sh와 유사함
  • 다른 점이라면 IDLE_PROFILE을 통해 properties 파일을 가져오고(application-$IDLE_PROFILE.yml), active profile을 지정하는 것 뿐임
(-Dspring.profiles.active=$IDLE_PROFILE)

 

 

  • 여기서도 IDLE_PROFILE을 사용하니 profile.sh을 가져와야 함

 

 

 

 

health.sh

#!/usr/bin/env bash

ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh
source ${ABSDIR}/switch.sh

IDLE_PORT=$(find_idle_port)

echo "> Health Check Start!"
echo "> IDLE_PORT: $IDLE_PORT"
echo "> curl -s http://localhost:$IDLE_PORT/profile "
sleep 10

for RETRY_COUNT in {1..10}
do
  RESPONSE=$(curl -s http://localhost:${IDLE_PORT}/profile)
  UP_COUNT=$(echo ${RESPONSE} | grep 'real' | wc -l)

  # 엔진엑스와 연결되지 않은 포트로 스프링 부트가 잘 수행되었는지 체크
  if [ ${UP_COUNT} -ge 1 ]
  then # $UP_COUNT >= 1 ("real" 문자열이 있는지 검증)
    echo "> Health check 성공"
    switch_proxy  # 잘 떴는지 확인 되면, 엔진엑스 프록시 설정을 변경 (switch.sh의 switch_proxy 메서드)
    break
  else
    echo "> Health check의 응답을 알 수 없거나 혹은 실행 상태가 아닙니다."
    echo "> Health check: ${RESPONSE}"
  fi

  if [ ${RETRY_COUNT} -eq 10 ]
  then
    echo "> Health check 실패"
    echo "> 엔진엑스에 연결하지 않고 배포를 종료합니다."
    exit 1
  fi
  echo "> Health check 연결 실패. 재시도..."
  sleep 10
done
  • 엔진엑스와 연결되지 않은 포트로 스프링 부트가 잘 수행되었는지 체크
  • 잘 떴는지 확인되어야 엔진엑스 프록시 설정을 변경(switch_proxy)함
  • 엔진엑스 프록시 설정 변경은 switch.sh에서 수행

 

 

 

switch.sh

#!/usr/bin/env bash

ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh

function switch_proxy() {
  IDLE_PORT=$(find_idle_port)

  echo "> 전환할 Port: $IDLE_PORT"
  echo "> Port 전환"
  echo "set \$service_url http://127.0.0.1:${IDLE_PORT};" | sudo tee /etc/nginx/conf.d/service-url.inc  # 하나의 문장으로 만들어 파이프 라인으로 넘겨주기 위해 echo 사용 -> 앞에서 넘겨준 문장을 service-url.inc에 덮어씀

  echo "> 엔진엑스 Reload"
  sudo service nginx reload # restart는 잠시 끊김 현상, reload는 끈김 없이 다시 불러옴.
}

🔶 echo "set \$service_url http://127.0.0.1:${IDLE_PORT};"

  • 하나의 문장을 만들어 파이프라인(|)으로 넘겨주기 위해 echo사용
  • 엔진엑스가 변경할 프록시 주소를 생성
  • 쌍따옴표(")를 사용해야 함
  • 사용하지 않으면 $service_url을 인식하지 못하고 변수를 찾게 됨

🔶 | sudo tee /etc/nginx/conf.d/service-url.inc

  • 앞에서 넘겨준 문장을 service_url.inc에 덮어씀

🔶 sudo service nginx reload

  • 엔진엑스 설정을 다시 불러옴
  • restart와는 다름
  • restart는 잠시 끊기는 현상이 있지만 reload는 끊김없이 다시 불러옴
  • 다만, 중요한 설정들은 반영되지 않으므로 restart를 사용해야 함
  • 여기선 외부의 설정 파일인 service-url을 다시 불러오는 거라 reload로 가능함

 

반응형

 

⚡ 무중단 배포 테스트

  • 잦은 배포로 Jar 파일명이 겹칠 수 있음
    • 매번 버전을 올리는 것은 귀찮으므로 자동으로 버전값이 변경될 수 있도록 조치

 

build.gradle

version '1.0.1-SNAPSHOT-' + new Date().format("yyyyMMddHHmmss")
  • build.gradle은 Groovy 기반의 빌드툴
  • Groovy 언어의 여러 문법을 사용할 수 있는데, 여기서는 new Data()로 빌드할 때마다 그 시간이 버전에 추가되도록 구성

 

 

✔ 배포가 완료되면 CodeDeploy 로그로 잘 진행되는지 확인

tail -f /opt/codedeploy-agent/deployment-root/deployment-logs/codedeploy-agent-deployments.log

 

 

✔ 스프링 부트 로그도 보고싶다면

vim ~/app/step3/nohup

 

 

✔ 자바 애플리케이션 실행 여부 확인

ps -ef | grep java

 

✔ 2개의 애플리케이션이 실행되고 있음을 확인(real1, real2)

 

 

role 역할이 guest로 되어있어서 설정해줌

  •  member 엔티티
    public void updateRoleKey(Role role) {
        this.role = role;
    }

 

  •  CustomOAuth2UserService에
    private Member saveOrUpdate(OAuthAttributes attributes) {
        Member member = userRepository.findByEmail(attributes.getEmail())
                .map(entity -> entity.update(attributes.getName(), attributes.getPicture()))
                .orElse(attributes.toEntity());
        member.updateRoleKey(Role.USER);
        return userRepository.save(member);
    }

 

 

완성 !!

 

 

 

스프링 부트와 AWS로 혼자 구현하는 웹 서비스_이동욱 지음 책을 읽고 정리하는 글입니다

 

 

 

 

 

 

 


PREV

 

스프링 부트와 AWS로 혼자 구현하는 웹 서비스(9)

📌 Travis CI배포 자동화 코드가 푸시되면 자동으로 배포한다. 여러 개발자의 코드가 실시간으로 병합되고 테스트가 수행되는 환경, master 브랜치가 푸시되면 배포가 자동으로 이루어지는 환경을

nyeroni.tistory.com

 

728x90
LIST

BELATED ARTICLES

more