본문 바로가기
Study

[스터디] 스프링 삼각형과 설정 정보 1)

by DuncanKim 2022. 8. 17.
728x90

[스터디] 스프링 삼각형과 설정 정보 

~ 1)편. IoC/DI - 제어의 역전 / 의존성 주입

 

 

객체 지향의 기본 원리를 4주에 걸쳐 알아보았다. 이제 실제로 그것이 쓰이는 것을 직접 눈으로 확인할 차례이다. 그렇지만, 그 전에도 스프링이 어떤 특징을 가지고 있는지, 어떤 개념이 토대가 되어 스프링이 작동하는 것인지는 기본적으로 알아야 할 필요가 있다. 어떻게 돌아가는지도 모르면서 원숭이 마냥 코드를 부르는 대로 자판에 찍는 것이 아니라면, 내가 어떤 일을 하고 있는지, 무엇을 해야 하는지, 도구의 특징 중 어떤 것을 활용해서 최대의 효율을 낼 수 있는지 등을 알아야 하지 않을까?

 

그중에 첫 번째로 중요한 것이 '의존성 주입'이다. DI라고도 한다. 지금 읽고 있는 챕터에서 소개하고 있는 것을 크게 나누어 보면, 의존성의 정의, 스프링 없이 의존성을 주입하는 것, 스프링을 활용해서 의존성을 주입하는 것 세 가지로 나눌 수 있다. 아래에서는 각각의 정의와 방법, 실제 활용에 대해 알아볼 것이다.

 

 

1. IoC/DI란?

 

후술 할 '의존성을 주입'한다는 것을 이해하기 위해서는 IoC와 DI가 무엇을 의미하는지 정확히 알고 갈 필요가 있다. 생소한 개념이고 뭘 주입한다는 거고 뭐에 의존한다는 것인지... 새로운 것을 보면 늘 경계하게 되어 있다. 그래서 어렵지만 차근차근히 나는 뜯어보고 씹어보려고 한다.

 

 

0) 의존이란?

 

의존이라는 개념은 SOLID에서도 다룬 적이 있다. DIP, 의존 역전의 원칙인데, 거기서 이야기했던 것을 가져오면 이렇다.


<DIP - 의존 역전 원칙>

"고차원 모듈은 저차원 모듈에 의존하면 안 된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야 한다."
"추상화된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야 한다."
"자주 변경되는 구체(Concrete) 클래스에 의존하지 마라." (로버트 C. 마틴)

 

먼저 위의 말을 의역하면 다음과 같다. "자신보다 변하기 쉬운 것에 의존하지 마라."

 

의존 역전 원칙은 개방 폐쇄 원칙과 비슷하다. 개방 폐쇄 원칙에서 든 삼성 인터페이스 만능 스마트폰은 변하기 쉽지 않다. 그렇지만, 엄마가 각각의 LG 단종된 핸드폰, 삼성 갤럭시, 애플 아이폰과 직접 연결이 되어 있다면, 어려움을 면치 못한다.

 

엄마가 변하기 보다는 핸드폰이 교체되기 쉽다. 그렇기 때문에 엄마는 핸드폰에 의존하면 안 된다. 그렇기 때문에 삼성 인터페이스 만능 스마트폰과 같이 불변하는 인터페이스나 상위 클래스를 두어서 변하기 쉬운 클래스에 영향을 덜 받도록 설계를 해야 한다는 원칙이라고 생각하면 된다.

 

상위 클래스일수록, 인터페이스일수록, 추상 클래스일수록 변하지 않을 가능성이 높다. 하위 클래스나 구체 클래스가 아닌 상위 클래스, 인터페이스, 추상 클래스를 통해 의존하라는 것이 의존 역전의 원칙인 것이다.


그렇다. A가 B에 의존한다는 것은, B가 변하면 A가 영향을 받게 된다는 것과 같은 말이다. B의 기능이 추가 또는 변경되거나 형식이 바뀌면 그 영향이 A에게 미치게 된다. 위의 엄마와 핸드폰의 사례를 생각해보면 된다. 엄마가 핸드폰에 의존하게 되면, 엄마는 핸드폰이 바뀔 때마다 사용방법(로직)을 익히거나 바꾸어야 한다. 저번에는 삼성 인터페이스 만능 스마트폰이라고 예시를 들었는데, DI를 설명할 때도 이 개념이 활용된다. 이 개념을 가지고, IoC와 DI를 알아보자.

 

 

1) Inversion of Control(IoC)

 

제어의 역전이라고 번역한다. 어떤 사람이 취준을 하는데, 서류를 넣고 면접까지 보았다고 하자. 회사가 연락할 때까지 그 취준생은 합, 불 여부를 모른다. 회사가 필요할 때 합, 불 여부를 알려주는 것이지 취준생이 필요할 때 합, 불 여부를 알려주는 것이 아니기 때문이다. 이것이 제어의 역전 비유라고 할 수 있겠다. 

 

일반적인 프로그램으로 생각해보면, 객체의 생명 주기를 클라이언트 구현 객체가 직접 관리한다. 다른 사람이 작성한 라이브러리를 호출해도 우리가 직접 호출 시점, 소멸 시점을 설정하여 관리할 수 있다. 그런데, 스프링부트와 같은 프레임워크를 쓰면 조금 상황이 달라진다. Controller, Repository 같은 것의 동작을 우리가 구현은 하지만, 어떤 시점에 호출될지는 신경 쓰지 않는다. 스프링부트님이 요구하시는 대로 객체를 생성하면 스프링부트님이 그 객체를 가져다가 생성하고 메서드를 호출하고 소멸시키는 것이다. 우리가 통제권을 '빼앗긴' 것이다. 이것이 제어의 역전이다.

 

이 개념을 도입하면 어떤 것을 이점으로 얻을 수 있을까?

 

프로그램의 진행 흐름과 구체적인 구현을 분리시킬 수 있을 것이다. 또한 개발자는 비즈니스 로직을 구현하는 것에만 집중을 할 수 있다. 그리고 객체 간의 의존성이 낮아지고 구현체 변경이 용이할 것이다.

 

 

2) Dependency Injection(DI)

 

의존관계 주입이라고 번역한다. IoC와 헷갈리거나 동일시하는 부분도 있는데, DI가 IoC에 포함이 되는 개념이라고 할 수는 있어도 같다고 할 수는 없다. IoC는 프로그램 제어권을 역전시키는 개념이고, DI는 그 개념을 구현하기 위해 사용하는 디자인 패턴 중 하나이다. 전에 언급하였던 '전략 패턴'과 비슷하다.

 

그렇다면, DI, 의존관계 주입이란 무엇일까?

 

위의 DIP 사례를 가지고 와서 이야기를 해보면 좋겠다. 엄마는 만능 스마트폰이라는 인터페이스와 내부적인 의존관계를 가지고 있다. 그 만능 스마트폰에 삼성 스마트폰을 연결시킬 지, 애플 아이폰을 연결시킬지는 엄마가 결정하는 것이다. 그런데 의존관계 주입, DI를 하면 제3자인 내가 만능 스마트폰에 무엇을 연결시킬지를 결정하고 주입하는 것이다. 약간 엄마한테 잘못을 하는 것 같지만, 장점도 있다. 아래의 장점을 살펴보자.

 

* DI의 장점

1) 의존성이 줄어든다.
: 의존한다는 것은 의존 대상의 변화에 취약하다는 것이다. 대상(스마트폰)이 바뀌었을 때, 이에 맞게 엄마를 수정해야 하는 것이다. DI로 구현했을 때, 주입하는 대상이 변해도 엄마에 대한 구현 자체를 수정할 일이 없거나 줄어들게 된다.

2) 재사용성이 높은 코드가 된다.
: 위의 사례를 또 예로 들자면, 엄마한테만 적용할 수 있었던 만능 스마트폰을 아빠한테도 적용할 수가 있다.

3) 테스트하기 좋은 코드가 된다.
: 또 위의 사례를 예로 들자면, 만능 스마트폰 테스트를 엄마의 핸드폰 활용능력 테스트와 분리하여 진행할 수 있다.

4) 코드의 가독성이 높아진다.
: 인터페이스와 클래스의 기능들을 별도로 분리하여 가독성이 높아진다.

 

 

2. 스프링 없이 의존성을 주입하는 것

 

위에서는 개념을 조금 정리해보았다. 그렇다면, 의존성을 주입하는 것은 어떻게 할 수 있을까. 사례를 나누어서 살펴본다.

 

 

1) 생성자를 활용한 의존성 주입

 

외부에서 생성된 객체를 내부 인자로 주입하는 형태이다. Car 객체를 완성시키는데, Driver에서 Tire 객체를 생성하여 직접 생성자의 인자로 제공하고 있다. 이런 방식으로 외부에서 주입 대상 클래스(a)에 필요한 다른 객체(b)를 만들고 그 클래스의 객체(a)에 다른 객체(b)를 주입하여 원하는 객체(a)를 Driver 클래스에서 사용하는 것이다.

 

interface Tire {
	String getBrand();
}

class KoreaTire implements Tire {
	public String getBrand() {
		return "코리아 타이어";
	}
}

class AmericaTire implements Tire {
	public String getBrand() {
		return "미국 타이어";
	}
}

class Car {
	Tire tire;

	public Car(Tire tire) {
		this.tire = tire;
	}

	public String getTireBrand() {
		return "장착된 타이어: " + tire.getBrand();
	}
}

public class Driver {
	public static void main(String[] args) {
		Tire tire = new KoreaTire();
		//Tire tire = new AmericaTire();
		Car car = new Car(tire);

		System.out.println(car.getTireBrand());
	}
}

 

 

2) 속성을 활용한 의존성 주입

 

setter를 활용하여 객체를 주입하는 것이다. 이 방법은 수시로 변경이 일어날 때, setter 메서드를 활용해서 그 객체 값을 바꿀 수 있다는 점이 특징이다. 위의 방식과 크게 다르지는 않다. 단지, Car 클래스에서는 생성자 대신 getter, setter가 생성되었고, Driver 클래스에서는 setTire라는 setter 메서드로 객체를 주입하고 있다는 점이 다르다.

 

interface Tire {
	String getBrand();
}

class KoreaTire implements Tire {
	public String getBrand() {
		return "코리아 타이어";
	}
}

class AmericaTire implements Tire {
	public String getBrand() {
		return "미국 타이어";
	}
}

class Car {
	Tire tire;

	public Tire getTire() {
		return tire;
	}

	public void setTire(Tire tire) {
		this.tire = tire;
	}

	public String getTireBrand() {
		return "장착된 타이어: " + tire.getBrand();
	}
}

public class Driver {
	public static void main(String[] args) {
		Tire tire = new KoreaTire();
		Car car = new Car();
		car.setTire(tire);

		System.out.println(car.getTireBrand());
	}
}

 

++

두 가지 방법을 살펴 보았는데, 두 가지 방법 중 어느 방법이 더 좋은 것인가라는 의견이 많았다고 한다. 최근에는 생성자를 활용해 의존성을 주입하는 방식을 더 선호하는 사람들이 많다고 한다. 그 이유는 어떤 프로그램에서 한 번 주입된 의존성을 계속 사용하는 경우가 더 일반적이기 때문에 setter 메서드로 객체를 바꿔 끼울 일이 별로 없기 때문이다.

 

 

3. 스프링으로 의존성을 주입하는 것

 

스프링도 생성자를 활용한 주입이 있지만, 아래에서는 속성을 활용한 주입만을 살펴볼 것이다.

 

 

1) XML 파일 사용

 

스프링을 사용하면 XML 파일을 만져야 한다. 여기서 파일을 사용한다는 것은, spring framework를 사용만 한다면 어떻게 되는지를 이야기 하는 것이다. 아래의 코드를 보면 알 수 있지만, 대부분 달라진 것이 없다. 다만, Driver 클래스에서 import 한 부분이 수정되었고 코드 하나 정도가 더 추가된 정도이다.

 

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;

interface Tire {
	String getBrand();
}

class KoreaTire implements Tire {
	public String getBrand() {
		return "코리아 타이어";
	}
}

class AmericaTire implements Tire {
	public String getBrand() {
		return "미국 타이어";
	}
}

class Car {
	Tire tire;

	public Tire getTire() {
		return tire;
	}

	public void setTire(Tire tire) {
		this.tire = tire;
	}

	public String getTireBrand() {
		return "장착된 타이어: " + tire.getBrand();
	}
}

public class Driver {
	public static void main(String[] args) {
		//ApplicationContext context = new FileSystemXmlApplicationContext("src/main/java/expert002/expert002.xml");
		//ApplicationContext context = new ClassPathXmlApplicationContext("expert002.xml", Driver.class);
		ApplicationContext context = new ClassPathXmlApplicationContext("expert002/expert002.xml");

		// Car car = (Car)context.getBean("car");
		Car car = context.getBean("car", Car.class);

		// Tire tire = (Tire)context.getBean("tire");
		Tire tire = context.getBean("tire", Tire.class);

		car.setTire(tire);

		System.out.println(car.getTireBrand());
	}
}

 

이 코드를 위해서 작성된 xml은 아래와 같다.

 

<?xml version="1.0" encoding="UTF-8"?>
<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">

	<bean id="tire" class="expert002.KoreaTire"></bean>

	<bean id="americaTire" class="expert002.AmericaTire"></bean>

	<bean id="car" class="expert002.Car"></bean>

</beans>

 

bean 태그를 보면, Driver 클래스에서 가져오는 tire, car, americaTire의 정보가 어느 클래스의 것인지를 명시해두었다. 각 상품을 구분하기 위해 id 값과 class 값을 부여한다. 

 

이렇게 되면 다음과 같이 작동한다고 비유할 수 있다.

 

운전자가 종합 쇼핑몰에서 타이어를 구매한다.
운전자가 종합 쇼핑몰에서 자동차를 구매한다.
운전자가 자동차에 타이어를 장착한다.

 

아직은 무엇인가 불편해보인다. 굳이 구매하고 나서 장착까지 내가 해야 되는지 하는 발전적인 귀찮음이 발생하였다. 아래에서 그것을 해결하는 절차를 밟아보도록 하자.

 

 

2) XML에서 속성 주입

 

xml 안에 Driver 클래스에 있던 set 메서드를 property라는 태그를 이용해 없애고, xml 안에서 직접 속성 주입을 하는 것이다. 이번엔 xml을 먼저 보자.

 

<?xml version="1.0" encoding="UTF-8"?>
<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">

	<bean id="koreaTire" class="expert003.KoreaTire"></bean>

	<bean id="americaTire" class="expert003.AmericaTire"></bean>

	<bean id="car" class="expert003.Car">
		<property name="tire" ref="koreaTire"></property>
		<!--  
		<property name="tire" ref="americaTire"></property>
		-->
	</bean>
	
</beans>

 

property 태그가 생긴 것을 볼 수 있는데 이것이 어떻게 작동되는지는 아래에서 살펴보겠다.

 

다음은 자바 코드이다.

 

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public interface Tire {
	String getBrand();
}

public class KoreaTire implements Tire {
	public String getBrand() {
		return "코리아 타이어";
	}
}

public class AmericaTire implements Tire {
	public String getBrand() {
		return "미국 타이어";
	}
}

public class Car {
	Tire tire;

	public Tire getTire() {
		return tire;
	}

	public void setTire(Tire tire) {
		this.tire = tire;
	}

	public String getTireBrand() {
		return "장착된 타이어: " + tire.getBrand();
	}
}

public class Driver {
	public static void main(String[] args) {
		//ApplicationContext context = new FileSystemXmlApplicationContext("src/main/java/expert003/expert003.xml");
		//ApplicationContext context = new ClassPathXmlApplicationContext("expert003.xml", Driver.class);
		ApplicationContext context = new ClassPathXmlApplicationContext("expert003/expert003.xml");

		//Car car = (Car)context.getBean("car");
		Car car = context.getBean("car", Car.class);
        
        System.out.println(car.getTireBrand());

	}
}

 

바뀐 것은 Driver 클래스에 setTire 메서드가 없어진 것, tire 객체 생성이 빠진 것뿐이다. 1)과 똑같이 작동한다.

 

왜 setTire(tire) 메서드가 삭제되어도 똑같이 작동할 수 있을까?

바뀌고 나서 의존성 주입 흐름은 다음과 같다. 1번은 getBean으로 id가 car인 bean을 불러오는 것이다. 이것은 차를 구매하는 것으로 비유를 들 수 있겠다. 이것은 코드 파일에 그대로 남겨 두어야 한다. 그러고 2번은 타이어를 구매하는 부분이고, 3번은 koreaTire를 자동차의 타이어 속성에 결합하는 것이다. tire의 경우 xml에서 정의되어 있으므로 필요가 없고, 나머지 3번에 해당하는 것도 자바 코드에는 필요 없기 때문에 나타나지 않는다. 그다음 property 태그에서 속성을 정의하는 property 태그 안에서 tire의 레퍼런스를 koreaTire로 했고, car의 tire가 이미 자리하고 있기 때문에 setTire도 사라질 수 있는 것이다.

 

 

아까와 같이 아래처럼 작동한다고 비유할 수 있다.

운전자가 종합 쇼핑몰에서 자동차 구매를 요청한다.
종합 쇼핑몰은 자동차를 생산한다.
종합 쇼핑몰은 타이어를 생산한다.
종합 쇼핑몰은 자동차에 타이어를 장착한다.
종합 쇼핑몰은 운전자에게 자동차를 인도한다.

 

3) @Autowired로 속성 주입

 

귀찮음이 이 어노테이션을 만들었다. setTire를 쓰지 않고도 스프링이 설정 파일을 통해 메서드 대신 속성을 주입하게 만드는 역할을 한다. 

 

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public interface Tire {
	String getBrand();
}

public class KoreaTire implements Tire {
	public String getBrand() {
		return "코리아 타이어";
	}
}

public class AmericaTire implements Tire {
	public String getBrand() {
		return "미국 타이어";
	}
}

public class Car {
	@Autowired
	Tire tire;

	public String getTireBrand() {
		return "장착된 타이어: " + tire.getBrand();
	}
}

public class Driver {
	public static void main(String[] args) {
		ApplicationContext context = new ClassPathXmlApplicationContext("expert004/expert004.xml");

		Car car = context.getBean("car", Car.class);

		System.out.println(car.getTireBrand());
	}
}

 

Car  클래스에 @Autowired가 붙었다. 이는 스프링 설정 파일을 보고 자동으로 속성의 setter 메서드에 해당하는 역할을 해주겠다는 의미이다. 그런데 xml 파일을 보면 아까와는 다르게 property 태그가 사라져있다.

 

<?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"
	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-3.1.xsd">

	<context:annotation-config />

	<bean id="tire" class="expert004.KoreaTire"></bean>

	<bean id="wheel" class="expert004.AmericaTire"></bean>

	<bean id="car" class="expert004.Car"></bean>
</beans>

 

이는 @Autowired를 통해 car의 property를 자동 의존성 주입을 해주기 때문에 생략이 가능해진 것이다.

 

 

아까와 같이 아래처럼 작동한다고 비유할 수 있다.

운전자가 종합 쇼핑몰에서 자동차 구매를 요청한다.
종합 쇼핑몰은 자동차를 생산한다.
종합 쇼핑몰은 타이어를 생산한다.
종합 쇼핑몰은 자동차에 타이어를 장착한다.
종합 쇼핑몰은 운전자에게 자동차를 인도한다.

 

4) @Resource로 속성 주입

 

Resource 어노테이션은 3)의 코드에 @Autowired 대신 쓰면 그대로 돌아간다. 그럼 Resource 어노테이션은 무엇이 다른 것일까? @Autowired는 스플이의 어노테이션인 반면, Resource는 자바 표준 어노테이션이라는 점이 다르다. 스프링 프레임워크를 사용하지 않는다면 @Resource를 써야 할 것이다.

 

 

 

 

 

 


<참고자료>

https://github.com/expert0226/oopinspring/tree/master/workspace_springjava

https://tecoble.techcourse.co.kr/post/2021-04-27-dependency-injection/

https://velog.io/@ohzzi/Spring-DIIoC-IoC-DI-%EA%B7%B8%EA%B2%8C-%EB%AD%94%EB%8D%B0

728x90

댓글