✅ 의존 관계 주입(Dependency Injection)
의존 관계 주입(Dependency Injection, DI)은 객체 간의 의존 관계를 코드 내에서 명시적으로 설정하는 것이 아니라 외부에서 의존 객체를 주입하여 사용하는 디자인 패턴입니다. 일반적으로 객체 간의 의존 관계는 해당 객체가 직접 의존하는 객체를 생성하거나 가져와 사용하는 방식으로 구현되지만, 이는 객체 간의 결합도를 높이고 코드 유지보수에 어려움을 초래할 수 있습니다. 그래서 IoC 컨테이너를 사용하여 외부로부터 객체 간의 관계를 설정합니다. 스프링 IoC 기능의 대표적인 동작원리는 주로 의존관계 주입이라고 불립니다. 스프링이 다른 프레임워크와 차별화돼서 제공해주는 기능은 DI를 사용할 때 명확하게 들어납니다.
✅ 의존 관계
두 개의 클래스 또는 모듈이 의존관계에 있다고 할 때는 항상 방향성을 부여합니다. UML 모델에서는 다음과 같이 점선으로 된 화살표로 표현하고 있습니다.
의존하고 있다는 것은 서로 영향을 미친다는 것입니다. 여기서는 B가 변하면 A에게 영향을 미치고, 반대로 B는 A에게 의존하고 있지 않습니다. 즉 A의 코드가 변해도 B의 코드 수행 결과에는 아무런 영향을 미치지 않습니다.
✅ UserDao의 의존관계
UserDao
가 ConnectionMaker
인터페이스에만 의존하고 있습니다. 따라서 ConnectionMaker
의 코드가 변한다면 UserDao
에도 영향을 미치게 됩니다. 하지만 ConnectionMaker
을 구현한 클래스인 DConnectionMaker
이 바뀌어도 UserDao
에는 영향을 주지 않습니다. 이는 서로 의존 관계가 아니기 때문입니다. 인터페이스에 대해서만 의존관계를 만들면 인터페이스 구현 클래스와의 관계는 느슨해져 변화에 영향을 덜 받는 결합도가 낮은 프로그래밍이 됩니다.
UserDao
객체가 런타임 시 사용할 클래스가 어떤 클래스로 만들어지는지 컴파일 단계에서 미리 알 수 없습니다. 설계와 코드 속에서 들어나지 않고, 단지 어떤 타입의 객체를 사용할 것인지만 설정하게 됩니다. 프로그램이 실행되고 나서(런타임 시) 의존관계를 맺는 대상을 의존 객체라고 합니다. DConnectionMaker
객체는 UserDao
객체의 의존 객체라고 정의할 수 있습니다.
의존관계 주입의 핵심은 설계 시점에는 알지 못했던 두 객체의 관계를 맺도록 도와주는 제 3의 존재, 외부에서 관계를 결정해주는 존재가 있다는 것입니다. 스프링에서는 애플리케이션 컨텍스트, 빈 팩토리, IoC 컨테이너를 통해서 구현하고 있습니다.
- 의존관계 주입의 세가지 충족 조건
- 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스에만 의존하고 있어야 한다.
- 런타임 시점의 의존관게는 컨테이너나 팩토리 같은 제 3의 존재, 외부에서 결정해야 한다.
- 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어진다.
✅ UserDao 의존관계설정
런타임 시점에서 의존관계를 설정하기 위해선 제 3의 존재가 필요합니다. DaoFactory가 그 역할을 담당하는 코드를 만들어보겠습니다.
- DaoFactory
package com.jhcode.spring.ch1.dao; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class DaoFactory { @Bean public UserDao userDao() { ConnectionMaker connectionMaker = new DConnectionMaker(); UserDao userDao = new UserDao(connectionMaker); return userDao; } @Bean public ConnectionMaker connectionMaker() { ConnectionMaker connectionMaker = new DConnectionMaker(); return connectionMaker; } }
- UserDao
package com.jhcode.spring.ch1.dao; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import com.jhcode.spring.ch1.domain.User; public class UserDao { private ConnectionMaker connectionMaker; public UserDao (ConnectionMaker connectionMaker) { this.connectionMaker = connectionMaker; }
DaoFactory
는 의존관계 주입을 담당하는 컨테이너라고 볼 수 있고, DI 컨테이너, DI의 근간이 되는 개념인 IoC와 함께 IoC/DI 컨테이너라고 부를 수도 있습니다.DI 컨테이너는
UserDao
를 만드는 시점에서 생성자의 파라미터로 이미 만들어진DConnectionMaker
의 객체를 전달합니다. 정확히는 참조 주소가 전달되고 있습니다. 생정자 파라미터를 통해 전달받은 객체는 필드변수에 저장되어 런타임시 의존관계를 생성합니다.의존관계 주입(DI)는 자신(
UserDao
)가 사용할 객체에 대한 선택과 생성 제어권을 외부로 넘기는 것입니다.
✅ 의존관계 검색과 주입
스프링이 제공하는 IoC 방법에는 의존관계 주입 뿐만 아니라 의존관계 검색이라는 것이 있습니다. 의존관계에서 자신이 필요로 하는 의존 객체를 능동적으로 찾는 것입니다. 어떤 객체를 사용할지 결정하지는 않고, 스스로 IoC 컨테이너에게 요청하는 방법을 사용합니다.
- UserDao : 빈 팩토리를 사용한 의존관계 검색
public class UserDao { private ConnectionMaker connectionMaker; public UserDao (ConnectionMaker connectionMaker) { DaoFactory daoFactory = new DaoFactory(); this.connectionMaker = daoFactory.connectionMaker(); }
DaoFactory
, 빈 팩토리에게 자신이 필요로하는 의존 객체를 메소드를 통해 요청했습니다. 그러나 여전히UserDao
는ConnectionMaker
가 어떤 구현 클래스로 사용될지 알지 못합니다. 이는 런타임 시에 빈 팩토리에 의해서 결정됩니다.
- UserDao : 스프링 IoC를 사용한 의존관계 검색
public class UserDao { private ConnectionMaker connectionMaker; public UserDao () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class); this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class); }
스프링 IoC 컨테이너인 애플리케이션 컨텍스트에서 제공하는
getBean()
메소드를 통해서UserDao
가 의존관계가 필요한 객체를 검색해서 주입받았습니다.
👉UserDao
가 빈 팩토리나 애플리케이션 컨텍스트와 같이 성격이 다른 객체에게 의존관계 검색을 의존하고 있기 때문에 대개는 의존관계 주입방식을 사용하는 것이 더 낫습니다. 그러나 UserDaoTest
와 같이 DI를 통해 객체를 주입받을 수 있는 방법이 없는 main()
메소드 같은 형태에서는 의존관계 검색을 통해 객체를 주입받는 것이 좋습니다.
DL(의존관계 검색)을 통해 검색되어 주입받는 객체가 꼭 스프링의 빈일 필요가 없다는 특징이 있습니다. 빈으로 등록하지 않더라도 다른 클래스에서 직접 생성하여 충분히 주입받을 수 있습니다. 그래서 DI와 DL은 각각의 사용 방법이 다릅니다.
✅ 의존관계 주입의 응용
- 기능 구현의 교환
개발 중에는 로컬 DB를 사용하고, 서버에 배포할 때 다시 서버가 제공하는 특별한 DB 연결 클래스를 사용해야 할 경우로 살펴보겠습니다. 만약 DI를 사용하지 않고 DAO에서 직접 생성해서 사용할 경우 DAO가 100개라면 DB 연결 객체를 생성한 100군데 코드를 전부 수정해야 합니다. 하지만 DI를 사용한다면 인터페이스로 선언된 기능을 통해 DAO의 코드 수정없이 해당 원하는 객체를 사용할 수 있을 것입니다.
- 개발용 ConnectionMaker 생성 코드
@Bean public ConnectionMaker connectionMaker() { return new LocalDBConnectionMaker(); }
- 서버용 ConnectionMaker 생성 코드
@Bean public ConnectionMaker connectionMaker() { return new ProductionDBConnectionMaker(); }
👉기존에 개발용 DB커넥션 생성코드를 서버용 DB커넥션 생성코드로 변경하기만 하면 DAO의 코드 수정없이 바뀐 DB 커넥션을 사용할 수 있는 것입니다.
- 개발용 ConnectionMaker 생성 코드
- 부가 기능 추가
DAO가 얼마나 많은 DB 커넥션을 생성하기 위해, 일일히 모든 DAO 클래스의 코드에 카운트를 위한 코드를 추가해야할까요? DI 컨테이너에서라면 DAO와 DB 커넥션을 호출과정에 새로운 오브젝트를 추가하여 쉽게 구현할 수 있습니다.
- CountingConnectionMaker
package com.jhcode.spring.ch1.dao; import java.sql.Connection; import java.sql.SQLException; public class CountingConnectionMaker implements ConnectionMaker { int counter = 0; private ConnectionMaker realConnectionMaker; //== 생성자 ==// public CountingConnectionMaker(ConnectionMaker realConnectionMaker) { this.realConnectionMaker = realConnectionMaker; } @Override public Connection makeConnection() throws ClassNotFoundException, SQLException { this.counter++; return realConnectionMaker.makeConnection(); } //== counter getter ==// public int getCounter() { return this.counter; } }
ConnectionMaker
을 구현했지만 내부에서 직접 DB 커넥션을 생성하지 않고, 생성자를 통해 주입받은ConnectionMake
r
의 구현 객체를 통해서 DB 커넥션을 생성해서 반환합니다. 이 클래스는 단순히 DAO와 DB가 얼마나 많이 연결하고 있는지 카운팅하는 역할만 하고 있습니다.
- CountingDaoFactory
package com.jhcode.spring.ch1.dao; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class CountingDaoFactory { @Bean public UserDao userDao() { return new UserDao(connectionMaker()); } @Bean public ConnectionMaker connectionMaker() { return new CountingConnectionMaker(realConnectionMaker()); } @Bean public ConnectionMaker realConnectionMaker() { return new DConnectionMaker(); } }
CountingConnectionMaker
을 적용하기 전에는 위의 그림과 같습니다.UserDao
는DaoFactory
에서ConnectionMaker
구현 객체DConnectionMaker
을 생성자를 통해 주입받아 사용합니다.CountingConnectionMaker
을 적용 후는 위의 그림과 같습니다.UserDao
는CountingDaoFactory
에서 생성한CountingConnectionMaker
을 생성자로 주입받는데,CountingConnectionMaker
은 실제로 DB를 생성하는ConnectionMaker
의 구현 객체를 생성자로 주입받고 있습니다. 실제 DB 커넥션은ConnectionMaker
의 구현 객체인DConnectionMaker
에서 담당하고,CountingConnectionMaker
은 DAO에서 DB 커넥션을 호출할 때마다 중간다리의 역할을 하며, DB커넥션 횟수를 카운팅하는 역할을 합니다.
- UserDaoConnectionCountingTest
package com.jhcode.spring.ch1.dao; import java.sql.SQLException; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import com.jhcode.spring.ch1.domain.User; public class UserDaoConnectionCountingTest { public static void main(String[] args) throws ClassNotFoundException, SQLException{ //== 앞에서 main() 메소드와 같을 경우 DL으로 객체를 주입받는다고 하였다 ==// AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CountingDaoFactory.class); UserDao dao = context.getBean("userDao", UserDao.class); User user = new User(); user.setId("Counting"); user.setName("jhcode"); user.setPassword("mariaDB"); dao.add(user); System.out.println(user.getId() + " 등록 성공"); User user2 = dao.get(user.getId()); System.out.println(user2.getName()); System.out.println(user2.getPassword()); System.out.println(user2.getId() + " 조회 성공"); CountingConnectionMaker ccm = context.getBean("connectionMaker", CountingConnectionMaker.class); System.out.println("Connection counter: " + ccm.getCounter()); } }
- CountingConnectionMaker
✅ 메소드를 이용한 의존관계 주입
지금까지는 생성자를 통해서 DI 컨테이너가 의존할 객체 레퍼런스(참조 주소)를 넘겨주었습니다. 메소드를 통한 방법도 있습니다.
- 수정자( setter ) 메소드를 이용한 주입
set으로 시작하는 메소드로 외부에서 객체 내부의 속성 값을 변경하려는 용도로 주로 사용된다. 파라미터로 전달된 값을 내부의 변수에 저장하는 것이다.
- 일반 메소드를 이용한 주입
setter을 사용하여 주입할 경우, 한 번에 한 개의 파라미터만 가질 수 있다. 그래서 여러 개의 파라미터를 가지는 일반 메소드를 DI용으로 사용할 수 있지만, 비슷한 타입이 많거나 주입할 객체가 많을 경우 실수할 수 있다.
- 수정자 메소드 DI 방식을 사용한 UserDao
public class UserDao { private ConnectionMaker connectionMaker; // //== 생성자를 통한 객체 주입 ==// // public UserDao (ConnectionMaker connectionMaker) { // this.connectionMaker = connectionMaker; // } //== setter을 통한 객체 주입 ==// public void setConnectionMaker(ConnectionMaker connectionMaker) { this.connectionMaker = connectionMaker; }
- DaoFactory
@Configuration public class DaoFactory { // @Bean // public UserDao userDao() { // ConnectionMaker connectionMaker = new DConnectionMaker(); // UserDao userDao = new UserDao(connectionMaker); // return userDao; // } @Bean public UserDao userDao() { UserDao userDao = new UserDao(); userDao.setConnectionMaker(connectionMaker()); return userDao; }
프로젝트 환경
- IDE : STS3 - 3.9.18.RELEASE
- SpringFramework : 5.3.20
- Java : 11
- Maven
📖토비 스프링 3.1 -p111~128
Uploaded by N2T