👩🏻‍💻 Programming/SpringBoot

@PostConstruct과 @FeignClient의 이슈

한국의 메타몽 2025. 8. 21. 18:12

목차

  1. @PostConsttuct
  2. 관련 이슈
  3. 해결책 (1) : 어플리케이션 구동 성공 후 호출
  4. 해결책 (2) : try - catch로 예외 처리


1. @PostConstruct

스프링 공식 문서에서 제공하는 @PostConstruct와 관련된 설명은 다음과 같다.

The PostConstruct annotation is used on a method that needs to be executed after dependency injection is done to perform any initialization. This method must be invoked before the class is put into service. This annotation must be supported on all classes that support dependency injection. The method annotated with PostConstruct must be invoked even if the class does not request any resources to be injected. Only one method in a given class can be annotated with this annotation. The method on which the PostConstruct annotation is applied must fulfill all of the following criteria:

핵심만 요약하자면 다음과 같다.

DI(Dependency Injection) 이후 해당 어노테이션이 붙은 함수는 반드시 자동으로 실행됨

스프링이 실행되고 종료되기까지의 라이프 사이클은 아래와 같은데, 여기서 @PostConstruct초기화 콜백 단계에 해당된다.

스프링 컨테이너 생성 -> Bean 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸 콜백 -> 스프링 종료

즉, 초기화 콜백이 정상적으로 진행되어야 사용(= 어플리케이션 구동 및 HTTP 호출) 단계에 진입할 수 있다.



2. 관련 이슈

만약 @PostConstruct으로 초기화 콜백이 이루어질때, 외부 네트워크를 통한 초기화가 이루어지면 어떻게될까?
예시로 @FeignClient를 통한 MSA API를 호출한 결과값으로 데이터 초기화 이루어진다고 가정해보자.
편의상 하기 서비스 로직을 가진 프로젝트를 my-project라고 지칭하겠다.

@Slf4j
@Service
@RequiredArgsConstructor
public class WeatherCacheService {

    private final WeatherApiClient weatherApiClient;

    // 캐시용 변수
    private final AtomicReference<WeatherResponse> cache = new AtomicReference<>();

    @PostConstruct
    public void init() {
        log.info("초기화 시작: MSA API에서 오늘의 날씨 정보를 불러옵니다.");

        try {
            WeatherResponse today = weatherApiClient.getTodayWeather();
            cache.set(today);
            log.info("날씨 정보 초기화 완료: {} - {} ({}°C)",
                     today.getCity(), today.getCondition(), today.getTemperature());
        } catch (Exception e) {
            log.error("날씨 정보 초기화 실패: {}", e.getMessage(), e);
            // 예외를 던지면 → 애플리케이션 구동 실패
            throw e;
        }
    }

    public WeatherResponse getTodayWeather() {
        return cache.get();
    }
}
@FeignClient(name = "weatherApiClient", url = "${msa.weather-api.url}", configuration = WeatherApiClientConfig.class)
public interface WeatherApiClient {

    @GetMapping("/today")
    WeatherResponse getTodayWeather();
}

weather-api의 서버가 살아있다면 문제될 것은 없다.
그런데 만약 해당 서버가 죽어있는 상태에서 my-project 어플리케이션을 구동하면 어떻게 될까?

WeatherApiClientConfig에 정의된 retry 횟수만큼 getTodayWeather()의 호출이 이루어지지만, 끝끝내 실패할 경우 초기화 콜백은 실패하게되고, 이는 어플리케이션 구동 실패로 이끌게 된다.

만약 상기 언급된 cache의 값이 정상적으로 로딩되지 않을 경우 my-project는 구동되지 않아도 무방하다면 상관이 없다.
하지만 my-project에 cache와 관련이 없는 API들이 존재한다면, 동시에 해당 API들의 역할도 중요하다면 이는 문제점이 될 수 있다.

때문에 보통 @PostConstruct 과정 중 외부 네트워크를 호출하는 것은 권장되지 않지만, 현업에서는 상황이 여의치 못해 지켜지지 못할때도 있다.



3. 해결책 (1) : 어플리케이션 구동 성공 후 호출

사고의 방향을 아래와 같이 전환하면 해결될 수도 있다.

[AS-IS] 빈 주입 종료 후, 어플리케이션 구동 전에 캐시 초기화 -> [TO-BE] 어플리케이션 구동 성공 후 캐시 초기화

@EventListener(ApplicationReadyEvent.class)를 사용하면 어플리케이션 구동이 성공적으로 진행된 직후에 캐시 초기화를 할 수 있다.
그러나 어플리케이션 구동 성공 후에 MSA API를 호출해 캐시를 초기화 하는 과정 역시 실패했을 경우도 고려를 해야한다.
기본 값을 내려줘도 무방한지, 또는 해당 캐시 데이터를 호출 시 예외를 던져주는게 좋을지, 정답은 없으니 상황에 가장 적합한 방법을 선택하면 된다.



4. 해결책 (2) : try - catch로 예외 처리

보다 간결한 방법을 원한다면 하기와 같이 예외를 던지지 않으면 해결될 수 있다.

@Slf4j
@Service
@RequiredArgsConstructor
public class WeatherCacheService {

    private final WeatherApiClient weatherApiClient;

    // 캐시용 변수
    private final AtomicReference<WeatherResponse> cache = new AtomicReference<>();

    @PostConstruct
    public void init() {
        log.info("초기화 시작: MSA API에서 오늘의 날씨 정보를 불러옵니다.");

        try {
            WeatherResponse today = weatherApiClient.getTodayWeather();
            cache.set(today);
            log.info("날씨 정보 초기화 완료: {} - {} ({}°C)",
                     today.getCity(), today.getCondition(), today.getTemperature());
        } catch (Exception e) {
            log.error("날씨 정보 초기화 실패: {}", e.getMessage(), e);
            // 예외를 던지면 → 애플리케이션 구동 실패
            // throw e;
        }
    }

    public WeatherResponse getTodayWeather() {
        return cache.get();
    }
}

throw e에 주석처리를해서 @PostConstruct 실패와 무관하게 어플리케이션 구동에 성공하도록 로직을 개선했다.
다만 이럴 경우, 1안와 동일하게 MSA API 호출시 cache의 기본값을 무엇으로 던져줄지, 아니면 cache 데이터 호출시 예외를 던져줄지에 대한 후속 처리를 고민해봐야한다.