Java & Spring Boot 면접 빈출 질문 정리
Java & Spring Boot 면접 빈출 질문 정리
📦 PART 1. Java 핵심
1. JVM 구조와 동작 원리
질문: JVM의 구조를 설명하고, 자바 코드가 실행되는 과정을 말해주세요.
JVM은 크게 Class Loader, Runtime Data Area, Execution Engine 세 부분으로 구성된다.
① Class Loader.java → 컴파일 → .class(바이트코드) → Class Loader가 JVM 메모리에 로드한다.
로딩 → 링킹(검증/준비/해석) → 초기화 순서로 진행된다.
② Runtime Data Area (메모리 구조)
| 영역 | 설명 | 공유 여부 |
|---|---|---|
| Method Area | 클래스 메타데이터, static 변수, 상수 풀 | 스레드 공유 |
| Heap | 객체 인스턴스, 배열 | 스레드 공유 |
| Stack | 메서드 호출 스택, 지역 변수, 프레임 | 스레드별 독립 |
| PC Register | 현재 실행 중인 명령어 주소 | 스레드별 독립 |
| Native Method Stack | C/C++ 네이티브 메서드 | 스레드별 독립 |
③ Execution Engine
바이트코드를 실제 기계어로 변환해서 실행한다.
- 인터프리터: 한 줄씩 해석 (느림)
- JIT 컴파일러: 자주 실행되는 코드를 통째로 기계어로 컴파일 (빠름)
- GC: Heap 영역의 불필요한 객체 정리
2. GC (Garbage Collection)
질문: GC의 동작 원리와 종류를 설명해주세요.
기본 원리: Reachability
GC Root(스택 변수, static 변수 등)에서 참조 가능한 객체는 살리고, 닿지 않는 객체는 수거한다.
Heap 구조
Young Generation Old Generation Metaspace
┌──────────────────┐ ┌──────────────┐ ┌──────────┐
│ Eden | S0 | S1 │ → │ │ │ 클래스 │
└──────────────────┘ └──────────────┘ │ 메타데이터 │
Minor GC 발생 Major GC └──────────┘
- Eden: 새 객체가 태어나는 곳
- Survivor(S0/S1): Minor GC에서 살아남은 객체 이동
- Old: 오래 살아남은 객체 (여기서 GC = Major GC, 느림)
GC 종류
| GC | 특징 | 사용 시점 |
|---|---|---|
| Serial GC | 단일 스레드, STW | 소형 앱 |
| Parallel GC | 멀티 스레드, 처리량 중심 | Java 8 기본 |
| G1 GC | 영역 분할, 예측 가능한 pause | Java 9+ 기본 |
| ZGC | 초저지연, 1ms 이하 pause | 대규모 서비스 |
| Shenandoah | Red Hat, ZGC 유사 | 저지연 필요 시 |
STW(Stop-The-World): GC 실행 중 모든 애플리케이션 스레드가 멈추는 현상. 이걸 줄이는 게 GC 튜닝의 핵심이다.
3. Java 메모리 누수
질문: Java에서 메모리 누수가 발생하는 경우를 설명해주세요.
Java는 GC가 있지만 메모리 누수가 생길 수 있다.
대표 케이스
// 1. static 컬렉션에 계속 추가하고 제거 안 할 때 static List<Object> cache = new ArrayList<>(); // 2. Connection, Stream 등을 close() 안 했을 때 Connection conn = dataSource.getConnection(); // close() 없음 → 누수 // 3. 리스너/콜백 등록 후 해제 안 했을 때 eventBus.register(listener); // unregister() 없음 // 4. ThreadLocal 사용 후 remove() 안 했을 때 ThreadLocal<UserContext> ctx = new ThreadLocal<>(); ctx.set(user); // ctx.remove() 없음 → 스레드풀에서 누수
해결: try-with-resources, WeakReference 활용, ThreadLocal.remove() 습관화
4. equals()와 hashCode()
질문: equals()와 hashCode()를 같이 오버라이드해야 하는 이유는?
// 오버라이드 안 한 경우
User u1 = new User("kim", 30);
User u2 = new User("kim", 30);
u1.equals(u2); // false (Object 기본: 주소 비교)
Set<User> set = new HashSet<>();
set.add(u1);
set.contains(u2); // false → 논리적으로 같은데 다르다고 판단
HashSet/HashMap 동작 원리
hashCode()로 버킷 위치 결정- 같은 버킷 안에서
equals()로 동일성 비교
→ hashCode만 같고 equals가 다르면: 같은 버킷에 중복 저장
→ equals만 같고 hashCode가 다르면: 다른 버킷에 저장돼서 contains() 실패
올바른 오버라이드
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return age == user.age && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
5. String, StringBuilder, StringBuffer
질문: 셋의 차이점과 언제 뭘 써야 하는지 설명해주세요.
| 구분 | 불변 여부 | 스레드 안전 | 성능 |
|---|---|---|---|
| String | 불변 (immutable) | 안전 | 연산 많으면 느림 |
| StringBuilder | 가변 (mutable) | 안전하지 않음 | 빠름 |
| StringBuffer | 가변 (mutable) | synchronized 안전 | 중간 |
// String: + 연산마다 새 객체 생성 → 루프에서 쓰면 메모리 낭비 String s = ""; for (int i = 0; i < 1000; i++) s += i; // 객체 1000개 생성 // StringBuilder: 단일 스레드 문자열 조작에 적합 StringBuilder sb = new StringBuilder(); for (int i = 0; i < 1000; i++) sb.append(i); // 객체 1개 // StringBuffer: 멀티스레드 환경에서만 사용
6. 컬렉션 프레임워크
질문: List, Set, Map의 차이와 각 구현체의 특징을 설명해주세요.
List
| 구현체 | 특징 | 시간복잡도 |
|---|---|---|
| ArrayList | 동적 배열, 인덱스 조회 빠름 | 조회 O(1), 삽입/삭제 O(n) |
| LinkedList | 이중 연결 리스트, 삽입/삭제 빠름 | 조회 O(n), 삽입/삭제 O(1) |
Set
| 구현체 | 특징 |
|---|---|
| HashSet | 순서 없음, 가장 빠름 O(1) |
| LinkedHashSet | 삽입 순서 유지 |
| TreeSet | 정렬 유지, O(log n) |
Map
| 구현체 | 특징 |
|---|---|
| HashMap | 순서 없음, O(1), null 키 허용 |
| LinkedHashMap | 삽입 순서 유지 |
| TreeMap | 키 기준 정렬 |
| ConcurrentHashMap | 멀티스레드 안전, 세그먼트 락 |
HashMap 동작 원리
key → hashCode() → 버킷 인덱스 결정 같은 버킷에 충돌 시 → LinkedList로 연결 (Java 8부터 8개 초과 시 TreeNode로 전환)
7. 스레드와 동시성
질문: synchronized, volatile, atomic의 차이를 설명해주세요.
synchronized
// 메서드 레벨
public synchronized void increment() { count++; }
// 블록 레벨 (더 세밀한 제어)
synchronized (this) { count++; }
→ 한 번에 한 스레드만 진입 보장. 성능 오버헤드 있음.
volatile
private volatile boolean running = true;
→ 변수를 CPU 캐시가 아닌 메인 메모리에서 직접 읽고 씀.
→ 가시성(visibility) 보장. 하지만 원자성(atomicity)은 보장 안 함.
→ count++ 같은 복합 연산에는 부족하다.
AtomicInteger
AtomicInteger count = new AtomicInteger(0); count.incrementAndGet(); // CAS(Compare-And-Swap)로 원자성 보장
→ synchronized보다 가볍고 빠름.
8. 인터페이스 vs 추상 클래스
질문: 인터페이스와 추상 클래스의 차이와 언제 무엇을 쓰는지 설명해주세요.
| 구분 | 인터페이스 | 추상 클래스 |
|---|---|---|
| 다중 구현 | 가능 | 불가능 |
| 생성자 | 없음 | 있음 |
| 상태(필드) | 상수만 | 인스턴스 변수 가능 |
| 접근제어자 | public 기본 | 제한 없음 |
| 목적 | 할 수 있는 것 정의 | 공통 속성/행동 정의 |
// 인터페이스: 기능 계약 (Flyable, Serializable, Runnable)
interface Flyable { void fly(); }
// 추상 클래스: 공통 베이스 (Animal → Dog, Cat)
abstract class Animal {
String name;
abstract void sound();
void breathe() { System.out.println("숨쉬기"); } // 공통 구현
}
→ “~할 수 있다”면 인터페이스, “~의 일종이다”면 추상 클래스
9. Java 8 주요 기능
질문: Java 8에서 추가된 주요 기능들을 설명해주세요.
① 람다 표현식
// 이전
Comparator<String> c = new Comparator<String>() {
public int compare(String a, String b) { return a.compareTo(b); }
};
// 이후
Comparator<String> c = (a, b) -> a.compareTo(b);
② Stream API
List<String> result = users.stream()
.filter(u -> u.getAge() > 20)
.map(User::getName)
.sorted()
.collect(Collectors.toList());
③ Optional
// NPE 방어
Optional<User> user = userRepository.findById(id);
user.ifPresent(u -> System.out.println(u.getName()));
String name = user.map(User::getName).orElse("unknown");
④ default 메서드
interface Collection {
default void forEach(Consumer action) { ... } // 인터페이스에 구현 가능
}
⑤ 날짜/시간 API
LocalDate today = LocalDate.now(); LocalDateTime now = LocalDateTime.now(); Duration d = Duration.between(start, end);
10. 제네릭
질문: 제네릭의 목적과 와일드카드를 설명해주세요.
// 제네릭 없으면: 타입 캐스팅 필요, 런타임 오류 위험
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // 캐스팅 필요
// 제네릭: 컴파일 타임 타입 체크
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // 캐스팅 불필요
와일드카드
PECS 원칙: Producer Extends, Consumer Super
🌱 PART 2. Spring Core
11. IoC와 DI
질문: IoC와 DI의 개념을 설명해주세요.
IoC (Inversion of Control, 제어의 역전)
원래 객체 생성과 의존성 관리를 개발자가 직접 했는데, 이 제어권을 스프링 컨테이너에게 넘기는 것.
// IoC 없을 때: 내가 직접 생성
UserService userService = new UserServiceImpl(new UserRepositoryImpl());
// IoC 있을 때: 컨테이너가 만들어서 주입
@Service
public class UserController {
@Autowired
private UserService userService; // 스프링이 알아서 주입
}
DI (Dependency Injection, 의존성 주입) 3가지 방법
// 1. 생성자 주입 (가장 권장)
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
// 2. 세터 주입 (선택적 의존성)
@Autowired
public void setRepository(UserRepository repo) { this.repo = repo; }
// 3. 필드 주입 (편리하지만 비권장)
@Autowired
private UserRepository userRepository;
생성자 주입이 권장되는 이유
- 불변성 보장 (
final사용 가능) - 순환 의존성 컴파일 타임에 감지
- 테스트 시 Mock 주입 용이
- 필수 의존성 명시
12. Spring Bean 스코프
질문: Spring Bean의 스코프 종류를 설명해주세요.
| 스코프 | 설명 | 사용 예 |
|---|---|---|
| singleton | 기본값, 컨테이너당 인스턴스 1개 | 대부분의 서비스 |
| prototype | 요청마다 새 인스턴스 | 상태를 가지는 객체 |
| request | HTTP 요청마다 새 인스턴스 | 웹 요청 처리 |
| session | HTTP 세션마다 새 인스턴스 | 로그인 세션 |
| application | ServletContext당 1개 | 앱 전역 상태 |
@Component
@Scope("prototype")
public class ShoppingCart { ... }
주의: singleton 빈이 prototype 빈을 주입받으면 prototype이 사실상 singleton처럼 동작한다. 이때는 ObjectProvider또는 @Lookup을 사용해야 한다.
13. AOP
질문: AOP(관점 지향 프로그래밍)가 무엇이고 스프링에서 어떻게 동작하는지 설명해주세요.
AOP란?
로깅, 보안, 트랜잭션 같은 횡단 관심사(cross-cutting concern) 를 핵심 비즈니스 로직에서 분리하는 기법.
핵심 용어
| 용어 | 설명 |
|---|---|
| Aspect | 횡단 관심사 모듈 (로깅, 트랜잭션 등) |
| JoinPoint | Advice를 적용할 수 있는 지점 (메서드 실행 등) |
| Pointcut | 어떤 JoinPoint에 적용할지 표현식 |
| Advice | 실제 실행될 코드 (Before/After/Around 등) |
| Proxy | AOP가 적용된 객체를 감싸는 대리 객체 |
@Aspect
@Component
public class LoggingAspect {
// "service 패키지의 모든 public 메서드"에 적용
@Around("execution(* com.example.service.*.*(..))")
public Object log(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed(); // 실제 메서드 실행
long end = System.currentTimeMillis();
System.out.println(pjp.getSignature() + " 실행 시간: " + (end - start) + "ms");
return result;
}
}
스프링 AOP 동작 방식
- JDK Dynamic Proxy: 인터페이스 기반
- CGLIB Proxy: 클래스 기반 (인터페이스 없을 때)
- 스프링 부트는 기본적으로 CGLIB 사용
14. @Transactional
질문: @Transactional의 동작 원리와 주의사항을 설명해주세요.
동작 원리
AOP 프록시가 트랜잭션을 시작하고, 메서드 실행 후 커밋/롤백을 결정한다.
@Service
public class OrderService {
@Transactional
public void createOrder(OrderDto dto) {
// 프록시가 트랜잭션 시작
orderRepository.save(order);
stockRepository.decrease(dto.getItemId());
// 성공 시 커밋, 예외 시 롤백
}
}
전파(Propagation) 옵션
| 옵션 | 설명 |
|---|---|
| REQUIRED (기본) | 기존 트랜잭션 있으면 참여, 없으면 새로 생성 |
| REQUIRES_NEW | 항상 새 트랜잭션 생성, 기존 것 일시 중단 |
| NESTED | 중첩 트랜잭션 (savepoint 사용) |
| SUPPORTS | 트랜잭션 있으면 참여, 없으면 없이 실행 |
| NOT_SUPPORTED | 트랜잭션 없이 실행 |
| NEVER | 트랜잭션 있으면 예외 |
격리(Isolation) 수준
| 수준 | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|
| READ_UNCOMMITTED | 발생 | 발생 | 발생 |
| READ_COMMITTED | 방지 | 발생 | 발생 |
| REPEATABLE_READ | 방지 | 방지 | 발생 |
| SERIALIZABLE | 방지 | 방지 | 방지 |
⚠️ 주의사항 (자주 실수하는 것들)
// 1. private 메서드에는 적용 안 됨 (프록시 통과 불가)
@Transactional
private void doSomething() { ... } // 무효
// 2. 같은 클래스 내 self-invocation은 프록시 우회
public void outer() {
inner(); // 트랜잭션 적용 안 됨! 프록시를 거치지 않음
}
@Transactional
public void inner() { ... }
// 3. RuntimeException만 기본 롤백
@Transactional // IOException은 롤백 안 됨
public void save() throws IOException { ... }
// rollbackFor 지정해야 함
@Transactional(rollbackFor = Exception.class)
public void save() throws IOException { ... }
🚀 PART 3. Spring Boot 핵심
15. Spring Boot 자동 설정 원리
질문: Spring Boot의 자동 설정(Auto Configuration)이 어떻게 동작하는지 설명해주세요.
@SpringBootApplication
↓
@EnableAutoConfiguration
↓
spring.factories (또는 AutoConfiguration.imports) 파일 읽기
↓
조건(@Conditional)에 맞는 설정 클래스들을 자동으로 빈으로 등록
예를 들어 spring-boot-starter-data-jpa 의존성을 추가하면:
- classpath에 JPA 라이브러리 존재 감지
@ConditionalOnClass(DataSource.class)조건 충족- DataSource, EntityManagerFactory, TransactionManager 자동 등록
// 이런 식의 자동 설정 클래스들이 내부에 있음
@Configuration
@ConditionalOnClass(DataSource.class)
@ConditionalOnMissingBean(DataSource.class) // 내가 직접 정의하면 적용 안 함
public class DataSourceAutoConfiguration { ... }
→ application.yml에서 spring.datasource.url 등을 설정하면 이 설정 클래스들이 읽어서 적용한다.
16. Spring Boot 실행 과정
질문: SpringApplication.run()이 실행될 때 내부적으로 무슨 일이 일어나는지 설명해주세요.
1. SpringApplication 객체 생성
2. ApplicationContext 타입 결정 (웹/비웹)
3. ApplicationListener, Initializer 로드
4. run() 실행
├── Environment 준비 (application.yml 로드)
├── ApplicationContext 생성 및 준비
├── Bean 정의 로드 (@ComponentScan)
├── 자동 설정 적용
├── 모든 Bean 생성 및 의존성 주입
├── CommandLineRunner, ApplicationRunner 실행
└── 서버 시작 (내장 Tomcat)
17. 필터 vs 인터셉터
질문: Filter와 Interceptor의 차이를 설명해주세요.
| 구분 | Filter | Interceptor |
|---|---|---|
| 속한 곳 | 서블릿 컨테이너 (J2EE) | 스프링 컨테이너 |
| 실행 시점 | DispatcherServlet 앞 | DispatcherServlet 뒤 |
| 스프링 빈 접근 | 어려움 | 쉬움 |
| 요청/응답 조작 | 가능 (바이트 레벨) | 일부 가능 |
| 사용 예 | 인코딩, CORS, XSS 방어 | 인증/인가, 로깅, 권한 체크 |
HTTP 요청
↓
[Filter 1] → [Filter 2]
↓
DispatcherServlet
↓
[Interceptor preHandle]
↓
Controller 실행
↓
[Interceptor postHandle]
↓
View 렌더링
↓
[Interceptor afterCompletion]
↓
[Filter 2] → [Filter 1] (역순)
↓
HTTP 응답
18. Spring Security
질문: Spring Security의 인증 처리 흐름을 설명해주세요.
HTTP 요청
↓
SecurityFilterChain (여러 Filter로 구성)
↓
UsernamePasswordAuthenticationFilter
↓
AuthenticationManager (ProviderManager)
↓
AuthenticationProvider (DaoAuthenticationProvider)
↓
UserDetailsService.loadUserByUsername()
↓
UserDetails 반환 → 비밀번호 검증
↓
Authentication 객체 생성 → SecurityContextHolder에 저장
↓
인증 성공 → 요청 계속 진행
JWT 방식에서의 차이
HTTP 요청 (Authorization: Bearer xxx)
↓
JwtAuthenticationFilter (커스텀 필터)
↓
토큰 검증 → Authentication 생성 → SecurityContext 저장
↓
나머지 필터/컨트롤러 진행
19. JPA / Hibernate
질문: JPA의 영속성 컨텍스트와 지연 로딩을 설명해주세요.
영속성 컨텍스트(Persistence Context)
엔티티를 관리하는 1차 캐시. EntityManager가 관리한다.
[영속성 컨텍스트]
┌─────────────────────────────┐
│ 1차 캐시: {id: 1 → User} │
│ 변경 감지 (Dirty Checking) │
│ 쓰기 지연 SQL 저장소 │
└─────────────────────────────┘
엔티티 생명주기
비영속(new) → persist() → 영속(managed)
영속 → detach() → 준영속(detached)
영속 → remove() → 삭제(removed)
더티 체킹(Dirty Checking)
@Transactional
public void update(Long id, String newName) {
User user = userRepository.findById(id).get(); // 영속 상태
user.setName(newName); // 그냥 값만 바꿈
// save() 안 해도 트랜잭션 종료 시 자동으로 UPDATE 쿼리 실행
}
N+1 문제
// LAZY 로딩일 때
List<Order> orders = orderRepository.findAll(); // 쿼리 1번
for (Order o : orders) {
o.getMember().getName(); // 각 Order마다 Member 쿼리 → N번 추가 발생
}
// 총 쿼리: 1 + N번
// 해결책: fetch join
@Query("SELECT o FROM Order o JOIN FETCH o.member")
List<Order> findAllWithMember();
지연(LAZY) vs 즉시(EAGER) 로딩
- EAGER: 연관 엔티티를 즉시 조회 → 예상치 못한 쿼리 발생 위험
- LAZY: 실제 사용 시점에 조회 → 권장. 하지만 세션 밖에서 접근 시
LazyInitializationException주의
20. REST API 설계
질문: RESTful API 설계 원칙을 설명해주세요.
REST 6가지 제약
- Client-Server 분리
- Stateless (서버는 클라이언트 상태 저장 안 함)
- Cacheable
- Uniform Interface
- Layered System
- Code on Demand (선택)
올바른 URL 설계
# 나쁜 예
GET /getUsers
POST /createUser
PUT /updateUser/1
DELETE /deleteUser/1
# 좋은 예
GET /users → 목록 조회
POST /users → 생성
GET /users/{id} → 단건 조회
PUT /users/{id} → 전체 수정
PATCH /users/{id} → 부분 수정
DELETE /users/{id} → 삭제
HTTP 상태 코드
| 코드 | 의미 |
|---|---|
| 200 OK | 성공 |
| 201 Created | 생성 성공 |
| 204 No Content | 성공, 응답 본문 없음 (DELETE) |
| 400 Bad Request | 잘못된 요청 |
| 401 Unauthorized | 인증 필요 |
| 403 Forbidden | 권한 없음 |
| 404 Not Found | 리소스 없음 |
| 409 Conflict | 충돌 (중복 등) |
| 500 Internal Server Error | 서버 오류 |
21. 예외 처리 전략
질문: Spring Boot에서 예외 처리를 어떻게 구성하는지 설명해주세요.
// 1. 커스텀 예외 클래스
public class UserNotFoundException extends RuntimeException {
private final ErrorCode errorCode;
public UserNotFoundException(Long id) {
super("User not found: " + id);
this.errorCode = ErrorCode.USER_NOT_FOUND;
}
}
// 2. 에러 코드 enum
public enum ErrorCode {
USER_NOT_FOUND(404, "사용자를 찾을 수 없습니다"),
DUPLICATE_EMAIL(409, "이미 사용 중인 이메일입니다");
private final int status;
private final String message;
}
// 3. 전역 예외 핸들러
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException e) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(e.getErrorCode()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpected(Exception e) {
log.error("Unexpected error", e);
return ResponseEntity.internalServerError()
.body(new ErrorResponse(ErrorCode.INTERNAL_ERROR));
}
}
22. 캐시 처리
질문: Spring의 캐시 추상화를 설명해주세요.
// @EnableCaching 활성화 후
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public User findById(Long id) {
// 캐시에 있으면 메서드 실행 안 하고 바로 반환
return userRepository.findById(id).orElseThrow();
}
@CacheEvict(value = "users", key = "#id")
public void deleteById(Long id) {
// 삭제 후 캐시도 제거
userRepository.deleteById(id);
}
@CachePut(value = "users", key = "#result.id")
public User update(UserUpdateDto dto) {
// 항상 실행하고 결과를 캐시에 저장
return userRepository.save(dto.toEntity());
}
}
Redis 연동 시 spring-boot-starter-data-redis + application.yml 설정만으로 자동 적용된다.
23. 빈 생명주기
질문: Spring Bean의 생명주기를 설명해주세요.
컨테이너 시작
↓
빈 인스턴스화 (생성자 호출)
↓
의존성 주입
↓
BeanPostProcessor.postProcessBeforeInitialization()
↓
@PostConstruct 실행
↓
InitializingBean.afterPropertiesSet()
↓
@Bean(initMethod) 실행
↓
빈 사용
↓
컨테이너 종료
↓
@PreDestroy 실행
↓
DisposableBean.destroy()
↓
@Bean(destroyMethod) 실행
@Component
public class DatabaseConnector {
@PostConstruct
public void init() {
// DB 커넥션 풀 초기화 등
}
@PreDestroy
public void cleanup() {
// 연결 종료, 리소스 반납
}
}
24. 멀티스레드 / 비동기
질문: Spring에서 비동기 처리를 어떻게 하는지 설명해주세요.
// @EnableAsync 활성화 후
@Service
public class MailService {
@Async // 별도 스레드풀에서 비동기 실행
public CompletableFuture<Void> sendMail(String to) {
// 시간 오래 걸리는 작업
mailSender.send(to);
return CompletableFuture.completedFuture(null);
}
}
// 스레드풀 커스텀 설정
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}
25. 테스트
질문: Spring Boot에서 단위 테스트와 통합 테스트를 어떻게 작성하는지 설명해주세요.
단위 테스트 (Mockito)
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
UserRepository userRepository;
@InjectMocks
UserService userService;
@Test
void 사용자_조회_성공() {
// given
User user = new User(1L, "kim");
given(userRepository.findById(1L)).willReturn(Optional.of(user));
// when
User result = userService.findById(1L);
// then
assertThat(result.getName()).isEqualTo("kim");
}
}
통합 테스트 (SpringBootTest)
@SpringBootTest
@AutoConfigureMockMvc
@Transactional // 테스트 후 롤백
class UserControllerTest {
@Autowired MockMvc mockMvc;
@Test
void 사용자_생성_API_테스트() throws Exception {
mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.content("{\"name\":\"kim\",\"age\":30}"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.name").value("kim"));
}
}
슬라이스 테스트
@WebMvcTest(UserController.class) // Controller 레이어만 @DataJpaTest // JPA 레이어만 @DataRedisTest // Redis 레이어만
💡 PART 4. 설계 & 아키텍처
26. SOLID 원칙
| 원칙 | 설명 | 위반 시 문제 |
|---|---|---|
| SRP (단일 책임) | 클래스는 하나의 책임만 | 변경 이유가 많아짐 |
| OCP (개방-폐쇄) | 확장엔 열려있고 수정엔 닫혀있음 | 기존 코드 수정 빈번 |
| LSP (리스코프 치환) | 하위 타입은 상위 타입 대체 가능 | 다형성 깨짐 |
| ISP (인터페이스 분리) | 작은 인터페이스 여러 개 | 불필요한 메서드 구현 강제 |
| DIP (의존 역전) | 추상에 의존, 구체에 의존 금지 | 결합도 높아짐 |
27. 디자인 패턴
질문: 스프링에서 사용되는 주요 디자인 패턴을 설명해주세요.
| 패턴 | 스프링에서의 사용 |
|---|---|
| Singleton | 기본 Bean 스코프 |
| Factory | BeanFactory, ApplicationContext |
| Proxy | AOP, @Transactional |
| Template Method | JdbcTemplate, RestTemplate |
| Strategy | AuthenticationProvider, HandlerMapping |
| Observer | ApplicationEvent / EventListener |
| Decorator | HttpServletRequestWrapper |
| Chain of Responsibility | FilterChain, HandlerInterceptor |
28. 마이크로서비스 관련
질문: 모놀리식과 MSA의 차이, 그리고 MSA에서 트랜잭션 처리를 어떻게 하는지 설명해주세요.
모놀리식 vs MSA
| 구분 | 모놀리식 | MSA |
|---|---|---|
| 배포 | 전체 재배포 | 서비스별 독립 배포 |
| 확장 | 전체 스케일업 | 서비스별 스케일아웃 |
| 트랜잭션 | ACID 보장 쉬움 | 분산 트랜잭션 어려움 |
| 복잡도 | 코드 복잡도 | 운영/인프라 복잡도 |
분산 트랜잭션 패턴: SAGA
# Choreography 방식 (이벤트 기반)
주문서비스 → OrderCreated 이벤트
→ 결제서비스 PaymentProcessed 이벤트
→ 재고서비스 StockDecreased 이벤트
→ 완료
# 실패 시 보상 트랜잭션
재고서비스 실패 → StockDecreaseFailed
→ 결제서비스 보상: PaymentCancelled
→ 주문서비스 보상: OrderCancelled
🗄️ PART 5. DB & 성능
29. 인덱스
질문: DB 인덱스의 원리와 주의사항을 설명해주세요.
B-Tree 인덱스 원리
인덱스 없을 때: 전체 테이블 스캔 O(n)
인덱스 있을 때: B-Tree 탐색 O(log n)
인덱스 걸 때 고려사항
- 카디널리티(중복도)가 높은 컬럼에 유리 (성별은 비효율, 이메일은 효율적)
- WHERE, JOIN, ORDER BY에 자주 쓰는 컬럼
- 복합 인덱스는 선두 컬럼이 WHERE에 있어야 효과
인덱스가 안 타는 경우
-- 함수 사용
WHERE YEAR(created_at) = 2024 -- ❌
WHERE created_at >= '2024-01-01' -- ✅
-- LIKE 앞에 %
WHERE name LIKE '%kim' -- ❌
WHERE name LIKE 'kim%' -- ✅
-- 타입 불일치
WHERE user_id = '123' -- user_id가 INT면 ❌
WHERE user_id = 123 -- ✅
30. 커넥션 풀
질문: DB 커넥션 풀의 원리와 HikariCP 주요 설정을 설명해주세요.
DB 연결은 비싼 작업이다 (TCP 연결 + 인증). 커넥션 풀은 미리 연결을 만들어두고 재사용하는 전략이다.
spring:
datasource:
hikari:
maximum-pool-size: 10 # 최대 커넥션 수
minimum-idle: 5 # 최소 유지 커넥션 수
connection-timeout: 30000 # 커넥션 못 얻을 때 대기 시간 (ms)
idle-timeout: 600000 # 유휴 커넥션 유지 시간
max-lifetime: 1800000 # 커넥션 최대 수명
풀 사이즈 공식 (HikariCP 권장)
connections = (core_count * 2) + effective_spindle_count
→ CPU 4코어면 약 8~10개가 적정. 무조건 크다고 좋지 않다.
💡 마지막 팁: 면접에서 중요한 건 단순 암기가 아니라 “왜?”를 설명할 수 있는 것이다. “생성자 주입을 씁니다” 보다 “불변성 보장과 순환참조 조기 감지를 위해 생성자 주입을 선호합니다” 처럼 이유를 붙이는 연습을 해라. 그게 합격과 불합격을 가른다.