컨테이너가 빈을 만들고, 의존성을 넣고, 초기화하고, 종료 시 정리하는 정해진 순서
@PostConstruct 실행)@PreDestroy 실행)생성자 주입 방식에선 생성과 주입이 동시에 일어난다. 이전에 컨트롤러와 서비스 구현 시 자주 사용한, @RequiredArgsConstructor와 함께 final로 다른 의존성을 선언하는 패턴이 이에 해당한다.
@PostConstruct: 초기화빈 생성과 의존성 주입 완료 직후 실행할 로직을 정의한다.
@Service
@RequiredArgsConstructor
public class BadWordFilter {
// 1. board.post.* 설정을 담은 빈 (1-6)
private final PostProperties properties;
private Set<String> words;
@PostConstruct
void prepare() {
// 2. 주입이 모두 끝난 뒤에 불립니다. 설정 문자열을 여기서 한 번만 다듬어 둡니다.
words = properties.getBadWords().stream()
.map(w -> w.strip().toLowerCase(Locale.ROOT))
.filter(w -> !w.isBlank())
.collect(Collectors.toUnmodifiableSet());
// 3. 잘못된 설정은 요청을 받기 전에 기동을 실패시킵니다.
if (words.isEmpty()) {
throw new IllegalStateException("board.post.bad-words 설정이 비어 있습니다.");
}
}
public boolean contains(String content) {
String lower = content.toLowerCase(Locale.ROOT);
return words.stream().anyMatch(lower::contains);
}
}
초기화를 생성자에서 하면 안되나?
스프링 컨테이너는 빈의 의존관계를 그래프로 그려 파악한 뒤, 필요한 순서에 맞춰 차례대로 빈을 완성한다.
A가B에 의존한다면, 컨테이너는B의 생성과 초기화까지 마친 뒤A에 의존성을 주입하고 초기화 로직을 실행한다. 따라서 논리적으로는 생성자에서 초기화까지 해도 동작은 한다.
그럼에도 초기화를@PostConstruct에 두도록 권장하는 이유는 다음과 같다. 안전하게 로직을 수행하기 위한 방어적 설계로 이해하면 된다.
1. 책임 분리: 생성자는 필드를 할당하고 메모리에 인스턴스를 생성하는 역할까지만 맡도록 하고, 비즈니스 런타임 준비 작업은 초기화 단계에서 진행하도록 하여 역할을 명확히 분리
2. 프록시:@Transactional과 같은 기능들은 원본 객체를 감싸는 대리자 껍데기(프록시)를 통해 동작한다. 생성자 시점에는 프록시가 적용되기 전 원본 객체가 노출되어 트랜잭션 등의 부가 기능이 동작하지 않을 수 있다. 프록시 관련해서는 추후 AOP를 통해 다룬다.
@PreDestroy: 소멸빈 제거 직전 자원 정리 로직에 사용된다.
@Service
public class AsyncWorker {
private final ExecutorService executor = Executors.newFixedThreadPool(4);
@PreDestroy
public void shutdown() {
executor.shutdown();
}
}
실제로는
@PostConstruct,@PreDestroy모두 쓸 일이 많지 않다. 특히@PreDestroy는 거의 사용할 일이 없다. (자원 정리를 명시적으로 할 일이 많지 않음)
A가 B를 참조하고, B가 A를 참조해서 어느 쪽도 먼저 만들 수 없는 상태
Bean 간에 순환 참조가 발생하면 에러가 발생한다.
대표적인 해결 방법은 공통 로직을 제 3의 클래스로 빼는 것이다.
@Lazy를 쓰는 등의 방식은 정상적인 해결 방법이 아니다.
순환 참조 예시
@Service
@RequiredArgsConstructor
public class MemberService {
private final PostService postService;
}
@Service
@RequiredArgsConstructor
public class PostService {
private final MemberService memberService;
}
해결
// 둘 다 필요로 하던 코드를 제3의 클래스로 꺼냅니다.
@Service
@RequiredArgsConstructor
public class MemberReader {
private final MemberRepository memberRepository;
// ...
}
// MemberService와 PostService가 모두 필요로 하던 로직을 MemberReader로 빼고, DI
@Service
@RequiredArgsConstructor
public class PostService {
private final MemberReader memberReader;
}
// MemberService와 PostService가 모두 필요로 하던 로직을 MemberReader로 빼고, DI
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberReader memberReader;
}
빈이 몇 개 만들어지고, 얼마나 오래 사는지를 정하는 값이다.
singleton(기본값): 컨테이너에 하나만 존재@Service, @Repository, @RestController, @Component 모두 싱글턴이 기본prototype: 사용할 때마다 새 인스턴스 생성 (재사용 안함)request: HTTP 요청마다 새 인스턴스 생성 (같은 요청 내에서 재사용)session: HTTP 세션마다 새 인스턴스 생성 (같은 새션 내에서 재사용)