[토비의 스프링] Week3(2.4~3.4)

23 분 소요

토비의 스프링 3.1 Chapter 2.4 ~ 3.4

Chapater 2.4

기존 UserTestDao의 문제 :

  • @Before 메소드가 테스트 메소드 개수 만큼 반복되기 때문에 애플리케이션 컨텍스트도 3번 만들어진다.
  • 빈이 많아지고 복잡해지면 애플리케이션 컨텍스트 생성에 적지 않은 시간이 걸릴 수 있다.

해결 :

  • 애플리케이션 오브젝트를 테스트 전체가 공유하는 오브젝트로 만들자!

@BeforeClass

여러 테스트가 참조할 애플리케이션 컨텍스트를 스태틱 필드에 저장하기 위해 테스트 클래스 전체에 걸쳐 딱 한번만 실행되는 @BeforeClass 스태틱 메소드를 사용할 수 있다.

-> But, 스프링이 직접 제공하는 애플리케이션 컨텍스트 테스트 지원 기능이 더 효율적

Spring Test Context FrameWork

여러 테스트 오브젝트가 사용하는 ApplicationContext를 인스턴스 변수로 선언하고 @Autowired 어노테이션을 붙혀 자동 DI가 되도록 한다.

RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/applicationContext.xml")	// 자동으로 만들어줄 ApplicationContext 위치 지정
public class UserDaoTest {

	@Autowired
	private ApplicationContext context;

	@Before
	public void setUp() {
		this.dao = context.getBean("userDao", UserDao.class);
		...
	}

@Autowired

@Autowired는 Spring DI에서 사용되는 특별한 어노테이션이다.
일반적으로 주입을 위해서는 생성자나, 수정자가 필요하지만, 이 경우에는 메소드 없이도 주입이 가능하다. 이를 타입에 의한 자동 와이어링 이라한다.

@Autowired가 붙은 인스턴스 변수가 있으면, test context framework는 변수 타입과 일치하는 컨텍스트 내의 빈을 찾아, 일치하면 인스턴스 변수로 주입해준다.

  • 참고로 Spring Application Context는 초기화 할 때 자기 자신도 빈으로 등록하기 때문에 ApplicationContext 타입의 빈이 주입이 가능한 것이다.
public class UserDaoTest {
    @Autowired
    UserDao userDao;
    ...
}

어플리케이션 컨텍스트가 가지고 있는 빈을 DI 받을 수 있다면 굳이 getBean()을 이용하지 않고 UserDao 빈을 받을 수 있다.

DI와 Test

우리는 절대로 DataSource의 구현 클래스를 바꾸지 않을 것이다 따라서 굳이 Datasource 인터세이스를 사용하고 DI를 통해 주입을 받아아햘까?

그럼에도 불구하고 인터페이스를 두고 DI를 적용해야한다

  • 소프트웨어 개발에서 절대로 바뀌지 않는 것은 없기 때문
    언젠간 구현 클래스가 바뀔 수도 있다.
  • 인터페이스를 두고 DI를 적용하게 해두면 다른 차원의 서비스 기능을 도입 가능
    1장에서 만든 DB 커넥션 개수를 카운팅 하는 부가기능이 그런 예
  • Test 용이성
    DI는 테스트가 작은 단위의 대상에 대해 독립적으로 만들어지고 실행되게 하는데 중요한 역할을 한다.

테스트 코드에 의한 DI

테스트를 위해 DB를 조작한다고 생각하자.
만약 운영 중인 DB를 건들였다가 큰일 나기 때문에 DB연결을 바꿀 필요가 있다.

따라서, DI를 이용해 테스트 중 DAO가 사용하는 DataSource 구현 클래스를 바꿔주는 방법으로 운영 DB와 테스트 DB를 분리할 수 있다.

@DirtiesContext
public class UserDaoTest {
    @Autowired
    UserDao userDao;

    @Before
    public void setUp() {
        ...
        DataSource dataSource = new SingleConnectionDataSource(
                "jdbc:mysql://localhost/testdb", "spring", "book", true
        );
        userDao.setDataSource(dataSource);
    }
}

이 방법으로 XML 설정파일 변경없이 테스트 코드를 통해 오브젝트 관계를 재구성 할 수 있다.

하지만 위 방식은 매우 주의하여야 하는데,
애플리케이션 컨텍스트는 테스트 중에 딱 한개만 만들어지 고 모든 테스트에서 공유하여 사용한다.
따라서, 이미 애플리케이션에서 applicationContext.xml 파일의 설정정보에 따라 구성한 오브젝트를 강제로 변경하면 나머지 모든 테스트를 수행하는 동안 변경된 애플리케이션 컨텍스트가 사용된다.

따라서, @DirtiesContext 어노테이션을 사용하여, 스프링의 테스트 컨텍스트 프레임워크에게 해당 클래스의 테스트에서 에플리케이션 컨텍스트의 상태를 변경한다는 것을 알려주고 애플리케이션 컨텍스트의 공유를 허용하지 않는다고 말한다.

xml 설정 파일 변경을 통한 별도의 DI 설정

테스트 코드에서 빈 오브젝트를 수동으로 DI 하는 방법은 장점보다 단점이 많다. 코드가 많아져 번거롭기도 하고 애플리케이션 컨텍스트를 매번 만들어야 하는 부담이 있다.

이 문제는 applicationContext.xml 을 복사해 text-applicationContext.xml 을 만들어 운영으로 사용할 DataSource 와 테스트에 적합하게 준비된 DB를 사용하는 가벼운 DataSource 를 빈으로 등록하여 테스트에서는 항상 테스트 전용 설정파일만 사용하게 해줌으로써 해결할 수 있다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/test-applicationContext.xml")
public class UserDaoTest {
    ...
}

이로써 번거롭게 수동 DI 하는 코드나 @DirtiesContext 가 필요 없어졌다.

스프링 컨테이너 없이 테스트

  • UserDao 나 DataSource 구현 클래스 어디에도 스프링 API를 직접 사용하거나 애플리케이션 컨텍스트를 이용하는 코드가 존재하지 않는다. 즉, 스프링 DI 컨테이너에 의존하지 않는다.

  • 스프링 컨테이너에서 UserDao가 잘 동작하는지는 UserDaoTest의 관심사가 아니다.

따라서, 스프링 컨테이너 없이 수동 DI를 이용해 테스트 코드를 만든다.

public class UserDaoTest {
    UserDao userDao;

    @Before
    public void setUp() {
        ...
        dao = new UserDao();
        DataSource dataSource = new SingleConnectionDataSource(
            "jdbc:mysql://localhost/testdb", "spring", "book", true
        );
        userDao.setDataSource(dataSource);
    }
}

애플리케이션 컨텍스트를 아예 사용하지 않으니 코드는 더 단순해지고 이해하기 쉬워졌다. 그리고 테스트 시간도 짧아졌다.

DI를 이용한 테스트 방법 선택

그렇다면 DI를 테스트에 이용하는 세 가지 방법 중 어떤 것을 선택해야 할까??

  1. 항상 스프링 컨테이너 없이 테스트할 수 있는 방법을 가장 우선적으로 고려하자.

이 방법이 테스트 수행 속도가 가장 빠르고 테스트 자체가 간결하다

  1. 여러 오브젝트와 복잡한 의존관계를 갖고 있는 오브젝트를 테스트해야 할 경우

이때는 스프링의 설정을 이용한 DI 방식의 테스트를 이용하면 편리하다.

테스트에서 애플리케이션 컨텍스트를 사용하는 경우에는 테스트 전용 설정파일을 따로 만들어 사용 하는 편이 좋다. 보통 개발환경과 테스트환경, 운영환경이 차이가 있기 때문에 각각 다른 설정파일을 만들어 사용하는 경우가 일반적이다.

  1. 예외적인 의존관계를 강제로 구성 해서 테스트해야 할 경우

이때는 컨텍스트에서 DI 받은 오브젝트에 다시 테스트 코드로 수동 DI 해서 테스트하는 방법을 사용하면 된다. 테스트 메소드나 클래스에 @DirtiesContext 를 붙이는 걸 잊지 말자!

2.5 학습 테스트로 배우는 스프링

학습 테스트란?
자신이 만들지 않은 프레임워크나 다른 개발팀에서 만들어서 제공한 라이브러리 등에 대한 테스트

학습 테스트의 목적

  • 자신이 사용할 API나 프레임워크의 기능을 테스트로 보면서 사용 방법을 익히려는 것

학습 테스트의 장점

  • 다양한 조건에 따른 기능을 손쉽게 확인해볼 수 있다.
    수동 테스트는 일일히 데이터를 변경하는 반면, 학습 테스트는 자동화 된 테스트 코드로 빠르게 확인 가능

  • 학습 테스트 코드를 개발 중에 참고할 수 있다.
    수동으로 예제를 만들면 매번 코드를 수정해야 하지만, 학습 테스트는 다양한 기능과 조건에 대한 테스트 코드를 개별적으로 남겨둘 수 있다.

  • 프레임워크나 제품을 업그레이드할 때 호환성 검증을 도와준다.
    기존에 사용했던 API에 새로운 버젼으로 기능에 변화가 있거나 버그가 있다면, 학습 테스트를 통해 미리 확인할 수 있다.
  • 테스트 작성에 좋은 훈련이 된다.
  • 새로운 기술을 공부하는 과정이 즐거워진다.

JUnit을 이용한 학습 테스트

전제

  • JUnit은 테스트 메소드를 수행 할 때마다 새로운 오브젝트를 만든다
  • 스프링의 테스트용 Application Context는 테스트 개수에 상관없이 한개만 만들어진다.
package com.taxol.chapter2_5;

import static org.hamcrest.CoreMatchers.either;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;

import java.util.HashSet;
import java.util.Set;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/*
 * list 2-25
 * 모든 오브젝트가 중복이 되지 않는 다는 것을 확인
 *
 * list 2-27
 * 스프링 테스트 컨텍스트는 테스트 개수에 상관없이 한 개만 만 만들어지는지 테스트
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/junit.xml")
public class JUnitTest {

	@Autowired
	ApplicationContext context;

	//static JUnitTest testObject;
	static Set<JUnitTest> testObjects = new HashSet<>();
	static ApplicationContext contextObject = null;

	@Test
	public void test1() {
		//assertThat(this, is(not(sameInstance(testObject))));
		assertThat(testObjects, not(hasItem(this)));
		testObjects.add(this);

		assertThat(contextObject == null || contextObject == this.context , is(true));
		contextObject = this.context;
	}

	@Test
	public void test2() {
		assertThat(testObjects, not(hasItem(this)));
		testObjects.add(this);

		assertTrue(contextObject == null || contextObject == this.context);
		contextObject = this.context;
	}

	@Test
	public void test3() {
		assertThat(testObjects, not(hasItem(this)));
		testObjects.add(this);

		assertThat(contextObject, either(is(nullValue())).or(is(this.context)));
		contextObject = this.context;
	}
}

버그 테스트

코드에 오류가 있을 때 그 오류를 가장 잘 드러내줄 수 있는 테스트이다.
버그가 발생하는 조건의 테스트를 만들고 테스트가 성공하면 버그가 해결되게 한다.

장점

  1. 테스트의 완성도를 높여준다. 기존에 미처 검증하지 못한 부분을 매꾼다
    이후에 비슷한 오류가 생겨도, 이전에 만들었던 버그 테스트로 쉽게 추적이 가능하다.

  2. 버그의 내용을 명확하게 분석하게 해준다
    버그를 테스트로 만들어 실패하게 만들려면, 버그의 이유를 명확히 알아야 한다.
    따라서 버그를 좀 더 효과적으로 분석이 가능.
    이를 통해 동등분할이나 경계값 분석을 적용해볼 수도 있다.

    또한, 다른 오류도 함께 발견 가능

  3. 기술적인 문제를 해결하는 데 도움이 된다
    자신의 코드와 설정이 문제가 없고 기술적인 문제로 버그가 생겼을 때,
    가장 단순한 코드와 그에 대한 버그 테스트를 만들어 보아 기술적 버그를 찾는다.

3.1 템플릿

JDBC 수정 기능의 예외 처리 코드를 추가하여, 무슨 일이 있어도 리소스를 반납하도록 만들었다.

public void deleteAll() throws SQLException{
	Connection c = null;
	PreparedStatement ps = null;

	try {
		c = dataSource.getConnection();
		ps = c.prepareStatement("delete from users");
		ps.executeUpdate();
	}catch (SQLException e) {
		throw e;
	}finally {
		if(ps != null) {
			try {
				ps.close();
			}catch (SQLException e) {
			}
		}
		if(c != null) {
			try {
				c.close();
			}catch (SQLException e) {
			}
		}
	}

	ps.close();
	c.close();
}

public int getCount() throws SQLException {
	Connection c = null;
	PreparedStatement ps = null;
	ResultSet rs = null;

	try {
	c = dataSource.getConnection();

	ps = c.prepareStatement("select count(*) from users");
	rs = ps.executeQuery();

	rs.next();
	return rs.getInt(1);
	}catch (SQLException e) {
		throw e;
	}finally {
		if(rs != null) {
			try {
				rs.close();
			}catch (SQLException e) {
			}
		}
		if(ps != null) {
			try {
				ps.close();
			}catch (SQLException e) {
			}
		}
		if(c != null) {
			try {
				c.close();
			}catch (SQLException e) {
			}
		}
	}
}

3.2 변하는 것과 변하지 않는 것

현재 코드에서는 try - catch - finally가 반복되고 복잡하다.
새로운 메서드를 만들다가 위 복잡한 코드로 인해 실수를 할 위험이 크다.

따라서, 변하지 않는 부분과 자주 변하는 부분을 분리하여야 한다.

3.2.2 분리와 재사용을 위한 디자인 패턴

현재 변하는 성격이 많은 부분을 메서드로 추출한다.

// 이 메소드를 기존 ps 선언 부분에서 사용함
private PreparedStatement makeStatement(Connection c) throws SQLException{
    PreparedStatement ps;
    ps = c.prepareStatement("delete from user");
    return ps;
}

하지만 위 분리는 변하지 않는 부분변하는 부분을 감싸고 있어서 먼가 어색하다.

템플릿 메서드 패턴 적용

템플릿 매서드 패턴의 방법인, 변하지 않는 부분(try/catch/finally)은 슈퍼클래스에 두고 변하는 부분(statement)은 추상 메소드로 정의해둬서 서브클래스에서 오버라이드하여 새롭게 정의해 쓰도록 하자.

// ...
public abstract class UserDao {
    // ...
    // executeDao()같은 메소드를 두고, 매번 반복되는 try~finally 구문을 기록하고
    // 그 사이에 이 아래 makeStatement()를 끼워 넣어둔다

    public abstract PreparedStatement makeStatement(Connection c) throws SQLException;
}
// ...
public class UserDaoDeleteAll extends UserDao{
    private PreparedStatement makeStatement(Connection c) throws SQLException{
        PreparedStatement ps;
        ps = c.prepareStatement("delete from user");
        return ps;
    }
}

위 패턴에도 문제점이 있다.

  • 모든 DAO 로직(메소드)마다 상속을 통해 새로운 클래스를 만들어야 된다.
  • 컴파일 시점에 클래스 간(슈퍼-서브) 관계가 결정되어 있어서 유연하지 못하다.

전략 패턴 적용

이제 전략 패턴을 사용하여 변하는 부분을, 아예 별도의 클래스로 만들어 추상화된 인터페이스를 통해 소통하도록 구성한다.
즉 오브젝트를 아예 둘로 분리하고 클래스 래벨에서는 인터페이스를 통해서만 의존하도록 만들자.

// ...
public interface StatementStrategy {
    PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}
// ...
public class DeleteAllStatement implements StatementStrategy {
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        PreparedStatement ps = null;
        ps = c.prepareStatement("delete from users");
        return ps;
    }
}
// ...
public void deleteAll() throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;

    try {
        c = dataSource.getConnection();
        // 아래 코드를 통해 구현 클래스를 결정한다.
        StatementStrategy stmt = new DeleteAllStatement();
        ps = stmt.makePreparedStatement(c);
        ps.executeUpdate();
    } catch (SQLException e) {
        throw e;
    } finally {
        if (ps != null)
            try {
                ps.close();
            } catch (SQLException e) {
            }
        if (c != null) try {
            c.close();
        } catch (SQLException e) {

        }
    }
}

이제는 UserDao 안에서, StatementStrategy를 상속한 클래스들을 이용해 객체를 만들고 똑같이 ps = stmt.makePreparedStatement(c) 구문으로 해결할 수 있다.

하지만 이는 컨텍스트(UserDao)가 무엇을 실행할 지 컴파일 시점에 알고 있다. 템플릿 메소드 패턴과 동일한 문제를 가진 것이다.

이를 해결하기 위해, Context를 사용하는 Client가 전략을 선택하도록 수정하자.

DI 적용을 위한 클라이언트 / 컨텍스트 분리

deleteAll을 하나의 Client로 본다면, 우리는 반복되는 부분을 Context로 뽑아내고, 거기에 전략을 주입해보자.

현재 deleteAll-PreparedStatement가 Context-Strategy의 구조를 보이고 있는 상태이며, 이제부터 할 것은,
deleteAll-jdbc-statement를 Client-Context-Strategy 형태로 바꾸어 나가는 작업이다.

// Client
public void deleteAll() throws SQLException {
    StatementStrategy stmt = new DeleteAllStatement();
    jdbcContextWithStatementStrategy(stmt);
}

// Context
public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException{
    // SQL 쿼리(PreparedStatement)와는 결합도가 낮은 "JDBC 작업 흐름"을 분리해 냄
    Connection c = null;
    PreparedStatement ps = null;

    try {
        c = dataSource.getConnection();

        ps = stmt.makeStatement(c); // 이 부분에서 strategy 사용

        ps.executeUpdate();
    } catch (SQLException e) {
        throw e;
    } finally {
        if (ps != null)
            try {
                ps.close();
            } catch (SQLException e) {
            }
        if (c != null) try {
            c.close();
        } catch (SQLException e) {
        }
    }
}

// Strategy (이전과 바뀐 것 없음)
public interface StatementStrategy {
    PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}

// ...

public class DeleteAllStatement implements StatementStrategy {
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        PreparedStatement ps = null;
        ps = c.prepareStatement("delete from users");
        // add() 같은 경우엔 여기에 ps.setString()같은 추가 작업이 필요함 (아래에서 추가 설명)
        return ps;
    }
}

3.3 JDBC 전략 패턴의 최적화

앞선 코드에는 2가지 문제가 있는데

  • 모든 DAO 메소드마다 새로운 StatementStrategy 구현 클래스를 만들어야 한다.
  • 부가정보를 전달하려면, StatementStrategy 구현체에 생성자, 인스턴스 변수 등등을 추가해주어야 한다.

익명 내부 클래스는 생성자를 구현 할 인터페이스로 대신하여 new 생성자로 만드는 이름 없는 클래스이다.
익명 내부 클래스로 각 StatementStrategy 구현 클래스를 선언하자.

참고 1) 익명 내부 클래스를 사용 할 때, 클래스 밖의 변수는 final 키워드를 사용해야 쓸 수 있다.

참고 2)중첩 클래스(nested class)의 분류

  1. 스태틱 클래스
  2. 내부 클래스
  • 클래스의 내부 클래스 : 스코프가 클래스에 속해있다.
  • 로컬 클래스 : 스코프가 메소드에 속해있다.
  • 익명 내부 클래스 : 선언 위치에 따라 스코프가 다름
public void add(final User user) throws SQLException {
		jdbcContextWithStatementStrategy(new StatementStrategy() {
			@Override
			public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
				PreparedStatement ps = c.prepareStatement(
						"insert into users(id, name, password) values(?, ?, ?)");
				ps.setString(1, user.getId());
				ps.setString(2, user.getName());
				ps.setString(3, user.getPassword());
				return ps;
			}
		});
	}

3.4 컨텍스트와 DI

현재 전략 패턴을 보면 UserDao의 메서드가 Client이며 익명 내부 클래스전략이고 jdbcContextWithStatementStrategy메서드는 Context이다.

jdbcContextWithStatementStrategy는 다른 DAO에서도 사용 가능하므로, UserDao 클래스 밖으로 독립시켜보자.

JdbcContext 클래스 분리

public class JdbcContext {

	private DataSource dataSource;

	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}

	public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException{
		Connection c = null;
		PreparedStatement ps = null;
		...
	}
}

클래스 분리를 위해 생각해야 할 것이 있는데, dataSource이다.
기존까지 jdbcContext 메소드는 지금까지 UserDao 내부에 위치했다. 따라서 그냥, UserDao의 인스턴스 변수 dataSource를 사용하면 되었다.

이제 클래스 분리가 되었으므로 의존성을 주입해 보자.

스프링 빈으로 DI

<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
        <!-- DB 접속 정보 properties -->
    </bean>

    <!-- datasource가 jdbcContext에 주입됨 -->
    <bean id="jdbcContext" class="springbook.user.dao.JdbcContext">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <!-- jdbcContext가 userDao에 주입됨 -->
    <bean id="userDao" class="springbook.user.dao.UserDao">
        <property name="jdbcContext" ref="jdbcContext" />
        <!-- dataSource를 안지우는 것은, 아직 다른 메소드들 리팩토링이 덜 끝나서 -->
        <property name="dataSource" ref="dataSource" />
    </bean>

현재 의존 관계는 다음과 같다 (왼쪽이 오른쪽을 의존 중)

UserDao -> jdbcContext -> dataSource

현재 UserDao는 구체 클래스에 의존하고 있다.

하지만, 스프링의 DI는 기본적으로 인터페이스를 사이에 두고 의존 클래스를 바꿔서 사용하도록 하여 구체적인 의존관계가 만들어지지 않도록 하였다.

그러나 스프링의 DI는 넓게 보자면 객체의 생성과 관계설정에 대한 제어권한을 오브젝트에서 제거하고 외부로 위임한 IOC라는 개념을 포괄하므로, JdbcContext를 스프링을 이용해 UserDao 객체에서 사용하도록 주입했다는 건 DI의 기본을 따르고 있다.

또한, JdbcContext는 그 자체로 구현 바뀔 가능성이 가능성이 거의 없기 때문에 가능하다.

jdbcContext 구체 클래스를 빈으로 등록하는 이유는 다음과 같다.

  1. 싱글톤이 되기에 충분한 조건을 갖추고 있음
    상태 정보를 가지지 않고, JDBC 컨텍스트 메서드를 제공해주는 일종의 서비스 오브젝트로 의미가 있음
    따라서, 여러 오브젝트에서 공유가 가능

  2. JdbcContext가 DI를 통해 다른 빈에 의존 중 DI를 위해서는 주입되는/주입하는 두 오브젝트가 모두 스프링 빈으로 등록돼야 한다

  3. 두 오브젝트 사이의 실제 의존관계가 설정 파일에 명확하게 드러난다.

하지만 구체적인 클래스 간 관계가 컴파일 단에 노출된다. DI의 근본적인 원칙에 부합하지 않는다.

수동 DI

JdbcContext를 스프링의 빈으로 등록하여 UserDaoDI 하는 대신 UserDao에 직접 DI 적용하자 이는 DAO마다 하나의 JdbcContext오브젝트를 갖고 있도록 하자.

JdbcContext 내부에 setDataSource 메소드를 두고, UserDao 생성자를 다음과 같이 수정하여 구현한다.

public class UserDao {
    JdbcContext jdbcContext;
    DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
        this.jdbcContext = new JdbcContext();
        this.jdbcContext.setDataSource(dataSource);
    }

    // ...
}

수동 DI를 통해 다음과 같은 장점이 있다.

  • 인터페이스도 없는 긴밀한 관계를 같은 두 객체를 어색하게 빈으로 분리하지 않는다.

  • 내부에서 직접 만들어 사용하며 다른 오브젝트에 대한 DI를 적용할 수 있다.

  • 두 클래스 사이 관계가 노출되지 않는다.

하지만 싱글톤으로 사용이 불가하고, DI를 위한 추가 코드가 발생했다는 단점이 있다.

댓글남기기