1. JVM이 정확히 무엇이고, 어떤 기능을 하는지 설명해 주세요.
기본 답변
JVM은 Java 바이트코드를 실행하는 가상 머신입니다.
Java 코드는 컴파일되면 .class 파일 형태의 바이트코드가
되고, JVM은 이 바이트코드를 각 운영체제와 CPU 환경에 맞게 실행합니다.
JVM은 단순 실행기만이 아니라 클래스 로딩, 바이트코드 검증, 런타임 메모리 관리, JIT 컴파일, GC, 예외 처리, 스레드 관리 같은 기능을 제공합니다.
그래서 Java는 JVM이 설치된 환경이라면 비교적 플랫폼에 덜 의존적으로 실행될 수 있습니다.
핵심 키워드
- 바이트코드
- 클래스 로더
- 런타임 데이터 영역
- JIT 컴파일러
- GC
- 플랫폼 독립성
꼬리질문
질문 그럼, 자바 말고 다른 언어는 JVM 위에 올릴 수 없나요?
답변 포인트 가능합니다.
Kotlin, Scala, Groovy, Clojure 같은 언어도 JVM 바이트코드로 컴파일되어 JVM 위에서 실행됩니다.
질문 반대로 JVM 계열 언어를 일반적으로 컴파일해서 사용할 순 없나요?
답변 포인트 일부는 네이티브 컴파일이 가능합니다.
예를 들어 GraalVM Native Image를 사용하면 JVM 없이 실행 가능한 바이너리를 만들 수 있지만, 리플렉션이나 동적 로딩에서 제약이 생길 수 있습니다.
질문 VM을 사용함으로써 얻을 수 있는 장점과 단점에 대해 설명해 주세요.
답변 포인트 장점은 플랫폼 독립성, GC, 런타임 최적화, 안정성입니다.
단점은 JVM 구동 비용, 메모리 사용량, GC 튜닝 부담, 네이티브 대비 예측 어려운 지연 시간입니다.
질문 JVM과 내부에서 실행되고 있는 프로그램은 부모 프로세스 - 자식 프로세스 관계를 갖고 있다고 봐도 무방한가요?
답변 포인트 아닙니다.
JVM은 명세상 바이트코드를 실행하는 추상 머신입니다. 일반적인 구현에서는 실행 중인 JVM 인스턴스와 Java 애플리케이션이 하나의 운영체제 프로세스 안에서 동작합니다.
Java 프로그램이 JVM의 자식 프로세스로 별도 생성되는 구조는 아닙니다.
주의할 점
- JVM이 Java 소스코드를 직접 실행한다고 말하면 부정확합니다. JVM은 컴파일된 바이트코드를 실행합니다.
- JVM 개념 자체를 운영체제 프로세스와 동일시하면 안 됩니다. 구체적인 JVM 실행 인스턴스가 보통 Java 애플리케이션과 같은 프로세스 안에서 동작한다고 설명하는 것이 정확합니다.
2. final 키워드를 사용하면, 어떤 이점이 있나요?
기본 답변
final은 대상에 따라 의미가 조금 다릅니다.
변수에 붙으면 한 번만 할당할 수 있고, 메서드에 붙으면 오버라이딩할 수 없으며, 클래스에 붙으면 상속할 수 없습니다.
이점은 의도를 명확히 표현한다는 점입니다.
값이 바뀌면 안 되는 변수, 재정의되면 안 되는 메서드, 확장되면 안 되는 클래스를 코드 차원에서 제한할 수 있습니다.
특히 불변 객체를 만들거나, 상속으로 인해 깨질 수 있는 동작을 막을 때 유용합니다.
핵심 키워드
- 재할당 금지
- 오버라이딩 금지
- 상속 금지
- 불변성
- 의도 표현
꼬리질문
질문 그렇다면 컴파일 과정에서, final 키워드는 다르게 취급되나요?
답변 포인트 컴파일러는
final 제약을 검사합니다.
static final 기본형이나 문자열 상수처럼 컴파일 타임
상수인 경우 값이 사용하는 쪽에 인라인될 수 있습니다.
다만 모든 final이 성능 최적화로 직결되는 것은 아니고,
실제 최적화는 JIT가 런타임 정보를 바탕으로 수행하는 경우가 많습니다.
주의할 점
-
final참조 변수는 참조를 바꿀 수 없다는 뜻이지, 참조 대상 객체의 내부 상태까지 항상 불변이라는 뜻은 아닙니다. -
불변 객체를 만들려면 필드를
final로 두는 것 외에도 setter 제거, 방어적 복사, 가변 객체 노출 방지 등이 필요합니다.
3. 인터페이스와 추상 클래스의 차이에 대해 설명해 주세요.
기본 답변
인터페이스는 어떤 기능을 제공해야 하는지에 대한 계약에 가깝고, 추상 클래스는 공통 상태와 공통 구현을 일부 제공하면서 하위 클래스가 나머지를 구현하도록 만드는 기반 클래스에 가깝습니다.
Java에서 클래스는 하나만 상속할 수 있지만 인터페이스는 여러 개 구현할 수 있습니다.
그래서 인터페이스는 여러 타입의 역할을 조합할 때 좋고, 추상 클래스는 공통 필드, 생성자, 템플릿 메서드처럼 상속 계층 안에서 공유할 구현이 있을 때 적합합니다.
핵심 키워드
- 계약
- 공통 구현
- 다중 구현
- 단일 상속
- default method
- 상태 공유
꼬리질문
질문 왜 클래스는 단일 상속만 가능한데, 인터페이스는 2개 이상 구현이 가능할까요?
답변 포인트 클래스 다중 상속은 필드와 구현이 충돌하는 다이아몬드 문제가 생기기 쉽습니다.
인터페이스는 원래 상태 없이 계약 중심이었기 때문에 여러 개 구현이 가능했습니다.
Java 8 이후 default method가 생겼지만, 충돌 시 구현 클래스가 명시적으로 해결해야 합니다.
주의할 점
-
Java 8부터 인터페이스에
default,static메서드를 정의할 수 있고,private인터페이스 메서드는 Java 9부터 지원됩니다. 따라서 “인터페이스는 구현을 가질 수 없다”는 설명은 틀립니다. - 그래도 인스턴스 상태와 생성자를 중심으로 한 공통 기반은 추상 클래스가 더 자연스럽습니다.
4. 리플렉션에 대해 설명해 주세요.
기본 답변
리플렉션은 런타임에 클래스, 메서드, 필드, 생성자, 애노테이션 정보를 조회하고 동적으로 접근하거나 호출할 수 있게 해주는 기능입니다.
컴파일 시점에 구체 타입을 직접 알지 못해도 런타임 정보를 기반으로 객체를 생성하거나 메서드를 실행할 수 있습니다.
Spring, JPA, Jackson, 테스트 프레임워크처럼 애노테이션과 객체 구조를 읽어 동작해야 하는 프레임워크에서 많이 사용합니다.
대신 일반 호출보다 느릴 수 있고, 캡슐화를 깨거나 보안 문제가 생길 수 있으므로 제한적으로 사용해야 합니다.
핵심 키워드
- 런타임 메타데이터
- Class 객체
- Method, Field, Constructor
- Annotation
- 동적 호출
- 프레임워크
꼬리질문
질문 의미만 들어보면 리플렉션은 보안적인 문제가 있을 가능성이 있어보이는데, 실제로 그렇게 생각하시나요? 만약 그렇다면, 어떻게 방지할 수 있을까요?
답변 포인트 가능합니다.
private 멤버 접근, 임의 클래스 로딩, 사용자 입력 기반 메서드 호출은 위험할 수 있습니다.
사용자 입력을 직접 클래스명이나 메서드명으로 쓰지 않고, 허용 목록을 두며, 접근 범위를 최소화하고, 모듈과 접근 제어를 지키는 방식으로 방어합니다.
질문 리플렉션을 언제 활용할 수 있을까요?
답변 포인트 DI 컨테이너의 객체 생성, 애노테이션 스캔, ORM 엔티티 매핑, JSON 직렬화/역직렬화, 테스트에서 private 필드 주입, 플러그인 구조처럼 런타임에 타입을 다뤄야 할 때 사용합니다.
주의할 점
- 리플렉션은 강력하지만 타입 안정성과 성능, 캡슐화 측면에서 비용이 있습니다.
- 애플리케이션 코드에서 남용하기보다 프레임워크나 도구 레벨에서 제한적으로 쓰는 것이 좋습니다.
5. static class와 static method를 비교해 주세요.
기본 답변
Java에서 static class는 정확히 말하면 최상위 클래스에는
붙일 수 없고, 중첩 클래스에만 붙일 수 있습니다.
static nested class는 외부 클래스의 인스턴스 없이 생성할 수
있는 중첩 클래스입니다.
외부 클래스의 인스턴스 멤버에는 직접 접근할 수 없고 static 멤버에는 접근할 수 있습니다.
static method는 객체 인스턴스가 아니라 클래스에 속한
메서드입니다.
인스턴스를 만들지 않고 ClassName.method() 형태로 호출할 수
있으며, 인스턴스 필드나 인스턴스 메서드에는 직접 접근할 수 없습니다.
핵심 키워드
- static nested class
- 클래스 소속
- 인스턴스 불필요
- 유틸리티 메서드
- 클래스 초기화
- invokestatic
꼬리질문
질문 static 을 사용하면 어떤 이점을 얻을 수 있나요? 어떤 제약이 걸릴까요?
답변 포인트 인스턴스 없이 접근할 수 있어 유틸리티 함수, 상수, 공유 자원 표현에 편합니다.
하지만 객체지향적 다형성을 활용하기 어렵고, 전역 상태처럼 사용하면 테스트와 동시성 관리가 어려워질 수 있습니다.
질문 컴파일 과정에서 static 이 어떻게 처리되는지 설명해 주세요.
답변 포인트 static 멤버는 인스턴스가 아니라 클래스 단위로 관리됩니다.
바이트코드에서는 static 메서드 호출에 invokestatic이
사용됩니다.
static 필드는 클래스가 로딩되고 초기화될 때 준비되며,
static final 컴파일 타임 상수는 인라인될 수 있습니다.
주의할 점
- Java에는 top-level static class가 없습니다. static class라고 하면 보통 static 중첩 클래스를 말합니다.
- static method는 오버라이딩되지 않고 숨김, 즉 method hiding이 발생합니다.
6. Java의 Exception에 대해 설명해 주세요.
기본 답변
Java의 예외는 프로그램 실행 중 발생한 비정상 상황을 표현하는 객체입니다.
최상위에는 Throwable이 있고, 크게 Error와
Exception으로 나뉩니다.
Error는 보통 애플리케이션에서 복구하기 어려운 JVM 수준
문제이고, Exception은 애플리케이션에서 처리할 수 있는 예외
상황입니다.
Exception은 다시 checked exception과 unchecked
exception으로 나뉩니다.
checked exception은 컴파일러가 처리 여부를 강제하고, unchecked
exception은 RuntimeException 하위 타입으로 컴파일러가
강제하지 않습니다.
핵심 키워드
- Throwable
- Error
- Exception
- Checked Exception
- Unchecked Exception
- try-catch
- throws
꼬리질문
질문 예외처리를 하는 세 방법에 대해 설명해 주세요.
답변 포인트 직접
try-catch로 잡아 복구하거나, throws로
호출자에게 전파하거나, 더 의미 있는 예외로 감싸서 변환 후 던질 수
있습니다.
실무에서는 복구 가능하면 처리하고, 복구 불가능하면 적절한 계층에서 공통 처리합니다.
질문 CheckedException, UncheckedException 의 차이에 대해 설명해 주세요.
답변 포인트 checked exception은 컴파일 시점에 처리나 선언이 강제됩니다.
unchecked exception은 RuntimeException 계열로 강제되지
않으며, 주로 프로그래밍 오류나 복구하기 어려운 상황에 사용합니다.
질문 예외처리가 성능에 큰 영향을 미치나요? 만약 그렇다면, 어떻게 하면 부하를 줄일 수 있을까요?
답변 포인트 예외 객체 생성과 스택 트레이스 수집은 비용이 있습니다.
정상 제어 흐름에 예외를 사용하지 않고, 자주 발생하는 검증은 조건문으로 처리하며, 필요한 경우 예외 메시지와 로깅 범위를 적절히 조절해야 합니다.
주의할 점
- 예외를 무시하는 빈
catch는 장애 원인을 숨깁니다. - 무조건 checked exception이 좋거나 unchecked exception이 좋다고 말하기보다, 복구 가능성과 API 사용성을 기준으로 판단해야 합니다.
7. Synchronized 키워드에 대해 설명해 주세요.
기본 답변
synchronized는 Java에서 임계 영역을 보호하기 위한 동기화
키워드입니다.
특정 객체의 모니터 락을 획득한 스레드만 synchronized 블록이나 메서드에 진입할 수 있게 하여, 여러 스레드가 공유 자원을 동시에 수정하면서 생기는 race condition을 막습니다.
또한 synchronized는 단순히 상호 배제만 제공하는 것이 아니라 메모리 가시성도 보장합니다.
락을 해제하기 전의 변경 사항은 같은 락을 이후 획득한 스레드에게 보이도록 happens-before 관계가 형성됩니다.
핵심 키워드
- 모니터 락
- 임계 영역
- 상호 배제
- 메모리 가시성
- happens-before
- race condition
꼬리질문
질문 Synchronized 키워드가 어디에 붙는지에 따라 의미가 약간씩 변화하는데, 각각 어떤 의미를 갖게 되는지 설명해 주세요.
답변 포인트 인스턴스 메서드에
붙으면 this 객체 락을 사용합니다.
static 메서드에 붙으면 해당 클래스의 Class 객체 락을
사용합니다.
블록에 붙으면 괄호 안에 지정한 객체의 락을 사용합니다.
질문 효율적인 코드 작성 측면에서, Synchronized는 좋은 키워드일까요?
답변 포인트 단순하고 안전하지만 락 범위가 크거나 경쟁이 심하면 성능 저하가 생깁니다.
임계 영역을 작게 유지하고, 상황에 따라 더 세밀한 동시성 도구를 사용하는 것이 좋습니다.
질문 Synchronized 를 대체할 수 있는 자바의 다른 동기화 기법에 대해 설명해 주세요.
답변 포인트
ReentrantLock, ReadWriteLock,
StampedLock, AtomicInteger,
ConcurrentHashMap, Semaphore,
CountDownLatch, CompletableFuture 등을
상황에 따라 사용할 수 있습니다.
질문 Thread Local에 대해 설명해 주세요.
답변 포인트
ThreadLocal은 각 스레드마다 독립적인 값을 보관하게
해주는 도구입니다.
같은 변수처럼 보여도 스레드별로 값이 분리됩니다.
인증 정보, 트랜잭션 컨텍스트 등에 쓰이지만, 스레드 풀 환경에서는 사용 후 제거하지 않으면 메모리 누수나 이전 요청 데이터 노출 위험이 있습니다.
주의할 점
- synchronized는 같은 락을 기준으로 해야 의미가 있습니다. 서로 다른 객체에 락을 걸면 보호되지 않습니다.
- 락을 오래 잡거나 중첩해서 잡으면 성능 저하나 데드락 위험이 커집니다.
8. Java Stream에 대해 설명해 주세요.
기본 답변
Java Stream은 컬렉션이나 배열 같은 데이터 소스를 함수형 스타일로 처리할 수 있게 해주는 API입니다.
데이터를 직접 저장하는 자료구조가 아니라, 데이터 흐름에 대해 필터링, 매핑, 정렬, 집계 같은 연산을 선언적으로 표현합니다.
Stream은 중간 연산과 최종 연산으로 구성됩니다.
중간 연산은 지연 실행되고, 최종 연산이 호출될 때 실제 처리가 수행됩니다.
코드의 의도를 읽기 쉽게 만들 수 있지만, 모든 상황에서 반복문보다 빠른 것은 아닙니다.
핵심 키워드
- 선언형 처리
- 중간 연산
- 최종 연산
- lazy evaluation
- 함수형 인터페이스
- parallelStream
꼬리질문
질문 Stream과 for ~ loop의 성능 차이를 비교해 주세요,
답변 포인트 단순 반복에서는 for문이 오버헤드가 적어 더 빠를 수 있습니다.
Stream은 가독성과 조합성이 장점입니다.
성능은 데이터 크기, 연산 종류, 박싱 여부, 병렬화 가능성에 따라 달라집니다.
질문 Stream은 병렬처리 할 수 있나요?
답변 포인트
parallelStream()이나 stream().parallel()로
병렬 처리가 가능합니다.
내부적으로 공용 ForkJoinPool을 사용합니다.
CPU 바운드이고 데이터 분할이 쉽고 공유 상태가 없을 때 효과적이며, I/O 작업이나 작은 데이터에는 오히려 손해일 수 있습니다.
질문 Stream에서 사용할 수 있는 함수형 인터페이스에 대해 설명해 주세요.
답변 포인트 대표적으로
Predicate는 조건 판단, Function은 변환,
Consumer는 소비, Supplier는 공급,
Comparator는 비교, BinaryOperator는 같은
타입 두 값을 하나로 합치는 역할을 합니다.
질문 가끔 외부 변수를 사용할 때, final 키워드를 붙여서 사용하는데 왜 그럴까요? 꼭 그래야 할까요?
답변 포인트 람다에서 캡처하는 지역 변수는 final 또는 effectively final이어야 합니다.
명시적으로 final을 붙이지 않아도 이후 값이 변경되지
않으면 사용 가능합니다.
이는 캡처된 값의 일관성과 동시성 문제를 줄이기 위한 제약입니다.
주의할 점
- Stream 내부에서 외부 상태를 변경하면 예측하기 어렵고 병렬 처리에서 문제가 생길 수 있습니다.
-
parallelStream은 무조건 빠르게 만드는 버튼이 아닙니다.
9. Java의 GC에 대해 설명해 주세요.
기본 답변
Java의 GC는 더 이상 참조되지 않는 객체를 자동으로 찾아 메모리에서 회수하는 기능입니다.
개발자가 직접 free를 호출하지 않아도 되기 때문에 메모리
누수나 해제 후 접근 같은 오류를 줄여줍니다.
GC는 객체가 살아 있는지 reachability, 즉 GC Root로부터 도달 가능한지를 기준으로 판단합니다.
JVM은 힙 영역의 객체를 대상으로 세대별 수집, 마킹, 이동, 압축 등의 방식으로 메모리를 관리합니다.
대표 GC로는 G1, ZGC, Shenandoah 등이 있습니다.
핵심 키워드
- Heap
- GC Root
- Reachability
- Mark and Sweep
- Stop The World
- Young/Old Generation
꼬리질문
질문 finalize() 를 수동으로 호출하는 것은 왜 문제가 될 수 있을까요?
답변 포인트
finalize()는 호출 시점이 보장되지 않고 성능과 안정성
문제가 있어 사용이 권장되지 않습니다.
수동 호출은 일반 메서드 호출일 뿐 GC의 finalization을 의미하지 않습니다.
자원 정리는 try-with-resources와
AutoCloseable을 사용하는 것이 좋습니다.
질문 어떤 변수의 값이 null이 되었다면, 이 값은 GC가 될 가능성이 있을까요?
답변 포인트 가능성은 있지만 즉시 GC된다는 뜻은 아닙니다.
해당 객체를 참조하는 다른 참조가 없고 GC Root에서 도달할 수 없어야 수거 대상이 됩니다.
실제 회수 시점은 JVM이 결정합니다.
주의할 점
- GC가 있다고 해서 메모리 누수가 없는 것은 아닙니다. 컬렉션, static 필드, ThreadLocal 등에 불필요한 참조가 남아 있으면 객체가 회수되지 않습니다.
-
System.gc()도 GC 실행을 강제한다고 보기는 어렵고, 보통 직접 호출하지 않습니다.
10. equals()와 hashcode()에 대해 설명해 주세요.
기본 답변
equals()는 두 객체가 논리적으로 같은지 비교하는 메서드이고,
hashCode()는 객체를 해시 기반 자료구조에서 빠르게 찾기 위해
사용하는 정수 값을 반환하는 메서드입니다.
중요한 규칙은 equals()가 true인 두 객체는 반드시 같은
hashCode()를 가져야 한다는 것입니다.
이 규칙을 지키지 않으면 HashMap, HashSet 같은
자료구조에서 같은 객체를 찾지 못하거나 중복 저장되는 문제가 생길 수
있습니다.
핵심 키워드
- 동등성
- 동일성
- hashCode contract
- HashMap
- HashSet
- 불변 필드
꼬리질문
질문 본인이 hashcode() 를 정의해야 한다면, 어떤 점을 염두에 두고 구현할 것 같으세요?
답변 포인트 equals에 사용하는 필드와 일관되게 구성해야 합니다.
같은 값이면 같은 해시를 반환해야 하고, 해시 충돌을 줄이기 위해 주요 필드를 균형 있게 반영해야 합니다.
가능하면 불변 필드를 기준으로 작성하는 것이 좋습니다.
질문 그렇다면 equals() 를 재정의 해야 할 때, 어떤 점을 염두에 두어야 하는지 설명해 주세요.
답변 포인트 반사성, 대칭성, 추이성, 일관성, null 비교 false 규칙을 지켜야 합니다.
타입 비교 방식도 주의해야 하며, equals를 재정의하면 hashCode도 함께 재정의해야 합니다.
주의할 점
-
==는 참조 동일성 비교이고,equals()는 논리적 동등성 비교로 설계할 수 있습니다. - JPA 엔티티의 equals/hashCode는 식별자 생성 시점과 프록시 때문에 특히 조심해야 합니다.
11. IoC와 DI에 대해 설명해 주세요.
기본 답변
IoC는 Inversion of Control, 즉 제어의 역전입니다.
객체 생성과 의존성 연결을 개발자가 직접 제어하는 대신 프레임워크나 컨테이너가 제어하도록 넘기는 개념입니다.
DI는 Dependency Injection, 즉 의존성 주입입니다.
객체가 필요한 의존 객체를 직접 생성하지 않고 외부에서 주입받는 방식입니다.
Spring에서는 IoC 컨테이너가 Bean을 생성하고, 필요한 의존성을 주입해 줍니다.
이를 통해 결합도를 낮추고 테스트와 변경에 유리한 구조를 만들 수 있습니다.
핵심 키워드
- IoC Container
- DI
- Bean
- 결합도
- 생성자 주입
- 생명주기
꼬리질문
질문 후보 없이 특정 기능을 하는 클래스가 딱 한 개하면, 구체 클래스를 그냥 사용해도 되지 않나요? 그럼에도 불구하고 왜 Spring에선 Bean을 사용 할까요?
답변 포인트 단순히 구현체 교체 때문만은 아닙니다.
Bean으로 관리하면 생명주기 관리, 의존성 주입, AOP, 트랜잭션, 설정 관리, 테스트 대체, 싱글톤 관리 같은 컨테이너 기능을 사용할 수 있습니다.
질문 Spring의 Bean 생성 주기에 대해 설명해 주세요.
답변 포인트 Bean 정의를 읽고 객체를 생성한 뒤 의존성을 주입하고, aware 콜백, BeanPostProcessor, 초기화 콜백을 거쳐 사용 가능한 상태가 됩니다.
컨테이너 종료 시 destroy 콜백이 호출될 수 있습니다.
질문 프로토타입 빈은 무엇인가요?
답변 포인트 요청할 때마다 새 인스턴스를 생성하는 스코프입니다.
싱글톤 빈과 달리 Spring은 생성과 의존성 주입까지만 관리하고, 이후 소멸 생명주기는 클라이언트가 관리해야 합니다.
주의할 점
- DI는 Spring만의 개념이 아닙니다. Spring은 DI를 강력하게 지원하는 프레임워크입니다.
- 필드 주입은 테스트와 불변성 측면에서 불리하므로 생성자 주입이 보통 권장됩니다.
12. AOP에 대해 설명해 주세요.
기본 답변
AOP는 Aspect Oriented Programming, 즉 관점 지향 프로그래밍입니다.
여러 비즈니스 로직에 반복적으로 흩어지는 공통 관심사를 핵심 로직과 분리해서 모듈화하는 방식입니다.
예를 들어 로깅, 트랜잭션, 보안, 성능 측정 같은 기능은 여러 서비스 메서드에 반복해서 들어갈 수 있습니다.
AOP를 사용하면 이런 공통 로직을 Aspect로 분리하고, 특정 지점에 자동으로 적용할 수 있습니다.
핵심 키워드
- 공통 관심사
- 핵심 관심사
- Aspect
- Advice
- Pointcut
- Proxy
- Join Point
꼬리질문
질문 @Aspect는 어떻게 동작하나요?
답변 포인트 Spring은
@Aspect로 정의된 클래스의 pointcut과 advice를 읽고,
대상 Bean에 프록시를 만들어 메서드 호출 전후에 advice를 실행합니다.
Spring AOP는 기본적으로 프록시 기반이라 Spring Bean의 메서드 호출에 적용됩니다.
주의할 점
- Spring AOP는 프록시 기반이므로 같은 클래스 내부에서 자기 자신의 메서드를 호출하는 self-invocation에는 적용되지 않을 수 있습니다.
- private 메서드나 final 메서드처럼 프록시로 가로채기 어려운 구조도 주의해야 합니다.
13. Spring 에서 Interceptor와 Servlet Filter에 대해 설명해 주세요.
기본 답변
Servlet Filter는 서블릿 컨테이너 레벨에서 요청과 응답을 가로채는 기능입니다.
DispatcherServlet에 도달하기 전후에 동작하므로 Spring MVC 바깥의 요청에도 적용될 수 있습니다.
Interceptor는 Spring MVC 레벨에서 Controller 호출 전후를 가로채는 기능입니다.
DispatcherServlet 이후, HandlerMapping으로 컨트롤러가 결정된 뒤 실행됩니다.
따라서 컨트롤러 정보나 Spring MVC 컨텍스트를 활용하기 좋습니다.
핵심 키워드
- Servlet Filter
- HandlerInterceptor
- DispatcherServlet 전후
- preHandle
- postHandle
- afterCompletion
꼬리질문
질문 설명만 들어보면 인터셉터만 쓰는게 나아보이는데, 아닌가요? 필터는 어떤 상황에 사용 해야 하나요?
답변 포인트 필터는 Spring MVC 이전 단계에서 모든 요청에 적용해야 하는 작업에 적합합니다.
예를 들어 인코딩 처리, CORS, 보안 필터 체인, 요청/응답 래핑, 로깅처럼 DispatcherServlet 전 단계에서 처리해야 하는 기능은 Filter가 자연스럽습니다.
컨트롤러 기반 인증 체크나 요청 처리 시간 측정은 Interceptor가 적합합니다.
주의할 점
- Filter와 Interceptor는 실행 위치가 다릅니다. “둘 다 요청을 가로챈다”에서 끝내지 말고 DispatcherServlet 전후 기준으로 설명해야 합니다.
- Spring Security는 주로 Filter Chain 기반으로 동작합니다.
14. DispatcherServlet 의 역할에 대해 설명해 주세요.
기본 답변
DispatcherServlet은 Spring MVC의 Front Controller입니다.
클라이언트 요청을 가장 앞에서 받아 적절한 컨트롤러로 전달하고, 컨트롤러 실행 결과를 View나 HTTP 응답으로 변환하는 전체 흐름을 조율합니다.
요청이 들어오면 HandlerMapping을 통해 처리할 컨트롤러를 찾고, HandlerAdapter를 통해 해당 핸들러를 실행합니다.
이후 반환값을 처리하고, 필요한 경우 ViewResolver를 통해 뷰를 렌더링하거나 MessageConverter를 통해 JSON 응답을 만듭니다.
핵심 키워드
- Front Controller
- HandlerMapping
- HandlerAdapter
- ViewResolver
- MessageConverter
- Controller
꼬리질문
질문 여러 요청이 들어온다고 가정할 때, DispatcherServlet은 한번에 여러 요청을 모두 받을 수 있나요?
답변 포인트 DispatcherServlet
객체는 보통 하나지만, 서블릿 컨테이너의 여러 스레드가 동시에 같은
DispatcherServlet의 service 로직을 호출해 요청을
처리합니다.
따라서 DispatcherServlet 자체는 공유되므로 상태를 두면 안 됩니다.
질문 수많은 @Controller 를 DispatcherServlet은 어떻게 구분 할까요?
답변 포인트 애플리케이션 시작
시 @RequestMapping, @GetMapping 같은 매핑
정보를 HandlerMapping이 등록합니다.
요청 URL, HTTP 메서드, 파라미터, 헤더 조건 등을 기준으로 적절한 핸들러를 찾습니다.
주의할 점
- DispatcherServlet이 모든 비즈니스 로직을 직접 처리하는 것이 아니라 흐름을 조율합니다.
- 동시 요청 처리는 DispatcherServlet이 여러 개라서가 아니라 컨테이너 스레드 모델 덕분입니다.
15. JPA와 같은 ORM을 사용하는 이유가 무엇인가요?
기본 답변
ORM은 Object Relational Mapping, 즉 객체와 관계형 데이터베이스 테이블 사이의 매핑을 도와주는 기술입니다.
JPA를 사용하면 SQL 중심이 아니라 객체 모델 중심으로 데이터를 다룰 수 있고, 반복적인 CRUD SQL 작성 부담을 줄일 수 있습니다.
또한 영속성 컨텍스트를 통해 1차 캐시, 변경 감지, 지연 로딩, 쓰기 지연 같은 기능을 제공합니다.
다만 SQL이 사라지는 것은 아니므로, 성능이 중요한 경우 실제 실행 SQL과 트랜잭션 범위를 이해하고 사용해야 합니다.
핵심 키워드
- ORM
- Entity
- Persistence Context
- 1차 캐시
- Dirty Checking
- Lazy Loading
- JPQL
꼬리질문
질문 영속성은 어떤 기능을 하나요? 이게 진짜 성능 향상에 큰 도움이 되나요?
답변 포인트 영속성 컨텍스트는 엔티티를 관리하며 1차 캐시, 동일성 보장, 변경 감지, 쓰기 지연을 제공합니다.
성능 향상에 도움이 되는 경우도 있지만, 핵심은 객체 상태 변경을 트랜잭션 안에서 자연스럽게 DB 변경으로 반영하게 해주는 관리 기능입니다.
질문 N + 1 문제에 대해 설명해 주세요.
답변 포인트 처음 1번의 쿼리로 N개의 엔티티를 가져온 뒤, 연관 엔티티를 조회하면서 N번의 추가 쿼리가 발생하는 문제입니다.
fetch join, EntityGraph, batch size, DTO 조회 등으로 해결할 수 있습니다.
주의할 점
- JPA는 SQL을 몰라도 되게 해주는 기술이 아닙니다. 오히려 SQL과 DB 동작을 알아야 안전하게 사용할 수 있습니다.
- 모든 연관관계를 즉시 로딩으로 바꾸는 것은 N + 1의 좋은 해결책이 아닙니다.
16. @Transactional 은 어떤 기능을 하나요?
기본 답변
@Transactional은 Spring에서 트랜잭션 경계를 선언적으로
지정하는 애노테이션입니다.
메서드 실행 전에 트랜잭션을 시작하고, 정상 종료되면 커밋하며, 예외가 발생하면 설정에 따라 롤백합니다.
Spring은 주로 프록시 기반 AOP로 @Transactional을
적용합니다.
따라서 트랜잭션 전파, 격리 수준, readOnly, timeout, rollbackFor 같은 옵션을 통해 트랜잭션 동작을 제어할 수 있습니다.
핵심 키워드
- 트랜잭션 경계
- 프록시
- commit
- rollback
- propagation
- isolation
- readOnly
꼬리질문
질문 @Transactional(readOnly = true)는 어떤 기능인가요? 이게 도움이 되나요?
답변 포인트 읽기 전용 트랜잭션임을 Spring과 하위 기술에 알려 최적화 여지를 줍니다.
JPA에서는 flush 모드 조정으로 변경 감지 비용을 줄이는 데 도움이 될 수 있고, DB에 따라 읽기 전용 힌트로 활용될 수 있습니다.
다만 무조건 큰 성능 향상을 보장하지는 않습니다.
질문 그런데, 읽기에 트랜잭션을 걸 필요가 있나요? @Transactional을 안 붙이면 되는거 아닐까요?
답변 포인트 읽기에도 트랜잭션이 필요한 경우가 있습니다.
같은 작업 범위에서 일관된 읽기를 보장하거나, Lazy Loading이 필요한 경우, 복수 쿼리 간 일관성이 필요한 경우 유용합니다.
단순 단건 조회에서는 없어도 동작할 수 있지만 서비스 정책상 읽기 트랜잭션을 명시하는 경우가 많습니다.
주의할 점
-
Spring의 기본 롤백은 unchecked exception과 Error에 대해 동작합니다.
checked exception 롤백은
rollbackFor설정이 필요할 수 있습니다. -
같은 클래스 내부 메서드 호출에는 프록시가 개입하지 않아
@Transactional이 적용되지 않을 수 있습니다.
17. Java 에서 Annotation 은 어떤 기능을 하나요?
기본 답변
Annotation은 코드에 메타데이터를 붙이는 문법입니다.
클래스, 메서드, 필드, 파라미터 등에 부가 정보를 선언할 수 있고, 컴파일러나 런타임 프레임워크가 이 정보를 읽어 특정 동작을 수행할 수 있습니다.
Java 자체에서는 @Override, @Deprecated처럼
컴파일러 확인이나 문서화를 위한 애노테이션이 있고, Spring에서는
@Component, @Autowired,
@Transactional, @RequestMapping처럼 런타임
동작을 구성하는 데 적극적으로 사용합니다.
핵심 키워드
- 메타데이터
- Retention
- Target
- Reflection
- Component Scan
- Bean 등록
꼬리질문
질문 별 기능이 없는 것 같은데, 어떻게 Spring 에서는 Annotation 이 그렇게 많은 기능을 하는 걸까요?
답변 포인트 애노테이션 자체가 기능을 수행하는 것은 아닙니다.
Spring이 리플렉션과 클래스패스 스캔으로 애노테이션을 읽고, Bean 등록, 의존성 주입, 프록시 생성, 요청 매핑 같은 동작을 수행하기 때문에 기능이 생깁니다.
질문 Lombok의 @Data를 잘 사용하지 않는 이유는 무엇일까요?
답변 포인트
@Data는 getter, setter, toString, equals, hashCode 등을
한 번에 생성합니다.
편하지만 불필요한 setter로 객체 불변성이 깨지고, JPA 연관관계에서 toString 순환 참조나 equals/hashCode 문제가 생길 수 있습니다.
필요한 애노테이션만 명시적으로 쓰는 편이 안전합니다.
주의할 점
- 애노테이션은 표시이고, 실제 동작은 컴파일러, annotation processor, 런타임 프레임워크가 만듭니다.
-
Retention 정책이
RUNTIME이 아니면 런타임 리플렉션으로 읽을 수 없습니다.
18. Tomcat이 정확히 어떤 역할을 하는 도구인가요?
기본 답변
Tomcat은 Java Servlet 컨테이너이자 웹 서버 역할을 하는 도구입니다.
HTTP 요청을 받아 Servlet API 기반 애플리케이션으로 전달하고, 응답을 클라이언트에게 돌려줍니다.
Spring MVC 애플리케이션에서는 Tomcat이 요청을 받고, 그 요청을 DispatcherServlet으로 전달합니다.
Spring Boot에서는 내장 Tomcat을 사용해 별도의 WAS 설치 없이 애플리케이션을 실행할 수 있습니다.
핵심 키워드
- Servlet Container
- WAS
- HTTP 요청 처리
- Thread Pool
- DispatcherServlet
- Embedded Tomcat
꼬리질문
질문 혹시 Netty에 대해 들어보셨나요? 왜 이런 것을 사용할까요?
답변 포인트 Netty는 비동기 이벤트 기반 네트워크 애플리케이션 프레임워크입니다.
Servlet 기반 요청-스레드 모델보다 비동기 I/O와 이벤트 루프를 활용하기 좋습니다.
고성능 네트워크 서버, WebFlux, gRPC, 프록시, 게임 서버 같은 곳에서 사용될 수 있습니다.
주의할 점
- Tomcat은 단순 정적 파일 웹 서버만이 아니라 Servlet 컨테이너입니다.
- Spring Boot가 실행된다고 해서 Tomcat이 없는 것은 아닙니다. 기본 설정에서는 내장 Tomcat이 함께 실행됩니다.
19. Java의 제네릭과 타입 소거에 대해 설명해 주세요.
기본 답변
제네릭은 클래스, 인터페이스와 메서드가 사용할 타입을 매개변수화해 컴파일
시점에 타입 오류를 찾고 불필요한 형변환을 줄이는 기능입니다. 같은
자료구조와 알고리즘을 여러 타입에 재사용하면서도
List<String>에 다른 타입이 들어가는 실수를 컴파일러가
막을 수 있습니다.
Java 제네릭은 타입 소거 방식으로 구현됩니다. 컴파일러는 타입 변수를 가장
왼쪽 경계 타입이나 경계가 없으면 Object로 소거하고, 반환값
등에 필요한 형변환을 삽입하며, 소거 후에도 다형성을 보존해야 할 때
bridge method를 생성합니다. 그래서 List<String>과
List<Integer>를 위해 별도 런타임 클래스가
만들어지지는 않습니다.
그렇다고 제네릭 정보가 클래스 파일에서 모두 사라지는 것은 아닙니다.
필드, 메서드 매개변수와 상위 타입 선언의 generic signature가
메타데이터로 남아 있으면 reflection으로
List<String> 같은 선언 정보를 읽을 수 있습니다. 다만
일반적인 new ArrayList<String>() 객체 자체에는 실제
타입 인자가 실체화되어 있지 않아 런타임 객체만 보고 String 목록인지
확인할 수 없습니다.
런타임에 타입을 완전히 확인할 수 있는 타입을 reifiable type이라고 하며
일반 클래스, raw type, 기본 타입과 List<?> 같은 일부
타입이 여기에 해당합니다. 타입 소거 때문에 new T(),
new T[], instanceof List<String>에는
제약이 있고, 변성 문제는 wildcard와 ? extends·? super를 이용해 필요한 읽기·쓰기 범위만 허용하는 방식으로 해결합니다.
핵심 키워드
- Generic
- Type Erasure
- Invariance
- Wildcard
- PECS
- Bridge Method
- Generic Signature
- Reifiable Type
꼬리질문
질문
List<Integer>는 왜
List<Number>의 하위 타입이 아닌가요?
답변 포인트 이를 허용해
List<Integer>를 List<Number>로
본다면 Number를 받는 코드가 Double을 추가할 수 있고,
원래 Integer 목록의 타입 안전성이 깨집니다. 그래서 Java의 일반적인
제네릭 타입은 무공변입니다.
읽기만 필요한 경우에는 List<? extends Number>로
공통 상한을 표현할 수 있지만, 어떤 구체 타입의 목록인지 모르므로
null 이외의 값을 안전하게 추가할 수 없습니다.
질문
? extends T와 ? super T는 언제 사용하나요?
답변 포인트 값을 주로 읽는
생산자에는 ? extends T, T 값을 넣는 소비자에는
? super T를 사용한다는 PECS 원칙으로 설명할 수
있습니다.
? extends T에서는 값을 T로 읽을 수 있지만 구체 타입을
모르므로 추가가 제한됩니다. ? super T에는 T와 그 하위
타입 값을 넣을 수 있지만 읽은 값은 일반적으로
Object로만 안전하게 다룰 수 있습니다.
질문 왜
new T(), new T[]와
instanceof List<String>를 사용할 수 없나요?
답변 포인트 타입 소거 후에는 JVM이 T의 실제 생성자나 배열의 런타임 컴포넌트 타입을 알 수 없습니다. 배열은 런타임에 컴포넌트 타입을 검사하는 reified 구조라 소거되는 제네릭과 직접 결합하면 타입 안전성을 보장하기 어렵습니다.
instanceof List<?>처럼 런타임에 확인 가능한
타입은 사용할 수 있습니다. T 객체 생성이 필요하면
Class<T>, Supplier<T>나
팩터리를 명시적으로 전달하는 방식을 사용할 수 있습니다.
질문 타입 소거가 된다면 reflection으로 제네릭 타입을 어떻게 읽을 수 있나요?
답변 포인트 소거된 실행 타입과
별도로 컴파일러가 선언부의 generic signature를 클래스 파일
메타데이터에 남길 수 있습니다. 그래서 필드나 메서드 선언에 적힌
타입은 getGenericType(),
getGenericParameterTypes() 등으로 확인할 수 있습니다.
그러나 지역 변수로 생성한 일반 객체의 실제 타입 인자는 보통 알 수 없습니다. 익명 하위 클래스나 구체 상속 선언처럼 타입 인자를 상위 타입 signature에 남기는 패턴은 별도의 경우입니다.
주의할 점
- Raw type과 unchecked 형변환은 heap pollution을 만들 수 있습니다.
-
타입 소거 때문에
new T(),new T[],instanceof List<String>에는 제약이 있습니다. - 컴파일 시점의 제네릭 안전성도 raw type, 가변 인자와 unchecked 경계를 통과하면 깨질 수 있으므로 경고를 무시하지 않아야 합니다.
20. Java의 플랫폼 스레드와 가상 스레드를 비교해 주세요.
기본 답변
플랫폼 스레드는 운영체제 스레드를 얇게 감싼 형태로 수명 동안 해당 OS
스레드를 점유하므로 생성 비용과 스택 등 자원 사용량이 비교적 큽니다.
Java 21에서 정식 기능이 된 가상 스레드는 JVM이 스케줄링하는 경량
Thread이며, 많은 가상 스레드가 소수의 플랫폼 스레드를
carrier로 공유할 수 있습니다.
가상 스레드가 실행될 때 JVM은 이를 carrier에 mount합니다. 지원되는 blocking I/O를 만나면 가상 스레드를 중단하고 carrier에서 unmount해 그 플랫폼 스레드가 다른 가상 스레드를 실행하도록 하며, I/O가 준비되면 다시 스케줄링해 carrier에 mount합니다. 이 때문에 동기식 blocking 코드와 thread-per-task 구조를 유지하면서도 대기 중인 작업이 OS 스레드를 계속 점유하지 않을 수 있습니다.
가상 스레드는 네트워크·파일·JDBC처럼 대기가 많은 동시 작업의 처리량을 높이는 데 적합합니다. CPU 연산 자체를 더 빠르게 하거나 개별 요청 지연을 자동으로 낮추지는 않으며, 이미 적은 수의 이벤트 루프에서 논블로킹으로 처리되는 코드도 같은 이점을 얻지 못할 수 있습니다.
플랫폼 스레드처럼 가상 스레드를 고정 크기 풀로 재사용하지 않고 작업마다
생성하는 것이 권장됩니다. 외부 API나 DB의 동시 처리량을 제한해야 한다면
스레드 풀의 크기를 대신 사용하지 말고 Semaphore나 커넥션
풀처럼 해당 자원의 용량을 나타내는 장치로 제한해야 합니다.
Pinning은 가상 스레드가 carrier에서 분리되지 못하는 상황입니다. JDK 24의
JEP 491부터 synchronized 블록과 메서드 때문에 발생하던
pinning은 제거됐지만, 가상 스레드는 native method나 FFM foreign
function을 실행하는 동안 carrier에서 unmount하지 못하고 pinned됩니다.
짧고 blocking하지 않는 호출은 영향이 작지만 native 실행이 오래 걸리거나
그 코드가 호출한 Java callback이 blocking하면 확장성을 제한하며, 드문
class loading·initialization 대기도 남아 있습니다. 사용하는 JDK 버전과
JFR 같은 관측 도구로 실제 원인을 확인해야 합니다.
핵심 키워드
- Platform Thread
- Virtual Thread
- Carrier Thread
- Thread-per-task
- Blocking I/O
- Java 21
- Mount/Unmount
- Semaphore
- Pinning
꼬리질문
질문 CPU 연산이 많은 작업도 가상 스레드를 많이 만들면 빨라지나요?
답변 포인트 아닙니다. 동시에 실행할 CPU 연산 수는 코어 수에 제한되며 가상 스레드는 연산을 더 빠르게 수행하는 기술이 아닙니다. CPU 집약 작업은 코어 수에 맞춘 제한된 병렬성과 작업 분할이 더 중요합니다.
CPU 작업을 무제한 가상 스레드로 만들면 스케줄링과 컨텍스트 전환 비용만 늘 수 있습니다. 가상 스레드의 장점은 주로 작업 시간이 I/O 대기로 구성된 환경에서 나타납니다.
질문 가상 스레드를 사용하면 race condition을 신경 쓰지 않아도 되나요?
답변 포인트 가상 스레드도
Thread이므로 공유 가변 상태의 visibility, atomicity와
순서 문제는 그대로 발생합니다. 불변 객체, 락, atomic 클래스와 동시성
컬렉션 같은 동기화 수단이 여전히 필요합니다.
오히려 동시에 실행 가능한 작업 수가 크게 늘어 공유 상태와 외부 자원의 경합이 더 잘 드러날 수 있으므로 부하 테스트와 제한 정책이 중요합니다.
질문 가상 스레드를 풀링하지 않는다면 외부 시스템의 동시 요청 수는 어떻게 제한하나요?
답변 포인트 가상 스레드는
희소한 작업자 자원이 아니라 작업 자체를 표현하므로 작업마다
생성합니다. 외부 서비스가 동시에 20개 요청만 처리할 수 있다면
Semaphore(20)처럼 그 자원의 용량을 직접 표현하는 동시성
제한을 둡니다.
DB 커넥션 풀도 획득 가능한 연결 수로 자연스럽게 동시성을 제한합니다. 다만 연결을 기다리는 가상 스레드와 요청 큐가 지나치게 쌓이지 않도록 timeout, 전체 요청 상한과 backpressure도 함께 고려해야 합니다.
질문 Pinning은 무엇이며
synchronized를 피해야 하나요?
답변 포인트 Pinning은 가상 스레드가 blocking되어도 carrier를 놓지 못해 플랫폼 스레드까지 함께 묶이는 현상입니다. 장시간·빈번하게 발생하면 가상 스레드의 확장성을 떨어뜨릴 수 있습니다.
JDK 23 이하에서는 synchronized 내부 blocking이 주요
원인이었지만 JDK 24의 JEP 491로 이 제약이 제거됐습니다. 현재
JDK에서는 native·FFM 실행 중의 장시간 blocking, 그 코드가 호출한
Java callback의 blocking과 드문 class loading·initialization 대기
같은 잔여 사례를 관측해야 합니다. 반대로 짧은 native 호출까지 성능
문제로 단정하거나 synchronized를 무조건
ReentrantLock으로 바꾸는 조언도 적절하지 않습니다.
질문 가상 스레드에서 ThreadLocal을 사용할 때 무엇을 주의해야 하나요?
답변 포인트 가상 스레드도 ThreadLocal을 지원하지만 수십만 개의 가상 스레드마다 큰 객체나 캐시를 보관하면 총메모리 사용량이 커집니다. 플랫폼 스레드 풀의 소수 작업자에게 데이터를 저장한다는 기존 가정도 더 이상 맞지 않을 수 있습니다.
요청 범위 데이터는 수명을 짧게 유지하고 반드시 정리하며, 라이브러리와 JDK 버전이 지원한다면 명시적인 컨텍스트 전달이나 더 적합한 범위 관리 수단을 검토해야 합니다.
주의할 점
- 스레드가 가벼워도 DB 커넥션과 외부 API 용량은 제한적입니다.
- 많은 가상 스레드에 큰 ThreadLocal 데이터를 두면 메모리 사용량이 커질 수 있습니다.
- 가상 스레드의 pinning 원인과 진단 방식은 JDK 버전에 따라 달라지므로 Java 21 초기 자료를 현재 JDK에 그대로 적용하면 안 됩니다.
21. Spring Boot의 Starter와 자동 구성은 어떻게 동작하나요?
기본 답변
Starter는 웹, 데이터 접근, 보안처럼 특정 기능에 일반적으로 필요한 의존성을 호환되는 조합으로 제공하는 의존성 모음입니다. Starter 자체가 애플리케이션을 설정하는 것은 아니며, 함께 들어온 자동 구성 모듈이 클래스패스, 등록된 Bean, 설정 프로퍼티와 애플리케이션 종류를 보고 필요한 Bean과 설정을 조건부로 등록합니다.
@SpringBootApplication은
@SpringBootConfiguration,
@EnableAutoConfiguration과 @ComponentScan을
합친 편의 애노테이션입니다. Component Scan은 애플리케이션 패키지의
컴포넌트를 찾고, @EnableAutoConfiguration은 Spring Boot가
제공하거나 외부 라이브러리가 등록한 자동 구성 후보를 불러오도록 합니다.
현재 Spring Boot의 자동 구성 라이브러리는 JAR의
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports에 @AutoConfiguration 클래스를 등록합니다. 후보가 로드되면
@ConditionalOnClass,
@ConditionalOnMissingBean,
@ConditionalOnProperty 같은 조건을 평가합니다. 예를 들어
필요한 클래스가 있고 사용자가 같은 역할의 Bean을 등록하지 않았을 때만
기본 Bean을 제공하는 back-off 방식으로 사용자의 구성을 존중합니다.
자동 구성은 편리한 기본값이지 반드시 적용되는 설정은 아닙니다. Condition Evaluation Report로 어떤 조건이 일치하거나 불일치했는지 확인하고, 필요한 경우 프로퍼티, 사용자 Bean이나 exclude로 일부 구성을 교체해야 합니다. 여러 자동 구성 사이에 순서가 필요하면 before·after 관계를 선언하지만, 자동 구성 클래스 내부 구현에 직접 의존하기보다 공개된 Bean과 설정 계약을 사용해야 합니다.
핵심 키워드
- Starter
- Auto-configuration
- @SpringBootApplication
- @EnableAutoConfiguration
- @ConditionalOnClass
- @ConditionalOnMissingBean
- AutoConfiguration.imports
- Back-off
- Condition Evaluation Report
꼬리질문
질문 자동 구성이 적용되지 않을 때 어떻게 확인하나요?
답변 포인트 debug 모드나 Condition Evaluation Report에서 해당 자동 구성과 Bean 조건이 왜 일치하거나 불일치했는지 확인합니다. 클래스패스 누락, 프로퍼티 값, 기존 사용자 Bean, 애플리케이션 타입과 자동 구성 제외 여부를 순서대로 확인합니다.
단순히 패키지 스캔 범위만 확인해서는 해결되지 않을 수 있습니다. 자동 구성 후보가 등록됐는지와 조건 평가 결과를 함께 봐야 합니다.
질문 Component Scan과 자동 구성은 같은 기능인가요?
답변 포인트 아닙니다.
Component Scan은 지정한 패키지에서 @Component,
@Service, @Configuration 같은 클래스를
찾아 Bean 정의로 등록합니다.
자동 구성은 AutoConfiguration.imports에 등록된 별도
후보를 불러와 클래스패스, Bean과 프로퍼티 조건에 따라 적용합니다.
@SpringBootApplication이 두 기능을 함께 활성화하므로
같은 동작처럼 보일 수 있습니다.
질문 사용자가 같은 타입의 Bean을 등록하면 자동 구성은 어떻게 되나요?
답변 포인트 해당 자동 구성이
@ConditionalOnMissingBean을 사용한다면 조건이 불일치해
기본 Bean 등록을 건너뜁니다. 이것이 사용자 설정이 자동 구성보다
우선하도록 만드는 대표적인 back-off 방식입니다.
모든 자동 구성이 같은 조건을 사용하는 것은 아니므로 타입, 이름, 검색할 ApplicationContext 범위와 평가 순서를 공식 문서나 Condition Evaluation Report로 확인해야 합니다.
질문 직접 만든 라이브러리에 자동 구성을 제공하려면 어떻게 하나요?
답변 포인트 현재 Spring
Boot에서는 @AutoConfiguration 클래스를 만들고 필요한
조건을 선언한 뒤 JAR의
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports에 클래스 이름을 등록합니다. 필요한 일반 의존성을 모은 별도
Starter를 함께 제공할 수 있습니다.
자동 구성 패키지를 Component Scan 대상으로 삼거나 내부 구현 Bean을
공개 API처럼 노출하지 않아야 합니다. Spring Boot 2.6 이하의 오래된
자료는 spring.factories 등록 방식을 설명할 수 있으므로
대상 Boot 버전을 확인해야 합니다.
주의할 점
- Starter는 의존성 모음이고 자동 구성 로직 자체와는 구분해야 합니다.
- 자동 구성을 마법으로 취급하지 말고 실제 Bean과 조건을 확인해야 합니다.
-
AutoConfiguration.imports는 현재 Spring Boot 기준이며 오래된 버전의 등록 방식과 혼동하지 않아야 합니다.
22. JPA 연관관계 매핑과 연관관계의 주인에 대해 설명해 주세요.
기본 답변
JPA는 @OneToOne, @OneToMany,
@ManyToOne, @ManyToMany로 객체 참조와 관계형
데이터베이스의 외래 키 또는 조인 테이블을 매핑합니다. 단방향 관계는 한쪽
객체만 상대를 참조하고, 양방향 관계는 양쪽 객체가 서로를 참조하지만
데이터베이스 관계 자체가 두 개가 되는 것은 아닙니다.
양방향 관계의 주인은 DB 관계 업데이트를 결정하는 쪽이고 반대쪽은
mappedBy로 주인의 필드나 프로퍼티를 지정합니다.
일대다·다대일에서는 외래 키를 매핑하는 다대일 쪽이 주인입니다. 양방향
일대일은 외래 키 기반이면 그 키를 매핑한 쪽이, 조인 테이블 기반이면
@JoinTable을 선언한 쪽이 주인입니다. 다대다에서는 어느 쪽도
주인으로 정할 수 있으며 주인 쪽이 조인 테이블 매핑과 변경을 결정합니다.
JPA가 DB 반영에 주인 쪽 값을 사용하더라도 런타임 객체 관계의 일관성은 애플리케이션이 책임집니다. 주인만 수정하면 DB에는 반영되더라도 같은 트랜잭션 안에서 반대쪽 컬렉션을 읽을 때 이전 상태가 보일 수 있고, 반대쪽만 수정하면 기대한 DB 변경이 발생하지 않을 수 있습니다. 연관관계 편의 메서드로 두 참조를 함께 갱신하는 이유입니다.
Cascade는 PERSIST, MERGE,
REMOVE 같은 EntityManager 작업을 연관 엔티티에 전파하는
기능이며 데이터베이스의 ON DELETE CASCADE와는 다른 계층의
동작입니다. 두 엔티티의 생명주기가 실제로 함께 움직일 때만 필요한 범위의
cascade를 선택해야 하며, Jakarta Persistence 명세상
cascade=REMOVE는 OneToOne과
OneToMany 관계에 적용하는 것이 이식 가능한 방식입니다.
orphanRemoval은 OneToOne과
OneToMany에서 지원되며, 부모가 사라지지 않아도 관계에서
제거된 전용 자식을 flush 시 삭제 대상으로 만듭니다. 부모 삭제를 자식에
전파하는 CascadeType.REMOVE와 목적이 다르고, 다른 부모와
독립적으로 공유되는 엔티티에는 사용하면 안 됩니다.
핵심 키워드
- Association Mapping
- Foreign Key
- Owning Side
- Inverse Side
- mappedBy
- Join Table
- Cascade
- orphanRemoval
꼬리질문
질문 주인이 아닌 쪽만 수정하면 어떻게 되나요?
답변 포인트 메모리의 반대쪽 참조나 컬렉션은 바뀌어도 DB 관계 업데이트를 결정하는 주인 값이 변경되지 않았으므로 flush 시 기대한 외래 키나 조인 테이블 변경이 발생하지 않을 수 있습니다.
반대로 주인만 수정하면 DB는 갱신되지만 현재 객체 그래프의 반대쪽은 이전 상태로 남을 수 있습니다. 두 방향을 함께 갱신하는 편의 메서드를 한쪽에 두고 일관되게 사용하는 것이 좋습니다.
질문 Cascade REMOVE와 orphanRemoval은 어떻게 다른가요?
답변 포인트 Cascade REMOVE는
부모 엔티티에 수행한 remove 작업을 자식에게 전파합니다.
orphanRemoval은 부모가 살아 있어도 자식 참조를 null로
바꾸거나 컬렉션에서 제거해 관계를 끊으면 해당 자식을 삭제 대상으로
만듭니다.
orphanRemoval은 부모가 자식의 생명주기를 독점하는
OneToOne·OneToMany 관계에 적합합니다.
공유되거나 독립적으로 존재해야 하는 엔티티에는 적합하지 않습니다.
질문 관계 종류별로 연관관계의 주인은 어떻게 결정되나요?
답변 포인트 양방향
일대다·다대일에서는 외래 키를 매핑하는 다대일 쪽이 주인이고
ManyToOne에는 mappedBy를 지정할 수
없습니다. 양방향 일대일은 외래 키 기반이면 그 키를 매핑한 쪽이, 조인
테이블 기반이면 @JoinTable을 선언한 쪽이 주인이며
반대쪽이 mappedBy로 참조합니다.
다대다에서는 어느 쪽도 주인이 될 수 있지만 한쪽만 주인으로 정하고
반대쪽이 mappedBy로 참조합니다. 실무에서는 다대다 조인
테이블에 추가 속성이나 수명 관리가 필요한 경우가 많아 연결 엔티티로
풀어내기도 합니다.
질문 CascadeType.ALL을 모든 연관관계에 적용하면 어떤 문제가 생길 수 있나요?
답변 포인트 부모의 persist, merge와 remove가 의도하지 않은 독립 엔티티까지 전파될 수 있습니다. 특히 여러 부모가 공유하는 엔티티나 다대다 관계에서 remove 전파는 다른 데이터의 참조와 수명을 깨뜨릴 위험이 있습니다.
두 엔티티의 생성·수정·삭제 생명주기가 실제로 동일한지 확인하고 필요한 cascade 종류만 선택해야 합니다. 대량 cascade는 예상하지 못한 SQL과 락 범위를 만들 수 있으므로 실행 SQL도 확인해야 합니다.
질문 단방향과 양방향 연관관계 중 무엇을 선택해야 하나요?
답변 포인트 두 방향 탐색이 실제 도메인 로직에 필요하지 않다면 단방향이 상태 관리가 단순합니다. 양방향은 반대 방향 탐색이 빈번하고 객체 모델에 자연스러울 때 선택하되 두 참조의 일관성을 유지해야 합니다.
방향성과 fetch 전략은 별개입니다. 양방향으로 만든다고 필요한 데이터가 자동으로 효율적으로 조회되는 것은 아니므로 쿼리와 로딩 전략을 따로 설계해야 합니다.
주의할 점
- 생명주기가 독립적인 엔티티에 삭제 Cascade를 무분별하게 적용하면 안 됩니다.
- 조회 편의만으로 모든 관계를 양방향으로 만들면 상태 관리가 복잡해집니다.
-
orphanRemoval은OneToOne·OneToMany의 전용 자식 수명 관리 기능이며 단순히 컬렉션을 정리하는 옵션이 아닙니다.
23. Spring Security의 필터 체인과 인증 처리 흐름을 설명해 주세요.
기본 답변
Servlet 기반 Spring Security에서는 Servlet 컨테이너에 등록된
DelegatingFilterProxy가 Spring Bean인
FilterChainProxy에 처리를 위임합니다.
FilterChainProxy는 요청과 일치하는
SecurityFilterChain을 순서대로 찾고 가장 먼저 일치한 체인
하나의 보안 필터를 실행합니다. 따라서 API와 웹 화면에 서로 다른 체인을
둘 때 matcher와 체인 우선순위가 중요합니다.
인증 필터는 요청에서 아이디·비밀번호나 bearer token 같은 자격 증명을
읽어 아직 인증되지 않은 Authentication을 만들고
AuthenticationManager에 인증을 위임합니다. 가장 일반적인
구현인 ProviderManager는 Authentication 타입을 지원하는
AuthenticationProvider를 찾아 실제 검증을 맡기므로 폼
로그인, JWT, LDAP처럼 서로 다른 인증 방식을 한 구조 안에서 조합할 수
있습니다.
인증에 성공하면 principal과 권한을 담은 인증된
Authentication이 반환됩니다. ProviderManager는
기본적으로 민감한 credentials를 제거하며, 필터는 반환된 Authentication을
SecurityContextHolder가 관리하는
SecurityContext에 설정합니다. 이후 인가 필터는 현재
Authentication의 권한과 요청 규칙을 비교해 접근을 결정합니다. 인증과
인가는 순서가 다르므로 커스텀 필터도 어떤 보안 단계 이후에 실행돼야
하는지 기준으로 위치를 정해야 합니다.
SecurityContextRepository는 요청 사이에 SecurityContext를
어떻게 불러오고 저장할지 결정합니다. 세션 기반 로그인은 보통 세션에 인증
상태를 보존하고, stateless bearer token API는 서버 세션에 저장하지 않고
매 요청 토큰을 검증해 컨텍스트를 다시 만듭니다. 어느 방식이든 요청
처리가 끝난 뒤 현재 스레드의 SecurityContext는 정리되어 다음 요청으로
누출되지 않아야 합니다.
인증되지 않았거나 AuthenticationException이 발생하면
AuthenticationEntryPoint가 인증 절차를 시작합니다. Form
Login은 로그인 페이지로 redirect할 수 있고 Basic·Bearer API는 일반적으로
401과 WWW-Authenticate로 응답합니다. 인증된 사용자의 권한이
부족하면 AccessDeniedHandler가 보통 403을 처리하며,
ExceptionTranslationFilter는 보안 필터 체인의 예외를 이러한
처리 흐름으로 연결합니다.
핵심 키워드
- DelegatingFilterProxy
- FilterChainProxy
- SecurityFilterChain
- Authentication
- AuthenticationManager
- ProviderManager
- AuthenticationProvider
- SecurityContext
- SecurityContextRepository
- Authorization
꼬리질문
질문 여러 SecurityFilterChain이 같은 요청과 일치하면 모두 실행되나요?
답변 포인트 아닙니다.
FilterChainProxy가 가장 먼저 일치한 체인 하나만
선택하므로 /api/**처럼 구체적인 matcher를 가진 체인을
범용 체인보다 앞에 둬야 합니다.
앞쪽 체인이 일치하면 뒤쪽 체인의 필터는 실행되지 않습니다. 체인마다 인증 방식과 필터 구성이 독립적일 수 있으므로 DEBUG 로그와 실제 요청으로 선택 결과를 확인해야 합니다.
질문 AuthenticationManager, ProviderManager와 AuthenticationProvider는 어떻게 다른가요?
답변 포인트
AuthenticationManager는 필터가 인증을 요청하는 진입점
인터페이스이고, ProviderManager는 가장 일반적인
구현입니다. ProviderManager는 자신이 가진 여러
AuthenticationProvider에 인증을 위임합니다.
각 AuthenticationProvider는 특정 Authentication 타입을 지원하고 실제 자격 증명 검증과 사용자 조회를 수행합니다. 지원하지 않으면 다음 provider가 시도할 수 있고, 성공하면 인증된 Authentication을 반환하며 실패하면 AuthenticationException을 발생시킵니다.
질문 JWT 기반 stateless API라면 Spring Security가 필요 없나요?
답변 포인트 아닙니다. Spring Security 필터가 매 요청의 bearer token을 추출하고 서명, 만료, issuer와 audience를 검증한 뒤 Authentication을 만들어 권한 규칙에 사용할 수 있습니다.
Stateless는 인증 정보를 서버 세션에 지속하지 않는다는 뜻입니다. 요청 동안에는 SecurityContext가 필요하고, 토큰 폐기·refresh token·사용자 상태를 별도 저장한다면 시스템 전체가 완전히 무상태인 것도 아닙니다.
질문 인증 실패와 권한 부족은 필터 체인에서 어떻게 다른 응답으로 처리되나요?
답변 포인트 인증 정보가 없거나
유효하지 않으면 AuthenticationEntryPoint가 인증 절차를
시작합니다. Form Login은 로그인 페이지로 redirect할 수 있고
Basic·Bearer API는 일반적으로 401을 반환합니다. 이미 인증됐지만
필요한 권한이 없으면 AccessDeniedHandler가 보통 403을
반환합니다.
ExceptionTranslationFilter가 보안 예외를 이러한
처리기로 연결하지만, 인증 필터 자체에서 실패한 경우에는 해당 필터의
failure handler가 직접 응답할 수도 있으므로 필터 종류와 위치를
확인해야 합니다.
질문 커스텀 인증 필터는 체인의 어디에 추가해야 하나요?
답변 포인트 고정된 한 위치를 외우기보다 필터가 필요로 하는 선행 보안 단계와 이후 소비자를 기준으로 결정합니다. 인증 필터라면 exploit protection과 컨텍스트 준비 이후, 인가보다 앞에서 Authentication을 만들어야 합니다.
addFilterBefore, addFilterAfter,
addFilterAt으로 기준 필터와의 상대 위치를 명시하고,
같은 필터가 Servlet 컨테이너와 SecurityFilterChain 양쪽에 중복
등록되지 않았는지도 확인해야 합니다.
주의할 점
- 커스텀 필터는 보안 단계와 순서를 판단해 정확한 위치에 추가해야 합니다.
- Stateless API라도 브라우저의 자격 증명 전송 방식에 따라 CSRF 위험을 판단해야 합니다.
- SecurityContext를 직접 설정한다면 실패·비동기·스레드 재사용 경로에서도 저장과 정리가 올바른지 확인해야 합니다.