: Test Driven Development, 테스트 주도 개발
반복 테스트를 이용한 소프트웨어 방법론으로 작은 단위의 테스트 케이스를 작성하고 이를 통과하는 코드를 추가하는 단계를 반복하여 구현
✅ Fast: 빠르게 동작하여 여러번 test 가능
✅ Independent: 각 테스트는 독립적이며 서로 의존X
✅ Repeatable: 어느 환경에서도 반복 가능
✅ Self-Validating: 테스트는 성공 또는 실패로 bool값으로 결과를 내어 자체적으로 검증가능
✅ Timely: 테스트는 적시에 즉, 테스트하려는 실제 코드를 구현하기 직전에 구현해야 함
✔️ JUnit의 각 test는 하나하나를 별도의 객체에서 실행시키므로 iv라 해도 같은 class에 있는 test method들이 iv 공유X
✔️ statement 대신 PreparedStatemnet 사용
: SQL Injection 공격을 막고, SQL문을 재사용하여 성능향상
// JUnit이 작동하면서 SpringJUnit4ClassRunner를 가지고 test
// 수동과 다르게 하나의 ac를 재사용하기 때문에 성능 ↑
@RunWith(SpringJUnit4ClassRunner.class) // ApplicationContext ac 자동생성
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"}) //xml 표준 파일 위치 지정
public class DBConnectionTest2Test {
@Autowired //자동
DataSource ds;
private void deleteAll() throws Exception{
Connection conn = ds.getConnection();
String sql = "delete from user_info";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.executeUpdate();
}
@Test
public void insertUserTest() throws Exception {생략}
public int insertUser(User user) throws Exception {생략}
@Test
public void selectUserTest() throws Exception {생략}
public User selectUser(String id) throws Exception {생략}
@Test
public void springJdbcConnectionTest() throws Exception{
// 수동
// ApplicationContext ac = new GenericXmlApplicationContext(
// "file:src/main/webapp/WEB-INF/spring/**/root-context.xml");
// DataSource ds = ac.getBean(DataSource.class);
Connection conn = ds.getConnection(); // 데이터베이스의 연결
System.out.println("conn = " + conn);
assertTrue(conn!=null); // 괄호안의 조건이 true면 테스트 성공, 아니면 실패
}
}
@Test
public void insertUserTest() throws Exception {
User user = new User("iubar", "1234", "hello",
"aaaa@aaa.com", new Date(), "fb", new Date());
deleteAll(); // 중복방지
int rowCnt = insertUser(user);
assertTrue(rowCnt==1); // 0은 실패
}
// 사용자 정보를 user_info테이블에 저장하는 메서드
public int insertUser(User user) throws Exception {
Connection conn = ds.getConnection();
// insert into user_info (id, pwd, name, email, birth, sns, reg_date)
// values ('iubar','1234','hello','aaa@aaa.com','2000-01-01','facebook',now());
// String sql = "insert into user_info values ('"user.getId()+"'
// ,'1234','hello','aaa@aaa.com','2000-01-01','facebook',now())";
// PreparedStatement 이용 시 ? 사용, SQL Injection공격과 성능향상(sql문 재사용)
String sql = "insert into user_info values (?, ?, ?, ?, ?, ?, now())";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, user.getId());
pstmt.setString(2, user.getPwd());
pstmt.setString(3, user.getName());
pstmt.setString(4, user.getEmail());
pstmt.setDate(5, new java.sql.Date(user.getBirth().getTime()));
pstmt.setString(6, user.getSns());
// SQL문 실행, SELECT: executeQuery
int rowCnt = pstmt.executeUpdate(); // INSERT or UPDATE or DELETE
return rowCnt;
}
@Test
public void selectUserTest() throws Exception {
deleteAll(); // select 값이 없는 오류 방지
User user = new User("iubar", "1234", "hello",
"aaaa@aaa.com", new Date(), "fb", new Date());
int rowCnt = insertUser(user);
User user2 = selectUser("iubar");
assertTrue(user.getId().equals("iubar"));
}
public User selectUser(String id) throws Exception {
Connection conn = ds.getConnection();
String sql = "select * from user_info where id= ? ";
PreparedStatement pstmt = conn.prepareStatement(sql); // SQL Injection공격, 성능향상
pstmt.setString(1,id);
ResultSet rs = pstmt.executeQuery(); // select, 결과값 type은 resultSet
if(rs.next()) {
User user = new User();
user.setId(rs.getString(1));
user.setPwd(rs.getString(2));
user.setName(rs.getString(3));
user.setEmail(rs.getString(4));
user.setBirth(new Date(rs.getDate(5).getTime()));
user.setSns(rs.getString(6));
user.setReg_date(new Date(rs.getTimestamp(7).getTime()));
return user;
}
return null;
}
@Test
public void deleteUserTest() throws Exception {
deleteAll();
int rowCnt = deleteUser("iubar");
assertTrue(rowCnt==0); // delete 후 실패(다 지운 다음 삭제하므로 실패)
User user = new User("iubar", "1234", "hello", "aaaa@aaa.com", new Date(), "fb", new Date());
rowCnt = insertUser(user);
assertTrue(rowCnt==1); // insert 성공
rowCnt = deleteUser(user.getId());
assertTrue(rowCnt==1); // delete 성공
assertTrue(selectUser(user.getId())==null); // delete되었으므로 null
}
public int deleteUser(String id) throws Exception{
Connection conn = ds.getConnection();
String sql = "delete from user_info where id= ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1,id);
// int rowCnt = pstmt.executeUpdate();
// return rowCnt;
return pstmt.executeUpdate();
}
@Test
public void updateUserTest() throws Exception {
deleteAll();
User user = new User("iubar", "1234", "hello",
"aaaa@aaa.com", new Date(), "fb", new Date());
int rowCnt = insertUser(user);
assertTrue(rowCnt==1); // insert 성공
rowCnt = updateUser(user);
assertTrue(rowCnt==1); // update 성공
}
// 매개변수로 받은 사용자 정보로 user_info테이블을 update하는 메서드
public int updateUser(User user) throws Exception {
Connection conn = ds.getConnection();
String sql = "update user_info set pwd=? where id =?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "5555");
pstmt.setString(2, user.getId());
int rowCnt = pstmt.executeUpdate(); // INSERT or UPDATE or DELETE
return rowCnt;
}
: 데이터(data)에 접근(access)하기 위한 객체(object)
database에 저장된 데이터를 CRUD(읽기, 쓰기, 삭제, 변경)을 수행
DB테이블 당 하나의 DAO를 작성(1:1)
✔️ 변경에 유리: 추후 DB(ex.MySQL->ORACLE)가 변경되더라도 UserDao만 수정하면 됨
✔️ 메모리 효율(자원반환)을 위해 Connection, PreparedStatement는 항상 .close() 해줌
↪ close()의 호출순서는 생성된 순서의 역순
↪ close()를 호출하다가 예외가 발생할 수 있으므로, try-catch로 감싸야함
try{ 생략
} catch (SQLException e) {
return null;
} finally {
// try { if(rs!=null) rs.close(); } catch (SQLException e) { e.printStackTrace();}
// try { if(pstmt!=null) pstmt.close(); } catch (SQLException e) { e.printStackTrace();}
// try { if(conn!=null) conn.close(); } catch (SQLException e) { e.printStackTrace();}
close(rs, pstmt, conn); // private void close(AutoCloseable... acs) {
}
✔️ try-with-resources - since jdk7
부터 AutoCloseable 인터페이스가 구현되어있는 Connection, PreparedStatement 등과 같은 개체를 try() 안에 생성하면,
선언한 conn, pstmt는 작업 중 예외와 무관하게 자동으로 close됨
public int updateUser(User user) {
int rowCnt = FAIL; // insert, delete, update
String sql = "update user_info " +
"set pwd = ?, name=?, email=?, birth =?, sns=?, reg_date=? " +
"where id = ? ";
// try-with-resources - since jdk7
try (
Connection conn = ds.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
){
pstmt.setString(1, user.getPwd());
pstmt.setString(2, user.getName());
pstmt.setString(3, user.getEmail());
pstmt.setDate(4, new java.sql.Date(user.getBirth().getTime()));
pstmt.setString(5, user.getSns());
pstmt.setTimestamp(6, new java.sql.Timestamp(user.getReg_date().getTime()));
pstmt.setString(7, user.getId());
rowCnt = pstmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
return FAIL;
}
return rowCnt;
}
✔️ User class에 birth는 java.util.date 타입, DB에 넣을 때는 java.sql.date
타입으로 변환
✔️ reg_date(회원가입 일시)에 날짜와 시간 모두 넣으려면 java.sql.Timestamp
로 변환
✔️ cf) MySQL은 date타입으로 날짜까지만 들어가고, java의 util.Date는 시간까지 설정되므로 임시로 날짜를 세팅해줌
Calendar cal = Calendar.getInstance();
cal.clear();
cal.set(2023, 1, 1);
package com.fastcampus.ch3;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Date;
// @Component - @Controller, @Repository, @Service, @ControllerAdvice
@Repository
public class UserDaoImpl implements UserDao {
@Autowired
DataSource ds;
final int FAIL = 0;
@Override
public int deleteUser(String id) {
int rowCnt = FAIL; // insert, delete, update
Connection conn = null;
PreparedStatement pstmt = null;
String sql = "delete from user_info where id= ? ";
try {
conn = ds.getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, id);
return pstmt.executeUpdate(); // insert, delete, update
} catch (SQLException e) {
e.printStackTrace();
return FAIL;
} finally {
// close()를 호출하다가 예외가 발생할 수 있으므로, try-catch로 감싸야함.
// try { if(pstmt!=null) pstmt.close(); } catch (SQLException e) { e.printStackTrace();}
// try { if(conn!=null) conn.close(); } catch (SQLException e) { e.printStackTrace();}
close(pstmt, conn); // private void close(AutoCloseable... acs) {
}
}
@Override
public User selectUser(String id) {
User user = null;
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
String sql = "select * from user_info where id= ? ";
try {
conn = ds.getConnection();
pstmt = conn.prepareStatement(sql); // SQL Injection공격, 성능향상
pstmt.setString(1, id);
rs = pstmt.executeQuery(); // select
if (rs.next()) {
user = new User();
user.setId(rs.getString(1));
user.setPwd(rs.getString(2));
user.setName(rs.getString(3));
user.setEmail(rs.getString(4));
user.setBirth(new Date(rs.getDate(5).getTime()));
user.setSns(rs.getString(6));
user.setReg_date(new Date(rs.getTimestamp(7).getTime()));
}
} catch (SQLException e) {
return null;
} finally {
// close()를 호출하다가 예외가 발생할 수 있으므로, try-catch로 감싸야함.
// close()의 호출순서는 생성된 순서의 역순
// try { if(rs!=null) rs.close(); } catch (SQLException e) { e.printStackTrace();}
// try { if(pstmt!=null) pstmt.close(); } catch (SQLException e) { e.printStackTrace();}
// try { if(conn!=null) conn.close(); } catch (SQLException e) { e.printStackTrace();}
close(rs, pstmt, conn); // private void close(AutoCloseable... acs) {
}
return user;
}
// 사용자 정보를 user_info테이블에 저장하는 메서드
@Override
public int insertUser(User user) {
int rowCnt = FAIL;
Connection conn = null;
PreparedStatement pstmt = null;
// insert into user_info (id, pwd, name, email, birth, sns, reg_date)
// values ('asdf22', '1234', 'smith', 'aaa@aaa.com', '2022-01-01', 'facebook', now());
String sql = "insert into user_info values (?, ?, ?, ?,?,?, now()) ";
try {
conn = ds.getConnection();
pstmt = conn.prepareStatement(sql); // SQL Injection공격, 성능향상
pstmt.setString(1, user.getId());
pstmt.setString(2, user.getPwd());
pstmt.setString(3, user.getName());
pstmt.setString(4, user.getEmail());
pstmt.setDate(5, new java.sql.Date(user.getBirth().getTime()));
pstmt.setString(6, user.getSns());
return pstmt.executeUpdate(); // insert, delete, update;
} catch (SQLException e) {
e.printStackTrace();
return FAIL;
} finally {
close(pstmt, conn); // private void close(AutoCloseable... acs) {
}
}
// 매개변수로 받은 사용자 정보로 user_info테이블을 update하는 메서드
@Override
public int updateUser(User user) {
int rowCnt = FAIL; // insert, delete, update
String sql = "update user_info " +
"set pwd = ?, name=?, email=?, birth =?, sns=?, reg_date=? " +
"where id = ? ";
// try-with-resources - since jdk7
try (
Connection conn = ds.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql); // SQL Injection공격, 성능향상
){
pstmt.setString(1, user.getPwd());
pstmt.setString(2, user.getName());
pstmt.setString(3, user.getEmail());
pstmt.setDate(4, new java.sql.Date(user.getBirth().getTime()));
pstmt.setString(5, user.getSns());
pstmt.setTimestamp(6, new java.sql.Timestamp(user.getReg_date().getTime()));
pstmt.setString(7, user.getId());
rowCnt = pstmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
return FAIL;
}
return rowCnt;
}
@Override
public void deleteAll() throws Exception {
Connection conn = ds.getConnection();
String sql = "delete from user_info ";
PreparedStatement pstmt = conn.prepareStatement(sql); // SQL Injection공격, 성능향상
pstmt.executeUpdate(); // insert, delete, update
}
private void close(AutoCloseable... acs) {
for(AutoCloseable ac :acs)
try { if(ac!=null) ac.close(); } catch(Exception e) { e.printStackTrace(); }
}
}
public class UserDaoImpl implements UserDao { 생략 }
public interface UserDao {
int deleteUser(String id);
User selectUser(String id);
// 사용자 정보를 user_info테이블에 저장하는 메서드
int insertUser(User user);
// 매개변수로 받은 사용자 정보로 user_info테이블을 update하는 메서드
int updateUser(User user);
void deleteAll() throws Exception;
}
✳️ UserDaoImpl → goto → test
package com.fastcampus.ch3;
import junit.framework.TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.Calendar;
import java.util.Date;
@RunWith(SpringJUnit4ClassRunner.class) // ApplicationContext ac 자동생성
// root-context에 servlet-context에 있는 <context:component-scan base-package="com.fastcampus.ch3"> 설정 복붙
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"})
public class UserDaoImplTest extends TestCase {
@Autowired
UserDao userDao;
@Test
public void testDeleteUser() {
}
@Test
public void testSelectUser() {
}
@Test
public void testInsertUser() {
}
@Test
public void testUpdateUser() throws Exception {
Calendar cal = Calendar.getInstance();
cal.clear();
cal.set(2023, 1, 1);
// userDao.deleteUser("iuber");
userDao.deleteAll();
User user = new User("iuber", "1234", "abc",
"aaa@aaa.com", new Date(cal.getTimeInMillis()),
"fb", new Date(cal.getTimeInMillis()));
int rowCnt = userDao.insertUser(user);
assertTrue(rowCnt==1);
user.setPwd("4321");
user.setEmail("bbb@bbb.com");
rowCnt = userDao.updateUser(user);
assertTrue(rowCnt==1);
User user2 = userDao.selectUser(user.getId());
System.out.println("user = " + user);
System.out.println("user2 = " + user2);
assertTrue(user.equals(user2));
}
}
✔️ pom.xml에 dependency 추가: validation
<dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency>
@Controller
@RequestMapping("/login")
public class LoginController {
@Autowired
UserDao userDao;
@GetMapping("/login")
public String loginForm() {
return "loginForm";
}
@GetMapping("/logout")
public String logout(HttpSession session) {
// 1. 세션을 종료
session.invalidate();
// 2. 홈으로 이동
return "redirect:/";
}
@PostMapping("/login")
public String login(String id, String pwd, String toURL, boolean rememberId,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// 1. id와 pwd를 확인
if(!loginCheck(id, pwd)) {
// 2-1 일치하지 않으면, loginForm으로 이동
String msg = URLEncoder.encode("id 또는 pwd가 일치하지 않습니다.", "utf-8");
return "redirect:/login/login?msg="+msg;
}
// 2-2. id와 pwd가 일치하면,
// 세션 객체를 얻어오기
HttpSession session = request.getSession();
// 세션 객체에 id를 저장
session.setAttribute("id", id);
if(rememberId) {
// 1. 쿠키를 생성
Cookie cookie = new Cookie("id", id); // ctrl+shift+o 자동 import
// 2. 응답에 저장
response.addCookie(cookie);
} else {
// 1. 쿠키를 삭제
Cookie cookie = new Cookie("id", id); // ctrl+shift+o 자동 import
cookie.setMaxAge(0); // 쿠키를 삭제
// 2. 응답에 저장
response.addCookie(cookie);
}
// 3. 홈으로 이동
toURL = toURL==null || toURL.equals("") ? "/" : toURL;
return "redirect:"+toURL;
}
private boolean loginCheck(String id, String pwd) {
User user = userDao.selectUser(id);
if(user==null) return false;
return user.getPwd().equals(pwd);
// return "iubar".equals(id) && "1234".equals(pwd);
}
}
@Controller // ctrl+shift+o 자동 import
@RequestMapping("/register")
public class RegisterController {
@Autowired
UserDao userDao;
final int FAIL = 0;
@InitBinder
public void toDate(WebDataBinder binder) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
binder.registerCustomEditor(Date.class, new CustomDateEditor(df, false));
binder.setValidator(new UserValidator()); // UserValidator를 WebDataBinder의 로컬 validator로 등록
// List<Validator> validatorList = binder.getValidators();
// System.out.println("validatorList="+validatorList);
}
@GetMapping("/add")
public String register() {
return "registerForm"; // WEB-INF/views/registerForm.jsp
}
@PostMapping("/add")
public String save(@Valid User user, BindingResult result, Model m) throws Exception {
System.out.println("result="+result);
System.out.println("user="+user);
// User객체를 검증한 결과 에러가 없으면, DB에 신규회원 정보를 저장
if(!result.hasErrors()) {
int rowCnt = userDao.insertUser(user);
// 저장 결과 성공 시 registerInfo을 이용하여 가입정보 보여줌
if(rowCnt!=FAIL) return "registerInfo";
}
// 실패 시 registerForm을 이용해서 에러를 보여줌
return "registerForm";
}
private boolean isValid(User user) {
return true;
}
}
참고) 자바의 정석 | 남궁성과 끝까지 간다