Java Off-Heap 메모리와 DirectByteBuffer 누수 탐지 실전 가이드

"☕" 11 "min read"

메모리를 분명히 해제했는데 프로세스 RSS는 왜 계속 올라갈까요? Heap GC는 정상인데 서버 메모리가 부족하다는 알람이 울린다면, JVM Heap 바깥의 네이티브 메모리를 의심해봐야 합니다. 이 글에서는 Off-Heap 메모리 누수의 원리부터 jcmd, pmap, NativeMemoryTracking을 이용한 탐지, 그리고 코드 레벨 해결까지 단계별로 정리합니다.


1. 왜 Off-Heap 메모리를 알아야 할까요?

Java 개발자라면 -Xmx로 Heap 크기를 설정하고 GC 로그를 확인하는 것에 익숙합니다. 그런데 운영 서버에서 이런 상황이 발생하는 경우가 있습니다.

GC 로그가 낯선 분들을 위한 간단 가이드

GC 로그를 활성화하려면 JVM 시작 시 아래 옵션을 추가합니다. (Java 9+ / Java 17 기준)

java -Xlog:gc*:file=gc.log:time,uptime,level,tags -jar app.jar

출력되는 로그 예시와 각 항목의 의미입니다.

[2025-02-25T10:30:15.123+0900][12.456s][info][gc] GC(42) Pause Young (Normal) (G1 Evacuation Pause) 1024M->256M(4096M) 15.234msCode language: CSS (css)
항목예시 값의미
타임스탬프2025-02-25T10:30:15GC가 발생한 실제 시각
uptime12.456sJVM 시작 후 경과 시간
GC 번호GC(42)42번째 GC 이벤트
GC 유형Pause YoungYoung 영역 GC (Minor GC). Pause Full이면 Full GC
원인G1 Evacuation PauseGC가 트리거된 이유
Heap 변화1024M->256M(4096M)GC 전 사용량 → GC 후 사용량 (전체 Heap 크기)
소요 시간15.234msGC에 걸린 시간. 이 동안 애플리케이션이 멈춤(STW)

핵심 포인트: GC 후 Heap 사용량(256M)이 정상 범위이고, Full GC가 빈번하지 않다면 Heap은 문제가 아닙니다. 그런데도 서버 메모리가 부족하다면? 바로 Off-Heap을 의심해야 합니다.

# Heap 사용률: 40% (안정적)
# GC 발생 빈도: 낮음 (문제 없음)
# 그러나...
$ free -m
              total        used        free
Mem:          16384       15100        200   ← 메모리 거의 없음

$ ps aux | grep java
user   1234  200  92  9800000  ...  java -Xmx4g ...
# RSS(Resident Set Size)가 Heap 설정(4GB)보다 훨씬 큼Code language: PHP (php)

이 현상의 주범 중 하나가 Off-Heap 메모리(네이티브 메모리) 입니다.

핵심 개념 정리

구분설명관리 주체
Heap 메모리new Object() 등 일반 객체가 할당되는 공간JVM GC
Off-Heap 메모리JVM 외부 네이티브 메모리. GC 대상이 아님개발자/라이브러리
DirectByteBufferOff-Heap을 Java에서 다루는 대표적 APIGC 간접 + 개발자

Off-Heap 메모리를 사용하는 대표적인 상황

Netty, Spring WebFlux, Kafka 클라이언트, Redis 클라이언트(Lettuce), 파일 I/O 최적화 등 대용량 I/O를 다루는 대부분의 라이브러리가 내부적으로 Off-Heap을 사용합니다. 이유는 단순합니다. Heap에서 네이티브 메모리로 복사하는 과정 없이 OS와 직접 데이터를 주고받을 수 있어 훨씬 빠르기 때문입니다.

Heap vs Off-Heap 메모리 구조 다이어그램 — JVM 프로          -세스 내에서 Heap 영역과 Native Memory 영역이 분리된 구조
Heap vs Off-Heap 메모리 구조 다이어그램 — JVM 프로
-세스 내에서 Heap 영역과 Native Memory 영역이 분리된 구조

2. DirectByteBuffer의 동작 원리

DirectByteBufferjava.nio 패키지에 속하며, ByteBuffer.allocateDirect()로 생성합니다.

// Heap 메모리에 버퍼 생성 (일반적인 방법)
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);

// Off-Heap(네이티브) 메모리에 버퍼 생성
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);Code language: JavaScript (javascript)

내부적으로 DirectByteBuffersun.misc.Unsafe.allocateMemory()를 호출하여 JVM Heap 밖의 네이티브 메모리를 직접 할당받습니다.

해제가 되는 순간

여기서 함정이 있습니다.

// 이렇게 해도 즉시 메모리가 해제되지 않습니다
ByteBuffer directBuffer = ByteBuffer.allocateDirect(100 * 1024 * 1024); // 100MB
directBuffer = null; // 참조만 끊음
System.gc();         // GC를 요청하지만 보장되지 않음Code language: JavaScript (javascript)

DirectByteBuffer 객체 자체는 Heap에 있는 아주 작은 Java 객체(약 64바이트)입니다. 이 객체가 GC로 수거될 때 내부의 Cleaner(또는 PhantomReference)가 트리거되어, 비로소 네이티브 메모리가 해제됩니다.

[Heap 영역]                         [Native Memory]
┌───────────────────────────┐       ┌───────────────────────────┐
│ DirectByteBuffer 객체      │──────▶│ 실제 데이터 100MB          │
│ (아주 작은 객체 ~64B)       │       │ (GC와 무관하게 존재)       │
│ + Cleaner reference       │       └───────────────────────────┘
└───────────────────────────┘
       │
       ▼ GC로 이 객체가 수거될 때
       Cleaner.clean() 호출 → 네이티브 메모리 해제Code language: CSS (css)

즉, 네이티브 메모리 해제는 Heap GC에 의존합니다. Heap 객체 자체가 매우 작기 때문에 Young GC로는 수거되지 않고, Full GC 시에야 겨우 수거되는 경우도 많습니다. 그래서 마치 누수처럼 보이는 현상이 발생하는 것입니다.


3. 누수 시나리오 — 이런 코드가 위험합니다

시나리오 1: 명시적 해제 없는 반복 할당

@Service
public class FileProcessingService {

    public void processLargeFile(Path filePath) throws IOException {
        // ❌ 매 호출마다 100MB Off-Heap 할당, 명시적 해제 없음
        ByteBuffer buffer = ByteBuffer.allocateDirect(100 * 1024 * 1024);

        try (FileChannel channel = FileChannel.open(filePath)) {
            channel.read(buffer);
            // ... 처리 로직
        }
        // buffer가 scope를 벗어나도 GC 타이밍에 따라 메모리 해제가 지연됩니다
    }
}Code language: PHP (php)

API 요청이 몰리면 GC가 수거하기 전에 새로운 100MB 버퍼가 계속 쌓이게 됩니다.

시나리오 2: Netty의 ByteBuf 릴리즈 누락

Spring WebFlux나 Netty 기반 서버에서 자주 발생하는 패턴입니다.

// ❌ 잘못된 예: release() 호출 누락
@Component
public class BadNettyHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf buf = (ByteBuf) msg;
        // buf를 읽고 처리하지만...
        processData(buf);
        // buf.release() 호출이 없으면 Off-Heap 메모리가 누수됩니다
    }
}Code language: JavaScript (javascript)
// ✅ 올바른 예: try-finally로 반드시 해제
@Component
public class GoodNettyHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf buf = (ByteBuf) msg;
        try {
            processData(buf);
        } finally {
            // 참조 카운트를 0으로 만들어 네이티브 메모리 해제
            ReferenceCountUtil.release(buf);
        }
    }
}Code language: JavaScript (javascript)

시나리오 3: Spring WebFlux에서 DataBuffer 누수

// ❌ 잘못된 예: DataBuffer 소비 후 해제 없음
@RestController
public class BadController {

    @PostMapping("/upload")
    public Mono<String> upload(ServerHttpRequest request) {
        return request.getBody()
            .map(dataBuffer -> {
                byte[] bytes = new byte[dataBuffer.readableByteCount()];
                dataBuffer.read(bytes);
                // ❌ DataBufferUtils.release(dataBuffer) 누락!
                return new String(bytes);
            })
            .reduce("", String::concat);
    }
}Code language: PHP (php)
// ✅ 올바른 예: DataBufferUtils.release() 명시
@RestController
public class GoodController {

    @PostMapping("/upload")
    public Mono<String> upload(ServerHttpRequest request) {
        return request.getBody()
            .map(dataBuffer -> {
                byte[] bytes = new byte[dataBuffer.readableByteCount()];
                dataBuffer.read(bytes);
                DataBufferUtils.release(dataBuffer); // ✅ 반드시 해제
                return new String(bytes);
            })
            .reduce("", String::concat);
    }
}Code language: PHP (php)

WebFlux에서 DataBuffer를 직접 다룰 때는 반드시 release()를 호출해야 합니다. bodyToMono(String.class) 같은 고수준 API를 사용하면 프레임워크가 자동으로 처리해주므로 가능하면 고수준 API를 활용하세요.


4. 탐지 도구 총정리

환경별로 적합한 도구가 다릅니다. 아래 표를 참고하여 상황에 맞는 도구를 선택하세요.

환경권장 도구특징
로컬/개발jcmd, JVM 플래그 (-XX:NativeMemoryTracking)JDK에 기본 포함되어 별도 설치 불필요. jcmd VM.native_memory로 Heap/Off-Heap/Thread/Code 등 영역별 메모리 사용량을 즉시 확인 가능. baseline과 diff 비교로 누수 구간을 빠르게 좁힐 수 있음
운영 서버 (Linux)pmap, /proc/{pid}/smapsJVM 밖에서 프로세스의 실제 물리 메모리(RSS) 매핑을 확인. [ anon ] 영역의 크기로 Off-Heap 할당량을 OS 레벨에서 직접 검증할 수 있어, JVM NMT와 교차 확인 시 유용
Spring Boot ActuatorMicrometer + Prometheus + Grafanajvm_buffer_memory_used_bytes{id="direct"} 메트릭으로 Direct Buffer 사용량을 시계열로 수집. Grafana 대시보드에서 시간 경과에 따른 메모리 추세를 시각화하여 서서히 증가하는 누수 패턴을 조기에 발견 가능
APM 연동Datadog, Pinpoint, Elastic APM메모리 지표뿐 아니라 요청 트레이싱, 스레드 덤프, 알람까지 통합 제공. 특정 API 엔드포인트와 메모리 증가의 상관관계를 추적할 수 있어 누수 원인이 되는 코드 경로를 특정하기 쉬움
Netty 애플리케이션Netty ResourceLeakDetectorNetty의 ByteBuf 참조 카운트 기반 누수를 코드 레벨에서 탐지. ADVANCED 모드로 설정하면 누수 발생 위치의 스택 트레이스를 로그에 출력하여, 어떤 핸들러에서 release()를 누락했는지 정확히 파악 가능

5. 단계별 누수 탐지 방법

STEP 1: NativeMemoryTracking 활성화

JVM 시작 옵션에 아래 플래그를 추가합니다. 운영 환경에서는 summary 모드로 오버헤드를 최소화하세요.

# 개발/스테이징: detail 모드 (오버헤드 약 5~10%)
java -XX:NativeMemoryTracking=detail -jar app.jar

# 운영 환경: summary 모드 (오버헤드 약 5% 이하)
java -XX:NativeMemoryTracking=summary -jar app.jarCode language: PHP (php)

STEP 2: jcmd로 메모리 스냅샷 비교

핵심 전략은 기준점(baseline)을 잡고 시간 경과 후 변화량을 비교하는 것입니다.

# 현재 JVM 프로세스 PID 확인
jps -l
# 출력 예: 12345 com.example.MyApplication

# 현재 시점을 기준점으로 설정
jcmd 12345 VM.native_memory baseline

# (부하를 가한 후) 기준점 대비 변화량 확인
jcmd 12345 VM.native_memory summary.diffCode language: CSS (css)

실제 출력 예시입니다. Direct 항목에 주목하세요.

Native Memory Tracking:

Total: reserved=5.2GB, committed=1.8GB  (+350MB from baseline)

-                 Java Heap (reserved=4096MB, committed=1200MB)
-                     Class (reserved=1056MB, committed=56MB)
-                    Thread (reserved=200MB, committed=200MB)
-                      Code (reserved=240MB, committed=48MB)
-                        GC (reserved=400MB, committed=400MB)
-                  Internal (reserved=12MB, committed=12MB)
-                    Direct (reserved=450MB, committed=450MB)  ← ⚠️ +350MB 증가!
-                   Mapping (reserved=80MB, committed=80MB)Code language: JavaScript (javascript)

Direct 항목이 지속적으로 증가한다면 DirectByteBuffer 누수를 강하게 의심해야 합니다.

STEP 3: JMX / MXBean으로 DirectBuffer 풀 모니터링

코드 레벨에서 주기적으로 Direct Buffer 상태를 확인할 수 있습니다.

import java.lang.management.ManagementFactory;
import java.lang.management.BufferPoolMXBean;
import java.util.List;

@Component
@Slf4j
public class DirectBufferMonitor {

    /**
     * Direct Buffer 사용 현황을 로깅합니다.
     * 스케줄러나 Actuator endpoint에서 주기적으로 호출하세요.
     */
    public void printDirectBufferStats() {
        List<BufferPoolMXBean> pools = ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class);

        for (BufferPoolMXBean pool : pools) {
            if ("direct".equals(pool.getName())) {
                log.info("[DirectBuffer] count={}, used={}MB, capacity={}MB",
                    pool.getCount(),                         // 현재 버퍼 개수
                    pool.getMemoryUsed() / 1024 / 1024,      // 실제 사용량 (MB)
                    pool.getTotalCapacity() / 1024 / 1024    // 총 할당 용량 (MB)
                );
            }
        }
    }
}Code language: JavaScript (javascript)

Spring Boot Actuator + Micrometer를 사용한다면 별도 코드 없이 자동으로 수집됩니다.

# application.yml
management:
  metrics:
    enable:
      jvm: true
  endpoints:
    web:
      exposure:
        include: metrics, prometheusCode language: PHP (php)

Prometheus에서 아래 메트릭으로 조회 가능합니다.

# Direct Buffer 사용량 (bytes)
jvm_buffer_memory_used_bytes{id="direct"}

# Direct Buffer 개수
jvm_buffer_count_buffers{id="direct"}Code language: PHP (php)

STEP 4: pmap으로 OS 레벨 확인 (Linux 운영 서버)

JVM 도구만으로 원인이 좁혀지지 않을 때, OS 레벨에서 프로세스 메모리 맵을 직접 확인합니다.

# 프로세스 메모리 맵을 RSS 기준 내림차순 정렬
pmap -x 12345 | sort -k3 -n -r | head -20

# 출력 예시
Address           Kbytes     RSS   Dirty Mode  Mapping
00007f8b00000000 4194304  350000  350000 rw---   [ anon ]   ← Off-Heap 영역
00007f8c00000000  524288  524288  524288 rw---   [ anon ]
...

# /proc을 통해 더 상세히 확인
cat /proc/12345/smaps | grep -A 10 "anon" | grep -E "Size|Rss|Anonymous"Code language: PHP (php)

RSS 값이 Heap 설정(-Xmx)보다 현저히 크다면 Off-Heap 누수를 의심해야 합니다.

STEP 5: Netty ResourceLeakDetector 설정

Netty 기반 애플리케이션(Spring WebFlux, gRPC 등)에서는 Netty의 자체 누수 탐지기를 활용할 수 있습니다.

# application.yml (Spring Boot)
# PARANOID: 모든 버퍼 추적 (개발환경 전용, 성능 저하 큼)
# ADVANCED: 샘플링 추적 + 누수 위치 로그 (스테이징 권장)
# SIMPLE: 누수 발생 여부만 감지 (운영 환경 권장)
# DISABLED: 비활성화
io:
  netty:
    leakDetection:
      level: ADVANCEDCode language: PHP (php)

또는 JVM 시작 옵션으로 설정합니다.

java -Dio.netty.leakDetectionLevel=ADVANCED -jar app.jar

누수가 발생하면 아래와 같은 로그가 출력됩니다. 스택 트레이스에서 누수 위치를 바로 확인할 수 있습니다.

LEAK: ByteBuf.release() was not called before it's garbage-collected.
Recent access records:
#1: io.netty.handler.codec.http.HttpObjectDecoder.decode(HttpObjectDecoder.java:456)
#2: com.example.handler.MyHandler.channelRead(MyHandler.java:78)  ← 누수 발생 위치

6. 해결 방법 — 코드 레벨 수정

방법 1: DirectByteBuffer 명시적 해제

Java 9+ 환경에서는 Cleaner를 직접 호출하여 GC를 기다리지 않고 즉시 해제할 수 있습니다.

import sun.nio.ch.DirectBuffer;
import java.nio.ByteBuffer;

public class DirectBufferUtil {

    /**
     * DirectByteBuffer의 네이티브 메모리를 즉시 해제합니다.
     * GC를 기다리지 않고 명시적으로 해제할 때 사용합니다.
     *
     * @param buffer 해제할 DirectByteBuffer (Direct 타입이 아니면 무시)
     */
    public static void free(ByteBuffer buffer) {
        if (buffer == null || !buffer.isDirect()) {
            return;
        }
        ((DirectBuffer) buffer).cleaner().clean();
    }
}

// 사용 예시
public void processFile(Path path) throws IOException {
    ByteBuffer buffer = ByteBuffer.allocateDirect(50 * 1024 * 1024); // 50MB
    try (FileChannel channel = FileChannel.open(path)) {
        channel.read(buffer);
        buffer.flip();
        // ... 처리 로직
    } finally {
        DirectBufferUtil.free(buffer); // ✅ 명시적 해제
    }
}Code language: JavaScript (javascript)

⚠️ sun.nio.ch.DirectBuffer는 내부 API입니다. Java 17+에서는 --add-opens java.base/sun.nio.ch=ALL-UNNAMED 옵션이 필요하며, 향후 버전에서 변경될 수 있습니다. 가능하면 아래 방법 2를 우선 고려하세요.

방법 2: DirectByteBuffer Pool 패턴 도입

매번 할당/해제하는 대신 풀링을 사용하면 GC 압박과 메모리 단편화를 줄일 수 있습니다.

import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;

/**
 * DirectByteBuffer 재사용 풀
 * 의존성: commons-pool2
 */
public class DirectByteBufferPool {

    private final GenericObjectPool<ByteBuffer> pool;
    private final int bufferSize;

    public DirectByteBufferPool(int bufferSize, int maxPoolSize) {
        this.bufferSize = bufferSize;

        GenericObjectPoolConfig<ByteBuffer> config = new GenericObjectPoolConfig<>();
        config.setMaxTotal(maxPoolSize);       // 최대 풀 크기
        config.setMaxIdle(maxPoolSize / 2);    // 최대 유휴 버퍼 수
        config.setMinIdle(2);                  // 최소 유지 버퍼 수

        this.pool = new GenericObjectPool<>(new BasePooledObjectFactory<ByteBuffer>() {
            @Override
            public ByteBuffer create() {
                return ByteBuffer.allocateDirect(bufferSize);
            }

            @Override
            public PooledObject<ByteBuffer> wrap(ByteBuffer buffer) {
                return new DefaultPooledObject<>(buffer);
            }

            @Override
            public void passivateObject(PooledObject<ByteBuffer> p) {
                // 풀에 반납할 때 버퍼를 초기화
                p.getObject().clear();
            }
        }, config);
    }

    /** 풀에서 버퍼를 빌려옵니다 */
    public ByteBuffer borrow() throws Exception {
        return pool.borrowObject();
    }

    /** 사용 완료 후 풀에 반납합니다 */
    public void release(ByteBuffer buffer) {
        pool.returnObject(buffer);
    }
}Code language: PHP (php)
// Spring Bean으로 등록
@Configuration
public class BufferPoolConfig {

    @Bean
    public DirectByteBufferPool directByteBufferPool() {
        return new DirectByteBufferPool(
            64 * 1024,   // 버퍼 1개당 64KB
            50           // 최대 50개 = 총 3.2MB Off-Heap
        );
    }
}Code language: PHP (php)

방법 3: -XX:MaxDirectMemorySize로 상한 설정

근본 해결은 아니지만, 운영 환경에서 Off-Heap 메모리의 무제한 증가를 막는 안전장치입니다. 상한에 도달하면 OutOfMemoryError: Direct buffer memory가 발생하여 빠르게 인지할 수 있습니다.

# Direct Memory 최대 허용량을 512MB로 제한
java -Xmx4g -XX:MaxDirectMemorySize=512m -jar app.jarCode language: PHP (php)

7. 결과 및 검증

수정 후 아래 지표들로 효과를 확인하세요.

# 1. jcmd로 Direct 메모리 변화량 재확인
jcmd 12345 VM.native_memory summary.diff

# 기대 결과: Direct 항목이 더 이상 증가하지 않음
-  Direct (reserved=150MB, committed=150MB)  (+0MB from baseline) ✅

# 2. Prometheus 쿼리로 시계열 확인
# 일정 시간 후에도 Direct Buffer 사용량이 수렴하는지 확인
jvm_buffer_memory_used_bytes{id="direct"}

# 3. RSS 비교 (프로세스 전체 메모리)
watch -n 5 'ps -o pid,rss,vsz -p 12345'Code language: PHP (php)

누수가 해결되었다면 Direct Buffer 사용량이 일정 수준에서 수렴하고, RSS도 더 이상 단조 증가하지 않는 것을 확인할 수 있습니다.


8. 추가로 알아두면 좋은 점

추가로 운영 환경에서 함께 고려하면 좋은 내용들을 정리했습니다.

Metaspace도 Off-Heap입니다

Java 8 이후 PermGen이 사라지고 Metaspace가 도입되었는데, 이것 역시 Off-Heap입니다. 동적으로 클래스를 많이 로딩하는 서비스(리플렉션, CGLIB 프록시 등)에서는 -XX:MaxMetaspaceSize도 함께 모니터링하세요.

Mapped File(MappedByteBuffer)도 주의

FileChannel.map()으로 생성하는 MappedByteBuffer도 Off-Heap을 사용합니다. 대용량 파일을 mmap으로 처리할 때는 MappedByteBuffer의 해제 시점을 명시적으로 관리해야 합니다.

Docker/Kubernetes 환경 주의사항

컨테이너 환경에서는 JVM이 컨테이너 메모리 한도를 인식하지 못하는 경우가 있어, -Xmx와 Off-Heap 합계가 컨테이너 limit을 초과하면 OOM Kill을 당합니다. 운영 시 아래 비율을 참고하세요.

컨테이너 메모리 limit = Heap(-Xmx) + Off-Heap + OS/JVM 오버헤드
예: limit=8GB → -Xmx=4GB, MaxDirectMemorySize=2GB, 나머지 2GB는 여유분

Netty 버전 업그레이드

Netty 4.1.x에서 Pooled DirectByteBuf 누수 관련 버그들이 꾸준히 수정되고 있습니다. 의존성 버전이 오래되었다면 업그레이드 자체로 누수가 해결되는 경우도 있으니, 릴리즈 노트를 확인해보세요.


9. 마무리

이번 글의 핵심을 세 줄로 정리합니다.

  1. DirectByteBuffer는 Heap 밖 네이티브 메모리를 사용하며, GC만으로는 즉시 해제가 보장되지 않습니다.
  2. jcmd VM.native_memory, pmap, BufferPoolMXBean을 조합하면 누수 위치를 구체적으로 특정할 수 있습니다.
  3. 명시적 해제(release(), Cleaner.clean()), 풀링 패턴, MaxDirectMemorySize 설정을 함께 적용하는 것이 안전합니다.

Off-Heap 메모리 문제는 Heap 모니터링만으로는 절대 발견할 수 없기 때문에, 운영 초기부터 NativeMemoryTracking과 Direct Buffer 메트릭을 모니터링 대시보드에 포함시키는 것을 권장합니다.

다음 글에서는 Java 17 기준 ZGC와 G1GC에서의 Off-Heap 상호작용 차이에 대해 다뤄보겠습니다.

Y

yshyuk

Java 백엔드 개발자 | Spring, AWS, DevOps
2020년부터 Java/Spring boot 서버 개발자로 일하면서, 인프라(AWS/NCP), DevOps 업무를 병행하였고, 현재는 OpenSource 기여에도 관심을 가지고 있습니다.

조회수: 0