티스토리 뷰
설정 : Java + Spring + Console + JDBC + Gradle 테스트 용 설정 2
Korean Eagle 2021. 11. 28. 23:330. 기본적인 스프링과 자바 개발 설정이 가물가물해서 다시 적어 보는 포스트이다.
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 설정하여 정보가 나오는 것이다.
'Spring > Spring Basic' 카테고리의 다른 글
Settings : Gradle + XML 없는 Spring MVC (0) | 2021.12.08 |
---|---|
Spring Basic : 스프링 학습 1 (0) | 2021.12.06 |
설정 : Java + Spring + Console + JDBC + Gradle 테스트 용 설정 1 (0) | 2021.11.28 |
Spring Basic : profile 사용하기 - 2 (0) | 2020.08.19 |
Spring Basic : Spring MVC Internationalization i18n 설정 (0) | 2020.08.09 |
- Total
- Today
- Yesterday
- 도커 개발환경 참고
- AWS ARN 구조
- Immuability에 관한 설명
- 자바스크립트 멀티 비동기 함수 호출 참고
- WSDL 참고
- SOAP 컨슈머 참고
- MySql dump 사용법
- AWS Lambda with Addon
- NFC 드라이버 linux 설치
- electron IPC
- mifare classic 강의
- go module 관련 상세한 정보
- C 메모리 찍어보기
- C++ Addon 마이그레이션
- JAX WS Header 관련 stackoverflow
- SOAP Custom Header 설정 참고
- SOAP Custom Header
- SOAP BindingProvider
- dispatcher 사용하여 설정
- vagrant kvm으로 사용하기
- git fork, pull request to the …
- vagrant libvirt bridge network
- python, js의 async, await의 차이
- go JSON struct 생성
- Netflix Kinesis 활용 분석
- docker credential problem
- private subnet에서 outbound IP 확…
- 안드로이드 coroutine
- kotlin with, apply, also 등
- 안드로이드 초기로딩이 안되는 경우
- navigation 데이터 보내기
- 레이스 컨디션 navController
- raylib
- spring boot
- jsp
- one-to-one
- 스프링
- Many-To-Many
- 자바
- Rest
- 외부파일
- 매핑
- 설정
- 상속
- one-to-many
- 하이버네이트
- form
- MYSQL
- 설정하기
- RestTemplate
- crud
- XML
- Spring Security
- hibernate
- WebMvc
- Angular
- Security
- 스프링부트
- Validation
- 로그인
- login
- mapping
- Spring