728x90
반응형

앞 글에서 던진 떡밥, 오늘 회수합니다.
"Virtual Threads가 게임 체인저"라고 했는데, 그래서 어떻게 써야 잘 쓰는 건지 코드와 함께 정리해드립니다.

Virtual Threads(가상 스레드)는 Java 21에서 정식 출시된 후 Java 24의 JEP 491로 가장 큰 약점이었던 pinning 문제까지 해결되면서, 2026년 현재 운영 환경 도입의 모든 장벽이 사라진 상태입니다.

이 글은 "개념 설명"이 아니라 "실전 코드 + 함정 회피" 중심입니다. Spring Boot 통합, HikariCP 이슈, ThreadLocal 트랩까지 모두 다룹니다.


✅ TL;DR — 5줄 요약

  • Spring Boot 3.2+에서는 spring.threads.virtual.enabled=true 한 줄이면 끝
  • Virtual Thread는 풀링 금지 — 매 작업마다 새로 만드는 게 정석
  • 동시성 제한은 풀 대신 Semaphore
  • Java 21~23에서 synchronized는 pinning 발생 → Java 24/25부터 해결됨
  • CPU-bound 작업은 여전히 Platform Thread가 유리

🎯 왜 가상 스레드인가? (1분 복습)

기존 방식의 한계

전통적인 Java 스레드는 OS 스레드와 1:1 매핑됩니다. 스레드 하나당 약 1MB의 스택 메모리를 잡아먹어요. 그래서:

  • 동시 요청 1만 개? → 메모리 폭발 💥
  • 해결책으로 나온 게 WebFlux 같은 리액티브 프로그래밍
  • 하지만 코드 스타일이 완전히 달라서 학습 비용 ↑

가상 스레드의 해법

Platform Thread (기존):  1 스레드 = 1 OS 스레드 = 1MB 메모리
Virtual Thread (신규):   수백만 개 가능, 메모리 거의 안 씀

JVM이 자체 스케줄러로 가상 스레드를 carrier(플랫폼) 스레드에 mount/unmount 하면서 관리합니다. "동기 코드처럼 짜되, 리액티브처럼 확장된다"가 핵심이에요.


🛠️ 활용법 1 — 순수 Java로 사용하기

가장 기본적인 방법

// 방법 1: Thread.ofVirtual() 빌더
Thread vt = Thread.ofVirtual()
    .name("my-task")
    .start(() -> {
        System.out.println("가상 스레드 실행 중!");
    });
vt.join();

// 방법 2: 한 줄로 시작
Thread.startVirtualThread(() -> {
    System.out.println("간단 버전");
});

실무에서 쓰는 건 이 패턴 ⭐

// ExecutorService를 가상 스레드로 — 가장 많이 쓰는 패턴
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<String>> futures = IntStream.range(0, 10_000)
        .mapToObj(i -> executor.submit(() -> {
            // 각 작업이 별도의 가상 스레드에서 실행됨
            return callExternalApi(i);
        }))
        .toList();

    for (var future : futures) {
        System.out.println(future.get());
    }
}
// try-with-resources 종료 시 모든 작업 완료 대기

⚠️ 중요: newVirtualThreadPerTaskExecutor()는 작업마다 새 가상 스레드를 생성합니다. 이게 정상이에요. 풀링하지 마세요!


🌱 활용법 2 — Spring Boot 3.2+에서 사용하기

단 한 줄로 활성화

# application.properties
spring.threads.virtual.enabled=true
# application.yml
spring:
  threads:
    virtual:
      enabled: true

이 한 줄이면 Spring Boot가 알아서 다음 영역에 가상 스레드를 적용합니다.

  • Tomcat/Jetty 웹 요청 처리
  • @Async 메서드 실행
  • @Scheduled 작업
  • WebClient 비동기 요청
  • 기타 내부 TaskExecutor

적용 확인하기

@RestController
@RequestMapping("/thread")
public class ThreadCheckController {

    @GetMapping("/info")
    public String getThreadInfo() {
        Thread current = Thread.currentThread();
        return String.format(
            "Thread: %s, Virtual: %s",
            current.getName(),
            current.isVirtual()
        );
    }
}

호출 결과:

Thread: VirtualThread[#171]/runnable@ForkJoinPool-1-worker-4, Virtual: true

isVirtual()true로 나오면 성공!

환경 요구사항

항목 최소 사양 권장
Java 21 24 이상 (JEP 491 적용)
Spring Boot 3.2 3.3+
Spring Framework 6.1 6.2+

⚙️ 활용법 3 — 세밀한 제어가 필요할 때

전체 적용이 아니라 특정 영역에만 가상 스레드를 쓰고 싶다면:

@Configuration
public class VirtualThreadConfig {

    // Tomcat 요청 처리만 가상 스레드로
    @Bean
    public TomcatProtocolHandlerCustomizer<?> protocolHandlerCustomizer() {
        return protocolHandler -> protocolHandler.setExecutor(
            Executors.newVirtualThreadPerTaskExecutor()
        );
    }

    // @Async 메서드만 가상 스레드로
    @Bean
    public AsyncTaskExecutor asyncTaskExecutor() {
        return new TaskExecutorAdapter(
            Executors.newVirtualThreadPerTaskExecutor()
        );
    }

    // 특정 비즈니스 로직 전용
    @Bean
    public ExecutorService emailSenderExecutor() {
        return Executors.newVirtualThreadPerTaskExecutor();
    }
}

조건부 활성화도 가능합니다.

@Bean
@ConditionalOnProperty(value = "app.thread-mode", havingValue = "virtual")
public AsyncTaskExecutor virtualThreadExecutor() {
    return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}

⚠️ 함정 1 — Virtual Thread는 절대 풀링하지 마세요

가장 많이 하는 실수입니다.

// ❌ 잘못된 예 — 가상 스레드의 의미를 완전히 무시
ExecutorService bad = Executors.newFixedThreadPool(20,
    Thread.ofVirtual().factory());

// ✅ 올바른 예 — 매 작업마다 새 가상 스레드 생성
ExecutorService good = Executors.newVirtualThreadPerTaskExecutor();

가상 스레드는 생성/소멸이 거의 무료입니다. 풀링은 플랫폼 스레드의 비싼 비용을 아끼려는 패턴이지, 가상 스레드에 적용하면 오히려 손해예요.

그럼 동시성 제한은 어떻게?

다운스트림 서비스 보호를 위해 동시 호출을 제한하고 싶다면 Semaphore를 쓰세요.

public class RateLimitedService {
    // 외부 API 동시 호출을 20개로 제한
    private final Semaphore semaphore = new Semaphore(20);

    public String callExternalApi(String url) throws InterruptedException {
        semaphore.acquire();
        try {
            return httpClient.send(url);
        } finally {
            semaphore.release();
        }
    }
}

// 사용 시 — 가상 스레드는 마음껏 만들어도 됨
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 10_000; i++) {
        executor.submit(() -> service.callExternalApi("..."));
    }
}

⚠️ 함정 2 — Pinning 이슈 (Java 21~23에서 발생)

Pinning이 뭔가요?

가상 스레드가 carrier 스레드에 달라붙어서 떨어지지 않는 현상입니다. 이렇게 되면 carrier가 다른 가상 스레드를 처리하지 못해 사실상 풀링 모델로 퇴화해요.

Java 21~23에서의 원인

// ❌ Java 21~23에서 pinning 발생
public synchronized void cachedFetch(String key) {  // synchronized
    var data = database.query(key);  // 블로킹 I/O
    cache.put(key, data);
}

synchronized 블록 안에서 블로킹 I/O가 일어나면 가상 스레드가 carrier에 pinning됩니다.

Java 24/25에서 해결됨 (JEP 491) 🎉

Java 24부터 JEP 491: Synchronize Virtual Threads without Pinning이 적용되어 synchronized로 인한 pinning이 거의 사라졌습니다. JVM이 모니터를 virtual thread 단위로 추적하도록 변경됐어요.

그래서 2026년 현재의 권장사항은:

  • Java 24+ 사용 가능synchronized 그대로 사용해도 OK
  • Java 21~23 사용 → 핫 패스의 synchronizedReentrantLock으로 교체 권장
// Java 21~23용 — pinning 회피 패턴
public class SafeCache {
    private final ReentrantLock lock = new ReentrantLock();

    public Data fetch(String key) {
        lock.lock();
        try {
            return database.query(key);  // pinning 안 일어남
        } finally {
            lock.unlock();
        }
    }
}

아직 pinning이 발생하는 케이스

JEP 491 이후에도 다음 경우엔 여전히 pinning이 발생합니다.

  • 네이티브 메서드 호출 중 블로킹
  • Foreign Function & Memory API 사용 중 블로킹
  • 클래스 초기화 중 블로킹

JFR 이벤트 jdk.VirtualThreadPinned로 모니터링 가능합니다.


⚠️ 함정 3 — HikariCP & ThreadLocal 트랩

Spring Boot에서 가상 스레드를 켰는데 DB 연결이 자꾸 타임아웃난다면 의심할 곳이 여기입니다.

문제 상황

HikariCP는 ThreadLocal로 connection을 캐싱하는데, 가상 스레드는 매번 새로 만들어지므로 캐시 히트율이 0에 수렴합니다. 결과적으로:

HikariPool-1 - Connection is not available, request timed out after 30000ms

해결 방법

1. 커넥션 풀 사이즈 증설

spring.datasource.hikari.maximum-pool-size=50
spring.datasource.hikari.connection-timeout=30000

2. 동시 DB 호출에 Semaphore 적용

@Service
public class UserService {
    private final Semaphore dbSemaphore = new Semaphore(50);  // pool size에 맞춤
    private final UserRepository repository;

    public User findById(Long id) throws InterruptedException {
        dbSemaphore.acquire();
        try {
            return repository.findById(id).orElseThrow();
        } finally {
            dbSemaphore.release();
        }
    }
}

3. JDBC 드라이버 최신 버전 사용

MySQL Connector/J 9.0+, PostgreSQL JDBC 최신 버전은 내부 synchronizedReentrantLock으로 교체해 가상 스레드 친화적으로 개선됐습니다.

<!-- pom.xml -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>9.1.0</version> <!-- 9.0+ 권장 -->
</dependency>

⚠️ 함정 4 — ThreadLocal 의존 코드 주의

ThreadLocal을 캐시처럼 쓰는 코드는 가상 스레드에서 무용지물입니다. 매 요청마다 새 스레드라 캐시 히트가 안 일어나요.

// ❌ 가상 스레드에서 의미 없음
private static final ThreadLocal<DateFormat> dateFormat =
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

대안: ScopedValue (Java 21 Preview, Java 25 정식)

Java 25에서 정식화된 ScopedValue를 쓰면 가상 스레드에서도 효율적으로 컨텍스트를 전달할 수 있습니다.

// Java 25+
public class RequestContext {
    public static final ScopedValue<String> USER_ID = ScopedValue.newInstance();

    public void handleRequest(String userId, Runnable task) {
        ScopedValue.where(USER_ID, userId).run(task);
    }

    public void someMethod() {
        String currentUser = USER_ID.get();  // 어디서든 접근
        // ...
    }
}

🚫 가상 스레드를 쓰면 안 되는 경우

만능이 아닙니다. 다음 상황에서는 여전히 Platform Thread가 유리해요.

1. CPU-bound 작업

// ❌ 이런 건 가상 스레드 의미 없음
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> {
        // 행렬 곱셈, 이미지 처리, 암호화 등
        return computePrime(1_000_000);  // 순수 CPU 연산
    });
}

// ✅ CPU-bound는 ForkJoinPool 또는 코어 수만큼의 플랫폼 스레드
ExecutorService cpuPool = Executors.newFixedThreadPool(
    Runtime.getRuntime().availableProcessors()
);

가상 스레드의 강점은 블로킹 I/O 처리입니다. CPU 작업은 어차피 코어 수만큼만 동시 실행되니 이득이 없어요.

2. 짧고 동기적인 작업만 하는 경우

레이턴시가 1ms 이하의 매우 짧은 작업이라면 컨텍스트 스위칭 오버헤드가 더 클 수 있습니다.

3. 기존 시스템이 잘 돌아가는 경우

리액티브 코드(WebFlux)가 이미 잘 동작 중이고 성능 이슈가 없다면, 굳이 다 갈아엎을 필요는 없습니다.


📊 모니터링 & 디버깅

Pinning 감지

# Java 21~23: 시스템 프로퍼티
-Djdk.tracePinnedThreads=full

# Java 24+: JFR 이벤트로
java -XX:StartFlightRecording=filename=app.jfr,settings=profile MyApp

가상 스레드 사용 여부 확인

@Component
public class ThreadMonitor {

    @EventListener(ApplicationReadyEvent.class)
    public void logThreadInfo() {
        log.info("Available processors: {}",
            Runtime.getRuntime().availableProcessors());
        log.info("Default Virtual Thread Scheduler parallelism: {}",
            System.getProperty("jdk.virtualThreadScheduler.parallelism"));
    }
}

Spring Boot Actuator 활용

management.endpoints.web.exposure.include=health,metrics,threaddump

/actuator/threaddump에서 가상 스레드와 플랫폼 스레드를 모두 확인할 수 있습니다.


🚀 실전 마이그레이션 체크리스트

기존 Spring Boot 프로젝트에 가상 스레드를 적용하려면 이 순서로 진행하세요.

  • Java 24 이상으로 업그레이드 (또는 최소 Java 21)
  • Spring Boot 3.2 이상으로 업그레이드
  • JDBC 드라이버를 가상 스레드 친화 버전으로 업데이트 (MySQL 9.0+, pgjdbc 최신)
  • HikariCP maximum-pool-size 재검토 (필요 시 증설)
  • ThreadLocal 사용 코드 점검 — 캐시 용도라면 ScopedValue 또는 다른 방식으로 교체
  • 핫 패스의 synchronized 블록에서 블로킹 호출 여부 점검 (Java 21~23인 경우)
  • 스테이징 환경에서 부하 테스트 후 프로덕션 적용
  • application.propertiesspring.threads.virtual.enabled=true 추가
  • 프로덕션 적용 후 JFR/Actuator로 pinning 모니터링

✍️ 결론

Virtual Threads는 더 이상 "신기술 한번 써보자" 단계가 아닙니다. JEP 491로 마지막 장벽까지 사라진 2026년 현재, 다음과 같은 워크로드라면 도입을 진지하게 고민할 시점이에요.

  • 외부 API를 많이 호출하는 백엔드
  • 데이터베이스 I/O가 주요 병목인 애플리케이션
  • 동시 접속자가 많은 웹 서비스
  • 마이크로서비스 간 통신이 빈번한 시스템

리액티브 프로그래밍의 학습 비용 없이도 수만 동시 요청을 처리하는 백엔드를 만들 수 있다는 것, 이게 가상 스레드의 진짜 가치입니다.


📌 핵심 요약 (TL;DR)

🔹 활성화: Spring Boot 3.2+에서 spring.threads.virtual.enabled=true 한 줄
🔹 풀링 금지: newVirtualThreadPerTaskExecutor() 사용, 동시성 제한은 Semaphore
🔹 Pinning: Java 24+에서 JEP 491로 거의 해결됨, Java 21~23은 ReentrantLock 권장
🔹 HikariCP: pool size 재조정 + 최신 JDBC 드라이버 사용
🔹 CPU-bound 작업: 여전히 플랫폼 스레드가 정답


👉 다음 글 예고

다음 편에서는 Java 25에서 정식 출시된 Structured Concurrency 활용법을 다룰게요. 가상 스레드와 함께 쓰면 동시 작업의 생명주기를 깔끔하게 관리할 수 있습니다.

도움이 되셨다면 공감 + 댓글 부탁드려요 😊

 

728x90
반응형
728x90
반응형

백엔드 개발자라면 한 번쯤 고민해본 그 질문
"지금 프로젝트, 자바 몇으로 시작해야 안 망할까?"

2026년 4월 기준, 자바 생태계는 또 한 번 큰 변화를 맞았습니다. 작년 9월에 Java 25 LTS가 나왔고, 한 달 전인 3월엔 Java 26까지 출시됐죠. 그런데 현실은? 아직도 많은 기업이 Java 8, 11을 쓰고 있습니다.

이 글에서는 2026년 현재 자바 버전별 점유율, 특징, 그리고 상황별 추천 버전까지 한 방에 정리해드립니다.


✅ 한눈에 보는 자바 버전 요약 (2026년 기준)

순위 버전 출시 상태 한줄평
🥇 Java 21 LTS 2023.09 현역 메인 신규 프로젝트의 안전한 기본값
🥈 Java 17 LTS 2021.09 엔터프라이즈 표준 Spring Boot 3.x의 최소 사양
🥉 Java 11 LTS 2018.09 레거시 강자 아직도 많은 기업이 운영 중
4위 Java 8 LTS 2014.03 고인물 공공/금융권에 여전히 잔존
🆕 Java 25 LTS 2025.09 최신 LTS 그린필드 프로젝트라면 도전해볼 만
🧪 Java 26 2026.03 비-LTS 6개월 후 지원 종료, 실험용

💡 TIP: LTS(Long-Term Support)는 오라클이 8년간 지원하는 안정 버전이에요. 비-LTS는 6개월짜리 단명 버전이라 실서비스엔 부적합합니다.


📊 진짜 현실: 2025~2026년 자바 점유율 데이터

귀를 의심하실 수도 있는데, 현실은 생각보다 보수적입니다.

  • New Relic 2024년 자바 생태계 리포트: 응답자의 약 2/3가 Java 11 이하 사용 중
  • Azul 2025년 State of Java Survey: 19%가 아직도 Java 6 또는 7을 운영 중
  • JRebel 2024년 리포트: 업그레이드 사유 1위는 신기능(19%)이 아닌 LTS 지원(25%)과 보안(24%)

즉, 대부분의 기업은 "신기능이 좋아서"가 아니라 "지원이 끝나서 어쩔 수 없이" 업그레이드합니다. 트렌드와 현실의 격차가 꽤 크다는 점, 꼭 기억해두세요.


🥇 Java 21 LTS — 2026년 현재의 진짜 메인스트림

출시: 2023년 9월
프리미어 지원: 2028년 9월까지
한 마디로: "지금 신규 프로젝트의 가장 안전한 선택"

왜 Java 21이 메인인가?

  • Virtual Threads (가상 스레드) 정식 도입 → 동시성 처리 패러다임이 바뀜
  • Pattern Matching, Record Patterns, Sealed Classes 등 모던 문법 완성
  • Spring Boot 3.2+, Quarkus, Micronaut 등 주요 프레임워크 완벽 지원
  • 출시 2년 차로 안정성 검증 완료

Virtual Threads가 왜 게임 체인저인가?

기존엔 동시 요청 1만 개 처리하려면 WebFlux 같은 리액티브 프로그래밍을 써야 했습니다. 코드 스타일 자체가 완전히 달라져서 학습 곡선이 가팔랐죠.

가상 스레드는 평소처럼 동기 코드로 작성하면서도 수만 개 동시 요청을 처리할 수 있게 해줍니다. 즉, "리액티브 세금"을 안 내도 되는 거죠.

// Java 21 가상 스레드 예시
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i -> {
        executor.submit(() -> {
            // 각 작업이 가상 스레드에서 실행됨
            return fetchUserData(i);
        });
    });
}

🥈 Java 17 LTS — 엔터프라이즈의 든든한 허리

출시: 2021년 9월
프리미어 지원: 2026년 9월까지 (⚠️ 올해 종료!)
한 마디로: "지금 운영하는 시스템의 표준, 하지만 곧 업그레이드 시점"

Java 17의 역사적 의미

Spring Boot 3.0부터 최소 요구사항이 Java 17로 올라갔습니다. 이게 결정적이었어요. 수많은 기업이 Java 8/11에서 17로 점프한 이유가 바로 여기 있습니다.

주요 특징

  • Sealed Classes — 상속 계층 봉인
  • Pattern Matching for instanceof — 타입 체크 + 캐스팅 통합
  • Records — 데이터 클래스 보일러플레이트 제거
  • Text Blocks — 멀티라인 문자열 깔끔하게
// Records 예시
public record User(Long id, String name, String email) {}

// Pattern Matching 예시
if (obj instanceof User user && user.id() > 0) {
    System.out.println(user.name());
}

⚠️ 주의사항

오라클 프리미어 지원이 2026년 9월에 종료됩니다. 운영 중인 시스템이라면 슬슬 Java 21 또는 25로 마이그레이션 계획을 세워야 할 시점이에요.


🥉 Java 11 LTS — 아직 살아있는 국민템

출시: 2018년 9월
프리미어 지원: 2026년 9월까지
한 마디로: "현실의 표준, 하지만 슬슬 빠질 시간"

대기업, SI, 공공기관에서 가장 많이 쓰는 버전이 여전히 Java 11입니다. 모듈 시스템, var 키워드, HTTP Client API 등 8 대비 충분히 모던하면서도 안정성이 검증돼 있죠.

하지만 2026년 9월 이후엔 OTN 라이선스로 전환되어 상용 환경에서 무료 사용이 어려워집니다. 마이그레이션 시점이 다가오고 있다는 거예요.


🗿 Java 8 LTS — 사라지지 않는 그분

출시: 2014년 3월
한 마디로: "공공/금융권의 영원한 표준?"

람다, 스트림, Optional을 도입해 자바를 한 단계 끌어올린 역사적 버전이지만, 출시 12년이 지난 지금도 현역입니다. 특히 다음 환경에서 여전히 강세:

  • 공공기관 SI 프로젝트 (제안서에 "Java 8 기준"이 박혀 있는 경우)
  • 금융권 코어뱅킹 시스템
  • 오래된 레거시 운영 환경

다만 새로 시작하는 프로젝트에 Java 8을 선택하는 건 기술 부채를 미리 쌓는 행위입니다. 피할 수 있다면 무조건 피하세요.


🆕 Java 25 LTS — 최신 LTS, 도전해볼 가치 있나?

출시: 2025년 9월
프리미어 지원: 2030년 9월까지
한 마디로: "그린필드 프로젝트라면 적극 고려"

Java 25의 핵심 기능

  • 간소화된 main 메서드public static void main(String[] args) 보일러플레이트 제거
  • Structured Concurrency 정식화 — 동시 작업의 생명주기 관리
  • Scoped Values — ThreadLocal의 현대적 대체재
  • Module Import Declarationsimport module java.base 한 줄로 끝
  • Compact Source Files — 스크립트성 자바 코드 작성이 진짜 쉬워짐
// Java 25 — 이것도 유효한 자바 코드입니다
void main() {
    System.out.println("Hello, Java 25!");
}

도입 권장 시점

  • 2026년 신규 프로젝트로 시작하는 경우
  • 팀 내 자바 숙련도가 높고 빠른 도입 의지가 있는 경우
  • 프레임워크(Spring Boot 3.4+, Quarkus 3.15+ 등) 호환성 확인 후

🧪 Java 26 (2026.03) — 써도 될까?

출시: 2026년 3월 17일
상태: 비-LTS (6개월 후 지원 종료)

Java 26은 HTTP/3 지원, AOT 캐시 개선(ZGC와 호환), G1 처리량 향상 등 의미 있는 개선이 있지만, 실서비스 도입은 비추천입니다. 6개월 후 Java 27이 나오면 지원이 끊기기 때문이죠.

미리 신기능을 테스트해보고 싶은 개발자나 오픈소스 프로젝트에서만 사용하세요.


💼 라이선스 이슈 — 절대 놓치면 안 되는 부분

2023년 오라클이 자바 라이선스를 CPU 단위에서 직원 수 단위(Employee 기반)로 바꾸면서 비용이 폭등했습니다. 그 결과:

  • 오라클 JDK 시장 점유율: 2020년 75% → 2024년 21%로 급락
  • 70% 이상의 기업이 비-오라클 JDK 대안을 검토 중

추천 무료 JDK 배포판

배포판 특징 추천 대상
Eclipse Temurin (구 AdoptOpenJDK) 가장 대중적, 벤더 중립 일반적인 모든 환경
Amazon Corretto AWS 환경 최적화, 무료 LTS AWS 기반 서비스
Azul Zulu 다양한 플랫폼 지원 멀티 플랫폼 운영
BellSoft Liberica 임베디드/JavaFX 포함 데스크톱/IoT
SAP Machine SAP 환경 최적화 SAP 통합 시스템

💡 오라클 JDK 라이선스 비용이 부담된다면 Eclipse Temurin이 가장 무난한 선택입니다.


🎯 상황별 자바 버전 추천 가이드

✅ 신규 프로젝트를 시작한다면

  • 첫 번째 선택: Java 21 LTS (안정성 + 가상 스레드)
  • 공격적 선택: Java 25 LTS (최신 LTS, 5년 더 긴 지원)

✅ Spring Boot 사용 시

  • Spring Boot 3.x → Java 17 이상 필수 (21 권장)
  • Spring Boot 4.0+ (예정) → Java 21 이상 필수

✅ 레거시 시스템 유지보수

  • 기존 Java 8 → 현행 유지하되, 점진적으로 11/17 마이그레이션 계획
  • 기존 Java 11 → 2026년 9월 지원 종료 대비 17 또는 21로 업그레이드 시작

✅ 공공/금융 SI 프로젝트

  • 발주처 요구사항 확인이 최우선
  • 일반적으로 Java 8 또는 11이 기준
  • 자체 판단 가능하다면 17 제안 시도

✅ 마이크로서비스 / 클라우드 네이티브

  • Java 21 + Virtual Threads 조합 강력 추천
  • 컨테이너 환경 → AOT 컴파일 고려 시 GraalVM Native Image도 검토

✍️ 결론 — 그래서 뭘 쓰라고?

복잡하게 생각할 것 없이 이렇게 정리됩니다.

🎯 2026년 현재의 정답

  • 신규 프로젝트 → Java 21 LTS (가장 안전한 메인스트림)
  • 보수적 환경 → Java 17 LTS (단, 2026년 9월 지원 종료 주의)
  • 그린필드 + 도전 → Java 25 LTS
  • 레거시 운영 → 현재 버전 유지 + 마이그레이션 계획 수립

자바는 더 이상 "느리고 무거운 언어"가 아닙니다. Virtual Threads로 동시성 문제를 해결했고, Project Amber로 보일러플레이트를 줄였고, Project Leyden으로 시작 시간까지 개선되고 있어요. 2026년의 자바는 그 어느 때보다 모던합니다.


📌 핵심 요약 (TL;DR)

  • 🎖 현재의 표준: Java 21 LTS (2023년 출시, 2028년까지 지원)
  • 🆕 최신 LTS: Java 25 (2025년 출시, 2030년까지 지원)
  • ⚠️ 곧 EOL: Java 11, 17 — 2026년 9월 프리미어 지원 종료
  • 💰 라이선스: 오라클 JDK 대신 Temurin, Corretto 등 무료 대안 추천
  • 🚀 Virtual Threads: 자바 동시성의 게임 체인저, Java 21부터 사용 가능

👉 이 글이 도움 되셨다면 공감과 댓글 부탁드려요 😊

다음 글에서는 Virtual Threads 실전 활용법을 자세히 다뤄볼게요!


태그: #Java #Java21 #Java25 #자바버전 #JavaLTS #SpringBoot #백엔드개발 #VirtualThreads #자바개발자 #JDK #Temurin #Corretto #2026개발트렌드

728x90
반응형
728x90
반응형

📚 목차

  1. 이게 왜 중요한데?
  2. 표 하나로 비교 끝!
  3. 언제 뭘 써야 하나요?
  4. 마무리 요약 (진짜 1분 컷)

1. 이게 왜 중요한데?

자바 개발하면서 List, Set, Map 구분 못 하면...
👉 NullPointerException, IndexOutOfBoundsException, 동기화 오류
줄줄이 터집니다.
그럼 PM이 와서 말하죠:

“그거... 그냥 ArrayList 말고 HashSet 쓰면 안 되나?”


2. 표 하나로 비교 끝! ✅

구분ListSetMap
중복 허용 ✅ O ❌ X ✅ Value만 O
순서 유지 ✅ O ❌ X (HashSet), ✅ TreeSet은 정렬됨 ❌ X (정렬 불가)
인덱스로 접근 ✅ O (get(index)) ❌ X ❌ X
Key-Value 구조 ❌ X ❌ X ✅ O
대표 클래스 ArrayList, LinkedList HashSet, TreeSet HashMap, TreeMap
 

3. 언제 뭘 써야 하나요?

상황추천 컬렉션
데이터 순서 유지 + 중복 허용 ArrayList
중복 없이 빠르게 저장 HashSet
정렬된 데이터 필요 TreeSet, TreeMap
Key로 조회 필요 HashMap
삽입/삭제가 많음 LinkedList
 

4. 마무리 요약 (1분 컷 핵심)

List<String> list = new ArrayList<>(); // 순서 O, 중복 O
Set<String> set = new HashSet<>();     // 순서 X, 중복 X
Map<String, String> map = new HashMap<>(); // Key-Value

📌 기억법:

  • List: 줄세우기 좋아함
  • Set: 중복 싫어함
  • Map: Key로 놀고 Value로 살고
728x90
반응형
728x90
반응형

JVM은 자바(Java)의 핵심이라 해도 과언이 아닙니다.
우리가 작성한 자바 코드가 어떻게 실행되는지,
플랫폼 독립성이 어떻게 보장되는지,
모든 답은 JVM에 있습니다.

이 글에서는 JVM이 무엇인지, 내부 구성 요소는 무엇이며,
왜 Java가 JVM 덕분에 강력한 언어인지 쉽게 정리해드립니다.

☕ JVM이란?

JVM (Java Virtual Machine) = 자바 가상 머신

자바 프로그램을 실행하는 엔진이자,
"한 번 작성하면 어디서나 실행된다 (Write Once, Run Anywhere)"라는 자바 철학을 실현해주는 핵심 기술입니다.


🔧 무슨 일을 할까?

JVM의 핵심 역할은 딱 하나:

컴파일된 .class 바이트코드 파일을 실행하는 것

즉, 우리가 작성한 .java 소스 코드는 **JDK의 컴파일러(javac)**에 의해 .class라는 바이트코드로 바뀌고,
이 바이트코드를 운영체제에 맞게 해석해서 실행하는 주체가 바로 JVM입니다.


🔍 JVM 내부 동작 구조 요약

[.class 파일]
      ↓
 Class Loader → 메모리에 로딩
      ↓
 Bytecode Verifier → 유효성 검사
      ↓
 Execution Engine → 실행 (JIT 컴파일 포함)
      ↓
 Native Code → 실제 OS에서 동작

📦 구성 요소 간단 설명

구성 요소역할
Class Loader 클래스 파일 메모리에 로드
Execution Engine 바이트코드 해석 & 실행
JIT Compiler 바이트코드를 네이티브 코드로 동적 변환
GC (Garbage Collector) 불필요한 객체 메모리 자동 정리
Runtime Data Area 메모리 공간 (Heap, Stack 등)
 

🤔 JVM이 왜 중요한가요?

  • 플랫폼 독립성: 같은 자바 프로그램이 Windows, Linux, macOS에서 실행 가능
  • 메모리 관리 자동화: GC 덕분에 개발자가 직접 메모리 해제할 필요 없음
  • 보안성: 바이트코드 검사 및 제한된 실행 환경 제공

📌 용어 간 관계 정리

용어설명
JDK 자바 개발 도구 세트 (JRE + 컴파일러 포함)
JRE 실행 환경 (JVM + 라이브러리)
JVM 실제 실행기 (JRE 내부에 있음)

 

728x90
반응형
728x90
반응형

✅ 한눈에 보는 차이표

구성 요소설명포함 관계주요 역할
JDK (Java Development Kit) 자바 개발 도구 전체 세트 JDK ⊃ JRE ⊃ JVM 개발 + 실행
JRE (Java Runtime Environment) 자바 실행 환경 JRE ⊃ JVM 실행만 가능
JVM (Java Virtual Machine) 자바 가상 머신 (실행 엔진) JRE ⊃ JVM 바이트코드 실행
 

🔍 1. JVM (Java Virtual Machine)

“Write once, run anywhere”를 실현시키는 핵심”

  • 역할: .class 파일(바이트코드)을 실행
  • 동작:
    1. Class Loader: 클래스를 메모리에 로딩
    2. Bytecode Verifier: 코드 검증
    3. Execution Engine: 코드 실행 (JIT 컴파일 포함)
  • OS/환경별로 다름: 각 OS에 맞는 JVM이 존재

🔸 JVM만으로는 자바 코드 작성 불가


🔍 2. JRE (Java Runtime Environment)

“자바 프로그램을 실행할 수 있는 환경”

  • 구성:
    • JVM
    • 자바 클래스 라이브러리(rt.jar)
    • 필요한 기타 실행파일 (예: java 명령어)
  • 목적: 자바 프로그램 실행만 가능 (컴파일 ×)
  • 대상: 자바 개발이 아닌 실행만 필요한 사용자 (예: 서버 운영자)

🔸 개발 도구 없음 (javac X)


🔍 3. JDK (Java Development Kit)

“자바를 개발하기 위한 모든 도구 포함”

  • 구성:
    • JRE
    • 개발 툴 (javac, javadoc, jar 등)
  • 역할: 자바 소스 코드를 작성, 컴파일, 디버깅 가능
  • 대상: 자바 개발자

🔸 JDK 설치하면 JRE, JVM 포함

[JDK]
 └── [JRE]
       └── [JVM]

🧠 쉽게 외우는 포인트

외우는 방법
JVM은 바이트코드를 실행하는 엔진
JRE는 JVM + 실행 라이브러리 = 실행 환경
JDK는 JRE + 개발 도구 = 개발 키트
 

🎯 개발자 입장에서 요약

  • 자바 코드 작성 ➡️ JDK 필수
  • 자바 앱 실행만 ➡️ JRE만 설치해도 가능
  • 바이트코드 실행 책임자 ➡️ JVM

 

728x90
반응형
728x90
반응형

자바 개발자라면 꼭 알고 있어야 하는 GC(Garbage Collector)!
객체는 언제 메모리에서 사라지고, 애플리케이션이 왜 가끔씩 멈추는지 궁금하셨나요?
아래 이미지와 함께 GC의 작동 구조를 쉽게 설명해드립니다.


🔍 GC 개념 요약

**Garbage Collector(GC)**는 사용하지 않는 객체를 힙 메모리에서 자동으로 제거하는 JVM의 기능입니다.
메모리 누수를 방지하고 개발자가 직접 메모리를 관리하지 않아도 되게 해주는 아주 고마운 기능이죠.


🧠 Java 힙 메모리 구조

아래 이미지는 자바 힙(Heap) 메모리의 구성과 객체 이동 과정을 시각적으로 표현한 구조입니다.

✅ 설명:

  • 객체는 먼저 Eden 영역에 생성됩니다.
  • GC(Minor GC)가 발생하면 Eden → Survivor1 → Survivor2를 오가며 살아남습니다.
  • 여러 번 살아남으면 Old 영역으로 이동합니다.
  • Old가 꽉 차면 **Major GC (또는 Full GC)**가 발생합니다.
  • Permanent(또는 Java 8 이후 MetaSpace)는 클래스 정보 등 JVM 메타데이터를 저장하는 공간입니다.

💥 GC의 종류

GC 종류대상 영역특징
🧹 Minor GC Young Generation (Eden + Survivor) 빠르고 자주 발생함
🧹 Major GC Old Generation 느리고 멈춤(STW)이 발생함
🧹 Full GC 전체 힙 영역 Major GC 포함 + 기타 리소스 정리
 

🛑 STW(Stop The World)란?

GC를 수행하는 동안 JVM의 모든 스레드가 멈추는 현상입니다.
특히 Major GC나 Full GC 시 STW가 발생하며 사용자 체감 지연이 생길 수 있습니다.

📌 GC 튜닝의 핵심: STW 시간을 최소화하는 것!


⚙ 대표적인 GC 알고리즘

GC 종류특징적합 환경
Serial GC 단일 스레드, 기본 구조 저사양/테스트용
Parallel GC 멀티스레드 병렬 처리 일반 서버, 병렬 CPU
Parallel Old GC Old까지 병렬 처리 고사양 서버
G1 GC Region 단위 처리, STW 최소화 Java 9 이상 대규모 앱
ZGC/Shenandoah 실시간 GC, STW 거의 없음 실시간 처리 시스템
 

🧰 개발 팁 요약

  • 객체는 가능한 빨리 Eden에서 제거되도록 설계하자
  • Old로 가는 객체는 재사용하거나 Pool 처리가 바람직
  • GC 튜닝 시에는 G1 GC, ZGC 등 최신 GC 전략 고려

✅ 마무리

GC는 Java의 강력한 장점 중 하나지만,
그 내부 동작을 모르면 애플리케이션 지연이나 STW 문제를 겪기 쉽습니다.
이제 GC 동작 구조를 알았으니, 메모리 튜닝과 성능 최적화에 자신감이 붙으실 겁니다!

728x90
반응형
728x90
반응형

🔎 접근자(Accessor)란?

객체 지향 프로그래밍(OOP)에서는 객체가 가진 **프로퍼티(속성)**에 직접 접근하기보다,
메서드를 통해 안전하게 접근하는 것을 권장합니다.
이때 사용하는 메서드가 바로 **접근자(Accessor)**입니다.


✅ 접근자가 필요한 이유

user.name = "Steve";  // ❌ 직접 접근

이처럼 필드에 직접 접근하게 되면:

  • 데이터 무결성을 보장할 수 없고,
  • 나중에 로직을 추가하거나 디버깅하기 어려워집니다.

→ 그래서 우리는 GetterSetter를 통해 간접 접근을 유도합니다.


🧩 Getter / Setter 정의

  • Getter: 특정 속성 값을 읽을 때 사용
  • Setter: 특정 속성 값을 변경할 때 사용

✅ Java 예시

public class User {
    private String name;
    private int age;

    // Getter
    public String getName() {
        return name;
    }

    // Setter
    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

→ 위와 같이 getName(), setName() 메서드를 통해
외부에서 안전하게 name 필드를 조회/수정할 수 있습니다.


📚 정리

역할이름예시
값 읽기 Getter getName()
값 설정 Setter setName("Steve")
 

접근자는 객체 캡슐화의 핵심이며,
이 원칙을 지킴으로써 객체의 상태를 안정적으로 관리할 수 있습니다.

728x90
반응형
728x90
반응형

Java에서 자주 마주치는 static 키워드, 정확히 어떤 역할을 하고 어떻게 동작할까요?
이번 포스트에서는 static 키워드의 메모리 구조, 특징, 사용 예시까지 한 번에 정리해 드립니다.


✅ static이란?

  • static은 클래스 로딩 시 메모리에 한번만 할당되고,
    프로그램이 종료될 때까지 공유되고 유지되는 자원입니다.
  • 객체를 생성하지 않아도 클래스명으로 직접 접근할 수 있다는 것이 특징입니다.

🧠 static의 메모리 구조

구분메모리 영역설명
클래스 자체 Method Area (static 영역) 클래스 정보 로드
new 생성 객체 Heap 영역 Garbage Collector 관리 대상
static 변수/메서드 Method Area (static 영역) 프로그램 종료 시까지 유지
 

🔸 static은 GC의 대상이 아니며, 모든 인스턴스가 공유합니다.


📌 static 변수/메소드 특징

  • 클래스 변수 (모든 객체가 공통으로 사용)
  • 객체 생성 없이도 사용 가능 (클래스명.변수, 클래스명.메서드)
  • 자주 사용하면 메모리 누수 위험 존재 (GC가 해제하지 않음)

✅ Java 코드 예시

public class Calculator {
    public static String calName = "myBoard";

    public static int add(int x, int y) {
        return x + y;
    }

    public int min(int x, int y) {
        return x - y;
    }
}

☑ 사용 예시

Calculator.add(5, 1);  // ✅ 객체 없이 사용 가능
Calculator.min(5, 1);  // ❌ 오류 (static 아님)

Calculator cal = new Calculator();
cal.add(5, 1);  // 가능은 하지만 ❗비권장
cal.min(5, 1);  // 정상 사용

⚠ static 사용 시 주의사항

  • 전역 상태처럼 작동하므로 남용 시 테스트 어려움, 의존성 증가
  • 웹 서버 환경에서는 동시성 이슈 발생 가능성
  • 주로 공용 유틸성 클래스 또는 상수 선언에 사용
728x90
반응형

+ Recent posts