데일리 CS 학습
Q. List, Set, Map의 차이를 설명해주세요.
A. List는 순서가 있고 중복을 허용합니다. Set은 순서가 없고 중복을 허용하지 않습니다. Map은 key-valu 쌍으로 저장하고 key는 중복 불가입니다.
Q. ArrayList와 LinkedList의 차이를 설명해주세요.
A. ArrayList는 내부적으로 배열을 사용해 인덱스 조회가 O(1)로 빠르지만, 중간 삽입, 삭제 시 요소를 밀어야 해서 O(n)입니다. LinkedList는 노드를 포인터로 연결해 중간 삽입, 삭제가 O(1)이지만 특정 위치 조회는 O(n)입니다.
Q. 제네릭이란 무엇이고 왜 사용하나요?
A. 클래스나 메서드를 정의할 때 타입을 미리 고정하지 않고, 사용 시점에 지정할 수 있게 하는 기능입니다. 가장 큰 장점은 두 가지로 첫째 타입 안정성입니다. 컴파일 타임에 타입을 체크하므로 잘못된 타입이 들어오면 즉시 에러가 납니다. 둘째로 형변환 생략입니다. Object로 받아서 매번 캐스팅하는 번거로움이 없어집니다.
Q. Optional이란 무엇이고 어떻게 활용하나요?
A. null을 직접 다루는 대신 값이 있을 수도, 없을 수도 있다는 것을 명시적으로 표현하는 컨테이너 클래스입니다. NullPointerException을 방지하기 위해 사용합니다.
Spring Bean 생명주기
등록 · 의존성 주입 · 초기화 · 소멸
스프링 컨테이너가 빈을 만들고 없애기까지 — 각 단계에서 무슨 일이 일어나는지, 콜백을 어떻게 활용하는지 완전 정리
01 Bean 생명주기 전체 흐름
스프링 빈은 컨테이너가 시작되면서 생성되고, 컨테이너가 종료되면 소멸합니다. 그 사이에 의존성 주입과 초기화·소멸 콜백이 순서대로 호출됩니다.
객체 생성(생성자)과 초기화는 분리하는 것이 좋습니다. 생성자는 객체를 만드는 역할에만 집중하고, DB 연결 같은 무거운 초기화 작업은
@PostConstruct에서 합니다. 이렇게 해야 모든 의존성이 주입된 상태에서 안전하게 초기화할 수 있습니다.02 스프링 컨테이너와 빈 등록
IoC 컨테이너(ApplicationContext)는 빈의 생성·관리·소멸을 담당합니다. 빈을 등록하는 방법은 크게 세 가지입니다.
// 방법 1 — @Component 계열 (컴포넌트 스캔) ✅ 가장 일반적
@Service // 비즈니스 로직 레이어
@Repository // 데이터 접근 레이어 (DataAccessException 변환 포함)
@Controller // 웹 요청 처리 레이어
@Component // 위 셋에 해당하지 않는 일반 컴포넌트
// → 모두 내부에 @Component를 포함, 자동 스캔 대상
// 방법 2 — @Bean (수동 등록) — 외부 라이브러리, 세밀한 설정 필요 시
@Configuration
public class AppConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
// 지연 초기화 — @Lazy: 처음 요청할 때 생성 (컨테이너 시작 시 아님)
@Service
@Lazy
public class HeavyService {
// 무거운 초기화 → 실제 사용 전까지 생성 지연
}Java03 의존성 주입 (DI) 3가지 방식
// ✅ 방법 1 — 생성자 주입 (가장 권장)
@Service
@RequiredArgsConstructor // final 필드 생성자 자동 생성 (Lombok)
public class OrderService {
private final MemberRepository memberRepository; // final → 불변 보장
private final OrderRepository orderRepository;
// 생성자가 하나면 @Autowired 생략 가능
}
// ❌ 방법 2 — 필드 주입 (지양)
@Service
public class OrderService {
@Autowired
private MemberRepository memberRepository; // 테스트 시 주입 불가, 불변 보장 안 됨
}
// △ 방법 3 — setter 주입 (선택적 의존관계에만)
@Service
public class OrderService {
private MemberRepository memberRepository;
@Autowired(required = false) // 빈이 없어도 무시
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}Java- final로 불변 보장
- 순환 참조 조기 감지
- 테스트 시 직접 주입 가능
- 필수 의존관계 명확
- 코드 간결 (유일한 장점)
- 테스트 시 주입 어려움
- 순환 참조 런타임 발견
- 불변성 보장 안 됨
- 선택적 의존관계 표현
- 주입 후 변경 가능
- 불변성 보장 안 됨
- 실무 사용 드묾
같은 타입 빈이 여러 개 — @Primary, @Qualifier
@Component
@Primary // 우선순위 높음 — 설정 없으면 이 빈 주입
public class RateDiscountPolicy implements DiscountPolicy { ... }
@Component
@Qualifier("fixDiscount")
public class FixDiscountPolicy implements DiscountPolicy { ... }
// 주입 시 명시적으로 지정
public OrderService(@Qualifier("fixDiscount") DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}Java04 초기화 콜백 — @PostConstruct
@PostConstruct는 DI 완료 직후 딱 한 번 호출됩니다. 모든 의존 객체가 주입된 상태이므로 안전하게 초기화 작업을 수행할 수 있습니다.
@Service
@RequiredArgsConstructor
public class CacheService {
private final ProductRepository productRepository;
private Map<Long, Product> cache;
// 생성자에서 하면 안 되는 이유:
// 필드/setter 주입 시 생성자 실행 시점엔 의존 객체가 아직 null
@PostConstruct // DI 완료 후 호출 — productRepository 안전하게 사용 가능
public void init() {
cache = productRepository.findAll().stream()
.collect(Collectors.toMap(Product::getId, p -> p));
log.info("상품 캐시 초기화 완료: {}건", cache.size());
}
}
// DB 연결 확인 — 시작 시 즉시 문제 감지
@Component
@RequiredArgsConstructor
public class DatabaseHealthChecker {
private final DataSource dataSource;
@PostConstruct
public void checkConnection() {
try (Connection conn = dataSource.getConnection()) {
log.info("DB 연결 확인 완료: {}", conn.getMetaData().getURL());
} catch (SQLException e) {
throw new IllegalStateException("DB 연결 실패", e);
}
}
}Java예외가 발생하면 애플리케이션 컨텍스트 로딩이 실패합니다. DB 연결 실패처럼 치명적인 문제를 조기에 발견하는 장점이 있지만, 너무 무거운 작업을 넣으면 서버 시작이 느려집니다.
05 소멸 콜백 — @PreDestroy
@PreDestroy는 스프링 컨테이너가 종료되기 직전 호출됩니다. 커넥션 반환, 스레드 종료, 외부 리소스 해제 등 정리 작업에 사용합니다.
@Component
public class NetworkClient {
private Connection connection;
@PostConstruct
public void connect() {
connection = openConnection();
log.info("커넥션 연결");
}
@PreDestroy // 컨테이너 종료 직전 — 리소스 정리
public void disconnect() {
if (connection != null) {
connection.close();
log.info("커넥션 해제");
}
}
}
// 스레드 풀 종료 예시
@Component
public class SchedulerManager {
private final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(5);
@PreDestroy
public void shutdown() {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
}
}
}Java프로토타입 스코프 빈은 @PreDestroy가 호출되지 않습니다. 생성 후 컨테이너가 더 이상 관리하지 않기 때문입니다. 또한 JVM이 강제 종료(kill -9)되면 소멸 콜백이 호출되지 않을 수 있습니다.
06 3가지 생명주기 콜백 방법 비교
// ✅ 방법 1 — @PostConstruct / @PreDestroy (권장)
// JSR-250 표준 어노테이션 → 스프링 종속 아님, 코드 간결
@PostConstruct
public void init() { ... }
@PreDestroy
public void destroy() { ... }
// △ 방법 2 — InitializingBean / DisposableBean 인터페이스 (비권장)
// 스프링 인터페이스에 강하게 결합 → 외부 라이브러리 적용 불가
public class NetworkClient implements InitializingBean, DisposableBean {
@Override
public void afterPropertiesSet() { connect(); } // 초기화
@Override
public void destroy() { disconnect(); } // 소멸
}
// △ 방법 3 — @Bean(initMethod, destroyMethod) (외부 라이브러리에 유용)
@Bean(initMethod = "init", destroyMethod = "close")
public DataSource dataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
return ds;
// → ds.init() 초기화, ds.close() 소멸 시 자동 호출
}Java| 방법 | 스프링 종속 | 외부 라이브러리 | 권장 |
|---|---|---|---|
@PostConstruct/@PreDestroy |
❌ 없음 (JSR-250) | 적용 불가 | ✅ 기본 사용 |
| InitializingBean / DisposableBean | ✅ 있음 | 적용 불가 | ❌ 비권장 |
@Bean(initMethod, destroyMethod) |
❌ 없음 | ✅ 가능 | △ 외부 라이브러리만 |
07 빈 스코프 — Singleton · Prototype · Request
스코프는 빈이 존재할 수 있는 범위입니다. 기본값은 싱글톤입니다.
컨테이너에 딱 1개만 생성. 모든 요청에서 같은 인스턴스 공유.
빈 요청 시마다 새 객체 생성. 스프링이 생성까지만 관여. @PreDestroy 호출 안 됨.
HTTP 요청 1개마다 1개 생성. 요청 종료 시 소멸. 웹 스코프(session, application도 있음).
@Component
@Scope("prototype")
public class PrototypeBean { ... }
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
// proxyMode: 싱글톤 빈에 request 스코프 빈 주입 시 프록시로 해결
private String requestURL;
}
// ⚠ Singleton 빈에 Prototype 빈 주입 시 문제
@Service
public class SingletonService {
@Autowired
private PrototypeBean prototypeBean;
// 싱글톤 생성 시 한 번만 주입 → 매번 같은 객체! (prototype 의미 없음)
// ✅ 해결: ObjectProvider 사용
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public void logic() {
PrototypeBean bean = prototypeBeanProvider.getObject(); // 매번 새 객체
}
}Java08 BeanPostProcessor — 빈 후처리기
BeanPostProcessor는 빈이 생성되고 초기화되는 전·후에 끼어들어 빈을 조작할 수 있는 확장 포인트입니다. 스프링이 내부적으로 AOP 프록시 생성, @Autowired 주입 등에 활용합니다.
// BeanPostProcessor 실행 순서
// 빈 생성 → postProcessBeforeInitialization() → @PostConstruct → postProcessAfterInitialization()
@Component
public class LoggingBeanPostProcessor implements BeanPostProcessor {
// @PostConstruct 실행 전
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
log.debug("초기화 전: {}", beanName);
return bean; // 원본 빈 또는 다른 객체 반환 가능
}
// @PostConstruct 실행 후
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
log.debug("초기화 후: {}", beanName);
// 여기서 프록시 객체로 교체하면 AOP 동작
return bean;
}
}
// 스프링이 내부적으로 사용하는 BeanPostProcessor들
// AutowiredAnnotationBeanPostProcessor → @Autowired 처리
// CommonAnnotationBeanPostProcessor → @PostConstruct, @PreDestroy 처리
// AnnotationAwareAspectJAutoProxyCreator → @Transactional, @Async 등 AOP 프록시 생성Java@PostConstruct는 스프링이 직접 처리하는 것이 아니라, CommonAnnotationBeanPostProcessor가 빈의 메서드를 스캔해서 호출합니다. 마찬가지로 @Transactional의 AOP 프록시도 BeanPostProcessor를 통해 생성됩니다.09 실무 함정 패턴
함정 1 — 생성자에서 의존 빈 사용 (필드 주입 시)
// ❌ 필드 주입 시 생성자에서 의존 빈 사용 — NPE 발생
@Service
public class BadService {
@Autowired
private MemberRepository memberRepository; // 생성자 호출 후 주입됨
public BadService() {
memberRepository.findAll(); // NPE! 아직 null
}
}
// ✅ 해결 1 — 생성자 주입 (주입과 생성이 동시에)
@Service
@RequiredArgsConstructor
public class GoodService {
private final MemberRepository memberRepository;
}
// ✅ 해결 2 — @PostConstruct로 초기화 분리
@Service
public class GoodService2 {
@Autowired
private MemberRepository memberRepository;
@PostConstruct
public void init() {
memberRepository.findAll(); // 안전 — DI 완료 후 호출
}
}Java함정 2 — 싱글톤 빈의 상태 공유
// ❌ 싱글톤 빈에 상태(인스턴스 변수) 저장 — 동시성 문제 발생
@Service
public class StatefulService {
private int price; // 여러 스레드가 공유 → 데이터 꼬임!
public void order(int price) {
this.price = price; // Thread-A 10000 저장 중 Thread-B가 20000 덮어씀
}
}
// ✅ 싱글톤 빈은 무상태(stateless)로 설계
@Service
public class StatelessService {
public int order(int price) {
return price; // 상태를 지역변수로 처리 → 스레드 안전
}
}Java싱글톤 빈은 여러 스레드가 동시에 접근합니다. 인스턴스 변수에 상태를 저장하면 동시성 버그가 발생합니다. 스프링 빈은 항상 무상태(stateless)로 설계하고, 상태가 필요하면 지역변수나 파라미터를 사용하거나 prototype/request 스코프를 사용하세요.
10 면접 예상 질문
Q1. 스프링 빈의 생명주기를 설명하세요.
Q2. 생성자 주입을 권장하는 이유는?
Q3. @PostConstruct와 생성자의 차이는?
@PostConstruct는 DI가 완전히 완료된 후 호출되므로 주입된 의존 객체를 안전하게 사용해 초기화할 수 있습니다. DB 연결 확인, 캐시 로드 등 의존 객체가 필요한 초기화 작업은 반드시 @PostConstruct에서 해야 합니다.Q4. 싱글톤 빈이 무상태여야 하는 이유는?
Q5. BeanPostProcessor란 무엇이고 어디에 쓰이나요?
AutowiredAnnotationBeanPostProcessor는 @Autowired 처리, CommonAnnotationBeanPostProcessor는 @PostConstruct/@PreDestroy 처리, AnnotationAwareAspectJAutoProxyCreator는 AOP 프록시 생성을 담당합니다.- 생명주기 순서 — 컨테이너 생성 → 빈 등록 → 객체 생성 → DI → @PostConstruct → 사용 → @PreDestroy → 소멸
- 생성자 주입 권장 — final 불변 보장, 순환 참조 조기 감지, 테스트 용이, 필수 의존관계 명확
- @PostConstruct — DI 완료 후 호출. 의존 빈을 사용하는 초기화 위치. 예외 시 앱 시작 실패
- @PreDestroy — 컨테이너 종료 직전 호출. 리소스 해제. 프로토타입 빈은 호출 안 됨
- 싱글톤 무상태 — 모든 스레드가 공유하므로 인스턴스 변수 상태 저장 금지
- BeanPostProcessor — @Autowired, @PostConstruct, AOP 프록시가 모두 이것을 통해 동작하는 스프링 핵심 확장 포인트
'스프링' 카테고리의 다른 글
| HTTP & HTTPS (0) | 2026.04.28 |
|---|---|
| Spring MVC 동작 원리 (0) | 2026.04.22 |
| 예외 처리 전략 @ControllerAdvice · @ExceptionHandler · 공통 에러 응답 설계 (1) | 2026.04.20 |
| JPQL & QueryDSL객체지향 쿼리부터 동적 쿼리까지 (1) | 2026.04.17 |
| @Transactional 동작 원리 / AOP 프록시 · self-invocation · readOnly 최적화 (0) | 2026.04.16 |