▪️ 24시간 365일 중단 없는 서비스를 만들자
배포 자동화 환경을 구축했지만, 배포하는 동안 애플리케이션이 종료되는 문제가 남아 있었다. 긴 기간은 아니지만, 새로운 Jar가 실행되기 전까진 기존 Jar를 종료시켜 놓기 때문에 서비스가 중단된다.
반면, 24시간 서비스하는 네이버나 카카오톡 같은 경우 배포하는 동안 서비스가 정지되지는 않는다. 그럼, 어떻게 서비스 중단 없이 배포를 계속할 수 있는지 확인하고 서비스에 적용해보자.
무중단 배포 소개
서비스를 정지하지 않고, 배포할 수 있는 방법들을 찾기 시작했고 이를 무중단 배포한다.
- AWS에서 블루 그린(Blue-Green) 무중단 배포
- 도커를 이용한 웹서비스 무중단 배포
이번 장에서 우리가 진행할 방법은 엔진엑스를 이용한 무중단 배포이다. 엔진엑스는 웹 서버, 리버스 프록시, 캐싱, 로드 밸런싱, 미디어 스트리밍 등을 위한 오픈소스 소프트웨어이다.
이전에 아파치가 대세였던 자리를 완전히 빼앗은 가장 유명한 웹서버이자 오픈소스이다.
엔진엑스가 가지고 있는 여러 기능 중 리버스 프록시가 있다.리버스 프록시란 엔진엑스가 외부의 요청을 받아 백엔드 서버로 요청을 전달하는 행위이다. 리버스 프록시 서버(엔진엑스)는 요청을 전달하고, 실제 요청에 대한 처리는 뒷단의 웹 애플리케이션 서버들이 처리한다.
엔진엑스를 이용한 무중단 배포를 하는 이유는 간단하다.
가장 저렴하고 쉽기 때문이다.
구조는 간단하다. 하나의 EC2 혹은 리눅스 서버에 엔진엑스 1대와 스프링 부트 Jar를 2대를 사용하는 것이다.
- 엔진엑스는 80(http), 443(https) 포트를 할당한다.
- 스프링 부트 1은 8081 포트로 실행한다.
- 스프링 부트 2는 8082 포트로 실행한다.
구조
전체적인 구조는 위와 같다.
구조는 간단하게 리눅스 서버에 엔진엑스 1대와 스프링 부트 Jar을 2대 사용하는 방식이다.
- 엔진엑스는 80(http), 443(https) 포트 할당
- 스프링 부트 1은 8081 포트 할당
- 스프링 부트 2는 8082 포트 할당
운영 과정은 다음과 같다.
- 사용자는 서비스 주소로 접속 (80 또는 442 포트)
- 엔진엑스는 사용자의 요청을 받아 현재 연결된 스프링 부트 1인 8081포트로 요청을 전달. (8082 포트로 연결된 스프링 부트 2는 아직 엔진엑스와 연결된 상태가 아니므로 요청을 받지 못한다.)
- 1.1 버전 배포를 진행한다. 1.1 버전으로 신규 배포가 필요하면, 엔진엑스와 연결되지 않은 스프링부트2(8082 포트)로 배포한다.엔진엑스는 스프링 부트1을 바라보기 때문에 배포하는 동안에도 서비스가 중단되지 않는다.
- 배포가 끝나면 스프링 부트 2가 정상적으로 구동되는지 확인.
- 정상적으로 구동된다면 nginx reload 명령어를 통해 nginx가 8081 대신 8082를 바라보도록 한다. 이 과정은 0.1초내에 완료된다.
- 이후 배포과정은 8081으로 진행된다.
즉, 8081포트와 8082포트를 두개 두고 처음에는 8081로 배포, ver1.1배포는 8082로 배포후 nginx를 8082바라보도록 reload...이 과정을 반복 하는 방식이다.
엔진엑스 설치와 스프링 부트 연동하기
먼저 EC2에 엔진엑스를 설치한다.
sudo yum install nginx
설치 완료 후 다음 명령어로 엔진엑스를 실행한다.
sudo service nginx start
엔진엑스가 잘 실행되었다면 다음과 같은 메시지를 볼 수 있다.
Starting nginx: [ OK ]
외부에서 잘 노출되는지 확인해 보자.
보안 그룹 추가
먼저 엔진엑스 포트번호를 보안 그룹에 추가하자. 엔진엑스 포트번호는 기본적으로 80이다.
[EC2->보안그룹->EC2 보안 그룹 선택->인바운드 편집]으로 차례로 이동해서 변경한다.
리다이렉션 주소 추가
8080이 아닌 80포트로 주소가 변경되니 구글,네이버 로그인에도 변경된 주소를 등록해야만 한다.
기존 등록된 리디렉션 주소에서 8080부분을 제거하여 추가 등록한다.
추가한 후에는 EC2의 도메인으로 접근하되,8080포트를 제거하고 접근해 본다.(80 포트는 기본적으로 도메인에서 포트번호가 제거된 상태이다.)
즉, 포트번호 없이 도메인만 입력해서 브라우저에 접속한다.
그럼 엔진엑스 웹페이지를 볼 수 있다.
엔진엑스와 스프링 부트 연동
sudo vim /etc/nginx/nginx.conf
설정 내용 중 server 아래의 location / 부분을 찾아서 추가한다.
include /etc/nginx/conf.d/service-url.inc;
location / {
proxy_pass $service_url;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
}
proxy_pass : 엔진엑스로 요청이 오면 해당 url로 전달. 앞서 만들었던 파일을 include해서 변수로 사용.
proxy_set_header : 실제 요청 데이터를 header의 각 항목에 할당. 예를 들면, 요청헤더에 X-Real-IP 값으로 요청자의 IP를 넣는다.
여기서, 에러가 떠서 다음을 추가하니 정상 작동되었다.
다음을 추가
set $service_url http://127.0.0.1:8080;
설정파일 열기
sudo vim /etc/nginx/nginx.conf
그 후 엔진엑스를 재시작해보자.
sudo service nginx restart
▪️ 무중단 배포 스크립트 만들기
API를 하나 추가한다. 이 API는 이후 배포 시에 8081을 쓸지, 8082를 쓸지 판단하는 기준이 된다.
import java.util.Arrays;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@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도 남겨둔다.
이 코드가 잘 작동하는지 테스트 코드를 작성해보자. 해당 컨트롤러는 특별히 스프링 환경이 필요하지는 않아, @SpringBootTest없이 테스트 코드를 작성한다.
ProfileController나 Environment 모두 자바 클래스(인터페이스)이기 때문에 쉽게 테스트할 수 있다.
public 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
assertThat(profile).isEqualTo(expectedProfile);
}
@Test
public void real_profile이_없으면_첫_번째가_조회된다() {
//given
String expectedProfile = "oauth";
MockEnvironment env = new MockEnvironment();
env.addActiveProfile(expectedProfile);
env.addActiveProfile("real-db");
ProfileController controller = new ProfileController(env);
//when
String profile = controller.profile();
//then
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
assertThat(profile).isEqualTo(expectedProfile);
}
}
그리고 /profile이 인증 없이도 호출될 수 있게 SecurityConfig 클래스에 제외 코드를 추가한다.
.requestMatchers("/", "/css/**", "/image/**", "/js/**", "/h2-console/**","/profile").permitAll()
이제 아래를 통해 Security 설정이 잘 되었는지도 테스트 코드로 검증해보자. 아래 클래스 파일을 추가한다.
이 검증은 스프링 시큐리티 설정을 불러와야 한다.따라서 @SpringBootTest를 사용하는 테스트 클래스를 하나 더 추가한다.
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
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.junit4.SpringRunner;
@RunWith(SpringRunner.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);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isEqualTo(expected);
}
}
테스트가 모두 성공했다면 깃허브로 푸시후 배포한다.
배포가 끝나면 브라우저에서 /profile로 접속해서profile이 잘 나오는지 확인한다.
real1,real2 profile생성
현재 EC2 환경에서 실행되는 profile은 real밖에 없다.
해당 profile은 Travis CI 배포 자동화를 위한 profile이니 무중단 배포를 위한 profile 2개(real1,real2)를 추가한다.
엔진엑스 설정 수정
무중단 배포의 핵심은 엔진엑스 설정이다.
배포 때마다 엔진엑스의 프록시 설정(스프링 부트로 요청을 흘려보내는)이 순식간에 교체된다.
여기에서 프록시 설정이 교체될 수 있도록 설정을 추가하자.
엔진엑스 설정이 모여있는 /etc/nginx/conf.d/에 service-url.inc 라는 파일을 하나 생성한다.
sudo vim /etc/nginx/conf.d/service-url
그리고 아래와 같이 입력하고 저장,종료한다.(:wq)
set $service_url http://127.0.0.1:8080;
이제 해당파일은 엔진엑스가 사용할 수 있게 설정하자.
아래로 nginx.conf파일을 연다.
sudo vim /etc/nginx/nginx.conf
그 후 location / 부분을 찾아 다음과 같이 변경한다.
location / 부분을 찾아서, 아래와 같이 수정한다.
include를 통해 방금 작성한 파일을 포함시켜 주고, proxy_pass가 변수명 service_url을 참고하도록 한다.
마찬가지로 저장하고 나온 후, Nginx를 재시작해 주자.
sudo service nginx restart
다시 브라우저에서 정상 호출되는지 확인한다.확인된다면, 엔진엑스 설정까지 잘 된 것이다.
배포 스크립트들 작성
다음 순서는 배포 스크립트 작성이다.
먼저 step2와 중복되지 않기 위해 step3의 디렉토리를 만들어 주자.
mkdir ~/app/step3 && mkdir ~/app/step3/zip
무중단 배포는 앞으로 step3를 사용한다.그래서 appspec.yml 역시 step3로 배포되도록 수정한다.
version: 0.0
os: linux
files:
- source: /
destination: /home/ec2-user/app/step3/zip/
overwrite: yes
무중단 배포를 진행할 스크립트들은 총 5개이다.
- stop.sh: 기존 Nginx에 연결되어 있진 않지만, 실행 중이던 Spring Boot 종료
- start.sh: 배포할 신규 버전 Spring Boot 프로젝트를 stop.sh로 종료한 'profile'로 실행
- health.sh: start.sh로 실행시킨 프로젝트가 정상 실행되었는지 체크
- switch.sh: Nginx가 바라보는 Spring Boot를 최신 버전으로 변경
- profile.sh: 앞선 4개 스크립트 파일에서 공용으로 사용할 'profile'과 포트 체크 로직
appspec.yml에 앞선 스크립트를 사용하도록 설정한다.아래부분을 추가한다.
hooks:
AfterInstall:
- location: stop.sh # 엔진엑스와 연결되어 있지 않은 스프링부트를 종료
timeout: 60
runas: ec2-user
ApplicationStart:
- location: start.sh # 엔진엑스와 연결되어 있지 않은 Port 로 새 버전 스프링부트 시작
timeout: 60
runas: ec2-user
ValidateService:
- location: health.sh # 새 스프링부트가 정상적으로 실행됐는지 확인
timeout: 60
runas: ec2-user
Jar 파일이 복사된 이후부터 차례로앞선 스크립트들이 실행된다고 보면 된다.
아래 스크립트들을 scripts 디렉토리에 추가한다.
profile.sh
#!/usr/bin/env bash
# 쉬고 있는 profile 찾기: real1이 사용 중이면 real2가 쉬고 있고, real2가 실행 중이면 real1이 쉬고 있음
function find_idle_profile()
{
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
echo "${IDLE_PROFILE}"
}
# 쉬고 있는 profile의 port 찾기
function find_idle_port()
{
IDLE_PROFILE=$(find_idle_profile)
if [ ${IDLE_PROFILE} == real1 ]
then
echo "8081"
else
echo "8082"
fi
}
- (1) $(curl -s -o /dev/null -w "%{http_code}" http://localhost/profile)
- 현재 엔진엑스가 바라보고 있는 스프링 부트가 정상적으로 수행 중인지 확인합니다.
- 응답값을 HttpStatus로 받습니다.
- 정상이면 200, 오류가 발생한다면 400~503 사이로 발생하니 400 이상은 모두 예외로 보고 real2를 현재 profile로 사용합니다.
- (2) IDLE_PROFILE
- 엔진엑스와 연결되지 않은 profile입니다.
- 스프링 부트 프로젝트를 이 profile로 연결하기 위해 반환합니다.
- (3) 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_PORT} ]
then
echo "> 현재 구동중인 어플리케이션이 없으므로 종료하지 않습니다."
else
echo "> kill -15 $IDLE_PID"
kill 15 ${IDLE_PID}
sleep 5
fi
- (1) ABSDIR=$(dirname $ABSPATH)
- 현재 stop.sh가 속해 있는 경로를 찾습니다.
- 하단의 코드와 같이 profile.sh의 경로를 찾기 위해 사용됩니다.
- (2) 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-web-practise
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.properties,classpath:/application-real.properties,/home/ec2-user/app/application-oauth.properties,/home/ec2-user/app/application-real-db.properties\
-Dspring.profiles.active=real \
$JAR_NAME > $REPOSITORY/nohup.out 2>&1 &
(1)기본적인 스크립트는 step2의 deploy.sh와 유사합니다.
(2)다른 점이라면 IDLE_PROFILE을 통해 properties 파일을 가져오고(application-#IDLE_PROFILE.properties), 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
break
else
echo "> Health check의 응답을 알 수 없거나 혹은 실행 상태가 아닙니다."
echo "> Health check: ${RESPONSE}"
fi
if [ ${RETRY_COUNT} -eq 10 ]
then
echo "> Health check 실패. "
echo "> Nginx에 연결하지 않고 배포를 종료합니다."
exit 1
fi
echo "> Health check 연결 실패. 재시도..."
sleep 10
done
- (1)엔진액스와 연결되지 않은 포트로 스프링 부트가 잘 수행되었는지 체크합니다.
- (2)잘 떴는지 확인되어야 엔진엑스 프록시 설정을 변경(switch_proxy)합니다.
- (3)엔진엑스 프록시 설정 변경은 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 "> Nginx Reload"
sudo service nginx reload
}
- (1) echo "set \$service_url http://127.0.0.1:${IDLE_PORT};
- 하나의 문장을 만들어 파이프라인(|)으로 넘겨주기 위해 echo를 사용합니다.
- 엔진엑스가 변경할 프록시 주소를 생성합니다.
- 쌍따옴표(“)를 사용해야 합니다.
- 사용하지 않으면 $service_url을 그대로 인식하지 못하고 변수를 찾게 됩니다.
- (2) | sudo tee /etc/nginx/conf.d/service-url.inc
- 앞에서 넘겨준 문장을 service-url.inc에 덮어 씁니다.
- (3) sudo service nginx reload
- 엔진엑스 설정을 다시 불러옵니다.
- restart와는 다릅니다.
- restart는 잠시 끊기는 현상이 있지만, reload는 끊김 없이 다시 불러옵니다.
- 다만, 중요한 설정들은 반영되지 않으므로 restart를 사용해야 합니다.
- 여기선 외부의 설정 파일인 service-url을 다시 불러오는 거라 reload로 가능합니다.
이렇게 5개의 스크립트를 모두 작성했다.
무중단 배포 테스트
배포 테스트전, 잦은 배포로 Jar 파일명이 겹치지 않게 build.gradle에 코드를 수정 해야 한다.
매번 버전을 올리는 것이 귀찮으므로 자동으로 버전 값이 변경될 수있도록 조치하자.
build.gradle로 아래를 추가한다.
version '1.0.1-SNAPSHOT-' + new Date().format("yyyyMMddHHmmss")
여기까지 구성 후 최종 코드를 깃허브에 푸시한다.CodeDeploy 로그로 잘 진행되는지 확인해 본다.
스프링 부트 로그도 확인 가능하다.
# CodeDeploy 로그 확인
tail -f /opt/codedeploy-agent/deployment-root/deployment-logs/codedeploy-agent-deployments.log
# 스프링 부트 로그 확인
vim ~/app/step3/nohup.out
이제 한번 더 배포하면 그때는 real2로 배포된다. 이 과정에서 브라우저 새로고침시, 전혀 중단이 없는 것을 확인 가능하다.
2번 배포를 진행한 뒤에 자바 애플리케이션 실행 여부를 확인한다.
ps -ef | grep java
그럼 2개의 애플리케이션이 실행되고 있음(java -jar)을 알 수 있다.
이제 이 시스템은 마스터 브랜치에 푸시가 발생하면 자동으로 배포가 진행되고,서버 중단 역시 전혀 없는 시스템이 되었다.
▪️ 1인 개발시 도움이 될 도구와 조언들
댓글
Disqus - 소셜 댓글 서비스. 자바스크립트 코드만 사이트에 등록하면 위젯 형태로 서비스에서 바로 사용 가능
LiveRe - 국내에서 만든 소셜 댓글 서비스
외부 서비스 연동
Zapier - 수많은 클라우드와 SNS 서비스들의 오픈 API를 이용하여 서로 간의 연동을 지원. 트리거라는 기능을 이용해 이벤트 발생 조건을 만들고, 액션을 이용해 어떤 행위를 할지 결정.
대표적 예로 페이스북에 새 글이 올라오면 슬랙으로 메시지를 보내도록 설정가능.
월 100건은 무료,그 이상 사용은 요금 지불.
OFTTT
A를하면 B를 하라는 의미로 클라우드와 SNS 연동을 지원하는 서비스
방문자 분석
구글 애널리틱스 - 구글에서 지원하는 사이트 분석 도구.스크립트만 넣으면 수준급 사이트 분석이 가능
CDN
전세계 분산 서버 네트워크. 흔히 정적 콘텐츠라고 불리는 JS, CSS, 이미지 등을 전 세계에 퍼진서버에 전달하여 사용자가 서비스에 접속할 때 가장 가까운 서버에서 가져가도록 지원하는 서비스.
클라우드 플레어 - 해외,국내에서 가장 많이 사용되는 CDN 서비스.정적 파일 캐싱하여 제공하는 CDN기능은 무료.
이메일 마케팅
Mailchimp - 해외에서 서비스 중인 이메일 마케팅 서비스. 회원 2000명에게 월 12000개의 메일을 무료로 보낼 수 있음.
'개발프로젝트' 카테고리의 다른 글
[CleanCode] 주석은 변명이 아니다 – 네이버 쇼핑 API 응용 코드로 알아보는 클린 코드 (3) | 2025.05.19 |
---|---|
[CleanCode] 함수 – 클린코드 원칙대로 리뷰 기능 함수를 리팩토링 해보자 (1) | 2025.05.19 |
[개발프로젝트] 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 chapter 09- 코드가 푸시되면 자동으로 배포해 보자 (1) | 2024.01.11 |
[개발프로젝트] 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 chapter 08- EC2 서버에 프로젝트를 배포해 보자 (2) | 2024.01.07 |
[개발프로젝트] 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 chapter 07- AWS에 데이터베이스 환경을 만들어보자 - AWS RDS (1) | 2024.01.06 |