자바 면접 빈출 주제 TOP 10 — 핵심 개념과 답변 예시


자바 기술 면접을 앞두고 “도대체 어디서부터 공부해야 하지?”라는 막막함, 한 번쯤 느껴보셨을 겁니다. JVM부터 스트림까지 자바의 학습 범위는 방대하지만, 실제 면접에서는 자바 면접 빈출 주제가 뚜렷하게 반복됩니다. 국내외 주요 IT 기업과 스타트업의 자바 개발자 면접 후기, 기술 블로그, 공개된 면접 가이드를 분석하면 OOP 원칙·JVM 구조·가비지 컬렉션·컬렉션 프레임워크·동시성·스트림·제네릭·예외 처리·equals·hashCode·인터페이스와 추상클래스라는 주제가 압도적으로 자주 등장합니다. 이 글에서는 그 10가지 주제 각각에 대해 면접관이 원하는 핵심 포인트, 모범 답변 구조, 코드 예시를 완전히 정리합니다.


목차

  1. 자바 면접의 평가 방식 — 면접관이 진짜 보는 것
  2. 주제 1~3: OOP·JVM·가비지 컬렉션
  3. 주제 4~6: 컬렉션 프레임워크·동시성·스트림과 람다
  4. 주제 7~9: 제네릭·예외 처리·equals와 hashCode
  5. 주제 10: 인터페이스 vs 추상클래스
  6. 자바 면접 실전 대비 전략 — 공부법과 답변법

1. 자바 면접의 평가 방식 — 면접관이 진짜 보는 것

자바 면접에서 면접관은 단순히 API 이름을 외웠는지를 테스트하지 않습니다. 세 가지를 동시에 평가합니다.

첫째, 원리 이해. “왜 그렇게 설계됐는가”를 설명할 수 있는지 봅니다. ArrayList를 쓴다는 사실보다 내부 구조와 LinkedList 대비 어떤 트레이드오프가 있는지를 묻습니다.

둘째, 실무 연결. 이론을 코드와 실제 상황에 연결할 수 있는지 봅니다. synchronized를 설명하면서 실제로 어떤 상황에서 썼는지, 어떤 문제를 해결했는지를 기대합니다.

셋째, 깊이 탐색. 답변 후 “그렇다면 …”으로 이어지는 꼬리 질문에 어떻게 대응하는지 봅니다. 한 주제를 표면적으로 아는지, 깊이 있게 이해하는지를 확인합니다.

이 세 가지를 염두에 두고 각 주제의 답변을 준비하세요.


2. 주제 1~3: OOP·JVM·가비지 컬렉션

주제 1. 객체지향 프로그래밍(OOP)의 4가지 특성을 설명하고 자바에서 어떻게 구현되는지 말해주세요

빈출 이유: 자바 면접의 출발점이자 거의 모든 면접에서 등장하는 필수 질문입니다. 개념 암기 수준에서 끝내지 않고 자바 코드와 연결해 설명할 수 있는지가 핵심입니다.

H3: 4가지 특성 핵심 정리

① 캡슐화(Encapsulation)

데이터(필드)와 그것을 다루는 메서드를 하나의 단위(클래스)로 묶고, 외부에서 직접 접근하지 못하도록 접근 제어자로 숨기는 것입니다. 정보 은닉(Information Hiding)이 핵심 목적입니다.

java

public class BankAccount {
    // private으로 외부 직접 접근 차단
    private double balance;

    // 공개된 메서드로만 접근 허용
    public void deposit(double amount) {
        if (amount > 0) balance += amount; // 내부 유효성 검증 가능
    }

    public double getBalance() { return balance; }
}

// 외부에서 balance에 직접 접근 불가
// account.balance = -1000; // ❌ 컴파일 오류
account.deposit(500);       // ✅ 메서드를 통해서만 가능

② 상속(Inheritance)

부모 클래스의 필드와 메서드를 자식 클래스가 물려받아 코드를 재사용하는 것입니다. 자바는 extends로 단일 상속만 지원합니다.

java

public class Animal {
    protected String name;
    public void eat() { System.out.println(name + " is eating"); }
}

public class Dog extends Animal {
    public void bark() { System.out.println("Woof!"); }
    // eat() 메서드를 상속받아 재사용
}

③ 다형성(Polymorphism)

같은 타입의 참조 변수로 다양한 구현을 다룰 수 있는 것입니다. 오버라이딩(Override)과 오버로딩(Overload)으로 구현됩니다.

java

// 오버라이딩 기반 다형성 — 런타임에 실제 타입 결정
Animal a1 = new Dog();
Animal a2 = new Cat();
a1.makeSound(); // "Woof!" — Dog의 구현 실행
a2.makeSound(); // "Meow!" — Cat의 구현 실행

// List 인터페이스로 다양한 구현체를 같은 타입으로 다룸
List<String> list = new ArrayList<>();  // 또는 LinkedList, CopyOnWriteArrayList

④ 추상화(Abstraction)

복잡한 내부 구현을 숨기고 핵심적인 기능만 외부에 노출하는 것입니다. 자바에서는 추상 클래스(abstract class)와 인터페이스(interface)로 구현합니다.

면접 심화 포인트: “상속보다 컴포지션을 선호하라는 원칙(Effective Java Item 18)이 있는데, 어떤 상황에서 상속 대신 컴포지션을 쓰시나요?”까지 연결하면 차별화됩니다.


주제 2. JVM(Java Virtual Machine)의 구조와 자바 코드의 실행 과정을 설명해주세요

빈출 이유: “자바는 플랫폼 독립적이다”는 말의 근거이자, GC·메모리·성능 질문으로 이어지는 핵심 주제입니다. JVM을 모르면 자바의 동작을 제대로 설명할 수 없습니다.

H3: JVM 구조와 실행 흐름

자바 코드 실행 과정:

[소스코드] .java 파일
     ↓  javac (자바 컴파일러)
[바이트코드] .class 파일  ← 플랫폼 독립적
     ↓  JVM (Java Virtual Machine)
[실행] OS·하드웨어    ← 플랫폼 의존적 (JVM이 OS별로 존재)

JVM의 핵심 구성 요소:

┌─────────────────────────────────────────┐
│              JVM                        │
│  ┌───────────────────────────────────┐  │
│  │    클래스 로더 (Class Loader)      │  │
│  │  로딩 → 링킹 → 초기화             │  │
│  └───────────────────────────────────┘  │
│  ┌───────────────────────────────────┐  │
│  │      런타임 데이터 영역             │  │
│  │  ┌────────┬────────┬───────────┐  │  │
│  │  │메서드영역│  힙   │ PC레지스터│  │  │
│  │  │(공유)  │(공유)  │(스레드별) │  │  │
│  │  ├────────┴────────┴───────────┤  │  │
│  │  │  스택(스레드별) │ 네이티브스택 │  │  │
│  │  └─────────────────────────────┘  │  │
│  └───────────────────────────────────┘  │
│  ┌───────────┐  ┌────────────────────┐  │
│  │실행 엔진  │  │네이티브 메서드 인터페이스│  │
│  │인터프리터  │  └────────────────────┘  │
│  │JIT 컴파일러│                          │
│  │GC        │                          │
│  └───────────┘                          │
└─────────────────────────────────────────┘

각 영역의 역할:

  • 메서드 영역(Method Area): 클래스 정보, static 변수, 상수 풀, 메서드 바이트코드 저장. JVM 전체에서 공유됩니다. Java 8부터 PermGen이 사라지고 Metaspace(네이티브 메모리)로 대체됐습니다.
  • 힙(Heap): new로 생성된 객체와 배열이 저장됩니다. GC의 주 관리 대상이며 모든 스레드가 공유합니다.
  • 스택(Stack): 각 스레드마다 독립적으로 가집니다. 메서드 호출 시 스택 프레임이 생성되고 지역 변수·매개변수·반환 주소가 저장됩니다.
  • PC 레지스터: 현재 실행 중인 JVM 명령어의 주소를 저장합니다. 각 스레드마다 독립적.
  • JIT 컴파일러: 자주 실행되는 바이트코드를 네이티브 코드로 컴파일해 캐싱합니다. 이것이 “자바가 처음에는 느리다가 점점 빨라지는” 이유입니다.

면접 심화 포인트: “Java 8에서 PermGen이 Metaspace로 바뀐 이유”나 “JIT 컴파일러의 워밍업(Warm-up) 이슈”를 언급하면 깊은 이해를 보여줄 수 있습니다.


주제 3. 가비지 컬렉션(GC)의 동작 원리와 주요 GC 알고리즘을 설명해주세요

빈출 이유: JVM 주제와 자연스럽게 이어지며, 서비스 성능과 직결됩니다. “GC가 뭔지”를 아는 것과 “어떤 GC를 왜 쓰는지”를 아는 것은 완전히 다른 수준입니다.

H3: GC 동작 원리 — 도달 가능성(Reachability)

GC의 핵심 원리는 도달 불가능(Unreachable)한 객체를 메모리에서 제거하는 것입니다. GC Root(스택의 지역 변수, static 변수, JNI 참조 등)에서 시작해 참조를 따라가며 도달 가능한 객체를 표시하고, 나머지를 수거합니다.

힙의 세대별 구조 (Generational GC):

┌─────────────────────────────────────────────┐
│                   힙(Heap)                   │
│  ┌──────────────────────┐  ┌──────────────┐ │
│  │     Young Generation  │  │     Old       │ │
│  │  ┌──────┬─────┬─────┐│  │   Generation  │ │
│  │  │ Eden │ S0  │ S1  ││  │               │ │
│  │  └──────┴─────┴─────┘│  │               │ │
│  └──────────────────────┘  └──────────────┘ │
│     Minor GC 대상              Major GC 대상  │
└─────────────────────────────────────────────┘
  • Eden: 새로 생성된 객체가 들어오는 곳
  • Survivor 0/1: Minor GC 후 살아남은 객체가 이동하며 age가 증가
  • Old Generation: age가 임계값(기본 15)을 초과한 객체가 승격(Promotion)
  • Minor GC: Young 영역만 수거, 빠르고 자주 발생
  • Major(Full) GC: Old 영역까지 수거, 느리고 Stop-The-World 시간이 김

주요 GC 알고리즘:

GC 종류특징적합한 상황기본 적용 버전
Serial GC단일 스레드, STW 김소규모 단일 CPU
Parallel GC멀티스레드, 처리량 최대화배치 처리, 처리량 우선Java 8 기본
G1 GC힙을 Region으로 분할, 예측 가능한 STW대용량 힙, 지연 균형Java 9+ 기본
ZGC거의 STW 없음(< 1ms), 대용량초저지연 요구 서비스Java 15+ 프로덕션
ShenandoahZGC와 유사, Red Hat 개발초저지연Java 12+

Stop-The-World(STW): GC 실행 중 모든 애플리케이션 스레드가 일시 정지하는 현상. 서비스의 응답 지연(Latency Spike)의 주요 원인입니다. 최신 GC(G1, ZGC)는 STW 시간을 최소화하는 방향으로 발전하고 있습니다.

한 줄 요약 답변: “GC는 도달 불가능한 객체를 자동으로 수거하는 메모리 관리 메커니즘으로, 세대별(Young/Old) 가설에 기반해 작동합니다. Java 9부터 기본 G1 GC가 적용되며, 초저지연이 필요한 서비스에는 ZGC를 검토합니다.”


3. 주제 4~6: 컬렉션 프레임워크·동시성·스트림과 람다

주제 4. 자바 컬렉션 프레임워크의 주요 인터페이스와 구현체를 비교해주세요

빈출 이유: 자바 개발자가 매일 사용하는 도구이지만, 내부 구조와 선택 기준을 물어보면 많은 분들이 막힙니다. “어떤 자료구조를 왜 썼나요?”는 단골 꼬리 질문입니다.

H3: 컬렉션 계층 구조

java.util.Collection (인터페이스)
├── List        — 순서 있음, 중복 허용
│   ├── ArrayList    — 동적 배열, 랜덤 접근 O(1)
│   ├── LinkedList   — 이중 연결 리스트, 삽입/삭제 O(1)
│   └── Vector       — 동기화 O, 레거시 (비권장)
├── Set         — 순서 없음(일부 예외), 중복 불허
│   ├── HashSet      — 해시 기반, O(1) 조회
│   ├── LinkedHashSet — 삽입 순서 유지
│   └── TreeSet      — 정렬 순서 유지, O(log n)
└── Queue/Deque — FIFO/양방향
    ├── LinkedList
    ├── ArrayDeque   — 스택/큐 모두에 권장
    └── PriorityQueue — 우선순위 큐, O(log n) 삽입

java.util.Map (별도 인터페이스)
├── HashMap      — 해시 기반, O(1), 순서 없음
├── LinkedHashMap — 삽입/접근 순서 유지
├── TreeMap      — 키 정렬 순서 유지, O(log n)
└── ConcurrentHashMap — 스레드 안전 HashMap

핵심 선택 기준:

java

// 상황별 최적 선택

// 1. 일반 리스트 — ArrayList가 대부분 최선
List<String> list = new ArrayList<>();

// 2. 빠른 조회가 필요한 집합
Set<String> set = new HashSet<>(); // O(1) 조회

// 3. 정렬된 집합이 필요할 때
Set<String> sortedSet = new TreeSet<>(); // 자동 정렬 유지

// 4. 키-값 조회
Map<String, Integer> map = new HashMap<>(); // 일반
Map<String, Integer> sorted = new TreeMap<>(); // 키 정렬

// 5. 멀티스레드 환경
Map<String, Integer> concurrent = new ConcurrentHashMap<>();
// HashMap 절대 사용 금지, Collections.synchronizedMap보다 성능 우수

// 6. 스택 용도
Deque<String> stack = new ArrayDeque<>(); // Stack 클래스 대신
stack.push("a");
stack.pop();

면접 심화 포인트: “HashMap의 초기 용량(capacity)과 로드 팩터(load factor)가 성능에 미치는 영향”이나 “Java 8 이후 HashMap의 체이닝이 8개 초과 시 Red-Black Tree로 전환되는 이유”를 설명하면 깊이 있는 답변이 됩니다.


주제 5. 자바의 동시성(Concurrency) — synchronized, volatile, 그리고 java.util.concurrent를 설명해주세요

빈출 이유: 멀티스레드 환경에서 발생하는 버그는 재현과 디버깅이 가장 어렵습니다. 이 주제를 제대로 이해하는지가 시니어-주니어를 가르는 기준 중 하나입니다.

H3: 가시성과 원자성 — 두 가지 핵심 문제

문제 1 — 가시성(Visibility): 한 스레드가 변경한 값을 다른 스레드가 즉시 볼 수 없는 문제. CPU 캐시와 메인 메모리 불일치가 원인입니다.

문제 2 — 원자성(Atomicity): 복합 연산이 중간에 다른 스레드에 의해 끼어들릴 수 있는 문제. i++는 사실 읽기→증가→쓰기의 세 단계로, 원자적 연산이 아닙니다.

java

// 위험한 코드 — 멀티스레드 환경에서 결과가 10000이 아닐 수 있음
int count = 0;
// 스레드 10개가 각각 1000번 증가 → 기대값: 10000, 실제: 불확실

H3: 세 가지 해결책

① synchronized — 완전한 상호 배제

java

// 메서드 레벨
public synchronized void increment() {
    count++; // 한 번에 하나의 스레드만 실행
}

// 블록 레벨 (더 좁은 범위, 성능 유리)
public void increment() {
    synchronized (this) {
        count++;
    }
}

synchronized는 가시성 + 원자성 모두 보장합니다. 단, 한 번에 하나의 스레드만 실행 가능해 경합이 심하면 성능 병목이 됩니다.

② volatile — 가시성만 보장

java

private volatile boolean running = true;

// 스레드 A
public void stop() { running = false; } // 메인 메모리에 즉시 반영

// 스레드 B
public void run() {
    while (running) { // 항상 메인 메모리에서 최신값 읽음
        doWork();
    }
}

volatile은 가시성만 보장하고 원자성은 보장하지 않습니다. i++ 같은 복합 연산에는 부적합합니다. 플래그 변수처럼 단순 읽기/쓰기에 적합합니다.

③ java.util.concurrent — 실무 권장

java

// AtomicInteger — 원자적 연산 보장 (CAS 기반, lock-free)
AtomicInteger atomicCount = new AtomicInteger(0);
atomicCount.incrementAndGet(); // 원자적 증가, synchronized보다 빠름

// ReentrantLock — synchronized보다 유연한 잠금
ReentrantLock lock = new ReentrantLock();
try {
    lock.lock();
    // 임계 영역
} finally {
    lock.unlock(); // 반드시 finally에서 해제
}

// CountDownLatch — 여러 스레드 완료 대기
CountDownLatch latch = new CountDownLatch(3); // 3개 스레드 대기
// 각 스레드 완료 시 latch.countDown()
latch.await(); // 3개 모두 완료될 때까지 대기

// ExecutorService — 스레드 풀 관리
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> doTask());
executor.shutdown();

선택 기준 요약:

도구보장사용 상황
volatile가시성단순 플래그, 한 스레드 쓰기
synchronized가시성 + 원자성간단한 임계 영역
AtomicXxx원자성 (CAS)단일 변수 원자적 연산
ReentrantLock가시성 + 원자성복잡한 잠금 제어
ConcurrentHashMap스레드 안전멀티스레드 맵

주제 6. 자바 스트림(Stream)과 람다(Lambda)를 설명하고 for-loop과 언제 무엇을 쓰나요?

빈출 이유: Java 8의 가장 큰 변화로, 모던 자바 코딩 스타일의 핵심입니다. 단순히 “스트림이 편하다”가 아니라 내부 동작과 성능 트레이드오프를 이해하는지 봅니다.

H3: 람다와 함수형 인터페이스

java

// 람다 이전 — 익명 클래스
Runnable r1 = new Runnable() {
    @Override
    public void run() { System.out.println("Hello"); }
};

// 람다 이후 — 간결한 표현
Runnable r2 = () -> System.out.println("Hello");

// 핵심 함수형 인터페이스
Predicate<Integer> isEven   = n -> n % 2 == 0;  // T → boolean
Function<String, Integer> len = String::length;   // T → R
Consumer<String> printer    = System.out::println; // T → void
Supplier<List<String>> listFactory = ArrayList::new; // () → T

H3: 스트림의 핵심 특성

지연 평가(Lazy Evaluation):

java

// 이 코드에서 filter, map은 즉시 실행되지 않음
Stream<String> stream = names.stream()
    .filter(n -> { System.out.println("filter: " + n); return n.startsWith("A"); })
    .map(n -> { System.out.println("map: " + n); return n.toUpperCase(); });

// collect() — 최종 연산이 호출될 때 파이프라인 전체가 한 번에 실행
List<String> result = stream.collect(Collectors.toList());
// 실행 순서: filter("Alice") → map("Alice") → filter("Bob") → ...
// 즉, 원소 하나씩 파이프라인 전체를 통과 (전체 filter 후 전체 map이 아님)

단락 연산(Short-circuit):

java

// 조건을 만족하는 첫 번째 원소를 찾으면 파이프라인 즉시 중단
Optional<String> first = names.stream()
    .filter(n -> n.startsWith("Z"))
    .findFirst(); // 찾는 순간 이후 원소는 처리 안 함

실전 활용 패턴:

java

List<Employee> employees = getEmployees();

// 부서별 평균 급여 — 스트림이 빛나는 복합 연산
Map<String, Double> avgSalaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingDouble(Employee::getSalary)
    ));

// 병렬 처리 — 대용량 CPU 집약적 연산
long count = largeList.parallelStream()
    .filter(this::isExpensiveCheck)
    .count();
// 단, 소규모 데이터에서 parallelStream은 오히려 느림 (스레드 오버헤드)

for-loop vs 스트림 선택 기준:

  • break·continue·return 중간 탈출 필요 → for 루프
  • 지역 변수 수정 필요 → for 루프 (스트림 내부에서 외부 변수 수정 불가)
  • 복잡한 다단계 파이프라인, 그룹핑, 집계 → 스트림
  • 성능 최적화가 최우선, 소규모 데이터 → for 루프

4. 주제 7~9: 제네릭·예외 처리·equals와 hashCode

주제 7. 자바 제네릭(Generic)이란 무엇이고, 와일드카드와 타입 경계를 설명해주세요

빈출 이유: 컬렉션·스트림을 쓸 때마다 등장하는 개념이지만, 공변성·반공변성·타입 소거까지 깊이 파면 어렵습니다. 깊이 있는 이해를 확인하는 질문입니다.

H3: 제네릭의 목적과 기본 문법

java

// 제네릭 없는 코드 — 타입 안전하지 않음
List list = new ArrayList();
list.add("hello");
list.add(42);              // 컴파일 오류 없음
String s = (String) list.get(1); // 런타임 ClassCastException 발생

// 제네릭 코드 — 컴파일 타임에 타입 오류 검출
List<String> safeList = new ArrayList<>();
safeList.add("hello");
// safeList.add(42); // ❌ 컴파일 오류 — 런타임 전에 방지
String s = safeList.get(0); // 캐스팅 불필요

H3: 와일드카드와 PECS 원칙

java

// 상한 경계 와일드카드 (Upper Bounded) — extends
// "Number 또는 그 하위 타입의 리스트를 읽을 수 있다"
public double sum(List<? extends Number> list) {
    return list.stream().mapToDouble(Number::doubleValue).sum();
    // list.add(1); // ❌ 쓸 수 없음 — 정확한 타입을 모르므로
}

// 하한 경계 와일드카드 (Lower Bounded) — super
// "Integer 또는 그 상위 타입의 리스트에 쓸 수 있다"
public void addNumbers(List<? super Integer> list) {
    list.add(1);
    list.add(2); // ✅ Integer이므로 항상 추가 가능
}

// PECS 원칙 (Producer Extends, Consumer Super)
// 데이터를 생산(읽기)하는 곳 → extends
// 데이터를 소비(쓰기)하는 곳 → super

타입 소거(Type Erasure): 제네릭 타입 정보는 컴파일 후 제거됩니다. 런타임에 List<String>과 List<Integer>는 둘 다 그냥 List입니다. 이것이 instanceof List<String>이 불가능한 이유입니다.


주제 8. 자바 예외(Exception)의 계층 구조와 Checked/Unchecked 예외의 차이를 설명해주세요

빈출 이유: 예외를 제대로 다루지 못하면 실무에서 장애의 원인이 됩니다. 예외를 언제 catch하고, 언제 던지고, 어떻게 커스텀 예외를 만드는지를 이해하는지 봅니다.

H3: 예외 계층 구조

Throwable
├── Error          — JVM 수준 심각한 오류 (OutOfMemoryError, StackOverflowError)
│                    → 복구 불가, catch하지 않는 것이 원칙
└── Exception
    ├── RuntimeException (Unchecked)
    │   ├── NullPointerException
    │   ├── IllegalArgumentException
    │   ├── ArrayIndexOutOfBoundsException
    │   └── ClassCastException
    └── Checked Exception (RuntimeException 외)
        ├── IOException
        ├── SQLException
        └── ClassNotFoundException

Checked vs Unchecked 차이:

구분Checked ExceptionUnchecked Exception
컴파일 강제반드시 try-catch 또는 throws 선언선택적 처리
발생 시점예측 가능한 외부 오류 (파일, 네트워크)프로그래밍 오류 (NullPointer 등)
대표 예시IOException, SQLExceptionNPE, IllegalArgumentException
복구 가능성복구를 시도해야 하는 경우보통 코드 수정으로 해결

예외 처리 베스트 프랙티스:

java

// ❌ 나쁜 예 1: 빈 catch 블록 (예외 삼켜버리기)
try {
    riskyOperation();
} catch (Exception e) {
    // 아무것도 안 함 → 디버깅 불가능한 조용한 실패
}

// ❌ 나쁜 예 2: 너무 광범위한 예외 catch
try { ... } catch (Exception e) { log(e); } // 무엇이 잘못됐는지 알 수 없음

// ✅ 좋은 예: 구체적 예외 처리 + 의미 있는 로깅
try {
    user = userRepository.findById(id)
        .orElseThrow(() -> new EntityNotFoundException("User not found: " + id));
} catch (EntityNotFoundException e) {
    log.error("사용자 조회 실패: id={}", id, e);
    throw e; // 상위로 전파
}

// ✅ 좋은 예: 커스텀 예외로 도메인 맥락 표현
public class InsufficientBalanceException extends RuntimeException {
    private final double required;
    private final double actual;

    public InsufficientBalanceException(double required, double actual) {
        super("잔액 부족: 필요 %.2f, 현재 %.2f".formatted(required, actual));
        this.required = required;
        this.actual = actual;
    }
}

try-with-resources (Java 7+):

java

// AutoCloseable 구현체는 try-with-resources로 자동 close
try (BufferedReader reader = new BufferedReader(new FileReader(path));
     Connection conn = dataSource.getConnection()) {
    // 사용
} // 블록 종료 시 reader.close(), conn.close() 자동 호출
// finally에서 수동으로 close하던 반복 코드 불필요

주제 9. equals()와 hashCode()의 관계를 설명하고, 올바르게 구현하는 방법을 말해주세요

빈출 이유: HashMap·HashSet에서 객체를 키나 원소로 사용할 때 직결되는 개념입니다. 잘못 구현하면 HashMap에서 값을 찾지 못하는 미묘한 버그가 발생합니다.

H3: equals-hashCode 계약 (Contract)

자바 언어 명세는 equals()와 hashCode() 사이의 계약을 명시합니다.

계약 1: a.equals(b) 가 true이면, a.hashCode() == b.hashCode() 여야 합니다.

계약 2: a.hashCode() == b.hashCode() 라고 해서 a.equals(b) 가 true일 필요는 없습니다. (해시 충돌 허용)

즉, equals를 재정의하면 반드시 hashCode도 재정의해야 합니다.

java

// ❌ hashCode 없이 equals만 재정의한 경우
public class Point {
    int x, y;

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Point)) return false;
        Point p = (Point) o;
        return x == p.x && y == p.y;
    }
    // hashCode 없음!
}

Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);

System.out.println(p1.equals(p2)); // true — equals는 같다고 판단
Set<Point> set = new HashSet<>();
set.add(p1);
System.out.println(set.contains(p2)); // false ← 버그!
// p1과 p2의 hashCode가 다른 버킷에 저장되어 p2를 찾지 못함

java

// ✅ 올바른 구현
public class Point {
    final int x, y;

    public Point(int x, int y) { this.x = x; this.y = y; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;         // 자기 자신 비교
        if (!(o instanceof Point)) return false; // 타입 확인
        Point p = (Point) o;
        return x == p.x && y == p.y;       // 필드 비교
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y); // 모든 equals 비교 필드 포함
    }
}

// Java Record 사용 시 자동 구현 (Java 16+)
public record Point(int x, int y) { }
// equals, hashCode, toString 모두 자동 생성, 모든 컴포넌트 포함

equals 구현 시 5가지 원칙:

① 반사성(Reflexive):   x.equals(x) == true
② 대칭성(Symmetric):   x.equals(y) → y.equals(x)
③ 추이성(Transitive):  x.equals(y) && y.equals(z) → x.equals(z)
④ 일관성(Consistent):  여러 번 호출해도 결과 동일 (필드 변경 없으면)
⑤ null 비교:           x.equals(null) == false

5. 주제 10: 인터페이스 vs 추상클래스

주제 10. 인터페이스(Interface)와 추상클래스(Abstract Class)의 차이와 각각 언제 사용하는지 설명해주세요

빈출 이유: Java 8 이후 인터페이스에 default 메서드가 추가되면서 둘의 경계가 흐려졌습니다. “그렇다면 언제 무엇을 써야 하는가?”라는 설계적 사고를 묻는 질문입니다.

H3: 기술적 차이

java

// 추상클래스 — 상태(필드)를 가질 수 있음
public abstract class Vehicle {
    // 인스턴스 필드 가능
    protected String brand;
    protected int year;

    // 생성자 가능
    public Vehicle(String brand, int year) {
        this.brand = brand;
        this.year = year;
    }

    // 구현된 메서드 가능
    public String getBrand() { return brand; }

    // 추상 메서드 — 하위 클래스가 반드시 구현
    public abstract void startEngine();
}

// 인터페이스 — Java 8 이후
public interface Flyable {
    // 상수만 가능 (암묵적으로 public static final)
    int MAX_ALTITUDE = 10000;

    // 추상 메서드 (암묵적으로 public abstract)
    void fly();

    // default 메서드 (Java 8+) — 구현 포함, 하위 클래스 선택적 오버라이드
    default void land() {
        System.out.println("기본 착륙 절차 실행");
    }

    // static 메서드 (Java 8+) — 인터페이스의 유틸리티 메서드
    static Flyable of(String type) { /* 팩토리 */ return null; }

    // private 메서드 (Java 9+) — 내부 코드 재사용
    private void commonCheck() { /* ... */ }
}

핵심 차이 비교:

항목추상클래스인터페이스
인스턴스 필드가능불가 (상수만)
생성자가능불가
접근 제어자모든 접근 제어자public (암묵적)
다중 상속불가 (단일 extends)가능 (다중 implements)
상태(State)가질 수 있음가질 수 없음
설계 의미“~이다(is-a)” 관계“~할 수 있다(can-do)” 능력

H3: 언제 무엇을 선택하는가

추상클래스를 선택하는 경우:

  • 공통 상태(필드)와 공통 동작(메서드)을 여러 하위 클래스가 공유해야 할 때
  • 관련성이 높은 클래스들의 계층 구조를 표현할 때
  • 하위 클래스가 “~이다(is-a)” 관계일 때

java

// 모든 동물은 이름·나이를 가지며, 먹는 행동을 공유
public abstract class Animal {
    protected String name;
    protected int age;

    public void eat() { /* 공통 구현 */ }
    public abstract void makeSound(); // 각자 다른 구현 강제
}

인터페이스를 선택하는 경우:

  • 관련 없는 클래스들이 공통 능력을 가져야 할 때
  • 다중 구현이 필요할 때
  • “~할 수 있다(can-do)” 능력을 표현할 때
  • API를 외부에 공개할 때 (구현을 숨기고 계약만 노출)

java

// 비행기, 새, 드론은 서로 관련 없지만 모두 날 수 있음
class Airplane extends Vehicle implements Flyable, Maintainable { }
class Bird extends Animal implements Flyable { }
class Drone implements Flyable, RemoteControlled { }
// 다중 implements로 여러 능력 표현 가능

실무에서의 선택 기준:

Q: 두 개 이상의 클래스가 상태를 공유하는가?
  → YES → 추상클래스 고려

Q: 서로 관련 없는 클래스들이 같은 동작을 해야 하는가?
  → YES → 인터페이스 선택

Q: 다중 구현이 필요한가?
  → YES → 인터페이스 선택 (단일 상속 한계 회피)

Q: 외부에 API를 공개하는가?
  → YES → 인터페이스로 계약만 노출

일반 원칙: 가능하면 인터페이스를 우선하고,
           공통 상태/구현이 반드시 필요할 때만 추상클래스 사용

6. 자바 면접 실전 대비 전략 — 공부법과 답변법

10가지 주제 학습 우선순위

1단계 (필수, 2주):
  ① OOP 4원칙      ② JVM 구조      ③ equals/hashCode
  → 거의 모든 자바 면접의 기본

2단계 (핵심, 2주):
  ④ 컬렉션 프레임워크  ⑤ GC          ⑥ 예외 처리
  → 실무 코드와 직결

3단계 (심화, 2주):
  ⑦ 동시성          ⑧ 스트림/람다   ⑨ 제네릭
  → 시니어 포지션이나 심화 면접에서 차별화

4단계 (최신, 1주):
  ⑩ 인터페이스 vs 추상클래스 + Record·Sealed·Pattern Matching
  → Java 14~21 최신 문법과 연결

꼬리 질문 대비 — 각 주제별 예상 심화 질문

주제예상 꼬리 질문
OOP“상속보다 컴포지션을 쓰는 경우는?”
JVM“PermGen이 Metaspace로 바뀐 이유는?”
GC“GC 튜닝을 해본 경험이 있나요?”
컬렉션“HashMap의 로드 팩터가 성능에 미치는 영향은?”
동시성“ConcurrentHashMap은 내부적으로 어떻게 동작하나요?”
스트림“parallelStream의 주의사항은?”
제네릭“타입 소거(Type Erasure)란 무엇인가요?”
예외“Checked 예외 vs Unchecked 예외 중 어느 쪽을 선호하나요?”
equals/hashCode“왜 equals를 재정의하면 hashCode도 재정의해야 하나요?”
인터페이스“Java 8 이후 default 메서드가 생긴 이유는?”

모범 답변 구조 — 4단계 공식

1단계 — 정의 (한 문장):
  "GC는 힙 메모리에서 도달 불가능한 객체를 자동으로 수거하는 메커니즘입니다."

2단계 — 원리 (핵심 메커니즘):
  "세대별 가설에 기반해 Young Generation과 Old Generation으로 나누고,
   Minor GC와 Major GC를 다르게 처리합니다."

3단계 — 비교/트레이드오프:
  "G1 GC는 예측 가능한 STW 시간을 제공하지만,
   ZGC는 STW를 1ms 미만으로 줄이는 대신 CPU 오버헤드가 있습니다."

4단계 — 실무 연결:
  "서비스 응답 지연 스파이크가 GC 원인으로 의심될 때
   GC 로그(-Xlog:gc*)를 활성화해 STW 시간을 분석했습니다."

결론

자바 면접 빈출 주제 TOP 10은 OOP(객체지향 4원칙)·JVM 구조·GC·컬렉션 프레임워크·동시성·스트림과 람다·제네릭·예외 처리·equals와 hashCode·인터페이스 vs 추상클래스입니다. 이 10가지를 단순 암기가 아니라 “왜 이렇게 설계됐는가”와 “실무에서 어떻게 적용하는가”의 관점으로 이해하면, 어떤 꼬리 질문이 나와도 논리적으로 답변할 수 있는 기반이 만들어집니다. 오늘부터 각 주제를 소리 내어 4단계(정의→원리→비교→실무 연결) 형식으로 3분씩 말하는 연습을 시작해보세요. 면접은 무엇을 아는가가 아니라 무엇을 어떻게 말하는가의 싸움이기도 합니다.


⚠️ 면책 고지: 이 글에서 소개한 질문과 답변 예시는 일반적인 자바 기술 면접 경향을 바탕으로 정리한 것이며, 특정 기업의 면접 내용과 다를 수 있습니다. 실제 면접에서는 지원 기업의 기술 스택·Java 버전·프레임워크에 따라 질문의 방향과 깊이가 달라질 수 있습니다. 코드 예시는 JDK 21 기준으로 작성됐습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다