교재 실습/자바 웹 개발 워크북

82. DI를 이용한 빈 의존성 관리 (1)

Jint 2022. 7. 31. 18:29

MemberListController 클래스가 작업을 수행하려면 데이터베이스로부터 회원 정보를 가져다줄 MemberDao 객체가 필요하다. 이렇게 특정 작업을 수행할 때 사용하는 객체를 '의존 객체'라고 하고, 이런 관계를 '의존 관계(dependency)'라고 한다(그림 1).

그림 1 (의존 관계)

 

1. 의존 객체의 관리

의존 객체 관리에는 필요할 때마다 의존 객체를 직접 생성하는 고전적인 방법에서부터 외부에서 의존 객체를 주입해 주는 최근의 방법까지 다양한 방안이 존재한다. 먼저 고전적인 방법부터 살펴본다.

 

- 의존 객체가 필요하면 즉시 생성

의존 객체를 관리하는 고전적인 방법은 의존 객체를 사용하는 쪽에서 직접 그 객체를 생성하고 관리하는 것이다. 다음은 이전에 작성했던 MemberListServlet 클래스의 일부 코드이다.

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    try {
        ServletContext sc = this.getServletContext();
        Connection conn = (Connection)sc.getAttribute("conn");
        
        MemberDao memberDao = new MemberDao();
        memberDao.setConnection(conn);
        
        request.setAttribute("members", memberDao.selectList());
        ...
        
}

회원 목록 데이터를 가져오기 위해 직접 MemberDao 객체를 생성하고 있다. 이 방식의 문제는 doGet() 메서드가 호출될 때마다 MemberDao 객체를 생성하기 때문에 비효율적이다.

 

- 의존 객체를 미리 생성해 두었다가 필요할 때 사용

앞의 방법을 조금 개선한 것이 사용할 객체를 미리 생성해 두고 필요할 때마다 꺼내 쓰는 방식이다. 웹 애플리케이션이 시작될 때 MemberDao 객체를 미리 생성하여 ServletContext에 보관해 둔다. 그리고 다음과 같이 필요할 때마다 꺼내 쓴다.

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    try {
        ServletContext sc = this.getServletContext();

        //ServletContext에 저장된 DAO 객체 사용
        MemberDao memberDao = (MemberDao)sc.getAttribute("memberDao");

        //DAO의 selectList() 메서드 반환값을 request에 담기
        request.setAttribute("members", memberDao.selectList());
        ...
        
}

이전에 작성한 MemberListController 클래스 또한 이 방식을 사용하고 있다. 다만, ServletContext 보관소가 아닌 Map 객체에서 꺼낸다는 것이 다를 뿐이다. 다음은 MemberListController 클래스의 일부 코드이다.

@Override
public String execute(Map<String, Object> model) throws Exception {
    //Map 객체에서 MemberDao를 꺼냄
    MemberDao memberDao = (MemberDao)model.get("memberDao");

    //회원 목록 데이터를 Map 객체에 저장
    model.put("members", memberDao.selectList());

    //화면을 출력할 JSP 페이지의 URL 반환
    return "/member/MemberList.jsp";
}

 

■ 의존 객체와의 결합도 증가에 따른 문제

앞에서 처럼 의존 객체를 직접 생성하거나 보관소에서 꺼내는 방식으로 관리하다 보니 문제가 발생한다.

 

- 코드의 잦은 변경

의존 객체를 사용하는 쪽과 의존 객체(또는 보관소) 사이의 결합도가 높아져서 의존 객체나 보관소에 변경이 발생하면 바로 영향을 받는다는 것이다. 예를 들어 의존 객체의 기본 생성자가 public에서 private으로 바뀐다면 의존 객체를 생성하는 모든 코드를 찾아 변경해 주어야 한다. 보관소도 마찬가지이다. 보관소가 변경되면 그 보관소를 사용하는 모든 코드를 변경해야 한다(그림 2).

그림 2 (코드의 유지 보수가 어려움)

 

- 대체가 어렵다

의존 객체를 다른 객체로 대체하기가 어렵다. 현재 MemberListController 클래스가 사용하는 MemberDao 객체는 MySQL 데이터베이스를 사용하도록 작성되었다. 만약 오라클 데이터베이스를 사용해야 한다면, 일부 SQL 문을 그에 맞게끔 변경해야 한다.

다른 방법으로는 다음 그림과 같이 각 데이터베이스 별로 MemberDao 객체를 준비하는 것이다. 그래도 여전히 문제는 남아 있는데, 데이터베이스가 바뀔 때마다 DAO를 사용하는 코드도 변경해야 한다는 것이다(그림 3).

그림 3 (다른 객체로 교체하기가 어려움)

물론 인터페이스와 보관소를 이용하여 DAO를 보다 쉽게 교체하게 만들 수 있지만, 이 방식으로도 보관소에 종속되는 문제는 피할 수 없다. 그래서 다음의 방법이 등장하게 된 것이다.

 

2. 의존 객체를 외부에서 주입

인간의 실제 생활을 비교하여 설명하자면, 규모가 작은 원시 사회에서는 사람들이 직접 필요한 도구들을 만들어 썼다. 농사도 직접 짓고, 집도 직접 만들고, 사냥도 직접 하였다. 그러나 사회 규모가 점점 커짐에 따라 분업화가 진행되어 일부는 개인이 직접 만들지만, 대부분은 남이 만든 것을 이용하게 되었다. 당장 사용하는 컴퓨터나 노트북도 우리가 직접 만든 것이 아니라 남이 만든 것이다.

프로그래밍도 현실 세계와 똑같다. 초창기 객체 지향 프로그래밍에서는 의존 객체를 직접 생성하였다. 그러다가 위에서 언급한 문제를 해결하기 위해 의존 객체를 외부에서 주입받는 방식(Dependency Injection)으로 바뀌게 된다(그림 4).

그림 4 (빈 컨테이너와 의존 객체의 주입)

그림 4와 같이 의존객체를 전문으로 관리하는 '빈 컨테이너(Java Beans Container)'가 등장하게 되었다. 빈 컨테이너는 객체가 실행되기 전에 그 객체가 필요로 하는 의존 객체를 주입해주는 역할을 수행한다. 이런 방식으로 의존 객체를 관리하는 것을 '의존성 주입(DO; Dependency Injection)'이라 한다. 좀 더 일반적인 말로 '역 제어(IoC; Inversion of Control)'라고 부른다. 즉 역제어(IoC) 방식의 한 예가 의존성 주입(DI)이다.

 

3. MemberDao와 DataSource

지금까지 사용했던 MemberDao 클래스에 DI 기법이 이미 적용되어 있다. MemberDao 클래스가 작업을 수행하려면 데이터베이스와 연결을 수행하는 DataSource 객체가 필요하다. 그런데 이 DataSource 객체를 MemberDao 클래스에서 직접 생성하는 것이 아니라 외부에서 주입 받는다. 다음은 MemberDao 클래스에 DataSource 객체를 주입하는 코드이다.

@Override
public void contextInitialized(ServletContextEvent event) {
    try {
        //DB 커넥션 객체 준비
        ServletContext sc = event.getServletContext();

        //InitialContext 객체 생성 - 톰캣 서버에서 자원 찾기 위함
        InitialContext initialContext = new InitialContext();
        //lookup() 메서드로 JNDI 이름으로 등록된 서버 자원 찾을 수 있다.
        //lookup() 메서드 매개변수는 서버 자원의 JNDI 이름
        DataSource ds = (DataSource)initialContext.lookup("java:comp/env/jdbc/studydb");

        //MemberDao 객체 준비하여 ServletContext에 보관
        MemberDao memberDao = new MemberDao();
        memberDao.setDataSource(ds); //DAO에 DataSource 객체 주입
        sc.setAttribute("memberDao", memberDao);
        ...
        
}

ContextLoaderListener 클래스의 contextInitialized() 메서드는 웹 애플리케이션이 시작될 때 호출되는 메서드이다. 이 메서드를 살펴보면 setDataSource() 메서드를 호출하는 부분이 있다. MemberDao 클래스가 사용할 의존 객체인 'DataSource'를 주입하고 있다.

 

4. MemberListController에 MemberDao 주입

MemberListController 클래스가 작업을 수행하려면 MemberDao 객체가 필요하다. MemberListController 클래스에도 DI를 적용해본다. spms.controls.MemberListController 클래스를 다음과 같이 수정한다.

package spms.controls;

import java.util.Map;

import spms.dao.MemberDao;

//의존 객체 주입을 위해 인스턴스 변수와 셋터 메서드 추가, 의존 객체를 꺼내는 기존 코드 변경(주석처리)
public class MemberListController implements Controller {
	MemberDao memberDao; //인스턴스 변수
	  
	public MemberListController setMemberDao(MemberDao memberDao) {//셋터 메서드
		this.memberDao = memberDao;
		return this; //셋터 메서드 쉽게 사용하기 위해 자신의 인스턴스 값 반환
	}
	
	@Override
	public String execute(Map<String, Object> model) throws Exception {
		//Map 객체에서 MemberDao를 꺼냄
		//MemberDao memberDao = (MemberDao)model.get("memberDao");

		//회원 목록 데이터를 Map 객체에 저장
		model.put("members", memberDao.selectList());

		//화면을 출력할 JSP 페이지의 URL 반환
		return "/member/MemberList.jsp";
	}
}

 

- 의존 객체 주입을 위한 인스턴스 변수와 셋터 메서드

MemberDao 클래스에서 DataSource 객체를 주입 받고자 인스턴스 변수와 셋터 메서드를 선언했듯이, MemberListController 클래스에도 MemberDao 객체를 주입 받기 위한 인스턴스 변수와 셋터 메서드를 추가하였다.

MemberDao memberDao; //인스턴스 변수
	  
public MemberListController setMemberDao(MemberDao memberDao) {//셋터 메서드
    this.memberDao = memberDao;
    return this; //셋터 메서드 쉽게 사용하기 위해 자신의 인스턴스 값 반환
}

셋터 메서드를 좀 더 쉽게 사용하기 위해 Member 클래스에서처럼 리턴 타입이 그 자신의 인스턴스 값을 리턴하게 했다.

 

- 의존 객체를 스스로 준비하지 않는다!

외부에서 MemberDao 객체를 주입해 줄 것이기 때문에 이제 더 이상 Map 객체에서 MemberDao 객체를 꺼낼 필요가 없다. 따라서 해당 코드를 제거한다.

//Map 객체에서 MemberDao를 꺼냄
//MemberDao memberDao = (MemberDao)model.get("memberDao");

 

MemberListController 클래스 외에 나머지 모든 페이지 컨트롤러도 MemberListController 클래스와 같이 MemberDao 객체를 주입하기 위한 인스턴스 변수와 셋터 메서드를 LogInController, MemberAddController, MemberUpdateController, MemberDeleteController 클래스에 추가했다.

 

참고도서 : https://freelec.co.kr/book/1674/

 

[열혈강의] 자바 웹 개발 워크북

[열혈강의] 자바 웹 개발 워크북

freelec.co.kr