티스토리 뷰

728x90

0. 기본적인 스프링과 자바 개발 설정이 가물가물해서 다시 적어 보는 포스트이다.

 

1. 지난 번에 프로젝트 만들고, DB, 유저 만들고, 기본 라이브러리를 가져왔다. 이젠 코드를 붙일 부분이다.

  1-1 여기에 붙이는 소스는 토비의 스프링 책의 DB와 User 클래스를 사용한다. 귀찮다.

  1-2 다만, 책에 구현되지 않은 나머지 count, get의 내용을 추가로 구현한 부분이 있다.

 

2. 우선 테스트 코드이다. 기본적인 코드 없이 테스트 부터 생성하였다.

  2-1 기본적인 테스트를 수행하는 코드이다. 어떻게 UserDao를 작성할지와 상관없이 입출력만으로 작성할 수 있다.

  2-2 @ExtendWith(SpringExtension.class)는 ApplicationContext를 공유하기 위해서 사용한다. 

    2-2-1 메소드 마다 ApplicationContext를 생성하거나 @BeforeEach에서 매번 생성하기에는 부담이 되기 때문이다.

    2-2-2 @Autowired를 통해 객체 주입이 가능하다. ApplicationContext를 받아왔다.

  2-3 @ContextConfiguration은 어떤 설정파일을 사용할지를 지정해 주는 부분이다.

  2-4 @BeforeEach에서는 2개의 User fixture를 만들어 두고 편하게 사용한다. UserDao객체도 받아와 테스트로 사용한다.

  2-5 나머지는 모두 단순한 부분이지만 TestGetFailure라는 메소드가 있다. 오류가 정상적으로 발생하는지 체크를 한다.

    2-5-1 JUnit4와 약간 다른 부분이다. assertThrow를 사용하는데, 어떤 예외를 기대하는지를 지정하고,

    2-5-2 두번째 인자로 테스트를 수행할 callback을 지정한다. callback수행결과를 Exception으로 받아 확인하고 있다.

 

 

package basic_project;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.sql.SQLException;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {basic_project.TestDaoFactory.class})
public class UserDaoTest {

  @Autowired
  private ApplicationContext context;

  private UserDao dao;
  private User user;
  private User user2;;

  @BeforeEach
  void setUp() {
    // ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
    user = new User("heops79", "pilseong", "password");
    user2 = new User("lilymii", "sangmi", "password");

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

  @Test
  void testAdd() throws SQLException {
    dao.deleteAll();
    assertThat(dao.getCount(), is(0));

    dao.add(user);

    assertThat(dao.getCount(), is(1));
  }

  @Test
  void testDeleteAll() throws SQLException {
    dao.deleteAll();
    dao.add(user);
    dao.add(user2);

    assertThat(dao.getCount(), is(2));

    dao.deleteAll();

    assertThat(dao.getCount(), is(0));
  }

  @Test
  void testGet() throws SQLException {
    dao.deleteAll();
    dao.add(user);

    User pilseong = dao.get("heops79");

    assertThat(pilseong.getName(), is(user.getName()));
    assertThat(pilseong.getId(), is(user.getId()));
  }

  @Test
  void testGetFailure() throws SQLException {
    Exception exception = assertThrows(EmptyResultDataAccessException.class, () -> {
      dao.deleteAll();
      dao.add(user);

      dao.get("testtest");
    });

    assertThat(exception.getMessage(), is("empty result"));
  }

  @Test
  void testGetCount() throws SQLException {
    dao.deleteAll();
    assertThat(dao.getCount(), is(0));

    dao.add(user);

    System.out.println(user.getId() + " enrolled successfully");

    dao.add(user2);

    System.out.println(user2.getId() + " enrolled successfully");

    User pilseong = dao.get("heops79");
    System.out.println(pilseong.getName() + " is fetched from the DB");

    assertThat(pilseong.getName(), is(user.getName()));
    assertThat(pilseong.getId(), is(user.getId()));

    User sangmi = dao.get("lilymii");
    System.out.println(sangmi.getName() + " is fetched from the DB");

    assertThat(sangmi.getName(), is(user2.getName()));
    assertThat(sangmi.getId(), is(user2.getId()));

    assertThat(dao.getCount(), is(2));
  }

  @Test
  void testSetDataSource() {
    assertThat(this.dao.getDataSource(), notNullValue());
  }
}

 

3. 위의 테스트 코드를 만족하기 위한 코드를 작성하면 되는데, 데이터베이스 접근 로직을 콜백 패턴으로 구현한다.

  3-1 우선 JdbcTemplate와 콜백 인터페이스를 정의하였다. 토비 책처럼 add, deleteAll, get, count를 구현한다.

    3-1-1 add와 deleteAll을 위해서는 하나의 statement를 작성하는 콜백만 있으면 되지만, get과 count는 ResultSet을 요구한다.

    3-1-2 ResultStrategy도 추가로 작성하였다. 람다를 사용하기 위해서는 인터페이스는 하나의 메소드를 정의해야 한다.

    3-1-3 아래처럼 StatementStrategy는 PreparedStatement를 정의하는 콜백이고 실행 시 Connection이 필요하다.

    3-1-4 ResultStrategy는 템플릿이 실행한 결과를 인자로 받아서 결과를 만들어 돌려준다.

      3-1-4-1 어떤 타입인지 모르기 때문에 Generic으로 정의하였다.

 

package basic_project;

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

public interface StatementStrategy {
  PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}


package basic_project;

import java.sql.ResultSet;
import java.sql.SQLException;

import org.springframework.dao.DataAccessException;

public interface ResultStrategy<T> {
  T extractData(ResultSet rs) throws SQLException, DataAccessException;
}

 

  3-2 이제 위의 인터페이스를 사용하는 템플릿을 정의할 부분이다. 이것이 JdbcTemplate이다.

    3-2-1 토비 책에 일부 코드가 있지만, 거기에 없는 것도 그냥 참고로 구현했다. 아래의 코드가 핵심이다.

    3-2-2 아래의 코드를 구현하면 사실 다 한 거다. 데이터베이스 설정과 나머지는 Configuration에서 하면 된다.

    3-2-3 deleteAll, add 함수는 executeSQL를 사용하고 add의 경우를 위해 가변인자를 사용하였다.

      3-2-3-1 두 기능을 한 템플릿에 넣기 위해 가변인자의 길이를 확인하는 부분이 있다.

    3-2-4 get과 count는 결과 값이 다르기 때문에 두개로 분리하여 콜백을 작성하였다.

    3-2-5 템플릿을 만든 이유는 다 알겠지만 지긋지긋한 close 때문이다.

 

package basic_project;

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

import javax.sql.DataSource;

public class JdbcContext {
  private DataSource dataSource;

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

  public DataSource getDataSource() {
    return this.dataSource;
  }

  public void executeSQL(String sql, String... values) throws SQLException {
    this.jdbcContextWithStatementStrategy((Connection c) -> {
      PreparedStatement ps = c.prepareStatement(sql);
      if (values.length > 0) {
      	for (int i=0; i<values.length; i++) {
          ps.setString(i+1, values[i]);
        }
      }

      return ps;
    });
  }

  public int query(String sql) throws SQLException {
    return this.jdbcContextWithStatementStrategyAndResultStrategy(
        (Connection c) -> c.prepareStatement(sql),
        (ResultSet rs) -> {
          if (rs.next()) {
            return rs.getInt(1);
          }
          return 0;
        });
  }

  public User query(String sql, String... values) throws SQLException {
    return this.jdbcContextWithStatementStrategyAndResultStrategy((Connection c) -> 
    {
      PreparedStatement ps = c.prepareStatement(sql);
      if (values.length > 0) {
        ps.setString(1, values[0]);
      }
      return ps;
    },
    (ResultSet rs) -> {  
      if (rs.next()) {
        return new User(rs.getString(1), rs.getString(2), rs.getString(3));
      }
      return null;
    });
  }

  public List<Map<String, Object>> query(final String sql, final String... values) throws SQLException {
    return this.jdbcContextWithStatementStrategyAndResultStrategy(
        new StatementStrategy() {
          @Override
          public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
            PreparedStatement ps = c.prepareStatement(sql);
            if (values.length > 0) {
              for (int i=0; i < values.length; i++) {                
                ps.setString(i+1, values[i]);
              }
            }
            return ps;
          }
        }, 
        new ResultStrategy<List<Map<String,Object>>>() {
          @Override
          public List<Map<String,Object>> extractData(ResultSet rs) throws SQLException, DataAccessException {
            List<Map<String, Object>> results = new ArrayList<Map<String, Object>>();
            ResultSetMetaData rsmd = rs.getMetaData();
            Map<String, Object> row = null;
            while (rs.next()) {
              row = new HashMap<String, Object>();
              for (int i=0; i<rsmd.getColumnCount();i++) {
                row.put(rsmd.getColumnName(i+1), rs.getString(i+1));
              }
              results.add(row);
            }
            return results;
          }
        });
  }

  private <T> T jdbcContextWithStatementStrategyAndResultStrategy(
    StatementStrategy stmt, ResultStrategy<T> rsst)
      throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;
    ResultSet rs = null;

    try {
      c = dataSource.getConnection();
      ps = stmt.makePreparedStatement(c);
      rs = ps.executeQuery();
      return rsst.extractData(rs);
    } catch (SQLException e) {
      throw e;
    } finally {
      if (ps != null) {
        try {
          ps.close();
        } catch (SQLException e) {
        }
      }
      if (c != null) {
        try {
          c.close();
        } catch (SQLException e) {
        }
      }
    }
  }

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

    try {
      c = dataSource.getConnection();
      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) {
        }
      }
    }
  }
}

 

  3-3 이제 가장 중심에 있는 User, UserDao를 작성한다.

    3-3-1 아주 단순하다. 이미 JdbcTemplate을 만들어 두어, 한 줄 짜리 쿼리문이면 된다.

    3-3-2 별로 할 말은 없고 get에서 외예 발생하는 부분 정도가 눈에 띈다. 토비 책에서 제시한 형식이다.

 

package basic_project;

import java.sql.SQLException;

import javax.sql.DataSource;

import org.springframework.dao.EmptyResultDataAccessException;

public class UserDao {

  JdbcContext jdbcContext = null;

  private DataSource dataSource;

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

  public DataSource getDataSource() {
    return this.dataSource;
  }

  public void setJdbcContext(JdbcContext jdbcContext) {
    this.jdbcContext = jdbcContext;
  }

  public void add(User user) throws SQLException {
    jdbcContext.executeSQL("insert into users(id, name, password) values(?,?,?)",
        user.getId(), user.getName(), user.getPassword());
  }

  public User get(String id) throws SQLException {
    User user = jdbcContext.query("select * from users where id = ?", id);
    if (user == null) {
      throw new EmptyResultDataAccessException("empty result", 1);
    }
    return user;
  }

  public void deleteAll() throws SQLException {
    jdbcContext.executeSQL("delete from users");
  }

  public int getCount() throws SQLException {
    return jdbcContext.query("select count(*) from users");
  }
}

 

4. 이제 설정파일을 작성한다. 실전용 설정과 테스트 설정 두 개를 만들었다. 거의 동일하지만 다른 DataSource를 사용하도록 했다.

  4-1 테스트에서는 TestDaoFactory 설정을 사용하였다.

  4-2 설정 클래스에는 반드시 @Configuration이 있어야 한다. 

  4-3 두 설정의 차이는 사용하는 DB이름이 springbook과 testdb의 차이가 있고, DataSource가 다른 방식이다.

  4-4 테스트에 사용된 SingleConnectionSource는 suppress close 설정을 해주어야 제대로 동작한다.

    4-4-1 Connection이 꼴랑 하나이기 때문에 동작 방식을 지정해 주는 것이다.

 

package basic_project;

import javax.sql.DataSource;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;

@Configuration
public class DaoFactory {
  @Bean
  public UserDao userDao() {
    UserDao userDao = new UserDao();

    userDao.setDataSource(simpleDriverDataSource());
    return userDao;
  }
  
  @Bean
  public DataSource simpleDriverDataSource() {
    SimpleDriverDataSource dataSource = new SimpleDriverDataSource();

    dataSource.setDriverClass(com.mysql.cj.jdbc.Driver.class);
    dataSource.setUrl("jdbc:mysql://localhost/springbook?characterEncoding=UTF-8");
    dataSource.setUsername("pilseong");
    dataSource.setPassword("");

    return dataSource;
  }
}
package basic_project;

import javax.sql.DataSource;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;

@Configuration
public class TestDaoFactory {
  @Bean
  public UserDao userDao() {
    UserDao userDao = new UserDao();
    userDao.setDataSource(singleConnectionDataSource());
    return userDao;
  }
  
  @Bean
  public DataSource singleConnectionDataSource() {
    SingleConnectionDataSource dataSource = new SingleConnectionDataSource();

    dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql://localhost/testdb?characterEncoding=UTF-8");
    dataSource.setUsername("pilseong");
    dataSource.setPassword("");
    dataSource.setSuppressClose(true);

    return dataSource;
  }
}

 

5. 이제 테스트를 수행해 본다. 왼쪽의 전체 트리 구조와 아래 테스트 결과를 보면 구조와 결과를 알 수 있다.

  5-1 이전 포스트에 적어왔지만 gradle test는 아무 결과를 보여주지 않는다. 아래 결과는 plugin 설정하여 정보가 나오는 것이다.

 

728x90
댓글