애플리케이션에서 사용자의 요구 사항에 따라 클래스 기능을 변경하거나 다른 클래스 기능으로 대체해야 하는 경우, 좀 더 수월하게 대체할 수 있게 도입된 기능이 바로 의존성 주입 DI과 제어 역행 IoC이다.
0. 의존성?
의존성
사용하는 객체를 A 클래스라고 하고 사용되는 객체를 B 클래스라고 할 때,
A 클래스에서 B 클래스를 사용하려면 new 키워드를 이용해 B 클래스의 인스턴스를 생성하고 B 클래스의 메서드를 사용하게 된다.
이때 B 클래스에서 구현했던 메서드를 변경하면 그 영향으로 A 클래스에서도 해당 메서드를 변경해야 한다.
이런 관계를 'A 클래스는 B 클래스에 의존한다'라고 한다.
의존은 클래스 의존과 인터페이스 의존이 있다.
만약 둘 중 하나를 선택해야 한다면 인터페이스 의존을 사용하는 것이 낫다.
인터페이스는 참조를 받는 유형으로 사용할 수 있으므로 변수의 이름을 변경하지 않아도 되고,
인터페이스가 선언된 메서드를 이용하면 클래스가 바뀌어도 메서드명을 변경하지 않아도 되기 때문이다.
1. 의존성 주입이란?
의존성 주입이란?
Dependency Injection
연관 관계를 개발자가 아닌 컨테이너가 규정하는 것
👉 코드에서 직접적인 연관 관계가 발생하지 않으므로 각 클래스들의 변경이 자유로워짐(loosely coupled, 약한 결합)
소스 코드에서 다른 클래스의 생성자를 호출해서 사용할 경우 기능을 구현하는 과정에서 다른 변경 사항이 발생하면 빠르게 대처하기가 어렵다.
why? 또다시 관련이 있는 모든 클래스들의 소스 코드를 수정해주어야 하기 때문
👉 스프링 프레임워크에서는 각 클래스들의 연관 관계를 스프링 프레임워크에서 설정을 통해 맺어줌
2. 의존성 주입의 필요성
의존성 주입을 해보기에 앞서 의존성 주입이 왜 필요한지 느껴보자.
오라클과 연동하는 게시판 클래스 계층 구조가 위와 같을 때,
BoardServiceImpl이 BoardDAO(인터페이스)를 이용해 BoardOracleDAOImpl(하위 클래스) 객체를 생성한 후 오라클 데이터베이스와 연동한다고 가정해보자.
개발 중에 MySQL과 연동하는 기능이 생겼다면,
지금처럼 인터페이스로 구현한 경우에는 BoardOracleDAOImpl 클래스를 변경할 필요가 없다.
BoardDAO 인터페이스를 구현한 또 다른 BoardMySqlDAOImpl 클래스를 구현한 후 BoardServiceImpl에서 사용하면 된다.
이런 식으로 클래스들 간 의존 관계를 약화시킬 수 있다.
하지만, 인터페이스를 사용해도 BoardServiceImpl 클래스 자체는 여전히 소스 코드에서 직접 수정해야 한다.
3. 의존성 주입의 장점
- 클래스들 간의 의존 관계를 최소화 👉 코드를 단순화할 수 있음
- 애플리케이션을 더 쉽게 유지 및 관리할 수 있음
- 기존 구현 방법은 개발자가 직접 코드 안에서 객체의 생성과 소멸을 제어했음
하지만 의존성 주입은 객체의 생성, 소멸과 객체 간의 의존 관계를 컨테이너가 제어
👉 각 클래스들의 변경 및 수정이 자유로워짐
4. 의존성 주입 적용
DI는 객체의 생성, 소멸, 의존 단계를 개발자가 직접 설정하는 것이 아니라 XML이나 애너테이션 설정을 통해 경량 컨테이너에 해당하는 스프링 프레임워크가 제어한다.
따라서 기존 코드에서와는 달리, 스프링 프레임워크에서는 객체의 제어를 스프링이 직접 담당한다.
👉 제어의 역전(IoC, Inversion of Control)이라는 단어 등장
IoC의 종류도 여러 가지인데, 일반적으로 스프링에서는 DI로 IoC의 기능을 구현하므로 IoC보다는 DI라는 용어를 더 많이 사용한다.
아래는 BoardServiceImpl 클래스의 코드이다.
public class BoardServiceImpl implements BoardService {
/**
* 기존 코드--------------------------------------------------
* BoardDAO boardDAO;
* public BoardService() {
* boardDAO = new BoardMySqlDAOImpl();
* }
*/
// DI를 적용해 기능 구현--------------------------------------
private BoardDAO boardDAO;
/* 생성자 이용 */
public BoardServiceImpl(BoardDAO boardDAO) {
this.boardDAO = boardDAO;
}
/* setter 이용 */
public void setBoard(BoardDAO boardDAO) {
this.boardDAO = boardDAO;
}
}
기존 코드와 비교해보았을 때, DI를 적용해 구현한 코드를 살펴보면
BoardServiceImpl 클래스는 의존하는 BoardDAOIpl 객체를 전달받기 위해 new 키워드를 사용해 객체를 생성하는 것이 아니라,
생성자를 호출할 때나 setter를 이용하여 외부에서 객체를 주입 받아 사용한 것을 알 수 있다.
즉, 소스 코드에서 new를 사용해 객체를 생성하는 것이 아니라
BoardSerivceImpl 생성자를 호출하거나 setBoard와 같은 setter를 호출할 때 컨테이너에 의해 주입되는 객체로
boardDAO 변수를 초기화한 것이다.
스프링에서는 의존하는 객체를 컨테이너 실행 시 주입하기 때문에 DI라고 부른다.
여기서 각 클래스 객체(인스턴스)를 bean 빈이라고 부르는데,
이는 의존 관계를 설정하는 외부 XML 파일에서 각각의 객체를 <bean> 태그로 표시하기 때문이다.
5. 의존성 주입 다섯 가지 규칙
DI 컨테이너에 인스턴스 생성을 맡기고 다음의 규칙을 지키는 것으로 '사용하는 객체' 클래스를 전혀 수정할 필요가 없게 만들 수 있다.
1) 인터페이스를 이용하여 의존성을 만든다.
: 의존하는 부분에 인터페이스를 이용한다.
2) 인스턴스를 명시적으로 생성하지 않는다.
: 인스턴스 생성에 new 키워드를 사용하지 않는다.
3) 어노테이션을 클래스에 부여한다.
4) 스프링 프레임워크에서 인스턴스를 생성한다.
: 3, 4를 함께 설명하자면 인스턴스를 생성하려는 클래스에 인스턴스 생성 어노테이션(@Component)을 부여한다는 것이다.
컴포넌트 스캔 - 스프링 프레임워크는 시작할 때 대상 프로젝트의 모든 패키지를 스캔한다.
컴포넌트 스캔 후 스프링 프레임워크는 인스턴스 생성 어노테이션이 부여된 클래스를 추출하고
추출한 클래스의 인스턴스를 생성한다.
어노테이션 | 개요 |
@Controller | 인스턴스 생성 지시. 스프링 MVC를 이용할 때 컨트롤러에 부여 |
@Service | 인스턴스 생성 지시. 트랜잭션 경계가 되는 도메인(서비스) 기능에 부여 |
@Repository | 인스턴스 생성 지시. 데이터베이스 액세스(리포지토리) 기능에 부여 |
@Component | 위 용도 이외의 클래스에 부여 |
5) 인스턴스를 이용하고 싶은 곳에 어노테이션을 부여한다.
: 스프링 프레임워크에 의해 생성된 인스턴스를 이용하는 클래스에 참조를 받는 필드를 선언하고 필드에 @Autowired 어노테이션을 부여한다.
6. 의존성 주입 방법: setter, 생성자
의존성 주입 방식 두 가지를 살펴보기에 앞서 <bean> 태그에 사용되는 여러 가지 속성들을 정리해보자.
속성 이름 | 설명 |
id | 빈 객체의 고유 이름. 빈 id를 이용해 빈에 접근함 |
name | 객체의 별칭 |
class | 생성할 클래스. 패키지 이름까지 입력해야 함 |
constructor-arg | 생성자를 이용해 값을 주입할 때 사용함 |
property | setter를 이용해 값을 주입할 때 사용함 |
- setter를 이용한 DI
person.xml
(의존관계 설정 파일)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans>
<bean id="personService" class="com.spring.ex01.PersonServiceImpl">
<property name="name">
<value>홍길동</value>
</property>
</bean>
</beans>
위 코드처럼 실행 클래스 실행 시 <bean> 태그를 이용해 com.spring.ex01.PersonServiceImpl 클래스에 대한 빈을 생성한다.
그리고 이 빈에 대해 접근할 수 있는 빈 id를 personService로 지정한 후
<property> 태그를 이용해 PersonServiceImpl 클래스 객체의 name 속성에 <value> 태그의 값으로 초기화한다.
소스 코드에서 new를 이용해 직접 객체를 생성하던 것을 person.xml에서 설정한 것이다.
PersonServiceImpl
(구현 클래스)
package com.spring.ex01;
public class PersonServiceImpl implements PersonService {
private String name;
private int age;
public void setName(String name) {
this.name = name
}
@Override
public void sayHello() {
System.out.println("이름: " + name);
System.out.println("나이: " + age);
}
}
구현 클래스 PersonServiceImpl에서 인터페이스 PersonService를 구현하고
setter를 이용해 person.xml에서 <value> 태그로 설정한 값을 name 속성에 주입한다.
단, age 속성은 setter가 없으므로 빈이 생성되더라도 값이 초기화되지 않는다.
PersonTest
(실행 클래스)
package com.spring.ex01;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.FileSystemResource;
public class PersonTest {
public static void main(String[] args) {
// 실행 시 person.xml을 읽어 들여 빈을 생성함
BeanFactory factory = new XmlBeanFactory(new FileSystemResource("person.xml"));
// id가 personService인 빈을 가져옴
PersonService person = (PersonService) factory.getBean("personService");
// 생성된 빈을 이용해 name 값을 출력함
person.sayHello();
}
}
실행 시 콘솔에 name 속성 값은 person.xml에서 <value> 태그로 설정한 값이 출력되지만 age 속성값은 0이 출력된다.
- 생성자를 이용한 DI
person.xml
(의존관계 설정 파일)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans>
// 인자가 한 개인 생성자로 id가 personService1인 빈을 생성
// 생성자로 value인 이순신을 전달하여 속성 name을 초기화함
<bean id="personService1" class="com.spring.ex01.PersonServiceImpl">
<constructor-arg value="이순신" />
</bean>
// 인자가 두 개인 생성자로 id가 personService2인 빈을 생성
// 생성자로 두 개의 값을 전달하여 속성 name과 age를 초기화함
<bean id="personService2" class="com.spring.ex02.PersonServiceImpl">
<constructor-arg value="손흥민" />
<constructor-arg value="23" />
</bean>
</beans>
PersonServiceImpl
(구현 클래스)
package com.spring.ex01;
public class PersonServiceImpl implements PersonService {
private String name;
private int age;
// person.xml에서 인자가 한 개인 생성자 설정 시 사용됨
public void PersonServiceImpl(String name) {
this.name = name;
}
// person.xml에서 인자가 두 개인 생성자 설정 시 사용됨
public void PersonServiceImpl(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void sayHello() {
System.out.println("이름: " + name);
System.out.println("나이: " + age);
}
}
PersonTest2
(실행 클래스)
package com.spring.ex01;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.FileSystemResource;
public class PersonTest2 {
public static void main(String[] args) {
// 실행 시 person.xml을 읽어 들여 빈을 생성함
BeanFactory factory = new XmlBeanFactory(new FileSystemResource("person.xml"));
// id가 personService1인 빈을 가져옴
PersonService person1 = (PersonService) factory.getBean("personService2");
person1.sayHello();
// id가 personService2인 빈을 가져옴
PersonService person2 = (PersonService) factory.getBean("personService2");
person2.sayHello();
}
}
id가 personService1인 빈에 접근하여 sayHello() 메서드를 호출하면 age는 0으로 출력된다.
반면에 id가 personService2인 빈에 접근한 후 sayHello() 메서드를 호출하면 두 속성 값이 모두 출력된다.
- DI 예제
앞에서는 설정 파일에서 기본형 데이터를 빈의 속성 값으로 직접 주입해서 사용했다.
이번에는 빈에 주입되는 값이 의존 관계에 있는 다른 빈을 주입하는 경우를 살펴보겠다.
위 그림은 회원 기능 관련 Service 클래스와 DAO 클래스의 계층 구조를 나타낸 것이다.
Service 클래스는 데이터베이스와의 연동을 위해 DAO 클래스에 의존하는 관계인 것을 알 수 있다.
DI를 이용하지 않는 경우, Service 클래스는 소스 코드에서 직접 DAO 객체를 생성한 후 DAO에서 제공하는 메서드를 이용해 데이터베이스와 연동해야 한다.
이것과 DI를 이용하는 경우가 어떻게 다른지 살펴보려고 한다.
member.xml
(의존 관계 설정 파일)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans>
// MemberServiceImpl 클래스를 이용해 id가 memberService인 빈을 만듦
// 빈을 만들면서 setter 주입 방식으로 id가 memberDAO인 빈을 자신의 속성에 주입함
<bean id="memberService" class="com.spring.ex03.MemberServiceImpl">
// 주입되는 데이터가 기본형이 아닌 참조형인 경우 ref 속성으로 설정함
<property name="memberDAO" ref="memberDAO"/>
</bean>
// id가 memberDAO인 빈을 MemberDAOImpl을 이용해 만듦
<bean id="memberDAO" class="com.spring.ex03.MemberDAOImpl" />
</beans>
의존하는 빈을 주입할 때는 주입되는 타입이 기본형 데이터가 아닌 참조형 데이터일 경우 ref 속성을 이용해 주입해야 한다.
MemberServiceImpl
(주입되는 빈을 받을 클래스)
package com.spring.ex03;
public class MemberServiceImpl implements MemberService {
// 주입되는 빈을 저장할 MemberDAO 타입의 속성을 선언
private MemberDAO memberDAO;
// 설정 파일에서 memberDAO 빈을 생성한 후 setter로 속성 memberDAO에 주입
public void setMemberDAO(MemberDAO memberDAO) {
this.memberDAO = memberDAO;
}
@Override
public void listMembers() {
// 주입된 빈을 이용해 listMembers() 메서드 호출
memberDAO.listMembers();
}
}
MemberDAOImpl
(주입되는 빈)
package com.spring.ex03;
public class MemberDAOImpl implements MemberDAO {
@Override
public void listMembers() {
System.out.println("listMembers 메서드 호출");
System.out.println("회원정보를 조회합니다.");
}
}
MemberTest1
(실행 클래스)
package com.spring.ex01;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.FileSystemResource;
public class MemberTest1 {
public static void main(String[] args) {
// 실행 시 member.xml에 설정한 대로 빈을 생성한 후 주입함
BeanFactory factory = new XmlBeanFactory(new FileSystemResource("member.xml"));
// id가 memberService인 빈을 가져옴
MemberService service = (MemberService) factory.getBean("memberService");
service.listMembers();
}
}
실행 클래스인 MemberTest1에서는 member.xml을 읽어 들인 후 빈을 생성한다.
그리고 setter 주입 방식으로 주입한 후 id인 memberService로 접근해 listMembers() 메서드를 호출한다.
실행해보면 MemberDAO의 listMembers() 메서드를 호출한다는 것을 알 수 있다.
이 예제에서는 자바 코드로 어떤 클래스 객체도 생성하지 않았다.
오로지 스프링의 DI 기능을 이용해 빈을 생성했고, 의존 관계에 있는 빈을 속성에 주입한 후 빈의 기능을 사용했다.
참고
이병승, 「자바 웹을 다루는 기술」, 길벗, 2019, p.768-790
후루네스 외 1명,「스프링 프레임워크 첫걸음」, 위키북스, 2022, p.40-54
'Web > Spring' 카테고리의 다른 글
[Spring] Spring MVC란? (1) | 2024.05.03 |
---|---|
[Spring] JDBC 프로그래밍을 위한 API와 용어들 (0) | 2024.04.30 |
[Spring] 컴포넌트 스캔 Component Scan (0) | 2024.04.17 |
[Spring] DI 자동주입 (0) | 2024.04.14 |
[Spring] 스프링 컨테이너 (2) | 2024.04.10 |