Java 백엔드 면접 준비

Java 면접 질문과 답변 2026

백엔드 개발자를 위한 Java 면접 질문 30개 세트로, 언어 기초와 JVM 동작부터 컬렉션, Stream API, 동시성까지 다룹니다.

Java 모의 면접 시작신용카드가 필요하지 않습니다. 무료 세션 1회 제공.
영어 기술 면접 연습비원어민이 기술 면접 통과를 연습할 수 있는 모드입니다.

Java와 JVM

1Java란 무엇이며 왜 플랫폼 독립적이라고 여겨지나요?

Java는 정적 타입 범용 프로그래밍 언어입니다. 소스 코드는 바이트코드로 컴파일되며, 이 바이트코드는 JVM(Java Virtual Machine) 위에서 실행됩니다. 호환되는 JVM만 제공된다면 동일한 .class 또는 .jar 파일을 재컴파일하지 않고도 서로 다른 운영체제에서 실행할 수 있습니다. 다만 이것이 절대적인 독립성을 뜻하지는 않습니다. 네이티브 라이브러리, 플랫폼 고유의 파일 경로, 시스템 명령어, 운영체제별 동작 차이 등으로 인해 애플리케이션이 플랫폼에 종속될 수도 있습니다.

Java source -> javac -> bytecode -> JVM -> machine code
AI 코치와 함께 이 질문에 답해 보세요

2JVM (Java Virtual Machine), JRE (Java Runtime Environment), JDK (Java Development Kit)의 차이점은 무엇인가요?

JVM은 바이트코드를 로드하고 실행하며 메모리, 스레드, 가비지 컬렉션을 관리합니다. 과거에 JRE는 JVM과 애플리케이션 실행에 필요한 라이브러리를 합친 것을 의미했습니다. JDK는 런타임뿐만 아니라 javac, java, 디버거, javadoc, jar, 진단 도구와 같은 개발 도구를 포함합니다. 최신 배포판에서는 애플리케이션이 맞춤형 런타임 이미지와 함께 배포되는 경우가 많아 별도의 JRE를 사용하는 일은 드뭅니다.

JDK = development tools + runtime
Runtime = JVM + standard libraries
AI 코치와 함께 이 질문에 답해 보세요

3JVM (Java Virtual Machine)은 Java 코드를 어떻게 실행하며, JIT (Just-In-Time compilation) 컴파일러는 왜 필요한가요?

javac가 소스 코드를 바이트코드로 컴파일합니다. JVM은 클래스를 로드하고 바이트코드 명령어를 인터프리트하거나 실행하기 시작합니다. 런타임에 자주 실행되는 코드 경로는 JIT 컴파일러에 의해 감지되어 최적화된 기계어로 컴파일됩니다. JIT는 메서드 인라인화, 루프 최적화, 탈출 분석(escape analysis) 적용, 불필요한 메모리 할당 제거 등을 수행할 수 있습니다. 이러한 동작 덕분에 Java 애플리케이션은 웜업(warm-up) 후 성능이 향상될 수 있으며, JMH 없이 작성된 마이크로벤치마크는 오해를 불러일으키기 쉽습니다.

AI 코치와 함께 이 질문에 답해 보세요

4ClassLoader는 어떻게 동작하나요?

ClassLoader는 클래스 정의를 JVM (Java Virtual Machine)으로 로드합니다. 일반적인 로더로는 Bootstrap, Platform, Application 및 사용자 정의 로더가 있습니다. 클래스는 완전한 패키지 경로를 포함한 이름뿐만 아니라 해당 클래스를 로드한 ClassLoader에 의해서도 식별되므로, 서로 다른 로더에서 로드된 이름이 같은 두 클래스는 서로 다른 타입이 될 수 있습니다. Java는 보통 부모 위임(parent delegation) 방식을 사용합니다. 로더가 부모 로더에 먼저 로드를 요청함으로써 플랫폼 클래스가 임의로 대체될 위험을 줄입니다.

Loading
Linking: verification, preparation, resolution
Initialization
AI 코치와 함께 이 질문에 답해 보세요

5JVM (Java Virtual Machine) 메모리는 어떻게 구성되어 있나요?

JVM의 주요 메모리 영역으로는 객체와 배열을 저장하는 힙(Heap), 각 스레드의 호출 프레임을 보관하는 자바 스택, 클래스 메타데이터를 저장하는 메타스페이스(Metaspace), JIT (Just-In-Time compilation) 컴파일된 코드를 위한 코드 캐시, PC 레지스터, 그리고 네이티브 스택 등이 있습니다. 원시 타입(primitive)은 항상 스택에 저장되고 객체는 항상 힙에 저장된다는 식의 지나친 단순화는 피해야 합니다. 원시 타입 필드는 자신이 속한 객체 내부에 저장되며, JIT의 탈출 분석(escape analysis)을 통해 힙 할당이 제거되거나 객체의 표현 방식이 최적화될 수도 있습니다.

AI 코치와 함께 이 질문에 답해 보세요

6가비지 컬렉터(Garbage Collector)는 어떻게 동작하나요?

가비지 컬렉터는 GC Root에서 더 이상 도달할 수 없는 객체가 차지하는 메모리를 해제합니다. Root에는 활성 메서드의 지역 변수, 정적 필드, 활성 스레드, JNI(Java Native Interface) 참조 등이 포함됩니다. 대다수의 컬렉터는 대부분의 새로운 객체가 생성 직후 빠르게 소멸한다는 가설에 기반하여 힙 메모리를 영(young) 세대와 올드(old) 세대로 나누어 관리합니다. GC는 파일이나 네트워크 연결을 결정론적으로 닫아주지 않으므로, 외부 리소스는 try-with-resources를 사용해 직접 해제해야 합니다.

try (var stream = Files.newInputStream(path)) {
    // use stream
}
AI 코치와 함께 이 질문에 답해 보세요

OOP와 타입

7Java가 지원하는 4가지 OOP (Object-Oriented Programming) 원칙은 무엇인가요?

Java는 캡슐화, 추상화, 상속, 다형성을 지원합니다. 캡슐화는 공개 API 뒤로 내부 상태를 숨깁니다. 추상화는 핵심적인 동작에 집중합니다. 상속은 기존 타입을 기반으로 새로운 타입을 생성합니다. 다형성은 코드가 공유된 규약(contract)을 통해 다양한 구현체와 동작할 수 있도록 합니다. 실무에서는 결합도를 낮추기 위해 상속보다 컴포지션(합성)을 사용하는 것이 권장되는 경우가 많습니다.

interface PaymentService {
    void pay(BigDecimal amount);
}

class CardPaymentService implements PaymentService {
    @Override
    public void pay(BigDecimal amount) {
        // payment implementation
    }
}
AI 코치와 함께 이 질문에 답해 보세요

8메서드 오버로딩과 오버라이딩의 차이점은 무엇인가요?

오버로딩은 메서드 이름은 같지만 매개변수가 다른 여러 메서드를 정의하는 것을 의미하며, 컴파일러가 정적 인자 타입을 기준으로 호출할 메서드를 선택합니다. 오버라이딩은 하위 클래스가 상위 클래스의 인스턴스 메서드 구현을 재정의하는 것을 의미하며, 런타임 디스패치가 실제 객체 타입을 기반으로 실행할 메서드를 선택합니다. static 메서드는 오버라이드되는 것이 아니라 숨겨지는(shadowing/hiding) 것이며, private 메서드는 접근 가능한 멤버로 상속되지 않으므로 오버라이드할 수 없습니다.

void send(String message) {}
void send(String message, int priority) {}

class Animal {
    void speak() { System.out.println("Animal"); }
}

class Dog extends Animal {
    @Override
    void speak() { System.out.println("Dog"); }
}
AI 코치와 함께 이 질문에 답해 보세요

9인터페이스와 추상 클래스의 차이점은 무엇인가요?

인터페이스는 계약을 정의하며 추상 메서드, 디폴트 메서드, 정적 메서드, 상수, 그리고 private 헬퍼 메서드를 포함할 수 있습니다. 반면 추상 클래스는 이에 더해 인스턴스 상태, 생성자, protected 필드, 공유 구현, 추상 또는 구체 메서드를 가질 수 있습니다. 클래스는 하나의 클래스만 상속할 수 있지만 인터페이스는 여러 개를 구현할 수 있습니다. 독립적인 계약에는 인터페이스를 사용하고, 구현과 상태를 공유하는 밀접하게 관련된 타입들에는 추상 클래스를 사용합니다.

interface Repository<T> {
    Optional<T> findById(long id);
}
AI 코치와 함께 이 질문에 답해 보세요

10final 키워드는 어떤 의미를 갖나요?

final 변수는 값을 한 번만 할당할 수 있지만, 참조가 final이라고 해서 참조 대상 객체 자체가 불변(immutable)이 되는 것은 아닙니다. final 메서드는 오버라이드할 수 없으며, final 클래스는 상속(확장)할 수 없습니다. final은 코드의 의도를 명확히 전달하고 참조 변경이나 상속을 제한하지만, 그 자체만으로 완전한 불변 모델이나 스레드 안전성(thread safety)을 제공하지는 않습니다.

final int maxAttempts = 3;

final List<String> names = new ArrayList<>();
names.add("Alice"); // allowed

public final void validate() {}

public final class Token {}
AI 코치와 함께 이 질문에 답해 보세요

11Java에서 레코드(record)란 무엇인가요?

레코드(record)는 불변 데이터 객체를 간결하게 정의하기 위한 문법입니다. 컴파일러가 private final 필드, 생성자, 접근자(accessor), equals, hashCode, toString을 자동으로 생성합니다. 레코드는 DTO(Data Transfer Object), 값 객체(value object), 메서드 반환 결과 등에 유용합니다. 하지만 모든 도메인 엔티티를 대체할 수는 없습니다. 콤팩트 생성자(compact constructor)에서 유효성 검증을 추가할 수는 있지만, 복잡한 가변 동작, 지연 로딩(lazy loading), 상속 기반 모델에는 레코드가 잘 맞지 않습니다.

public record User(long id, String name) {}

public record Order(List<String> items) {
    public Order {
        items = List.copyOf(items);
    }
}
AI 코치와 함께 이 질문에 답해 보세요

12Java에서 불변 객체(immutable object)는 어떻게 만드나요?

클래스를 final로 선언하거나 상속을 엄격히 제한하고, 필드를 private final로 선언하며, setter 메서드를 제공하지 않고 생성자에서 유효성 검사를 수행합니다. 변경 가능한(mutable) 입력값에 대해서는 방어적 복사를 수행하고, 조회 시에도 복사본이나 수정 불가능한 뷰(unmodifiable view)를 반환해야 합니다. 깊은 불변성(deep immutability)을 보장하려면 모든 중첩된 상태 역시 불변이어야 합니다. final 필드는 안전한 초기화 및 공개(safe publication)를 돕지만, 변경 가능한 컬렉션이나 중첩 객체를 외부로 노출하면 해당 객체는 논리적으로 여전히 변경 가능할 수 있습니다.

public final class User {
    private final String name;
    private final List<String> roles;

    public User(String name, List<String> roles) {
        this.name = name;
        this.roles = List.copyOf(roles);
    }

    public List<String> getRoles() {
        return roles;
    }
}
AI 코치와 함께 이 질문에 답해 보세요

참조, 문자열 및 비교

13Java는 값에 의한 전달(pass by value) 방식을 사용하나요, 아니면 참조에 의한 전달(pass by reference) 방식을 사용하나요?

Java는 항상 인수를 값에 의해 전달(pass by value)합니다. 기본형(primitive)의 경우 값 자체가 복사됩니다. 객체의 경우 참조값(주소값)이 복사되어 전달됩니다. 메서드 내부에서는 복사된 참조를 통해 객체의 상태를 변경할 수 있지만, 호출자의 원본 참조가 가리키는 대상을 다른 객체로 교체할 수는 없습니다. 이러한 차이 때문에 객체의 필드를 변경하면 호출자에게 반영되지만, 매개변수에 새로운 객체를 할당하더라도 호출자 쪽에는 아무런 영향을 주지 않습니다.

void change(int value) {
    value = 100;
}

void rename(User user) {
    user.setName("Kate");
}

void replace(User user) {
    user = new User("New");
}
AI 코치와 함께 이 질문에 답해 보세요

14== 연산자와 equals() 메서드의 차이점은 무엇인가요?

원시 타입(primitive type)의 경우 ==는 값을 비교합니다. 객체 참조의 경우 ==는 두 참조가 동일한 객체를 가리키고 있는지 비교합니다. equals()는 해당 클래스에서 올바르게 구현되었을 때 논리적 동등성을 비교하는 메서드입니다. String, Integer, LocalDate를 비롯한 수많은 값 기반 클래스들은 equals를 재정의(override)합니다. 사용자 정의 도메인 객체의 경우 equals는 의도된 식별자 또는 값의 의미를 반영하도록 구현되어야 합니다.

int a = 10;
int b = 10;
System.out.println(a == b); // true

String first = new String("java");
String second = new String("java");

System.out.println(first == second);      // false
System.out.println(first.equals(second)); // true
AI 코치와 함께 이 질문에 답해 보세요

15equals와 hashCode 규약은 무엇인가요?

두 객체가 `equals` 비교 결과 같다면 반드시 동일한 `hashCode`를 반환해야 합니다. 그 역은 성립하지 않아도 되므로 서로 다른 객체가 동일한 `hashCode`를 가질 수 있습니다. `equals`는 반사성, 대칭성, 추이성, 일관성을 만족해야 하며, `null`에 대해서는 `false`를 반환해야 합니다. 이 규약을 위반하면 `HashMap`, `HashSet` 등 해시 기반 컬렉션의 동작이 깨져, 객체가 컬렉션에 실제로 존재하더라도 찾지 못하는 문제가 발생할 수 있습니다.

@Override
public boolean equals(Object object) {
    if (this == object) return true;
    if (!(object instanceof User other)) return false;
    return id == other.id;
}

@Override
public int hashCode() {
    return Long.hashCode(id);
}
AI 코치와 함께 이 질문에 답해 보세요

16String은 왜 불변(immutable)인가요?

String은 안전성, 예측 가능성, 성능상의 이유로 불변입니다. 불변성을 통해 문자열 인턴(string interning), 스레드 간 안전한 공유, 신뢰할 수 있는 hashCode 캐싱이 가능해지며, 파일 경로, URL, 클래스 이름 및 보안에 민감한 값에 안전하게 사용할 수 있습니다. concat, replace, substring과 같은 연산은 모두 새로운 문자열을 생성합니다. Java 9부터 String은 내부적으로 메모리 절약을 위한 컴팩트 표현(compact strings)에 byte[]를 사용할 수 있지만, 외부로 공개된 불변 계약은 그대로 유지됩니다.

String value = "Java";
value.toUpperCase();

System.out.println(value); // Java

String a = "java";
String b = "java";

System.out.println(a == b); // often true due to interning
AI 코치와 함께 이 질문에 답해 보세요

17String, StringBuilder, StringBuffer의 차이점은 무엇인가요?

String은 불변(immutable) 객체이므로, 컴파일러가 단순한 경우는 최적화할 수 있지만 반복문 내에서 문자열을 계속 연결하면 다수의 중간 객체가 생성될 수 있습니다. StringBuilder는 변경 가능(mutable)하며 단일 스레드 환경에서 문자열을 조합할 때 권장됩니다. StringBuffer는 이와 유사하지만 동기화(synchronized) 처리가 되어 있어 일반적으로 속도가 더 느리며, 주로 스레드 안전성이 필요한 레거시 API에서 사용됩니다. 서식 지정의 경우 String.format이나 템플릿 방식의 API가 가독성 면에서는 더 명확할 수 있으나, 항상 더 빠른 것은 아닙니다.

StringBuilder builder = new StringBuilder();

for (String item : items) {
    builder.append(item).append(',');
}

String result = builder.toString();

String message = "Hello, " + name;
AI 코치와 함께 이 질문에 답해 보세요

제네릭과 컬렉션

18제네릭(Generics)이란 무엇이며, 타입 소거(type erasure)란 무엇인가요?

제네릭을 사용하면 `List<String>`과 같이 타입 안정성을 갖춘 컨테이너와 API를 표현할 수 있습니다. 컴파일 시점에 컴파일러가 타입을 검사하고 필요한 위치에 형변환 코드를 삽입합니다. 런타임에는 대부분의 제네릭 타입 정보가 소거되므로 `List<String>`과 `List<Integer>`는 런타임에 동일한 클래스를 공유합니다. 이 때문에 `new T()`를 직접 생성하거나, 원시 타입(primitive type) 파라미터를 사용하거나, `instanceof List<String>`과 같은 검사를 안정적으로 수행할 수 없습니다.

List<String> names = new ArrayList<>();
names.add("Alice");

List<? extends Number> producer;
List<? super Integer> consumer;
AI 코치와 함께 이 질문에 답해 보세요

19Java Collections Framework란 무엇인가요?

Collections Framework는 여러 객체의 그룹을 저장하고 처리하기 위한 인터페이스와 구현체의 집합입니다. 핵심 인터페이스로는 `List`, `Set`, `Queue`, `Deque`, `Map` 등이 있습니다. 구현체로는 `ArrayList`, `LinkedList`, `HashSet`, `TreeSet`, `HashMap`, `TreeMap`, `PriorityQueue` 등이 있습니다. 접근 패턴, 순서 보장 여부, 고유성(중복 허용 여부), 탐색 시간 복잡도, 메모리 오버헤드, 동시성 요구사항에 따라 적절한 컬렉션을 선택해야 합니다.

List<String> list = new ArrayList<>();
Set<Long> ids = new HashSet<>();
Map<Long, User> usersById = new HashMap<>();
Deque<String> queue = new ArrayDeque<>();
AI 코치와 함께 이 질문에 답해 보세요

20HashMap은 어떻게 동작하나요?

`HashMap`은 키의 해시값을 기반으로 선택된 버킷(bucket)에 엔트리를 저장합니다. 조회 시에는 해시값을 계산하여 해당 버킷을 찾은 뒤 `equals`로 키를 비교합니다. 해시 충돌은 버킷 내부에서 처리되며, 최신 Java에서는 특정 조건을 만족하면 긴 체인이 균형 트리로 변환(treeify)될 수 있습니다. `HashMap`은 하나의 `null` 키와 다수의 `null` 값을 허용하고, 스레드 안전하지 않으며, 부하율(load factor) 임계값을 초과하면 크기 재조정(resize)이 발생합니다.

Map<Long, User> users = new HashMap<>();
users.put(user.id(), user);

User found = users.get(user.id());
AI 코치와 함께 이 질문에 답해 보세요

21HashMap, LinkedHashMap, TreeMap, ConcurrentHashMap의 차이점은 무엇인가요?

HashMap은 정렬되지 않은 빠른 접근을 제공하며 스레드 안전(thread-safe)하지 않습니다. LinkedHashMap은 삽입 순서 또는 접근 순서를 유지하므로 LRU(Least Recently Used) 방식의 캐시를 구현할 때 유용합니다. TreeMap은 자연 순서(natural order) 또는 Comparator에 따라 키를 정렬된 상태로 유지하며 O(log n)의 시간 복잡도를 가집니다. ConcurrentHashMap은 세그먼트 및 내부 동기화 방식을 통해 동시 접근이 가능하도록 설계되었으며, null 키나 값을 허용하지 않고 부하가 높은 환경에서 synchronizedMap보다 선호됩니다.

map.computeIfAbsent(key, ignored -> loadValue());
AI 코치와 함께 이 질문에 답해 보세요

22ArrayList와 LinkedList의 차이점은 무엇인가요?

ArrayList는 내부적으로 배열을 사용합니다. 인덱스를 통한 빠른 접근, 컴팩트한 메모리 구조, 효율적인 순회를 제공하지만, 중간에 요소를 삽입할 때는 기존 요소들을 이동시켜야 합니다. LinkedList는 이중 연결 리스트입니다. 삽입할 노드의 위치를 이미 알고 있다면 삽입 비용이 적지만, 인덱스를 통한 접근은 O(n)이 걸리고 메모리 오버헤드가 크며 캐시 지역성(cache locality)이 떨어집니다. 실제 많은 애플리케이션에서는 중간 삽입이 일부 발생하더라도 ArrayList가 더 빠른 경우가 많습니다.

AI 코치와 함께 이 질문에 답해 보세요

23Comparable과 Comparator의 차이점은 무엇인가요?

Comparable은 클래스 내부에서 compareTo를 통해 자연 순서(natural order)를 정의합니다. Comparator는 외부에서 정렬 순서를 정의하는 별도의 객체 또는 람다입니다. Comparable은 특정 타입에 명확한 기본 정렬 순서가 하나 있을 때 유용합니다. Comparator는 이름순, 날짜순, 우선순위순과 같이 여러 정렬 기준이 필요할 때 더 적합합니다. 정렬된 셋이나 맵에서 사용할 때는 compare와 compareTo가 일관성을 유지해야 하며, 가급적 equals와도 호환되도록 작성하는 것이 좋습니다.

class User implements Comparable<User> {
    private int age;

    @Override
    public int compareTo(User other) {
        return Integer.compare(age, other.age);
    }
}

Comparator<User> byName =
    Comparator.comparing(User::getName);

Comparator<User> byAgeThenName =
    Comparator.comparingInt(User::getAge)
              .thenComparing(User::getName);
AI 코치와 함께 이 질문에 답해 보세요

예외와 리소스

24체크 예외(checked exception)와 언체크 예외(unchecked exception)의 차이점은 무엇인가요?

체크 예외는 Exception을 상속하되 RuntimeException을 상속하지 않으며, 컴파일러가 호출자에게 예외를 처리하거나 선언하도록 요구합니다. 언체크 예외는 RuntimeException을 상속하며 메서드 시그니처에 명시할 필요가 없습니다. 체크 예외는 API 경계에서 복구 가능한 상황에 적합합니다. 언체크 예외는 프로그래밍 오류, 유효하지 않은 상태, 규약 위반에 적합합니다. 최신 서비스 코드에서는 체크 예외가 계층을 거치며 번거롭게 전파되기 때문에 과도한 사용을 피하는 편입니다.

void load() throws IOException {
    // ...
}

throw new IllegalArgumentException("Invalid id");
AI 코치와 함께 이 질문에 답해 보세요

25throw, throws, try-with-resources의 차이점은 무엇인가요?

throw는 예외를 즉시 발생시킵니다. throws는 메서드 시그니처에 발생 가능한 예외를 선언합니다. try-with-resources는 AutoCloseable 리소스를 역순으로 자동 해제합니다. 메인 블록과 close 모두에서 예외가 발생하면 두 번째 예외는 억제된 예외(suppressed exception)로 보존됩니다. 이는 여러 리소스를 다룰 때 수동으로 finally 블록을 작성하는 것보다 훨씬 안전하고 명확합니다.

throw new IllegalArgumentException("Invalid id");

void readFile() throws IOException {}

try (var reader = Files.newBufferedReader(path)) {
    return reader.readLine();
}
AI 코치와 함께 이 질문에 답해 보세요

Stream API와 함수형 스타일

26Stream API란 무엇인가요?

Stream API를 사용하면 데이터 시퀀스를 선언형(declarative)으로 처리할 수 있습니다. filter, map, sorted와 같은 중간 연산은 지연 평가(lazy)되며, toList, collect, reduce, forEach와 같은 최종 연산이 호출될 때 실행이 트리거됩니다. 스트림은 컬렉션이 아니며, 보통 한 번만 소비되고 부수 효과(side effect)가 없는 순수 함수와 함께 사용할 때 가장 적합합니다. parallelStream은 더 빠른 실행을 보장하지 않으며, 오히려 성능을 저하시키거나 정확성을 해칠 수 있습니다.

List<String> names = users.stream()
    .filter(User::isActive)
    .sorted(Comparator.comparing(User::getName))
    .map(User::getName)
    .toList();
AI 코치와 함께 이 질문에 답해 보세요

27Optional이란 무엇이며 언제 사용해야 하나요?

Optional<T>는 결과의 부재(값이 없음)를 명시적으로 나타내며, 메서드 반환 타입으로 사용하기에 적합합니다. 유용한 메서드로는 map, flatMap, filter, orElse, orElseGet, orElseThrow 등이 있습니다. orElse는 인수를 즉시(eager) 평가하는 반면, orElseGet의 Supplier는 값이 없을 때만 실행됩니다. 명확한 이유가 없다면 엔티티 필드, 메서드 매개변수, 직렬화 대상 DTO (Data Transfer Object)에는 Optional 사용을 권장하지 않습니다.

Optional<User> user = repository.findById(id);

String name = user
    .map(User::getName)
    .orElse("Unknown");

optional.orElse(expensiveOperation());
optional.orElseGet(() -> expensiveOperation());
AI 코치와 함께 이 질문에 답해 보세요

동시성

28synchronized, volatile, 그리고 Atomic 클래스는 무엇을 보장하나요?

synchronized는 상호 배제(mutual exclusion)와 happens-before 관계를 보장합니다. volatile은 최신 쓰기 작업의 가시성을 보장하고 명령어 재정렬을 방지하지만, 복합 연산의 원자성까지 보장하지는 않습니다. Atomic 클래스는 incrementAndGet과 같은 개별 연산의 원자성을 제공합니다. 서로 연관된 여러 상태를 변경할 때는 락(lock)이나 synchronized를 사용하고, 단순한 상태 플래그에는 volatile을, 경합(contention)이 발생하는 카운터에는 AtomicInteger나 LongAdder를 사용하는 것이 좋습니다.

synchronized (lock) {
    counter++;
}

private volatile boolean running = true;

AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();
AI 코치와 함께 이 질문에 답해 보세요

29경쟁 상태(race condition), 교착 상태(deadlock), 그리고 스레드 안전성(thread safety)이란 무엇인가요?

경쟁 상태(race condition)는 예측할 수 없는 스레드 실행 순서에 따라 결과가 달라질 때 발생합니다. 교착 상태(deadlock)는 스레드들이 서로가 가진 리소스를 무한히 기다릴 때 발생합니다. 스레드 안전성을 확보하려면 공유 가변 상태를 줄이고, 일관된 락 획득 순서를 유지하며, 임계 구역(critical section)을 최소화하고, 락 내부에서 외부 코드를 호출하지 않아야 합니다. 또한 불변 객체, 동시성 컬렉션, 타임아웃 또는 메시지 전달 방식을 활용할 수 있습니다. 스레드 안전성은 상태에 접근하는 프로토콜 전체의 특성입니다.

AI 코치와 함께 이 질문에 답해 보세요

30ExecutorService, CompletableFuture, 그리고 가상 스레드(virtual thread)의 차이점은 무엇인가요?

ExecutorService는 스레드 풀과 작업 실행을 관리합니다. CompletableFuture는 비동기 작업의 조합을 용이하게 합니다. 가상 스레드는 수많은 블로킹 I/O 작업 처리에 유용한 경량 JVM (Java Virtual Machine) 스레드입니다. 가상 스레드는 요청당 스레드(thread-per-request) 모델을 단순화하지만, CPU 바운드 작업 속도를 높여주지는 않으며, 데이터베이스나 외부 리소스의 한계를 없애거나 가변 상태를 자동으로 스레드로부터 안전하게 만들어 주지는 않습니다.

try (var executor = Executors.newFixedThreadPool(4)) {
    Future<Integer> future = executor.submit(() -> calculate());
    int result = future.get();
}

CompletableFuture<User> userFuture =
    CompletableFuture.supplyAsync(this::loadUser)
        .thenApply(this::enrichUser)
        .exceptionally(error -> fallbackUser());

try (var executor =
         Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> handleRequest());
}
AI 코치와 함께 이 질문에 답해 보세요