의존성주입 (2)

스프링 순환참조 문제

스프링으로 프로젝트를 진행하다보면 여러 빈들의 순환참조로 인해 앱 기동이 안되는 상황에 종종 맞딱드리게 됩니다. 최근에 젠킨스로 빌드한 앱이 순환참조 문제로 인해 정상적으로 기동이 되지 않는 문제가 발생했습니다. 그런데 웃긴건 로컬환경에서는 아무런 문제없이 잘 돌아간다는 것이었죠. 결국 담당자의 확인하에 젠킨스의 파이프라인 옵션 설정으로 해결은 했습니다만, setter injection (field injection) 이라도 순환참조가 발생하는 디자인은 좋지 않죠. 그래서 관련내용을 알아보다가 끄적여봅니다.

순환참조란? (What is Circular Reference ? )

순환참조란 서로 다른 여러 빈들이 서로 물고늘어져서 계속 연결되어 있음을 의미합니다.

즉, 아래처럼 A는 B에서 필요한데 B는 또 A에서 필요한 상태를 말합니다.

Bean A → Bean B → Bean A

 

만약 Bean A -> Bean B -> Bean C 처럼 연결되어있다면 스프링은 A를 먼저 만들고 A를 필요로 하는 B를 만들고 B를 필요로 하는 C를 만들게 됩니다. 하지만 순환참조가 발생하면 스프링은 어느 빈을 먼저 생성해야할지 결정하지 못하게되고 순환참조 오류가 발생하게 됩니다. 이렇게 순환참조가 발생한다는건 결국 설계가 잘못되었다는 것입니다. 하지만 그렇다고 설계를 다 뜯어고치자니 비용이 너무 많이 들어갈 수도 있죠. 순환참조 오류는 참고로 스프링의 의존성 주입방법 중에서도 특히 생성자 주입방법을 사용했을 때 발생합니다. 빈 생성시 필요한 다른 빈이 서로 물고늘어져 있으니 어떤 빈도 생성이 불가능한 상황이 되어버리는 것이죠.

순환참조 문제 해결방법

순환참조는 발생하지 않도록 해주는게 제일 좋긴 하지만 어쩔 수 없는 상황은 꼭 생기기 마련입니다. 그럼 어떻게 순환참조 문제를 해결할 수 있을까요? 가장 우선시 해야할 것은 순환참조의 고리를 끊어버리는 것입니다. 이것이 스프링에서 권장하는 방법이며 설계를 조금만 바꿔서 해결이 가능한 경우가 이에 해당합니다. 하지만 만약 설계의 변경이 힘든 경우라면 @Lazy 어노테이션을 사용 해볼 수 있습니다. (해볼 수 있다 라고 얘기한 이유는 아래에 나옵니다)

@Component
public class BeanA {

    private BeanB beanB;

    @Autowired
    public BeanA(BeanB beanB) {
        this.beanB = beanB;
    }
}

 

이렇게 되어있던 코드를 아래처럼 바꿔주는 것이죠.

@Component
public class BeanA {

    private BeanB beanB;

    @Autowired
    public BeanA(@Lazy BeanB beanB) {
        this.beanB = beanB;
    }
}

 

하지만 이런 접근 방식은 문제가 있습니다. Lazy initialization 에 대한 문제는 Spring 공식문서(Lazy Initialization)를 참고하시면 되겠습니다. 간단히 말씀드리면 앱 기동시점이 아닌 실제 해당 빈이 필요한 시점에 빈을 생성하기 때문에 특정 http 요청을 받았을 때 힙메모리가 증가할 수 있으며 메모리가 충분하지 않은 상황이었다면 장애로 이어질 수 있다는 얘기죠. 아무튼 스프링에서 권장하지 않는 방식이니 사용하지 않으시면 됩니다. 

 

또 다른 방법은 생성자 주입방법 대신에 setter 주입방법을 사용하시면 됩니다.

아래 setter 주입 예제를 한번 보시죠.

@Component
public class BeanA {

    private BeanB beanB;

    @Autowired
    public void setBeanB(BeanB beanB) {
        this.beanB = beanB;
    }

    public BeanB getBeanB() {
        return beanB;
    }
}

@Component
public class BeanB {

    private BeanA beanA;

    @Autowired
    public void setBeanA(BeanA beanA) {
        this.beanA = beanA;
    }
}

 

 

혹시나 주입방법에 다시 한번 짚고 넘어가고 싶으시다면 스프링 의존성 주입 포스팅을 참고하시기 바랍니다.

 

Conclusion

스프링으로 개발을 하다가 순환참조가 발생한다면 가장 좋은 해결책은 순환참조의 연결고리를 끊어버리는 것이며 그렇게 하는 것이 깔끔하게 설계된 디자인이라고 할 수 있습니다. 스프링에서도 권장하는 방식이죠. 그래서 setter주입이 아닌 생성자 주입방식을 사용하는 것이 권장됩니다. 우회적으로 순환참조를 잠시 피한다 하더라도 언제 또 똑같은 문제가 발생할지 아무도 알 수 없으며 메모리 부족(lazy initialization의 경우) 현상이 발생할 수도 있기 때문에 가급적 순환참조해야하는 설계는 피하도록 하는 것이 좋겠습니다.

 

 

 

 

Reference

- www.baeldung.com/circular-dependencies-in-spring 

- docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-dependencies

💻 Programming

스프링 의존성 주입(DI)

스프링 의존성 주입 (Spring Dependency Injection)

스프링에서 의존성을 주입하는 방법은 현재 세 가지가 있습니다.

첫 번째는 생성자 주입 방식이고 두 번째는 Setter 주입 방식, 그리고 마지막으로 Field 주입 방식이 있습니다.

순서대로 한번 보도록 하겠습니다.

 

1. 생성자 주입 (Constructor Based Dependency Injection)

말 그대로 생성자에 @Autowired로 다른 빈을 주입하는 방식입니다.

@Controller
public class MyController {
    private MyService myService;
 
    @Autowired
    public MyController(MyService myService) {
        this.myService = myService;
    }
}

주입해야하는 빈이 많을 경우 생성자 자체가 커지게 됩니다. 그런경우 만약 lombok 을 사용한다면 아래처럼 작성할 수 있습니다.

@Controller
@RequiredArgsConstructor
public class MyController {

    private final MyService myService;
 
}

@RequiredArgsConstructor 어노테이션을 class 레벨에 붙여주고 주입할 빈은 final 키워드를 붙여주면 lombok에서 자동으로 생성자 주입을 이용하여 빈들을 주입해줍니다. 필요한 빈들은 모두 final 키워드를 붙여서 필드로 추가만 해주면 되죠. 

2. Setter 주입 (Setter Based Dependency Injection)

Setter 메서드를 이용하여 주입하는 방식이죠. 

@Controller
public class MyController {

    private MyService myService;
    
    @Autowired
    public void setMyService(MyService myService) {
        this.myService = myService;
    }
}

특이한 경우를 제외하고는 이 방식은 거의 사용하는 것을 못봤네요

3. Field 주입 (사용하지 마세요)

예전부터 많이 사용되는 방식입니다. 하지만 스프링에서 권고하지 않는 방식이기도 하죠. 

@Controller
public class MyController {

    @Autowired
    private MyService myService;
}

필드 주입 방식은 공식문서에도 소개조차 하고있지 않습니다. 공식문서에는 두 가지 방식만 있다고 설명합니다. 생성자 주입 방식과 setter 주입방식이죠. 그 이유는 필드 주입을 사용하게되면 순환참조 문제를 우회할 수 있게되어 순환참조를 고려하지 않고 개발을 하게되고, 그렇게 설계된 클래스는 하나 이상의 기능을 하게되어 하나의 책임만 가져야 한다는 Single Responsibility 원칙에 위배될 가능성이 높아지게되죠. 또 다른 여러 단점들에 대해서 stackoverflow를 참고하시면 좋을 것 같습니다. 만약 필드 주입을 사용하고 계시다면 생성자 주입 방식으로 바꿔주세요. 그러면 순환참조를 찾아낼 수 있고 순환참조 고리 안에 있는 클래스들을 살펴보면 Single Responsibility 원칙을 위배하는 클래스가 보일겁니다. 그런 것들은 클래스 분리를 통해 개선해주시면 됩니다.

 

 

 

 

 

참고문서

docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-dependencies

stackoverflow.com/questions/39890849/what-exactly-is-field-injection-and-how-to-avoid-it