728x90

지금까지 테스트를 실행하면서 가장 불편한 점은, 매번 UserDaoTest 태스트를 실행하기 위해 직접 데이터베이스를 모두 비워줬던 것이다.

그냥 실행하면 기본키가 충돌하기 때문이다.

 

테스트가 외부 상태에 따라 성공하기도 하고 실패하기도 하는 것이 좋은 방법일까?

당연히 테스트는 별도의 준비 작업 없이 반복적으로 수행해도 일관되게 성공해야 한다.

 

그렇기에 테스트를 마치고 나면 테스트에서 삽입한 데이터를 삭제하여, 테스트를 수행하기 이전 상태로 만들어줘야 한다.

일단 UserDao에 다음과 같은 메서드를 추가해보자.

 

    public void deleteAll() throws SQLException, ClassNotFoundException {
        Connection connection = connectionHelper.makeConnection();

        PreparedStatement preparedStatement = connection.prepareStatement("delete from users");

        preparedStatement.executeUpdate();

        preparedStatement.close();
        connection.close();
    }

    public int getCount() throws ClassNotFoundException, SQLException {
        Connection connection = connectionHelper.makeConnection();

        PreparedStatement preparedStatement = connection.prepareStatement("select count(*) from users");

        var resultSet = preparedStatement.executeQuery();

        resultSet.next();

        int count = resultSet.getInt(1);

        resultSet.close();
        preparedStatement.close();
        connection.close();

        return count;
    }

 

그리고는 테스트에 다음과 같이 추가해본다.

class UserDaoTest {

    @Test
    public void addAndGet() throws SQLException, ClassNotFoundException {

        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

        UserDao userDao = context.getBean("userDao", UserDao.class);

        userDao.deleteAll();
        Assertions.assertEquals(0, userDao.getCount());

        User createdUser = new User();
        createdUser.setId("seungkyu");;
        createdUser.setName("승규");
        createdUser.setPassword("password");

        userDao.add(createdUser);
        Assertions.assertEquals(1, userDao.getCount());

        User readUser = userDao.get("seungkyu");

        Assertions.assertEquals(createdUser.getId(), readUser.getId());
        Assertions.assertEquals(createdUser.getName(), readUser.getName());
        Assertions.assertEquals(createdUser.getPassword(), readUser.getPassword());
    }

}

 

이 테스트가 좋은 테스트는 아니라고 생각한다.

하나의 테스트에서 너무 많은 메서드를 테스트한다.

만약 테스트를 작성하면 이렇게 너무 많은 메서드를 하나의 테스트에서 진행하지는 말도록 하자.

이런 경우에는 어디서 문제가 생겼는지 파악하기 힘들어진다.

 

이제 테스트를 반복 실행해보아도, 동일하게 성공하는 것을 볼 수 있다.

 

getCount() 메서드를 테스트에 적용했지만, 기존의 테스트에서 확인할 수 있었던 것은 deleteAll()을 실행했을 때 테이블이 비어있는 경우와 add()를 한번 호출한 뒤의 결과만 테스트했다.

2개 이상의 add()도 물론 잘 작동하겠지만, 그래도 테스트 해보는 것이 더 좋은 자세이다.

 

그렇기에 다른 메서드부터 차례로 꼼꼼하게 테스트 해보도록 하자.

여러개의 User를 등록하며 getCount()를 호출하는 것이다.

    @Test
    public void count() throws SQLException, ClassNotFoundException {
        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

        UserDao userDao = context.getBean("userDao", UserDao.class);

        userDao.deleteAll();

        for(int i = 0; i < 3; i++){
            User user = new User();

            user.setId(UUID.randomUUID().toString().substring(0, 6));
            user.setName(UUID.randomUUID().toString().substring(0, 6));
            user.setPassword(UUID.randomUUID().toString().substring(0, 6));

            userDao.add(user);

            Assertions.assertEquals(i + 1, userDao.getCount());
        }
    }

데이터를 모두 지우고 하나씩 추가하며 개수를 세어보니, 잘 동작하는 것을 볼 수 있다.

이렇게 get() 메서드가 제대로 동작한다고 믿을 수 있게 되었다.

 

만약 아이디가 충돌해서 에러가 생긴다면 어떨까?

이렇게 말이다.

당연히 

 

에러가 발생해도 테스트를 통과시킬 수 있도록 하는 방법이 있다.

    @Test
    public void countWithDuplicate() throws SQLException, ClassNotFoundException {
        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

        UserDao userDao = context.getBean("userDao", UserDao.class);

        userDao.deleteAll();

        String name = UUID.randomUUID().toString().substring(0, 6);

        User user = new User();

        user.setId(name);
        user.setName(UUID.randomUUID().toString().substring(0, 6));
        user.setPassword(UUID.randomUUID().toString().substring(0, 6));
        userDao.add(user);

        for(int i = 0; i < 2; i++){

            user.setId(name);
            user.setName(UUID.randomUUID().toString().substring(0, 6));
            user.setPassword(UUID.randomUUID().toString().substring(0, 6));

            Assertions.assertThrows(SQLIntegrityConstraintViolationException.class,
                    () -> userDao.add(user));

            Assertions.assertEquals(1, userDao.getCount());
        }
    }

대신 기대했던 에러가 발생하지 않는다면, 해당 테스트는 실패한다.

예외가 반드시 발생하는 경우에 사용할 수 있다.

 

이제 마지막으로 할 일은 get() 메서드의 테스트를 완료하는 것이다.

우선 get()에서 유저가 존재하지 않는다면 NPE를 발생하도록 수정했다.

public User get(String id) throws ClassNotFoundException, SQLException {

        Connection connection = connectionHelper.makeConnection();

        PreparedStatement preparedStatement = connection.prepareStatement(
                "select * from users where id = ?"
        );

        preparedStatement.setString(1, id);

        var resultSet = preparedStatement.executeQuery();

        User user = null;

        if(resultSet.next()){

            user = new User();

            user.setId(resultSet.getString("id"));
            user.setName(resultSet.getString("name"));
            user.setPassword(resultSet.getString("password"));
        }

        resultSet.close();
        preparedStatement.close();
        connection.close();

        if(user == null){
            throw new NullPointerException("user is null");
        }

        return user;
    }

그리고 없는 계정으로 조회할 때 에러가 잘 발생하는지 테스트를 해보았다.

 

    @Test
    public void get() throws SQLException, ClassNotFoundException {
        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

        UserDao userDao = context.getBean("userDao", UserDao.class);

        Assertions.assertThrows(NullPointerException.class, () -> userDao.get(UUID.randomUUID().toString().substring(0, 6)));
    }

 

자 이제 테스트코드를 리펙토링 해보자.

지금 테스트 코드에서는 데이터베이스에 연결하는 공통적인 코드를 가지고 있다.

 

항상 테스트의 시작 부분에서 작성된 코드이다.

ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao userDao = context.getBean("userDao", UserDao.class);
userDao.deleteAll();

 

이 부분을 추출할 수는 없을까?

당연히 가능하다.

JUnit에서는 공통 시작 부분을 추출할 수 있도록 @BeforeEach annotation을 지원한다.

해당 클래스의 모든 테스트에서 시작 전에 수행되어야 할 명령어를 명시해두면 된다.

 

    @BeforeEach
    public void setUp() throws SQLException, ClassNotFoundException {
        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
        this.userDao = context.getBean("userDao", UserDao.class);
        userDao.deleteAll();
    }

 

당연히 메서드의 종료 후 동작할 명령어도 명시할 수 있다.

    @AfterEach
    public void tearDown(){
        System.out.println("테스트 종료");
    }

 

이렇게 테스트를 수행할 때에는 JUnit에서 각각의 테스트 메서드에 대하여, 각각의 오브젝트가 생성된다고 한다. 그렇기 때문에 각 테스트끼리는 정보를 인스턴스를 제외하고는 주고받을 수 없다.

 

 

728x90

우선 테스트 결과를 검증하는 부분을 자동화로 만들어보자.

이 테스트에서는 add()로 전달한 사용자의 정보와 get()으로 조회하는 사용자의 정보가 일치하는지 검증한다.

 

기존에는 단순하게 로그를 확인해 테스트의 결과를 확인했다면, 이것을 코드로 성공과 실패여부를 출력하도록 만들어보자.

 

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {

        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

        UserDao userDao = context.getBean("userDao", UserDao.class);


        User createdUser = new User();
        createdUser.setId("seungkyu");;
        createdUser.setName("승규");
        createdUser.setPassword("password");

        userDao.add(createdUser);

        System.out.println(createdUser.getId() + "계정을 등록했습니다.");

        User readUser = userDao.get("seungkyu");

        if(!readUser.getName().equals(createdUser.getName()))
        {
            System.out.println("테스트 실패 - name");
        }
        else if(!readUser.getPassword().equals(createdUser.getPassword()))
        {
            System.out.println("테스트 실패 - password");
        }
        else
        {
            System.out.println("조회 테스트 성공");
        }
    }
}

 

이렇게 결과를 비교하여 테스트의 결과를 확인하고, 그에 맞는 결과를 출력한다.

이러면 출력된 로그에 따라 적절한 대응만 하면 된다.

 

굉장히 좋은 테스트이다.

하지만 main() 메서드를 이용한 테스트 작성법으로는 규모가 커지고 테스트의 수가 많아지면, 부담이 될 것이다.

 

그렇기에 JUnit을 사용해본다.

JUnit은 프레임워크로 IoC를 사용해 main() 메서드에 직접 제어권을 작성해 작성할 필요가 없도록 한다.

 

class UserDaoTest {

    @Test
    public void addAndGet() throws SQLException, ClassNotFoundException {
    	//테스트 코드
    }
}

 

이렇게 test 패키지에 test관련 종속성을 추가하고, 해당 메서드에 @Test annotation을 추가한 후 코드를 옮겨적어주면 된다.

그러면 이렇게 초록색으로 결과가 나오게된다.

 

다른 방법으로 검증 할 수는 없을까?

지금 이 방법도 결국 우리가 눈으로 로그를 확인해야 하는 것이다.

JUnit에서는 이런 검증을 할 수 있도록 matcher를 제공한다.

첫번째와 두번째의 파라미터를 비교하여 결과를 만들어주는 기능이다.

 

    @Test
    public void addAndGet() throws SQLException, ClassNotFoundException {

        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

        UserDao userDao = context.getBean("userDao", UserDao.class);

        User createdUser = new User();
        createdUser.setId("seungkyu");;
        createdUser.setName("승규");
        createdUser.setPassword("password");

        userDao.add(createdUser);

        System.out.println(createdUser.getId() + "계정을 등록했습니다.");

        User readUser = userDao.get("seungkyu");

        Assertions.assertEquals(createdUser.getId(), readUser.getId());
        Assertions.assertEquals(createdUser.getName(), readUser.getName());
        Assertions.assertEquals(createdUser.getPassword(), readUser.getPassword());
    }

 

이렇게 Assertions에서 제공하는 메서드들로 값을 검증하면 된다.

 

만약 실패한다면

이런 식으로 결과가 나오게 된다.

 

일단 간단하게 테스트를 알아보았고, 다음부터 JUnit에 대해서 더 깊게 공부해보도록 하자

728x90

이번 챕터의 주제는 테스트이다.

개발을 하고 1년이 지난 시점부터 굉장히 중요하다고 생각했던 주제이다.

 

기존에 작정한 UserDao는 데이터를 메인 메서드에서 데이터를 삽입해보고 정상적으로 들어갔는지 확인하는 방법이다.

 

  • 웹을 통한 DAO 테스트 방법의 문제점

기존에 웹 프로그램에서 사용하는 테스트 방법은 모든 입출력 기능을 대충이라도 만들고 테스트 웹 애플리케이션을 배치한 뒤, 다시 데이터를 조회하는 URL을 만들어서 제대로 들어갔는지 확인하는 방법이라고 한다.

당연히 이러한 방법은 큰 문제들이 있다.

JSP, API까지 모두 만든 후에나 테스트가 가능하다는 단점이 가장 치명적이라고 한다. 또한, 버그가 발생하면 어느 부분에서 버그가 발생했는지 찾아야 한다고 한다.

 

  • 작은 단위의 테스트

그렇기 때문에 테스트를 작은 단위로 나누고, 해당 대상에만 집중해서 테스트를 해야한다고 한다. 너무 많은 로직들이 모여있으면 정확한 원인을 찾기 힘들기 때문이다. 그렇기에 테스트의 관심이 다르다면 테스트 할 대상을 분리하고 집중해서 접근해야 한다. 그리고 이렇게 작은 단위테스트를 진행하는 이유는 개발자가 작성한 코드를 최대한 빠르게 검증받기 위해서다.

 

  • 자동 수행 테스트 코드

기존에 사용하던 UserDaoTest는 테스트가 코드를 통해 자동으로 실행되고 있었다. 웹을 통해 데이터를 계속 넣어줄 필요가 없다는 말이다.

이렇게 하나하나 데이터를 넣으면서 테스트 하도록 만드는 것이 아니라, 코드를 통해서 자동으로 테스트가 진행되도록 만들어야 한다. 빠르고 지속적으로 기존에 만들었던 코드들을 검증해야 하기 때문이다.

 

  • 지속적인 개선과 점진적인 개발을 위한 테스트

UserDaoTest는 처음에 무식하고 단순한 방법으로 정상적으로 동작하는 코드를 만들고, 지속적으로 테스트를 진행하며 코드를 검증했다.

그렇기에 코드를 리펙토링하면서 발생하는 에러들을 빠르게 검증 할 수 있었다. 이렇게 테스트를 이용하면 새로운 기능도 기대한 대로 동작하는지 확인할 수 있을 뿐 아니라, 기존에 만든 기능을 검증할 수 있는 것이다.

 

하지만 기존에 작성한 UserDaoTest가 완벽한 것은 아니다. 테스트는 자동으로 동작하지만, 검증은 데이터베이스와 로그를 확인해야 한다는 것이다. 그리고 main() 메서드라고 하더라도 매번 그것을 실행해야 하는 것이다. 이런 방법보다 더 편리하고 체계적인 테스트가 필요하다.

728x90

스프링에서는 DaoFactory와 같은 자바 클래스를 이용하는 거 말고도 XML을 이용해 DI를 위한 의존관계 정보를 담을 수 있다고 한다.

 

우선 기존의 DaoFactory에서 얻을 수 있는 빈 DI의 정보는 다음 3가지였다.

 

  • 빈의 이름: @Bean 메서드의 이름이 빈의 이름이다. getBean()에서 사용된다.
  • 빈의 클래스: 빈 오브젝트를 어떤 클래스를 이용해서 만들지를 정의한다.
  • 빈의 의존 옵젝트: 빈의 생성자나 수정자 메서드를 통해 의존 오브젝트를 넣어준다. 의존 오브젝트도 하나의 빈이기에 이름이 있을 것이고, 그 이름에 해당하는 메서드를 호출해서 의존 오브젝트를 가져온다.

XML은 그렇다고 해서 자바 코드처럼 유연하게 정의될 수 있는 것은 아니기에, 핵심 요소를 잘 짚어서 태그와 속성을 알아야 한다고 한다.

 

클래스에서와 XML에서의 설정은 다음과 같이 대응된다.

  자바 코드 설정정보 XML 설정정보
빈 설정파일 @Configuration <beans>
빈의 이름 @Bean
methodName()
<bean id="methodName">
빈의 클래스 return new BeanClass(); class = "a.b.c...BeanClass">

 

기존에 작성했던 코드를 대응해보면

@Bean // <bean
public ConnectionHelper connectionHelper(){ // id = "connectionHelper"
    return new SeungkyuConnection(); // class = "package...SeungkyuConnection"/>
}

 

XML에서는 property 태그를 통해 의존 오브젝트와의 관계를 정의한다.
<property> 태그를 통해 관계를 정의하는데, name은 프로퍼티의 이름을 정의하고 ref는 수정자 메서드를 통해 주입해줄 오브젝트의 빈 이름이다.

수정자를 통해 주입을 하게 된다.

 

만약 다음과 같이 코드를 작성했다고 하자.

public class UserService {
    private String name;

    public void setName(String name) {
        this.name = name;
    }

    public void printName() {
        System.out.println("User name: " + name);
    }
}

 

public class UserController {
    private UserService userService;

    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    public void handleRequest() {
        userService.printName();
    }
}

 

그리고 UserController에서 UserService를 사용하기 위해 setter로 주입을 받으려고 한다면 XML을 다음과 같이 작성해준다.

<bean id="userService" class="com.example.UserService">
    <property name="name" value="Seungkyu" />
</bean>

<bean id="userController" class="com.example.UserController">
    <property name="userService" ref="userService" />
</bean>

 

이제 애플리케이션 컨텍스트가 DaoFactory 대신 XML 설정 정보를 활용하도록 만들어보자.

XML에서 빈의 의존관계 정보를 이용하는 IoC/DI 작업에는 GenericXmlApplicationContext를 사용한다.

 

<?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">

    <!-- ConnectionHelper 타입의 Bean 등록 -->
    <bean id="connectionHelper" class="seungkyu.SeungkyuConnection" />

    <!-- UserDao Bean 등록 및 connectionHelper 주입 -->
    <bean id="userDao" class="seungkyu.UserDao">
        <constructor-arg ref="connectionHelper" />
    </bean>

</beans>

 

다음과 같이 applicationContext.xml을 작성하고 resources 폴더에 넣어준다.

 

그리고 DaoFactory에서 @Configuration과 @Bean을 제거해준다.

Annotation이 아닌 XML을 통해서 Bean을 주입하기 때문이다.

 

ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

UserDao userDao = context.getBean("userDao", UserDao.class);

 

그리고 이렇게 Bean을 가져오면 정상적으로 작동하는 것을 볼 수 있다.

 

근데 알겠지만 이미 스프링에는 데이터베이스의 연결정보를 관리하는 DataSourceProperties라는 클래스가 존재한다.

@ConfigurationProperties("spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {

	private ClassLoader classLoader;

	private boolean generateUniqueName = true;

	private String name;

	private Class<? extends DataSource> type;

	private String driverClassName;

	private String url;

	private String username;

	private String password;

	private String jndiName;

	private EmbeddedDatabaseConnection embeddedDatabaseConnection;

	private Xa xa = new Xa();

	private String uniqueName;

	@Override
	public void setBeanClassLoader(ClassLoader classLoader) {
		this.classLoader = classLoader;
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		if (this.embeddedDatabaseConnection == null) {
			this.embeddedDatabaseConnection = EmbeddedDatabaseConnection.get(this.classLoader);
		}
	}

	public DataSourceBuilder<?> initializeDataSourceBuilder() {
		return DataSourceBuilder.create(getClassLoader())
			.type(getType())
			.driverClassName(determineDriverClassName())
			.url(determineUrl())
			.username(determineUsername())
			.password(determinePassword());
	}

	public boolean isGenerateUniqueName() {
		return this.generateUniqueName;
	}

	public void setGenerateUniqueName(boolean generateUniqueName) {
		this.generateUniqueName = generateUniqueName;
	}

	public String getName() {
		return this.name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Class<? extends DataSource> getType() {
		return this.type;
	}

	public void setType(Class<? extends DataSource> type) {
		this.type = type;
	}

	/**
	 * Return the configured driver or {@code null} if none was configured.
	 * @return the configured driver
	 * @see #determineDriverClassName()
	 */
	public String getDriverClassName() {
		return this.driverClassName;
	}

	public void setDriverClassName(String driverClassName) {
		this.driverClassName = driverClassName;
	}

	public String determineDriverClassName() {
		String driverClassName = findDriverClassName();
		if (!StringUtils.hasText(driverClassName)) {
			throw new DataSourceBeanCreationException("Failed to determine a suitable driver class", this,
					this.embeddedDatabaseConnection);
		}
		return driverClassName;
	}

	String findDriverClassName() {
		if (StringUtils.hasText(this.driverClassName)) {
			Assert.state(driverClassIsLoadable(), () -> "Cannot load driver class: " + this.driverClassName);
			return this.driverClassName;
		}
		String driverClassName = null;
		if (StringUtils.hasText(this.url)) {
			driverClassName = DatabaseDriver.fromJdbcUrl(this.url).getDriverClassName();
		}
		if (!StringUtils.hasText(driverClassName)) {
			driverClassName = this.embeddedDatabaseConnection.getDriverClassName();
		}
		return driverClassName;
	}

	private boolean driverClassIsLoadable() {
		try {
			ClassUtils.forName(this.driverClassName, null);
			return true;
		}
		catch (UnsupportedClassVersionError ex) {
			// Driver library has been compiled with a later JDK, propagate error
			throw ex;
		}
		catch (Throwable ex) {
			return false;
		}
	}

	public String getUrl() {
		return this.url;
	}

	public void setUrl(String url) {
		this.url = url;
	}

	public String determineUrl() {
		if (StringUtils.hasText(this.url)) {
			return this.url;
		}
		String databaseName = determineDatabaseName();
		String url = (databaseName != null) ? this.embeddedDatabaseConnection.getUrl(databaseName) : null;
		if (!StringUtils.hasText(url)) {
			throw new DataSourceBeanCreationException("Failed to determine suitable jdbc url", this,
					this.embeddedDatabaseConnection);
		}
		return url;
	}

	public String determineDatabaseName() {
		if (this.generateUniqueName) {
			if (this.uniqueName == null) {
				this.uniqueName = UUID.randomUUID().toString();
			}
			return this.uniqueName;
		}
		if (StringUtils.hasLength(this.name)) {
			return this.name;
		}
		if (this.embeddedDatabaseConnection != EmbeddedDatabaseConnection.NONE) {
			return "testdb";
		}
		return null;
	}
	public String getUsername() {
		return this.username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String determineUsername() {
		if (StringUtils.hasText(this.username)) {
			return this.username;
		}
		if (EmbeddedDatabaseConnection.isEmbedded(findDriverClassName(), determineUrl())) {
			return "sa";
		}
		return null;
	}

	public String getPassword() {
		return this.password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public String determinePassword() {
		if (StringUtils.hasText(this.password)) {
			return this.password;
		}
		if (EmbeddedDatabaseConnection.isEmbedded(findDriverClassName(), determineUrl())) {
			return "";
		}
		return null;
	}

	public String getJndiName() {
		return this.jndiName;
	}

	public void setJndiName(String jndiName) {
		this.jndiName = jndiName;
	}

	public EmbeddedDatabaseConnection getEmbeddedDatabaseConnection() {
		return this.embeddedDatabaseConnection;
	}

	public void setEmbeddedDatabaseConnection(EmbeddedDatabaseConnection embeddedDatabaseConnection) {
		this.embeddedDatabaseConnection = embeddedDatabaseConnection;
	}

	public ClassLoader getClassLoader() {
		return this.classLoader;
	}

	public Xa getXa() {
		return this.xa;
	}

	public void setXa(Xa xa) {
		this.xa = xa;
	}

	public static class Xa {

		/**
		 * XA datasource fully qualified name.
		 */
		private String dataSourceClassName;

		/**
		 * Properties to pass to the XA data source.
		 */
		private Map<String, String> properties = new LinkedHashMap<>();

		public String getDataSourceClassName() {
			return this.dataSourceClassName;
		}

		public void setDataSourceClassName(String dataSourceClassName) {
			this.dataSourceClassName = dataSourceClassName;
		}

		public Map<String, String> getProperties() {
			return this.properties;
		}

		public void setProperties(Map<String, String> properties) {
			this.properties = properties;
		}

	}

	static class DataSourceBeanCreationException extends BeanCreationException {

		private final DataSourceProperties properties;

		private final EmbeddedDatabaseConnection connection;

		DataSourceBeanCreationException(String message, DataSourceProperties properties,
				EmbeddedDatabaseConnection connection) {
			super(message);
			this.properties = properties;
			this.connection = connection;
		}

		DataSourceProperties getProperties() {
			return this.properties;
		}

		EmbeddedDatabaseConnection getConnection() {
			return this.connection;
		}
	}
}

 

JPA를 사용했다면 여기에 username과 password, url의 정보를 입력한 경험이 있을 것이다.

이런 값들은 어떻게 주입을 하는걸까?

 

당연히 수정자를 통해 주입을 하는 것이고, 수정자 메서드에는 다른 빈이나 오브젝트 뿐만 아니라 스트링과 같은 단순 값을 넣어줄 수도 있다.

 

만약 기존에 다음과 같이 DB 연결정보를 주입받고 있었다면

dataSource.setDriverClass(com.mysql.jdbc.Driver.class);
dataSource.setUrl("대충 URL");
dataSource.setUsername("seungkyu");
dataSource.setPassword("1234);

 

다음과 같이 XML을 통해서 주입 할 수 있는 것이다.

<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/test"/>
<property name="username" value="seungkyu"/>
<property name="password" value="1204"/>

그렇기에 우리는 지금까지 자바 코드를 수정하지 않고도 데이터베이스의 연결 정보를 바꿀 수 있었던 것이다.

 

여기에서 driverClass 타입은 단순 문자열이 아닌 클래스 타입이다.

이렇게 클래스를 넣을 수 있는 이유도 setDriverClass() 메서드의 파라미터 타입을 참고로 적절한 형태로 변환해주기 때문이다.

com.mysql.jdbc.Driver을 com.mysql.jdbc.Driver.class의 오브젝트로 자동 변경해주는 것이다.

 

내부적으로는 다음과 같은 작업이 일어난다고 한다.

Class driverClass = Class.forName("com.mysql.jdbc.Driver");
dataSource.setDriverClass(driverClass);

 

이렇게 XML을 통해 Bean을 주입하는 방법을 알아보았다.

728x90

우선 의존관계부터 알아보자.

이렇게 A가 B를 사용하면 A가 B에게 의존하는 것이다.

여기서 의존한다는게 무슨 의미일까?

 

B가 변하면 A에게 영향을 미친다는 것이다.

B의 기능이 변화하면 당연히 B를 사용하는 A에게 영향을 미친다. 여기서 A가 변화더라도 B는 아무 영향을 받지 않는다. A를 전혀 사용하고 있지 않기 때문이다.

이런 관계 때문에 의존관계는 방향성도 존재한다.

 

지금의 UserDao가 이렇게 의존하고 있다. 

UserDao가 내부에서 이렇게 ConnectionHelper를 사용하기 때문이다.

 

하지만 여기서는 클래스가 아닌 인터페이스에 의존하고 있다.

이렇게 만들어야 관계가 느슨해지면서 변화에 영향을 덜 받게 된다.

 

런타임시에 의존관계가 만들어지는 경우도 있다.

UserDao와 ConnectionMaker등의 설계와 코드에서 드러나지 않는다는 말이다.

 

프로그램이 시작되고 UserDao 오브젝트가 만들어지고 나서 런타임 시에 의존관계를 맺는 대상을 의존 오브젝트라고 말한다.

 

의존관계 주입은 이렇게 구체적인 의존 오브젝트와 그것을 사용할 주체, 보통 클라이언트라고 부르는 오브젝트를 런타임 시에 연결해주는 작업을 말한다.

 

정리하면 의존관계 주입은 다음과 같은 세가지 조건을 충족하는 작업을 말한다.

  • 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스에만 의존하고 있어야 한다.
  • 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제 3의 존재가 결정한다.
  • 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공해줌으로써 만들어진다.

의존관계 주입은 설계 시점에서 알지 못했던 두 오브젝트가 관계를 맺도록 제 3의 존재가 도와주고, 이걸 IoC 컨테이너 같은 존재가 해준다고 볼 수 있다.

 

public class UserDao {

    private ConnectionHelper connectionHelper;

    public UserDao(ConnectionHelper connectionHelper) {
        this.connectionHelper = connectionHelper;
    }
}

 

이렇게 의존관계를 주입해주는 것이다.

코드를 작성하는 과정에서는 인터페이스로만 작업을 하고, 런타임시에 ConnectionHelper를 구현한 구현체를 생성자를 통해 주입해주는 것이다.

 

이렇게 주입하는 것이 아니라, 스스로 검색을 해서 관계를 맺어주는 의존관계 검색이라고 불리는 것도 있다.

의존관계 검색은 자신이 필요로하는 의존 오브젝트를 능동적으로 찾는다.

 

다음과 같은 UserDao 생성자는 의존관계 검색을 사용하는 것이다.

    public UserDao(){
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
        this.connectionHelper = context.getBean("connectionHelper", ConnectionHelper.class);
    }

 

이렇게 이름과 타입을 사용하여 원하는 의존관계를 가져오는 것이다.

 

자 이제 기존의 문제였던 상황을 살펴보자.

기존에는 데이터베이스의 연결 정보를 변경하기 위해서, 코드가 작성된 모든 부분을 수정했을 것이다.

하지만 DI 방식을 사용해 다음과 같이 구현했다면

@Bean
public ConnectionMaker connectionMaker(){
	return new LocalDBConnectionMaker();
}

 

이제 이 연결 정보를 변경하기 위해서, 이렇게 한 줄만 수정하게 될 것이다.

@Bean
public ConnectionMaker connectionMaker(){
	return new ChangedDBConnectionMaker();
}

 

 

이런 의존관계 주입에는 3가지 방법이 있다.

  • 생성자 주입
  • 필드 주입
  • setter 주입

이 중에서 가장 권장되는 방식은 생성자 주입이라고 한다.(스프링의 공식 의견이라고...)

728x90

@Configuration을 사용하는 것과 그냥 팩토리를 사용하는 것과 다른 것이 없어보인다.

 

만약 팩토리를 사용하여 userDao() 메서드를 여러번 호출한다면 동일한 오브젝트가 돌아올까?

당연히 아니고, 매번 새로운 오브젝트가 생성되어 다른 오브젝트가 만들어진다.

 

하지만 애플리케이션 컨텍스트에서 getBean()을 사용해 가져오는 객체는 모두 동일한 오브젝트를 가져오게 된다.

싱글톤으로 저장하고 관리하기 때문이다.

 

왜 스프링에서는 빈을 싱글톤으로 관리할까?

스프링은 대규모의 요청을 처리하기 위해 만들어졌다.

만약 싱글톤을 사용하지 않는다면 요청마다 오브젝트를 생성해야 하고, 생성에도 비용이 드는 것이 아니라 가비지 컬렉션에도 큰 비용이 들게 될 것이다.

그렇기에 하나의 오브젝트만 생성하고, 해당 오브젝트를 공유하는 싱글톤 패턴을 사용하는 것이다.

 

하지만 기존의 싱글톤 패턴은 약간의 문제점이 있었다. (상속 받으면 기본 생성자가 private이다 등등..)

그렇기에 평범한 자바 클래스를 이런 싱글톤으로 활용할 수 있도록 스프링이 기능을 제공한다.

 

하지만 자바는 멀티 스레드 환경이다.

이러한 상황에서 싱글톤을 사용하게 된다면 인스턴스 변수의 사용에 큰 문제가 발생할 수도 있다.

 

기존의 코드를 다음과 같이 바꿔보자.

package seungkyu;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class UserDao {

    private final ConnectionHelper connectionHelper;
    
    private Connection c;
    private User user;

    public UserDao(ConnectionHelper connectionHelper) {
        this.connectionHelper = connectionHelper;
    }

    public User get(String id) throws ClassNotFoundException, SQLException {

//        Connection connection = connectionHelper.makeConnection();
        this.c = connectionHelper.makeConnection();
        PreparedStatement preparedStatement = c.prepareStatement(
                "select * from users where id = ?"
        );

        preparedStatement.setString(1, id);

        var resultSet = preparedStatement.executeQuery();

        resultSet.next();

        this.user = new User();
        this.user.setId(resultSet.getString("id"));
        this.user.setName(resultSet.getString("name"));
        this.user.setPassword(resultSet.getString("password"));

        resultSet.close();
        preparedStatement.close();
        c.close();

        return user;
    }
}

 

이러면 UserDao의 User를 공유하게 되기에 해당 메서드를 사용하는 중간에 다른 메서드가 끼어든다면, 내용이 바뀔 수도 있다는 문제가 있다.

 

그렇기에 싱글톤을 사용할 때 다른 싱글톤 빈을 저장하는 용도나 final이라면 인스턴스 변수를 사용해도 괜찮지만, 수정이 될 수 있는 경우에는 인스턴스 변수를 사용하지 않는것이 좋다.

 

 

728x90

스프링에서는 스프링이 제어권을 가지고 직접 만들며 관계를 부여하는 오브젝트를 빈이라고 부른다.

 

자바빈과 비슷한 오브젝트 단위이지만, 스프링 빈은 여기에 스프링 컨테이너가 생성하며 관계설정, 사용등을 제어해주는 제어의 역전이 적용된 오브젝트를 말한다.

 

스프링의 빈 팩토리가 사용할 수 있는 설정정보를 만들어보자.

@Configuration
public class DaoFactory {

    @Bean
    public UserDao userDao(){
        return new UserDao(connectionHelper());
    }

    @Bean
    public ConnectionHelper connectionHelper(){
        return new SeungkyuConnection();
    }
}

 

스프링이 빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스라고 인식할 수 있도록 @Configuration annotation을 달아야 한다.

 

그리고 이 안의 userDao() 메서드는 UserDao 타입 오브젝트를 생성하고 초기화해서 돌려주는 것이기에 @Bean이 붙는다.

다른 부분도 마찬가지로 @Bean을 설정해준다.

 

이 두가지의 annotation 만으로도 애플리케이션 컨텍스트가 IoC 방식을 제공할 수 있게 된다.

 

 

이제 DaoFactory를 설정정보로 사용하는 애플리케이션 컨텍스트를 만들어보자.

ApplicationContext를 구현한 클래스 중에서 Annotation을 사용했기에, AnnotationConfigApplicationContext를 사용했다.

 

ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);

UserDao userDao = context.getBean("userDao", UserDao.class);

 

해당 방법으로 Configuration을 ApplicationContext에 등록하고 getBean을 사용해 해당 userDao의 이름의 빈을 UserDao.class로 가져오는 것이다.

 

같은 타입의 메서드가 여러개 지정되어 있다면, 이 이름을 통해서 빈을 가져올 수 있다.

 

이렇게 스프링의 ApplicationContext로 기존의 오브젝트 팩토리를 대체했다.

 

ApplicationContext는 DaoFactory 클래스를 설정정보로 등록해두고 @Bean이 붙은 메서드의 이름을 가져와 빈 목록을 만들어둔다.

클라이언트가 애플리케이션 컨텍스트의 getBean() 메서드를 호출하면 자신의 빈 목록에서 요청한 이름이 있는지 찾고, 있다면 빈을 생성하는 메서드를 호출해서 오브젝트를 생성시킨 후 클라이언트에 돌려준다.

 

이렇게 IoC 기능으로 ApplicationContext를 사용하면 다음과 같은 장점이 있다.

 

  • 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다.

프로젝트가 커져가며 Factory들도 점차 많아질텐데, 그러면 클라이언트가 필요한 오브젝트를 가져오기 위해 Factory 클래스들을 찾아야 한다는 문제가 있다. 이를 ApplicationContext에 등록하면 일관된 방식으로 원하는 오브젝트를 가져올 수 있다.

 

  • ApplicationContext는 종합 IoC 서비스를 제공해준다.

ApplicationContext는 단순하게 오브젝트와 관계설정에만 관여하는 것이 아니다. 오브젝트가 만들어지는 방식, 시점과 전략을 다르게 가져갈 수 있다고 한다. 이를 통해 오브젝트를 효과적으로 활용할 수 있는 다양한 기능을 제공한다.

 

  • 애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공한다.

애플리케이션 컨텍스트의 getBean() 메서드는 빈의 이름을 이용해 빈을 찾아주기에, 특별한 설정이 되어 있는 빈도 찾을 수 있다.

 

이제 스프링의 IoC 용어를 정리하고 마무리해보겠다.

 

  • 빈(Bean)

빈은 스프링 IoC 방식으로 관리하는 오브젝트라는 뜻이다. 스프링을 사용하는 애플리케이션에서 만들어지는 모든 오브젝트가 빈은 아니다. 그 중에서 스프링이 직접 생성과 제어를 담당하는 오브젝트만을 빈이라고 부른다.

 

  • 빈 팩토리(Bean factory)

스프링의 IoC를 담당하는 핵심 컨테이너를 가리킨다. 빈을 등록하고, 생성하고, 조회하고 돌려주고 그 외의 빈을 관리하는 기능을 담당한다. 하지만 보통은 이를 확장한 ApplicationContext를 사용한다.

 

  • 애플리케이션 컨텍스트(Application Context)

빈 팩토리를 확장한 IoC 컨테이너이다. 기본적인 기능은 빈 팩토리와 동일하며, 여기에 스프링이 제공하는 각종 부가 서비스를 추가로 제공한다. 그렇기에 스프링의 기능을 모두 포함하여 이야기 할 때 Application Context라고 말한다.

 

  • 설정정보(Configuration metadata)

빈 팩토리가 IoC를 적용하기 위해 사용하는 메타정보를 말한다. 컨테이너에 어떤 기능을 세팅하거나 조정하는 경우에도 사용하지만, IoC 컨테이너에 의해 관리되는 애플리케이션 오브젝트를 생성하고 구성할 때도 사용된다.

 

  • 컨테이너

IoC 방식으로 빈을 관리한다는 의미에서 애플리케이션 컨텍스트나 빈 팩토리를 컨테이너라고 한다. 

728x90

책을 읽어봤지만, IOC에 대해 바로 이해할 수는 없어서 자료들을 많이 찾아보고 해당 자료대로 작성해보려 한다.

 

기존에는 개발자가 작성한대로 제어의 흐름이 진행되었다.

이것을 프렘워크가 담당하도록 하는 것을 제어의 역전이라고 한다고 한다.

 

이해가 어려우니 코드로 보도록 하자.

 

만약, UserService를 작성하고 UserServiceImpl로 구현을 해서 개발한다면, 이런 방식으로 UserService를 호출하게 될 것이다.

// 개발자가 직접 객체를 생성하는 방식 (IOC 아님)
UserService userService = new UserServiceImpl();

 

이것에 IOC를 적용한다면 다음과 같다.

// IOC 방식 (스프링이 객체를 대신 생성해주고 주입해줌)
@Autowired
private UserService userService;

 

UserService를 사용은 하지만, 어떤 객체로 사용하는지는 모르고 스프링이 제어해준대로 사용하게 된다.

 

이렇게 생성이나 호출 제어는 프레임워크에게 맡기는 방법을 제어의 역전이라고 한다.

 

스프링에서 굉장히 자주 사용하며, 결합도를 낮춰 확장성과 유지보수에 도움을 준다고 한다.

+ Recent posts