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
반응형

Spring Boot 4.0 핵심 변경점 정리 — Java 25 지원부터 API 버저닝까지

2025년 11월, Spring Boot 4.0이 정식 출시됐다. 단순한 마이너 업데이트가 아니라 Spring Boot의 다음 세대를 여는 메이저 릴리즈다. Spring Framework 7과 Jakarta EE 11 위에 새로 올라섰고, 코드베이스 자체가 재구성됐다. 3.x를 잘 쓰고 있다면 굳이 지금 올려야 하나 고민될 수도 있는데, 결론부터 말하면 지금 당장 옮길 필요는 없지만, 신규 프로젝트라면 4.x로 시작하는 게 합리적이다.

이 글에서는 실무 관점에서 가장 중요한 변경점만 추려서 정리한다.


한눈에 보는 핵심 변경점

  • Spring Framework 7 / Jakarta EE 11 기반으로 전환
  • Java 25에 대한 1급 지원, 단 Java 17 호환은 유지
  • 거대한 auto-configure JAR을 작은 단위 모듈로 분해
  • JSpecify 기반 null 안전성을 표준으로 채택
  • API 버저닝 내장 지원 (@GetMapping(version = "1.0"))
  • 인터페이스로 HTTP 클라이언트를 만드는 HTTP Service Clients
  • Jackson 3 기본 채택 (패키지 경로 변경)
  • Gradle 9 빌드 지원
  • GraalVM 25+ 네이티브 이미지 요구사항 상향

출시 배경 — 왜 4.0인가

Spring Boot 3.x는 2022년 말 출시된 이후 약 3년 동안 안정적인 LTS 라인으로 쓰였다. 그동안 Java는 17 → 21 → 25로 두 번의 LTS를 거쳤고, Jakarta EE도 10에서 11로 올라갔다. 무엇보다 Spring Framework 7이 등장하면서, Boot 쪽도 그에 맞게 베이스라인을 끌어올릴 시점이 됐다.

Spring 팀이 이번 릴리즈에서 강조하는 키워드는 두 개다. 모듈화null 안전성. 둘 다 *"이미 동작하는 앱"* 보다는 *"앞으로 만들어질 앱"* 을 위한 변화에 가깝다. 즉, 4.0은 화려한 신기능이라기보다 앞으로 5년치 토대를 다시 까는 작업이라고 보는 편이 정확하다.


1. 코드베이스 모듈화 — 거대한 가방을 여러 개의 작은 가방으로

3.x 시절에는 spring-boot-autoconfigure라는 거대한 JAR 하나가 거의 모든 자동 설정을 들고 있었다. WebMVC만 쓰는 프로젝트에서도 LDAP, Neo4j, Quartz 같은 자동 설정 클래스가 클래스패스에 함께 끌려왔다.

Spring Boot 4.0은 이 구조를 깨고, 각 기술별로 작고 집중된 JAR로 쪼갰다. 예를 들어 spring-boot-starter-webmvc를 추가하면 WebMVC 관련 자동 설정만 따라온다. 그 결과 다음과 같은 효과가 있다.

  • 시작 시간 단축: 클래스패스 스캔 대상이 줄어든다
  • 메모리 사용 감소: 불필요한 자동 설정이 안 올라온다
  • 디버깅 편의성: 어디서 어떤 자동 설정이 들어왔는지 추적이 쉬워진다

다만 모듈이 잘게 쪼개진 만큼, starter POM을 쓰지 않고 직접 의존성을 관리하던 프로젝트는 의존성 누락 이슈가 발생할 수 있다. 마이그레이션 가이드에서도 이 부분을 가장 먼저 강조한다.


2. Java 25 1급 지원, Java 17 호환 유지

Spring Boot 4.0의 최소 요구 Java 버전은 여전히 Java 17이다. 다만 권장은 Java 25 LTS다.

Spring Boot 4.0 requires Java 17 or newer, with first-class support for Java 25.

여기서 "1급 지원(first-class support)"이 의미하는 바는, 단순히 Java 25에서 동작한다는 게 아니라 Virtual Threads, Pattern Matching, Sequenced Collections, 향상된 GC 같은 최신 JVM 기능을 프레임워크 차원에서 적극 활용한다는 뜻이다.

네이티브 이미지를 쓰려면 이야기가 좀 다르다. GraalVM 25 이상이 필요하다. 이전 GraalVM 23/24에서 빌드되던 네이티브 이미지가 그대로 빌드되지 않을 수 있으니, 네이티브를 쓰는 팀은 빌드 파이프라인을 먼저 점검해야 한다.


3. JSpecify로 표준화된 null 안전성

이번 릴리즈에서 가장 조용하지만 가장 영향력이 큰 변화다.

기존 Spring 코드에서 null 처리는 사실상 컨벤션에 의존했다. @Nullable 같은 애노테이션이 있긴 했지만, 어디 패키지의 어떤 애노테이션을 쓰느냐가 라이브러리마다 달랐다(Spring, JetBrains, Checker Framework, FindBugs…). IDE는 이걸 어찌어찌 통합해서 표시해줬지만, 일관성은 부족했다.

Spring Boot 4.0은 JSpecify를 표준 null 안전성 라이브러리로 채택했다. JSpecify는 Google, JetBrains, Spring 팀이 함께 합의한 일종의 표준 신호 체계다. 핵심 애노테이션은 다음과 같다.

  • @Nullable — 이 값은 null일 수 있다
  • @NonNull — 이 값은 절대 null이 아니다
  • @NullMarked — 이 패키지/클래스의 모든 참조는 기본 non-null이다

package-info.java@NullMarked를 한 번만 붙이면 패키지 안의 모든 참조 타입이 기본 non-null로 간주되고, null이 들어올 수 있는 곳만 @Nullable로 표시하면 된다. 코드 가독성이 명확해지고, IntelliJ나 NullAway 같은 정적 분석 도구가 빌드 단계에서 더 잘 잡아낸다.

// package-info.java
@NullMarked
package com.kraft.member;

import org.jspecify.annotations.NullMarked;
public Member findById(Long id) { ... }       // 절대 null 아님
public @Nullable Member findByEmail(String email) { ... }  // null 가능

기존 코드에 즉시 영향을 주진 않지만, 새로 작성하는 코드에는 처음부터 적용하는 게 좋다.


4. API 버저닝 — 이제 컨트롤러 한 줄로 처리

API 버저닝은 그동안 모든 팀이 각자의 방식으로 고생하던 영역이었다. URL 경로(/v1/...)에 박아 넣거나, 커스텀 헤더로 분기하거나, 미디어 타입으로 분기하거나. 어느 방식을 택하든 컨트롤러를 직접 갈라야 했고, 전략을 바꾸면 모든 컨트롤러를 다시 손대야 했다.

Spring Boot 4.0에서는 @RequestMapping 계열 애노테이션에 version 속성이 추가됐다.

@RestController
@RequestMapping("/api/posts")
public class PostController {

    @GetMapping(version = "1.0")
    public PostV1Response getPostsV1() { ... }

    @GetMapping(version = "2.0")
    public PostV2Response getPostsV2() { ... }
}

버전 분기 전략(URL, 헤더, 미디어 타입)은 설정에서 한 번만 정의하면 되고, 컨트롤러는 버전 번호만 신경 쓰면 된다. 전략을 나중에 바꿔도 컨트롤러 코드는 그대로 둘 수 있다는 점이 핵심이다.


5. HTTP Service Clients — 인터페이스만 선언하면 끝

외부 API를 호출할 때 RestTemplate이나 WebClient로 매번 코드를 짜는 대신, 인터페이스만 선언하고 구현체를 자동 생성하는 방식이 정식 자동 설정으로 들어왔다.

@HttpExchange(url = "https://api.example.com")
public interface WeatherClient {

    @GetExchange("/weather/{city}")
    WeatherResponse getWeather(@PathVariable String city);

    @PostExchange("/alerts")
    AlertResponse createAlert(@RequestBody AlertRequest request);
}

Feign이나 Retrofit을 써 본 사람이라면 친숙할 것이다. 차이는 Spring 자체에 통합됐다는 점이다. 별도 라이브러리 없이 @HttpExchange만 붙이면 Spring Boot가 알아서 구현체를 만들어 빈으로 등록한다. 4.x의 Spring Cloud Commons 5.0에서는 여기에 Circuit Breaker, 로드밸런싱(lb:// 스킴) 같은 기능까지 선언적으로 얹을 수 있다.

마이크로서비스 환경이 아니더라도, 외부 API를 호출하는 곳이 많은 프로젝트라면 코드량이 눈에 띄게 줄어든다.


6. Jackson 3 — 패키지 경로가 바뀐다

Spring Boot 4.0은 Jackson 3을 기본 JSON 라이브러리로 채택했다. 가장 눈에 띄는 변경은 패키지/그룹 ID다.

  • com.fasterxml.jackson.*tools.jackson.* 로 이동
  • 단, jackson-annotations 모듈은 호환성 때문에 com.fasterxml.jackson.core 그룹 ID와 com.fasterxml.jackson.annotation 패키지를 유지

직접 ObjectMapper를 import해서 쓰는 코드가 있다면 패키지 경로를 바꿔야 한다. 다만 Jackson 2를 요구하는 외부 라이브러리도 여전히 많기 때문에, Jackson 2와 3을 한 프로젝트에서 공존시킬 수 있도록 의존성 관리는 계속 제공된다.


그 외 주목할 변화

위에 적은 6가지 외에도 실무에서 한 번씩 만나게 되는 변경이 많다.

  • OpenTelemetry Starter 신설: spring-boot-starter-opentelemetry 추가만으로 OTLP 메트릭/트레이스를 내보낼 수 있다
  • Gradle 9 지원: Gradle 8.14 이상이면 둘 다 지원
  • Kotlin 2.2+ 요구: Kotlin 프로젝트는 빌드 환경부터 점검 필요
  • 클래식 uber-jar 로더 제거: 빌드 스크립트에서 관련 설정을 떼야 한다
  • Spring Retry 의존성 제거: 재시도 로직은 spring-core의 새 retry API로 이동
  • Testcontainers 모듈 prefix 정리: 모든 모듈이 testcontainers- 접두어로 통일
  • SSL 인증서 만료 모니터링 강화: expiringChains 항목 추가로 만료 임박 인증서를 health 응답에서 분리해 보여줌

3.x → 4.x 업그레이드, 무엇을 점검해야 하나

기존 프로젝트를 올리려면 다음 순서로 접근하는 게 안전하다.

  1. 3.5로 먼저 올린다: 3.x 안에서 deprecated 된 API를 모두 정리한 뒤 4.x로 점프
  2. Java 17 이상인지 확인: 11이나 8을 쓰고 있다면 자바 마이그레이션이 선행
  3. starter POM 사용 여부 점검: 직접 의존성 관리를 하고 있다면 누락 모듈 식별이 가장 먼저
  4. Jackson 사용처 점검: com.fasterxml.jackson.* 직접 import 여부, 커스텀 직렬화 코드 확인
  5. 네이티브 이미지 쓰는 경우 GraalVM 25로 업그레이드
  6. JSpecify 기반 정적 분석 도입 검토: 즉시 적용은 부담스러우면 신규 패키지부터

운영 중인 서비스를 무조건 빨리 올릴 이유는 없다. 3.x도 한동안 유지보수가 계속될 예정이고, 4.0의 진가는 신규 코드를 쓸 때 더 잘 드러난다.


마무리 — 4.0을 어떻게 받아들여야 할까

Spring Boot 4.0은 눈에 확 띄는 신기능 보다는 앞으로 5년의 토대를 다시 까는 릴리즈에 가깝다. 모듈화로 슬림해진 런타임, JSpecify로 표준화된 null 안전성, 인터페이스 기반 HTTP 클라이언트, 내장 API 버저닝 — 이 네 가지만으로도 신규 프로젝트라면 4.x로 시작할 명분이 충분하다.

반면 운영 중인 3.x 서비스는 서두를 필요가 없다. 3.5에서 deprecated 정리부터 차근차근 하면서, 분기점이 잡힐 때 옮기면 된다. 진짜로 챙겨야 할 건 새로 짜는 코드부터 4.x 스타일로 쓰는 것이다. JSpecify 애노테이션, 인터페이스 기반 HTTP 클라이언트, API 버저닝 attribute 같은 건 지금 손에 익혀두면 마이그레이션 시점에 부담이 훨씬 줄어든다.

Spring Boot 4.0은 이미 동작하는 앱을 깨러 온 게 아니라, 앞으로 만들 앱을 더 잘 만들게 하러 온 릴리즈다. 그 관점으로 보면 어디서부터 손을 댈지 답이 보인다.


참고 자료

  • Spring Boot 4.0 Release Notes (spring-projects/spring-boot Wiki)
  • Spring Boot 4.0 Migration Guide
  • Spring 공식 블로그 — Spring Boot 4.0.0 available now (2025.11.20)
  • JetBrains Blog — Spring Boot 4: Leaner, Safer Apps and a New Kotlin Baseline

#SpringBoot4 #SpringBoot #Java25 #JSpecify #APIVersioning #HTTPInterface #Jackson3 #SpringFramework7 #JakartaEE11 #백엔드 #Spring업그레이드 #자바개발


728x90
반응형
728x90
반응형

요약 한 줄
스프링 MVC는 DispatcherServlet이 중앙에서 요청을 받아 HandlerMapping → HandlerAdapter → (Controller) → ViewResolver → View 순으로 흐름을 조율하고, 중간에 필터/인터셉터/예외처리/리졸버들이 참여하는 파이프라인입니다.

목차

  1. 전체 흐름(빅픽처)
  2. 주요 컴포넌트 한 줄 정의
  3. 요청~응답 단계별 상세
  4. 인터셉터 vs 필터 vs AOP
  5. 예외 처리 흐름(@ExceptionHandler, @ControllerAdvice, HandlerExceptionResolver)
  6. 확장 포인트(커스터마이징 체크리스트)
  7. 실전 코드 스니펫 모음
  8. 면접/시험 포인트 & 체크리스트

1) 전체 흐름(빅픽처)

HTTP 요청
  ↓ (Servlet Filter 체인)
DispatcherServlet
  ↓ HandlerMapping(매핑 찾기)
HandlerExecutionChain(핸들러 + 인터셉터들)
  ↓ HandlerAdapter(호출 어댑터)
@Controller/@RestController 메서드 실행
  ↓ (Model/ModelAndView or @ResponseBody)
ViewResolver(뷰 선택) ─ 또는 HttpMessageConverter(바디 변환)
  ↓ View 렌더링(템플릿/정적리소스) or JSON 직렬화
HTTP 응답

  

2) 주요 컴포넌트 한 줄 정의

  • DispatcherServlet: 프론트 컨트롤러. 요청/응답의 트래픽 경찰.
  • HandlerMapping: 어떤 컨트롤러 메서드가 이 URL/HTTP 메서드를 처리하는지 탐색.
  • HandlerAdapter: 찾아낸 핸들러를 실행 가능한 형태로 호출.
  • HandlerMethodArgumentResolver: 컨트롤러 파라미터(@RequestParam, @PathVariable, @RequestBody, 커스텀 타입 등) 주입.
  • HttpMessageConverter: HTTP 바디 ↔ 객체(JSON, XML 등) 직렬화/역직렬화.
  • ViewResolver: 뷰 이름을 템플릿(View)으로 해석(Thymeleaf, JSP 등).
  • HandlerExceptionResolver: 예외를 적절한 응답/뷰로 변환.
  • HandlerInterceptor: 컨트롤러 전/후/완료 시점 가로채기(로깅, 인증 등).

3) 요청~응답 단계별 상세

  1. 필터 체인
    • 서블릿 컨테이너 레벨. 인코딩 처리, 보안(스프링 시큐리티), 공통 로깅 등.
  2. DispatcherServlet 진입
    • doDispatch()에서 실질 처리 시작.
    • HandlerMapping으로 HandlerExecutionChain(핸들러 + 인터셉터 목록) 획득.
  3. 인터셉터(preHandle)
    • 인증/인가, 트랜잭션 컨텍스트, 트레이싱 ID 부여 등.
  4. HandlerAdapter → 컨트롤러 실행
    • 메서드 파라미터는 ArgumentResolver들이 채워줌.
    • 바디(JSON 등)는 HttpMessageConverter로 역직렬화.
  5. 핸들러 반환값 처리
    • 템플릿 렌더: ModelAndView → ViewResolver로 뷰 탐색 후 렌더.
    • REST 응답: @ResponseBody/ResponseEntity → HttpMessageConverter로 JSON 직렬화.
  6. 인터셉터(postHandle, afterCompletion)
    • 모델 가공, 뷰 렌더 이후 리소스 정리, 예외 로깅 등.
  7. 예외 발생 시
    • @ExceptionHandler(로컬) → @ControllerAdvice(글로벌) → HandlerExceptionResolver 순으로 처리.

4) 인터셉터 vs 필터 vs AOP

구분                                    적용 레벨                          주요 시점                                                       사용 예

Filter 서블릿 컨테이너 DispatcherServlet 전/후 인코딩, 보안, CORS, 공통 로깅
Interceptor 스프링 MVC 컨트롤러 전/후/완료 로그인 체크, 요청 컨텍스트, 성능 측정
AOP 스프링 빈 메서드 메서드 호출 단위 트랜잭션, 캐시, 로깅 단면화
 

인증/인가는 필터/시큐리티, 컨트롤러 전후 로직은 인터셉터, 서비스/리포지토리横단 관심사는 AOP가 깔끔합니다.

5) 예외 처리 흐름

우선순위: 로컬 @ExceptionHandler → 글로벌 @ControllerAdvice → HandlerExceptionResolver

  • REST라면 적절한 상태코드 + JSON 바디로 통일.
  • HTML 뷰 프로젝트면 공통 오류 페이지로 렌더.

6) 확장 포인트 체크리스트

  • WebMvcConfigurer로 인터셉터/리졸버/메시지컨버터 추가
  • HandlerMethodArgumentResolver로 커스텀 파라미터 주입(예: @CurrentUser)
  • MessageConverter로 Protobuf/CSV 등 포맷 지원
  • LocaleResolver/TimeZone로 지역화
  • ContentNegotiation으로 ?format=json/Accept 헤더 대응
  • ViewResolver 우선순위 조정(Thymeleaf, JSP, JSON 등)
  • ExceptionResolver 또는 @ControllerAdvice로 에러 표준화
  • 정적 리소스 캐싱과 ResourceHandler 튜닝

7) 실전 코드 스니펫

7-1. 간단 컨트롤러

@RestController
@RequestMapping("/api")
public class HelloController {
  @GetMapping("/hello")
  public Map<String, Object> hello(@RequestParam String name) {
    return Map.of("message", "Hello " + name);
  }
}

7-2. 인터셉터 등록

@Component
public class LogInterceptor implements HandlerInterceptor {
  @Override
  public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
    req.setAttribute("startAt", System.currentTimeMillis());
    return true;
  }
  @Override
  public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object handler, Exception ex) {
    long took = System.currentTimeMillis() - (long) req.getAttribute("startAt");
    System.out.println("[TIME] " + req.getRequestURI() + " -> " + took + "ms");
  }
}

@Configuration
public class WebConfig implements WebMvcConfigurer {
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new LogInterceptor()).addPathPatterns("/api/**");
  }
}

7-3. 커스텀 ArgumentResolver (예: @CurrentUser)

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {}

@Component
public class CurrentUserResolver implements HandlerMethodArgumentResolver {
  @Override public boolean supportsParameter(MethodParameter parameter) {
    return parameter.hasParameterAnnotation(CurrentUser.class);
  }
  @Override public Object resolveArgument(MethodParameter p, ModelAndViewContainer mav,
      NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
    return webRequest.getNativeRequest(HttpServletRequest.class).getAttribute("user");
  }
}

@Configuration
class MvcConfig implements WebMvcConfigurer {
  @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
    resolvers.add(new CurrentUserResolver());
  }
}

7-4. 글로벌 예외 처리

@ControllerAdvice
public class GlobalExceptionHandler {
  @ExceptionHandler(IllegalArgumentException.class)
  public ResponseEntity<Map<String, Object>> handleBadRequest(IllegalArgumentException e) {
    return ResponseEntity.badRequest().body(Map.of(
      "error", "BAD_REQUEST",
      "message", e.getMessage()
    ));
  }
}

7-5. 메시지 컨버터 추가(예: CSV)

@Configuration
public class HttpMsgConfig implements WebMvcConfigurer {
  @Override
  public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    converters.add(0, new CsvHttpMessageConverter()); // 우선 등록
  }
}

8) 면접/시험 포인트 & 체크리스트

  • DispatcherServlet이 하는 일 3줄로 설명할 수 있는가?
  • HandlerMapping/Adapter/ExceptionResolver/MessageConverter 역할 구분 가능한가?
  • 인터셉터 vs 필터 vs AOP 적용 위치를 사례로 설명할 수 있는가?
  • REST에서 Content NegotiationHttpMessageConverter의 연결을 아는가?
  • 예외를 일관된 응답 스키마로 표준화하는가?
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
반응형

Java에서 하나의 공통 자원을 공유하려 할 때 static 키워드와 Singleton 패턴은 자주 비교됩니다.
두 방식은 비슷해 보이지만, 메모리 처리, 객체화 여부, 유지보수성에서 큰 차이가 있습니다.

이 글에서는 두 개념의 본질적인 차이언제 어떤 방식이 적합한지를 명확히 정리해드립니다.


✅ static이란?

  • 클래스 로딩 시 Method Area에 한번만 할당
  • 객체 생성 없이 클래스명으로 직접 접근 가능
  • 공통 유틸리티나 상수 정의에 적합
public class Util {
    public static int add(int a, int b) {
        return a + b;
    }
}
Util.add(3, 5);  // 객체 없이 바로 사용

✅ Singleton이란?

Singleton 구현 방식은 내 블로그 - Java Singleton 패턴 설명 포스트를 참고해주세요.

  • 객체를 하나만 생성하도록 보장하는 디자인 패턴
  • 지연 생성(Lazy Initialization) 가능
  • 상태를 유지할 수 있으며, 다형성 사용 가능

🧠 static vs Singleton 비교

항목staticSingleton
생성 시점 클래스 로딩 시 필요할 때 (Lazy 가능)
메모리 위치 Method Area Heap
객체화 여부 ❌ (객체 없음) ✅ (객체 존재)
상태 관리 불가능 (stateless) 가능 (stateful)
테스트 용이성 어렵다 (전역처럼 작동) 비교적 쉬움 (Mock 객체 가능)
다형성 불가능 가능 (인터페이스 구현 등)
 

✅ 언제 static을 쓰고 언제 Singleton을 써야 할까?

🔹 static이 적합한 경우

  • 상태가 필요 없는 순수 유틸성 로직
  • 계산기능, 공통 상수, 로그 포맷 등

🔹 Singleton이 적합한 경우

  • 상태 저장이 필요한 공용 객체
  • 의존성 주입이 필요한 서비스, DB 연결 등

🎯 결론

  • static: 단순하고 빠르지만 유연하지 않음
  • Singleton: 설계는 복잡하지만 유연성과 확장성이 좋음

상황에 맞는 선택이 유지보수성과 테스트 효율성을 크게 좌우합니다.

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
반응형

+ Recent posts