- 백엔드 서버를 개발해서 실행시키는 시점에 개발은 운영의 범위로 넘어가게 됩니다.
운영에서 중요한 것은, 우리가 밥을 먹을 때나 잠을 잘 때나, 잠시 휴식을 취할때 상관 없이 에러가 발생할 가능성이 있는 서비스가 정상 동작하도록 감시하고, 만약 문제가 발생하면 신속하게 대응하는 것입니다.
단순히 서비스를 개발해서 네트워크상 오픈해두는 것으로 그치는 것이 아니라 주기적으로 상태를 확인해줘야만 합니다.
이때, 런타임 에러가 발생하거나 서버 컴퓨터에 장애가 발생한다면 바로 관련자에게 에러 발생 로그를 발송하여 에러 상황을 알린다던지, 언제 어디서든 서버 상태를 파악하고, 이전 로그를 확인 가능한 기능이 있다면 운영에 큰 도움이 되겠죠?
이러한 기능을 제공해주는 것이 바로 서버 모니터링 시스템입니다.
- 이번 글에서는 서비스에 대한 모니터링 시스템을 Grafana, Prometheus, Loki, Promtail 기술 스택을 사용하여 Docker 을 사용하여 구축하는 방식을 정리하겠습니다.
각 기술에 대해 간략히 설명부터 드리겠습니다.
Prometheus 는 서버에서 주기적으로 시스템 정보를 가져오는 역할을 합니다.
서비스가 실행되는 디바이스의 메모리 사용량, CPU 사용량, 서비스 On/Off 여부, Request 횟수 등의 모니터링 타겟의 동작 환경에 대한 정보를 수집합니다.
Loki 는 서버에서 발생하는 로그 정보를 수집하는 역할이고,
Promtail 은 Log 파일에서 로그 정보를 수집해 Loki 에 전달하는 역할입니다.
Grafana 는 웹 UI 로, Prometheus 와 Loki 가 수집한 모니터링 데이터를 화면에 표시해주는 역할을 합니다
- 아래 그림은 모니터링 시스템 아키텍쳐 구성도입니다.
설명을 드리자면,
Log 파일들이 존재한다면 이를 Promtail 로 읽어들인 후, Loki 로 보내고, Grafana 가 이를 읽어들여 사용자에게 UI 로 보여주는 로그 관리 시스템과,
각 서버별 시스템 정보를 Prometheus 로 수집하고, 이 역시 Grafana 가 읽어들여 사용자에게 UI 로 보여주는 서버 환경 모니터링 시스템을 나타낸 것입니다.
모두 Grafana 라는 웹 UI 서비스로 모이는 형태라 사용이 편합니다.
참고로, Loki 의 로그 수집은, Promtail 을 제외하고, 서버에서 직접 로그 파일을 Loki 로 전달하는 방식도 있는데, 여기서는 로그 파일을 읽어들여 전달하는 역할을 하는 Promtail 을 추가하였습니다.
- Docker 파일 구조
도커 컨테이너 생성을 위한 compose 파일과, 그에 관련된 파일들은 위와 같은 형식으로 저장되어 있습니다.
- docker-compose.yml
services:
prometheus:
container_name: prometheus
image: prom/prometheus:latest
restart: always
volumes:
- ./conf/prometheus.yml:/etc/prometheus/prometheus.yml
- C:/Users/raill/Downloads/tmp/prometheus:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
ports:
- "9090:9090"
grafana:
container_name: grafana
image: grafana/grafana:latest
restart: always
ports:
- "3000:3000"
volumes:
- C:/Users/raill/Downloads/tmp/grafana:/var/lib/grafana
depends_on:
- prometheus
loki:
container_name: loki
image: grafana/loki:latest
restart: always
ports:
- "3100:3100"
command: -config.file=/etc/loki/loki-config.yaml
volumes:
- ./conf/loki-config.yaml:/etc/loki/loki-config.yaml
- C:/Users/raill/Downloads/tmp/loki/data:/loki
promtail:
container_name: promtail
image: grafana/promtail:latest
restart: always
volumes:
- C:/dev/springboot/SpringBoot_MvcTemplate/by_product_files/logs:/logs
- ./conf/promtail-config.yml:/etc/promtail/config.yml
command: -config.file=/etc/promtail/config.yml
depends_on:
- loki
도커 컴포즈 파일은 위와 같이 구성되어 있습니다.
각 모니터링 서비스 컨테이너를 생성하도록 설정하였고, 그에 관련한 설정 파일을 volume 으로 이어준 것입니다.
- loki-config.yaml
Loki 설정 파일입니다.
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
ingester:
wal:
enabled: true
dir: /loki/wal
lifecycler:
address: 127.0.0.1
ring:
kvstore:
store: inmemory
replication_factor: 1
final_sleep: 0s
chunk_idle_period: 1h # Any chunk not receiving new logs in this time will be flushed
max_chunk_age: 1h # All chunks will be flushed when they hit this age, default is 1h
chunk_target_size: 1048576 # Loki will attempt to build chunks up to 1.5MB, flushing first if chunk_idle_period or max_chunk_age is reached first
chunk_retain_period: 30s # Must be greater than index read cache TTL if using an index cache (Default index read cache TTL is 5m)
max_transfer_retries: 0 # Chunk transfers disabled
schema_config:
configs:
- from: 2020-10-24
store: boltdb-shipper
object_store: filesystem
schema: v11
index:
prefix: index_
period: 24h
storage_config:
boltdb_shipper:
active_index_directory: /loki/boltdb-shipper-active
cache_location: /loki/boltdb-shipper-cache
cache_ttl: 24h # Can be increased for faster performance over longer query periods, uses more disk space
shared_store: filesystem
filesystem:
directory: /loki/chunks
compactor:
working_directory: /loki/boltdb-shipper-compactor
shared_store: filesystem
chunk_store_config:
max_look_back_period: 336h # how far data can be queried
table_manager:
retention_deletes_enabled: true
retention_period: 336h # how long data remaind
ruler:
storage:
type: local
local:
directory: /loki/rules
rule_path: /loki/rules-temp
alertmanager_url: http://localhost:9093
ring:
kvstore:
store: inmemory
enable_api: true
별로 설명할만한 거리가 없습니다.
커스텀을 하시려면 각 설정을 검색하여 수정하시면 됩니다.
- prometheus.yml
Prometheus 설정 파일입니다.
global:
# 타겟으로부터의 수집 주기
scrape_interval: 15s
# rule evaluation 주기 (prometheus 는 rule 을 통해 time series 를 만들고 alert 를 생성한다.)
evaluation_interval: 15s
# target별로 범주들을 묶고, 범주별로 수집 주기나 룰들을 유동적으로 설정
scrape_configs:
- job_name: 'spring_boot_service_1'
metrics_path: '/actuator/prometheus'
static_configs:
# actuator 요청을 보낼 위치 (ex : ['192.168.0.3:9090']), metrics_path 와 결합합니다.
- targets: ['host.docker.internal:8080']
labels:
# 인스턴스 이름 지정
instance: 'service_1'
# - job_name: 'spring_boot_service_2'
# metrics_path: '/actuator/prometheus'
# static_configs:
# - targets: ['host.docker.internal:8080']
# labels:
# instance: 'service_2'
서비스별 환경 정보를 가져올 요청 api 를 설정한다고 생각하면 됩니다.
- job_name 을 기준으로 해서 각 환경 정보를 가져오는데, 이를 추가하여 여러 위치에서 데이터를 수집하게 할 수도 있습니다.
저는 springboot 의 actuator 정보를 가져오도록 하였습니다.
springboot 의 actuator 는 시스템 환경정보를 반환하는 api 모음입니다.
해당 서버의 /actuator 로 시작되는 주소로 요청을 보내면 시스템 환경 정보를 받을 수 있죠.
springboot 에서 이에 대한 설정을 하려면,
build.gradle 안에,
implementation("org.springframework.boot:spring-boot-starter-actuator:3.3.0")
runtimeOnly("io.micrometer:micrometer-registry-prometheus:1.13.0")
위와 같이 actuator 와 prometheus 의 라이브러리를 넣어주고,
application.yml 파일 안에,
# actuator 설정
management:
endpoints:
web:
exposure:
include: "*" # open 할 actuator 경로 설정
endpoint:
health:
show-details: always # actuator/health 에 디테일 정보 반환
위와 같이 actuator 공개 설정을 하면 됩니다.
참고로 actuator 정보는 매우 민감하고 중요한 정보이므로 관련자가 아니라면 공개하면 안되기에, 저의 경우는 springboot filter 를 사용하여 actuator 에 접근 가능한 화이트리스트에 등록된 ip 가 아니라면 접근할 수 없도록 처리하였으므로 참고하세요.
어쨌건 위와 같이 설정한다면 prometheus 설정에 따라 prometheus 는 localhost 의 8080 포트의 actuator/prometheus 라는 주소로 일정주기마다 요청을 보내 그 정보를 가져와 저장할 것입니다.
- promtail-config.yml
로그 파일에서 로그 정보를 추출하여 loki 에 전달하는 promtail 의 설정 파일입니다.
server:
# Promtail 의 HTTP 서버가 요청을 수신할 포트를 지정합니다.
# Promtail 의 상태 페이지, 메트릭스, 로그 수집 상태 등을 확인하는 데 사용됩니다.
http_listen_port: 9080
# gRPC 서버가 수신할 포트를 지정합니다. 이 경우 0으로 설정되었으므로 gRPC 서버가 비활성화됩니다.
grpc_listen_port: 0
positions:
# 동기화 작업을 이루기 위해 promtail이 읽은 마지막 로그 정보를 저장하는 곳
filename: /tmp/positions.yaml
clients:
# push할 Loki의 주소
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
# 해당 로그 수집 작업의 이름을 정의합니다.
- job_name: spring-logs
static_configs:
# 수집할 로그 파일이 있는 시스템의 대상 호스트를 지정합니다.
- targets:
- localhost
labels:
# 수집되는 로그에 추가될 메타데이터(레이블)를 정의합니다.
# 예시 : job=varlogs
job: varlogs
# 수집할 로그 파일의 경로를 지정합니다.
__path__: /logs/*.log
pipeline_stages:
# 여러 줄에 걸친 로그 항목을 하나로 묶는 역할을 합니다.
- multiline:
firstline: '^\[ls\] \[' # 로그의 시작을 나타내는 정규식
separator: '\] \[le\]' # 멀티라인 로그의 구분자
promtail 에서 로그 정보를 수집하면, 위의 clients.url 에 보이는 것처럼 http://loki:3100/loki/api/v1/push 위치로 정보를 보내도록 설정하였고, scrape_configs 설정 부분에서는 로그 정보를 추출할 로그 파일의 경로를 설정하였습니다.
pipeline_stages 설정은 이렇게 생각하시면 됩니다.
로그파일에서
[ls] [2024-04-02 13:12:11] [info] [testtesttest] [le]
[ls] [2024-04-02 13:12:12] [info] [testtest
ekjleee
test] [le]
위와 같이 로그 되어있다고 합시다.
promtail 은 기본적으로 로그별 구분을 줄 단위로 하기에, 이 파일에 대해 수집을 한다면
log1 : [ls] [2024-04-02 13:12:11] [info] [testtesttest] [le]
log2 : [ls] [2024-04-02 13:12:12] [info] [testtest
log3 : ekjleee
log4 : test] [le]
위와 같이 수집을 할 것입니다.
하지만 위 로그 형식은 규칙이 있죠.
[ls] 로 시작되고, [le] 로 끝나는 것이 하나의 로그입니다.
즉, pipeline_stages 설정에서는 로그 하나가 어떻게 구성되어있는지에 대해 promtail 에게 설명해주는 것이고, 여기선 위 예시와 같이 [ls] 로 시작되고, [le] 로 끝나는 것이 하나의 로그라고 설정했습니다.
springboot 의 logback 설정은,
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--로그 파일 저장 위치-->
<property name="LOGS_PATH" value="./by_product_files/logs"/>
<!--local 프로필 환경-->
<springProfile name="local">
<!-- File 에 로그 출력 세팅 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 출력패턴 설정-->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<charset>UTF-8</charset>
<pattern>[ls] [%23d{yyyy_MM_dd_'T'_HH_mm_ss_SSS_z}] [%-5level] [%msg] [le]%n
</pattern>
</encoder>
<!-- Rolling 정책 : 로그 보관 정책 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 로그 백업 파일 경로 설정 -->
<!-- .gz,.zip 등을 넣으면 자동 일자별 로그파일 압축 -->
<fileNamePattern>${LOGS_PATH}/local_%d{yyyy_MM_dd}(%i).log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- 파일당 최고 용량 KB, MB, GB -->
<!-- 아래 용량을 넘으면 파일 분할-->
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- 일자별 로그파일 최대 보관주기(~일), 해당 설정일 이상된 파일은 자동으로 제거-->
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<!-- CONSOLE 에 로그 출력 세팅 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>[ls] [%23d{yyyy_MM_dd_'T'_HH_mm_ss_SSS_z}] [%highlight(%-5level)] [%msg] [le]%n
</pattern>
</layout>
</appender>
<!-- <appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">-->
<!-- <!– 로그 스태시 주소 –>-->
<!-- <destination>localhost:5000</destination>-->
<!-- <encoder class="net.logstash.logback.encoder.LogstashEncoder">-->
<!-- </encoder>-->
<!-- </appender>-->
<!-- appender 별 세팅 -->
<!-- 로그 전역 세팅 -->
<!-- 로그 레벨 -->
<!--
1) ERROR : 오류 메시지 표시
2) WARN : 경고성 메시지 표시
3) INFO : 정보성 메시지 표시
4) DEBUG : 디버깅하기 위한 메시지 표시
5) TRACE : Debug보다 훨씬 상세한 메시지 표시
예를들어 info 로 설정시, INFO 보다 위에 있는 DEBUG 와 TRACE 는 표시하지 않는다.
-->
<root level="INFO">
<appender-ref ref="FILE"/>
<appender-ref ref="CONSOLE"/>
<!-- <appender-ref ref="LOGSTASH"/>-->
</root>
</springProfile>
<!--dev 프로필 환경-->
<springProfile name="dev">
<!-- File 에 로그 출력 세팅 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 출력패턴 설정-->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<charset>UTF-8</charset>
<pattern>[ls] [%23d{yyyy_MM_dd_'T'_HH_mm_ss_SSS_z}] [%-5level] [%msg] [le]%n
</pattern>
</encoder>
<!-- Rolling 정책 : 로그 보관 정책 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 로그 백업 파일 경로 설정 -->
<!-- .gz,.zip 등을 넣으면 자동 일자별 로그파일 압축 -->
<fileNamePattern>${LOGS_PATH}/dev_%d{yyyy_MM_dd}(%i).log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- 파일당 최고 용량 KB, MB, GB -->
<!-- 아래 용량을 넘으면 파일 분할-->
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- 일자별 로그파일 최대 보관주기(~일), 해당 설정일 이상된 파일은 자동으로 제거-->
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<!-- <appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">-->
<!-- <!– 로그 스태시 주소 –>-->
<!-- <destination>localhost:5000</destination>-->
<!-- <encoder class="net.logstash.logback.encoder.LogstashEncoder">-->
<!-- </encoder>-->
<!-- </appender>-->
<!-- appender 별 세팅 -->
<!-- 로그 전역 세팅 -->
<!-- 로그 레벨 -->
<!--
1) ERROR : 오류 메시지 표시
2) WARN : 경고성 메시지 표시
3) INFO : 정보성 메시지 표시
4) DEBUG : 디버깅하기 위한 메시지 표시
5) TRACE : Debug보다 훨씬 상세한 메시지 표시
예를들어 info 로 설정시, INFO 보다 위에 있는 DEBUG 와 TRACE 는 표시하지 않는다.
-->
<root level="INFO">
<appender-ref ref="FILE"/>
<!-- <appender-ref ref="LOGSTASH"/>-->
</root>
</springProfile>
<!--prod 프로필 환경-->
<springProfile name="prod">
<!-- File 에 로그 출력 세팅 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 출력패턴 설정-->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<charset>UTF-8</charset>
<pattern>[ls] [%23d{yyyy_MM_dd_'T'_HH_mm_ss_SSS_z}] [%-5level] [%msg] [le]%n
</pattern>
</encoder>
<!-- Rolling 정책 : 로그 보관 정책 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 로그 백업 파일 경로 설정 -->
<!-- .gz,.zip 등을 넣으면 자동 일자별 로그파일 압축 -->
<fileNamePattern>${LOGS_PATH}/prod_%d{yyyy_MM_dd}(%i).log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- 파일당 최고 용량 KB, MB, GB -->
<!-- 아래 용량을 넘으면 파일 분할-->
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- 일자별 로그파일 최대 보관주기(~일), 해당 설정일 이상된 파일은 자동으로 제거-->
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<!-- <appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">-->
<!-- <!– 로그 스태시 주소 –>-->
<!-- <destination>localhost:5000</destination>-->
<!-- <encoder class="net.logstash.logback.encoder.LogstashEncoder">-->
<!-- </encoder>-->
<!-- </appender>-->
<!-- appender 별 세팅 -->
<!-- 로그 전역 세팅 -->
<!-- 로그 레벨 -->
<!--
1) ERROR : 오류 메시지 표시
2) WARN : 경고성 메시지 표시
3) INFO : 정보성 메시지 표시
4) DEBUG : 디버깅하기 위한 메시지 표시
5) TRACE : Debug보다 훨씬 상세한 메시지 표시
예를들어 info 로 설정시, INFO 보다 위에 있는 DEBUG 와 TRACE 는 표시하지 않는다.
-->
<root level="INFO">
<appender-ref ref="FILE"/>
<!-- <appender-ref ref="LOGSTASH"/>-->
</root>
</springProfile>
</configuration>
위와 같이 설정되어 있습니다.
- 컨테이너 실행
이제 준비는 끝났습니다.
커맨드 창을 띄우고, docker-compose 파일이 있는 위치로 이동후,
docker-compose -f docker-compose.yml up -d
위 명령어로 도커 컨테이너를 실행시킵니다.
설정에 이상이 없다면 위와 같이 잘 동작할 것입니다.
- Loki Grafana 설정
Grafana 가 현재 실행중일텐데,
http://127.0.0.1:3000
이 주소로 접속해봅시다.
초기 아이디와 비밀번호는 모두 admin 인데,
입력 후에 admin 아이디의 비밀번호를 설정해줍시다.
Datasource 에서 Add Data source 를 누르고, Loki 를 선택합니다.
loki 설정에는, Connection 에 loki 의 접속 주소를 입력하면 됩니다.
여기서는 동일 docker 네트워크 내의 loki 로 접속하도록 하였습니다.
이것만 입력하고 가장 아래에서 Save & test 버튼을 눌러 이상이 없으면 됩니다.
다시 Data Sources 를 누르면 앞서 등록한 loki 가 보일 것입니다.
이렇듯, Grafana 에서는 Data 를 가져오는 Data Source 를 여러개 등록이 가능합니다.
로그를 확인하려면, Explore 를 누르고, 데이터 소스를 앞서 등록한 loki 로 선택한 후,
Label filters 의 앞뒤 드롭다운 메뉴 버튼을 누르면 선택 가능한 옵션이 뜹니다.
filename 은 promtail 이 로그를 수집할 때 사용한 파일명을 나타내고, job 은 앞서 promtail 설정 파일 내에 설정한 job 입니다.
앞서 promtail 에서 설정했듯, 파일에서 로그를 잘 가져왔으며, [ls] 로 시작하고 [le] 로 끝나도록 로그들을 잘 나눈 것을 볼 수 있습니다.
- Prometheus Grafana 설정
도커가 실행되면 prometheus 가 실행되며 해당 서버에 요청을 지속적으로 보낼 것입니다.
prometheus 의 실행 상태를 확인하려면,
http://127.0.0.1:9090/graph
에 접속하여 logback_events_total 를 Search 하여 메트릭 정보를 조회할 수 있고,
http://127.0.0.1:9090/targets
에 접속하여 host 로 작성했던 EndPoint 와 서버의 상태를 확인해 볼 수 있습니다.
바로 Grafana 를 설정하겠습니다.
홈 화면에서 Basic 부분의 DATA SOURCES 를 클릭합니다.
이번에는 앞서 실행시킨 Prometheus 를 선택합니다.
설정에는, Connection 에 prometheus 의 접속 주소를 입력하면 됩니다.
여기서는 동일 docker 네트워크 내의 prometheus 로 접속하도록 하였습니다.
입력하고 가장 아래에서 Save & test 버튼을 눌러 이상이 없으면 됩니다.
이번에는 모니터링 Dashboard 를 구성할 것입니다.
building a dashboard 링크를 누르거나,
Dashboards 탭에 이동하여, Create Dashboard 버튼 -> Import a dashboard 를 누릅니다.
여기서는 미리 만들어진 대시보드 스타일을 가져와서 사용하는 것으로,
https://grafana.com/grafana/dashboards/
위 링크에서 대시보드를 검색하여 사용할 것인데,
Springboot 프로젝트용 대시보드를 사용할 것이므로,
Spring 으로 검색한 뒤, 마음에 드는 대시보드를 선택한 후,
우측의 대시보드 아이디 복사 버튼을 누르고,
Grafana 대시보드 설정 화면에서 Find and import dashboards for... 아래의 입력창에 붙여넣어 Load 를 누르면 대시보드 설정 창이 나옵니다.
가장 아래 드롭다운 버튼에서 Prometheus 를 선택한 후 Import 를 누르면 됩니다.
대쉬보드는 위와 같이 표현될 것입니다.
Prometheus 에서 수집한 actuator 정보를 사람이 파악하기 쉬운 형태로 보여주므로 편리합니다.
참고로 Instance 를 눌러 데이터를 가져올 서버를 변경 가능한데, 여기에 표시되는 이름은 앞서 prometheus 설정 안에서 labels.instance 설정으로 수정 가능합니다.
- 이상입니다.
서비스가 다운되면 이메일을 보내는 기능이나 그외의 개선된 설정 방식 등은 추후 수정하도록 하겠습니다.
'Programming > ETC' 카테고리의 다른 글
[Java] JNI 정리 및 개발 방식 정리 (2) | 2024.10.13 |
---|---|
[Java] 자바를 사용한 병렬 프로그래밍 정리와 synchronized, volatile 설명 (0) | 2024.10.13 |
Docker 컨테이너 안에서 Docker 사용하기 (Windows, Linux, MacOS) (5) | 2024.10.12 |
[Kotlin] List 타입 종류 (0) | 2024.09.29 |
JVM 메모리 누수 방지를 위한 체크사항 (5) | 2024.09.29 |