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

ChatGPT나 Claude를 써 본 사람이라면 한 번쯤은 겪어봤을 것이다. 존재하지 않는 책을 그럴듯하게 추천하고, 가짜 논문 제목과 가짜 저자를 만들어내고, 작년에 끝난 회사를 아직 운영 중이라고 답하는 그 현상. 이걸 학술 용어로 환각(Hallucination) 이라고 부른다.

문제는 이게 단순한 버그가 아니라 현재 LLM 아키텍처에 내재된 구조적 특성이라는 점이다. 원리를 모르면 "AI가 똑똑하니까 알아서 잘 하겠지" 하고 그대로 쓰게 되고, 운영 중인 서비스에서 사고가 난다. 이 글에서는 환각이 왜 발생하는지, 어떤 유형이 있는지, 그리고 개발자가 실무에서 환각을 줄이려면 어떤 전략을 써야 하는지를 정리한다.


환각이란 무엇인가

가장 자주 인용되는 학술적 정의는 이렇다. 2025년 10월 arXiv에 공개된 한 종합 서베이 논문에서는 환각을 "LLM이 생성한 내용 중, 유창하고 문법적으로는 정확하지만 사실과 다르거나 외부 근거로 뒷받침되지 않는 출력" 이라고 정의한다.

핵심은 두 가지다.

  • 유창함: 문장은 매끄럽고 문법적으로 자연스럽다
  • 사실 불일치: 그러나 실제 사실, 인용된 출처, 또는 주어진 문맥과 어긋난다

즉 환각은 "틀렸는데 틀린 줄 모르게 잘 쓴 글" 이다. 이게 가장 위험한 이유는, 사용자가 어디가 틀렸는지 알아채기 어렵다는 데 있다. 명백한 오류는 차라리 낫다. 환각은 99%가 맞고 1%만 조용히 틀려 있다.


환각의 유형 — 무엇이 어떻게 틀리는가

연구에 따라 분류 방식은 다양하지만, 실무 관점에서 자주 마주치는 유형은 다음 4가지다.

1. 사실 환각 (Factual Hallucination)

존재하지 않는 사실을 만들어내거나, 실제 사실을 잘못 기술한다.

"아인슈타인은 1921년 노벨 물리학상을 받았으며, 그의 수상 사유는 상대성이론이었다."

수상 연도는 맞지만, 사유는 틀렸다(실제로는 광전 효과). 이런 "미묘하게 틀린 사실" 이 가장 흔하고 잡기 어렵다.

2. 컨텍스트/논리 환각 (Contextual / Logical Hallucination)

주어진 문맥과 모순되거나, 추론 과정에 논리적 오류가 들어간다. RAG로 문서를 제공했는데도 모델이 문서에 없는 내용을 끼워 넣거나, 문서 내용을 자기 멋대로 해석하는 경우가 여기 해당한다.

3. 시간적 혼동 (Temporal Disorientation)

학습 데이터의 시점과 현재 시점을 구분하지 못한다. 이미 끝난 사건을 진행 중이라고 답하거나, 폐업한 회사의 CEO를 현직으로 답하는 식이다. 학습 컷오프 이후 정보는 모델 입장에서는 존재하지 않는 시간대에 있다.

4. 출처 조작 (Reference Fabrication)

가장 악명 높은 유형이다. 존재하지 않는 논문, 책, 판례, URL을 그럴듯한 형식으로 만들어낸다. 저자 이름, 출판 연도, 저널명이 모두 그럴싸하지만 실제로 검색하면 아무것도 안 나온다. 변호사가 ChatGPT가 만든 가짜 판례를 법정에 제출했다가 징계받은 사례가 미국에서 여러 번 보도된 적이 있다.

이 외에도 코드 생성 환각(존재하지 않는 라이브러리 함수 호출), 윤리적 환각, 멀티모달 환각 등 도메인별 변종이 계속 보고되고 있다.


왜 발생하는가 — 환각의 4단계 원인

환각은 한 군데서 발생하는 게 아니라 LLM 개발 라이프사이클의 모든 단계에서 누적된다. 단계별로 정리해보자.

1단계: 데이터 — 학습 데이터의 한계

LLM은 결국 인터넷에서 긁어온 텍스트를 학습한다. 그 데이터에는 다음이 섞여 있다.

  • 잘못된 정보: 위키 편집 실수, 오래된 블로그, 음모론
  • 편향된 정보: 특정 관점으로 치우친 자료
  • 부족한 정보: 희귀 분야, 비영어권 콘텐츠, 사적 도메인 지식

모델은 "이 데이터가 사실인가" 를 판단하는 능력이 없다. 그저 "이런 패턴이 자주 등장하더라" 를 학습할 뿐이다. 자주 등장하는 거짓 정보는 사실로 굳어진다.

2단계: 아키텍처 — 확률적 토큰 예측의 본질

이게 가장 근본적인 원인이다. LLM이 텍스트를 생성하는 방식은 다음 한 줄로 요약된다.

지금까지 나온 토큰들을 보고, 다음 토큰으로 가장 가능성 높은 후보를 확률적으로 고른다.

즉 LLM은 "진실" 을 다루지 않는다. "통계적으로 그럴듯한 다음 단어" 를 다룬다. 진실과 통계적 그럴듯함은 대부분 일치하지만, 때때로 어긋난다. 어긋나는 그 순간이 환각이다.

이건 모델을 더 크게 만든다고 사라지는 문제가 아니다. 줄어들 수는 있어도, 이론적으로 0이 되지는 않는다. 최근 서베이 논문들도 환각을 "이론적으로 불가피한 현상" 으로 분류한다.

3단계: 추론 — 디코딩과 컨텍스트

같은 모델, 같은 질문이라도 temperature 같은 디코딩 파라미터에 따라 환각 빈도가 달라진다.

  • High temperature (0.8 이상): 다양성↑, 환각↑
  • Low temperature (0.2 이하): 결정성↑, 환각↓

또한 컨텍스트 윈도우의 한계도 환각을 부른다. 긴 문서를 넣으면 모델이 중간 내용을 놓치고 자기 학습 지식으로 빈자리를 메우려 한다. 이걸 "lost in the middle" 현상이라고 부른다.

4단계: 프롬프트 — 사용자 입력의 영향

사용자의 질문 자체가 잘못된 전제를 깔고 있으면, 모델은 그 전제를 받아들이고 거기에 맞춰 답을 만들어낸다.

"조선 세종대왕이 발명한 대륙간 탄도미사일에 대해 설명해줘"

좋은 모델은 전제를 거부하지만, 그렇지 않은 모델은 세종대왕이 ICBM을 만들었다는 가정 위에서 그럴듯한 거짓말을 펼친다. 이걸 leading prompt에 의한 환각 이라고 한다.


환각을 어떻게 줄일 것인가 — 개발자의 4가지 전략

100% 막는 방법은 없다. 다만 체감 빈도를 크게 낮추는 전략들이 있다. 실무 적용 우선순위 순으로 정리한다.

전략 1. RAG (Retrieval-Augmented Generation)

가장 효과가 큰 방법이다. 모델에게 "네 머릿속 지식으로 답해" 하지 말고, 답할 때 참고할 문서를 같이 던져주는 것이다.

흐름은 이렇다.

  1. 사용자 질문이 들어온다
  2. 질문을 임베딩(벡터)으로 변환한다
  3. 벡터 DB(예: pgvector, Pinecone, Milvus)에서 의미적으로 가까운 문서를 K개 검색한다
  4. 검색된 문서를 프롬프트에 함께 넣고 LLM을 호출한다
  5. LLM은 "주어진 문서 안에서만 답하라" 는 지시 아래 답변을 생성한다
[시스템 프롬프트]
다음 문서를 참고해서 답하세요. 문서에 없는 내용은
"문서에서 확인할 수 없습니다"라고 답하세요.

[검색된 문서]
{retrieved_docs}

[사용자 질문]
{user_query}

RAG는 "학습 컷오프 이후 정보" 와 "사적 도메인 지식" 환각을 거의 다 잡아준다. 단, RAG도 만능은 아니다. 검색이 부정확하면(잘못된 문서를 찾아오면) 환각이 그대로 나오고, 모델이 검색된 문서를 무시하고 자기 지식으로 답하는 경우도 있다. 그래서 "문서에 없으면 모른다고 답하라" 같은 명시적 지시가 중요하다.

전략 2. 프롬프트 엔지니어링

모델 호출 코드를 손대지 않고도 환각을 줄일 수 있는 가장 싼 방법이다. 효과적인 패턴 몇 가지.

1) 명시적 불확실성 허용

모르는 내용에 대해서는 추측하지 말고 "확실하지 않습니다"라고
답하세요. 잘못된 정보를 자신 있게 답하는 것보다 모른다고
답하는 것이 훨씬 낫습니다.

이 한 문장만 시스템 프롬프트에 넣어도 환각이 눈에 띄게 줄어든다.

2) 출처 명시 요구

답변에 사용한 사실에 대해서는 반드시 출처(문서명·페이지·URL)를
함께 표기하세요. 출처를 댈 수 없는 정보는 답변에서 제외하세요.

출처를 요구하면 모델이 "근거가 있는 정보" 만 골라서 답하려는 경향이 강해진다. 단, 출처 자체를 환각하는 경우도 있으니 후처리에서 검증이 필요하다.

3) Chain-of-Thought + Self-Verification

답을 바로 내지 말고 추론 과정을 먼저 적게 한 다음, 그 추론을 스스로 검증 하게 한다. 두 단계로 호출이 늘어나서 비용은 올라가지만, 사실성이 중요한 도메인(법률, 의료, 금융)에서는 충분히 가치 있다.

전략 3. 검증 워크플로우 — AI 출력은 항상 초안

이건 기술이 아니라 운영 원칙이다. 사용자가 메모리에서 강조하는 것과도 정확히 같은 관점인데, AI의 출력을 그대로 신뢰하지 않고 항상 검증 단계를 둬야 한다. 특히 다음은 무조건 검증한다.

  • 숫자: 통계, 가격, 날짜, 버전, 비율
  • 출처: 논문 제목, 저자, URL, 책 ISBN
  • 법규/기준: 법조문, 표준 번호, 인증 기준
  • 최신 정보: 현직, 진행 중 사건, 최신 릴리즈 버전

검증 워크플로우는 "AI 답변 → 사람 검토 → 게시" 의 단순한 형태일 수도 있고, "AI 답변 → 다른 모델로 cross-check → 검증 통과한 부분만 저장" 같은 자동화 형태일 수도 있다. 도메인 위험도에 맞춰 설계하면 된다.

전략 4. 모델·파라미터 선택

모든 환각이 같지 않다. 작업 성격에 맞춰 모델과 파라미터를 고르면 환각을 구조적으로 줄일 수 있다.

  • 사실성 중요한 작업: 큰 모델 + temperature 0.1~0.3
  • 창의성 필요한 작업: 작은 모델도 OK + temperature 0.7~1.0
  • 코드 생성: 코드 특화 모델 + 낮은 temperature + 컴파일/테스트로 검증
  • 에이전트(도구 호출) 작업: tool-use 학습이 잘 된 모델 + 함수 시그니처 엄격 정의

실무 체크리스트

지금까지 정리한 내용을 한 페이지로 압축하면 다음과 같다.

  • [ ] 숫자·출처·법규·최신 정보는 무조건 검증한다
  • [ ] 사적 도메인 지식이나 최신 정보가 필요하면 RAG를 도입한다
  • [ ] 시스템 프롬프트에 "모르면 모른다고 답하라" 를 명시한다
  • [ ] 사실성 작업은 temperature를 낮게 둔다
  • [ ] 답변 출처를 요구하되, 출처 자체도 검증한다
  • [ ] 위험도 높은 도메인에서는 사람 검토 또는 cross-check 단계를 둔다
  • [ ] AI 출력은 최종 결과물이 아니라 초안 으로 본다

마무리 — 환각은 없앨 대상이 아니라 관리할 대상

LLM 환각은 잡초 같은 게 아니라 날씨 같은 것이다. 막을 수 없으니, 그 위에서 제대로 일하는 법을 익혀야 한다. 모델이 더 좋아져도 환각은 0이 되지 않고, 새 도메인·새 데이터·새 모델이 나올 때마다 새로운 형태의 환각이 같이 나온다.

가장 위험한 건 환각 자체가 아니라, 환각을 인지하지 못하는 워크플로우다. AI 답변을 검증 없이 그대로 게시하거나, 그대로 코드에 박거나, 그대로 보고서에 인용하는 순간 사고가 난다. 반대로 "AI 출력은 항상 초안" 이라는 원칙만 팀이 공유하고 있으면, 환각이 있어도 시스템은 안전하게 굴러간다.

환각을 두려워하지 말고, 이해하고 다루는 쪽으로 가자. 이 글에서 정리한 4가지 원인과 4가지 대응 전략이 그 출발점이 될 것이다.


참고 자료

  • Aisha Alansari, Hamzah Luqman. Large Language Models Hallucination: A Comprehensive Survey (arXiv:2510.06265, 2025.10)
  • A Comprehensive Taxonomy of Hallucinations in Large Language Models (arXiv:2508.01781, 2025.08)
  • LLM-based Agents Suffer from Hallucinations (arXiv:2509.18970, 2025.09)
  • Lewis et al. Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks

 

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

AI 전쟁의 중심이 바뀌고 있다: 모델 경쟁에서 인프라·반도체·규제 경쟁으로

2026년 4월 말 AI 업계의 주요 뉴스를 보면, 이제 AI 경쟁은 단순히 “어떤 모델이 더 똑똑한가”의 문제가 아닌 것으로 보입니다. 모델 성능 경쟁은 여전히 중요하지만, 실제 승부는 가격, 클라우드 인프라, 반도체, 플랫폼 접근권, 교육 시장으로 빠르게 확장되고 있습니다.

이번 글에서는 최근 AI 관련 주요 이슈를 6가지 흐름으로 정리해 보겠습니다.


1. DeepSeek, 새 AI 모델 출시와 가격 인하

중국 AI 기업 DeepSeek가 새로운 AI 모델 DeepSeek-V4-Pro를 공개하고, 개발자 대상 할인과 API 가격 인하를 내세웠습니다.

이번 이슈에서 중요한 점은 단순한 신모델 출시가 아닙니다. DeepSeek는 고성능 모델 경쟁에 참여하면서 동시에 가격 인하 전략을 펼치고 있습니다. 이는 OpenAI, Anthropic, Google 등 기존 강자들에게도 가격 압박으로 작용할 수 있습니다.

AI 모델 시장이 점점 스마트폰 시장처럼 바뀌는 모습입니다. 고성능 프리미엄 모델과 저렴한 실용형 모델이 나뉘고, 기업 고객은 성능뿐 아니라 비용 대비 효율을 따지게 됩니다.


2. OpenAI, GPT-5.5 공개

OpenAI는 GPT-5.5를 공개했습니다. GPT-5.5는 코딩, 리서치, 데이터 분석, 문서 작업 등 실제 업무 수행 능력을 강조한 모델입니다.

이제 AI 모델의 핵심 경쟁력은 단순 대화 능력이 아닙니다. 사용자가 요청한 작업을 얼마나 정확하게 이해하고, 여러 단계를 거쳐 실제 결과물로 완성할 수 있는지가 중요해지고 있습니다.

특히 개발자와 기업 입장에서는 AI가 단순 답변 도구가 아니라 코딩 보조, 분석 도구, 업무 자동화 도구로 자리 잡고 있습니다.


3. Google과 Amazon, Anthropic에 대규모 투자

Google 모회사 Alphabet은 Anthropic에 최대 400억 달러 투자를 추진하는 것으로 알려졌습니다. Amazon 역시 Anthropic과의 협력을 확대하며 AWS 인프라 사용 계약을 강화했습니다.

Anthropic은 Claude 모델로 잘 알려진 AI 기업입니다. 최근 빅테크 기업들이 Anthropic에 집중하는 이유는 단순합니다. AI 모델 경쟁에서 살아남기 위해서는 막대한 컴퓨팅 자원이 필요하기 때문입니다.

AI 기업은 모델만 잘 만들어서는 부족합니다. 클라우드, 데이터센터, AI 반도체, 전력 인프라까지 확보해야 합니다. 결국 AI 산업의 경쟁은 소프트웨어 경쟁이면서 동시에 인프라 경쟁입니다.


4. SK하이닉스, AI 메모리 수요로 주목

AI 반도체 수요가 커지면서 한국 기업도 직접적인 영향을 받고 있습니다. SK하이닉스는 AI 메모리 수요에 대응하기 위해 국내 신규 공장 투자를 발표했고, AI 수요 기대감으로 주가도 강세를 보였습니다.

특히 HBM과 같은 고대역폭 메모리는 AI 서버와 데이터센터에 필수적인 부품입니다. 생성형 AI 서비스가 확산될수록 더 많은 GPU와 메모리가 필요해집니다.

한국 입장에서는 AI 서비스 기업보다 반도체 공급망에서 더 큰 기회를 얻을 가능성이 있습니다. AI 경쟁이 치열해질수록 SK하이닉스, 삼성전자 같은 메모리 기업의 역할도 커질 수 있습니다.


5. Samsung SDS·LG CNS, ChatGPT Edu 국내 판매권 확보

Samsung SDS와 LG CNS는 OpenAI와의 파트너십을 통해 한국에서 ChatGPT Edu 판매권을 확보했습니다.

ChatGPT Edu는 교육기관을 대상으로 한 AI 서비스입니다. 이 이슈는 국내 대학과 교육기관의 AI 도입이 본격화될 수 있다는 점에서 의미가 있습니다.

앞으로 AI는 개인이 구독해서 쓰는 도구를 넘어, 학교·기업·공공기관 단위로 도입되는 인프라가 될 가능성이 큽니다. 특히 교육 분야에서는 과제 보조, 연구 지원, 행정 자동화, 맞춤형 학습 등에 활용될 수 있습니다.


6. EU, Meta의 WhatsApp AI 정책에 제동

EU는 Meta가 WhatsApp에서 경쟁 AI 서비스의 접근을 제한하거나 수수료를 부과하는 방식에 대해 반독점 문제를 제기했습니다.

이 이슈는 앞으로 AI 경쟁이 플랫폼 접근권 싸움으로 번질 수 있다는 점을 보여줍니다. 메신저, 검색, 운영체제, 앱스토어 같은 플랫폼을 가진 기업이 자사 AI를 우선 노출하면, 경쟁 AI 기업은 사용자에게 접근하기 어려워질 수 있습니다.

즉, AI 경쟁은 모델 성능만으로 결정되지 않습니다. 사용자가 어떤 플랫폼에서 어떤 AI를 만나게 되는지도 중요한 경쟁 요소가 됩니다.


정리: AI 산업은 어디로 가고 있나?

최근 AI 뉴스를 종합하면 다음과 같은 흐름이 보입니다.

첫째, AI 모델 가격 경쟁이 본격화되고 있습니다.
DeepSeek의 가격 인하는 AI 모델 시장의 수익 구조에 영향을 줄 수 있습니다.

둘째, 업무형 AI 모델 경쟁이 심화되고 있습니다.
OpenAI GPT-5.5처럼 코딩, 리서치, 분석, 문서 작업에 특화된 모델이 중요해지고 있습니다.

셋째, AI 인프라 투자가 폭발적으로 증가하고 있습니다.
Google, Amazon, Anthropic의 협력은 AI 산업이 클라우드와 데이터센터 중심으로 움직이고 있음을 보여줍니다.

넷째, 한국은 AI 반도체 공급망에서 중요한 위치를 차지하고 있습니다.
SK하이닉스의 AI 메모리 투자는 한국 기업이 글로벌 AI 경쟁에서 중요한 역할을 할 수 있음을 보여줍니다.

다섯째, AI는 교육과 기업 시장으로 확산되고 있습니다.
ChatGPT Edu 국내 판매권 확보는 한국 교육기관의 AI 도입 확대 가능성을 보여줍니다.

여섯째, AI 규제와 플랫폼 독점 문제가 커지고 있습니다.
EU와 Meta의 갈등은 앞으로 AI 서비스가 공정하게 유통될 수 있는지에 대한 논쟁으로 이어질 수 있습니다.


결론

AI 산업은 이제 단순한 챗봇 경쟁을 넘어섰습니다. 모델 성능, 가격, 클라우드 인프라, 반도체, 교육 시장, 규제까지 모두 연결된 거대한 산업 경쟁으로 바뀌고 있습니다.

앞으로 AI 뉴스를 볼 때는 “어떤 모델이 더 뛰어난가”만 볼 것이 아니라, 그 모델을 운영하는 데 필요한 반도체, 데이터센터, 클라우드, 플랫폼, 규제 환경까지 함께 살펴봐야 합니다.

AI 전쟁의 진짜 핵심은 이제 모델 그 자체가 아니라, 모델을 둘러싼 전체 생태계입니다.


출처 참고:
- Reuters: DeepSeek, 새 AI 모델 가격 인하
- OpenAI: GPT-5.5 공개
- Reuters: Google의 Anthropic 투자 추진
- AP: Anthropic의 AWS 사용 계약
- Reuters: SK하이닉스 AI 메모리 투자 및 주가 강세
- Yonhap: Samsung SDS·LG CNS, ChatGPT Edu 국내 판매권 확보
- Reuters: EU의 Meta WhatsApp AI 반독점 조치


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

요약 한 줄
쿠키는 브라우저가 들고 다니는 작은 메모, 세션은 서버가 기억하고 쿠키(JSESSIONID)로 찾아오는 저장공간입니다. 로그인/장바구니는 보통 “세션”으로, “아이디 기억하기”는 “쿠키”로!
 

목차

  1. 쿠키 vs 세션 한눈 비교
  2. 쿠키: 브라우저가 저장하는 메모
  3. 세션: 서버가 관리하는 사용자 상태
  4. 로그인 흐름(쿠키·세션 조합) 실전 이해
  5. 보안 옵션(HttpOnly/Secure/SameSite) 핵심
  6. 서블릿/톰캣 기준 코드 예제
  7. 빈출 질문 & 실수 모음
  8. 체크리스트(면접/시험 대비)

1) 쿠키 vs 세션 한눈 비교

구분                                       쿠키(Cookie)                                                                                           세션(Session)

저장 위치클라이언트(브라우저)서버(메모리/세션 저장소)
식별 방식Cookie 헤더로 키-값 전달JSESSIONID 쿠키로 세션을 조회
수명Max-Age/Expires로 지정(영구/단기)유휴 시간(예: 30분) 지나면 만료
용도아이디 기억, 다크모드, A/B 실험 등로그인 상태, 장바구니, 권한 정보
보안탈취 위험 → HttpOnly/Secure/SameSite 필수서버 보안 중심, 세션 고정 공격 주의
용량도메인당 수 KB 수준서버 자원 사용(사용자 수↑ = 메모리↑)
 

포인트: “민감한 정보는 쿠키에 절대 직접 저장하지 말고, 세션에 저장하고 식별용 세션ID만 쿠키로!”


2) 쿠키: 브라우저가 저장하는 메모

  • 구성: name=value; Path=/; Domain=.example.com; Max-Age=2592000; Secure; HttpOnly; SameSite=Lax
  • 수명
    • 세션 쿠키: 브라우저 종료 시 삭제(수명 미지정)
    • 영속 쿠키: Max-Age/Expires 지정
  • 도메인/경로: 전송 범위를 최소화하면 노출면을 줄여 보안↑
  • 보안 옵션
    • HttpOnly: JS로 접근 차단(XSS 완화)
    • Secure: HTTPS에서만 전송
    • SameSite: 크로스사이트 전송 제어(Lax 기본 추천, 제3자 필요 시 None; Secure)

3) 세션: 서버가 관리하는 사용자 상태

  • 생성: 로그인 성공 → HttpSession에 사용자 정보를 저장
  • 식별: 컨테이너가 발급한 랜덤한 세션ID를 쿠키(JSESSIONID)로 클라이언트에 심음
  • 유지: 요청마다 JSESSIONID로 세션을 찾아서 상태를 이어감
  • 만료: 유휴 시간 초과 or 명시적 invalidate()
  • 확장: 서버 다중대응 시 스티키 세션 또는 외부 세션 저장소(예: Redis) 고려

4) 로그인 흐름(쿠키·세션 조합)

  1. 사용자가 로그인 폼 제출 → 서버에서 인증 성공
  2. HttpSession에 loginUser 저장, 세션ID 발급 → Set-Cookie: JSESSIONID=...
  3. 이후 요청에서 브라우저는 JSESSIONID를 자동 전송 → 서버는 해당 세션으로 사용자 식별
  4. “아이디 기억하기”는 별도 쿠키(rememberId)로 저장(민감정보 X)

5) 보안 옵션 핵심 정리

  • 항상 HttpOnly + Secure(HTTPS 전제)
  • SameSite=Lax 기본. 외부 리다이렉트/결제 등 크로스사이트 필요 시 SameSite=None; Secure
  • 세션 고정 방지: 로그인 성공 시 세션 재발급(invalidate 후 새 세션)
  • 쿠키 범위 최소화: Path, Domain 축소
  • 민감정보(토큰·개인정보) 쿠키 값에 직접 저장 금지(서버 세션 or 안전 저장소 사용)

6) 서블릿/톰캣 기준 코드 예제

6-1. 세션 사용 (로그인 성공 시)

// 로그인 검증 이후
HttpSession session = req.getSession();               // 없으면 생성
session.setAttribute("loginUser", user);              // 사용자 객체/ID 등
session.setMaxInactiveInterval(30 * 60);             // 30분 비활성 시 만료

// 필요 시 세션 고정 방지: 새 세션 발급
session.invalidate();
HttpSession newSession = req.getSession(true);
newSession.setAttribute("loginUser", user);

6-2. 로그아웃

HttpSession session = req.getSession(false); // 있으면 가져오고, 없으면 null
if (session != null) {
    session.invalidate();                    // 세션 파기
}
resp.sendRedirect("/");                      // 홈으로

6-3. “아이디 기억하기” 쿠키

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import jakarta.servlet.http.Cookie;

// 로그인 폼에서 remember 체크 시
String userId = req.getParameter("userId");
Cookie remember = new Cookie("rememberId",
        URLEncoder.encode(userId, StandardCharsets.UTF_8));
remember.setPath("/");                // 필요한 최소 경로
remember.setMaxAge(60 * 60 * 24 * 30);// 30일
remember.setHttpOnly(true);           // JS 접근 차단
remember.setSecure(true);             // HTTPS에서만
resp.addCookie(remember);

6-4. SameSite 설정(서블릿 표준 속성 미지원 시 헤더로)

// Some containers still need manual header for SameSite
String cookie = "rememberId=" + URLEncoder.encode(userId, StandardCharsets.UTF_8)
        + "; Max-Age=2592000; Path=/; HttpOnly; Secure; SameSite=Lax";
resp.setHeader("Set-Cookie", cookie);

6-5. 쿠키 읽기

Cookie[] cookies = req.getCookies();
if (cookies != null) {
    for (Cookie c : cookies) {
        if ("rememberId".equals(c.getName())) {
            String id = java.net.URLDecoder.decode(c.getValue(), StandardCharsets.UTF_8);
            // 폼 기본값으로 채우기 등 활용
        }
    }
}

7) 빈출 질문 & 실수 모음

  • Q. 세션은 어디 저장되나요?
    A. 기본은 서버 메모리(컨테이너). 규모가 커지면 외부 저장소/세션 클러스터링.
  • Q. 쿠키에 토큰/JWT 넣어도 되나요?
    A. 가능하나 HttpOnly+Secure+SameSite를 지키고, 만료·회수 전략 및 XSS/CSRF 대비 필수.
  • Q. 로드밸런싱에서 세션이 사라져요!
    A. 스티키 세션(같은 서버로 라우팅) 또는 공유 세션 저장소를 사용하세요.
  • 실수1: 쿠키에 개인정보/권한을 평문으로 저장
  • 실수2: HttpOnly/Secure 빠짐
  • 실수3: 로그인 시 세션 재발급 미흡(세션 고정 취약점)
  • 실수4: SameSite 미설정으로 외부 연동 시 쿠키 전송 안 돼서 로그인 깨짐

8) 체크리스트(면접/시험 대비)

  • 쿠키/세션 차이 10초 안에 설명 가능?
  • JSESSIONID 역할 & 세션 재발급 타이밍 이해?
  • HttpOnly/Secure/SameSite 각각의 의미/기본값/언제 쓰는지?
  • 분산 환경에서 세션 유지 전략(스티키 vs 저장소) 장단점?

썸네일/타이틀 이미지

  • 파일: session_cookie_title.png
  • 컨셉: 쿠키 캐릭터가 “세션” 메모를 브라우저에게 전달하는 귀여운 낙서 스타일
  • 티스토리 글 상단에 업로드 후 대표 이미지로 설정하세요.
728x90
반응형
728x90
반응형

📖 본문

1. 크리티컬 렌더링 패스란?

브라우저가 HTML, CSS, JavaScript를 해석하고,
**사용자가 볼 수 있는 화면(Pixel)**으로 만드는 전체 과정을 말합니다.
즉, “코드를 → 화면”으로 바꾸는 여정이죠.


2. 단계별 과정

① HTML 파싱 → DOM 생성

  • 브라우저가 HTML 파일을 위에서부터 한 줄씩 읽음
  • 태그를 구조화하여 DOM(Document Object Model) 트리로 만듦

② CSS 파싱 → CSSOM 생성

  • <link> 또는 <style> 태그의 CSS를 해석
  • CSS 규칙을 트리 구조로 만들어 CSSOM 형성

③ Render Tree 생성

  • DOM + CSSOM 결합 → 렌더 트리(Render Tree) 생성
  • 실제 화면에 보여질 요소와 스타일 정보만 포함

④ 레이아웃(Layout)

  • 각 요소의 크기와 위치 계산
  • 반응형이라면 화면 크기에 맞춰 재계산

⑤ 페인팅(Paint)

  • 색상, 그림자, 이미지 등을 픽셀 단위로 채움

⑥ 합성(Composite)

  • 여러 레이어를 합쳐 최종 화면을 그림

3. 중요한 점 — 렌더링 속도에 영향을 주는 요소

  • CSS와 JS 로드 순서
    • CSS가 늦게 로드되면 렌더링이 지연됨
    • JS는 DOM 파싱을 멈추게 할 수 있음 (defer, async 활용)
  • 이미지 용량
    • 용량이 크면 페인팅에 시간 많이 걸림
  • 리플로우 / 리페인트 최소화
    • DOM 구조나 스타일 변경이 많으면 속도 저하

4. 브라우저 렌더링 흐름 그림

HTML → DOM 생성
CSS → CSSOM 생성
DOM + CSSOM → Render Tree 생성
Render Tree → Layout(크기·위치 계산)
Layout → Paint(색칠)
Paint → Composite(합성)

📌 정리 표

단계                                                                                   설명

DOM 생성 HTML 구조 파싱
CSSOM 생성 CSS 해석
Render Tree DOM+CSSOM 결합
Layout 요소 크기·위치 계산
Paint 픽셀 색칠
Composite 화면 합성
728x90
반응형

+ Recent posts