Spring 숙련 (Bean 생명주기)

KimGwangmin·2026년 9월 14일

Bean의 일생

컨테이너가 빈을 만들고, 의존성을 넣고, 초기화하고, 종료 시 정리하는 정해진 순서

  1. 생성
  2. 주입
  3. 초기화(@PostConstruct 실행)
  4. 사용
  5. 소멸(직전 @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는 거의 사용할 일이 없다. (자원 정리를 명시적으로 할 일이 많지 않음)

순환 참조 (Circular Dependency)

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;
}

Bean Scope

빈이 몇 개 만들어지고, 얼마나 오래 사는지를 정하는 값이다.

  • singleton(기본값): 컨테이너에 하나만 존재
    • @Service, @Repository, @RestController, @Component 모두 싱글턴이 기본
    • 거의 대부분의 상황에 사용
  • prototype: 사용할 때마다 새 인스턴스 생성 (재사용 안함)
  • request: HTTP 요청마다 새 인스턴스 생성 (같은 요청 내에서 재사용)
  • session: HTTP 세션마다 새 인스턴스 생성 (같은 새션 내에서 재사용)

0개의 댓글