면접 준비 (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) 연산을 사용합니다. "현재 값이 내가 읽은 값과 같다면 새 값으로 바꾼다"를 하드웨어 수준의 원자적 명령으로 수행합니다.
  • 값이 그사이 바뀌었다면 실패하고, 다시 읽어서 재시도합니다.
  • 락을 걸지 않아 컨텍스트 스위칭 비용이 없지만, 경쟁이 매우 심하면 재시도가 반복되어 오히려 비효율적일 수 있습니다.
synchronizedReentrantLock의 차이는 무엇인가요?
  • synchronized는 블록을 벗어나면 락이 자동으로 해제되어 간결하고 실수가 적습니다.
  • ReentrantLockunlock()finally에서 직접 호출해야 하지만, 타임아웃을 두고 락을 시도하거나(tryLock), 대기 중 인터럽트를 받거나, 공정성(먼저 기다린 스레드 우선)을 설정하는 등 더 세밀한 제어가 가능합니다.
  • 특별한 제어가 필요 없다면 synchronized를, 세밀한 제어가 필요하면 ReentrantLock을 사용합니다.
CompletableFuture는 무엇인가요?
  • 비동기 작업의 결과를 표현하며, 작업들을 연결하고 조합할 수 있는 클래스입니다.
  • 기존 Future는 결과를 얻으려면 get()으로 블로킹해야 했지만, CompletableFuturethenApply, 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로 별도 스레드나 프로세스에 넘겨야 합니다.
asyncawait는 각각 무슨 역할인가요?
  • async: 해당 함수가 코루틴임을 선언합니다.
  • await: 다른 코루틴이나 비동기 작업의 완료를 기다리며, 기다리는 동안 이벤트 루프에 제어권을 넘겨 다른 작업이 실행될 수 있게 합니다.
  • awaitasync 함수 안에서만 사용할 수 있습니다.
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.txtpyproject.toml의 차이는 무엇인가요?
  • requirements.txt: 설치할 패키지 목록을 나열한 단순한 텍스트 파일입니다.
  • pyproject.toml: 프로젝트 메타데이터, 의존성, 빌드 설정, 각종 도구 설정까지 한 파일에 담는 표준 형식입니다.
  • 최근에는 pyproject.toml이 표준으로 자리 잡아, 여러 설정 파일로 흩어져 있던 내용을 한곳에서 관리하는 방향으로 가고 있습니다.

3. C++

3.1 메모리

Stack과 Heap 메모리의 차이는 무엇인가요?
  • Stack: 지역 변수와 함수 호출 정보가 저장되며, 스코프를 벗어나면 자동으로 해제됩니다. 할당·해제가 매우 빠르지만 크기가 제한적입니다.
  • Heap: newmalloc으로 직접 할당하는 영역으로, 개발자가 명시적으로 해제해야 합니다. 크기가 크고 수명을 자유롭게 제어할 수 있지만 관리 부담과 단편화 문제가 있습니다.
포인터와 참조자(Reference)의 차이는 무엇인가요?
  • 포인터: 주소를 담는 변수로, 선언 후 나중에 초기화할 수 있고 다른 대상을 가리키도록 바꿀 수 있으며 nullptr을 가질 수 있습니다.
  • 참조자: 기존 객체의 별칭으로, 선언과 동시에 반드시 초기화해야 하고 이후 다른 객체를 참조하도록 바꿀 수 없으며 null이 될 수 없습니다.
  • 반드시 유효한 대상이 있고 재바인딩이 필요 없다면 참조자가 더 안전하고 의도가 명확합니다.
new/deletemalloc/free의 차이는 무엇인가요?
  • new는 메모리를 할당한 뒤 생성자를 호출하고, delete는 소멸자를 호출한 뒤 메모리를 해제합니다. malloc/free는 생성자·소멸자를 호출하지 않습니다.
  • new는 타입에 맞는 포인터를 반환해 형변환이 필요 없지만, mallocvoid*를 반환합니다.
  • 할당 실패 시 newstd::bad_alloc 예외를 던지고 mallocNULL을 반환합니다.
  • 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는 요소를 추가하다 용량이 초과되어 재할당이 일어나면 기존의 모든 반복자와 포인터가 무효화됩니다. 중간 삽입·삭제 시에는 그 지점 이후가 무효화됩니다.
  • listmap은 노드 기반이라 삭제된 원소를 가리키던 반복자만 무효화되고 나머지는 유효합니다.
  • 무효화된 반복자를 사용하면 정의되지 않은 동작이 발생하므로, 순회 중 원소를 지울 때는 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과 차이가 있습니다.
xminxmax는 무엇인가요?
  • 각 행(튜플)이 가지고 있는 시스템 컬럼입니다.
  • 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(지속적 배포·전달): 검증을 통과한 결과물을 자동으로 배포 가능한 상태로 만들거나 실제 배포까지 진행하는 것입니다.
  • 통합 주기가 길수록 충돌이 커지고 어느 변경이 문제를 일으켰는지 찾기 어려워지므로, 작은 단위로 자주 통합하는 것이 핵심입니다.