백엔드 기술 면접에서 자주 나오는 질문 정리
여러 회사 백엔드 개발자 면접에 나올법한 질문들을 주제별로 정리해봤어요. 회사마다 결이 조금씩 다르긴 하지만, 자바/스프링/DB/운영 큰 줄기는 어디서나 비슷하죠. 지원하기 전에 한 번씩 점검해두면 도움이 될 만한 항목 위주로 묶었어요. 자료구조 &x26; 알고리즘 기본기 점검용으로 거의 빠지지 않는 영역이에요. 특히 해시 충돌, 탐색 알고리즘, 페이지 교체는 정말 단골이에요. 해시 충돌 해결 방식 해시 함수가 아무리 좋아도 비둘기집 원리상 충돌은 무조건 발생해요. 그래서 충돌을 어떻게 처리하느냐가 자료구조 성능을 결정해요. Open Addressing (개방 주소법) 테이블의 빈 공간을 활용해서 충돌을 해결해요. 추가 메모리가 필요 없어서 메모리 사용량이 적고, 연속된 메모리에 데이터를 배치하기 때문에 캐시 친화적이에요. Linear Probing: 충돌 시 옆 칸을 순차적으로 탐색해요. 구현이 가장 간단하지만 Primary Clustering이라는 군집화 문제가 있어요. 데이터가 한쪽에 몰리면 탐색 시간이 급격히 늘어나거든요. Quadratic Probing: n² 칸 떨어진 곳을 탐색해요. Linear Probing의 군집화는 줄지만, 같은 해시값이면 탐색 경로가 동일해지는 Secondary Clustering 문제가 있어요. Double Hashing: 1차 해시로 위치를 정하고, 2차 해시로 이동 폭을 정해요. 가장 불규칙한 분포를 만들 수 있어 충돌 회피 성능이 뛰어나지만, 두 번 계산하는 비용이 있어요. 일반적으로 Open Addressing은 Load Factor가 0.7 이상이 되면 성능이 급격히 떨어져요. 그래서 적절한 시점에 리해싱(rehashing)이 필요해요. Separate Chaining (분리 연결법) 같은 버킷에 연결 리스트나 트리로 데이터를 매다는 방식이에요. 자바의 HashMap이 대표적인 예시인데, 흥미로운 디테일이 있어요. 자바 8부터는 버킷에 들어있는 노드 수가 8개를 넘으면 Linked List를 Red-Black Tree로 변환해요 (Treeify) 트리에서 노드 수가 6개 이하로 줄면 다시 Linked List로 되돌아가요 (Untreeify) 이렇게 하는 이유는 Worst Case에서 O(n)이 되는 걸 O(log n)으로 줄이기 위해서예요 면접 답변 팁: "어떤 걸 더 선호하느냐"는 질문이 따라올 수 있어요. 정답은 없고, 데이터 분포와 메모리 제약에 따라 다르다고 답하면 좋아요. 예를 들어 캐시 효율이 중요한 작은 데이터셋이면 Open Addressing, 충돌이 잦고 동적으로 크기가 변하면 Separate Chaining이 유리해요. DFS와 BFS 탐색 알고리즘의 양대산맥이에요. 단순히 "뭐냐"보다 언제 어떤 걸 쓰는지를 알고 있어야 해요. 항목 DFS BFS 자료구조 스택 (재귀) 큐 (FIFO) 메모리 깊이만큼 사용 너비만큼 사용 최단 경로 보장 안 됨 보장됨 (가중치 없는 그래프) 활용 백트래킹, 위상 정렬, 사이클 검출 최단 거리, 레벨 탐색 언제 무엇을 쓰는가? 모든 경로를 탐색해야 하면 → DFS 최단 경로(간선 수 기준)가 필요하면 → BFS 그래프 깊이가 매우 깊으면 DFS는 스택 오버플로우 위험이 있어요 그래프가 매우 넓으면 BFS는 메모리 폭발 위험이 있어요 가중치가 있는 그래프의 최단 경로는 다익스트라(Dijkstra) 나 벨만-포드(Bellman-Ford) 를 써요. BFS만으로는 부족하다는 걸 함께 언급하면 깊이 있어 보여요. 페이지 교체 알고리즘 운영체제 영역에서 LRU가 거의 필수로 나와요. 캐시 정책에서도 동일한 개념이 적용되니까 꼭 알아두면 좋아요. FIFO: 가장 먼저 들어온 페이지부터 교체. 구현이 단순하지만 Belady's Anomaly (페이지 프레임을 늘렸는데 페이지 폴트가 더 늘어나는 현상)가 발생할 수 있어요. Optimal (OPT): 앞으로 가장 오래 안 쓸 페이지를 교체. 이론상 최적이지만 미래를 알아야 해서 구현 불가능. 다른 알고리즘의 성능 비교 기준선으로 사용돼요. LRU (Least Recently Used): 가장 오래 사용되지 않은 페이지를 교체. 시간 지역성(Temporal Locality)을 활용해서 Optimal에 근접한 성능을 내요. 구현은 보통 HashMap + Doubly Linked List 조합으로 O(1) 보장. LFU (Least Frequently Used): 참조 횟수가 가장 적은 페이지를 교체. 단점은 한 번 많이 참조된 페이지가 계속 남아있어 새로운 데이터가 자리잡기 힘들어요 (Cache Pollution). MFU: LFU의 반대. 참조 횟수가 많은 건 이미 충분히 썼으니 교체한다는 발상. LRU 캐시 직접 구현 문제는 LeetCode에도 있고 면접에서도 종종 나와요. LinkedHashMap을 활용하거나, Doubly Linked List + HashMap을 직접 구현하는 두 가지 방식을 모두 알아두면 좋아요. 자바 &x26; JVM 자바 백엔드 면접의 핵심 영역이에요. GC, 스레드, 컬렉션 세 가지는 무조건 깊이 있게 준비해야 해요. JVM 메모리 구조 GC를 이야기하기 전에 메모리 구조부터 알고 있어야 해요. Method Area (Metaspace): 클래스 정보, static 변수, 상수 풀 Heap: 모든 객체와 배열이 저장되는 영역. GC 대상. Stack: 메소드 호출마다 생성되는 프레임. 지역 변수, 매개변수. PC Register: 현재 실행 중인 명령어 주소 Native Method Stack: JNI를 통한 네이티브 메소드 호출용 자바 8부터는 PermGen이 사라지고 Metaspace로 대체됐어요. Metaspace는 Native Memory를 사용하기 때문에 OOM 발생 양상이 달라졌어요. GC (Garbage Collection) 힙은 크게 Young Generation과 Old Generation으로 나뉘어요. 대부분의 객체는 짧게 살다 죽는다는 Weak Generational Hypothesis에 기반한 설계예요. Young Generation 구조: Eden: 새 객체가 처음 할당되는 곳 Survivor 0, 1: Eden에서 살아남은 객체가 이동하는 곳. 두 영역을 번갈아 사용. GC 종류: Minor GC: Young Generation에서 발생. 빠르고 자주 일어남. Major GC (Full GC): Old Generation 포함 전체 GC. 느리고 Stop-the-World가 길어요. GC 알고리즘: Serial Collector: 단일 스레드. 100MB 미만 작은 앱에 적합. -XX:+UseSerialGC Parallel Collector: 멀티 스레드, 처리량 위주. JDK 8 기본값. -XX:+UseParallelGC CMS (Concurrent Mark Sweep): 동시성 마킹으로 STW 최소화. JDK 9부터 Deprecated, JDK 14에서 제거. G1 (Garbage First): 힙을 Region 단위로 나눠 관리. JDK 9부터 기본값. 큰 힙(4GB+)에 유리. ZGC: STW를 10ms 이하로 줄임. JDK 11+ 실험적, JDK 15+ 정식. 대용량 힙(TB 단위)에 적합. Shenandoah: ZGC와 비슷한 저지연 GC. Red Hat 주도. 면접 답변 팁: "어떤 GC를 써봤냐"는 질문에는 단순히 이름만 말하지 말고 왜 그걸 선택했는지를 함께 답하면 좋아요. 예: "응답 시간이 중요한 서비스라 G1을 사용했어요. CMS와 비교해서 풀 GC 발생 빈도를 줄일 수 있었거든요." OOM과 힙 덤프 OOM은 종류에 따라 원인과 해결법이 달라요. OOM 종류 원인 Java heap space 힙이 부족. 메모리 누수 또는 힙 크기 부족 GC Overhead limit exceeded GC에 시간을 너무 많이 쓰는데 회수량이 적음 Metaspace 클래스 메타데이터 영역 부족 Direct buffer memory NIO Direct Buffer 누수 Unable to create new native thread OS 스레드 생성 한계 도달 힙 덤프 분석 흐름: 자동 덤프 설정: -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump 수동 생성: jmap -dump:format=b,file=heap.hprof &x3C;pid> 분석 도구: Eclipse MAT(Memory Analyzer Tool), VisualVM, JProfiler 확인 포인트: Dominator Tree로 메모리를 가장 많이 차지하는 객체 찾기, GC Roots 추적 자주 나오는 누수 패턴: static 컬렉션에 객체가 계속 쌓이는 경우 ThreadLocal을 사용 후 remove() 안 한 경우 리스너/콜백 등록 후 해제 안 한 경우 DB Connection, Stream을 닫지 않은 경우 Thread Safe와 동기화 스레드 안전성을 보장하는 방법은 단계별로 여러 가지가 있어요. 1단계: 공유 자원 자체를 없애기 (가장 안전) 불변 객체(Immutable Object) 사용 ThreadLocal로 스레드별 인스턴스 분리 함수형 프로그래밍 (사이드 이펙트 제거) 2단계: 동기화 메커니즘 synchronized: 가장 기본. 모니터 락 사용. 진입 시 다른 스레드는 대기. volatile: 가시성(visibility) 보장. 원자성은 보장 안 됨. ReentrantLock: synchronized보다 유연. tryLock, fair 모드 지원. ReadWriteLock: 읽기 잠금과 쓰기 잠금 분리. 읽기가 많은 경우 유리. StampedLock: Java 8+. 낙관적 락 지원으로 성능 향상. 3단계: 원자 연산 (Lock-Free) AtomicInteger, AtomicLong, AtomicReference 등 내부적으로 CAS (Compare-And-Swap) 사용 락이 없어서 데드락 걱정 없고, 성능도 빠름 단점: 경쟁이 심하면 스핀이 늘어나 오히려 느려질 수 있음 (ABA 문제도 주의) 4단계: 동시성 컬렉션 ConcurrentHashMap: HashMap의 동시성 버전. Java 8부터 CAS + synchronized 혼합. CopyOnWriteArrayList: 쓰기 시 복사. 읽기가 압도적으로 많을 때 유리. BlockingQueue 계열: 생산자-소비자 패턴 synchronized의 문제점 면접에서 자주 따라오는 질문이에요. 성능 저하: 모니터 락 획득/해제 비용 데드락 위험: 락 획득 순서가 어긋나면 발생 경쟁 시 처리량 급감: 락 경합이 심하면 거의 직렬 처리 공정성 부족: 어느 스레드가 락을 받을지 보장 없음 인터럽트 불가: 락 대기 중 인터럽트로 깨울 수 없음 (ReentrantLock은 가능) 함수형 인터페이스 자바 8 이후 람다와 함께 자주 물어봐요. 단 하나의 추상 메소드(SAM) 를 가지는 인터페이스예요. @FunctionalInterface interface MyFunction { int apply(int x); } // 람다로 구현 MyFunction square = x -> x * x; 자주 쓰는 표준 함수형 인터페이스: 인터페이스 시그니처 용도 Supplier&x3C;T> () -> T 값 공급 Consumer&x3C;T> T -> void 값 소비 Function&x3C;T,R> T -> R 변환 Predicate&x3C;T> T -> boolean 조건 검사 BiFunction&x3C;T,U,R> (T,U) -> R 두 값 변환 UnaryOperator&x3C;T> T -> T 같은 타입 변환 컬렉션 프레임워크 Array vs ArrayList: 항목 Array ArrayList 크기 고정 동적 (내부 배열 확장) 타입 원시 타입 가능 객체만 (제네릭) 성능 빠름 약간의 오버헤드 메모리 효율적 메타데이터 추가 HashMap의 내부 동작: 기본 capacity: 16, load factor: 0.75 75%가 차면 2배로 리사이징 해시값을 (n - 1) &x26; hash로 인덱스 계산 (capacity가 2의 제곱이라 가능) Java 8부터 충돌이 많은 버킷은 트리화 HashSet 성능 저하 케이스: equals()와 hashCode()가 잘못 구현되어 충돌이 많을 때 mutable 객체를 키로 쓰고 해시값에 영향을 주는 필드를 변경한 경우 해결: 잘 분산되는 hashCode 작성, 가능하면 immutable 키 사용 Stream과 Parallel Stream // Stream List&x3C;Integer> result = list.stream() .filter(x -> x > 10) .map(x -> x * 2) .collect(Collectors.toList()); // Parallel Stream list.parallelStream() .filter(x -> x > 10) .map(x -> x * 2) .collect(Collectors.toList()); Parallel Stream을 언제 쓰면 좋은가? 좋은 경우: 데이터가 충분히 큼 (보통 10,000개 이상) CPU 바운드 작업 작업 단위가 독립적 결과 순서가 중요하지 않음 피해야 할 경우: 작은 데이터셋 (오버헤드가 더 큼) I/O 바운드 작업 (스레드 효율 떨어짐) 공유 상태 변경 (동기화 비용) 트랜잭션 컨텍스트 안 (스레드가 다르면 트랜잭션 전파 안 됨) Parallel Stream은 기본적으로 공통 ForkJoinPool을 사용해요. 한 작업이 풀을 점유하면 다른 작업까지 영향을 받을 수 있으니, 운영 환경에서는 별도의 풀을 명시적으로 지정하는 게 안전해요. 스프링 (Spring) 자바 백엔드에선 사실상 필수 프레임워크라 디테일까지 물어봐요. DI와 IoC IoC (Inversion of Control): 객체의 생성과 생명주기를 개발자가 아닌 컨테이너가 관리. 제어의 흐름이 역전됨. DI (Dependency Injection): IoC를 구현하는 한 가지 방법. 의존성을 외부에서 주입. 주입 방식: // 1. Constructor Injection (권장) @Service public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } } // 2. Setter Injection @Service public class UserService { private UserRepository userRepository; @Autowired public void setUserRepository(UserRepository userRepository) { this.userRepository = userRepository; } } // 3. Field Injection (지양) @Service public class UserService { @Autowired private UserRepository userRepository; } 왜 생성자 주입을 권장하는가? 면접 단골 질문이에요. 답할 수 있는 이유가 많을수록 좋아요. 순환 참조를 시작 시점에 발견 — 필드/세터 주입은 런타임에 메소드 호출 시점에야 발견되지만, 생성자 주입은 컨테이너 초기화 시 즉시 실패해요. Spring Boot 2.6+부턴 기본적으로 순환 참조를 막아요. 불변 객체 보장 — final 키워드로 한 번 주입된 의존성을 바꿀 수 없게 해요. 테스트 작성이 쉬움 — 컨테이너 없이 new UserService(mockRepo)로 직접 주입 가능. 필드 주입은 리플렉션 없이는 테스트 불가. 누락 발견 — 컴파일 시점에 필수 의존성 누락을 잡아낼 수 있어요. 단일 책임 원칙 강제 — 생성자 파라미터가 너무 많아지면 클래스가 너무 많은 책임을 지고 있다는 신호가 돼요. Bean Scope Scope 생명주기 Singleton (기본) 컨테이너당 1개 Prototype 요청할 때마다 새 인스턴스 Request HTTP 요청 단위 (웹) Session HTTP 세션 단위 (웹) Application ServletContext 단위 (웹) WebSocket WebSocket 단위 (웹) Singleton 빈에 Prototype 빈을 주입하면 처음 주입된 인스턴스만 계속 사용돼요. 매번 새 인스턴스를 받고 싶으면 ObjectProvider나 @Lookup 어노테이션을 사용해야 해요. AOP 핵심 관심사와 부가 관심사를 분리해 모듈화하는 패러다임이에요. 로깅, 트랜잭션, 인증/인가 같은 횡단 관심사에 잘 어울려요. 핵심 용어: Aspect: 횡단 관심사를 모듈화한 것 Join Point: Aspect가 적용될 수 있는 지점 (메소드 실행 등) Pointcut: 어디에 적용할지 정의하는 표현식 Advice: 무엇을 할지 정의 (@Before, @After, @Around, @AfterReturning, @AfterThrowing) Weaving: Aspect를 실제 코드에 적용하는 과정 @Aspect @Component public class LoggingAspect { @Around("execution(* com.example.service.*.*(..))") public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); long elapsed = System.currentTimeMillis() - start; log.info("{} took {}ms", joinPoint.getSignature(), elapsed); return result; } } 스프링 AOP의 한계: 프록시 기반이라 같은 클래스 내부 호출(self-invocation)에는 적용 안 됨 메소드 레벨만 가능 (필드 접근은 안 됨) public 메소드만 적용 (CGLIB 사용 시 제외) 트랜잭션 (@Transactional) 면접에서 자주 깊게 파고드는 주제예요. 전파 속성 (Propagation): REQUIRED (기본): 기존 트랜잭션 있으면 참여, 없으면 생성 REQUIRES_NEW: 항상 새 트랜잭션 생성. 기존 트랜잭션은 일시 정지 NESTED: 중첩 트랜잭션. SavePoint 사용 SUPPORTS: 있으면 참여, 없으면 트랜잭션 없이 실행 MANDATORY: 반드시 기존 트랜잭션이 있어야 함 NEVER: 트랜잭션이 있으면 예외 격리 수준 (Isolation Level): READ_UNCOMMITTED: Dirty Read 발생 가능 READ_COMMITTED: Non-Repeatable Read 가능 (Oracle 기본) REPEATABLE_READ: Phantom Read 가능 (MySQL 기본) SERIALIZABLE: 가장 엄격, 성능 저하 자주 빠지는 함정: @Transactional은 public 메소드에만 적용돼요 같은 클래스 내부 호출은 프록시를 거치지 않아 적용 안 됨 기본적으로 RuntimeException과 Error만 롤백. Checked Exception은 명시적으로 rollbackFor 지정 필요 데이터베이스 &x26; JPA 성능과 직결되는 영역이라 디테일하게 물어봐요. 인덱스 왜 빠른가? B+ Tree 자료구조 기반. 모든 leaf 노드가 같은 깊이에 있어 균형 잡혀 있음. 100만 건 데이터도 보통 3~4단계 트리 탐색으로 접근 가능 Leaf 노드끼리 연결되어 있어 범위 검색에도 강함 B+ Tree vs B-Tree 차이: B-Tree는 모든 노드에 데이터 저장 B+ Tree는 leaf에만 데이터 저장, 내부 노드는 인덱스 역할만 B+ Tree가 범위 검색에 유리하고 디스크 I/O 효율도 더 좋음 인덱스가 항상 좋은 건 아니에요: 쓰기 비용 증가 (INSERT, UPDATE, DELETE 시 인덱스도 갱신) 디스크 공간 추가 사용 카디널리티가 낮은 컬럼(예: 성별)에는 비효율적 데이터가 적으면 풀 스캔이 더 빠를 수 있음 복합 인덱스의 함정: (A, B, C) 인덱스가 있을 때: WHERE A = ? → 사용 WHERE A = ? AND B = ? → 사용 WHERE A = ? AND C = ? → 부분 사용 WHERE B = ? → 사용 안 함 (왼쪽 컬럼부터 매칭) 커버링 인덱스 (Covering Index): 쿼리에 필요한 모든 컬럼이 인덱스에 포함되어 있으면 테이블에 접근하지 않고 인덱스만으로 결과를 반환해요. 매우 빠른 조회가 가능해요. N+1 문제 JPA를 쓰면 거의 무조건 마주치는 문제예요. 문제 상황: // Member 1건 조회 후 각 Member의 Team을 조회 List&x3C;Member> members = memberRepository.findAll(); // 쿼리 1번 for (Member m : members) { System.out.println(m.getTeam().getName()); // 쿼리 N번 } 총 N+1번의 쿼리가 나가요. Member가 100명이면 101번의 DB 호출. 해결 방법: JOIN FETCH: @Query("SELECT m FROM Member m JOIN FETCH m.team") List&x3C;Member> findAllWithTeam(); @EntityGraph: @EntityGraph(attributePaths = {"team"}) List&x3C;Member> findAll(); Batch Size 설정: spring.jpa.properties.hibernate.default_batch_fetch_size: 100 N개를 IN 절로 묶어서 한 번에 조회 (N+1이 N/batch+1로 줄어듦) QueryDSL: 동적 쿼리에 유연 Projection (DTO 직접 조회): 필요한 컬럼만 조회 JOIN FETCH는 Collection을 가져올 때 페이징이 안 돼요. (모든 데이터를 메모리에 올린 후 페이징하기 때문에 위험) 페이징이 필요하면 Batch Size 방식을 써야 해요. 캐시 전략 언제 갱신? — 데이터가 변경될 때 무효화하거나 갱신 정합성 깨지는 케이스 — 캐시와 DB가 불일치하는 모든 시점 캐시 패턴: Cache Aside (Look Aside): 가장 일반적. 캐시 조회 → 없으면 DB → 캐시에 저장 Write Through: 쓰기 시 DB와 캐시 동시 갱신 Write Behind (Write Back): 쓰기는 캐시에만, 비동기로 DB 반영 Read Through: 캐시 미스 시 캐시 라이브러리가 DB 조회까지 처리 캐시 무효화 전략: TTL (Time To Live): 시간 기반 만료 이벤트 기반: DB 변경 시 캐시 삭제 (@CacheEvict) 버저닝: 키에 버전을 포함시켜 신규 키로 우회 JPA 캐시: 1차 캐시: EntityManager 단위. 트랜잭션 내에서 동일 엔티티는 한 번만 조회. 2차 캐시: SessionFactory 단위. 애플리케이션 전체에서 공유. EhCache, Hazelcast 등 사용. Redis 장애 대응 Redis가 죽으면 어떻게 할 건지 물어볼 수 있어요. 영속화 전략: RDB (Snapshot): 특정 시점 메모리 전체를 디스크에 덤프. 복구 빠름, 데이터 유실 가능. AOF (Append Only File): 모든 쓰기 명령을 로그로 저장. 데이터 안정성 높지만 파일 커지고 복구 느림. 혼합 모드: RDB + AOF 함께 사용 (Redis 4.0+) 고가용성 구성: Replication: Master-Slave 복제 Sentinel: 자동 페일오버, 마스터 모니터링 Cluster: 데이터 샤딩 + 복제, 수평 확장 캐시 장애 시 대비책: Circuit Breaker 적용 (Resilience4j, Hystrix) DB 직접 조회로 폴백 Local Cache 병행 (Caffeine 등으로 2단계 캐시) Cache Stampede 방지: 캐시 만료 시 동시 다발적 DB 조회를 막기 위한 락 (PER 알고리즘 등) 네트워크 &x26; HTTP RESTful API 설계 원칙 리소스 중심 URL: /users/123/orders (행위가 아닌 리소스) HTTP 메소드로 행위 표현: GET, POST, PUT, PATCH, DELETE 상태 코드 의미 있게 사용: 200, 201, 204, 400, 401, 403, 404, 409, 500 HATEOAS (선택): 응답에 다음 동작 링크 포함 PUT vs PATCH: PUT: 리소스 전체 교체. 멱등성 보장. PATCH: 부분 수정. 멱등성은 구현에 따라 다름. HTTP 메소드 특성: 메소드 Safe Idempotent Cacheable GET O O O HEAD O O O OPTIONS O O X POST X X 조건부 PUT X O X PATCH X X X DELETE X O X Safe: 서버 상태를 변경하지 않음 Idempotent: 여러 번 호출해도 결과 동일 HTTPS와 SSL/TLS TLS Handshake: 클라이언트-서버가 암호화 방식과 키를 협상 인증서: CA가 발급, 서버 신원 보증 대칭키 vs 비대칭키: Handshake는 비대칭(RSA, ECDH), 실제 통신은 대칭(AES) HTTP/2: 멀티플렉싱, 헤더 압축, Server Push. 사실상 HTTPS 필수. HTTP/3: QUIC(UDP) 기반. 연결 설정이 빠름. 쿠키 vs 세션 vs JWT 항목 쿠키 세션 JWT 저장 위치 클라이언트 서버 클라이언트 보안 낮음 높음 중간 확장성 - 서버 동기화 필요 Stateless, 확장 쉬움 무효화 즉시 즉시 어려움 (만료 전엔 유효) 쿠키 보안 옵션: HttpOnly: JavaScript 접근 차단 (XSS 방어) Secure: HTTPS에서만 전송 SameSite: CSRF 방어 (Strict, Lax, None) SQL Injection 방어 핵심 원칙: 사용자 입력을 절대 SQL 문자열에 직접 합치지 말 것. 방어 방법: PreparedStatement (파라미터 바인딩): PreparedStatement ps = conn.prepareStatement( "SELECT * FROM users WHERE id = ?"); ps.setString(1, userInput); ORM 사용: JPA, MyBatis의 {} 는 자동으로 바인딩 MyBatis ${} 주의: 문자열 치환이라 인젝션 가능. ORDER BY 컬럼 등에만 제한적으로 사용하고 화이트리스트 검증 필수 입력 검증: 화이트리스트 기반 검증 최소 권한 원칙: DB 사용자에게 필요한 권한만 부여 프록시 vs 게이트웨이 Forward Proxy: 클라이언트 앞에 있음. 클라이언트 요청을 대신 수행 (회사 내부망 → 외부) Reverse Proxy: 서버 앞에 있음. 외부 요청을 받아 내부 서버로 전달 (Nginx, HAProxy) API Gateway: Reverse Proxy + 인증, 라우팅, 변환, 로깅 등 부가 기능 (Spring Cloud Gateway, Kong) 운영 &x26; 인프라 모니터링 (Observability 3축) Metrics (지표): CPU, 메모리, 응답 시간, 처리량 등 수치 Prometheus + Grafana DataDog, New Relic Logs (로그): 이벤트 기록 ELK Stack (Elasticsearch + Logstash + Kibana) Fluentd, Loki Traces (분산 추적): 요청이 여러 서비스를 거치는 흐름 Jaeger, Zipkin OpenTelemetry (표준) APM (Application Performance Monitoring): Pinpoint, Scouter (오픈소스, 한국에서 많이 씀) New Relic, DataDog APM (상용) 배포 전략 20대 이상 서버를 어떻게 배포할 거냐는 질문이 나온 적 있어요. 무중단 배포 방식: Rolling Update: 서버를 순차적으로 업데이트. 점진적이지만 두 버전이 공존하는 시간이 있음. Blue-Green: 동일한 환경 두 개를 두고 한 번에 트래픽 전환. 리소스 2배 필요하지만 즉시 롤백 가능. Canary: 일부 트래픽만 신규 버전에 보내고 문제없으면 점진적 확대. A/B 테스트와 비슷. CI/CD 파이프라인: 코드 푸시 → CI 트리거 테스트 + 빌드 도커 이미지 생성, 레지스트리 푸시 스테이징 배포 + 검증 프로덕션 배포 (수동 승인 또는 자동) 헬스체크 + 모니터링 이상 시 자동 롤백 장애 대응 장애가 나면 뭘 먼저 보냐는 질문도 자주 나와요. 대응 순서: 영향 범위 파악: 모든 사용자? 일부? 어떤 기능? 모니터링 대시보드 확인: CPU, 메모리, 응답 시간, 에러율 급격한 변화 로그 확인: ERROR, Exception 키워드, 트랜잭션 ID 추적 최근 변경 사항 확인: 배포 이력, 설정 변경, DB 마이그레이션 롤백 결정: 원인 모르고 시간 끌리면 일단 롤백 임시 대응: 장애가 퍼지지 않게 차단 (피처 플래그, 트래픽 제한) 근본 원인 분석 (RCA): 사후 회고, 재발 방지 좋은 답변 패턴: "단계별로 무엇을 확인하고, 왜 그 순서인지"를 설명하면 좋아요. Kubernetes 자원 관리 resources: requests: memory: '64Mi' cpu: '250m' limits: memory: '128Mi' cpu: '500m' requests: 컨테이너에 최소 보장되는 자원 limits: 컨테이너가 사용할 수 있는 최대 자원 requests = limits로 설정하면 Guaranteed QoS 클래스 (가장 안정적) 메모리 limit 초과 시 OOMKilled, CPU limit 초과 시 Throttling 아키텍처 (MSA) MSA의 장단점 장점: 독립적 배포: 서비스별로 따로 배포 가능 기술 스택 자유: 서비스마다 다른 언어/프레임워크 사용 가능 장애 격리: 한 서비스 장애가 전체로 퍼지지 않음 팀 자율성: 각 팀이 자기 서비스에 대한 오너십 단점: 네트워크 레이턴시: 서비스 간 호출이 모두 네트워크 통신 분산 트랜잭션: 여러 서비스에 걸친 트랜잭션 처리가 어려움 (Saga 패턴 등 필요) 복잡한 통합 테스트: 여러 서비스를 함께 띄워야 함 운영 복잡도 증가: 모니터링, 로깅, 추적 인프라 필요 데이터 정합성: 서비스마다 DB가 다르면 일관성 보장이 까다로움 MSA에서 자주 쓰는 패턴 API Gateway: 단일 진입점, 라우팅, 인증 Service Discovery: Eureka, Consul로 서비스 위치 동적 관리 Circuit Breaker: 장애 전파 방지 (Resilience4j) Saga: 분산 트랜잭션. Choreography(이벤트 기반) vs Orchestration(중앙 조정자) Event Sourcing: 상태 대신 이벤트 기록 CQRS: 명령(Command)과 조회(Query) 분리 테스트 왜 TDD를 도입하는가? 회귀 버그 방지 — 리팩터링에 자신감을 줘요 설계 개선 — 테스트하기 어려운 코드는 보통 잘못 설계된 코드예요 문서화 효과 — 테스트 코드 자체가 사용 예시 빠른 피드백 — 작은 단위로 개발하니 문제를 일찍 발견 TDD 사이클 (Red-Green-Refactor): Red: 실패하는 테스트 작성 Green: 테스트를 통과하는 최소한의 코드 작성 Refactor: 중복 제거, 가독성 개선 F.I.R.S.T 원칙 Fast: 빨라야 자주 돌릴 수 있음 Independent: 테스트끼리 의존하면 안 됨 Repeatable: 환경에 관계없이 같은 결과 Self-validating: 통과/실패가 명확 Timely: 적시에 작성 (TDD: 코드보다 먼저) Mock 활용 @ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private UserRepository userRepository; @InjectMocks private UserService userService; @Test void findUser() { // given given(userRepository.findById(1L)) .willReturn(Optional.of(new User("kim"))); // when User user = userService.findById(1L); // then assertThat(user.getName()).isEqualTo("kim"); verify(userRepository).findById(1L); } } 서비스 vs 컨트롤러 테스트: 서비스 단위: Mockito로 의존 객체를 모킹해서 비즈니스 로직만 검증 컨트롤러 단위: MockMvc로 HTTP 요청-응답 흐름 검증 mockMvc.perform(get("/users/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("kim")); 컨트롤러에는 비즈니스 로직을 두지 않는 게 좋아요. 호출 위임 정도만 테스트하면 충분하거든요 테스트 종류 Unit Test: 단일 클래스/메소드. 빠르고 격리됨. Integration Test: 여러 컴포넌트 통합. DB, 외부 API 포함 가능. @SpringBootTest E2E Test: 사용자 시나리오 끝까지. 가장 느리고 비싸지만 가장 신뢰성 높음. Test Pyramid: Unit이 가장 많고, E2E가 가장 적은 형태가 이상적 마무리 면접 질문은 기술마다 결이 비슷해요. "왜 이걸 쓰는가? 단점은? 대안은?" 이 세 가지 흐름으로 답을 준비하면 어느 회사를 가더라도 흔들리지 않게 답할 수 있어요. 특히 자기가 직접 써본 기술은 "왜 그 선택을 했는지" 를 한 줄로 정리해두는 게 좋아요. 면접관은 정답을 듣고 싶은 게 아니라 사고 과정을 보고 싶어 하거든요. 마지막으로, 모르는 질문이 나왔을 때 "모릅니다" 라고 솔직하게 답하는 것도 능력이에요. 그 다음에 "이런 식으로 접근해볼 것 같습니다"라고 사고 과정을 보여주면 오히려 좋은 인상을 줄 수 있어요. 모든 걸 아는 사람은 없으니까요. Lose an hour in the morning, and you will spend all day looking for it.— Richard Whately