Java 면접 Q&A 정리 — 인강 노트
강의 형식: 하나의 강의 = 하나의 면접 질문. 브론즈(단순 암기) → 실버(핵심 일부 누락/과함) → 골드(원리 + 맥락) 답변을 비교하고, 골드 답변의 키워드가 다음 꼬리 질문의 출발점이 되는 구조다.
목차
섹션 2. JVM과 실행 원리
- JVM의 장단점을 설명해주세요
- Java는 왜 C/C++에 비해 느린가요?
- Java 실행 과정을 설명해주세요
- JVM 실행 엔진과 인터프리터·JIT 컴파일러의 차이를 설명해주세요
- JIT 컴파일러는 자주 사용되는 코드를 어떻게 판단하나요?
- JIT 코드 캐시가 꽉 차면 어떻게 되나요?
- 서버 배포 시 초기 요청의 응답 속도가 느린 이유는 무엇인가요?
섹션 3. GC (가비지 컬렉션) 8. GC 알고리즘을 설명하고, 자바는 어떤 알고리즘을 사용하나요? 9. Java의 힙 구조를 설명하고, GC는 언제 실행되나요? (Java 8 기준) 10. 루트 스페이스가 무엇인가요? 11. 왜 힙을 Young과 Old로 나누었나요? 12. Stop the World가 무엇이고 왜 발생하나요? 13. JVM이 제공하는 가비지 컬렉터의 종류와 특징을 설명해주세요 14. Java 8과 Java 11의 디폴트 가비지 컬렉터는 무엇이고 왜 달라졌나요? 15. G1 GC의 힙 구조와 동작 방식을 설명해주세요 16. GC 모니터링이 왜 필요하고, OOM 에러가 발생하면 어떻게 대처하나요?
섹션 4. 동시성 이슈 17. Java의 동시성 이슈를 설명해주세요 18. 가시성 문제는 정확히 언제 발생하나요? 19. 원자성 문제가 발생하는 구체적인 예시를 들어보세요 20. Java의 동시성 이슈를 해결하는 방법을 설명해주세요 21. synchronized의 문제점은 무엇인가요? 22. synchronized는 내부적으로 어떻게 구현되어 있나요? 23. 스레드 풀이 무엇이고 왜 사용하나요? 24. 스프링 MVC는 컨텍스트 스위칭 비용을 감수하면서 왜 스레드를 수백 개 운영하나요?
섹션 5. 객체지향 프로그래밍 25. 캡슐화가 무엇이고 왜 중요한가요? 26. Getter/Setter를 무분별하게 만들면 어떤 문제가 있나요? 27. 상속이 무엇이고 언제 사용하나요? 28. 상속의 단점은 무엇인가요? 29. 상속과 조합(Composition)의 차이를 설명해주세요 30. 다형성이 무엇이고 왜 필요한가요? 31. instanceof를 사용하면 어떤 문제가 있나요? 32. 인터페이스와 추상 클래스의 차이는 무엇인가요? 33. 언제 인터페이스를 쓰고 언제 추상 클래스를 쓰나요?
섹션 6. 람다와 스트림 34. 람다와 스트림이 무엇인가요? 35. Java 8에서 람다와 스트림을 도입한 이유가 가독성 때문만일까요? 36. 함수형 프로그래밍이 정확히 무엇인가요? 37. 스트림의 지연 연산이 무엇인가요?
부록
섹션 2. JVM과 실행 원리
Q. JVM의 장단점을 설명해주세요
핵심 답변 (골드 수준)
JVM의 장점은 크게 두 가지다. 첫째, Java와 OS 사이에서 중재자 역할을 수행하므로 OS와 무관하게 실행을 보장한다. "플랫폼 독립적"이라는 결과론적 표현 대신 JVM이 어디에서 무엇을 하는지 구조로 설명하는 것이 핵심이다. 둘째, GC를 통해 프로그램 메모리를 자동으로 관리하고 최적화한다. 개발자가 직접 메모리를 할당·해제할 필요가 없어 메모리 관리에서 발생하는 휴먼 에러를 예방할 수 있다.
단점은 바이트코드를 기계어로 바꾸는 과정이 필요하므로, 컴파일 시점에 한 번에 기계어가 만들어지는 C/C++에 비해 실행 속도가 느리다는 점이다. 다만 최근에는 JIT 컴파일러가 자주 실행되는 바이트코드 영역을 컴파일해 두기 때문에 속도 격차를 많이 줄였다. 단점을 말하면서 개선 방향까지 함께 제시해야 면접관의 추가 궁금증("그래서 요즘은 어떤데?")까지 통제할 수 있다.
핵심 키워드: 중재자 역할, GC를 통한 메모리 자동 관리, 바이트코드→기계어 변환 과정, JIT 컴파일러
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 브론즈 | "플랫폼 독립적이고 GC로 메모리 자동 관리. 단점은 C/C++보다 느림" | 키워드는 맞지만 왜 독립적인지, 왜 느린지 이유가 없어 "외운 사람"이라는 인상을 준다 |
| 골드 | 중재자 역할(구조), 느린 이유(바이트코드 변환 과정), 개선(JIT)까지 제시 | 정보량이 크게 늘지 않아도 구조·원인·개선 방향이 담겨 이해도가 드러난다 |
꼬리 질문
- "그러면 Java는 왜 C/C++에 비해 느린가요?" — "느리다"라는 키워드를 꺼내는 순간 반드시 따라온다. 다음 질문에서 상세히 다룬다. 면접에서 "느리다/성능이 안 좋다"는 표현을 쓰면 십중팔구 "왜요?"가 오므로, 동작 원리와 개선 방식을 반드시 함께 준비해야 한다.
Q. Java는 왜 C/C++에 비해 느린가요?
핵심 답변 (골드 수준)
Java는 바이트코드를 런타임에 기계어로 변환하는 과정이 필요한 반면, C/C++은 컴파일 시점에 이미 기계어로 변환되어 있기 때문에 상대적으로 느리다. 구체적으로 Java는 처음에 인터프리터 방식으로 바이트코드를 한 줄씩 해석하며 실행하는데, 이 과정에서 오버헤드가 발생한다. 다만 최근 JVM은 JIT 컴파일러를 통해 자주 실행되는 코드를 미리 기계어로 컴파일해 두기 때문에, 실행을 거듭할수록 C/C++과의 성능 격차가 줄어든다.
답변의 출발점을 "인터프리터라서"(실행 방식)가 아니라 "런타임 변환 vs 컴파일 시점 변환"(구조적 차이)으로 잡는 것이 포인트다. 또한 인터프리터만 말하면 "항상 느린 언어"가 되고, JIT까지 말해야 "느리지만 개선되는 구조를 가진 언어"가 된다.
참고로 C를 몰라도 되지만, C가 코드를 기계어로 바꾸는 기본 원리 정도는 알아야 Java의 동작 방식을 비교 설명할 수 있다.
핵심 키워드: 런타임 변환 vs 컴파일 시점 변환, 인터프리터의 오버헤드, JIT 컴파일러, 인터프리터와 JIT의 관계
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 브론즈 | "인터프리터 방식이라 느립니다" 한 줄 | 왜 인터프리터가 느린지, C/C++과의 비교가 없다. "느린가요?"에 "느려요"라고 답한 것과 다름없다 |
| 실버 | 바이트코드를 런타임에 한 줄씩 해석 vs C/C++은 컴파일 시점에 기계어 변환 | 구체적이지만 JIT가 빠져, 자바가 항상 인터프리터로만 도는 듯한 인상을 준다 |
| 골드 | 구조적 차이로 시작 + 인터프리터 오버헤드 + JIT로 격차 축소 | 느린 이유와 개선 방향성을 동시에 보여준다 |
꼬리 질문
- 여기서 나온 인터프리터와 JIT 컴파일러는 다음 메인 질문 "Java 실행 과정"의 핵심 구성 요소가 된다.
Q. Java 실행 과정을 설명해주세요
핵심 답변 (골드 수준)
자바 실행 과정은 다섯 단계로 이루어진다.
- 프로그램이 실행되면 JVM이 OS로부터 필요한 메모리를 할당받는다.
- 자바 파일(.java)이 **자바 컴파일러(javac)에 의해 바이트코드(클래스 파일)**로 변환된다.
- 클래스 로더가 바이트코드를 필요한 시점에 JVM으로 로딩한다(동적 로딩).
- 로딩된 바이트코드는 런타임 데이터 영역에 배치되고, 실행 엔진에 의해 실질적인 수행이 이루어진다.
- 실행 과정에서 JVM은 필요에 따라 GC와 같은 관리 작업을 수행한다.
OS로부터 메모리를 할당받는 단계를 맨 앞에 두면 "JVM은 OS 위에서 돌아가는 프로세스"임을 이해한다는 신호가 된다. "필요한 시점에 로딩한다"는 동적 로딩의 핵심 표현이며, 런타임 데이터 영역과 실행 엔진을 구분해 말하면 JVM 내부 구조를 명확히 안다고 인식된다.
핵심 키워드: OS로부터 메모리 할당, 클래스 로더와 동적 로딩, 런타임 데이터 영역과 실행 엔진, GC
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 브론즈 | "Java 코드를 컴파일하면 바이트코드가 되고, JVM이 이걸 실행합니다" | 큰 그림만 있고 단계 디테일이 없다 |
| 실버 | 컴파일러가 클래스 파일 변환 → JVM이 메모리에 올림 → 실행 엔진이 기계어 변환·실행 | OS 메모리 할당 과정과 클래스 로더(동적 로딩), GC 언급이 빠져 있다 |
| 골드 | 5단계로 구조화, 동적 로딩·영역/엔진 구분·GC까지 포함 | 구조화된 답변으로 내부 구조 이해를 보여준다 |
꼬리 질문
- "실행 엔진이 구체적으로 어떻게 동작하나요? 인터프리터와 JIT 컴파일러의 차이는요?" — "실행 엔진에 의해 수행된다"는 키워드를 잡고 들어온다. 실행 엔진·인터프리터·JIT는 세트로 묶여 출제된다.
Q. JVM 실행 엔진과 인터프리터·JIT 컴파일러의 차이를 설명해주세요
핵심 답변 (골드 수준)
실행 엔진은 메모리에 적재된 바이트코드를 기계어로 변환하여 명령어 단위로 실행하며, 이때 인터프리터와 JIT 방식이 사용된다. 인터프리터는 런타임 중에 바이트코드를 한 줄씩 읽고 실행하는데 속도가 느리다. JIT 컴파일러는 인터프리터의 속도 이슈를 해결하기 위해 함께 사용되며, 자주 실행되는 바이트코드 영역을 런타임 중에 컴파일하여 사용한다.
핵심은 둘이 선택 관계가 아니라 상호 보완 관계라는 점이다. 인터프리터가 기본으로 돌다가, 자주 실행되는 바이트코드 영역을 JIT가 컴파일해서 속도를 올리는 구조다. 면접관은 이 관계를 이해하고 있는지를 본다.
핵심 키워드: 인터프리터와 JIT의 상호 보완 관계
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 실버 | 실행 엔진 정의 + "인터프리터는 느리고 JIT는 컴파일해서 빠르다" | "그럼 처음부터 JIT만 쓰면 되지 않나?"라는 의문이 남는다. 왜 같이 쓰이는지가 빠져 있다 |
| 골드 | JIT가 인터프리터의 속도 이슈를 해결하기 위해 함께 사용된다는 관계를 명시 | 상호 보완 구조를 이해하고 있음을 보여준다 |
꼬리 질문
- "자주 실행되는 코드를 어떻게 판단하나요? '자주'가 몇 번인가요?" — "자주 실행되는 바이트코드"라는 키워드를 잡고 들어온다.
Q. JIT 컴파일러는 자주 사용되는 코드를 어떻게 판단하나요?
핵심 답변 (골드 수준)
JIT 컴파일러는 런타임 중에 메서드의 호출 횟수를 프로파일링하여 자주 사용되는 코드를 판단한다. 구체적으로 C1 컴파일러는 메서드 호출 횟수가 약 2,000회를 넘으면 제한된 수준으로 최적화하여 컴파일하고, C2 컴파일러는 약 15,000회를 넘으면 더 높은 수준으로 최적화하여 컴파일한다.
- 프로파일링은 단순 카운팅을 포함하는 더 넓은 개념으로, 호출 횟수 외에 메서드에 들어오는 파라미터 타입, for문 같은 루프의 반복 횟수 등 런타임 데이터를 종합적으로 분석하는 것을 의미한다. 전문성을 보여줄 수 있는 키워드다.
- C2의 대표적인 최적화 기법으로는 함수 내부에서 호출하는 다른 함수의 코드를 그대로 붙여 넣는 함수 인라이닝(inlining) 이 있다.
핵심 키워드: 프로파일링, C1 컴파일러, C2 컴파일러
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 실버 | "호출 횟수를 카운팅하여 판단하고, 일정 횟수 이상이면 컴파일" | 방향은 맞지만 "일정 횟수"가 얼마인지, C1/C2 세분화가 없다 |
| 골드 | 프로파일링 + C1(약 2,000회, 제한된 최적화)/C2(약 15,000회, 높은 수준 최적화) | 구체적 임계치와 내부 구조까지 알고 있다고 판단된다 |
꼬리 질문
- "JIT 코드 캐시가 꽉 차면 어떻게 되나요?" — JIT가 컴파일 결과를 캐싱해 두는 영역이 코드 캐시이며, 그 한계 상황을 묻는다.
Q. JIT 코드 캐시가 꽉 차면 어떻게 되나요?
핵심 답변 (골드 수준)
코드 캐시가 꽉 차면 JIT 컴파일러는 더 이상 새로운 코드를 컴파일하지 못하고, 인터프리터 방식으로 전환하여 바이트코드를 한 줄씩 해석하며 실행한다. 이로 인해 컴파일된 기계어를 사용할 수 없게 되므로 애플리케이션 실행 속도가 느려지는 성능 저하가 발생한다. 단, 이미 컴파일된 메서드는 계속해서 JIT 컴파일 방식으로 수행된다.
JVM은 에러를 내지 않고 인터프리터로 폴백하는 구조를 갖고 있다는 것, 그리고 "JIT 컴파일의 이점이 사라지면 어떤 결과가 오는가"라는 인과관계를 이해하고 있음을 보여줘야 한다.
핵심 키워드: JIT 컴파일 중단, 인터프리터 방식으로 전환, 성능 저하의 인과관계
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 브론즈 | "에러가 발생하거나 애플리케이션이 중단될 수 있습니다" | 추측성 오답. JVM은 코드 캐시가 꽉 찼다고 에러를 내지 않는다 |
| 실버 | "더 이상 JIT 컴파일을 못하고, 새 코드는 컴파일되지 않은 상태로 실행" | 맞지만 "컴파일되지 않은 상태로 실행"이 구체적으로 무엇인지(인터프리터 전환)가 빠졌다 |
| 골드 | 인터프리터 전환 메커니즘 + 성능 저하 원인 + 기존 컴파일 메서드는 유지 | 폴백 구조와 인과관계를 모두 설명한다 |
꼬리 질문
- 반대 상황: 서버가 막 부팅되어 코드 캐시가 아예 비어 있으면? — 모든 코드가 인터프리터로 실행된다. 다음 질문으로 이어진다.
Q. 서버 배포 시 초기 요청의 응답 속도가 느린 이유는 무엇인가요?
핵심 답변 (골드 수준)
서버 부팅 직후에는 JIT 컴파일러의 코드 캐시가 비어 있기 때문에 모든 코드가 인터프리터 방식으로 한 줄씩 해석되며 실행된다. 이 상태에서 갑작스럽게 대량의 트래픽이 유입되면 모든 요청을 인터프리터 방식으로 처리해야 하므로 응답 속도가 매우 느려지거나, 심할 경우 서버가 다운될 수 있다.
이 문제는 JVM 워밍업으로 해결한다. 서버를 실제 트래픽에 노출시키기 전에 미리 주요 코드 경로를 실행하여 JIT 컴파일러가 코드를 컴파일하고 캐시에 두도록 하는 방법이다. 실무에서는 서버가 뜰 때 자주 쓰이는 API를 미리 반복 호출하여 코드 캐시를 채워두며, K8s 환경에서는 레디니스 프로브(readiness probe) 를 활용해 워밍업이 끝나기 전까지 실제 트래픽이 들어오지 않도록 제어한다.
강사의 실무 경험: 결제 스타트업에서 배포 직후 초기 요청이 느려지는 현상을 추적해 보니 코드 캐시가 비어 모든 코드가 인터프리터로 돌고 있었고, JVM 워밍업으로 해결했다. 이 경험을 이력서에 쓰자 면접에서 디테일한 질문을 받았다. 관련해 C1/C2 상세 내용까지 다루는 카카오 발표 영상이 참고 자료로 추천됐다.
핵심 키워드: 코드 캐시가 비어 있음, 인터프리터 방식으로 인한 성능 저하, JVM 워밍업
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 실버 | "JIT가 아직 컴파일하지 않아 모든 코드가 인터프리터로 실행되어 느립니다" | 방향은 맞지만 실제로 어떤 문제가 생기는지(심각성), 해결책, "코드 캐시가 비어 있다"는 표현이 빠졌다 |
| 골드 | 코드 캐시가 비어 있음 + 대량 트래픽 시 서버 다운 가능성 + JVM 워밍업 해결책 | 같은 개념(코드 캐시)의 양쪽 끝(꽉 참/비어 있음)을 이해하고, 문제→해결까지 완결한다 |
꼬리 질문
- 여기까지가 섹션 2의 흐름 완성이다: JVM 장단점 → 왜 느린가 → 실행 과정 → 실행 엔진 → JIT 판단 기준·한계 → 콜드 스타트 문제.
섹션 3. GC (가비지 컬렉션)
Q. GC 알고리즘을 설명하고, 자바는 어떤 알고리즘을 사용하나요?
핵심 답변 (골드 수준)
GC 알고리즘에는 레퍼런스 카운팅(Reference Counting) 과 마크 앤 스윕(Mark and Sweep) 이 있고, 자바는 마크 앤 스윕을 사용한다.
- 레퍼런스 카운팅: 각 객체가 몇 번 참조되는지 참조 카운트를 유지하고, 이 값이 0이 되면 즉시 해제하는 방식이다. 그러나 서로를 참조하는 순환 참조가 있으면 카운트가 0이 되지 않아 메모리 누수가 발생할 수 있다.
- 마크 앤 스윕: 루트 스페이스부터 그래프 순회로 도달 가능한 객체를 마킹하고, 마킹되지 않은 객체를 해제한다. 즉시 해제 대신 특정 시점에 GC를 수행하므로 STW(Stop the World) 현상이 발생한다.
각 알고리즘의 문제점(순환 참조 vs STW)까지 함께 말하면, 자바가 마크 앤 스윕을 선택한 설계 의도(순환 참조로 인한 메모리 누수보다는 STW가 낫다는 판단)가 자연스럽게 드러난다.
참고: GC는 Java만의 개념이 아니다. Go 등 다른 언어 경험이 있으면 해당 언어의 GC와 메모리 관리 방식을 묻는 경우도 있으므로, Java 외의 GC 알고리즘도 한두 개는 알아두는 것이 좋다.
핵심 키워드: 레퍼런스 카운팅, 마크 앤 스윕, 순환 참조, STW
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 실버(흔한 답변) | "레퍼런스 카운팅과 마크 앤 스윕이 있고, 자바는 마크 앤 스윕을 씁니다" | 알고리즘 이름만 나열했고 동작 원리·선택 이유가 없다 |
| 골드 | 각각의 동작 방식 + 각각의 문제점 + 자바의 선택 근거 | 단순 암기가 아닌 원리와 설계 의도까지 이해하고 있음을 보여준다 |
꼬리 질문
- "루트 스페이스가 뭔가요?", "힙에서 GC가 어떻게 실행되나요?", "STW는 왜 발생하나요?" — 골드 답변에서 꺼낸 키워드 하나하나가 꼬리 질문의 재료가 된다.
Q. Java의 힙 구조를 설명하고, GC는 언제 실행되나요? (Java 8 기준)
핵심 답변 (골드 수준)
Java의 힙은 Young Generation과 Old Generation으로 나뉜다. Young Generation은 다시 Eden, Survivor 0, Survivor 1 영역으로 구성된다.
- Eden 영역이 꽉 차면 Minor GC가 실행된다. 이때 마크 앤 스윕 알고리즘을 통해 참조되고 있는 객체만 Survivor 0 또는 Survivor 1 영역으로 옮긴다.
- 여러 번의 Minor GC에서 살아남은 객체는 Old Generation으로 승격(promotion) 된다.
- 시간이 지나 Old Generation이 꽉 차면 Major GC가 실행되며, 역시 마크 앤 스윕 알고리즘을 통해 필요 없는 메모리를 해제한다.
앞서 배운 마크 앤 스윕이 실제 힙에서 어떻게 적용되는지 연결하고, Eden → Survivor → Old로 이어지는 객체 이동 경로를 설명하는 것이 포인트다. 이 "승격" 개념이 뒤에 나올 "왜 Young과 Old를 나누었나"의 밑바탕이 된다.
핵심 키워드: Young/Old Generation, 객체 이동 경로(Eden→Survivor→Old), Minor GC와 Major GC, 마크 앤 스윕과의 연결
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 브론즈 | "힙은 객체를 저장하는 영역이고, 메모리가 부족하면 GC가 실행됩니다" | GC를 한 줄 요약했을 뿐 힙 구조 설명이 아니며, "메모리 부족"이 어느 영역인지도 모호하다 |
| 실버 | Young(Eden, S0, S1)/Old 구분 + Eden 꽉 차면 Minor, Old 꽉 차면 Major | 구조는 나왔지만 GC가 어떻게 동작하는지(마크 앤 스윕 연결)와 객체 이동 흐름이 빠졌다 |
| 골드 | 구조 + 알고리즘 연결 + 객체 이동·승격 흐름 | 개념을 따로따로가 아니라 연결해서 이해하고 있다고 판단된다 |
꼬리 질문
- "루트 스페이스가 뭔가요?" — 마크 앤 스윕이 루트 스페이스부터 그래프 순회를 시작한다는 표현을 잡고 들어온다.
Q. 루트 스페이스가 무엇인가요?
핵심 답변 (골드 수준)
루트 스페이스는 GC가 마크 앤 스윕 알고리즘을 수행할 때 그래프 순회를 시작하는 출발점이다. 구체적으로는 스택 영역의 로컬 변수와 메서드 영역의 static 변수처럼 힙 영역의 객체를 참조하는 변수들을 의미한다. GC는 이 루트 스페이스부터 시작하여 도달 가능한 모든 객체를 마킹하고, 도달할 수 없는 객체는 사용되지 않는 객체로 판단하여 메모리에서 해제한다.
포인트는 두 가지다. 첫째, 루트 스페이스를 단독 개념이 아니라 마크 앤 스윕의 일부(그래프 순회의 출발점)로 설명하는 것. 둘째, 위치를 나열하는 데서 그치지 않고 그것들의 공통 본질 — "힙 객체를 참조하는 출발점이기 때문에 루트" — 을 짚는 것이다.
핵심 키워드: 그래프 순회의 시작점, 스택의 로컬 변수·메서드 영역의 static 변수, 힙 참조와 도달 가능성(reachability)
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 실버 | "GC가 시작되는 위치로, 스택의 로컬 변수와 메서드 영역의 static 변수가 있습니다" | 위치와 예시는 맞지만 왜 그것들이 루트인지가 빠졌다 |
| 골드 | 그래프 순회의 출발점 + "힙 객체를 참조하는 변수들"이라는 본질 + 도달 가능성 판단 | 나열이 아니라 이유까지 담긴 설명이다 |
꼬리 질문
- "왜 힙을 Young과 Old로 나누었나요?" — 구조를 아는 것과 설계 의도를 아는 것은 다른 레벨이다.
Q. 왜 힙을 Young과 Old로 나누었나요?
핵심 답변 (골드 수준)
통계적으로 대부분의 객체는 수명이 짧다. 즉 대부분의 객체는 생성되자마자 금방 사용되지 않게 된다. 따라서 Young/Old로 분리하면 수명이 짧은 객체가 많이 모여 있는 Young 영역만 집중적으로 탐색하는 방식으로 GC를 수행할 수 있다. 전체 힙을 매번 탐색하는 것보다 효율적이기 때문에 GC 성능 향상을 크게 얻을 수 있다.
핵심은 "수명이 짧다(팩트) → 영역 분리(설계) → 전체 힙 대신 일부만 탐색(효과)"라는 논리의 연결이다. 면접관은 통계적 팩트만 듣고 싶은 것이 아니라, 그 팩트가 어떻게 성능 최적화로 연결되는지까지 듣고 싶어 한다.
핵심 키워드: 대부분의 객체는 수명이 짧다는 통계적 근거, Young 영역만 집중 탐색, 팩트→설계→효과의 연결고리
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 브론즈 | "메모리를 효율적으로 관리하기 위해서입니다" | 질문을 되풀이한 수준. "어떤 효율인지"가 없다 |
| 실버 | "대부분의 객체는 수명이 짧기 때문에 나누어 관리합니다" | 통계적 근거는 나왔지만 그것이 어떻게 성능 향상으로 이어지는지 논리가 빠졌다 |
| 골드 | 팩트 → 분리 설계 → Young만 탐색 → 성능 향상의 완결된 논리 | 설계 의도를 논리적으로 완성한다 |
꼬리 질문
- "GC가 탐색하는 동안 애플리케이션은 어떻게 되나요?" — STW 질문으로 이어진다.
Q. Stop the World가 무엇이고 왜 발생하나요?
핵심 답변 (골드 수준)
Stop the World(STW)는 GC를 수행하는 동안 애플리케이션의 모든 스레드가 일시 정지되는 현상이다.
STW가 발생하는 이유는 안전한 메모리 관리를 위해서다. 애플리케이션이 계속 실행되면 객체가 계속 생성되고 객체 참조 관계 변경이 일어나기 때문에, GC가 사용되지 않는 객체를 정확히 판별하고 해제하기 어렵다(움직이는 과녁에 활을 조준하는 것과 같다 — 정확히 판별하려면 모든 것을 멈추고 스냅샷을 찍어야 한다). 또한 메모리 파편화를 방지하기 위한 컴팩션(compaction) 작업을 수행할 때 객체의 메모리 주소가 변경되므로, 애플리케이션이 돌아가는 중에 주소가 바뀌면 참조 관계가 깨질 수 있어 멈춰야 안전하게 작업할 수 있다.
컴팩션은 GC가 객체를 해제한 뒤 듬성듬성 생긴 빈 공간을 정리하는 작업으로, 이후 GC 종류별 비교에서 중요한 기준이 된다. 실무에서도 GC 모니터링 시 STW 시간은 반드시 봐야 하는 지표다(길수록 애플리케이션이 오래 멈춰 있었다는 뜻).
핵심 키워드: 안전한 메모리 관리, 컴팩션, 현상과 원인의 구분(무엇인지만 말하면 실버, 왜까지 말해야 골드)
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 실버 | "GC 수행 중 애플리케이션이 일시 정지되는 현상으로, 모든 스레드가 멈춥니다" | 현상은 정확하지만 질문의 절반("왜 발생하나요?")에 답하지 못했다 |
| 골드 | 현상 + 왜 멈춰야 하는가(참조 관계 변경, 정확한 판별) + 컴팩션 시 주소 변경 | 현상과 원인을 모두 갖춘 완결된 답변이다 |
꼬리 질문
- 여기까지가 첫 번째 메인 질문(GC 알고리즘)에서 파생된 꼬리 흐름의 완성이다. 다음 메인 질문(GC 종류)에서 STW와 컴팩션이 비교 기준이 된다.
Q. JVM이 제공하는 가비지 컬렉터의 종류와 특징을 설명해주세요
핵심 답변 (골드 수준)
JVM의 주요 가비지 컬렉터는 Serial GC, Parallel GC, CMS GC, G1 GC가 있다. STW 시간과 컴팩션 지원 여부라는 두 가지 축으로 비교한다.
- Serial GC: 하나의 스레드로 GC를 실행하므로 STW 시간이 길다. 다만 컴팩션을 지원하며 메모리 파편화를 방지한다.
- Parallel GC: Serial GC를 멀티스레드로 개선한 버전. 여러 스레드로 GC를 병렬 처리하므로 STW 시간이 Serial GC보다 짧다.
- CMS GC: 대부분의 GC 작업을 애플리케이션 스레드와 동시에(concurrent) 수행하여 STW 시간을 최소화한다. 하지만 메모리와 CPU를 많이 사용하고, 컴팩션이 기본 제공되지 않아 메모리 파편화 문제가 발생할 수 있다.
- G1 GC: 힙을 리전(region) 이라는 논리적 단위로 나누어 관리한다. CMS보다 나은 방식으로 애플리케이션과 GC를 동시에 실행할 수 있고 컴팩션까지 지원한다.
이렇게 통일된 기준으로 비교하면 진화 흐름이 보인다: Serial → Parallel(멀티스레드 도입으로 STW 단축) → CMS(동시 실행으로 STW 최소화, 대신 컴팩션 포기) → G1(리전 기반이라는 새 패러다임으로 동시 실행 + 컴팩션 모두 확보). 각 GC를 따로 외운 것이 아니라 발전 이유를 이해하고 있음을 보여준다.
실무 사례: 비용 절감을 위해 컨테이너 CPU를 1코어 이하로 할당했더니 STW가 900ms 이상으로 치솟은 적이 있다. JVM은 CPU가 2코어 미만이면 자동으로 Serial GC를 선택하기 때문이었다. GC 종류를 모르면 이런 장애의 원인을 찾기 어렵다. 면접에서 CMS는 잘 안 나오지만 G1은 종종 나오므로 "리전" 키워드는 꼭 기억한다.
핵심 키워드: Serial→G1으로 가는 진화 흐름, STW 시간, 컴팩션 지원 여부, 리전 기반 관리
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 실버 | "Serial은 단일 스레드, Parallel은 멀티스레드, CMS는 동시성 중시, G1은 최신 GC" | 4개가 제각각 다른 기준으로 설명되어 사실상 이름 나열에 그친다 |
| 골드 | STW 시간·컴팩션이라는 일관된 두 축 + 진화 흐름 | 장단점이 명확히 드러나고 발전 맥락까지 이해했음을 보여준다 |
꼬리 질문
- "Java 8과 11의 디폴트 GC는 무엇이고 왜 달라졌나요?" — Parallel(자바 8 디폴트)과 G1(자바 11 디폴트)의 차이를 묻는다.
Q. Java 8과 Java 11의 디폴트 가비지 컬렉터는 무엇이고 왜 달라졌나요?
핵심 답변 (골드 수준)
Java 8은 Parallel GC를, Java 11은 G1 GC를 디폴트로 사용한다. G1 GC는 힙을 고정된 크기의 리전으로 나누고 각 리전을 독립적으로 수집한다. 이를 통해 특정 리전만 선택적으로 수집할 수 있어 전체 힙을 멈추고 GC할 필요를 줄인다.
여기에 두 가지 맥락을 더해야 한다.
- 대규모 힙 환경의 증가라는 현실적 배경: 이 방식은 힙 메모리가 클수록 효율적인데, 많은 기업들이 대규모 힙 메모리를 사용하는 애플리케이션을 운영하게 되면서 G1 GC가 적합해졌다. 과거에는 큰 RAM 하나를 가진 서버에 톰캣(JVM)을 여러 프로세스로 나눠 띄웠지만, 메모리가 커지면서 전체 힙을 한꺼번에 정리하는 기존 방식의 한계가 드러났고 리전 기반 선택적 수집이 필요해졌다.
- 예측 가능한 STW라는 비즈니스 맥락: 금융 거래 시스템이나 실시간 데이터 처리처럼 GC로 수백 ms 멈추면 치명적인 도메인에서 G1의 장점이 부각된다. G1은
-XX:MaxGCPauseMillis파라미터로 STW 시간을 예측 가능한 범위로 제어할 수 있다("이 시간만큼만 GC를 돌린다"는 형태).
기술 → 현실 → 비즈니스의 세 단계로 연결하면 답이 완성된다.
핵심 키워드: Java 8은 Parallel·Java 11은 G1이라는 팩트, 리전 기반 선택적 수집, 대규모 힙 환경 증가, 예측 가능한 STW
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 브론즈 | "8은 Parallel, 11은 G1. G1이 더 좋기 때문에 변경했습니다" | "더 좋다"는 이유가 되지 못한다 |
| 실버 | + G1은 리전 단위 관리로 선택적 수집이 가능해 전체 힙을 멈출 필요를 줄인다 | 기술적 장점은 연결했지만 "왜 11에서 바꿨는가"라는 배경이 없다 |
| 골드 | + 대규모 힙 환경 증가(현실) + 예측 가능한 STW(비즈니스) | 전환의 기술적 배경과 비즈니스 맥락을 모두 설명한다 |
꼬리 질문
- "그러면 G1 GC는 구체적으로 어떻게 동작하나요?" — 리전 기반 선택적 수집이라는 키워드를 잡고 들어온다.
Q. G1 GC의 힙 구조와 동작 방식을 설명해주세요
핵심 답변 (골드 수준)
구조와 동작 방식을 나눠 설명한다.
구조: G1 GC는 고정 크기 리전으로 힙을 나누며, 각 리전은 Young 또는 Old라는 역할을 동적으로 가질 수 있고 물리적으로 연속될 필요가 없다. 매우 큰 객체는 Humongous 리전에 저장된다.
동작 방식: 초기에는 Young-Only 수집을 반복하다가, Old 비율이 임계치를 넘으면 Young GC와 병행하여 Concurrent Marking을 시작해 각 리전에 살아 있는 객체의 양(liveness)을 측정한다. 이후 Space Reclamation 단계에서 Mixed GC로 Young과 가비지가 많은 Old 리전을 우선 수집해, 가비지 퍼스트(Garbage First) 전략으로 지연을 줄인다. Concurrent Marking은 애플리케이션 스레드와 동시에 실행되어 STW를 최소화하지만, Young GC와 Mixed GC 자체는 STW가 발생한다.
- 전통적인 힙은 Young끼리, Old끼리 물리적으로 붙어 있어야 하지만 G1의 리전은 그럴 필요가 없다. 이 물리적 비연속성이 리전의 역할을 Old였다가 Young이었다가 Humongous였다가 동적으로 바꿀 수 있는 유연성의 근거다.
- G1의 Young GC는 Minor GC와 동일한 개념이지만 G1에서는 특별히 Young GC라고 부른다.
- "가비지가 많은 Old 리전을 우선 수집한다"는 것이 Garbage First라는 이름의 의미다. 이름이 곧 전략이다.
핵심 키워드: 리전의 동적 역할과 물리적 비연속성, Young-Only → Concurrent Marking → Space Reclamation 사이클, Mixed GC, Garbage First
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 브론즈 | "힙을 여러 리전으로 나눠 관리하고, 가비지를 빠르게 수집합니다" | 결과만 말했을 뿐 리전이 뭔지, 왜 나눴는지, Garbage First의 의미가 없다 |
| 실버 | 동일 크기 리전, Young/Old 동적 할당, Humongous·Available 영역, 가비지 많은 리전 우선 회수 | 꽤 구체적이지만 물리적 비연속성과 Young-Only/Mixed GC 등 동작 사이클이 빠졌다 |
| 골드 | 구조(비연속·동적 역할·Humongous) + 동작 사이클 + STW 발생 지점 구분 | G1 특유의 동작 방식까지 설명해 확실히 다른 수준으로 보인다 |
꼬리 질문
- "그래서 GC를 왜 모니터링해야 하고, OOM이 나면 어떻게 대처하나요?" — 이론을 마친 뒤의 실무 관점 질문이다.
Q. GC 모니터링이 왜 필요하고, OOM 에러가 발생하면 어떻게 대처하나요?
핵심 답변 (골드 수준)
모니터링 필요성: Minor GC는 Young Generation만 대상으로 하므로 수 ms~수십 ms로 빠르게 처리되어 애플리케이션에 큰 영향을 주지 않는다. 하지만 Major GC는 Old Generation 전체를 대상으로 하므로 STW 시간이 수백 ms에서 수 초까지 길어질 수 있어, 응답 지연·타임아웃, 최악의 경우 서비스 장애로 이어질 수 있다. 따라서 GC 로그 모니터링 툴을 통해 GC 빈도, 수행 시간, 힙 사용률을 주기적으로 확인하여 Major GC 발생을 사전에 예방하고 최적화하는 것이 중요하다.
OOM 대처 4단계:
- JVM 옵션에
-XX:+HeapDumpOnOutOfMemoryError,-XX:HeapDumpPath파라미터를 설정하여 OOM 발생 시 자동으로 힙 덤프 파일이 생성되도록 한다. - 생성된 힙 덤프를 MAT(Memory Analyzer Tool) 또는 VisualVM으로 분석하여 메모리를 가장 많이 차지하는 객체와 참조 체인을 확인한다. Leak Suspects 리포트가 메모리 누수 의심 지점을 찾아준다.
- 분석 결과를 바탕으로 근본 원인을 해결한다. 예: 컬렉션에 무한정 데이터가 쌓이는 코드 수정, 메모리 효율이 낮은 레거시 라이브러리 교체, 캐시 정책 조정.
- 근본 원인 해결이 어려운 경우 임시방편으로 힙 크기를 늘리되, 이는 단기 조치이므로 반드시 근본 원인 분석을 병행한다.
단계를 전부 외울 필요는 없지만 "힙 크기 증설은 임시방편이고 근본 원인을 찾아야 한다"는 원칙은 반드시 기억해야 한다. 문제가 터진 후 대처와 터지기 전 예방은 실무에서 완전히 다른 수준이다.
핵심 키워드: Minor/Major GC의 차이, 사전 예방 관점, 힙 덤프→분석→근본 원인 해결→임시방편의 흐름, 힙 크기 조정은 임시방편
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 브론즈 | "성능에 영향을 주니 모니터링하고, OOM이 나면 메모리 부족이니 힙 크기를 늘리면 됩니다" | 막연하고 원인 분석 없는 임시방편. 장애를 겪어본 적이 없다고 판단된다 |
| 실버 | Major GC의 STW 문제 + VisualVM/JConsole로 확인 + 힙 덤프 분석 | 구체적이지만 사전 예방 관점과 체계적 단계(덤프 생성→분석 툴→해결)가 부족하다 |
| 골드 | 사전 예방 + JVM 파라미터로 자동 덤프 + 분석·해결·임시방편의 체계적 4단계 | 실제로 OOM이 나도 대처할 수 있는 사람으로 보인다 |
섹션 4. 동시성 이슈
Q. Java의 동시성 이슈를 설명해주세요
핵심 답변 (골드 수준)
동시성 프로그래밍에서는 CPU와 RAM 사이에 위치하는 CPU 캐시 메모리와 멀티스레드라는 특징 때문에, 다수의 스레드가 공유 자원에 접근할 때 가시성 문제와 원자성 문제가 발생할 수 있다.
- 가시성 문제: 여러 스레드가 각자 다른 CPU 캐시 메모리를 참조하게 되면서 CPU 캐시와 RAM의 데이터가 일치하지 않아 생기는 문제다. 한 스레드가 공유 변수를 수정했지만 다른 스레드는 여전히 이전 값을 읽는 상황이 발생한다.
- 원자성 문제: 여러 스레드가 공유 자원에 동시에 쓰기 연산을 수행할 때, 중간에 다른 스레드가 개입하여 최종 결과가 잘못되는 문제다. 예를 들어 5개의 스레드가 동시에 카운터를 1씩 증가시켰는데 최종값이 5가 아닌 3이 되는 경우다.
포인트: ① 가시성/원자성으로 바로 들어가지 않고 근본 원인(CPU 캐시 + 멀티스레드)을 먼저 깔면 하드웨어 아키텍처 차원의 이해를 보여준다. ② 가시성은 쓰기가 아니라 읽기의 문제임을 구분한다(대부분 쓰기 충돌만 말하므로 확실한 차별화 포인트다). ③ 원자성에는 구체적 숫자 예시를 붙여 설득력을 높인다. 백엔드 면접에서 거의 100% 나오는 질문이다.
핵심 키워드: CPU 캐시 메모리, 가시성 문제, 원자성 문제, 공유 자원
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 브론즈 | "여러 스레드가 동시에 같은 변수에 접근하면 문제가 발생합니다" | 어떤 종류의 문제가 왜 생기는지가 없다 |
| 실버 | 가시성(CPU 캐시-RAM 불일치)과 원자성(동시 쓰기) 구분 | 키워드는 다 꺼냈지만 왜 불일치하는지 등 원리 설명이 부족하다 |
| 골드 | 근본 원인 선제시 + 읽기/쓰기 문제 구분 + 숫자 예시 | 원인과 원리, 구체성이 모두 갖춰졌다 |
꼬리 질문
- "가시성 문제가 정확히 언제 발생하나요?" — 멀티코어와 코어별 독립 캐시 이야기로 이어진다.
Q. 가시성 문제는 정확히 언제 발생하나요?
핵심 답변 (골드 수준)
가시성 문제는 스레드들이 서로 다른 CPU 코어의 캐시 메모리를 참조할 때 발생한다. 현대 컴퓨터는 멀티코어 환경이므로 각 CPU 코어마다 독립적인 캐시를 가지고 있다. 스레드 A가 코어 1에서 실행되고 스레드 B가 코어 2에서 실행된다면 각각 다른 캐시에 같은 변수의 복사본을 가지게 된다. 이때 스레드 A가 공유 변수를 수정하면 코어 1의 캐시만 업데이트되고 코어 2의 캐시는 이전 값을 유지한다. 즉 캐시 간 동기화가 이루어지기 전까지 다른 스레드는 오래된 값을 읽게 된다.
핵심 조건은 "서로 다른 코어의 캐시"다. 같은 코어를 여러 스레드가 번갈아 사용할 때는 캐시가 같으므로 문제가 없다. 코어가 하나면 캐시도 하나이므로 발생하지 않는다. 이 구분이 진짜 발생 조건이며, "동기화"라고 말할 때는 그 대상(캐시 간 동기화)을 명확히 해야 한다.
핵심 키워드: 멀티코어 환경, 각 코어의 독립적인 캐시, 캐시 간 동기화
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 흔한 답변 | "여러 스레드가 CPU 캐시를 거쳐 읽고 쓰기 때문에 동기화가 안 되어 발생합니다" | "서로 다른 코어의 캐시"라는 핵심 조건이 누락됐고, 무엇에 대한 동기화인지도 모호하다 |
| 골드 | 멀티코어·독립 캐시 조건 + 코어 1/코어 2 시나리오 + 캐시 간 동기화 명시 | 발생 조건을 정확히 짚고 구체적 그림을 그려준다 |
꼬리 질문
- "원자성 문제가 구체적으로 어떤 상황에서 생기는지 예시를 들어보세요" — 가시성이 읽기 문제라면 쓰기 문제인 원자성의 시나리오를 요구한다.
Q. 원자성 문제가 발생하는 구체적인 예시를 들어보세요
핵심 답변 (골드 수준)
대표적인 예시는 공유 카운터다. 두 스레드가 동시에 count++ 연산을 수행한다고 가정한다.
// count++는 코드로는 한 줄이지만 CPU에서는 세 단계로 나뉜다
int temp = count; // 1) 메모리에서 count 값을 읽는다
temp = temp + 1; // 2) 읽어온 값에 1을 더한다
count = temp; // 3) 결과를 메모리에 다시 쓴다
두 스레드가 동시에 실행되면 스레드 A가 count 0을 읽고, 스레드 B도 count 0을 읽는다. 둘 다 1을 더해서 각각 1을 메모리에 쓴다. 결과적으로 두 번의 연산이 있었지만 count는 2가 아니라 1이 된다. 이것이 원자성 문제(연산 유실)다.
count++가 한 줄 코드지만 실제로는 읽기-수정-쓰기 3단계로 실행된다는 사실을 모르면 이 현상을 명확히 설명할 수 없다. "count++는 세 단계인데 그 사이에 다른 스레드가 끼어들면 연산이 유실된다"로 연결하면 된다. 이론 질문에 대해 자기만의 구체적 예시를 하나쯤 갖고 있으면 면접에서 큰 무기가 된다.
핵심 키워드: 읽기-수정-쓰기 3단계, 중간 개입, 연산 유실
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 실버 | "두 스레드가 동시에 1을 더하면 한 스레드의 연산이 무시되어 2가 아닌 1이 될 수 있습니다" | 현상은 맞지만 왜 그런 일이 벌어지는지 메커니즘이 없다 |
| 골드 | 3단계 분해 + 스레드 A/B의 단계별 타임라인 | 결과가 아니라 메커니즘을 설명해 직접 분석해 본 사람으로 보인다 |
꼬리 질문
- "그러면 그 문제를 어떻게 해결하나요?" — 다음 메인 질문으로 이어진다.
Q. Java의 동시성 이슈를 해결하는 방법을 설명해주세요
핵심 답변 (골드 수준)
자바의 동시성 이슈는 세 가지 방법으로 해결할 수 있다.
- volatile 키워드는 가시성 문제를 해결한다. volatile 변수는 CPU 캐시를 우회하여 RAM에 직접 접근하기 때문에 모든 스레드가 항상 최신 값을 본다.
- synchronized 키워드는 원자성 문제를 해결한다. 락을 통해 한 번에 하나의 스레드만 임계 영역에 접근할 수 있도록 보장하여 읽기-수정-쓰기 과정 전체를 원자적으로 만든다. 메서드나 블록 단위로 사용할 수 있다.
- Atomic 타입은 CAS(Compare And Swap) 알고리즘을 통해 논블로킹 방식으로 원자성을 보장한다. synchronized와 달리 락을 사용하지 않아 성능이 더 좋지만, 단순한 연산에만 사용하는 것이 적합하다.
포인트: ① 각 해결책의 동작 원리를 한 문장씩 붙인다. ② synchronized(락 기반 블로킹)와 Atomic(CAS 기반 논블로킹)의 차이를 명확히 대비해야 "왜 두 가지가 따로 존재하는가"가 해소된다. ③ Atomic의 한계까지 말해야 도구 선택의 기준을 아는 사람으로 보인다. Atomic이 단순 연산에 적합한 이유는 CAS 특성상 충돌이 발생하면 재시도하는데, 로직이 복잡할수록 재시도 비용이 커지기 때문이다.
핵심 키워드: volatile, synchronized, Atomic, 락을 통한 블로킹 vs 논블로킹
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 브론즈 | "synchronized를 사용하면 됩니다" | 가시성 해결책이 없고, 다른 방법과 동작 원리도 없다 |
| 실버 | "가시성은 volatile, 원자성은 synchronized나 Atomic으로 해결" | 도구 매핑은 맞지만 각각이 왜/어떻게 해결하는지 원리가 없다 |
| 골드 | 세 가지 도구 + 각각의 동작 원리 + 블로킹/논블로킹 대비 + Atomic의 한계 | 나열이 아니라 원리와 선택 기준까지 갖췄다 |
꼬리 질문
- "synchronized의 문제점은 뭔가요?" — 락을 언급하면 자연스럽게 따라온다.
Q. synchronized의 문제점은 무엇인가요?
핵심 답변 (골드 수준)
synchronized의 가장 큰 문제점은 블로킹(blocking) 방식으로 동작한다는 것이다. 한 스레드가 락을 획득하여 임계 영역에 접근하면, 해당 영역에 접근하려는 다른 모든 스레드가 대기(BLOCKED) 상태가 되어 아무 작업도 하지 못한다. 이 과정에서 스레드 상태를 RUNNABLE → BLOCKED → RUNNABLE로 변경하는 컨텍스트 스위칭 비용이 발생하고, 대기 중인 스레드는 CPU를 전혀 사용하지 못해 자원 활용도가 떨어진다. 특히 임계 영역이 길거나 경쟁이 심한 경우 성능 저하가 심각해진다.
"느려진다"는 결과가 아니라 비용을 두 가지(컨텍스트 스위칭 + CPU 자원 활용도 저하)로 분해해 설명하면 설득력이 올라가고, 스레드 상태 전환을 언급하면 운영체제 기본기가 있다고 판단된다. "블로킹이라서 스레드가 블록되고, 그때 컨텍스트 스위칭과 CPU 낭비가 생긴다"로 연결한다.
핵심 키워드: 블로킹 방식, BLOCKED 대기 상태, 컨텍스트 스위칭, 비용 구조(스위칭 비용 + 자원 활용도)
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 실버 | "한 스레드만 접근할 수 있어 다른 스레드들이 기다려야 하므로 성능이 떨어집니다" | 핵심은 맞지만 구체적 비용(블로킹, 컨텍스트 스위칭)이 없다 |
| 골드 | 블로킹 명시 + 스레드 상태 전환 + 비용을 두 가지로 분해 | 어디서 비용이 발생하는지까지 설명한다 |
꼬리 질문
- "synchronized는 내부적으로 어떻게 구현되어 있나요?" — 락을 쓴다고 하면 그 락의 구현을 묻는다.
Q. synchronized는 내부적으로 어떻게 구현되어 있나요?
핵심 답변 (골드 수준)
Java에서 synchronized는 모니터(monitor) 를 사용하여 구현된다. 모든 객체는 암묵적으로 모니터를 가지고 있으며, synchronized는 해당 객체의 모니터를 잠그는 역할을 한다. 모니터는 스레드 간 상호 배제(mutual exclusion) 와 조건 동기화를 제공하는 고수준의 동기화 메커니즘이다. 한 번에 하나의 스레드만 모니터를 소유할 수 있어 임계 구역에 대한 원자적 접근을 보장하고, wait/notify를 통해 조건에 따른 스레드 간 협력도 가능하다.
포인트: ① 락은 일반적인 개념이고 모니터는 Java가 그것을 구현하는 구체적인 방식이다 — 이 차이를 아는지가 관건이다. ② 모니터의 두 역할 중 대부분 상호 배제만 알고 있는데, 조건 동기화(wait/notify)까지 언급하면 전체 그림을 이해한다는 인상을 준다. "synchronized는 객체의 모니터를 잠그는 것이고, 모니터는 상호 배제와 조건 동기화를 제공한다"로 연결한다. 5년 차 경력 면접에서도 그대로 나온 질문이다.
핵심 키워드: 모니터, 객체의 암묵적 모니터, 상호 배제, 조건 동기화(wait/notify)
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 실버 | "락을 사용하여 한 번에 하나의 스레드만 접근할 수 있도록 합니다" | 동작 결과일 뿐, "어떻게 구현되어 있나"라는 질문에 답하지 못했다 |
| 골드 | 모니터 메커니즘 + 암묵적 모니터 + 상호 배제·조건 동기화 두 역할 | Java의 구체적인 구현 방식을 설명한다 |
Q. 스레드 풀이 무엇이고 왜 사용하나요?
핵심 답변 (골드 수준)
스레드 풀은 작업 처리에 사용되는 스레드를 제한된 개수만큼 정해 놓고, 작업 큐에 들어오는 작업들을 하나씩 스레드가 맡아 처리하는 기법이다.
사용하는 이유는 두 가지다.
- 매번 스레드를 생성·삭제하는 비용을 절약한다. 스레드 생성은 OS 레벨의 시스템 콜 작업이 필요하므로, 재사용을 통해 성능을 크게 개선할 수 있다.
- 작업 처리 요청이 급격히 증가해도 애플리케이션 성능이 안정적으로 유지된다. 작업들이 큐에 대기하다가 여유가 있는 스레드가 순차적으로 처리하므로, 스레드 개수가 폭증하지 않고 시스템 자원을 안정적으로 사용할 수 있다.
작업 큐가 핵심 메커니즘이다. 큐 없이 "재사용해서 빠르다"고만 말하면 스레드 풀의 절반만 설명한 것이다. Java의 대표 구현체인 ThreadPoolExecutor 클래스는 스레드 세이프한 BlockingQueue를 필드로 가지고 있으므로 코드를 한번 보는 것이 좋다. "큐에 작업이 쌓이고 제한된 수의 스레드가 재사용되며 처리하니까 비용도 줄고 안정적이다"로 연결한다.
핵심 키워드: 작업 큐, 제한된 개수의 스레드, 스레드 재사용, 안정적인 성능
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 브론즈 | "스레드를 미리 여러 개 만들어 두는 것입니다" | 현상만 있고 왜 그렇게 하는지가 없다 |
| 실버 | "제한된 개수를 미리 생성해 재사용, 생성·삭제 비용을 줄여 성능이 좋다" | 실전에서는 무난하지만 작업 큐 메커니즘과 안정성 축이 빠졌다 |
| 골드 | 작업 큐 구조 + 비용 절감(시스템 콜 근거) + 안정성의 두 축 | 구조와 근거를 모두 갖췄다 |
꼬리 질문
- "스프링 MVC는 스레드를 수백 개씩 운영하는데, 컨텍스트 스위칭 비용을 감수하면서까지 왜 그럴까요?"
Q. 스프링 MVC는 컨텍스트 스위칭 비용을 감수하면서 왜 스레드를 수백 개 운영하나요?
핵심 답변 (골드 수준)
스프링 MVC 같은 웹 애플리케이션에서는 네트워크 요청, 데이터베이스 조회, 파일 입출력 등 블로킹 IO 작업이 빈번하게 발생한다. 블로킹 IO는 작업이 완료될 때까지 스레드가 대기 상태로 전환되어 CPU를 전혀 사용하지 못한다. 만약 스레드 개수가 적다면 일부 스레드가 IO를 기다리는 동안 CPU 코어들이 놀게 되어 자원이 낭비된다. 따라서 스레드를 수백 개로 늘려, 일부 스레드가 IO로 블록되어도 다른 스레드들이 CPU를 계속 사용할 수 있도록 한다. 컨텍스트 스위칭 비용보다 CPU 가동률을 높이는 이득이 더 크기 때문이다. 쉽게 말해 비싼 돈 주고 산 CPU가 놀지 않도록 스레드를 늘려서 일을 계속 시키는 것이다.
트레이드오프 상세: 스레드를 수백 개로 늘리면 컨텍스트 스위칭이 잦아지며 CPU 레지스터 저장·복원, CPU 캐시 미스 등의 오버헤드가 발생한다. 그러나 블로킹 IO 환경에서 스레드 수가 적으면 대부분의 스레드가 IO 대기에 빠졌을 때 CPU에 할당할 작업 자체가 부족해진다. 스레드를 늘리면 IO 대기 중이 아닌 스레드가 항상 존재하게 되어 CPU 유휴 시간이 줄고 전체 처리량이 올라간다.
"비싼 CPU" 같은 자기 언어의 쉬운 비유도 무기가 된다. 복잡한 개념을 쉽게 풀어낼 수 있다는 것은 진짜 이해하고 있다는 증거이며, "초등학생도 이해하게 설명해보라"는 류의 질문에도 대응할 수 있다.
핵심 키워드: 블로킹 IO, CPU 유휴 시간, 컨텍스트 스위칭 vs CPU 가동률, 트레이드오프 판단
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 브론즈 | "동시에 많은 요청을 처리하기 위해서입니다" | 현상이지 이유가 아니다. 왜 스레드가 많아야 하는지 근본 원인이 없다 |
| 실버 | "IO 작업이 많아 스레드가 자주 대기하므로, 많이 만들면 다른 스레드가 CPU를 써 처리량이 는다" | 방향은 맞지만 블로킹 IO라는 정확한 용어, 대기 메커니즘, 트레이드오프 명시가 없다 |
| 골드 | 블로킹 IO 메커니즘 + IO 예시 + 스위칭 비용 vs 가동률 트레이드오프 + 직관적 비유 | OS 핵심 지식(컨텍스트 스위칭, 블로킹 IO, 스레드 상태)을 한 번에 보여준다 |
섹션 5. 객체지향 프로그래밍
Q. 캡슐화가 무엇이고 왜 중요한가요?
핵심 답변 (골드 수준)
캡슐화는 데이터와 메서드를 하나로 묶고 접근 제어자를 통해 외부 접근을 제한하는 것이다. 이를 통해 정보 은닉을 달성하여 객체 내부 구현을 외부로부터 보호할 수 있다.
캡슐화가 중요한 이유는 객체 내부 구현이 변경되더라도 외부 코드에 영향을 주지 않기 때문이다. 예를 들어 내부 필드를 ArrayList에서 LinkedList로 바꾸더라도 외부에서는 공개된 메서드만 사용하므로, 외부 코드는 아무것도 수정할 필요가 없다. 즉 변경에 유연한 코드를 작성할 수 있다.
public class UserRepository {
// 내부 구현을 ArrayList → LinkedList로 바꿔도
private List<User> users = new LinkedList<>();
// 외부는 공개 메서드만 사용하므로 영향이 없다
public void add(User user) { users.add(user); }
public List<User> findAll() { return List.copyOf(users); }
}
핵심 키워드: 데이터·메서드를 묶고 접근 제어자 사용, 정보 은닉, 외부 접근 제한, 변경에 유연한 코드
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 브론즈 | "데이터와 메서드를 하나로 묶는 것입니다" | 묶으면 뭐가 좋은지가 없다. 정보 은닉·접근 제어자 언급도 없다 |
| 실버 | + private 같은 접근 제어자로 외부 직접 접근 차단, 객체 내부 보호 | "보호한다"가 모호하고(무엇으로부터? 어떤 이점?) 정보 은닉이라는 핵심 용어가 여전히 없다 |
| 골드 | 정보 은닉 명시 + 변경 격리라는 진짜 이유 + ArrayList→LinkedList 예시 | 원리와 실제 예시로 이해도를 증명한다 |
꼬리 질문
- "그러면 Getter/Setter를 무분별하게 만들면 어떤 문제가 있나요?" — 접근 제어를 이해했다면 실무 적용을 검증한다.
Q. Getter/Setter를 무분별하게 만들면 어떤 문제가 있나요?
핵심 답변 (골드 수준)
Getter/Setter를 무분별하게 만들면 다음 문제가 발생한다.
- 객체의 내부 상태가 외부로 노출되어 캡슐화가 위반된다.
- 외부에서 setter로 객체 상태를 마음대로 변경할 수 있어 객체가 수동적이게 된다. 객체는 스스로 상태를 관리해야 하는데 setter는 이를 외부에 맡겨버린다.
해결하려면 Getter/Setter 대신 의미 있는 행동 메서드를 정의해야 한다. 예를 들어 setBalance 대신 deposit이나 withdraw 같은 메서드를 만들어 객체가 스스로 상태를 관리하도록 한다.
// 나쁜 예: 잔액 관리 로직이 외부에 흩어진다
account.setBalance(account.getBalance() + 3);
// setBalance(3)을 쓰는 클래스가 여러 곳이면, 정책이 바뀔 때 전부 찾아 수정해야 한다
// 좋은 예: 객체가 스스로 상태를 관리한다 (객체의 자율성)
public class Account {
private long balance;
public void deposit(long amount) { this.balance += amount; }
public void withdraw(long amount) { this.balance -= amount; }
}
setBalance를 쓰는 클래스가 여러 곳일 때 정책 값이 바뀌면 사용처를 모두 찾아 수정해야 하는데, 이런 현상이 생기는 이유가 바로 상태를 관리해야 할 객체가 수동적이 됐기 때문이다. IDE가 자동 생성해 주니 필드마다 Getter/Setter를 다 만드는 것은 실무에서 흔한 실수이며, 과제 전형 코드 리뷰에서 그대로 질문받은 사례도 있다.
핵심 키워드: 내부 상태 노출로 인한 캡슐화 위반, 객체의 수동성, 의미 있는 행동 메서드, 객체의 자율성
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 브론즈 | "코드가 지저분해집니다" | 실무 표현으로는 쓰이지만 평가 자리에서는 무엇이 어떻게 나빠지는지 알 수 없는 모호한 답이다 |
| 실버 | "내부 상태가 외부로 노출되어 캡슐화가 깨집니다" | 맞지만 그래서 어떤 악영향이 있는지, 어떻게 해결하는지가 없다 |
| 골드 | 캡슐화 위반 + 객체 수동성 + 행동 메서드 해결책과 구체적 예시 | 문제의 본질과 해결책, 실제 코딩 습관까지 보여준다 |
Q. 상속이 무엇이고 언제 사용하나요?
핵심 답변 (골드 수준)
상속은 객체들 간의 관계를 구축하는 것으로, 주로 다형성을 구현하기 위해 사용한다. 부가적으로 상위 클래스의 메서드나 필드를 하위 클래스가 재사용할 수 있다는 장점도 있다. 상속은 is-a 관계가 명확할 때 사용한다. 예를 들어 "고양이는 동물이다" 같은 관계가 성립하고, 향후 변화가 거의 없을 것으로 판단될 때 사용한다.
포인트: ① 상속의 목적에서 다형성 구현을 우선순위로 잡는다. 코드 재사용만 강조하면 "재사용이 목적이면 상속 말고 유틸 클래스로 빼서 돌려 써도 되지 않나?"라고 파고들 여지를 준다. ② "물려받는다"는 재사용 관점의 표현이지만 "관계를 구축한다"는 다형성을 강조한 관점의 표현이다. ③ is-a라는 사용 기준을 제시하면 상속을 무분별하게 쓰지 않을 사람으로 인식된다. 객체지향 4대 특징 중 가장 빈출인 질문이다.
핵심 키워드: 객체 간의 관계 구축, 다형성 구현, is-a 관계
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 브론즈 | "부모 클래스의 기능을 자식 클래스가 물려받는 것입니다" | 정의만 있고 왜 쓰는지, 본질적 목적이 없다 |
| 실버 | "메서드·필드를 재사용해 코드 중복을 줄이기 위해 사용합니다" | 코드 재사용이라는 목적은 나왔지만 더 핵심인 다형성 구현이 빠졌다 |
| 골드 | 관계 구축 + 다형성 우선 + is-a 사용 기준 | 목적의 우선순위와 사용 판단 기준까지 제시한다 |
꼬리 질문
- "그러면 상속의 단점은 뭔가요?" — 상속을 잘 설명하면 높은 확률로 이어진다.
Q. 상속의 단점은 무엇인가요?
핵심 답변 (골드 수준)
상속의 단점은 크게 두 가지다.
- 하위 클래스가 상위 클래스의 구현에 강하게 결합될 수 있다. 상위 클래스의 변경이 하위 클래스에 큰 영향을 주게 된다. 상위 클래스 하나를 고쳤는데 하위 클래스 10개가 다 깨지는 것이 실무에서 겪는 진짜 문제다.
- 기능 확장을 위해 상위 클래스의 메서드를 오버라이딩하면 캡슐화를 깨뜨릴 수 있다. 하위 클래스가 상위 클래스의 내부 구현을 알아야만 올바르게 오버라이딩할 수 있는데, 내부 구현을 알아야 한다는 것 자체가 캡슐화 위반이기 때문이다.
"상속이 캡슐화를 깨뜨릴 수 있다"는 상당히 날카로운 지적으로, 이를 언급하면 상속의 트레이드오프를 이해하고 있다고 느끼게 한다.
핵심 키워드: 강한 결합, 상위 클래스 변경의 파급력, 오버라이딩의 캡슐화 위반
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 브론즈 | "상속을 많이 쓰면 클래스 구조가 복잡해집니다" | 어디에나 붙일 수 있는 표현. 무엇이 어떻게 복잡해지는지가 없어 경험이 의심된다 |
| 골드 | 강한 결합의 구체적 현상 + 오버라이딩-캡슐화 관점 | 실무에서 부딪혀 본 사람의 답변이 된다 |
꼬리 질문
- "그러면 상속 말고 다른 방법은 없나요?" — 상속 vs 조합으로 이어진다.
Q. 상속과 조합(Composition)의 차이를 설명해주세요
핵심 답변 (골드 수준)
상속은 is-a 관계로 객체 간의 관계를 구축하여 다형성을 구현한다. 부가적으로 상위 클래스의 메서드·필드를 하위 클래스가 재사용하거나 수정할 수 있다. 반면 컴포지션(조합)은 has-a 관계로, A 객체가 B 객체를 필드로 가지고 B 객체의 메서드를 사용할 뿐 다형성과는 관계가 없다. 조합은 상속보다 결합도가 낮아 변경에 더 유연하므로, is-a 관계가 명확하지 않다면 조합을 선호하는 것이 좋다.
포인트: ① "물려받는다/포함한다"가 아니라 is-a/has-a로 관계 유형을 구분하면 개념이 선명해진다. ② 둘을 가르는 핵심은 다형성 구현 여부다 — 결합도 차이로만 이해하면 반쪽이다. ③ 차이 설명에서 끝나지 않고 "언제 어떤 걸 쓸지" 기준("상속보다 조합을 선호하라"는 객체지향 원칙)까지 제시해야 실무적 이해도를 보여준다. 객체지향을 중시하는 회사라면 충분히 나올 수 있는 질문이다.
핵심 키워드: 상속은 is-a·조합은 has-a, 다형성 구현 여부, 결합도 차이, 조합 선호 원칙
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 브론즈 | "상속은 클래스를 물려받는 거고, 조합은 다른 클래스를 포함하는 겁니다" | 표현만 다를 뿐 왜 다른지, 언제 뭘 써야 하는지가 없다 |
| 실버 | 조합은 인스턴스를 필드로 갖고 메서드 호출 + 결합도가 낮아 유연 | 구현 방식·결합도는 나왔지만 가장 중요한 차이(다형성 여부)가 빠졌다 |
| 골드 | is-a/has-a + 다형성 구현 여부 + 선택 기준 | 본질적 차이와 실무 판단 기준을 모두 갖췄다 |
Q. 다형성이 무엇이고 왜 필요한가요?
핵심 답변 (골드 수준)
다형성은 하나의 타입에 여러 객체를 대입할 수 있다는 것을 의미한다. 구현 방법으로는 크게 오버로딩과 오버라이딩 등이 있다. 다형성이 필요한 이유는 코드 재사용성과 유연성을 높이기 위함이다. 새로운 클래스가 추가되더라도 기존 코드를 수정할 필요가 없어 OCP(개방-폐쇄 원칙, Open-Closed Principle)를 준수할 수 있다.
// Animal 타입 하나로 Cat, Dog 등 여러 객체를 다룬다
Animal animal = new Cat();
animal = new Dog();
// 새로운 타입(Bird)이 추가되어도 이 메서드는 수정할 필요가 없다 → OCP
void feed(Animal animal) {
animal.eat();
}
포인트: ① 정의뿐 아니라 구현 방법(오버로딩·오버라이딩)을 언급해 코드 차원의 이해를 보여준다. ② 실버와 골드를 가르는 핵심은 OCP와의 연결이다. 단순히 중복을 줄이는 차원이 아니라 코드의 확장성 자체가 달라진다는 것이다.
핵심 키워드: 하나의 타입에 여러 객체 대입, 오버로딩과 오버라이딩, 변경에 유연한 코드, OCP
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 브론즈 | "여러 가지 형태를 가질 수 있다는 것입니다" | 다형성(polymorphism)의 어원을 번역한 수준. 프로그래밍적 의미와 필요성이 없다 |
| 실버 | 하나의 타입에 여러 객체 대입 + Animal/Cat/Dog 예시 + 코드 중복 감소 | 구체적이지만 "왜 필요한가"가 중복 감소에서 끝나 OCP 연결이 빠졌다 |
| 골드 | 정의 + 구현 방법 + 기존 코드 무수정 → OCP 준수 | 확장성 관점의 진짜 이점까지 연결한다 |
꼬리 질문
- "instanceof를 사용하면 어떤 문제가 있나요?" — 다형성을 무너뜨리는 대표적 안티 패턴을 반대쪽에서 파고든다.
Q. instanceof를 사용하면 어떤 문제가 있나요?
핵심 답변 (골드 수준)
instanceof를 사용하면 세 가지 문제가 발생한다.
- 다형성을 저해한다. 다형성의 핵심은 호출하는 쪽이 구체적인 타입을 몰라도 동작하는 것인데, instanceof는 호출하는 쪽에서 직접 타입을 확인하므로 하나의 타입으로 여러 객체를 다루는 다형성의 의미가 사라진다.
- 로직이 바뀔 때마다 instanceof 분기 로직을 수정해야 하므로 OCP를 위반하고 유지보수성이 떨어진다.
- 런타임에 객체 타입을 확인하는 성능 오버헤드가 발생한다. 특히 상속 계층 구조가 깊을수록 모든 부모 타입을 확인해야 하므로 성능이 저하된다.
포인트: ① 다형성 저해를 "이점을 못 누린다"가 아니라 다형성의 정의(호출부가 타입을 몰라도 됨)에서부터 연결해 설명한다. ② "유지보수성 저하"를 OCP 위반이라는 객체지향 원칙으로 연결하면 앞 질문의 답변과 유기적으로 이어진다. ③ 설계 관점뿐 아니라 성능 관점까지 짚으면 답변의 깊이가 달라진다.
핵심 키워드: 다형성 저해, OCP 위반, 성능 오버헤드
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 실버 | 구체 타입이 코드에 드러나 다형성 이점 상실 + 새 타입마다 분기 수정으로 유지보수성 저하 | 방향은 맞지만 OCP라는 원칙 연결과 런타임 성능 오버헤드가 빠졌다 |
| 골드 | 다형성 정의로부터의 연결 + OCP 위반 + 계층 깊이에 따른 성능 비용 | 설계 원칙과 성능까지 아우른다 |
Q. 인터페이스와 추상 클래스의 차이는 무엇인가요?
핵심 답변 (골드 수준)
인터페이스와 추상 클래스의 차이는 크게 세 가지다.
- 인터페이스는 상태(필드)를 가질 수 없지만, 추상 클래스는 상태를 가질 수 있다.
- 인터페이스는 다중 구현이 가능하지만, 추상 클래스는 단일 상속만 가능하다.
- 인터페이스의 모든 메서드는 public이지만, 추상 클래스는 접근 제어자를 자유롭게 사용할 수 있다.
이 질문이 까다로운 이유는 JDK 8 이후 인터페이스에 디폴트 메서드가 들어오면서 "메서드 구현 가능 여부"라는 전통적 기준이 무력화되어 둘의 경계가 모호해졌기 때문이다. 그래서 상태 보유 여부를 맨 앞에 배치하는 것이 중요하다. 인터페이스는 디폴트 메서드로 구현은 가질 수 있어도 상태는 여전히 가질 수 없다 — 이것이 JDK 8 이후에도 변하지 않는 본질적 차이이며, 실무에서 어떤 것을 쓸지 판단하는 가장 중요한 기준이다. 접근 제어자 차이(추상 클래스는 protected, package-private 사용 가능)는 많이 놓치는 부분이라 언급하면 세밀한 이해라는 인상을 준다.
핵심 키워드: 상태 보유 여부, 다중 구현 vs 단일 상속, 접근 제어자 차이
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 실버 | "인터페이스는 선언만 가능하고 추상 클래스는 구현을 가질 수 있다 + 다중 구현 vs 단일 상속" | JDK 8 이후에는 디폴트 메서드 때문에 구현 여부 기준이 무의미해진다. 다중/단일 기준 하나만 남는 셈 |
| 골드 | 상태 보유(본질) 우선 + 다중 구현/단일 상속 + 접근 제어자 | JDK 8 이후에도 유효한 본질적 차이를 우선순위 있게 제시한다 |
꼬리 질문
- "그러면 언제 인터페이스를 쓰고 언제 추상 클래스를 쓰나요?" — 문법적 차이를 안다고 하면 거의 반드시 이어진다.
Q. 언제 인터페이스를 쓰고 언제 추상 클래스를 쓰나요?
핵심 답변 (골드 수준)
기본적으로 인터페이스 + 디폴트 메서드 기반으로 정의하는 것을 선호한다. 인터페이스는 다중 구현이 가능하고 상태가 없으므로 결합도가 낮아 더 유연하기 때문이다.
추상 클래스는 상태를 유의미하게 이용해야 하는 경우에 사용한다. 예를 들어 모든 하위 클래스가 공통으로 사용하는 필드가 있거나, 템플릿 메서드 패턴처럼 공통 로직을 상태와 함께 공유해야 할 때 추상 클래스를 사용한다.
포인트: ① "인터페이스 + 디폴트 메서드"를 출발점으로 잡는 것 자체가 JDK 8 이후의 현대적인 자바를 이해한다는 신호다. ② "is-a면 추상 클래스, has-a면 인터페이스"는 너무 단순한 기준이며, 실제 선택 기준은 상태의 유의미한 이용 여부다. ③ 템플릿 메서드 패턴이라는 디자인 패턴 예시까지 연결하면 실무 경험 또는 학습 깊이를 보여준다. 코드 리뷰에서 "여기는 인터페이스가 맞지 않나요?" 같은 피드백에 답할 수 있어야 한다.
핵심 키워드: 인터페이스+디폴트 메서드 우선, 다중 구현과 낮은 결합도, 상태 공유가 필요할 때 추상 클래스, 템플릿 메서드 패턴
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 실버 | "has-a면 인터페이스, is-a면 추상 클래스. 가능하면 인터페이스 선호" | 선호 원칙은 맞지만 추상 클래스를 써야 하는 구체적 상황(상태 이용)이 모호하다 |
| 골드 | 인터페이스+디폴트 메서드 우선 + 상태 기준 + 템플릿 메서드 패턴 예시 | 현대적 자바 이해와 명확한 선택 기준을 제시한다 |
섹션 6. 람다와 스트림
Q. 람다와 스트림이 무엇인가요?
핵심 답변 (골드 수준)
람다는 메서드를 하나의 식으로 표현한 익명 함수로, 주로 함수형 인터페이스의 구현체를 간결하게 작성할 때 사용한다. 스트림은 컬렉션의 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 하는 내부 반복자다.
- "메서드를 하나의 식으로 표현한"이라는 수식어가 람다의 본질이다. 메서드로 따로 정의하지 않고 식 하나로 줄여 쓸 수 있다는 것이 핵심이다.
- Java에서 람다는 함수형 인터페이스 없이 존재할 수 없다. 이 연결을 보여주면 단순 문법이 아니라 Java의 타입 시스템과 연결해서 이해하고 있다고 평가된다.
- "컬렉션을 처리하는 기능"은 for문도 마찬가지다. 외부 반복자는 개발자가 직접 인덱스를 관리하며 하나씩 꺼내는 것이고, 내부 반복자는 스트림이 알아서 요소를 순회하며 처리하는 것이다. 이 한마디가 스트림과 기존 반복문의 본질적 차이를 보여준다.
정의만 묻는 것 같은 이 질문은 답변의 밀도에 따라 이후 꼬리 질문이 달라지는 위험한 질문이다. 짧게 답해도 되지만 이후 꼬리 질문을 커버할 수 있어야 한다.
핵심 키워드: 메서드를 식으로 표현, 함수형 인터페이스, 내부 반복자
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 브론즈 | "람다는 익명 함수이고 스트림은 컬렉션을 처리하는 기능입니다" | 틀리진 않지만 구글링해서 외운 건지 실제로 쓸 줄 아는 건지 구분이 안 된다 |
| 골드 | 식으로 표현한 익명 함수 + 함수형 인터페이스 연결 + 내부 반복자 | 정보량 차이는 크지 않지만 본질(식 표현, 타입 시스템, 반복 방식)이 담겼다 |
꼬리 질문
- "Java 8에서 람다·스트림을 도입한 이유가 가독성 때문만일까요?"
Q. Java 8에서 람다와 스트림을 도입한 이유가 가독성 때문만일까요?
핵심 답변 (골드 수준)
가독성도 중요하지만 가장 핵심적인 이유는 세 가지다.
- 멀티코어 CPU가 보편화되면서 병렬 처리를 쉽게 구현할 수 있도록 하기 위해서다. 스트림은
parallelStream을 통해 개발자가 멀티스레드 코드를 직접 작성하지 않고도 병렬 처리를 할 수 있게 해준다. Java 8이 나온 2014년은 듀얼코어·쿼드코어가 보편화되던 시기로, CPU가 여러 코어로 늘어나는데 기존 자바로는 그것을 제대로 활용하기 어려웠다는 하드웨어적 배경이 있다. - 함수형 프로그래밍의 장점인 부수 효과 최소화를 지원하기 위해서다. 람다와 스트림은 순수 함수 기반으로 동작하므로 외부 상태를 변경하지 않아 멀티스레드 환경에서도 안전하다(1번의 멀티코어 이야기와도 연결된다).
- 선언적 코드 작성을 가능하게 하기 위해서다. "어떻게 구현할지"가 아니라 "무엇을 처리할지"에 집중할 수 있어 코드의 의도가 명확해진다.
실무 참고: parallelStream은 자바 전체가 공유하는 커먼 풀(common pool) 을 사용하기 때문에, 수백 개의 요청이 동시에 들어오는 스프링 웹 서버 환경에서는 특정 잡이 스레드를 점유·블로킹하면 다른 작업이 스레드를 가져갈 수 없는 스레드 경합·블로킹 위험이 커진다. 따라서 IO 기반 웹 서버보다는 CPU 집약적인 배치·계산 중심 작업에 주로 쓰인다.
핵심 키워드: 멀티코어 CPU, parallelStream을 통한 병렬 처리, 부수 효과 최소화와 순수 함수, 선언적 코드
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 브론즈 | "가독성 때문입니다" | "가독성 말고 다른 이유는요?"라는 꼬리에서 막힌다 |
| 실버 | + 함수형 프로그래밍 지원, 순수 함수, 선언적 코드 | 꽤 잘한 답변이지만 소프트웨어 관점만 있고 멀티코어라는 하드웨어·시대적 배경이 빠졌다 |
| 골드 | 멀티코어 배경 + 부수 효과 최소화→멀티스레드 안전 + 선언적 코드 | 하드웨어 변화까지 연결해 도입 배경을 입체적으로 설명한다 |
꼬리 질문
- "함수형 프로그래밍이 정확히 뭔가요?" — 함수형·순수 함수·부수 효과라는 단어를 들으면 확인하고 싶어진다.
Q. 함수형 프로그래밍이 정확히 무엇인가요?
핵심 답변 (골드 수준)
함수형 프로그래밍은 순수 함수와 불변성을 기반으로 하는 프로그래밍 패러다임이다.
- 순수 함수: 동일한 입력에 대해 항상 동일한 출력을 반환하고, 외부 상태를 변경하지 않는 함수다. 이로 인해 부수 효과가 발생하지 않는다.
- 불변성: 데이터의 상태를 변경하지 않고, 변경이 필요하면 새로운 데이터를 생성하는 방식이다.
- 함수를 1급 객체로 다룬다: 함수를 변수에 할당하거나, 다른 함수의 인자로 전달하거나, 반환값으로 사용할 수 있다. 이것이 Java에서 람다가 가능한 이유이며, 람다가 함수형 인터페이스의 구현체라는 것과 직접 연결된다.
순수 함수와 불변성은 비슷해 보이지만 레이어가 다르다. 순수 함수가 외부 상태를 안 바꾸는 것이라면, 불변성은 데이터 자체를 안 바꾸는 것이다.
// 순수 함수 위반: 메서드 밖의 외부 상태(log)를 건드렸다
private List<String> log = new ArrayList<>();
public String greet(String name) {
log.add(name); // 외부 상태 변경 → 부수 효과
return "Hello, " + name;
}
// 불변성 위반: 원본 데이터를 직접 수정했다
public List<String> addName(List<String> names, String name) {
names.add(name); // 원본 수정
return names;
}
// 불변성을 지키려면 새 리스트를 만들어 반환한다
public List<String> addNameImmutable(List<String> names, String name) {
List<String> result = new ArrayList<>(names);
result.add(name);
return result;
}
답변할 때는 "첫째, 둘째, 셋째"로 각 특징을 구조화해서 말하는 것이 좋다.
핵심 키워드: 순수 함수, 부수 효과 없음, 불변성, 1급 객체
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 실버 | "순수 함수를 사용해 부수 효과를 최소화하는 패러다임 + 순수 함수 정의" | 정확하지만 불변성과 1급 객체라는 대표 특징이 빠졌다 |
| 골드 | 순수 함수 + 불변성 + 1급 객체, 그리고 순수 함수/불변성의 레이어 구분 | 특징을 구조화하고 미묘한 차이까지 구분한다 |
꼬리 질문
- "스트림의 지연 연산이 뭔지 아시나요?" — 내부 반복자라고 했으면 스트림 내부 동작을 하나 더 확인하고 싶어진다.
Q. 스트림의 지연 연산이 무엇인가요?
핵심 답변 (골드 수준)
지연 연산(lazy evaluation)은 스트림의 중간 연산이 호출 시점에 즉시 실행되지 않고, 최종 연산이 호출될 때까지 실행을 미루는 방식이다. 이를 통해 성능 최적화 효과를 얻을 수 있다.
list.stream()
.filter(x -> x > 10) // 중간 연산: 즉시 실행되지 않는다
.limit(5) // 5개를 찾는 순간 나머지 요소는 아예 보지 않는다
.collect(toList()); // 최종 연산: 이때 비로소 실행된다
- 불필요한 연산 스킵:
filter().limit(5)를 실행할 때 전체 컬렉션을 다 필터링하지 않고, 5개를 찾는 순간 나머지 연산을 스킵한다. 즉시 실행이었다면 filter가 전체 컬렉션을 다 돌고 나서 limit이 5개를 자르지만, 지연 연산이므로 5개가 모이는 순간 뒤의 수만 개는 쳐다보지도 않는다. - 파이프라인 결합과 한 번의 순회: filter, map, sorted 같은 중간 연산을 여러 개 체이닝하면 각각 따로 순회하는 것이 아니라 하나의 파이프라인으로 결합되어, 요소별로 filter→map→sorted를 "세로로" 한 번만 돈다. filter 전체 → map 전체 → sorted 전체로 "가로로" 도는 것이 아니므로 성능 차이가 크다.
"필요한 시점까지 실행을 미룬다"는 레이지(lazy) 원리는 스트림에서만 쓰이는 게 아니다. Hibernate의 레이지 로딩, JVM의 클래스 로딩(동적 로딩), JPA의 연관 엔티티 지연 로딩 등 개발 곳곳에 깔려 있으므로, 여기서 제대로 이해해 두면 다른 레이지 패턴을 이해할 때도 도움이 된다.
핵심 키워드: 중간 연산과 최종 연산, 불필요한 연산 스킵, 파이프라인과 한 번의 순회, 필요한 시점까지 미룬다는 레이지 원리
답변 레벨 차이
| 레벨 | 내용 | 평가 |
|---|---|---|
| 실버 | "중간 연산은 바로 실행되지 않고 최종 연산이 호출될 때 실행됩니다" | 정의는 맞지만 왜 그렇게 설계했는지(성능 최적화)가 없어 "동작은 아는데 이유는 모르는" 답이 된다 |
| 골드 | 정의 + filter/limit 스킵 예시 + 파이프라인 한 번 순회 | 지연 연산이 존재하는 이유와 실제 차이 지점을 보여준다 |
부록
섹션 7. 기타 질문 안내
섹션 2~6(JVM과 실행 원리, GC, 동시성 이슈, 객체지향 프로그래밍, 람다·스트림)에서 다루지 않았지만 면접에 빈출로 나오는 단건 질문들은 강의에서 별도 PDF 부록으로 제공된다. 대표 예시는 오버로딩과 오버라이딩의 차이, 제네릭, static 같은 자바 기본 문법이며, 꼬리 질문 흐름이 아닌 단순 기본기 검증용 질문들이 해당된다. 각 섹션 말미에도 섹션별 빈출 질문 PDF가 부록으로 제공된다.
보너스: 면접관 관점에서 본 서류·면접 전략
강의 마지막에 수록된 특강(채널톡 면접관 경험 기반)의 요지다.
1. 서류 작성 — STAR 원칙
가장 흔한 실수는 기술 스택과 구현 내용을 나열하는 것이다. 면접관이 궁금한 것은 "어떤 문제를 어떻게 해결했는가"이며, 이를 STAR 구조로 풀어낸다.
- S(Situation): 프로젝트의 배경과 직면한 문제 상황을 명확히 정의한다.
- T(Task): 문제 해결을 위해 내가 맡은 구체적인 역할과 목표를 설명한다.
- A(Action): 실제로 수행한 구체적인 액션과 기술적 의사결정을 작성한다.
- R(Result): 정량적·정성적 성과를 포함한 최종 결과를 작성한다.
문제 정의 → 해결 과정 → 성과라는 하나의 시나리오를 보여줘야 서류 합격 가능성이 올라간다.
2. 라이브 코딩 — 설계를 본다
AI로 코드 구현 자체는 누구나 빠르게 할 수 있는 시대라, 알고리즘 구현보다 객체지향 설계와 디자인 패턴을 본다. 요구사항이 바뀌었을 때 최소한의 수정으로 대응 가능한 구조인지 판단하고 방향을 잡을 수 있는지, 즉 지속 가능한 소프트웨어를 설계할 수 있는지를 보여줘야 한다.
3. 직무 면접 — 문제 해결력
문제 해결력은 두 가지로 평가된다.
- 문제를 정의하는 능력: 실무의 문제는 모호하게 주어진다("성능이 좀 안 좋은 것 같아요"). 어떤 API가, 어떤 조건에서, 어떤 기준(예: p99 레이턴시 200ms 이상)으로 느린지 측정 기준을 합의하고 범위를 좁혀, 해결 가능한 단위(예: DB 인덱스 누락)로 구체화하는 과정이다. 면접에서도 모호한 문제가 주어졌을 때 어떤 질문으로 범위를 좁혀가는지 그 커뮤니케이션 자체를 본다.
- 문제를 해결하는 능력: 해결 방법은 보통 여러 개다(예: A. 단순 캐시 적용 — 빠르지만 정확성 문제와 근본 DB 부하 미해결 / B. 쿼리 최적화·인덱싱 — 근본 해결, 추가 인프라 비용 없음, 분석 공수와 쓰기 성능 소폭 저하 / C. 아키텍처 전면 개편 — 확장성 최상이지만 기간 2개월·오버엔지니어링 위험). 각 옵션의 장단점과 트레이드오프를 비교하고, 왜 다른 방법이 아닌 이 방법(예: B)을 선택했는지 논리적으로 설득하는 과정이 정답 맞히기보다 중요하다.
4. AI 시대에 CS를 공부해야 하는 이유
위의 문제 해결 프로세스는 AI를 잘 쓰는 프로세스와 동일하다. AI가 내어준 답변의 진위를 판단하고 트레이드오프를 비교해 최적의 결정을 내리려면 탄탄한 CS 지식이 필수다.
- 자료구조/알고리즘: AI가 제안한 코드의 시간·공간 복잡도가 최적인지 검증한다.
- 운영체제/네트워크: 대규모 트래픽을 견디는 구조인지, 병목과 리소스 관리를 판단한다.
- 데이터베이스: AI가 제안한 인덱싱·트랜잭션 전략이 적절한지 판단한다.
- CS가 깊을수록 정교한 질문이 가능하다. "쿼리가 왜 느려?"(얕은 질문)보다 "WHERE절에 인덱스 걸린 컬럼을 썼는데도 느린데,
LIKE '%검색어'처럼 앞에 %를 붙이면 인덱스를 못 타는 것 아닐까?"(깊은 질문)가 AI 답변의 폭을 좁힌다.
면접 준비 전략 세 가지: ① AI 시대일수록 CS와 객체지향 설계를 열심히 공부한다. ② 문제 정의 훈련 — 모호한 문제를 풀어낼 수 있는 작은 단위로 구체화한다. ③ 해결책 비교 분석 — 여러 해결책의 트레이드오프를 비교해 하나를 결정하는 연습을 한다.