본문으로 건너뛰기

"review-me" 태그로 연결된 3개 게시물개의 게시물이 있습니다.

모든 태그 보기

스프링에서 예외를 잘! 처리하는 방법

· 약 12분
나영서 (산초)
리뷰미 BE

Intro

자바 프로그램에서 예외가 발생하면 스레드가 종료됩니다. 하지만 스프링에서는 예외가 발생해도 어플리케이션이 종료되지 않고, 예외 응답을 반환합니다. 스프링은 어떻게 예외를 처리하기에 이렇게 작동하는 것일까요?🤔 이번 글에서는 스프링에서 어떻게 예외를 처리하는지와, 어떻게해야 예외를 잘~ 처리할 수 있는지 알아보겠습니다.


🔍 스프링의 기본 예외 처리 방식

어떠한 예외처리도 하지 않았을 때, 스프링에서 어떤식으로 예외가 처리될까요? 우선 스프링의 기본 작동 방식에 대해 생각해봅시다. 스프링에 요청이 오면, WAS - Dispatcher Servlet - HandlerMapping - HandlerAdapter - Controller 를 거쳐 로직이 실행됩니다.

이때 예외가 발생하면 예외 내용은 WAS까지 거슬러 올라갑니다. 그럼 WAS는 어플리케이션에서 처리할 수 없는 예외라 판단하여 에러 컨트롤러로 예외 내용을 전달합니다. 즉, 예외가 발생하면 WAS - Controller - WAS - ErrorController 의 흐름을 갖게 됩니다. 이 과정을 통해 예외가 발생해도 어플리케이션을 종료하지 않고, 마치 정상 요청인 것처럼 예외 응답을 반환하는 것입니다.

BasicErrorController

이때 호출되는 에러 컨트롤러가 바로 BasicErrorController 입니다. 실제로 어떠한 예외처리도 하지 않고 BasicErrorController에 BreakPoint 를 걸어두고 디버깅하니, BasicErrorController에서 예외가 핸들링되는 것을 확인할 수 있었습니다.

BasicErrorController의 응답

BasicErrorController는 DefaultErrorAttributesgetErrorAttributes() 함수를 호출해서 응답할 내용을 불러옵니다. 이때 getErrorAttributes()가 기본적으로 제공하는 속성과, 설정을 통해 추가할 수 있는 속성들은 다음과 같습니다.

  • timestamp: 에러가 발생한 시간
  • status: 에러의 Http 상태
  • error: 에러 코드
  • path: 에러가 발생한 uri
  • exception: 최상위 예외 클래스의 이름(설정 필요)
  • message: 에러에 대한 내용(설정 필요)
  • errors: BindingExecption에 의해 생긴 에러 목록(설정 필요)
  • trace: 에러 스택 트레이스(설정 필요)
// 어떤 설정도 하지 않았을 때의 응답
{
"timestamp": "2024-07-21T16:16:40.463+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/reviews/999"
}
// 추가할 수 있는 속성들
server:
error:
include-message: always
include-binding-errors: always
include-stacktrace: always
include-exception: true
// 모든 속성을 추가했을 때의 응답
{
"timestamp": "2024-07-21T16:19:58.729+00:00",
"status": 500,
"error": "Internal Server Error",
"exception": "org.springframework.orm.jpa.JpaObjectRetrievalFailureException",
"trace": "org.springframework.orm.jpa.JpaObjectRetrievalFailureException\n\tat ….",
"message": "No message available",
"path": "/reviews/999"
}

BasicErrorController를 통한 예외 처리의 한계

혹시 '이것만으로도 충분히 훌륭한데?' 라는 생각이 드시나요? 하지만 이 방식에는 몇가지 한계가 존재합니다. 첫째로, 이 방식은 WAS에서 컨트롤러를 거쳐, 다시 WAS로 왔다가, 에러 컨트롤러로 가는 흐름인데요, 이 과정이 길고 복잡하게 느껴집니다. 또 이는 필터나 인터셉터를 2번 호출하는 등 다른 문제를 야기할 수 있습니다. 그리고 결정적으로, 예외에 따라 다른 HttpStatusCode와 메세지를 줄 수도 없습니다😓


🔍 스프링이 제공하는 다양한 예외 처리 방식

이런 단점을 보완하기 위해서, 스프링에서는 기본 예외 처리 외에도 다양한 예외 처리 방식을 제공합니다.

HandlerExceptionResolver

HandlerExceptionResolver는 예외 처리 방식을 추상화한 인터페이스입니다. 이를 구현하는 구현체들은 발생한 예외를 캐치하여 응답의 Http 상태나 메세지를 설정합니다.

HandlerExceptionResolver의 구현체들

HandlerExceptionResolver의 구현체들은 HandlerExceptionResolverComposite에 우선순위 순서대로 빈으로 등록되어 관리됩니다.

그리고 예외 발생 시, 우선순위 순으로 구현체들을 순회하며 핸들링을 할 수 있는지 확인합니다.

public class HandlerExceptionResolverComposite implements HandlerExceptionResolver, Ordered {

@Override
@Nullable
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
if (this.resolvers != null) {
// 구현체들을 순회하면서 예외를 처리할 수 있는지 확인
for (HandlerExceptionResolver handlerExceptionResolver : this.resolvers) {
ModelAndView mav = handlerExceptionResolver.resolveException(request, response, handler, ex);
if (mav != null) {
return mav;
}
}
}
return null;
}
}

빈으로 등록된 HandlerExceptionResolver 구현체과 그것이 처리하는 대상은 아래와 같습니다.

  • ExceptionHandlerExceptionResolver : ExceptionHandler를 처리
  • ResponseStatusExceptionResolver : ResponseStatus, ResponseStatusException를 처리
  • DefaultHandlerExceptionResolver : 스프링 내부의 기본 예외들을 처리

하지만 ResponseStatusExceptionResolver 만으로는 에러 응답을 수정할 수 없으며, 일괄 에러 처리를 할 수 없다는 단점이 있으므로 이번 글에서는 설명하지 않겠습니다.


🔍 HandlerExceptionResolver를 동작시키는 것들

@ExceptionHandler와 @ControllerAdvice

@ExceptionHandler는 가장 우선순위가 높은 예외 핸들러인 ExceptionHandler ExceptionResolver에서 처리됩니다. @ExceptionHandler는 예외 응답의 코드와 메세지를 자유롭게 설정할 수 있다는 장점이 있습니다. @ExceptionHandler는 컨트롤러의 메서드에 바로 사용할 수도 있으며, @ControllerAdvice나 @RestControllerAdvice가 있는 클래스의 메소드에도 사용할 수 있습니다. 만약 컨트롤러의 메서드에도 @ExceptionHandler를 사용하고, @ControllerAdvice에도 사용한다면 전자가 우선시됩니다.

스프링 내부의 기본 예외들

스프링에서 발생할 수 있는 기본 예외들은 미리 정의되어, DefaultHandler ExceptionResolver에서 처리됩니다. 대표적인 예외로는 지원하지 않는 HttpMethod 로 요청을 보낼 때 발생하는 HttpRequestMethod NotSupportedException가 있습니다. DefaultHandler ExceptionResolver는 에러를 핸들링해서 어떤 기본 예외인지 판별하는 역할까지만 하므로, 직접 예외 응답을 반환하지는 않습니다. 따라서 최종적으로 BasicController를 통해서 예외 응답을 반환합니다.

ResponseEntityExceptionHandler

여기까지 읽으시며 이상함을 느끼셨나요?😳 ExceptionHandler를 사용하면 예외 응답 형식을 지정할 수 있습니다. 반면 스프링 기본 예외는 DefaultHandler ExceptionResolver로 처리되기 때문에 BasicController의 응답 형식을 따르게 됩니다. 따라서 ExceptionHandler로 처리되는 예외와, 기본 예외의 응답 형식이 다르다는 문제가 생기게 됩니다. 이를 해결하기 위해 스프링은 ResponseEntity ExceptionHandler 를 제공합니다.

ResponseEntity ExceptionHandler는 스프링 기본 예외에 대한 핸들링을 미리 정의해둔 추상 클래스입니다. 따라서 ControllerAdvice가 ResponseEntity ExceptionHandler를 상속하게 하는것 만으로 스프링 기본 예외를 핸들링할 수 있습니다. 또한 ResponseEntity ExceptionHandler는 모든 기본 예외를 handleExceptionInternal() 함수로 처리하고 있기 때문에, 이 함수를 오버라이딩해줌으로써, 모든 기본 예외의 응답을 커스텀 예외 응답형식으로 통일할 수 있습니다.


어떻게 해야 예외를 잘 응답할 수 있을까?

여기까지 우리는 스프링에서 예외를 처리하는 방법에 대해 알아봤습니다. 그럼 이들을 바탕으로 우리는 어떻게 예외를 잘 처리하고, 잘 응답할 수 있을까요?🤔 제가 세운 기준은 아래와 같습니다.

1. ExceptionHandler를 사용하자.

스프링의 기본 예외 처리 방식인 BasicErrorController 를 사용하는 방식은, http status code 와 예외 메세지를 지정할 수 없다는 문제가 있습니다. ExceptionHandler 를 사용해서 유연하게 예외를 핸들링하는 것이 좋습니다.

2. ControllerAdvice 안에서 ExceptionHandler를 사용하자.

ExceptionHandler를 사용해서 각 컨트롤러마다 예외를 처리하는 방식은 중복되는 코드가 많을 것입니다. 또한 어떤 컨트롤러에는 ExceptionHandler를 붙이고, 다른 컨트롤러에는 붙이지 않는 등의 누락도 생길 수 있습니다. 따라서 예외 핸들링을 전역으로 할 수 있는 ControllerAdvice 안에서 ExceptionHandler를 사용하는 것이 좋습니다.

3. 최상위 예외인 Exception를 핸들링하자.

최상위 예외에 대한 핸들링을 하지 않는다면, BasicController 를 통해 응답되는 예외가 있을 수 있습니다. 그렇다면 우리가 정의한 커스텀 예외 응답과 다른 형식이 되므로, 일관성이 깨지게 됩니다. 따라서 최상위 예외에 대해 처리를 해줘야 합니다.

4. 스프링 기본 예외의 응답을 통일하자.

마찬가지로 일관성을 위해 스프링 기본 예외의 응답도 통일할 필요가 있습니다. 따라서 ControllerAdvice에서 ResponseEntityExceptionHandler를 상속하고, handleExceptionInternal() 함수를 오버라이딩해줘야 합니다.

GitHub Actions를 활용한 CI/CD (Self-Hosted Runner)

· 약 18분
이동훈 (아루)
리뷰미 BE

Continuous Integration

CI는 일련의 구체적인 과정을 의미하기보다는, 개발 방식 중 하나를 뜻합니다. 기능을 구현하고, 기존 저장소에 새로운 기능을 병합하는 일련의 과정을 말하죠. 이때 빌드/통합 오류를 가능한 한 빠르게 찾아내야 하며, 이때 자동화 빌드나 테스트 도구가 활용됩니다. 이 중 하나로 Jenkins, GitHub Actions와 같은 CI/CD 도구들이 사용돼요.

이슈를 발행해 할 일을 만들고, PR을 통해 서로 리뷰하고 머지하기. 추가적인 테스트나 자동화 빌드 도구가 필요할까요? 서로 확인했으니 믿고 사용해도 되지 않을까요? 실제로 팀 내에서 서로 다른 기능을 머지하려는 도중 충돌이 발생했어요. 이를 잘 해결한 뒤 머지할 수 있었습니다. 하지만 아래와 같은 일이 추가로 발생했어요.

사람은 실수하기 마련입니다. 코드에 변화가 있었음에도 회귀 테스트regression test를 진행하지 않았어요. 결국 테스트되지 않은 코드가 PR에 포함됐어요. 이를 다른 팀원들도 확인하지 못해 실제 개발 서버에 머지됐습니다. 이를 해결하기 위해 다시 이슈를 작성하고, 핫픽스를 적용하고, … 😮‍💨

테스트와 빌드가 매 PR마다, PR 안의 커밋마다 진행된다면 편리하지 않을까요? 만약 실패한다면 저장소에 통합하는 것이 실패하도록 한다면 더 좋겠습니다. GitHub Actions을 사용하면 이를 해결할 수 있습니다. 빌드부터 테스트 자동화와 더불어 Branch protection을 통해 빌드/테스트에 실패한다면 병합하지 않도록 설정할 수도 있어요.

정보

이 글에서는 GitHub Actions를 활용한 CI, CD에 대해서 다룹니다. 아래와 같은 사전지식을 요구하나, 모르더라도 따라오는 데에 큰 어려움이 없도록 글을 써보려고 합니다. 😄

  • Git
  • Shell script
  • Gradle task

CI with GitHub Actions

📚 용어 정리

GitHub Actions는 이곳에 정말 자세하게 설명돼 있습니다. 러닝커브가 낮은 편이 아니라서 꽤나 많은 사전지식을 필요로 하지만, 간단하게 action이 동작하는 방식을 알기 위한 용어들을 소개합니다.

  • 🛠️ Workflows
    Job의 집합입니다, repository에서 특정 이벤트가 트리거되면 실행됩니다. ./github/workflows 디렉토리 밑에 .yml 또는 .yaml을 생성해야 해요. Workflow는 다른 workflow에서 재사용할 수도 있습니다.

  • 🎉 Events
    Workflow가 실행되게끔 하는 이벤트입니다. PR을 열거나, 이슈를 열거나, push하거나 등 다양한 event가 존재합니다.

  • ♻️ Jobs
    Workflow에 속하는 step의 집합이며, 같은 runner 안에서 실행됩니다. Job끼리는 병렬적으로 실행됩니다. 다른 job과의 의존관계가 존재한다면, 이를 needs 로 명시해야 합니다. 각 step은 shell script이거나, action 중 하나입니다. step은 순서대로 실행되며, 이전 step이 다음 것에 영향을 끼칩니다. 빌드한 것을 다음 step에서 활용할 수도 있어요.

  • 📋 Actions
    Github Actions에서 사용되는 custom application. workflow에서의 중복되는 코드를 라이브러리로 만들었다고 생각하면 편합니다. GitHub에서 제공하는 여러 권한을 가져갈 수 있습니다. 이를 외부 Job에서 설정해줄 수도 있어요.

  • 🏃 Runners
    Workflow가 돌아가는 os가 포함된 서버입니다. GitHub는 Ubuntu Linux, Windows, macOS Runner를 제공합니다. 직접 runner를 호스팅할 수도 있는데, 이를 self-hosted runner라고 부릅니다.

🔍 요구사항 정의 및 분석

우리는 1. 빌드에 실패하는 경우 저장소에 반영되지 않도록 하고 싶습니다. 이는 2. PR이 생성되었을 때나 변경 사항이 저장소에 반영되었을 때, 빌드 테스트를 진행할 수 있어야 합니다.

앞쪽 문장인 빌드 실패 시 저장소 반영하지 않도록 하는 것은 GitHub에서 제공하는 Branch protection을 사용해 간단하게 해결할 수 있습니다. PR에 Approved 리뷰가 일정 개수 이상, Status check 등 다양한 브랜치 보호 옵션을 제공하니 중요한 저장소라면 꼭 챙겨가야 합니다.

브랜치 보호는 되었으니, 이젠 Status check에 반영될 빌드 - 테스트를 자동화해야 합니다. 위 요구사항 문장을 조금 더 잘게 쪼개 볼까요?

PR이 생성되었을 때나 변경 사항이 저장소에 반영되었을 때,

앞서 설명한 용어 중 event에 해당해요. GitHub actions에서는 on이라는 키워드로 적용할 수 있습니다.

빌드 및 테스트를 진행할 수 있다

테스트를 진행하기 위해서는 다양한 옵션이 필요합니다. 우선 테스트를 돌릴 환경이 있어야 해요. 나아가 그 환경 안에 Java가 설치돼 있어야 하고, 저장소를 복제해 코드를 가져와야 합니다. 프로젝트에 따라 자동 빌드 툴도 필요할 수 있습니다. 저희 팀에서는 Gradle을 활용해 빌드하므로, Gradle도 필요하겠네요.

할 일이 상당히 많아 보이는데, 굵게 표시한 문구들은 전부 GitHub에서 제공합니다. 따라서 이를 활용할 수 있는 workflow 파일 하나로 완성해낼 수 있습니다. 파일을 직접 만들어보고, 실행 결과를 확인해 봅시다!

📝 Workflow 파일 작성하기

# Workflow 파일은 .github/workflows 디렉토리 아래 .yml 또는 .yaml 파일로 존재한다.

# Workflow 이름. 하나의 파일이 하나의 Workflow를 담당한다. Workflow는 Job의 집합이다.
name: Build test with Gradle

# Event. 언제 이 Workflow가 트리거돼 실행될 지 작성한다.
on:
pull_request:
branches:
- develop # develop 브랜치에 PR이 작성되는 경우

jobs:
# build라는 이름의 Job 한 개로 이루어진 workflow이다.
# build는 이름이며, 자유롭게 적을 수 있다.
build:
# 이 Job이 실행될 환경. GitHub에서는 Linux, Windows, macOS 환경을 제공한다.
runs-on: ubuntu-latest

# Job은 여러 Step으로 이뤄져 있으며, 이는 순서대로 실행된다.
steps:
# 가상환경에서 Repository를 clone해 코드를 사용할 수 있도록 한다.
- name: Checkout to current repository
uses: actions/checkout@v4

# JDK를 설치한다.
- name: Setup JDK Corretto using cached gradle dependencies
uses: actions/setup-java@v4
with:
distribution: 'corretto'
java-version: 17
cache: 'gradle'

# 사전 작업이 모두 준비되었으므로, 테스트를 진행한다.
- name: Build and test with gradle
run: ./gradlew test

제공하는 action들은 저장소가 존재하며, 해당 저장소에 어떻게 사용하는지 자세히 적혀있는 경우가 많습니다. 예를 들어 actions/setup-java@v4의 경우, 특별한 버전과 벤더를 선택함과 더불어 gradle 캐싱까지 진행합니다.

run에서는 실제로 실행할 쉘 스크립트를 작성하면 됩니다. shell script에 익숙하지 않다면, 자신있는 다른 언어(python, nodejs 등)로 로직을 짜준 뒤, 실행하는 방법도 있습니다.

이제 PR을 올릴 때마다 빌드 및 테스트를 진행합니다. 실패하는 경우에는 머지하는 불상사가 발생하지 않게 되었어요 🔥

Continuous Deployment

🤔 Delivery? Deployment?

CI와 함께 붙어다니는 단어로 CD가 있습니다 💿. CD는 위와 같이 두 용어가 자주 혼용되는데요, 비슷한 맥락이지만 아래와 같은 차이점이 있답니다.

  • Continuous delivery는 프로덕션 코드에 직접 빌드 결과를 제공하기까지만 하며, 배포를 위해 추가적인 작업이 필요합니다. '출시일'이라는 개념이 존재한다고 보면 됩니다.
  • Continuous deployment는 배포까지 파이프라인에 속해요. 추가적으로 개입할 프로세스가 존재하지 않으며, 사용자는 항상 최신 버전의 코드를 마주합니다.

이번 프로젝트에서의 개발 서버는 Continuous Deployment로 진행되기를 원했습니다. 기능 개발이 완료된다면 즉시 배포돼 프론트엔드가 확인하는 것을 목표로 하기 때문이예요. 이 또한 GitHub actions를 활용해 CD 파이프라인도 작성할 수 있습니다.

🚫 제약 사항

CD는 다양한 방법으로 구현될 수 있습니다. ssh 접속, aws S3와 CodeDeploy 등 다양한데요, 프로젝트를 진행하면서 다음과 같은 제약 사항이 존재해 이들을 사용할 수 없었습니다.

  • 캠퍼스 외부에서의 접속은 금지돼 있습니다. 80/443 포트를 통한 웹 접근만 가능합니다. ssh는 22번 포트로 통신합니다. (다른 방법으로 우회할 수는 있겠습니다만, 강하게 권하지 않습니다 🥹)
  • 제공된 aws IAM 계정으로는 S3의 Access Token을 사용할 수 없습니다. 따라서 S3 자동 업로드 / CodeDeploy 사용이 불가합니다. 이를 해결하기 위해 새로운 aws 계정을 생성해야 하며, 다양한 설정이 필요합니다. (Reference)

간단한 해결 방법으로 GitHub의 Self-hosted runner를 활용합니다. 서비스를 띄우는 인스턴스에 리스너를 달아두는 형식입니다. actions에서 443번 포트를 통해 workflow 정보를 전달하기 때문에, 아직까지 80포트만을 사용하는 개발 단계에서는 도전해볼 가치가 있습니다.

🚀 Self-hosted runner 구성하기

프로젝트 인스턴스를 runner로 생성해 actions에서 사용할 수 있습니다. 방법은 어렵지 않습니다! Repository setting 탭의 Actions - Runners 탭의 New self-hosted runner를 클릭해 추가합시다.

일련의 과정을 수행한다면 443 포트를 계속해서 듣고 있을 거예요. 이제 모든 준비는 끝났습니다 😄

🔍 요구사항 정의 및 분석

요구사항은 다음과 같습니다.

  • 앞선 빌드 테스트가 선행되어야 합니다.
  • gradle의 bootJar task를 통해 실행 파일을 생성합니다.
  • 현재 80 포트에 바인딩된 프로그램이 존재한다면, 이를 종료합니다.
  • 새로운 실행 파일을 띄웁니다.

위의 두 개는 이전 CI에서 진행했던 것과 거의 같습니다. 아래 두 개는 Shell script를 통해서 진행할 수 있겠네요!

📝 Workflow 파일 작성하기

name: CD using Github self-hosted runner

on:
# actions 탭에서 실행할 수 있도록 한다
workflow_dispatch:

push:
branches:
- main
- develop

env:
ARTIFACT_NAME: # 프로젝트 이름과 같이 구별할 수 있는 문자열
ARTIFACT_DIRECTORY: ./build/libs

# 해당 workflow는 두 개의 job으로 이루어져 있다.
# 하나는 빌드를 통한 jar 생성, 하나는 생성된 jar 배포이다.
# 전자는 GitHub에서 제공하는 runner를, 후자는 self-hosted runner를 사용한다.
jobs:
# 대부분 앞선 CI와 동일하나, gradle에서 테스트를 하지 않고, jar 파일을 만들어낸다
build:
name: Build Jar file and upload artifact
runs-on: ubuntu-latest

steps:
- name: Checkout to current repository
uses: actions/checkout@v4

- name: Setup JDK Corretto using cached gradle dependencies
uses: actions/setup-java@v4
with:
distribution: 'corretto'
java-version: 17
cache: 'gradle'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
with:
gradle-version: 8.8

- name: Build and test with gradle
run: ./gradlew clean bootJar

- name: Rename artifact file
run: |
mv ${{ env.ARTIFACT_DIRECTORY }}/*.jar \
${{ env.ARTIFACT_DIRECTORY }}/${{ env.ARTIFACT_NAME }}.jar

- name: Upload created artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.ARTIFACT_NAME }}
path: ${{ env.ARTIFACT_DIRECTORY }}/${{ env.ARTIFACT_NAME }}.jar

deploy:
name: Deploy via self-hosted runner
needs: build
runs-on: self-hosted

steps:
- name: Download uploaded artifact
uses: actions/download-artifact@v4
# 앞선 actions/upload-artifact에서 업로드한 실행 파일을 다운로드한다
with:
name: ${{ env.ARTIFACT_NAME }}
path: ${{ env.ARTIFACT_DIRECTORY }}

# 80 포트에 열려있는 프로세스를 확인하고, 존재한다면 환경변수를 설정한다
# well-known port (~1024)를 확인하거나 바인딩, 종료할 때에는 권한이 필요하다
# bash에서 실패하는 경우 전체 job이 실패하므로, || (or) true 연산을 진행한다
- name: Check if the port 80 is in use
run: |
echo "Checking ports on http..."
PID=$(sudo lsof -t -i :http || true)
if [ -n "$PID" ]; then
echo "Found process running on port http: $PID"
echo "server_running=true" >> "$GITHUB_ENV"
echo "PID=$PID" >> "$GITHUB_ENV"
else
echo "Process not found running on port http!"
echo "server_running=false" >> "$GITHUB_ENV"
fi

# 앞선 step에서 실행되고 있는 프로세스가 발견되는 경우, 이를 종료한다.
# 단, -15와 같은 graceful 종료를 진행한다.
# 종료된 것을 확인하기 위해 tail 명령어를 사용한다
# 프로그램이 종료되는 경우 tail도 종료된다
- name: Stop server if available (gracefully)
# 이 step이 실행될 조건을 설정한다
if: env.server_running == 'true'
run: |
echo "Gracefully shutting down process ${{ env.PID }}"
sudo kill -15 ${{ env.PID }}
tail --pid=${{ env.PID }} -f /dev/null

- name: Start server
run: |
sudo nohup java -jar \
${{ env.ARTIFACT_DIRECTORY }}/${{ env.ARTIFACT_NAME }}.jar \
--server.port=80 &

추후 443번 포트를 통한 Http 웹 어플리케이션이 배포된다면 어떻게 될까요 🤨? 아직은 일어나지 않았지만, 이 경우에 대한 대비가 필요하겠네요.

이로써 PR을 작성하면 자동 빌드/테스트가, 해당 PR이 머지되면 전체 빌드/테스트/배포가 되도록 자동화할 수 있었습니다! Actions를 조금 더 활용한다면 Slack 알림과 같은 기능도 사용할 수 있으니, 익숙해져보는 것이 좋겠습니다 😄 개발할 때 답답하거나 불편함을 느낀다면, 이 또한 개선할 수 있는 점이니깐요!