티스토리 뷰
Q1. Spring Framework의 핵심 개념인 IoC(Inversion of Control)와 DI(Dependency Injection)의 차이를 설명해보세요.
IoC (Inversion of Control, 제어의 역전)
- 기존에는 개발자가 객체를 생성하고 생명주기를 관리했지만, IoC 컨테이너가 이 역할을 대신 수행
- 개발자는 객체의 생성·소멸을 직접 제어하지 않고, 필요할 때 컨테이너가 제공해 줌
- 이를 통해 객체 간의 결합도를 낮추고 유지보수성을 높일 수 있음
✅ DI (Dependency Injection, 의존성 주입)
- IoC의 구현 방식 중 하나
- 기존에는 new 키워드를 사용해 직접 객체를 생성했지만, DI를 사용하면 외부에서 객체를 주입받음
- DI 방식:
- 생성자 주입
- Setter 주입
- 필드 주입 (@Autowired)
Q2. Spring에서 DI를 구현하는 방법 3가지(생성자 주입, 필드 주입, Setter 주입)의 차이점을 설명하고, 어떤 방법을 권장하는지 이유와 함께 설명해보세요.
DI(의존성 주입) 방식별 설명
1. 생성자 주입 (권장)
@Service
public class UserService{
private final UserRepository userRepository;
public UserService(UserRepository userRepository){
this.userRepository = userRepository;
}
}
✅ 특징
- final 키워드를 사용하여 불변성 유지
- 순환 참조(Circular Dependency) 방지 가능
- 테스트 용이 (생성자를 통해 Mock 객체 주입 가능)
- 스프링 4.3부터 생성자가 하나만 있으면 @Autowired 없이도 자동 주입됨
2. 필드 주입 (비권장)
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
}
❌ 문제점
- DI 컨테이너 없이 직접 객체를 주입할 방법이 없음 → 테스트 어려움
- 순환 참조 문제가 발생할 가능성이 높음
- final 사용 불가 → 객체가 변경될 위험 있음
- Spring Container가 없으면 동작하지 않음 → 순수 Java 코드에서는 주입 불가
⚠ 필드 주입은 스프링이 관리하는 빈에서만 사용 가능하므로 가급적 사용을 지양하는 것이 좋음.
3. Setter 주입 (부분적으로 사용)
@Service
public class UserService{
private UserRepository userRepository;
@Autowired
public void setUserRepository(UserRepository userRepository){
this.userRepository = userRepository;
}
}
✅ 특징
- 선택적 의존 관계가 필요한 경우 유용
- Bean이 먼저 생성된 후 Setter를 통해 의존성을 주입하므로 객체가 변경될 가능성이 있음
❌ 문제점
- 불변성을 보장할 수 없음 (객체가 변경될 위험 있음)
- 주입이 필수가 아닐 때만 적절함
어떤 방식을 권장하는가?
✅ "생성자 주입"을 권장하는 이유
- 불변성 유지: final을 사용해 주입된 객체가 변경되지 않도록 보장
- 테스트 용이성: 스프링 컨테이너 없이도 테스트 가능(Mock 객체를 직접 주입 가능)
- 순환 참조 방지: 객체 간 의존성이 명확해지면서 순환 참조 문제를 방지할 수 있음
- Spring이 생성자를 통한 주입을 기본적으로 지원 (4.3 이상)
💡 Setter 주입은 필수 의존성이 아닌 경우에만 사용하고, 필드 주입은 가급적 사용하지 않는 것이 베스트 프랙티스!
Q3. Spring Bean의 생명주기(lifecycle)에 대해 설명해보세요. 스프링이 Bean을 생성하고 소멸시키는 과정에서 어떤 메서드가 호출되는지 포함해서 설명해주세요.
📌 Spring Bean의 생명주기 (Lifecycle)
Spring Bean은 스프링 컨테이너가 생성하고 관리하며, 생성 → 사용 → 소멸의 과정을 거침.
🔹 Bean Lifecycle 단계
1️⃣ 객체 생성 (Instantiated)
- @Component, @Service, @Repository, @Bean 등으로 등록된 클래스의 객체가 생성됨
- (단, @Lazy 설정 시 실제 호출될 때까지 생성되지 않음)
2️⃣ 의존성 주입 (Dependency Injection)
- @Autowired, 생성자 주입 등을 통해 필요한 의존성이 주입됨
3️⃣ 초기화 (Initialization)
- @PostConstruct (또는 InitializingBean 인터페이스의 afterPropertiesSet(), init-method) 실행
4️⃣ 사용 (Using Bean)
- 실제 서비스 로직이 수행됨
5️⃣ 소멸 (Destruction)
- @PreDestroy (또는 DisposableBean 인터페이스의 destroy(), destroy-method) 실행 후 컨테이너에서 제거됨
🔹 Bean 생명주기에서 실행되는 주요 메서드
시점 메서드 설명Bean 생성 후 | @PostConstruct | 빈이 초기화될 때 실행 |
Bean 생성 후 | afterPropertiesSet() | InitializingBean 인터페이스 구현 시 초기화 수행 |
Bean 생성 후 | init-method="initMethod" | XML 또는 @Bean(initMethod = "initMethod") 설정 가능 |
Bean 제거 전 | @PreDestroy | 빈이 소멸되기 직전에 실행 |
Bean 제거 전 | destroy() | DisposableBean 인터페이스 구현 시 실행 |
Bean 제거 전 | destroy-method="destroyMethod" | XML 또는 @Bean(destroyMethod = "destroyMethod") 설정 가능 |
📌 예제 코드
@Component
public class ExampleBean implements InitializingBean, DisposableBean {
public ExampleBean() {
System.out.println("1. Bean 생성: 생성자 호출");
}
@Autowired
public void setDependency(DependencyBean dependencyBean) {
System.out.println("2. 의존성 주입 완료");
}
@PostConstruct
public void initPostConstruct() {
System.out.println("3. @PostConstruct 실행");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("4. InitializingBean의 afterPropertiesSet() 실행");
}
public void customInitMethod() {
System.out.println("5. XML/Java 설정 기반 init-method 실행");
}
@PreDestroy
public void preDestroy() {
System.out.println("6. @PreDestroy 실행");
}
@Override
public void destroy() throws Exception {
System.out.println("7. DisposableBean의 destroy() 실행");
}
public void customDestroyMethod() {
System.out.println("8. XML/Java 설정 기반 destroy-method 실행");
}
}
💡 실제 실행 순서:
1️⃣ 생성자 호출 → 2️⃣ 의존성 주입 → 3️⃣ @PostConstruct → 4️⃣ afterPropertiesSet() → 5️⃣ init-method
➡ (사용)
➡ 6️⃣ @PreDestroy → 7️⃣ destroy() → 8️⃣ destroy-method
🔹 요약
✅ Spring Bean은 객체 생성 → 의존성 주입 → 초기화 → 사용 → 소멸 순서로 진행됨
✅ @PostConstruct, afterPropertiesSet(), init-method 등 초기화 방법이 있음
✅ @PreDestroy, destroy(), destroy-method 등 소멸 단계에서 수행됨
✅ 일반적으로 @PostConstruct와 @PreDestroy를 사용하는 것이 권장됨
✅ InitializingBean과 DisposableBean 인터페이스를 구현하는 방법보다 유지보수를 고려하면 애노테이션 방식이 더 좋음
Q4. Spring의 싱글톤(Singleton) 스코프와 프로토타입(Prototype) 스코프의 차이점을 설명하고, 각각 어떤 경우에 사용해야 하는지 말해보세요.
📌 싱글톤(Singleton) 스코프 vs 프로토타입(Prototype) 스코프
✅ Spring에서 빈(Bean)은 기본적으로 "싱글톤" 스코프
✅ 필요에 따라 "프로토타입" 스코프를 사용할 수 있음
🔹 1. 싱글톤(Singleton) 스코프
@Component
@Scope("singleton") // 기본값 (생략 가능)
public class SingletonBean {
public SingletonBean() {
System.out.println("싱글톤 빈 생성!");
}
}
📌 특징
✔ 스프링 컨테이너에서 단 하나의 인스턴스(객체)만 생성
✔ 같은 빈을 여러 번 요청해도 같은 객체가 반환됨
✔ 애플리케이션이 종료될 때까지 유지됨
📌 사용 예시
✔ 공통 로직을 관리하는 서비스 객체 (e.g. UserService, OrderService)
✔ DB 커넥션 관리 객체
✔ 공유 데이터를 저장하는 객체 (ex: Cache, Config 클래스 등)
🔹 2. 프로토타입(Prototype) 스코프
@Component
@Scope("prototype")
public class PrototypeBean {
public PrototypeBean() {
System.out.println("프로토타입 빈 생성!");
}
}
특징
✔ 빈을 요청할 때마다 새로운 객체가 생성됨
✔ 스프링 컨테이너에서 생성만 담당하고 관리하지 않음 → @PreDestroy가 호출되지 않음
✔ 가비지 컬렉션이 직접 관리
📌 사용 예시
✔ 상태를 유지하는 객체 (ex: User Session 정보 관리 객체)
✔ 매 요청마다 새로운 인스턴스를 생성해야 하는 경우
✔ 멀티스레드 환경에서 동시성 문제가 발생할 수 있는 경우 (e.g. Thread-Safe한 객체)
📌 싱글톤과 프로토타입의 차이점 정리
비교 항목 싱글톤(Singleton) 프로토타입(Prototype)빈 생성 시점 | 컨테이너 로딩 시 1회 생성 | 요청할 때마다 새로 생성 |
빈 관리 여부 | 스프링 컨테이너가 관리 | 한 번 생성 후 컨테이너가 관리하지 않음 |
객체 동일 여부 | 항상 동일한 객체 반환 | 요청할 때마다 새로운 객체 반환 |
사용 예 | 서비스, 공통 로직, DB 커넥션 | 상태가 변하는 객체, 멀티스레드 객체 |
📌 싱글톤 빈이 프로토타입 빈을 사용할 때 발생하는 문제점
싱글톤 빈에서 프로토타입 빈을 사용할 경우, 한 번 주입된 프로토타입 빈이 계속 재사용되는 문제가 발생할 수 있음.
👎 잘못된 예시
@Component
public class SingletonService {
private final PrototypeBean prototypeBean; // 생성 시점에 주입됨
@Autowired
public SingletonService(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public void printPrototypeBean() {
System.out.println(prototypeBean);
}
}
이렇게 하면 새로운 프로토타입 객체가 필요할 때마다 생성되지 않고, 최초 생성된 객체가 계속 사용됨.
✅ 해결 방법: ObjectProvider 사용
java
복사편집
@Component
public class SingletonService {
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public void printPrototypeBean() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject(); // 매번 새로운 객체 생성
System.out.println(prototypeBean);
}
}
💡 ObjectProvider를 사용하면 매번 새로운 프로토타입 객체를 가져올 수 있음!
🔹 결론: 어떤 경우에 어떤 스코프를 사용할까?
- 싱글톤(Singleton) → 기본적으로 대부분의 서비스에서 사용
- 프로토타입(Prototype) → 상태를 가지는 객체, 요청마다 새로운 객체가 필요한 경우 사용
- 싱글톤이 프로토타입 빈을 주입받을 때는 ObjectProvider를 사용하여 매번 새로운 객체를 생성해야 함