면접 준비 (2)
1. Java
1.1 언어 기초
기본형(Primitive)과 참조형(Reference)의 차이는 무엇인가요?
- 기본형은 값 자체를 저장하며
int, long, double, boolean 등 8가지가 있습니다. null을 가질 수 없습니다.
- 참조형은 힙에 생성된 객체의 주소를 저장하며, 값이 없으면 null을 가질 수 있습니다.
- Java는 항상 값에 의한 전달(pass by value)이며, 참조형을 넘길 때도 "주소값의 복사본"이 전달됩니다. 그래서 메서드 안에서 필드는 바꿀 수 있어도, 매개변수 자체를 다른 객체로 바꿔도 호출한 쪽에는 영향이 없습니다.
객체지향의 4대 특징을 설명해 주세요.
캡슐화: 데이터와 그 데이터를 다루는 로직을 하나로 묶고, 내부 구현은 감춘 채 필요한 기능만 외부에 공개합니다.
상속: 상위 클래스의 속성과 기능을 물려받아 코드를 재사용하고 계층 구조를 만듭니다.
다형성: 같은 타입(부모 타입)으로 참조해도 실제 객체에 따라 다른 동작이 수행됩니다.
추상화: 공통된 특성만 뽑아내어 인터페이스나 추상 클래스로 표현하고, 구체적인 구현은 하위 클래스에 맡깁니다.
추상 클래스와 인터페이스의 차이는 무엇인가요?
- 추상 클래스는
is-a 관계로, 공통된 상태(필드)와 구현을 물려주기 위해 사용하며 단일 상속만 가능합니다.
- 인터페이스는
has-a 또는 "할 수 있다"는 역할(계약)을 정의하며, 여러 개를 동시에 구현할 수 있습니다.
- Java 8부터 인터페이스도
default, static 메서드로 구현을 가질 수 있지만, 인스턴스 필드는 가질 수 없다는 점이 여전히 다릅니다.
Overloading과 Overriding의 차이는 무엇인가요?
Overloading(오버로딩): 같은 클래스 안에서 메서드 이름은 같지만 매개변수의 타입·개수를 다르게 정의하는 것입니다. 어떤 메서드를 호출할지 컴파일 타임에 결정됩니다.
Overriding(오버라이딩): 상위 클래스의 메서드를 하위 클래스에서 같은 시그니처로 재정의하는 것입니다. 실제 객체 타입에 따라 런타임에 결정되며, 다형성의 핵심입니다.
static 키워드는 무엇이며 언제 사용하나요?
- 클래스 단위로 하나만 존재하는 멤버를 만들며, 인스턴스를 생성하지 않아도 클래스 이름으로 접근할 수 있습니다.
- 모든 인스턴스가 값을 공유하므로 상수나 유틸리티 메서드에 적합합니다.
- 주의점으로, static 멤버는 클래스가 로딩될 때 만들어져 애플리케이션 종료까지 남아 있고, 여러 스레드가 동시에 접근하므로 가변 상태를 담으면 동시성 문제가 생길 수 있습니다.
final 키워드는 어디에 붙일 수 있고 각각 어떤 의미인가요?
변수: 한 번 초기화한 뒤 재할당할 수 없습니다. 단, 참조형이라면 참조 대상은 못 바꿔도 그 객체의 내부 상태는 바뀔 수 있습니다.
메서드: 하위 클래스에서 오버라이딩할 수 없습니다.
클래스: 상속할 수 없습니다. (예: String)
접근 제어자의 종류와 범위는 무엇인가요?
private: 같은 클래스 안에서만 접근할 수 있습니다.
default(package-private): 같은 패키지 안에서만 접근할 수 있습니다.
protected: 같은 패키지 + 다른 패키지의 자식 클래스에서 접근할 수 있습니다.
public: 어디서나 접근할 수 있습니다.
- 캡슐화를 위해 기본적으로 가장 좁은 범위를 쓰고, 필요한 만큼만 열어주는 것이 원칙입니다.
1.2 객체 동작
==와 equals()의 차이는 무엇인가요?
==는 기본형에서는 값을, 참조형에서는 주소(같은 객체인지)를 비교합니다.
equals()는 논리적 동등성을 비교하며, Object의 기본 구현은 ==와 같아서 의미 있는 비교를 하려면 재정의해야 합니다.
equals()를 재정의할 때 hashCode()도 함께 재정의해야 하는 이유는 무엇인가요?
- "equals가 true인 두 객체는 hashCode도 같아야 한다"는 규약이 있기 때문입니다.
HashMap, HashSet은 먼저 hashCode로 버킷을 찾고, 그 안에서 equals로 최종 비교합니다.
- hashCode를 재정의하지 않으면 논리적으로 같은 객체라도 서로 다른 버킷으로 흩어져, 값을 넣어놓고도 찾지 못하는 문제가 생깁니다.
String, StringBuilder, StringBuffer의 차이는 무엇인가요?
String: 불변 객체라 문자열을 더할 때마다 새로운 객체가 생성됩니다.
StringBuilder: 가변이며 동기화가 없어 빠릅니다. 단일 스레드 환경의 문자열 조작에 적합합니다.
StringBuffer: 가변이며 주요 메서드가 synchronized로 동기화되어 스레드 안전하지만 그만큼 느립니다.
String이 불변(Immutable)으로 설계된 이유는 무엇인가요?
- String Pool에서 같은 문자열 리터럴을 여러 곳이 공유해도 안전합니다.
- 값이 변하지 않으므로 hashCode를 캐싱할 수 있어
HashMap의 키로 쓰기 좋습니다.
- 여러 스레드가 동시에 읽어도 동기화가 필요 없습니다.
- 파일 경로나 DB 접속 정보처럼 민감한 값이 전달된 뒤 중간에 변조되는 것을 막을 수 있습니다.
얕은 복사와 깊은 복사의 차이는 무엇인가요?
얕은 복사: 객체의 필드 값만 그대로 복사하므로, 참조형 필드는 원본과 같은 객체를 가리킵니다. 복사본에서 내부 객체를 수정하면 원본도 함께 바뀝니다.
깊은 복사: 참조하고 있는 객체까지 새로 만들어 복사하므로 원본과 완전히 독립적입니다.
Record는 무엇이며 언제 사용하나요?
- Java 16부터 정식 도입된, 불변 데이터를 담기 위한 클래스 형태입니다.
- 생성자, 접근자,
equals(), hashCode(), toString()을 컴파일러가 자동으로 만들어 줍니다.
- 모든 필드가
final이라 값이 바뀌지 않으며, DTO나 값 객체(Value Object)처럼 "데이터를 담아 전달하는 목적"에 적합합니다.
Enum은 무엇이며 어떤 장점이 있나요?
- 서로 연관된 상수들의 집합을 하나의 타입으로 정의한 것입니다.
- 정해진 값만 사용할 수 있어 타입 안전하고, 오타나 잘못된 값이 컴파일 단계에서 걸러집니다.
- 필드와 메서드를 가질 수 있어 상수마다 다른 동작을 정의할 수 있고, JVM이 인스턴스를 하나만 보장하므로 싱글턴 구현에도 사용됩니다.
1.3 JVM
Java 코드가 실행되기까지의 과정을 설명해 주세요.
javac가 .java 소스를 JVM이 이해하는 .class 바이트코드로 컴파일합니다.
- JVM의 Class Loader가 필요한 클래스를 메모리에 적재합니다.
- Execution Engine이 바이트코드를 인터프리터로 실행하다가, 자주 실행되는 부분은 JIT 컴파일러가 네이티브 코드로 변환해 성능을 높입니다.
- 바이트코드라는 중간 단계 덕분에 플랫폼에 상관없이 같은 코드를 실행할 수 있습니다.
JVM은 어떻게 구성되어 있나요?
Class Loader: 클래스 파일을 읽어 메모리에 적재합니다.
Runtime Data Area: 실행에 필요한 메모리 공간입니다.
Execution Engine: 바이트코드를 실행하며 인터프리터, JIT 컴파일러, Garbage Collector로 구성됩니다.
Class Loader는 어떤 역할을 하나요?
- 클래스 파일을 읽어 메모리에 올리고 초기화하는 역할을 합니다.
- 과정은
Loading(적재) → Linking(검증, 준비, 해석) → Initialization(정적 초기화) 순으로 진행됩니다.
- 부모에게 먼저 로딩을 위임하는 위임 모델을 사용해, 사용자가 만든 클래스가 표준 라이브러리 클래스를 덮어쓰지 못하도록 막습니다.
Runtime Data Area는 어떻게 구성되나요?
Heap: 객체와 배열이 생성되는 영역으로 모든 스레드가 공유하며, GC의 주 대상입니다.
Method Area: 클래스 메타데이터, 상수 풀, static 변수가 저장되며 모든 스레드가 공유합니다.
Stack: 스레드마다 하나씩 생성되며 메서드 호출 정보(프레임), 지역 변수, 매개변수가 저장됩니다.
PC Register: 스레드마다 현재 실행 중인 명령의 위치를 저장합니다.
Native Method Stack: 네이티브(C/C++) 메서드 호출에 사용됩니다.
Heap과 Stack의 차이는 무엇인가요?
Heap: 모든 스레드가 공유하며 객체가 저장됩니다. 크기가 동적이고 GC가 정리하며, 부족하면 OutOfMemoryError가 발생합니다.
Stack: 스레드마다 독립적이며 지역 변수와 호출 정보가 저장됩니다. 메서드가 끝나면 자동으로 정리되고, 재귀가 너무 깊으면 StackOverflowError가 발생합니다.
Metaspace는 무엇이며 PermGen과 어떻게 다른가요?
- Java 8부터 클래스 메타데이터를 저장하는 영역으로, 기존의 PermGen(Permanent Generation)을 대체했습니다.
- PermGen은 힙 안에 있고 크기가 고정되어 클래스를 많이 로딩하면
OutOfMemoryError: PermGen space가 자주 발생했습니다.
- Metaspace는 힙이 아닌 네이티브 메모리를 사용하고 기본적으로 필요에 따라 자동 확장되어 이 문제가 크게 줄었습니다.
JIT Compiler는 무엇인가요?
- Just-In-Time 컴파일러로, 실행 중 반복적으로 호출되는 바이트코드를 네이티브 기계어로 컴파일해 캐싱해 두는 기능입니다.
- 한 줄씩 해석하는 인터프리터 방식의 느린 속도를 보완하며, 실행 중 수집한 정보(어떤 분기가 자주 타는지 등)를 바탕으로 최적화까지 수행합니다.
1.4 Garbage Collection
Garbage Collection은 어떤 원리로 동작하나요?
- GC Root에서 시작해 참조를 따라가며 도달할 수 있는 객체를 살아 있는 객체로 표시(Mark)합니다.
- 도달하지 못한 객체는 더 이상 사용되지 않는다고 보고 회수(Sweep)합니다.
- 회수 후 흩어진 공간을 정리해 단편화를 줄이는 압축(Compact) 과정을 수행하기도 합니다.
GC Root란 무엇인가요?
- 객체의 생존 여부를 판단하는 탐색의 시작점입니다.
- 각 스레드 스택의 지역 변수와 매개변수, static 변수, JNI 참조, 실행 중인 스레드 등이 해당합니다.
Heap을 Young Generation과 Old Generation으로 나누는 이유는 무엇인가요?
- "대부분의 객체는 생성된 직후 금방 사용이 끝난다"는 세대 가설에 기반합니다.
- 새로 만들어진 객체를 Young 영역에 모아두고 이곳만 자주, 좁게 수집하면 적은 비용으로 대부분의 쓰레기를 회수할 수 있습니다.
- Young에서 여러 번 살아남은 객체는 오래 살 가능성이 높다고 보고 Old 영역으로 승격시켜, 상대적으로 드물게 검사합니다.
Minor GC와 Major GC의 차이는 무엇인가요?
Minor GC: Young 영역(Eden + Survivor)을 대상으로 하며, 대상 영역이 작고 살아남는 객체가 적어 빠르게 끝납니다.
Major GC(또는 Full GC): Old 영역까지 포함해 수집하며, 검사 범위가 넓어 오래 걸리고 Stop-the-World 시간도 깁니다.
- 그래서 성능 튜닝의 핵심은 Full GC의 발생 빈도와 지속 시간을 줄이는 것입니다.
Stop-the-World란 무엇인가요?
- GC를 수행하기 위해 애플리케이션의 모든 스레드를 잠시 멈추는 것입니다.
- 이 시간 동안 요청 처리가 중단되므로 응답 지연으로 이어지며, 최신 GC들은 이 시간을 줄이는 방향으로 발전해 왔습니다.
G1 GC의 특징은 무엇인가요?
- Java 9부터 기본 GC로, 힙을 Region이라는 균일한 크기의 영역으로 나누어 관리합니다.
- 각 Region의 쓰레기 비율을 추적해, 회수 효율이 좋은 Region부터 우선 수집합니다. (Garbage First)
- 목표 최대 정지 시간을 설정할 수 있어, 대용량 힙에서도 예측 가능한 응답 시간을 유지하는 데 유리합니다.
ZGC의 특징은 무엇인가요?
- 힙 크기와 거의 무관하게 정지 시간을 수 밀리초 이하로 유지하는 것을 목표로 하는 GC입니다.
- 마킹과 객체 재배치 같은 대부분의 작업을 애플리케이션 스레드와 동시에(Concurrent) 수행합니다.
- 수십 GB 이상의 대용량 힙을 쓰면서 낮은 지연 시간이 중요한 서비스에 적합합니다.
1.5 Collection
List, Set, Map의 차이는 무엇인가요?
List: 순서가 있고 중복을 허용하며 인덱스로 접근합니다.
Set: 순서를 보장하지 않고(구현체에 따라 다름) 중복을 허용하지 않습니다.
Map: 키-값 쌍으로 저장하며 키는 중복될 수 없습니다.
ArrayList와 LinkedList의 차이는 무엇인가요?
ArrayList: 내부적으로 배열을 사용해 인덱스 조회가 O(1)로 빠릅니다. 중간 삽입·삭제 시 뒤 요소를 옮겨야 해서 O(n)이고, 용량이 차면 더 큰 배열로 복사하는 비용이 발생합니다.
LinkedList: 이중 연결 리스트로, 위치를 알고 있다면 삽입·삭제가 O(1)입니다. 대신 인덱스 조회는 처음부터 순회해야 해서 O(n)입니다.
- 실무에서는 조회가 압도적으로 많고 캐시 지역성도 좋은 ArrayList를 기본으로 사용합니다.
HashMap의 내부 구조는 어떻게 되나요?
- 버킷 배열을 두고, 키의
hashCode()를 가공한 해시값으로 인덱스를 계산해 해당 버킷에 저장합니다.
- 기본 용량은 16이고 로드 팩터는 0.75로, 저장된 개수가 용량의 75%를 넘으면 배열 크기를 두 배로 늘리고 재배치(resize)합니다.
- 그래서 담을 데이터 양을 미리 알면 초기 용량을 지정해 재배치 비용을 줄일 수 있습니다.
HashMap에서 해시 충돌은 어떻게 처리되나요?
- 같은 버킷에 여러 키가 들어오면 연결 리스트 형태로 이어 붙입니다.
- Java 8부터는 한 버킷의 노드가 일정 개수(기본 8개) 이상으로 늘어나고 전체 버킷 수도 충분히 크면, 해당 버킷을 Red-Black Tree로 변환합니다.
- 이렇게 하면 충돌이 심한 최악의 경우에도 조회 성능이 O(n)에서 O(log n)으로 개선됩니다.
HashSet은 어떻게 구현되어 있나요?
- 내부적으로
HashMap을 사용하며, 넣은 값을 키로 두고 값 자리에는 의미 없는 더미 객체를 넣습니다.
- 따라서 중복 판단은
hashCode()와 equals()에 의존하며, 두 메서드를 제대로 재정의하지 않으면 중복이 걸러지지 않습니다.
TreeMap은 언제 사용하나요?
- Red-Black Tree 기반이라 키가 항상 정렬된 상태로 유지됩니다.
- 조회·삽입·삭제가 O(log n)으로 HashMap보다 느리지만, 정렬된 순회나 "특정 값 이상/이하" 같은 범위 검색이 필요할 때 적합합니다.
HashMap과 ConcurrentHashMap의 차이는 무엇인가요?
HashMap은 동기화가 전혀 없어 여러 스레드가 동시에 수정하면 데이터가 유실되거나 구조가 깨질 수 있습니다.
ConcurrentHashMap은 Java 8부터 버킷(노드) 단위로 CAS와 부분 잠금을 사용해, 서로 다른 버킷에 대한 작업은 동시에 처리할 수 있습니다.
- 맵 전체에 락을 거는
Hashtable이나 Collections.synchronizedMap보다 동시성이 훨씬 좋습니다. 단, 키와 값에 null을 허용하지 않습니다.
Iterator는 무엇이며 ConcurrentModificationException은 왜 발생하나요?
- Iterator는 컬렉션의 구현 방식과 무관하게 요소를 순차적으로 순회할 수 있게 해주는 표준 인터페이스입니다.
- 컬렉션은 구조가 변경된 횟수(modCount)를 기록하는데, 순회 도중 컬렉션에 직접 요소를 추가·삭제하면 이 값이 어긋나 예외가 발생합니다.
- 순회 중 삭제가 필요하면
Iterator.remove()나 removeIf()를 사용해야 합니다.
1.6 제네릭
제네릭은 왜 필요한가요?
- 컬렉션 등에 담기는 타입을 컴파일 시점에 지정해, 잘못된 타입이 들어가는 것을 컴파일 단계에서 잡을 수 있습니다.
- 꺼낼 때마다 명시적으로 형변환할 필요가 없어져 코드가 간결해지고
ClassCastException 위험이 줄어듭니다.
- 같은 로직을 여러 타입에 대해 재사용할 수 있습니다.
Type Erasure란 무엇인가요?
- 컴파일 시점에 제네릭 타입 정보를 검사한 뒤, 컴파일된 바이트코드에서는 타입 정보를 지우는 방식입니다.
- 제네릭이 없던 시절의 코드와 호환성을 유지하기 위한 선택입니다.
- 그 결과 런타임에는 타입 정보가 없으므로
new T[]처럼 제네릭 타입으로 배열을 만들거나, instanceof List<String> 같은 검사를 할 수 없습니다.
Wildcard(?)는 무엇인가요?
- 구체적인 타입을 지정하지 않고 "어떤 타입이든"을 표현하는 문법입니다.
List<Object>와 List<String>은 상속 관계가 아니기 때문에, 여러 타입의 제네릭을 함께 받으려면 와일드카드가 필요합니다.
? extends T와 ? super T의 차이는 무엇인가요?
? extends T(상한 경계): T와 그 하위 타입을 받습니다. 꺼낸 값이 최소한 T라는 것은 보장되므로 읽기는 안전하지만, 실제 타입을 알 수 없어 쓰기는 불가능합니다.
? super T(하한 경계): T와 그 상위 타입을 받습니다. T 타입 값을 넣는 것은 안전하지만, 꺼낼 때는 Object로만 받을 수 있습니다.
PECS 원칙이란 무엇인가요?
- "Producer-Extends, Consumer-Super"의 약자입니다.
- 데이터를 꺼내서 제공하는(생산자) 역할이면
? extends T를, 데이터를 받아서 저장하는(소비자) 역할이면 ? super T를 사용하라는 원칙입니다.
1.7 예외
Error와 Exception의 차이는 무엇인가요?
- 둘 다
Throwable을 상속하지만 성격이 다릅니다.
Error: OutOfMemoryError, StackOverflowError처럼 시스템 수준에서 발생하는 심각한 문제로, 애플리케이션이 복구할 수 없어 잡아서 처리하지 않는 것이 일반적입니다.
Exception: 애플리케이션 로직에서 발생하며 적절히 처리해 복구할 수 있는 문제입니다.
Checked Exception과 Unchecked Exception의 차이는 무엇인가요?
Checked Exception: RuntimeException을 제외한 Exception의 하위 클래스로, 컴파일러가 try-catch 또는 throws로 처리하도록 강제합니다. (예: IOException)
Unchecked Exception: RuntimeException의 하위 클래스로 처리를 강제하지 않습니다. (예: NullPointerException, IllegalArgumentException)
- Spring의
@Transactional은 기본적으로 Unchecked Exception에서만 롤백하고, Checked Exception에서는 롤백하지 않는다는 점도 함께 알아두면 좋습니다.
예외 전파는 어떻게 이루어지나요?
- 메서드 안에서 예외가 발생했는데 잡지 않으면, 호출한 메서드로 예외가 전달되며 호출 스택을 거슬러 올라갑니다.
- 최상위까지 처리되지 않으면 스레드가 종료되고 스택 트레이스가 출력됩니다.
- 계층 구조에서는 하위 계층의 예외를 그대로 던지기보다, 상위 계층에 의미 있는 예외로 감싸서(예외 번역) 던지는 것이 좋습니다. 이때 원인 예외를 함께 담아야 추적이 가능합니다.
Custom Exception은 왜 만드나요?
- 예외 이름 자체로 어떤 비즈니스 상황인지 드러나 코드의 의도가 명확해집니다.
- 예외 종류별로 다른 처리(응답 코드 매핑 등)를 하기 쉬워집니다.
- 필요한 부가 정보(에러 코드, 도메인 값)를 예외 객체에 담아 전달할 수 있습니다.
try-with-resources는 무엇인가요?
AutoCloseable을 구현한 자원을 try(...) 괄호 안에 선언하면, 블록을 벗어날 때 자동으로 close()가 호출되는 문법입니다.
finally에서 직접 닫는 방식보다 간결하고 자원 반납 누락을 막을 수 있습니다.
- 또한 close 과정에서 발생한 예외 때문에 원래 예외가 덮이는 문제도 방지해 줍니다.
1.8 동시성
Thread와 Runnable 중 무엇을 사용해야 하나요?
Thread를 상속하면 이미 상속을 사용해 버려 다른 클래스를 상속할 수 없습니다.
Runnable은 인터페이스라 이런 제약이 없고, "실행할 작업"과 "실행하는 주체"를 분리할 수 있어 재사용성이 좋습니다.
- 그래서 일반적으로
Runnable(또는 반환값이 필요하면 Callable) 구현을 선호하며, 실무에서는 직접 스레드를 만들기보다 스레드 풀에 작업을 제출합니다.
ExecutorService와 Thread Pool을 사용하는 이유는 무엇인가요?
- 요청마다 스레드를 새로 만들면 생성·소멸 비용이 크고, 스레드 수가 무한히 늘어나 메모리 부족이나 과도한 컨텍스트 스위칭이 발생할 수 있습니다.
- 스레드 풀은 미리 만든 스레드를 재사용하고 동시 실행 개수를 제한해 자원 사용을 예측 가능하게 만듭니다.
ExecutorService는 작업 제출, 결과 조회(Future), 종료 관리를 표준화된 방식으로 제공합니다.
synchronized는 어떻게 동작하나요?
- 객체마다 존재하는 모니터 락을 획득한 스레드만 임계 구역에 진입할 수 있게 합니다.
- 메서드에 붙이면 인스턴스 메서드는 해당 인스턴스를, static 메서드는 클래스 객체를 락 대상으로 사용합니다.
- 상호 배제뿐 아니라, 락을 해제할 때 변경 내용을 메인 메모리에 반영해 가시성도 함께 보장합니다.
volatile은 무엇을 보장하고 무엇을 보장하지 않나요?
- 보장하는 것: 가시성입니다. 변수를 CPU 캐시가 아닌 메인 메모리에서 직접 읽고 쓰게 해, 한 스레드의 변경을 다른 스레드가 즉시 볼 수 있습니다. 또한 해당 변수 주변의 명령어 재정렬을 막습니다.
- 보장하지 않는 것: 원자성입니다.
count++처럼 읽고-더하고-쓰는 복합 연산은 여전히 안전하지 않습니다.
- 그래서 여러 스레드가 값을 갱신하는 상황에는
Atomic 클래스나 락을 써야 하고, volatile은 주로 플래그 값처럼 한 스레드가 쓰고 여러 스레드가 읽는 경우에 적합합니다.
Atomic 클래스는 어떻게 동시성을 보장하나요?
- CAS(Compare-And-Swap) 연산을 사용합니다. "현재 값이 내가 읽은 값과 같다면 새 값으로 바꾼다"를 하드웨어 수준의 원자적 명령으로 수행합니다.
- 값이 그사이 바뀌었다면 실패하고, 다시 읽어서 재시도합니다.
- 락을 걸지 않아 컨텍스트 스위칭 비용이 없지만, 경쟁이 매우 심하면 재시도가 반복되어 오히려 비효율적일 수 있습니다.
synchronized와 ReentrantLock의 차이는 무엇인가요?
synchronized는 블록을 벗어나면 락이 자동으로 해제되어 간결하고 실수가 적습니다.
ReentrantLock은 unlock()을 finally에서 직접 호출해야 하지만, 타임아웃을 두고 락을 시도하거나(tryLock), 대기 중 인터럽트를 받거나, 공정성(먼저 기다린 스레드 우선)을 설정하는 등 더 세밀한 제어가 가능합니다.
- 특별한 제어가 필요 없다면
synchronized를, 세밀한 제어가 필요하면 ReentrantLock을 사용합니다.
CompletableFuture는 무엇인가요?
- 비동기 작업의 결과를 표현하며, 작업들을 연결하고 조합할 수 있는 클래스입니다.
- 기존
Future는 결과를 얻으려면 get()으로 블로킹해야 했지만, CompletableFuture는 thenApply, thenCompose 같은 메서드로 완료 후 수행할 작업을 콜백처럼 연결할 수 있습니다.
allOf 등을 이용해 여러 비동기 작업을 병렬로 실행하고 모두 완료되길 기다리는 조합도 가능합니다.
ThreadLocal은 무엇이며 사용 시 주의점은 무엇인가요?
- 스레드마다 독립적인 값을 저장하는 공간으로, 값을 공유하지 않으므로 동기화 없이 안전하게 사용할 수 있습니다.
- 사용자 인증 정보나 트랜잭션 컨텍스트처럼 요청 처리 전 구간에서 참조해야 하는 값을 전달할 때 사용합니다.
- 주의점은 스레드 풀 환경입니다. 스레드가 재사용되므로
remove()로 정리하지 않으면 이전 요청의 값이 다음 요청에 그대로 남아 잘못 참조되거나 메모리 누수로 이어질 수 있습니다.
Virtual Thread는 무엇이며 왜 등장했나요?
- Java 21에서 정식 도입된, JVM이 관리하는 경량 스레드입니다.
- 기존 스레드는 OS 스레드와 1:1로 대응되어 개수를 늘리는 데 한계가 있었고, I/O 대기 중에도 OS 스레드를 점유해 낭비가 컸습니다.
- Virtual Thread는 블로킹 I/O를 만나면 실제 OS 스레드(캐리어 스레드)를 반납하고 대기하므로, 적은 수의 OS 스레드로 수십만 개의 작업을 처리할 수 있습니다.
- 다만 CPU 연산이 병목인 작업에서는 이점이 없으며, 실제 병렬성은 여전히 코어 수에 제한됩니다.
2. Python
2.1 언어 기초
Python이 동적 타입 언어라는 것은 무슨 의미인가요?
- 변수에 타입이 정해져 있는 것이 아니라, 값(객체)이 타입을 가지고 변수는 그 객체를 참조하기만 합니다.
- 타입 검사가 실행 시점에 이루어지므로 코드가 유연하고 간결해집니다.
- 대신 타입 오류가 런타임에야 드러나므로, Type Hint와 정적 분석 도구로 보완하는 것이 일반적입니다.
Duck Typing이란 무엇인가요?
- "오리처럼 걷고 오리처럼 운다면 오리로 취급한다"는 의미로, 객체의 실제 타입이 아니라 필요한 메서드나 속성을 가지고 있는지로 사용 가능 여부를 판단하는 방식입니다.
- 특정 인터페이스를 명시적으로 상속하지 않아도 동일한 메서드만 구현하면 같은 자리에 쓸 수 있어 유연합니다.
Mutable과 Immutable 객체의 차이는 무엇인가요?
Immutable: 생성 후 값을 바꿀 수 없습니다. int, float, str, tuple, frozenset이 해당합니다.
Mutable: 생성 후에도 내부 상태를 바꿀 수 있습니다. list, dict, set이 해당합니다.
- 함수의 기본 인자로 mutable 객체를 쓰면(
def f(x=[])) 그 객체가 함수 정의 시점에 한 번만 생성되어 호출 간에 공유되므로, 기본값은 None으로 두고 함수 안에서 생성하는 것이 안전합니다.
is와 ==의 차이는 무엇인가요?
==는 값이 같은지를 비교하며 __eq__ 메서드로 동작합니다.
is는 두 변수가 완전히 같은 객체(같은 id)를 가리키는지 비교합니다.
None 비교처럼 유일한 객체를 확인할 때는 is를, 값 비교에는 ==를 사용해야 합니다.
얕은 복사와 깊은 복사는 어떻게 다른가요?
copy.copy()(얕은 복사)는 최상위 객체만 새로 만들고 내부 요소는 원본과 같은 객체를 참조합니다. 중첩된 리스트를 수정하면 원본도 함께 바뀝니다.
copy.deepcopy()(깊은 복사)는 내부에 중첩된 객체까지 재귀적으로 새로 만들어 원본과 완전히 분리됩니다.
- 슬라이싱(
lst[:])이나 list()도 얕은 복사에 해당합니다.
Python의 상속과 MRO는 어떻게 동작하나요?
- 여러 클래스를 동시에 상속하는 다중 상속을 지원합니다.
- 이때 어떤 순서로 메서드를 찾을지 결정하는 규칙이 MRO(Method Resolution Order)이며, C3 선형화 알고리즘으로 계산됩니다.
클래스.__mro__로 확인할 수 있습니다.
super()는 단순히 부모 클래스를 가리키는 것이 아니라 MRO상 다음 클래스를 호출하므로, 다중 상속에서도 각 클래스의 메서드가 중복 없이 한 번씩 실행됩니다.
2.2 자료구조
List와 Tuple의 차이는 무엇인가요?
List: 가변이며 요소를 추가·삭제·수정할 수 있습니다. 내부적으로 동적 배열이라 인덱스 접근은 O(1), append는 상각 O(1)입니다.
Tuple: 불변이라 생성 후 변경할 수 없습니다. 값이 바뀌지 않으므로 해시 가능해(요소가 모두 해시 가능하다면) 딕셔너리의 키나 집합의 원소로 쓸 수 있습니다.
- 변하지 않아야 하는 묶음 데이터에는 Tuple이 의도를 더 명확히 드러냅니다.
Set과 Dictionary는 어떻게 동작하나요?
- 둘 다 해시 테이블 기반이라 조회·삽입·삭제가 평균 O(1)입니다.
Set은 중복을 허용하지 않아 중복 제거나 포함 여부 검사에 유리합니다. 리스트에서 in 검사를 하면 O(n)이지만 집합은 평균 O(1)입니다.
Dictionary는 키-값 쌍을 저장하며, Python 3.7부터 삽입 순서가 보장됩니다.
Comprehension은 무엇이며 왜 사용하나요?
- 반복문과 조건문을 한 줄의 표현식으로 작성해 리스트·딕셔너리·집합을 생성하는 문법입니다.
- 의도가 한눈에 드러나 가독성이 좋고, 일반 반복문에
append를 쓰는 것보다 내부적으로 최적화되어 있어 조금 더 빠릅니다.
- 다만 조건이 여러 겹으로 중첩되면 오히려 읽기 어려워지므로 그럴 때는 일반 반복문이 낫습니다.
2.3 함수
Python에서 함수가 일급 객체(First-class Function)라는 것은 무슨 의미인가요?
- 함수를 변수에 할당하고, 다른 함수의 인자로 전달하고, 반환값으로 돌려줄 수 있다는 뜻입니다.
- 이 성질 덕분에 콜백, 고차 함수, 데코레이터 같은 패턴을 자연스럽게 사용할 수 있습니다.
Closure란 무엇인가요?
- 외부 함수의 지역 변수를 내부 함수가 참조할 때, 외부 함수가 종료된 뒤에도 그 변수가 유지되는 구조입니다.
- 내부 함수가 자신이 정의된 환경을 함께 기억하기 때문에 가능합니다.
- 내부에서 그 변수를 수정하려면
nonlocal 키워드가 필요합니다.
Decorator는 무엇이며 어떻게 동작하나요?
- 기존 함수를 수정하지 않고 앞뒤에 공통 기능을 덧붙이는 문법입니다.
- 함수를 인자로 받아 감싼 새로운 함수를 반환하는 구조이며,
@데코레이터는 func = 데코레이터(func)와 같습니다.
- 로깅, 실행 시간 측정, 인증 확인처럼 여러 함수에 반복되는 관심사를 분리할 때 사용합니다.
- 감싸면 원본 함수의 이름이나 docstring이 가려지므로
functools.wraps를 함께 쓰는 것이 좋습니다.
*args와 **kwargs는 무엇인가요?
*args: 개수가 정해지지 않은 위치 인자들을 튜플로 받습니다.
**kwargs: 개수가 정해지지 않은 키워드 인자들을 딕셔너리로 받습니다.
- 함수를 호출할 때 사용하면 반대로 시퀀스나 딕셔너리를 인자들로 풀어서 전달하는 언패킹 역할을 합니다.
Type Hint는 런타임에 영향을 주나요?
- 기본적으로 영향을 주지 않습니다. 인터프리터는 타입 힌트를 강제하지 않으므로, 명시한 타입과 다른 값을 넘겨도 실행 자체는 됩니다.
- 주된 목적은 mypy 같은 정적 분석 도구와 IDE가 오류를 미리 잡고 자동 완성을 제공하도록 하는 것입니다.
- 다만 Pydantic이나 FastAPI처럼 타입 힌트를 읽어 런타임 검증에 활용하는 라이브러리도 있습니다.
2.4 Iterator와 Generator
Iterable과 Iterator의 차이는 무엇인가요?
Iterable: __iter__()를 구현해 반복할 수 있는 객체입니다. 리스트, 튜플, 문자열 등이 해당합니다.
Iterator: __iter__()와 __next__()를 모두 구현해, 실제로 다음 값을 하나씩 꺼내주는 객체입니다. 더 이상 값이 없으면 StopIteration을 발생시킵니다.
for 문은 내부적으로 Iterable에서 Iterator를 얻은 뒤 __next__()를 반복 호출하는 방식으로 동작합니다.
Generator는 무엇이며 어떤 장점이 있나요?
- 값을 미리 모두 만들어 두지 않고, 필요한 시점에 하나씩 생성해서 돌려주는 Iterator입니다.
- 전체 데이터를 메모리에 올리지 않으므로 대용량 데이터나 무한 수열을 다룰 때 메모리를 크게 절약할 수 있습니다.
yield를 사용하는 제너레이터 함수나 제너레이터 표현식으로 만듭니다.
yield는 어떻게 동작하나요?
- 함수 실행을 그 지점에서 일시 중단하고 값을 반환하며, 지역 변수와 실행 위치 같은 상태를 그대로 유지합니다.
- 다음에 값을 요청하면 중단된 지점부터 이어서 실행됩니다.
return이 함수를 완전히 끝내는 것과 달리, yield는 필요할 때마다 값을 하나씩 흘려보내는 지연 평가(Lazy Evaluation)를 가능하게 합니다.
2.5 메모리
Python의 메모리 관리 방식은 어떻게 되나요?
- CPython은 참조 카운팅(Reference Counting)을 기본으로 사용합니다. 객체를 참조하는 개수를 세다가 0이 되면 즉시 메모리를 해제합니다.
- 참조 카운팅만으로 해결되지 않는 순환 참조를 정리하기 위해 세대별(generational) GC를 보조로 함께 사용합니다.
Reference Counting만으로 부족한 이유는 무엇인가요?
- 두 객체가 서로를 참조하는 순환 참조가 생기면, 외부에서 아무도 사용하지 않아도 카운트가 0이 되지 않아 영원히 해제되지 않습니다.
- 이를 해결하기 위해 순환 참조를 탐지해 수거하는 GC가 필요하며,
gc 모듈로 제어할 수 있습니다.
Object Interning이란 무엇인가요?
- 자주 쓰이는 작은 객체를 미리 만들어 두고 재사용하는 최적화입니다.
- CPython은 -5부터 256까지의 작은 정수와 일부 짧은 문자열을 캐싱합니다.
- 그래서
a = 100; b = 100일 때 a is b가 True지만, 큰 수에서는 False가 될 수 있습니다. 값 비교에 is를 쓰면 안 되는 이유이기도 합니다.
GIL이란 무엇이며 어떤 영향을 주나요?
- Global Interpreter Lock으로, 한 시점에 하나의 스레드만 Python 바이트코드를 실행하도록 제한하는 락입니다.
- 참조 카운팅 같은 내부 자료구조를 락 없이 안전하게 다루기 위해 도입되었습니다.
- 그 결과 CPU 연산이 병목인 작업은 멀티스레딩으로 성능이 개선되지 않습니다. 이 경우
multiprocessing으로 별도 프로세스를 띄워 우회합니다.
- 반면 I/O 대기 중에는 GIL이 해제되므로, I/O가 병목인 작업에서는 멀티스레딩이 여전히 효과가 있습니다.
2.6 비동기
Coroutine이란 무엇인가요?
- 실행 도중 스스로 제어권을 반납했다가 나중에 그 지점부터 다시 이어서 실행할 수 있는 함수입니다.
async def로 정의하며, 호출해도 즉시 실행되지 않고 코루틴 객체를 반환합니다. 이벤트 루프가 스케줄링해야 실제로 실행됩니다.
- 스레드처럼 OS가 강제로 전환하는 것이 아니라,
await 지점에서 스스로 양보하는 협력적 방식이라 전환 비용이 훨씬 작습니다.
Event Loop는 어떻게 동작하나요?
- 실행할 작업들을 큐로 관리하면서, 하나를 실행하다가
await로 대기 상태가 되면 다른 작업으로 전환하는 단일 스레드 루프입니다.
- I/O가 완료되면 대기 중이던 작업을 다시 실행 가능 상태로 되돌립니다.
- 단일 스레드로 동작하므로, 코루틴 안에서 시간이 오래 걸리는 동기 작업을 수행하면 루프 전체가 멈춥니다. 이런 작업은
run_in_executor로 별도 스레드나 프로세스에 넘겨야 합니다.
async와 await는 각각 무슨 역할인가요?
async: 해당 함수가 코루틴임을 선언합니다.
await: 다른 코루틴이나 비동기 작업의 완료를 기다리며, 기다리는 동안 이벤트 루프에 제어권을 넘겨 다른 작업이 실행될 수 있게 합니다.
await는 async 함수 안에서만 사용할 수 있습니다.
CPU-bound와 I/O-bound 작업은 각각 어떻게 처리해야 하나요?
I/O-bound: 네트워크 요청이나 파일 입출력처럼 대기 시간이 병목인 작업입니다. asyncio나 스레딩으로 대기 시간 동안 다른 작업을 처리해 처리량을 크게 높일 수 있습니다.
CPU-bound: 연산 자체가 병목인 작업입니다. GIL 때문에 스레드를 늘려도 효과가 없으므로 multiprocessing으로 여러 프로세스에 분산해야 합니다.
Threading과 Multiprocessing은 언제 각각 사용하나요?
Threading: 같은 프로세스 안에서 메모리를 공유하므로 생성 비용이 작고 데이터 공유가 쉽습니다. GIL의 영향을 받으므로 I/O-bound 작업에 적합합니다.
Multiprocessing: 프로세스마다 독립된 인터프리터와 메모리를 가져 GIL 제약을 받지 않습니다. CPU-bound 작업에 적합하지만, 프로세스 생성 비용이 크고 데이터를 주고받을 때 직렬화 비용이 발생합니다.
2.7 패키지 관리
Module과 Package의 차이는 무엇인가요?
Module: 하나의 .py 파일로, 함수와 클래스를 담는 최소 단위입니다.
Package: 여러 모듈을 담은 디렉터리로, 전통적으로 __init__.py를 포함합니다. (Python 3.3부터는 이 파일이 없어도 네임스페이스 패키지로 인식됩니다)
가상 환경(Virtual Environment)은 왜 사용하나요?
- 프로젝트마다 필요한 패키지 버전이 다를 수 있는데, 전역에 설치하면 서로 충돌합니다.
- 가상 환경은 프로젝트별로 독립된 패키지 설치 공간을 제공해 이런 의존성 충돌을 막습니다.
- 또한 어떤 패키지가 실제로 필요한지 명확해져, 배포 환경을 동일하게 재현하기 쉬워집니다.
pip와 Poetry의 차이는 무엇인가요?
pip: 패키지를 설치하는 기본 도구입니다. 의존성의 의존성까지 정확히 고정하려면 별도의 작업이 필요합니다.
Poetry: 의존성 해결, 잠금 파일(poetry.lock) 관리, 가상 환경 생성, 빌드·배포까지 통합해서 제공합니다.
- 잠금 파일 덕분에 모든 개발자와 배포 환경이 정확히 동일한 버전을 설치하도록 보장할 수 있습니다.
requirements.txt와 pyproject.toml의 차이는 무엇인가요?
requirements.txt: 설치할 패키지 목록을 나열한 단순한 텍스트 파일입니다.
pyproject.toml: 프로젝트 메타데이터, 의존성, 빌드 설정, 각종 도구 설정까지 한 파일에 담는 표준 형식입니다.
- 최근에는
pyproject.toml이 표준으로 자리 잡아, 여러 설정 파일로 흩어져 있던 내용을 한곳에서 관리하는 방향으로 가고 있습니다.
3. C++
3.1 메모리
Stack과 Heap 메모리의 차이는 무엇인가요?
Stack: 지역 변수와 함수 호출 정보가 저장되며, 스코프를 벗어나면 자동으로 해제됩니다. 할당·해제가 매우 빠르지만 크기가 제한적입니다.
Heap: new나 malloc으로 직접 할당하는 영역으로, 개발자가 명시적으로 해제해야 합니다. 크기가 크고 수명을 자유롭게 제어할 수 있지만 관리 부담과 단편화 문제가 있습니다.
포인터와 참조자(Reference)의 차이는 무엇인가요?
포인터: 주소를 담는 변수로, 선언 후 나중에 초기화할 수 있고 다른 대상을 가리키도록 바꿀 수 있으며 nullptr을 가질 수 있습니다.
참조자: 기존 객체의 별칭으로, 선언과 동시에 반드시 초기화해야 하고 이후 다른 객체를 참조하도록 바꿀 수 없으며 null이 될 수 없습니다.
- 반드시 유효한 대상이 있고 재바인딩이 필요 없다면 참조자가 더 안전하고 의도가 명확합니다.
new/delete와 malloc/free의 차이는 무엇인가요?
new는 메모리를 할당한 뒤 생성자를 호출하고, delete는 소멸자를 호출한 뒤 메모리를 해제합니다. malloc/free는 생성자·소멸자를 호출하지 않습니다.
new는 타입에 맞는 포인터를 반환해 형변환이 필요 없지만, malloc은 void*를 반환합니다.
- 할당 실패 시
new는 std::bad_alloc 예외를 던지고 malloc은 NULL을 반환합니다.
new[]로 할당했다면 반드시 delete[]로 해제해야 하며, 짝을 맞추지 않으면 정의되지 않은 동작이 발생합니다.
Memory Leak은 왜 발생하며 어떻게 방지하나요?
- 힙에 할당한 메모리를 해제하지 않아 프로그램이 계속 점유하는 상태입니다.
- 해제를 잊거나, 중간에 예외가 발생해
delete까지 도달하지 못하거나, 포인터를 덮어써서 원래 주소를 잃어버릴 때 발생합니다.
- RAII 원칙에 따라 스마트 포인터나 표준 컨테이너에 자원 관리를 맡기면 스코프를 벗어날 때 자동으로 해제되어 근본적으로 방지할 수 있습니다.
Dangling Pointer란 무엇인가요?
- 이미 해제되었거나 수명이 끝난 메모리를 여전히 가리키고 있는 포인터입니다.
- 이를 역참조하면 정의되지 않은 동작이 발생하며, 즉시 크래시가 나지 않고 엉뚱한 값을 읽어 디버깅이 매우 어렵습니다.
- 해제 후
nullptr을 대입하거나, 스마트 포인터로 소유권을 명확히 하고 관찰용으로는 weak_ptr을 사용해 방지합니다.
3.2 객체
생성자와 소멸자는 언제 호출되나요?
- 생성자는 객체가 만들어질 때 호출되어 멤버를 초기화합니다. 멤버 초기화 리스트를 사용하면 기본 생성 후 대입하는 과정 없이 바로 초기화되어 더 효율적입니다.
- 소멸자는 객체의 수명이 끝날 때 호출됩니다. 스택 객체는 스코프를 벗어날 때, 힙 객체는
delete 시점에 호출됩니다.
- 소멸자는 객체가 획득한 자원(메모리, 파일 핸들 등)을 반납하는 자리이며, 이것이 RAII의 기반입니다.
복사 생성자와 이동 생성자의 차이는 무엇인가요?
복사 생성자: 원본을 그대로 두고 자원을 새로 복제합니다. 깊은 복사가 필요하면 비용이 큽니다.
이동 생성자: 원본이 가진 자원의 소유권을 그대로 가져오고, 원본은 안전하게 비워둡니다. 복사 없이 포인터만 옮기므로 훨씬 빠릅니다.
- 이동 생성자는 임시 객체(rvalue)가 전달될 때처럼, 원본을 더 이상 쓰지 않아도 되는 상황에서 선택됩니다.
Rule of Three, Rule of Five, Rule of Zero는 무엇인가요?
Rule of Three: 소멸자, 복사 생성자, 복사 대입 연산자 중 하나라도 직접 정의했다면 나머지도 함께 정의해야 한다는 원칙입니다. 자원을 직접 관리한다는 뜻이므로, 기본 제공되는 얕은 복사를 쓰면 같은 포인터를 두 번 해제하는 문제가 생깁니다.
Rule of Five: C++11부터 이동 생성자와 이동 대입 연산자가 추가되어 다섯 개를 함께 고려해야 한다는 원칙입니다.
Rule of Zero: 애초에 자원을 직접 관리하지 말고 스마트 포인터와 표준 컨테이너에 위임해, 이 특수 멤버 함수들을 하나도 정의하지 않는 것이 가장 좋다는 원칙입니다. 현대 C++에서 권장되는 방향입니다.
3.3 자원 관리
RAII란 무엇인가요?
- Resource Acquisition Is Initialization의 약자로, 자원의 수명을 객체의 수명에 묶는 기법입니다.
- 생성자에서 자원을 획득하고 소멸자에서 반납하므로, 객체가 스코프를 벗어나면 자원이 자동으로 정리됩니다.
- 중간에 예외가 발생해도 스택이 풀리면서(stack unwinding) 소멸자가 반드시 호출되므로 자원 누수를 근본적으로 막을 수 있습니다.
unique_ptr, shared_ptr, weak_ptr의 차이는 무엇인가요?
unique_ptr: 하나의 소유자만 가지며 복사는 불가능하고 이동만 가능합니다. 오버헤드가 거의 없어 소유권이 명확한 대부분의 상황에서 기본 선택지입니다.
shared_ptr: 참조 카운트를 두어 여러 곳이 소유권을 공유하고, 카운트가 0이 되면 해제합니다. 제어 블록과 원자적 카운트 연산 때문에 오버헤드가 있습니다.
weak_ptr: 소유권 없이 객체를 관찰만 합니다. 참조 카운트를 늘리지 않으며, 사용할 때 lock()으로 shared_ptr을 얻어 객체가 살아 있는지 확인합니다.
- 생성 시에는
make_unique, make_shared를 쓰는 것이 예외 안전성과 성능 면에서 좋습니다.
shared_ptr의 순환 참조 문제는 어떻게 해결하나요?
- 두 객체가 서로를
shared_ptr로 참조하면 참조 카운트가 서로를 붙잡아 절대 0이 되지 않고, 결국 메모리 누수가 발생합니다.
- 한쪽 방향의 참조를
weak_ptr로 바꾸면 참조 카운트를 증가시키지 않으므로 순환이 끊어집니다.
- 보통 부모가 자식을
shared_ptr로, 자식이 부모를 weak_ptr로 참조하는 형태로 설계합니다.
3.4 객체지향
C++에서 다형성은 어떻게 구현되나요?
- 가상 함수(
virtual)를 통해 구현됩니다.
- 가상 함수가 있는 클래스마다 함수 포인터 테이블인 vtable이 만들어지고, 각 객체는 자신의 vtable을 가리키는 포인터(vptr)를 가집니다.
- 부모 타입의 포인터로 호출해도 vptr을 따라가 실제 객체 타입의 함수가 실행되며, 이를 동적 바인딩이라고 합니다.
- 대신 함수 호출이 한 단계 간접적으로 이루어지고 인라인 최적화가 어려워 약간의 비용이 발생합니다.
가상 소멸자는 왜 필요한가요?
- 부모 클래스 포인터로 자식 객체를 가리키다가
delete할 때, 소멸자가 가상이 아니면 부모의 소멸자만 호출됩니다.
- 그러면 자식 클래스가 가진 자원이 해제되지 않아 메모리 누수가 발생하고, 표준상 정의되지 않은 동작입니다.
- 따라서 다형적으로 사용될 기반 클래스의 소멸자는 반드시
virtual로 선언해야 합니다.
순수 가상 함수와 추상 클래스는 무엇인가요?
- 선언 끝에
= 0을 붙여 구현 없이 선언만 한 함수가 순수 가상 함수입니다.
- 순수 가상 함수를 하나라도 가진 클래스는 추상 클래스가 되어 직접 인스턴스를 만들 수 없습니다.
- 파생 클래스가 반드시 해당 함수를 구현하도록 강제하는 인터페이스 역할을 합니다.
다중 상속의 다이아몬드 문제는 무엇이며 어떻게 해결하나요?
- 두 부모 클래스가 같은 조상을 상속받고, 자식이 그 두 부모를 모두 상속하면 조상의 멤버가 두 벌 존재하게 됩니다.
- 이로 인해 어느 쪽 멤버를 참조하는지 모호해지고 메모리도 낭비됩니다.
- 중간 클래스들이 조상을
virtual로 상속(가상 상속)하면 조상의 인스턴스가 하나만 유지되어 문제가 해결됩니다.
3.5 STL
vector, list, deque의 차이는 무엇인가요?
vector: 연속된 메모리의 동적 배열입니다. 임의 접근이 O(1)이고 캐시 효율이 좋습니다. 끝에 추가는 상각 O(1)이지만 중간 삽입·삭제는 O(n)입니다.
list: 이중 연결 리스트입니다. 위치를 알고 있다면 삽입·삭제가 O(1)이지만 임의 접근은 불가능하고 캐시 효율이 나쁩니다.
deque: 양쪽 끝에서의 삽입·삭제가 모두 O(1)이고 임의 접근도 O(1)입니다. 다만 내부적으로 여러 청크로 나뉘어 있어 메모리가 완전히 연속적이지는 않습니다.
- 특별한 이유가 없으면 캐시 지역성이 좋은
vector가 기본 선택지입니다.
map과 unordered_map의 차이는 무엇인가요?
map: Red-Black Tree 기반으로 키가 정렬된 상태를 유지하며, 조회·삽입·삭제가 O(log n)입니다. 순서대로 순회하거나 범위 검색이 필요할 때 사용합니다.
unordered_map: 해시 테이블 기반으로 평균 O(1)이지만 최악의 경우 O(n)이며 순서를 보장하지 않습니다.
- 정렬이 필요 없고 조회 성능이 중요하다면
unordered_map이 유리합니다.
priority_queue는 어떻게 구현되어 있나요?
- 기본적으로
vector를 기반으로 한 이진 힙으로 구현되어 있습니다.
- 기본 설정은 최대 힙이라 가장 큰 값이 먼저 나오며, 비교 함수를 바꾸면 최소 힙으로 쓸 수 있습니다.
- 삽입과 최댓값 삭제가 O(log n)이고, 최댓값 조회는 O(1)입니다.
반복자 무효화(Iterator Invalidation)는 언제 발생하나요?
vector는 요소를 추가하다 용량이 초과되어 재할당이 일어나면 기존의 모든 반복자와 포인터가 무효화됩니다. 중간 삽입·삭제 시에는 그 지점 이후가 무효화됩니다.
list나 map은 노드 기반이라 삭제된 원소를 가리키던 반복자만 무효화되고 나머지는 유효합니다.
- 무효화된 반복자를 사용하면 정의되지 않은 동작이 발생하므로, 순회 중 원소를 지울 때는
erase가 반환하는 다음 반복자를 받아서 사용해야 합니다.
3.6 고급 개념
Template은 무엇이며 어떻게 동작하나요?
- 타입을 매개변수로 받아, 여러 타입에 대해 동작하는 코드를 한 번만 작성할 수 있게 하는 기능입니다.
- 컴파일 시점에 실제 사용된 타입별로 코드가 생성(인스턴스화)되므로 런타임 오버헤드가 없습니다.
- 컴파일러가 인스턴스화하려면 정의 전체가 보여야 하기 때문에, 템플릿 구현은 보통 헤더 파일에 둡니다.
- 특정 타입에 대해서만 다르게 동작시키고 싶을 때는 템플릿 특수화(Specialization)를 사용합니다.
Lvalue와 Rvalue의 차이는 무엇인가요?
Lvalue: 이름이 있고 주소를 얻을 수 있는, 표현식 이후에도 지속되는 값입니다. (변수 등)
Rvalue: 임시적으로 만들어져 곧 사라지는 값입니다. (리터럴, 함수의 반환 임시 객체 등)
- Rvalue는 어차피 사라질 값이므로, 자원을 복사하지 않고 그대로 가져와도 안전하다는 것이 이동 시맨틱의 출발점입니다.
Move Semantics는 왜 필요한가요?
- 임시 객체를 다룰 때 자원을 깊은 복사하면 불필요한 할당과 복사 비용이 발생합니다.
- 이동 시맨틱은 원본의 자원 포인터를 그대로 넘겨받고 원본은 비워두는 방식으로, 실제 데이터 복사 없이 소유권만 옮깁니다.
- 큰 벡터나 문자열을 반환하고 전달할 때 성능이 크게 개선됩니다.
std::move는 실제로 무엇을 하나요?
- 이름과 달리 아무것도 이동시키지 않습니다. 인자를 rvalue reference 타입으로 캐스팅할 뿐입니다.
- 이 캐스팅 덕분에 오버로드 해석에서 복사 생성자 대신 이동 생성자가 선택되고, 실제 이동은 그 이동 생성자가 수행합니다.
std::move를 적용한 객체는 유효하지만 값이 불분명한 상태가 되므로, 이후에 값을 그대로 사용해서는 안 됩니다.
Perfect Forwarding이란 무엇인가요?
- 템플릿 함수가 전달받은 인자의 lvalue/rvalue 성질과 const 여부를 그대로 유지한 채 다른 함수로 넘기는 기법입니다.
- 템플릿 매개변수에
T&&(forwarding reference)를 쓰고 std::forward<T>로 전달해서 구현합니다.
- 이렇게 하지 않으면 rvalue로 넘어온 인자가 중간에서 lvalue가 되어, 이동이 가능한 상황인데도 불필요한 복사가 발생합니다.
Undefined Behavior란 무엇인가요?
- 표준이 동작을 전혀 규정하지 않은 코드로, 컴파일러가 어떤 결과를 내놓아도 규격 위반이 아닌 상태입니다.
- 널 포인터 역참조, 배열 범위를 벗어난 접근, 해제된 메모리 사용, 부호 있는 정수의 오버플로 등이 해당합니다.
- 가장 위험한 점은 즉시 크래시가 나지 않고 우연히 잘 동작하는 것처럼 보이다가, 컴파일러 버전이나 최적화 옵션이 바뀌면 갑자기 오동작한다는 것입니다.
컴파일, 링크, 런타임 단계에서 각각 어떤 일이 일어나나요?
컴파일: 전처리기가 헤더를 포함하고 매크로를 치환한 뒤, 각 소스 파일을 오브젝트 파일로 변환합니다. 문법 오류와 타입 오류가 여기서 잡힙니다.
링크: 여러 오브젝트 파일과 라이브러리를 묶어 심볼 참조를 실제 주소로 연결하고 실행 파일을 만듭니다. 함수를 선언만 하고 정의하지 않은 경우 이 단계에서 오류가 납니다.
런타임: 실행 파일이 메모리에 적재되어 실제로 동작하며, 널 포인터 접근 같은 문제는 이 시점에 드러납니다.
4. PostgreSQL
4.1 PostgreSQL 구조
PostgreSQL이 프로세스 기반 구조라는 것은 무슨 의미인가요?
- 클라이언트가 연결할 때마다 별도의 백엔드 프로세스를 생성해 그 연결의 쿼리를 전담 처리하는 구조입니다.
- 스레드 기반인 MySQL과 달리 연결끼리 메모리가 격리되어, 한 연결에서 문제가 생겨도 다른 연결에 영향이 적습니다.
- 대신 프로세스 생성 비용이 커서 연결이 잦으면 부담이 큽니다. 그래서 PgBouncer 같은 커넥션 풀러를 함께 쓰는 것이 일반적입니다.
클라이언트 연결은 어떻게 처리되나요?
- Postmaster라는 메인 프로세스가 연결 요청을 받습니다.
- 인증을 거친 뒤 해당 연결을 담당할 백엔드 프로세스를 생성하고, 이후 클라이언트는 그 백엔드 프로세스와 직접 통신합니다.
- 이와 별개로 Autovacuum Launcher, WAL Writer, Checkpointer 같은 백그라운드 프로세스들이 함께 동작합니다.
Shared Buffer는 어떤 역할을 하나요?
- 디스크의 데이터 페이지를 메모리에 캐싱해 두는 공유 메모리 영역입니다.
- 자주 읽는 페이지를 메모리에서 바로 제공해 디스크 I/O를 줄입니다.
- 데이터를 수정하면 먼저 이 버퍼가 변경되어 더티(dirty) 상태가 되고, 나중에 디스크로 반영됩니다.
WAL이란 무엇이며 왜 필요한가요?
- Write-Ahead Logging으로, 데이터 파일을 실제로 변경하기 전에 변경 내역을 로그로 먼저 디스크에 기록하는 방식입니다.
- 커밋 시점에 데이터 파일 전체가 아니라 WAL만 디스크에 확실히 기록하면 되므로, 랜덤 쓰기를 순차 쓰기로 바꿔 성능을 확보하면서 지속성(Durability)을 보장합니다.
- 장애가 발생해도 WAL을 재생해 복구할 수 있고, 이 로그를 전송하는 방식으로 복제(Replication)도 구현됩니다.
Checkpoint는 무엇인가요?
- 그 시점까지 메모리에서 변경된 더티 버퍼를 모두 실제 데이터 파일에 반영하는 작업입니다.
- 복구 시 재생해야 할 WAL의 시작 지점을 앞당겨 복구 시간을 줄여줍니다.
- 한 번에 많은 양을 쓰면 순간적인 I/O 부하가 발생하므로, 일정 시간에 걸쳐 분산해서 수행하도록 설정합니다.
4.2 MVCC
PostgreSQL의 MVCC는 어떻게 동작하나요?
- 데이터를 수정할 때 기존 행을 덮어쓰지 않고, 새로운 버전의 행을 추가한 뒤 기존 행을 더 이상 유효하지 않은 것으로 표시합니다.
- 각 트랜잭션은 자신의 스냅샷 기준으로 어떤 버전이 보이는지 판단합니다.
- 덕분에 읽기 작업이 쓰기 작업을 기다리지 않고, 쓰기 작업도 읽기를 막지 않습니다.
- 참고로 MySQL InnoDB는 Undo Log에 이전 버전을 따로 보관하는 방식이라, 구버전이 테이블 안에 쌓이는 PostgreSQL과 차이가 있습니다.
xmin과 xmax는 무엇인가요?
- 각 행(튜플)이 가지고 있는 시스템 컬럼입니다.
xmin: 그 행을 생성한 트랜잭션의 ID입니다.
xmax: 그 행을 삭제하거나 갱신한 트랜잭션의 ID이며, 아직 유효한 행이면 비어 있습니다.
- 이 두 값과 스냅샷을 비교해 현재 트랜잭션에게 그 행이 보여야 하는지를 판단합니다.
Snapshot이란 무엇인가요?
- 특정 시점에 어떤 트랜잭션들이 커밋되었고 어떤 것들이 아직 진행 중인지를 담은 정보입니다.
- 트랜잭션은 이 스냅샷을 기준으로 각 행 버전의 가시성을 판단합니다.
- Read Committed에서는 쿼리마다 새 스냅샷을 뜨고, Repeatable Read에서는 트랜잭션 시작 시점의 스냅샷을 끝까지 유지합니다.
Dead Tuple과 Table Bloat은 왜 발생하나요?
- UPDATE나 DELETE로 더 이상 어떤 트랜잭션에게도 보이지 않게 된 구버전 행이 Dead Tuple입니다.
- 이 행들은 즉시 삭제되지 않고 테이블 안에 남아 있습니다.
- 정리되지 않고 계속 쌓이면 테이블과 인덱스가 실제 데이터량보다 훨씬 커지는데(Table Bloat), 스캔해야 할 페이지가 늘어나 성능이 떨어지고 디스크도 낭비됩니다.
- 이를 정리하는 것이 VACUUM의 역할입니다.
4.3 Vacuum
VACUUM은 왜 필요한가요?
- Dead Tuple이 차지하던 공간을 재사용 가능한 상태로 표시해 테이블이 계속 부풀어 오르는 것을 막습니다.
- 오래된 트랜잭션 ID를 정리(freeze)해 ID 순환 문제를 예방합니다.
- 조회 성능을 유지하기 위한 필수적인 유지보수 작업입니다.
VACUUM과 VACUUM FULL의 차이는 무엇인가요?
VACUUM: Dead Tuple 공간을 재사용 가능하게 표시하지만 대부분 OS에 디스크를 반환하지는 않습니다. 강한 락을 걸지 않아 서비스 중에도 안전하게 수행할 수 있습니다.
VACUUM FULL: 테이블을 통째로 새로 쓰면서 완전히 압축하고 디스크를 실제로 반환합니다. 다만 해당 테이블에 ACCESS EXCLUSIVE 락을 걸어 그동안 읽기와 쓰기가 모두 차단되므로, 운영 중에는 신중하게 사용해야 합니다.
AUTOVACUUM은 무엇인가요?
- 백그라운드에서 자동으로 VACUUM과 ANALYZE를 실행해 주는 기능입니다.
- 테이블의 변경된 행 비율이 설정한 임계치를 넘으면 동작합니다.
- 쓰기가 매우 많은 테이블은 기본 설정으로는 정리 속도가 따라가지 못할 수 있어, 테이블 단위로 임계치를 낮춰 더 자주 수행하도록 조정하기도 합니다.
ANALYZE는 어떤 역할을 하나요?
- 테이블 데이터를 샘플링해 값의 분포, 고유 값 개수 같은 통계 정보를 수집합니다.
- Query Planner는 이 통계를 근거로 실행 계획의 비용을 추정합니다.
- 통계가 오래되면 실제 데이터와 달라져, 인덱스를 써야 할 상황에 전체 스캔을 고르는 등 잘못된 계획을 선택할 수 있습니다.
Transaction ID Wraparound란 무엇인가요?
- 트랜잭션 ID가 32비트라 약 40억 개를 쓰면 다시 처음으로 순환합니다.
- 순환이 일어나면 과거에 커밋된 트랜잭션이 미래의 것으로 보여, 이미 존재하던 데이터가 갑자기 보이지 않게 되는 심각한 문제가 생깁니다.
- VACUUM이 충분히 오래된 행을 frozen 상태로 표시해 이를 방지합니다.
- 방치되면 PostgreSQL이 데이터 손상을 막기 위해 경고를 내다가 결국 새로운 쓰기를 거부하므로, autovacuum이 정상 동작하는지 모니터링해야 합니다.
4.4 인덱스
PostgreSQL의 인덱스 종류와 각각의 용도는 무엇인가요?
B-Tree: 기본 인덱스로 등가 비교, 범위 검색, 정렬에 모두 사용됩니다.
Hash: 등가 비교에만 사용되며, B-Tree로 대부분 대체 가능해 활용도가 낮습니다.
GIN: 하나의 행이 여러 값을 포함하는 경우에 적합합니다. 배열, JSONB, 전문 검색에 사용됩니다.
GiST: 기하 데이터, 범위 타입, 유사도 검색처럼 확장된 형태의 검색에 사용됩니다.
BRIN: 블록 범위별 최솟값·최댓값만 저장해 인덱스 크기가 매우 작습니다.
GIN 인덱스는 언제 사용하나요?
- 하나의 컬럼 값 안에 여러 개의 검색 대상이 들어 있는 경우에 사용합니다.
- 역색인 구조로, 각 요소가 어떤 행들에 포함되어 있는지를 저장합니다.
- JSONB 컬럼에서 특정 키를 포함하는 행 검색, 배열에 특정 값이 있는지 검색, 전문 검색에 주로 쓰입니다.
- 조회는 빠르지만 인덱스 갱신 비용이 커서 쓰기가 많은 테이블에서는 부담이 될 수 있습니다.
BRIN 인덱스는 어떤 상황에 적합한가요?
- 물리적인 저장 순서와 컬럼 값의 순서가 강한 상관관계를 가진 대용량 테이블에 적합합니다.
- 대표적으로 시간순으로 계속 쌓이기만 하는 로그 테이블의 timestamp 컬럼이 있습니다.
- 블록 단위 요약 정보만 저장하므로 인덱스가 B-Tree보다 훨씬 작아 메모리 부담이 적습니다.
- 반대로 값이 테이블 전체에 무작위로 흩어져 있으면 걸러내는 효과가 거의 없습니다.
Partial Index와 Expression Index는 무엇인가요?
Partial Index: WHERE 조건을 붙여 일부 행에만 인덱스를 만드는 방식입니다. 예를 들어 삭제되지 않은 행만 인덱싱하면 크기가 훨씬 작아지고 갱신 비용도 줄어듭니다.
Expression Index: 컬럼이 아니라 표현식에 인덱스를 만드는 방식입니다. LOWER(email)처럼 함수를 적용한 조건은 일반 인덱스를 타지 못하는데, 같은 표현식으로 인덱스를 만들어 두면 인덱스를 사용할 수 있습니다.
4.5 실행 계획
EXPLAIN과 EXPLAIN ANALYZE의 차이는 무엇인가요?
EXPLAIN: 쿼리를 실제로 실행하지 않고, 옵티마이저가 예상한 실행 계획과 비용만 보여줍니다.
EXPLAIN ANALYZE: 쿼리를 실제로 실행한 뒤 실제 소요 시간과 실제 처리한 행 수까지 보여줍니다.
- 예상 행 수와 실제 행 수의 차이가 크다면 통계가 낡았다는 신호입니다.
- 주의할 점은
EXPLAIN ANALYZE가 INSERT나 UPDATE도 실제로 실행한다는 것입니다. 변경 쿼리를 확인할 때는 트랜잭션으로 감싸고 롤백해야 합니다.
Sequential Scan, Index Scan, Bitmap Index Scan은 각각 언제 선택되나요?
Sequential Scan: 테이블 전체를 순차적으로 읽습니다. 조회 대상이 테이블의 상당 부분을 차지하면 인덱스를 타는 것보다 오히려 빠릅니다.
Index Scan: 인덱스로 대상을 찾고 매번 실제 테이블에 접근합니다. 소수의 행만 가져올 때 유리합니다.
Bitmap Index Scan: 조회 행이 중간 규모일 때 선택됩니다. 인덱스로 대상 블록의 비트맵을 먼저 만든 뒤 테이블을 물리적 순서대로 한 번에 읽어, 랜덤 I/O를 줄입니다.
- 필요한 컬럼이 모두 인덱스에 있으면 테이블 접근을 생략하는
Index Only Scan이 선택되기도 합니다.
Nested Loop, Hash Join, Merge Join은 각각 어떤 상황에 유리한가요?
Nested Loop: 한쪽 테이블의 각 행마다 다른 쪽을 조회합니다. 한쪽 결과가 매우 작고 다른 쪽에 적절한 인덱스가 있을 때 유리합니다.
Hash Join: 한쪽 테이블로 해시 테이블을 만든 뒤 다른 쪽을 훑으며 매칭합니다. 등가 조인이면서 데이터가 많을 때 유리하지만, 해시 테이블이 메모리에 들어가야 효율적입니다.
Merge Join: 양쪽을 조인 키로 정렬한 뒤 나란히 훑으며 매칭합니다. 이미 정렬되어 있거나 정렬 비용을 감당할 수 있는 대용량 데이터에 유리합니다.
Query Planner는 어떻게 실행 계획을 선택하나요?
- ANALYZE로 수집한 통계를 바탕으로, 가능한 여러 실행 방법의 비용을 추정합니다.
- 디스크 페이지 읽기 비용과 CPU 처리 비용 등을 종합해 가장 비용이 낮은 계획을 선택합니다.
- 즉 규칙 기반이 아니라 비용 기반이므로, 통계가 부정확하면 실제로는 훨씬 느린 계획을 고를 수 있습니다.
4.6 트랜잭션과 Lock
PostgreSQL의 트랜잭션 격리 수준은 어떻게 되나요?
- 기본 격리 수준은
Read Committed입니다.
- 표준의 네 단계를 모두 지원하지만,
Read Uncommitted를 지정해도 내부적으로는 Read Committed처럼 동작해 Dirty Read가 발생하지 않습니다.
- PostgreSQL의
Repeatable Read는 스냅샷 격리로 구현되어 Phantom Read까지 발생하지 않습니다. 대신 충돌이 감지되면 직렬화 실패 오류가 발생하므로 애플리케이션에 재시도 로직이 필요합니다.
Serializable은 SSI 방식으로, 동시 실행 결과가 순차 실행과 동일함을 보장합니다.
Row-level Lock과 Table-level Lock의 차이는 무엇인가요?
Row-level Lock: UPDATE, DELETE, SELECT FOR UPDATE 시 해당 행에만 걸립니다. 다른 행에 대한 작업은 영향을 받지 않아 동시성이 높습니다.
Table-level Lock: 테이블 전체에 걸리며 ALTER TABLE 같은 DDL이나 VACUUM FULL에서 사용됩니다. 강도가 높은 락은 해당 테이블의 모든 접근을 차단합니다.
- MVCC 덕분에 일반적인 읽기는 락을 기다리지 않으므로, 읽기와 쓰기가 서로를 막지 않습니다.
SELECT ... FOR UPDATE는 언제 사용하나요?
- 조회한 행에 쓰기 락을 걸어, 다른 트랜잭션이 그 행을 수정하지 못하게 막을 때 사용합니다.
- 재고 차감처럼 "읽은 값을 기준으로 계산해서 수정"해야 하는, 갱신 손실이 발생하면 안 되는 로직에서 비관적 락으로 활용합니다.
SKIP LOCKED를 함께 쓰면 이미 잠긴 행을 건너뛰므로, 여러 워커가 같은 테이블에서 작업을 나눠 가져가는 큐 패턴을 구현할 수 있습니다.
Advisory Lock이란 무엇인가요?
- 특정 테이블이나 행이 아니라, 애플리케이션이 의미를 직접 정하는 논리적인 락입니다.
- DB는 그 숫자 키가 무엇을 뜻하는지 모르고 단지 잠금만 관리합니다.
- 여러 서버에서 같은 배치 작업이 동시에 실행되는 것을 막는 것처럼, 데이터 행과 직접 연결되지 않는 작업을 직렬화할 때 유용합니다.
PostgreSQL은 데드락을 어떻게 처리하나요?
- 락 대기가 설정된 시간(
deadlock_timeout)을 넘으면 대기 그래프를 검사해 순환 대기가 있는지 확인합니다.
- 데드락이 확인되면 한쪽 트랜잭션을 오류로 중단시켜 순환을 끊습니다.
- 애플리케이션은 이 오류를 잡아 재시도해야 하며, 근본적으로는 여러 행을 갱신할 때 항상 같은 순서로 접근하고 트랜잭션을 짧게 유지해 발생 자체를 줄이는 것이 좋습니다.
4.7 PostgreSQL 기능
JSON과 JSONB의 차이는 무엇인가요?
JSON: 입력받은 텍스트를 그대로 저장합니다. 공백과 키 순서가 보존되고 중복 키도 유지되지만, 조회할 때마다 파싱해야 해서 느립니다.
JSONB: 파싱한 뒤 바이너리 형태로 저장합니다. 저장 시 변환 비용이 있지만 조회가 빠르고, GIN 인덱스를 걸 수 있습니다.
- 키 순서가 보존되지 않고 중복 키는 마지막 값만 남습니다.
- 대부분의 경우 검색과 인덱싱이 가능한 JSONB를 사용합니다.
CTE는 무엇이며 언제 사용하나요?
WITH 절로 쿼리에 이름을 붙여 임시 결과 집합처럼 사용하는 문법입니다.
- 복잡한 쿼리를 단계별로 나눠 작성할 수 있어 가독성이 크게 좋아집니다.
WITH RECURSIVE를 쓰면 조직도나 카테고리 같은 계층 구조를 재귀적으로 조회할 수 있습니다.
- PostgreSQL 12부터는 CTE가 기본적으로 인라인되어 옵티마이저가 함께 최적화하며, 이전처럼 최적화 경계로 두고 싶으면
MATERIALIZED를 명시하면 됩니다.
Materialized View와 일반 View의 차이는 무엇인가요?
View: 쿼리에 이름만 붙여둔 것으로, 조회할 때마다 실제 쿼리가 실행됩니다. 항상 최신 데이터를 보지만 무거운 쿼리라면 매번 비용이 발생합니다.
Materialized View: 쿼리 결과를 물리적으로 저장해 두고 조회 시 그 결과를 바로 반환합니다. 매우 빠르지만 원본이 바뀌어도 자동 반영되지 않아 REFRESH가 필요합니다.
- 실시간성이 덜 중요한 집계나 통계 화면에 적합합니다.
Partitioning은 왜 사용하나요?
- 큰 테이블을 기준에 따라 여러 개의 작은 테이블로 나누어 저장하는 기능으로, Range·List·Hash 방식을 지원합니다.
- 조건에 해당하지 않는 파티션은 아예 읽지 않는 파티션 프루닝으로 스캔 범위가 줄어듭니다.
- 오래된 데이터를 지울 때 DELETE 대신 해당 파티션을 통째로 DROP할 수 있어 훨씬 빠르고 Bloat도 생기지 않습니다.
- 파티션별로 인덱스와 VACUUM이 나뉘어 유지보수 부담도 분산됩니다.
Full Text Search는 어떻게 동작하나요?
- 문서를
tsvector 타입으로 변환하면서 단어를 어간 형태로 정규화하고 불용어를 제거합니다.
- 검색어는
tsquery로 변환해 두 값을 매칭합니다.
tsvector 컬럼에 GIN 인덱스를 걸면 대용량 문서에서도 빠르게 검색할 수 있습니다.
- 단순
LIKE '%검색어%'와 달리 인덱스를 활용할 수 있고 단어 단위 검색과 순위 매기기가 가능합니다.
Extension이란 무엇인가요?
- PostgreSQL의 기능을 확장하는 모듈로,
CREATE EXTENSION으로 설치해 사용합니다.
- 대표적으로 쿼리 실행 통계를 수집하는
pg_stat_statements, 공간 데이터를 다루는 PostGIS, 벡터 검색을 지원하는 pgvector 등이 있습니다.
- 필요한 기능을 DB 엔진 교체 없이 추가할 수 있다는 점이 PostgreSQL의 큰 장점 중 하나입니다.
5. 객체지향과 설계
5.1 설계 원칙
SOLID 원칙을 설명해 주세요.
SRP(단일 책임): 클래스는 변경되어야 할 이유가 하나뿐이어야 합니다. 여러 이유로 자주 고쳐지는 클래스는 책임이 뭉쳐 있다는 신호입니다.
OCP(개방-폐쇄): 기능 확장에는 열려 있고 기존 코드 수정에는 닫혀 있어야 합니다. 추상화에 의존하게 만들어, 새 구현체를 추가해도 사용하는 쪽은 그대로 두는 것이 목표입니다.
LSP(리스코프 치환): 하위 타입은 상위 타입을 대체해도 프로그램이 정상 동작해야 합니다. 부모의 규약을 하위에서 깨뜨리면 다형성을 신뢰할 수 없게 됩니다.
ISP(인터페이스 분리): 사용하지 않는 메서드에 의존하지 않도록 인터페이스를 역할 단위로 나눠야 합니다.
DIP(의존관계 역전): 구체적인 구현이 아니라 추상에 의존해야 합니다. 상위 정책이 하위 세부 구현에 끌려다니지 않게 하는 원칙입니다.
응집도와 결합도는 무엇인가요?
응집도: 하나의 모듈 안에 있는 요소들이 얼마나 같은 목적을 향해 모여 있는지를 나타냅니다. 높을수록 좋습니다.
결합도: 모듈들이 서로 얼마나 강하게 의존하는지를 나타냅니다. 낮을수록 좋습니다.
- 이 둘이 중요한 이유는 결국 변경 비용 때문입니다. 응집도가 높고 결합도가 낮으면 수정이 필요할 때 고쳐야 할 범위가 그 모듈 안으로 한정됩니다.
상속과 조합 중 무엇을 선택해야 하나요?
- 상속은 부모의 구현에 강하게 묶입니다. 부모가 바뀌면 자식이 함께 영향을 받고, 부모의 내부 구현을 알아야 제대로 확장할 수 있어 캡슐화가 약해집니다.
- 조합은 필요한 기능을 가진 객체를 필드로 두고 위임하는 방식으로, 결합도가 낮고 런타임에 교체할 수도 있습니다.
- 상속은 "B는 A의 한 종류다"가 확실하게 성립할 때만 사용하고, 단순히 코드를 재사용하려는 목적이라면 조합을 우선하는 것이 좋습니다.
좋은 객체지향 설계란 무엇이라고 생각하시나요?
- 데이터와 그 데이터를 다루는 행위가 한곳에 있는 것이 핵심입니다.
- 서비스가 엔티티에서 값을 꺼내 계산한 뒤 다시 집어넣는 방식은 형태만 객체지향이고 실제로는 절차지향에 가깝습니다. 이런 구조에서는 같은 규칙이 여러 서비스에 흩어져 중복되기 쉽습니다.
- 객체가 자신의 상태를 스스로 책임지게 하면, 그 규칙이 바뀔 때 고칠 곳이 그 객체 하나로 좁혀집니다.
5.2 디자인 패턴
싱글턴 패턴은 무엇이며 어떤 문제가 있나요?
- 인스턴스를 하나만 만들어 전역에서 공유하는 패턴입니다.
- 전역 상태가 되기 때문에 테스트 간에 상태가 남아 서로 영향을 주고, 가변 상태를 가지면 동시성 문제가 생깁니다.
- 또한 사용하는 쪽이 구체 클래스를 직접 참조하게 되어 교체와 테스트가 어려워집니다.
- Spring의 싱글턴 빈은 컨테이너가 생명주기를 관리하고 주입해 주는 방식이라, 직접 구현하는 싱글턴 패턴의 단점 대부분을 피할 수 있습니다.
팩토리 패턴은 언제 사용하나요?
- 객체를 생성하는 로직을 한곳에 모아 캡슐화할 때 사용합니다.
- 조건에 따라 서로 다른 구현체를 반환해야 하거나, 생성 과정이 복잡해서 사용하는 쪽에 노출하고 싶지 않을 때 적합합니다.
- 사용하는 쪽은 구체 클래스를 몰라도 되므로, 새로운 구현체가 추가되어도 영향을 덜 받습니다.
전략 패턴은 무엇인가요?
- 서로 바꿔 쓸 수 있는 알고리즘들을 하나의 인터페이스로 추상화하고, 실행 시점에 원하는 것을 선택하는 패턴입니다.
- 결제 수단이나 할인 정책처럼
if-else로 분기하던 로직을 각각의 클래스로 분리하면, 새로운 정책이 생겨도 기존 코드를 건드리지 않고 클래스만 추가하면 됩니다.
- 개방-폐쇄 원칙을 실제 코드로 구현하는 대표적인 방법입니다.
옵저버 패턴은 무엇인가요?
- 한 객체의 상태가 바뀌면 그 사실을 구독하고 있는 여러 객체에게 알리는 패턴입니다.
- 알리는 쪽이 받는 쪽을 구체적으로 몰라도 되므로 결합도가 낮아지고, 새로운 구독자를 추가해도 기존 코드를 수정할 필요가 없습니다.
- Spring의 이벤트 발행·구독 구조가 이 패턴에 해당하며, 주문 완료 후 알림 발송처럼 부가 로직을 본 흐름에서 분리할 때 유용합니다.
프록시 패턴은 무엇인가요?
- 실제 객체에 대한 접근을 대신 받아, 부가 작업을 수행한 뒤 실제 객체에 위임하는 패턴입니다.
- 실제 객체와 같은 인터페이스를 구현하기 때문에 사용하는 쪽은 차이를 알지 못합니다.
- 지연 초기화, 접근 제어, 캐싱, 로깅에 활용되며 Spring AOP와 JPA의 지연 로딩이 대표적인 예입니다.
디자인 패턴을 적용할 때 주의할 점은 무엇인가요?
- 패턴 적용 자체가 목적이 되면 단순한 코드가 불필요하게 복잡해집니다.
- 패턴은 대부분 변경에 유연하게 대응하기 위한 것이므로, 실제로 변경이 예상되는 지점에 적용해야 값어치를 합니다.
- 아직 요구되지 않은 유연성을 미리 넣어두면 읽기만 어려워지고 그 유연성이 끝내 쓰이지 않는 경우가 많습니다.
6. 테스트와 협업
6.1 테스트
단위 테스트와 통합 테스트의 차이는 무엇인가요?
단위 테스트: 하나의 클래스나 메서드를 외부 의존 없이 검증합니다. 실행이 빠르고 실패했을 때 원인이 명확합니다.
통합 테스트: DB나 웹 계층처럼 실제 구성 요소를 함께 엮어 검증합니다. 실제 동작에 가깝지만 느리고 실패 원인을 좁히기 어렵습니다.
- 그래서 빠른 단위 테스트를 넓게 깔고 통합 테스트는 핵심 흐름 위주로 적게 두는 형태가 권장됩니다. 느린 테스트가 많아지면 결국 아무도 실행하지 않게 됩니다.
Mock, Stub, Fake는 어떻게 다른가요?
Stub: 미리 정해진 값을 돌려주는 가짜 객체로, 결과 상태를 검증할 때 사용합니다.
Mock: 어떤 메서드가 어떤 인자로 몇 번 호출되었는지를 검증하는 객체로, 행위 자체가 검증 대상일 때 사용합니다.
Fake: 실제로 동작하지만 단순하게 구현한 대역으로, 인메모리 저장소가 대표적입니다.
- 주의할 점은 Mock을 과하게 쓰면 테스트가 구현 세부사항에 묶인다는 것입니다. 동작은 그대로인데 내부 구조만 바꿔도 테스트가 깨진다면 리팩터링을 방해하게 됩니다.
TDD는 무엇이며 어떤 장점이 있나요?
- 실패하는 테스트를 먼저 작성하고, 통과할 만큼만 구현한 뒤, 리팩터링하는 사이클을 반복하는 방식입니다.
- 테스트를 먼저 쓰려면 무엇을 만들지 명확히 정의해야 하므로 요구사항을 먼저 정리하게 됩니다.
- 테스트하기 쉬운 구조를 자연스럽게 따라가게 되어, 작은 단위와 의존성 주입 같은 설계가 유도됩니다.
- 무엇보다 이미 검증된 테스트가 쌓여 있어 이후 리팩터링을 안심하고 할 수 있습니다.
좋은 테스트의 조건은 무엇인가요?
- 빨라야 합니다. 느리면 자주 실행하지 않게 되고 결국 방치됩니다.
- 독립적이어야 합니다. 실행 순서나 다른 테스트의 결과에 영향을 받으면 신뢰할 수 없습니다.
- 반복 실행해도 같은 결과가 나와야 합니다. 가끔 실패하는 테스트는 오히려 경고를 무시하게 만듭니다.
- 하나의 테스트는 하나만 검증하고, 이름만 봐도 무엇이 깨졌는지 알 수 있어야 합니다.
- 내부 구현이 아니라 외부에서 관찰 가능한 동작을 검증해야 리팩터링에도 살아남습니다.
테스트하기 어려운 코드는 어떤 특징이 있나요?
- 메서드 안에서 의존 객체를 직접 생성하면 대역으로 바꿔 끼울 수 없습니다.
- 현재 시각이나 랜덤 값을 직접 사용하면 실행할 때마다 결과가 달라져 검증이 불가능합니다.
- static 메서드 호출이나 전역 상태에 의존하면 테스트 간 격리가 되지 않습니다.
- 이런 것들은 의존성을 주입받고 시간이나 랜덤도 추상화하면 해결됩니다.
- 중요한 점은 테스트하기 어렵다는 것 자체가 결합도가 높다는 설계 신호라는 것입니다. 테스트를 위한 억지 변경이 아니라 설계 개선으로 이어져야 합니다.
6.2 버전 관리와 배포
merge와 rebase의 차이는 무엇인가요?
merge: 두 브랜치를 합치는 새로운 커밋을 만듭니다. 실제 작업 이력이 그대로 남지만 분기와 병합이 얽혀 이력이 복잡해집니다.
rebase: 내 커밋들을 대상 브랜치 위로 옮겨 다시 씁니다. 이력이 한 줄로 정리되어 읽기 좋습니다.
- 핵심 주의사항은 이미 다른 사람과 공유한 브랜치를 rebase하면 안 된다는 것입니다. 커밋이 새로 만들어지면서 해시가 바뀌기 때문에, 같은 브랜치를 받아 쓰던 사람과 이력이 어긋나게 됩니다.
브랜치 전략에는 어떤 것들이 있나요?
Git Flow: main, develop, feature, release, hotfix로 역할을 세분화합니다. 버전을 정해 배포하는 제품에 적합하지만 절차가 복잡합니다.
GitHub Flow: main과 작업 브랜치만 두고 리뷰 후 바로 배포합니다. 자주 배포하는 웹 서비스에 적합합니다.
Trunk Based: 아주 짧게 유지하는 브랜치로 자주 통합하고, 미완성 기능은 플래그로 감춥니다.
- 정답이 있다기보다 배포 주기와 팀 규모에 맞춰 선택하는 것이며, 브랜치가 오래 살아 있을수록 충돌 비용이 커진다는 점이 공통된 기준입니다.
CI/CD는 무엇인가요?
CI(지속적 통합): 변경사항을 자주 통합하면서 빌드와 테스트를 자동으로 실행해 문제를 빨리 발견하는 것입니다.
CD(지속적 배포·전달): 검증을 통과한 결과물을 자동으로 배포 가능한 상태로 만들거나 실제 배포까지 진행하는 것입니다.
- 통합 주기가 길수록 충돌이 커지고 어느 변경이 문제를 일으켰는지 찾기 어려워지므로, 작은 단위로 자주 통합하는 것이 핵심입니다.