본문 바로가기
Language/자바&코틀린

컴파일 타임 의존성과 런타임 의존성

by 코딩공장공장장 2024. 2. 11.

객체지향 프로그래밍에서는 객체간의 의존성이 빈번하게 발생한다.

 

의존이란 나(객체)의 변경이 나를 사용하는 다른 객체에 영향을 미치는 것을 말한다.

 

이러한 의존관계의 설정은 컴파일 타임에 결정되기도 하고 런타임에 결정되기도 한다.

 

컴파일 타임 의존성과 런타임 의존성은 말그대로 어느 시점에 의존성이 결정되느냐의 차이이다.

 

이 시점의 차이를 구분하기 어렵다면 일단은 아래와 같이 이해해보자.

 

컴파일 타임 의존성 : 컴파일러 관할 영역 -> 순수 프로그래밍 코드를 통해 의존관계 확인 가능 -> 의존성 변경시 코드 변경 필요

런타임 의존성 : 컴파일러 관할 영역 밖 -> xml, 어노테이션을 통해 의존관계 확인 가능 -> 의존성 변경시 코드 변경 필요X

 

컴파일 타임과 런타임 의존성의 구분

컴파일러는 자바 코드를 바이너리 코드의 클래스 파일로 변환한다.

(JVM환경을 기준으로 설명하겠다.)

 

컴파일러가 관할하는 영역은 순수 프로그래밍 코드이다.

 

config설정을 하는 xml파일은 컴파일러의 영역이 아니다.

 

어노테이션 처리는 컴파일러가 하지만 자바에서 기본적으로 제공해주는것이 아니라 프레임워크에 의해 런타임 환경에서 해석되어 처리되는 경우에도 컴파일러의 영역이라고 할 수 없다.

 

따라서 컴파일 타임 의존성은 컴파일러가 자바코드를 클래스 파일로 변환하는 과정에서 의존관계를 결정할 수 있는 경우이다.

 

즉, 프로그래밍 코드에 의존관계가 명확하게 나타나있다.

 

class Knight(
        val sword: Excalibur
) {
    fun skill(){
        sword.attack()
    }
}

class Excalibur {
    fun attack() {
        println("엑스칼리버")
    }
}

 

위의 Knight클래스를 보면 Excalibur라는 클래스를 의존하고 있다.

 

명확하게 소스 코드에 나타난다. 컴파일러가 컴파일 대상인 클래스 파일만 보더라도 의존관계를 파악할 수 있다.

class Knight(
        val sword: Sword
) {
    fun skill(){
        sword.attack()
    }
}

interface Sword {
    fun attack()
}

class Excalibur: Sword {
    override fun attack() {
        println("엑스칼리버")
    }
}

class Gungnir: Sword {
    override fun attack() {
        println("궁니르")
    }
}

 

위 예제에서 Knight는 Sword라는 인터페이스에 의존한다.

 

인터페이스는 인스턴스화하지 못하니 당연히 Sword의 구현체에 의존을 할테데 무엇에 의존하는지 알 수 있나??

 

알 수 없다.

<!-- applicationContext.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd">
  
    <!-- Excalibur 빈 등록 -->
    <bean id="excalibur" class="com.example.demo.di.Excalibur" />
  
    <!-- Knight 빈 등록 -->
    <bean id="knight" class="com.example.demo.di.Knight">
        <constructor-arg ref="excalibur" />
    </bean>
  
</beans>

 

프로젝트를 구동하면 스프링 IOC컨테이너 의해 위와 같은 설정파일을 참조하여 구현체가 결정된다.

(@Priamary나 @Qualifier와 같은 어노테이션을 통해 구현체를 결정하더라도 마찬가지이다.)

 

xml 설정 파일은 컴파일러의 영역 밖이고 스프링 어노테이션 또한 스프링 프로젝트가 실행됬을 때 스프링에서 제공하는 객체들에 의해 조작되는 것이지 순수 자바 어노테이션이 아니기에 컴파일러는 의존관계를 알 수 없다.

 

이해를 돕기 위해 컴파일 타임과 런타임 의존성 결정을 위와 같이 컴파일러의 관할 범위에 있냐 없냐로 표현하였다.

 

물론 맞는 말이지만 코드를 통해 직접 확인해야 정확한걸 알 수 있다. 

class KnightTest{
    @Test
    fun 테스트(){
        val knight = Knight(Excalibur())
        knight.skill()
    }
}

 

이전의 예제에서 인터페이스와 XML 설정을 통해 DI 관계를 설정하였더라도 위와 같이 명시적으로 구현체를 생성자를 통해 주입시켜주면 위 코드는 컴파일 타임 의존성이 된다.

 

따라서 컴파일 타임 의존성과 런타임 의존성을 구분은 코드를 통해 직접적으로 의존하고 있는지 아닌지에 대해 체크이고,

 

구분하는 것보다 중요한 것은 의존관계를 런타임에 설정되도록 할 것인지, 컴파일 타임에 설정되도록 할 것인지 설계하는 것이다.

 

컴파일 타임 의존성은 주로 구체 클래스간의 의존관계 설정을 통해 이루어지고

 

런타임 의존성은 추상화된 클래스나 인터페이를 통해 이루어진다.

 

컴파일 타임과 런타임 의존성의 차이

컴파일 시점에 의존성이 결정되는 것과 런타임 시점에 의존성이 결정되는 것에서 어떠한 차이가 있을까?

 

의존성 시점을 구분하는 방법으로 프로그래밍 코드에서 의존성을 명확하게 파악할 수 있냐 없냐를 얘기했는데

 

이 부분으로 인해 두 시점의 의존성 장단점이 명확하게 드러난다.

 

위의 두 케이스에서 의존객체를 변경해야하는 경우 어떻게 될까??

 

컴파일 타임 의존성

class Knight(
        val sword: Gungnir
) {
    fun skill(){
        sword.attack()
    }
}

 

Knight 클래스의 의존성을 기존 Excalibur에서 Gungnir로 바꾸어 보겠다.

 

Knight 클래스의 생성자에 Excalibur가 Gungnir로 직접적인 소스 코드의 변경이 일어났다.

 

 

런타임 의존성

    <bean id="knight" class="com.example.demo.di.Knight">
        <constructor-arg ref="gungnir" />
    </bean>

 

런타임 의존성의 경우 xml 파일이나 Gungnir 클래스 위에 @Primary와 같은 어노테이션을 붙이는 방식으로 의존성을 변경시킬 수 있다.

 

 

컴파일 타임 의존성은 클래스 간에 직접적으로 의존관계를 설정하기에 의존받는 쪽에서도 코드의 수정이 필요하다.

 

런타임 의존성은 인터페이스를 통해 의존하고 코드를 통해 의존관계를 설정하지 않기에 코드의 변경이 필요없다.

 

컴파일 타임 의존성과 런타임 의존성은 의존객체 변경이 프로그래밍 코드에 변경을 일으키는지 아닌지에 차이가 있다.

 

이는 결국, 변경에 유연한 구조를 갖추고 있느냐 없느냐로 볼 수 있고, 의존관계가 복잡하고 많아질 수록 유연성의 차이는 더욱 커질 것이다.

 

물론 컴파일 타임 의존성이 단점만 갖고 있는 것은 아니다.

 

의존관계 설정이 잘못 되었을 경우 컴파일 시점에 알아차릴 수 있다는 점도 있고 컴파일 타임에 의존관계가 결정되어있으니 성능에 좋은 점도 있다.

 

객체간의 관계가 어떠한지 지속적인 변경이 일어날 수 있는지를 파악하여 의존성을 컴파일 타임에 결정할지 런타임에 결정할지가 필요하다.

반응형