ecsimsw

생성자 주입을 통한 순환 참조 막기 본문

생성자 주입을 통한 순환 참조 막기

JinHwan Kim 2021. 4. 19. 01:59

양방향 의존성을 피하라

양방향 의존성은 정말 안좋은 참조 패턴이다. '의존'이라는 말마따라 A가 변경될 때 B가 변경되어야하고 B가 변경되면서 다시 A가 영향을 받게된다.

 

 

'우아한테크세미나'에서 조영호님은 이를 '하나의 클래스로 봐야할 것을 억지로 찢은 형태'라고 표현하셨다. 양방향 의존성은 성능 이슈를 만들고, 양쪽 객체의 싱크를 맞추기 위한 노력이 필요하게 된다. 또 메소드 순환 호출을 야기한다.

 

이런 양방향 의존을 피하기 위한 패턴으로 다음 두 가지 방식이 자주 소개된다.

 

1. 인터페이스 사용으로 의존성 분리 

2. 중간 객체를 만들어 사이클을 끊는다.

 

이 두 가지 방식에 대한 예시는 다음에 정리하려고 하고, 이번 글에서는 스프링에서 생성자 주입을 이용하면 이런 양방향 의존을 피할 수 있음을 소개하고자 한다.

 

스프링 : 생성자 주입을 통한 양방향 참조 피하기

먼저 생성자 주입이 아닌 필드 주입이나 setter을 이용한 주입의 경우를 보자.

 

@Component
public class A {
    @Autowired
    private B b;
}

@Component
public class B {
    @Autowired
    private A a;
}

 

이렇게 A와 B가 양방향 의존, 순환 참조하고 있는 형태에서 필드로 서로가 주입된다면 정상 작동한다. 아래 테스트 코드로 A 객체와 B객체가 문제없이 생성됨을 확인하였다.

 

class AppTest {
    private ApplicationContext ctx;

    @BeforeEach
    void initApplicationContext(){
        ctx = new AnnotationConfigApplicationContext(App.class);
    }

    @DisplayName("A, B 빈이 모두 정상 생성되었는지 확인한다.")
    @Test
    void beanInstanceTest(){
        boolean isAExist = Arrays.stream(ctx.getBeanDefinitionNames())
                .anyMatch(name -> name.equals("a"));

        boolean isBExist = Arrays.stream(ctx.getBeanDefinitionNames())
                .anyMatch(name -> name.equals("b"));

        assertThat(isAExist && isBExist).isTrue();
    }
}

 

이번에는 생성자 주입을 이용했다. 이번에도 정상 동작할까?

 

@Component
public class A {
    private B b;

    public A(B b){
        this.b = b;
    }
}

@Component
public class B {
    private final A a;

    public B(A a){
        this.a = a;
    }
}

 

조금 생각해보면 알 수 있다. A가 생성되기 위해선 B 생성이 필요하고, B가 생성되기 위해선 A의 생성이 필요하여 생성자가 순환 호출되면서  빈이 정상 생성되지 않는다. (StackOverFlowError가 발생한다.)

 

반면 앞선 Setter와 필드로 주입하는 경우, A, B 빈이 생성될 때는 각자의 영향을 받지 않는다. 주입이 되는 시점은 빈으로 만들어진 이후이고 따라서 그 방식들로는 각각의 관계가 양방향 참조인지, 아닌지 확인할 수 없다. 또 순환 참조에 의한 부작용, 이를 테면 메소드 순환 호출과 같은 심각한 버그를 런타임 시점에서야 알 수 있게 된다.

 

이렇게 생성자를 통한 주입을 사용하면 의존하는 관계가 양방향 관계인지 바로 확인할 수 있다.

 

 

Comments