[Spring]스프링 기초 - DI(Dependency Injection)

Inung_92·2023년 2월 8일
0

Spring

목록 보기
5/15
post-thumbnail

의존성(Dependency)이란?

📖다른 객체의 영향을 받고, 다른 객체에 따라 결과가 좌우되는 것

의존성의 사전적 정의는 위와 같다.(여기서 '객체'는 내가 사전적 의미에서의 '변인'을 바꾼것이다.) 객체지향에서 의존성은 객체 간의 협력은 필수적이며 객체가 협력하기 위해서는 각 객체들이 서로에게 영향을 받고 그에 따라 결과가 달라지는 것을 의미한다.
우리가 매개변수 또는 리턴값 등으로 다른 객체에 영향을 미치거나 받는다. 이러한 것들이 의존성과 관련되어있다고 생각하면 편하다. 그렇다면 의존성은 높은게 좋은건지 낮은게 좋은건지 알아보자.

⚡️ 의존성의 정도

객체지향 프로그래밍에서 객체가 다른 객체로부터 영향을 적게 받는 것이 좋을까? 아니면 영향을 많이 받는 것이 좋을까? 여기서 이야기하는 영향은 협력하는 특정 객체가 삭제 또는 변경될 시에 다른 객체들에게 미치는 효과를 이야기한다. 잘 판단이 되지 않는다면 의존성의 특징을 먼저 알아보자.

🖥️ 의존성이 높을 경우

의존성이 높다는 것은 협력하는 객체들 사이에서 특정 객체가 변할 시 다른 객체들에게 변경이 전파되며, 해당 전파 범위가 넓을수록 유지보수의 범위 또한 늘어나게 된다.
유지보수의 범위가 늘어나면 투입되는 시간과 인력이 많아지므로 이러한 부분은 곳 비용으로 연결된다.
실생활의 예를 들어보자. 요리사가 요리를 할 경우 특정 도구만 사용하도록 강제되거나 식당에 도구가 1개 밖에 없다면 어떨까? 다양한 경우의 수를 계산할 수 있다 하더라도 도구를 잃어버리거나 도구가 망가지면 요리를 할 수 없을 것이다.
이러한 상황이 의존성이 높은 상황이다. 요리사가 사용할 도구를 사전에 지정해두어야지만 요리를 할 수 있는 상황이다.

위 그림에서 후라이팬과 요리사를 클래스라고 가정하고 상황을 생각해보자.
후라이팬은 요리사가 보유한(has a) 객체이다. 후라이팬이라는 객체가 변경이 생겨서 사용이 불가능하면 요리사 클래스가 보유한 후라이팬과 관련된 코드가 수정이 필요하게 되는 것이다. 이게 의존도가 높을 때의 단점이다.

이러한 단점을 보완하기 위하여 스프링에서는 DI(의존성 주입)개념을 사용한다.

🖥️ 의존성이 낮은 경우

의존성이 낮다는 것은 협력 관계에서 특정 객체가 변경이 생길 시 다른 객체들에 전파되는 변경에 대한 영향이 없거나 최소한으로 줄어드는 것을 의미한다. 그렇다면 위에서 본 그림을 의존성이 낮은 경우를 가정하여 다시 그려보겠다.

위 그림을 보자. 다양한 요리도구가 존재한다.
여기서 요리도구라는 개념은 interface에 비유할 수 있으며, 각각의 요리도구는 class라고 볼 수 있다.

요리사가 요리에 필요한 도구를 직접 골라서 사용할 수 있으며 또는 보조에게 이야기하여 도구를 전달받을 수도 있다. 이러한 상태가 바로 의존성이 낮은 상태이다. 어떤 하나의 요리도구가 사용이 불가능하더라도 다른 요리도구를 사용할 수 있으며, 변경에 대한 전파의 영향이 없거나 작은 상태이다.

⚡️ 의존성을 낮추는 방법

그렇다면 의존성을 어떻게 낮출 수 있는지에 대하여 알아보자.
기본적으로 의존성을 클래스 내부에서 new 연산자를 사용하는 것이 아닌 외부로부터 객체의 의존성을 주입받아야한다. 외부로부터 의존성을 주입받는 것의 의미는 '의존성 주입'에 초점을 맞추는게 아니라 '의존성 탈피를 위하여 외부로부터 주입'이라는 부가적인 설명에 초점을 맞추어야한다.

그렇다면 외부로부터 의존성을 주입받는 경우에 고려해야하는 부분을 알아보자.

  • DI를 구현하기 위해 객체가 보유할 자료형은 추상적일수록 유연해짐
  • 개발자가 클래스 내에서 직접 new 연산자로 정확한 자료형의 객체를 생성하지 말아야함

요리사와 요리도구를 다시 예로 들면 요리사가 손에 들어야하는 요리도구를 후라이팬이라고 지정하지 말라는 것이다. 다양한 요리도구를 사용할 수 있도록 그냥 요리도구라는 추상적인 개념을 지정한다. 이렇게되면 요리도구를 보관함에서 꺼내서 쓸 수 있는 것이다.

고려해야하는 부분들까지 알아보았으니 스프링에서는 과연 의존성 주입을 어떻게하고 있는지 알아보자.

아까부터 외부에서 의존성을 주입해야한다고 설명하고 있다. 스프링에서는 외부에서 의존성을 주입해주는 역할을 Bean 컨테이너라고 부르는 객체를 모아둔 상자가 수행한다.

📖최초에는 Bean이 GUI 컴포넌트였으나 EE로 넘어오면서 클래스(객체)를 가르키는 용어로 사용.

위 그림에서 보는 것과 같이 Bean 컨테이너는 프로그램이 실행(runtime)시 상황과 조건에 맞게 객체의 의존성을 주입해주는 역할을 한다. 이렇게하면 개발자가 직접 정확한 자료형을 명시하지 않더라도 매핑되어 있는 조건에 따라 상위 자료형이 포함하고 있는 하위 자료형의 인스턴스가 생성되어지는 것이다.

이제 개념은 어느정도 감이 잡혔을 것이라 생각된다. 예제 코드를 통해 다시 한번 DI(의존성 주입)에 대하여 알아보자.


의존성 주입 예제

예제의 시나리오는 위의 그림을 그대로 코드로 표현해보겠다.

🖥️ FryPan.java

public class FryPan{
	//음식을 익히는 기능의 메서드
	public void heat(){
    	System.out.println("후라이팬으로 음식을 익히는 중");
    }
}

요리사가 사용할 후라이팬 클래스를 정의하였다. 이제 요리사가 후라이팬을 사용하기 위해 코드를 작성해보자.

🖥️ Cook.java

public class Cook{
	//클래스 보유 compile Dependency(컴파일 의존성)
	FryPan fryPan = new FryPan();
    
    public static void main(String[] args){
    	//후라이팬의 메서드 실행
    	fryPan.heat();
    }
}

요리사가 후라이팬을 이용해 음식을 익히는 기능을 사용중인 것을 알 수 있다. 여기서 만약에 후라이팬의 메서드명이 바뀌거나 후라이팬이 고장나서 사용하지 못하게 된다고 가정하면?
당연히 요리사 클래스의 코드도 수정이 필요하다. 바로 이런 부분이 의존성이 높다고 이야기하는 부분이다. 여기서 의존성이 낮아지려면 어떻게 해야하는지 아래 코드를 보면서 이야기하자.

🖥️ 인터페이스 사용

//모든 요리도구의 최상위 객체 선언
public interface Pan{
	//추상메서드 보유 및 구현객체들에게 메서드 구현을 강제
	public void heat();
}

위와 같이 모든 요리도구들이 구현할 최상위 객체를 선언해준다. 여기서 인터페이스를 사용하는 이유는 클래스의 경우 다중상속이 불가능하다는 단점이 있으며, 구현 객체들에게 메서드 구현을 강제할 수 있다는 장점이 있다. 이제 FryPan이 Pan을 구현할 것이다. 추가로 요리도구를 하나 더 선언해보자.

🖥️ 요리도구들 선언

//요리도구 1
public class FryPan implements Pan{
	@Override
	public void heat(){
    	System.out.println("후라이팬으로 음식을 익히는 중");
    }
}

//요리도구 2
public class ElectPan implements Pan{
	@Override
	public void heat(){
    	System.out.println("전기팬으로 음식을 익히는 중");
    }
}

이제 모든 요리도구는 Pan이라는 최상위 객체의 구현 객체로서 임무를 성실히 수행할 것이다. 그렇다면 요리사 클래스의 코드에는 어떤 변화가 있는지 살펴보자.

🖥️ 요리사

public class Cook{
	//추상적인 자료형으로 변경
	Pan pan;
    
    public static void main(String[] args){
    	//팬의 메서드 실행(어떤 팬인지는 런타임 시 의존성이 주입된다.)
    	pan.heat();
    }
}

위와 같이 코드를 변경해주면 Pan의 어떤 구현객체의 인스턴스가 주입되는지는 런타임 시점에서 확인이 가능하다. 여기서 의문점은 그럼 도대체 누가 객체를 new 해주는 것인가? 정답은 Bean 컨테이너이다.

이 그림을 기억하는가? Bean 컨테이너에 등록된 Bean들 중 매핑된 Bean의 의존성이 주입되는 것이다. 그렇다면 매핑은 어떻게 설정하는지 아래 xml 코드를 보자.

🖥️ Bean 매핑

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
	                    http://www.springframework.org/schema/beans/spring-beans.xsd
	                    http://www.springframework.org/schema/context
	                    http://www.springframework.org/schema/context/spring-context.xsd
	                    http://www.springframework.org/schema/aop
	                    http://www.springframework.org/schema/aop/spring-aop.xsd">

	<!-- 프로젝트에서 사용할 모든 객체 명단 작성 -->
	<bean id="fryPan" class="gui.cook.FryPan"/>
	<bean id="electPan" class="gui.cook.ElectPan"/>
	<bean id="cook" class="gui.cook.Cook"/>
</beans>

이렇게 xml을 통해 각각의 bean에 대한 매핑 정보를 설정해준다. 이 방법만 사용하는 것은 아니다. 현재는 MVC 프레임워크를 따라만드는 중이다보니 이러한 형태로 작성이 되는 것이다. 그렇다면 스프링 레거시에서는 해당 태그가 어떻게 변경되었는지 한번 보자.

<context:component-scan base-package="com.edu.springmvc" />

이 한줄이면 자동으로 매핑을 한다. 다만 조건이 있다. @Annotation이 붙은 객체만을 매핑 대상으로 본다는 것이다. 이 점에 유의하자. 이렇게 xml로 매핑 정보를 설정해두면 프로그램이 가동되면서 필요한 객체(Bean)를 매핑 정보에서 찾아내 의존성을 주입해주는 것이다.

결국 스프링에서 DI는 의존성을 완전히 탈피한 개념이 아닌 의존성의 정도를 최소한으로 가져가며 Model의 재사용성을 보장하고, 런타임 의존성 주입을 통해 객체 간의 협력관계를 느슨하게 가져가는 것이다.

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

이제까지 DI(의존성 주입)에 대한 개념과 간단한 예제를 알아보았다. 마지막으로 컴파일 의존성과 런타임 의존성을 비교해보자.

구분컴파일 의존성런타임 의존성
정의컴파일 시점에 결정되는 의존성프로그램을 실행하는 시점에 결정되는 의존성
의존성을 갖는 요소클래스 사이의 의존성객체 사이의 의존성
특징결합도가 높으며 변경에 유연하지 못함결합도가 낮으며 변경에 유연함

클래스와 객체가 다소 헷갈릴 수 있다. 이 부분은 컴파일 단계에서는 클래스의 인스턴스가 생성된 것이 아니기 때문에 클래스 코드 간의 의존성이라고 보면되고, 객체는 클래스가 인스턴스화가 진행된 상태라고 판단하면 편할 것이다.


마무리

결론적으로 의존성이 높을 경우 프로그램을 개발할 때 부정적인 영향을 미치므로 안좋다고 판단할 수 있다. 개발자들은 개발을 할 때 의존성을 탈피하는 것이 가장 좋은 상황이지만 그럴 수 없다면 의존성을 최소화하는 것에 초점을 맞추고 개발을 해야한다.

느낀점

  • 스스로 알아서 일하는 프로그램을 만드는 것이 차후 유지보수도 편하며 코드를 재사용할 수 있는 기회가 많아진다는 것을 알게됨.
  • xml을 사용하여 객체에 대한 정보를 매핑하고, 그렇게 매핑 정보가 설정된 객체들을 bean이라고 부르며 스프링이 컨테이너에 보관하고 있다가 런타임시 필요한곳에 객체를 주입해준다는 원리에 대해 이해함.
  • 인터페이스를 통해 다중구현의 이점을 살리고, 보유한 메서드를 구현객체에 강제구현하게 만들어 추상화를 달성 할 수 있음을 느낌.

다음 게시글에서는 스프링의 핵심 중 두번째인 AOP(관점지향프로그래밍)에 대해서 다뤄보겠다.
그럼 이만.👊🏽

profile
서핑하는 개발자🏄🏽

0개의 댓글