로그 파일 남기기

개발을 하면서 늘 로그도 편하게 확인할 수 있으면 좋겠다는 생각을 하였다.

하지만 어떻게 하는지 몰라서 그냥 IDE 터미널 로그만 보았었는데

최근 인프라 관련 편의성 설정들을 해보면서 로그도 어떻게 파일로 따로 저장할 수 있을거란 생각이 들었다.

이번 글에서는 스프링부트 애플리케이션의 로그를 저장하는 방법을 정리해두려 한다.


써보기

사실 로그는 거의 모든 애플리케이션에서 남기는거라 스프링에서 기본 로깅 라이브러리를 제공한다.

스프링부트에서 로깅을 할 때 쓰는 @Slf4j 는 다들 한번씩 써본 애노테이션일거라 생각한다.

우리가 그동안 쓰던 Slf4j는 인터페이스이고, 구현체로 Logback, Log4j 등이 있다.

이번 글에서 사용할 Logback 라이브러리는 스프링부트의 기본 로깅 라이브러리이므로 따로 의존성을 추가할 필요는 없다.

(Log4j는 따로 추가해야한다는것 같기는 하다.)

순서는 아래와 같다.

  1. logback-spring.xml 파일 작성
  2. 결과 확인

1. logback-spring.xml 파일 작성

logback 라이브러리로 로그 파일을 남기려면 아래 두 가지 방법 중 하나로 설정을 추가 해줘야한다.

  • yml 파일 설정
  • xml 파일 설정

xml 파일은 몬가 구닥다리 느낌이 나서 최대한 yml 파일로 해보고 싶었지만 세세한 설정은 xml 파일로 하는게 나은듯하다.

yml 파일로 설정하려면 yml 파일이 길어지는것 같고 길이도 비슷한것 같아서 xml 파일로 설정하였다.

logback에서 읽는 xml 설정 파일의 기본 값은 logback-spirng.xml 인데 따로 추가 설정을 해주면 파일명을 변경할 수 있다.

로그는 기본적으로 아래와 같은 형식으로 출력하고 저장한다.

[날짜] [쓰레드 이름] [로그 레벨] [로그 발생 지점] [로그 메시지]
ex) 2024-06-18 13:24:59 [http-nio-8080-exec-9] ERROR cohttp://m.example.dream.TestController - [error] hi2 error

logback-spring.xml

<configuration>
    <!-- 콘솔창(IDE 터미널 혹은 CLI 등)에 띄울 로그 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 모든 로그 저장용 파일 설정 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 로그 파일명 -->
        <file>logs/application.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 로그 파일을 일별로 롤오버 -->
            <fileNamePattern>logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!-- 로그를 30일간 저장하고 그 이상은 덮어씌움 -->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- error 로그 저장용 파일 설정 -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- error 로그 파일명 -->
        <file>logs/error.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 로그 파일을 일별로 롤오버 -->
            <fileNamePattern>logs/error.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!-- 로그를 30일간 저장하고 그 이상은 덮어씌움 -->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 루트 로거 - info 레벨 이상의 로그 저장 -->
    <root level="info">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FILE" />
        <appender-ref ref="ERROR_FILE" />
    </root>
</configuration>

 

<rollPolicy/> 에서는 롤링 정책에 따라 롤 오버 시의 로그 파일 이름을 지정한다.

롤 오버 시 logs/error.2024-06-18.log 이런 식으로 저장한다.

본인은 모든 로그를 저장하는 파일과 에러 로그를 저장하는 파일을 따로 두고 싶었다.

그래서 모든 로그는 application.log에, 에러 로그는 error.log에 저장하도록 설정하였다.


2. 결과 확인

애플리케이션을 실행하면 아래 사진처럼 logs 폴더를 만들고 그 아래에 로그 파일을 생성한다.

2-1. 모든 로그

원하는대로 INFO 레벨 이상의 로그를 남긴다.

2-2. 에러 로그

원하는대로 ERROR 로그만 남긴다.


EC2 에서 jar 파일로 배포할때는?

로컬에서 IDE로 실행할 때는 프로젝트 루트 디렉토리에 logs 폴더를 만들고 그 아래에 저장되었다.

이렇게 설정해두면 EC2에서 jar 파일로 빌드하면 로그 파일이 어디에 생성될까?

 

궁금해서 실험해보았더니 아래처럼 jar 파일이 있는 디렉토리에 logs 폴더가 생성되어 있다.

기본적으로 jar 파일로 실행할때는 jar 파일이 있는 디렉토리가 루트 디렉토리라고 생각해도 될것 같다.

 

Alert rules 설정

이전 글에서는 디스코드 웹 훅을 만들고 그라파나와 연동을 해보았다.

이번 글에서는 그라파나에서 Alert rules에 장애 조건을 설정하고,

장애 발생 시 연동한 디스코드로 경고 메시지를 보내는 설정을 해볼것이다.


구현하기

이번 글에서는 간단히 특정 상황 발생 시 디스코드로 경고 메시지를 발송하는 정도로 구현할 것이다.

기본적으로 그라파나는 프로메테우스 서버에서 받은 데이터를 이용하므로 프로메테우스 서버는 항상 켜져있어야 한다.

순서는 아래와 같다.

  1. 그라파나의 Alert rules 설정
  2. 결과 확인

1. 그라파나의 Alert rules 설정

다음은 그라파나에서 경고 메시지를 보낼 조건을 설정한다.

우리의 그라파나 서버는 프로메테우스에서 받은 데이터를 토대로 하므로 애플리케이션 상태에 따른 조건을 설정할 수 있다.

프로메테우스 설정을 원하는대로 설정하여 여러 애플리케이션 서버나 DB 서버의 알림 설정을 할 수 있다.

본인은 prometheus.yml 파일을 간단히 아래처럼 설정하였다.

# prometheus.yml
global:
  scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.

# Here it's Prometheus itself.
scrape_configs:
  - job_name: "spring-actuator"
    metrics_path: '/actuator/prometheus'
    scrape_interval: 15s
    static_configs:
      - targets: [ 'localhost:8080' ]

 

Alerting 탭에서 Alert rules 페이지로 이동하여 새로운 규칙을 추가한다.

1-1. 규칙 이름

규칙 이름을 설정한다.

1-2. 경고 메시지 발송 조건 설정

경고 메시지 발송 조건을 설정한다.

  • 최상단의 Query 측에서 프로메테우스(혹은 다른 DataSource)에서 받은 데이터로 쿼리를 작성할 수 있다.
  • 아래쪽 Expressions로 Query 결과를 이용한 표현식을 통해 조건을 설정할 수 있다.

필요하다면 Query, Expressions를 추가할 수도 있다.

본인은 Query 결과를 이용하여 Expressions에서 조건을 설정해보았다.

 

아래 사진의 promQL은 내려가있는 인스턴스가 있는지 확인하는 쿼리이다.

여기서 up은 인스턴스의 활성화 여부이며, 0은 비활성화 상태이고 1은 활성화된 상태임을 뜻한다.

count는 괄호 안의 조건에 맞는 결과의 갯수를 반환한다.

count(up == 0)에서 비활성화 된 인스턴스가 없으면 no data를 반환하므로 OR vector(0) 을 추가하여 0을 반환하도록 한다.

쿼리 값(A 값)이 0보다 크면 경고 메시지를 발송하도록 설정하였다.

1-3. 서비스 상태 확인 간격 및 메시지 발송 시간 설정

Evaluation behavior은 서비스의 상태 확인에 관련된 세부 설정이다.

여기서 서비스 상태를 얼마 간격으로 다시 확인하는지, 장애 발견 후 얼마 뒤에 메시지를 발송할 지 설정한다.

본인은 아래처럼 설정하였다.

ㄴFolder

디렉토리를 설정할 수 있다.

본인은 rules 라는 디렉토리를 만들었다.

 

ㄴEvaluation group

Evaluation group에서 해당 그룹은 얼마 간격으로 서비스 상태를 다시 확인하는지 설정할 수 있다.

본인은 테스트의 용이성을 위해 아래처럼 10초 간격으로 상태를 다시 확인하는 그룹을 만들고 설정하였다.

ㄴPending peorid

Pending peorid에서 장애 발생 후 얼마 뒤에 메시지를 보낼지 설정한다.

이 또한 테스트 용이성을 위해 10초 간격으로 설정하였다.

1-4. 장애 발생 시 메시지를 전송할 경로 설정

연동한 디스코드 웹 훅 경로로 메시지를 보내도록 Contact point를 지정한다.

1-5. 경고 메시지 내용 설정

아래처럼 경고 메시지 내용을 설정할 수 있다.

Add custom annotation 으로 원하는 내용을 추가할 수도 있다.

본인은 job 이름과 스프링부트 애플리케이션 상태를 확인할 수 있는 패널 경로를 넣어주었다.

 

여기까지 설정 했으면 우상단의 저장 버튼을 누르면 Alert rule이 추가되고, 아래처럼 추가된 Alert rule을 확인할 수 있다.


2. 결과 확인

알림 설정이 제대로 동작하는지 확인해보기 위해 애플리케이션을 종료해보았다.

아래처럼 디스코드로 설정한 경고 메시지가 잘 전달됨을 확인할 수 있다.

 

참고자료

 

[과제] prometheus와 grafana를 이용한 MySQL 모니터링 구축

prometheus와 grafana를 이용한 DB 모니터링 구축하기서버 1: DB 서버DB 설치프로메테우스 exporter 설치 (node exporter, db exporter)서버 2: 모니터링용 서버프로메테우스 설치그라파나 설치서버 1에는 2개 이상

velog.io

 

Grafana Alert 적용하기

Grafana를 프로젝트에 적용하고 모니터링을 완성한 후 장애가 발생했을 때 email로 받아보면 좋겠다는 생각을 하게 되었습니다. Grafana Alert 기능을 사용하면 장애 발생 시 email로 받아볼 수 있음을

velog.io

 

서비스 장애 알림

스프링부트 액추에이터, 프로메테우스, 그라파나를 이용하여 메트릭을 수집 및 시각화하였다.

이렇게 메트릭을 수집하면 비즈니스적인 인사이트를 얻을 수도 있지만, 특정 메트릭의 이상 수치 또한 파악할 수 있다.

만약, 이상 수치 또는 비정상적인 응답 값을 보일 때 경고 메시지를 보내도록 알림 설정을 하면 좀 더 빠르게 대처할 수 있을 것이다.

 

경고 메시지를 전송하도록 설정할 수 있는 방법은 Alert manager, 그라파나 등 여러가지가 있다.

이 중 그라파나가 제공하는 알림 기능을 이용해 디스코드로 경고 메시지를 보내도록 설정해보려 한다.

사실 프로메테우스의 Alert manager로 알림 설정을 해보려 했지만 실패했다.

그 대안으로 선택한게 그라파나이고, 이번 글에서는 디스코드 웹 훅을 만들고 그라파나와 연동을 해볼것이다.


사용 기술

  • Spring Boot 3.2.4 / gradle-kotlin
  • Java 17
  • Spring Boot Actuator, Prometheus, Grafana, Discord

구현하기

순서는 아래와 같다.

  1. 디스코드 웹 훅 생성
  2. 그라파나와 디스코드 연동
  3. 디스코드 연동 확인

1. 디스코드 웹 훅 생성

디스코드와 연동하려면 디스코드의 웹 훅을 만들어둬야 한다.

웹 훅을 만들려면 디스코드 서버 권한이 있어야 하기 때문에 본인은 연습용으로 디스코드 서버를 하나 만들었다.

1-1. 채널 설정창으로 이동

권한이 있는 상태에서 아래처럼 알람을 설정하고자 하는 채널의 톱니 모양 버튼을 누른다.

1-2. 웹 후크 만들기

채널 설정 창에서 연동 탭으로 이동하면 웹 후크 만들기 버튼이 보인다.

1-3. 웹 후크 URL 복사

만들기 버튼으로 들어가면 자동으로 해당 채널에 웹 훅을 생성해준다.

이제 여기서 복사한 웹 후크 URL를 그라파나와 연동한다.

 


2. 그라파나와 디스코드 연동

1에서 만든 디스코드 웹 훅을 그라파나와 연동할것이다.

2-1. Contact point 추가 페이지로 이동

그라파나의 기본 포트인 localhost:3000으로 접속하고 Alerting 탭에서 Contact point 페이지로 이동하면 아래와 같다.

여기서 Add contact point 버튼을 눌러서 Contact point 추가 페이지로 이동한다.

2-2. 새로운 Contact point 추가

Contact point 이름을 설정해주고, Integration에서 디스코드를 선택한다.

Webhook URL 에 1-3에서 복사한 웹 훅 URL을 넣어준다.

바로 저장을 해도 되지만 Test를 눌러서 원하는대로 연동이 되는지 확인할 수도 있다.


3. 디스코드 연동 확인

3-1. 테스트 연동 메시지 확인

연동이 되었다면 테스트를 눌렀을 때 아래처럼 메시지가 잘 온다

3-2. Contact point 등록 확인

저장 후 Contact points 페이지로 이동하면 우리가 추가한 디스코드 웹 훅이 등록되었음을 확인할 수 있다.

 

문제 발생

Spring Rest Docs + Swagger를 써보려고 하였다.

어느정도 설정이 다 끝나서 기분 좋게 마무리 하려고 배포 환경에서 실행하는것 처럼 jar 파일로 실행해보았다.

하지만 jar 파일로 빌드 후 java -jar 명령어로 실행하니 아래처럼 Swagger UI에서 openapi3.yaml을 읽지 못하였다.

분명 IDE에서는 몇번을 해봐도 잘 되는데 jar 파일로 실행해보면 openapi3.yaml 파일을 읽지 못하는 현상이 발생하였다.


문제 해결

jar 파일로 패키징 시 static 파일을 jar 파일에 추가해주지 않아서 발생한 문제였다.

jar 파일로 패키징하면 static 경로의 파일들은 jar 파일 내의 /BOOT-INF/classes/static 경로에서 읽는다.

IDE로 실행할 때는 static 파일을 읽을 수 있었지만, jar 파일로 패키징 할 때 openapi3.yaml 파일을 넣어준 적이 없다.

 

그래서 아래처럼 jar 파일을 만드는 단계인 bootJar 단계에서 openapi3.yaml 파일을 함께 패키징 하도록 설정하였다.

또한 openapi3.yaml 파일이 생성되는 단계인 copyOasToSwagger 다음에 실행되게 하였다.

// build.gradle.kts
tasks {
    bootJar {
        dependsOn("copyOasToSwagger")
        from("build/api-spec") {
            into ("BOOT-INF/classes/static/swagger-ui")
        }
    }
}

 

또한 swagger-initializer.js 의 url 경로를 아래처럼 수정해주었다.

... 생략
window.ui = SwaggerUIBundle({
  url: "/swagger-ui/openapi3.yaml", 
  dom_id: '#swagger-ui',
... 생략

 

task 실행 목록을 보면 원하는 순서대로 동작함을 알 수 있다.

 

Swagger UI 도 잘 동작한다.

구현하기

구현 순서는 아래와 같다.

  1. restdocs-api-spec을 이용한 OAS 파일을 생성하는 빌드 환경 구축
  2. Swagger-UI standalone, Static Routing 세팅
  3. 생성된 OAS 파일을 Swagger 디렉터리로 복사(copyOasToSwagger)
  4. SampleController 및 MockMvc REST Docs Test 코드 작성
  5. 결과 확인

사용 기술

  • Spring Boot 3.3.0 / gradle-kotlin
  • Java 17
  • restdocs-api-spec

1. restdocs-api-spec을 이용한 OAS 파일을 생성하는 빌드 환경 구축

1-1. 의존성 추가

아래처럼 의존성을 추가해준다.

// build.gradle.kts
plugins {
    ... 생략
    id("com.epages.restdocs-api-spec") version "0.15.3"
}

repositories {
    mavenCentral()
}

dependencies {
    ... 생략
    // Rest Docs
    testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc")
    testImplementation("org.springframework.restdocs:spring-restdocs-asciidoctor")
    testImplementation("com.epages:restdocs-api-spec-mockmvc:0.17.1")
    ... 생략
}

openapi3 {
    this.setServer("https://localhost:8080") // list로 넣을 수 있어 각종 환경의 URL을 넣을 수 있음!
    title = "My API"
    description = "My API description"
    version = "0.1.0"
    format = "yaml" // or json
}

1-2. task 확인

의존성을 추가하고 gradle의 tasks를 보면 아래 사진처럼 openapi3 과 같은 task가 추가돼있다.

 

여기서 openapi3 task를 실행해보면 아래 사진처럼 build/api-spec 경로에 openapi3.yaml 파일이 생성된다.

 

openapi3.yaml 파일을 보면 아래와 같다.


2. Swagger-UI standalone, 리소스 핸들러 설정

2-1. Swagger-ui 정적 파일 설치

위 사진은 Swagger-ui 정적 파일 설치 사이트 하단의 설명이다.

위 페이지에서 latest release 를 다운 받는 링크로 이동하여 압축 파일을 받아준다.

2-2. 스프링부트의 정적 파일 경로에 추가

압축을 해제하면 아래 사진처럼 나오는데, 여기서 /dist 의 내용물만 필요하다.

 

/dist 내용물을 아래 사진처럼 스프링부트의 /resources/static/swagger-ui 로 옮겨준다.

2-3. 파일명 변경 및 불필요한 파일 제거

index.html 은 swagger-ui.html 로 파일명을 변경하였다.

또한 불필요한 파일들은 삭제해주었다.

  • oauth2-redirect.html
  • swagger-ui.js
  • swagger-ui-es-bundle-core.js
  • swagger-ui-es-bundle.js

2-4. swagger-initializer.js 파일의 url 수정

swagger-initializer.js의 코드를 보면 아래 사진과 같다.

1-2 에서는 openapi3 task 를 돌리면 OAS 파일이 생성됐지만, 최종적으로는

Spring Rest Docs와 연동하여 빌드 시 테스트 코드를 돌릴 때 OAS 파일을 만들게 될것이다.

swagger-initializer.js에서 OAS 파일을 읽어 API 시각화를 하기 때문에 이를 우리가 만들 OAS 파일을 참조하도록 수정한다.

여기서 url 부분을 수정하여 우리가 만들 OAS 파일을 참조하도록 한다.

Swagger-initializer.js

2-5. 리소스 핸들러 설정

브라우저에서 스프링부트 애플리케이션 서버의 /static/swagger-ui 경로에 있는 정적 파일에 접근하기 위해서는

리소스 핸들러 설정을 바꿔줘야 한다.

(사실 이 부분은 2-4 에서 url 을 /static/swagger-ui/openapi3.yaml 으로 했을때는 필요했는데

본인은 url을 /swagger-ui/openapi3.yaml로 설정해서 굳이 필요 없을것 같기는 하다.)

@Configuration
public class AppConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
    }
}

3. 생성된 OAS 파일을 Swagger 디렉터리로 복사(copyOasToSwagger)

1-2에서 openapi3 task를 실행하면 build/api-spec 경로에 openapi3.yaml 파일이 생성되는걸 확인했다.

이걸 2에서 생성한 /static/swagger-ui 경로에 넣고 swagger-ui.html로 읽을 것이다.

build.gradle.kts에 아래 코드를 추가하여 이를 위한 task를 추가한다.

// build.gradle.kts

tasks.withType<Test> {
    useJUnitPlatform()
    // 기존에 있는 test 쪽에 이 코드를 추가
    finalizedBy("copyOasToSwagger")  // test 후 copyOasToSwagger task 진행
}

val buildDir = layout.buildDirectory.get().asFile
tasks.register<Copy>("copyOasToSwagger") {

    dependsOn("openapi3") // openapi3 Task가 먼저 실행되도록 설정
    delete("src/main/resources/static/swagger-ui/openapi3.yaml") // 기존 OAS 파일 삭제
    from("$buildDir/api-spec/openapi3.yaml") // 복제할 OAS 파일 지정
    into("src/main/resources/static/swagger-ui/") // 타겟 디렉터리로 파일 복제
}

tasks {
    bootJar {
        // openapi3.yaml 파일을 /static/swagger-ui 경로로 복사에 성공한 후 bootJar 진행
        dependsOn("copyOasToSwagger")
        from("build/api-spec") {
            into ("BOOT-INF/classes/static/swagger-ui")
        }
    }
}

 

위 코드를 추가하고 빌드나 테스트를 돌려서 로그를 살펴보면 아래처럼 task 를 진행함을 확인할 수 있다.

test -> openapi3 -> copyOasToSwagger -> bootJar -> build 의 순서로 task 를 진행한다.


4. Sample Controller 코드 및 Rest Docs 테스트 작성

간단하게 테스트용으로 컨트롤러를 작성하였다.

TestController

@RestController
@RequestMapping("/hi")
public class TestController {

    @GetMapping("/hi1")
    public String test() {
        return "hi1";
    }

    @GetMapping("/hi2")
    public String test2() {
        return "hi2";
    }
}

TestControllerTest

Spring Rest Docs는 MockMvc 와 결합해서 동작한다.

컨트롤러 테스트 코드를 작성할 때 아래 두 가지를 목적에 맞게 사용해야 한다.

  • Rest Docs 사용- MockMvcRestDocumentation.document 
  •   Swagger 사용 - MockMvcRestDocumentationWrapper.document

본인은 Rest Docs는 사용하지 않고 Swagger 용도로만 사용하였다.

어떤 메서드를 사용하는지 참고하기 쉽도록 패키지까지 올려두었다.

package com.example.dream;

import org.junit.jupiter.api.DisplayName;
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.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.test.web.servlet.MockMvc;

import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@ExtendWith(RestDocumentationExtension.class)
class TestControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @DisplayName("")
    @Test
    void test() throws Exception {
        mockMvc.perform(get("/hi/hi1")
                .accept(MediaType.APPLICATION_JSON)
            )
            .andDo(print())
            .andExpect(status().isOk())
            .andDo(document("나는짱1"));
    }

    @DisplayName("")
    @Test
    void test2() throws Exception {
        mockMvc.perform(get("/hi/hi2")
                .accept(MediaType.APPLICATION_JSON)
            )
            .andDo(print())
            .andExpect(status().isOk())
            .andDo(document("나는짱2"));
    }
}

 


5. 결과 확인

아래 사진처럼 Production 코드를 침투하지 않고도 Swagger 의 API Test가 가능하다.

또한 Spring Rest Docs 의 테스트 코드를 통한 유지 보수성 또한 확보 되었다.

 

위 결과는 빌드하고 java -jar 명령어로 실행해본 결과이다.

배포 환경에서는 서버 URL 설정을 변경해줘야할 수 있다.

그때는 build.gradle.kts 의 openapi3 쪽의 서버 URL을 변경해주자.

혹은 주석 달려있는것 처럼 List 형태로 넣어서 여러개의 URL을 사용해도 될듯 하다.

yml 파일로 동적으로 이용할 수 있는 방법도 있을듯 하니 추후 사용할 때 가볍게 정리할것 같다.

 

참고자료

 

OpenAPI Specification을 이용한 더욱 효과적인 API 문서화 | 카카오페이 기술 블로그

사실상의 표준으로 발돋움 중인 OpenAPI Specification을 이용한 API 문서화 방법(Swagger와 Spring REST Docs의 장점을 합치는 방법)을 공유드립니다.

tech.kakaopay.com

 

 

내가 만든 API를 널리 알리기 - Spring REST Docs 가이드편

'추석맞이 선물하기 재개발'에 차출되어 API 문서화를 위해 도입한 Spring REST Docs 를 소개합니다.

helloworld.kurly.com

 

 

[리팩토링] Swagger UI + Spring RestDocs 적용기

Swagger UI 와 Spring RestDocs 의 장 단점의 비교Swagger UI 의 장점직관적인 UI스웨거는 API 에 대한 요청과 응답 등을 시각적으로 표현하여 사용자가 쉽게 이해할 수 있습니다실시간 테스트API 엔드포인트

oth3410.tistory.com

 

API 문서화

개발자에게는 API를 만드는것 뿐 아니라 이에 대한 문서화 또한 필요하다.

수동으로 Notion이나 스프레드 시트에 정리할 수도 있겠지만 이는 일일이 최신화를 해줘야해서 너무 불편하다.

이런 불편함을 해소하기 위해 API 문서화를 자동으로 해주는 툴이 나왔고, 유명한건 아래 두가지 정도가 있다.

  • Spring Rest Docs
  • Swagger

한번 이 두가지를 비교해보자.


Spring Rest Docs  vs  Swagger

Spring Rest Docs

Spring Rest Docs는 컨트롤러 테스트를 통해 Integration 테스트를 진행하면서 최신화하는 형식이다.

테스트 코드를 통한 방식이기 때문에 Production 코드에 영향을 미치지 않아서 깔끔하게 분리가 가능하다.

또한 테스트를 강제하기 때문에 유지보수에 도움을 준다.

하지만 Swagger처럼 API Test 기능을 지원하지 않고 가시성이 조금 부족하다는 단점이 있다.

아래는 Rest Docs의 예시 이미지이다.

Swagger

Swagger는 컨트롤러나 DTO 등에 애노테이션을 다는걸로 API 문서를 만들어준다.

이전 글에서 Swagger를 써서 API 문서화를 해보았다.

Swagger는 API Test 기능을 지원하고 문서의 가시성이 좋다.

아래는 Swagger의 예시 이미지이다.

 

하지만 Swagger 문서화를 위한 코드가 Production 코드에 너무 깊게 침투한다.

그래서 아래처럼 컨트롤러 코드가 너무 지저분해진다.


Spring Rest Docs + Swagger 병용

Swagger를 쓰면서 늘 불편했던 컨트롤러 코드가 지저분해지는 단점을 커버할 수 있단 말에 굉장히 설렜다.

이 방법에 대한 간단한 설명은 아래와 같다.

OpenApi Specification(OAS) 기반 API 문서화

Swagger 팀이 SmartBear Software에 합류하면서 Swagger Spec.이 OpenApi Spec.으로 명칭이 바뀌었고,

오늘날에는 RESTful API 스펙에 대한 사실상의 표준으로서 활용되고 있다고 한다.

Swagger-ui는 이 OAS를 해석하여 API 스펙을 시각화해준다.

또한 Postman, Paw 같은 API Client들도 OAS를 지원하고 있어 OAS를 사용하면 활용도가 높을것으로 예상된다.

OAS를 생성하는 restdocs-api-spec 오픈소스 

그렇다면 Spring REST Docs와 Swagger의 장점을 어떻게 합칠 수 있을까? 

독일 기업 epages에서 Spring REST Docs를 연동하여 OAS 파일을 만들어주는 오픈소스(restdocs-api-spec)를 제공하고 있다.

위에서 Swagger-ui는 OAS를 해석해서 API 스펙을 시각화 해준다고 했다.

따라서 이 오픈소스를 이용해서 OAS 파일을 생성하고 Swagger-ui로 띄우면 된다!

 

 

최근에 한 팀 프로젝트에서는 Swagger를 사용하였는데, Production 코드에 침투하는게 마음에 들지는 않았지만 Swagger의 장점 때문에 유지했었다. (사실 팀원이 테스트 코드를 잘 작성하지 않아서 RestDocs 를 쓰기는 힘들기도 했다.)

하지만 이처럼 Spring Rest Docs와 Swagger를 혼용하여 장점만 취하는 방법을 접하였고 다음 글에서 시도해보려 한다.

리눅스 공부의 필요성

프로그래머가 구현에만 집중할 수 있도록, 나머지 환경들을 계속 추상화 시켜서 추상화의 아래 쪽으로 숨겨버리다 보니
정작 프로그래머가 컴퓨터 자체를 모르는 일이 자주 발생한다.
- 『모던 리눅스 교과서』(책만, 마이클 하우센블라스 지음, 송지연 옮김, 2023)

 

친구에게 추천받은 책의 서문에 적힌 문장이다.

최근 토이 프로젝트 배포를 위해 인프라 설정도 해보고, 도커나 MySQL도 공부하려니까 CLI를 만질 일이 많아졌다.

그래도 요즘은 챗 GPT 덕분에 웬만해선 리눅스의 필요성을 잘 모르고 살았다.

GUI에 절여져서 간단한 ls 같은 커맨드만 알고 있고, 막힐때는 챗 GPT의 힘을 빌려서 해결했었다.

하지만 최근 친구와 새로 토이 프로젝트를 구상하며 이야기를 나눠보면서 생각이 달라졌다.

환경변수 같은 간단한 설정도 Node.js 나 SpringBoot 같은 프레임워크의 도움을 받아야만 할 수 있을거라고 생각했지만,

간단한 리눅스 커맨드로도 해결하는걸 보고 리눅스라는 기본기를 깔고가면 사고의 폭이 넓어질 수 있을거란 생각이 들었다.

 

개발자라면 어쩔 수 없이 리눅스를 공부하긴 해야하는것 같다.

회사에 들어가면 인프라를 직접 만질 일이 얼마나 있을까 싶긴 하지만,

그래도 너무 추상화된 고수준 동작만 알고 있는건 바람직하지는 않다는 생각이 든다.

토이 프로젝트를 고도화 하면서 이것저것 써보려면 어떻게든 필요하기도 하고.

엄청 깊게 공부하지는 않더라도 적당히 책 1~2권 정도는 읽고 필요한 만큼은 쓸 수 있게 되는게 목표이다.

 

그라파나로 애플리케이션 상태 확인

이전 글에서 로컬에 프로메테우스 및 그라파나를 설치해보고 적용까지 해보았다.

공유 대시보드 중에는 스프링부트 애플리케이션 상태를 확인할 수 있는 대시보드가 있다.

임의로 몇 가지 장애 상황을 만들어보고 이 대시보드로 애플리케이션 상태를 확인해보았다.

장애 상황은 아래와 같이 설정했다.

  1. JVM 메모리 고갈
  2. 커넥션 풀 고갈
  3. error 로그 급증

사용 기술

  • Spring Boot 3.2.4 / gradle-kotlin
  • Java 17
  • Spring Boot Actuator, Prometheus, Grafana

1. memory 고갈

의도적으로 List에 문자열을 채워 애플리케이션의 힙 메모리를 가득 채워보았다.

예시 코드는 아래와 같다.

해당 엔드포인트를 여러번 호출하면서 그래프 추이를 살펴보았다.

TrafficController

private List<String> list = new ArrayList<>();

@GetMapping("/jvm")
public String jvm() {
    log.info("[jvm]");
    for (int i = 0; i < 1000000; i++) {
        list.add("hi" + i);
    }
    return "jvm success";
}

 

로컬에서 한거라 메모리를 다 채우기 힘들어서 도중에 멈췄지만 아래와 같이 메모리 사용량을 그래프로 확인할 수 있다.


2. Connection pool 고갈

커넥션 풀 고갈을 위해 의도적으로 커넥션을 열고 close() 해주지 않았다.

예시 코드는 아래와 같다.

해당 엔드포인트를 여러번 호출하면서 그래프 추이를 살펴보았다.

TrafficController

@Autowired
DataSource dataSource;

@GetMapping("/jdbc")
public String jdbc() throws SQLException {
    log.info("jdbc");
    Connection conn = dataSource.getConnection();
    // conn.close(); // 커넥션을 닫지 않아본다.
    return "ok";
}

2-1. 정상 상태

Hikari CP 커넥션 풀 기본 설정인 10개 중 8개를 사용해보니 아래와 같은 그래프를 확인할 수 있었다.

2-2. 커넥션 풀 고갈

커넥션 풀을 10개를 다 쓰고 추가로 커넥션 풀이 필요한 요청을 하면 그 요청은 이전 커넥션 풀이 반환될 때 까지 대기상태가 된다.

일정 시간 대기하다가 여전히 사용 가능한 커넥션 풀이 없다면 그 요청은 Timeout이 된다.

요청이 Timeout이 되어 반려되면 아래와 같이 Connection Timeout Count가 올라간다.


3. error 로그 급증

에러 로그를 급증시켜보았다.

예시 코드는 아래와 같다.

해당 엔드포인트를 여러번 호출하면서 그래프 추이를 살펴보았다.

TrafficController

@GetMapping("/error-log")
public String errorLog() {
    log.error("error log");
    return "error";
}

 

아래처럼 에러로그가 증가하는 그래프를 확인할 수 있었다.

 

+ Recent posts