[Spring] 09. Spring으로 DB 실습(TDD)/DAO

Hyeongmin Jung·2023년 7월 10일
0

Spring

목록 보기
8/17

TDD

: Test Driven Development, 테스트 주도 개발
반복 테스트를 이용한 소프트웨어 방법론으로 작은 단위의 테스트 케이스를 작성하고 이를 통과하는 코드를 추가하는 단계를 반복하여 구현

📜 FIRST

Fast: 빠르게 동작하여 여러번 test 가능
Independent: 각 테스트는 독립적이며 서로 의존X
Repeatable: 어느 환경에서도 반복 가능
Self-Validating: 테스트는 성공 또는 실패로 bool값으로 결과를 내어 자체적으로 검증가능
Timely: 테스트는 적시에 즉, 테스트하려는 실제 코드를 구현하기 직전에 구현해야 함

✅ INSERT / SELECT / DELETE / UPDATE

✔️ 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면 테스트 성공, 아니면 실패
    }
}

insertUser

	@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;
    }

selectUser

    @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;
    }

deleteUser

    @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();
    }

updateUser

	@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;
    }

DAO(Data Access Object)

: 데이터(data)에 접근(access)하기 위한 객체(object)
database에 저장된 데이터를 CRUD(읽기, 쓰기, 삭제, 변경)을 수행
DB테이블 당 하나의 DAO를 작성(1:1)

🪜 계층의 분리

✔️ 변경에 유리: 추후 DB(ex.MySQL->ORACLE)가 변경되더라도 UserDao만 수정하면 됨

🧰 CRUD

✔️ 메모리 효율(자원반환)을 위해 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);

code

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(); }
    }
}

📚 DAO interface 및 implement 분리

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;
}

📃 Junit Test

✳️ 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));
    }
}

🪄 UserDao 컨트롤러 적용

✔️ pom.xml에 dependency 추가: validation

<dependency>
	<groupId>javax.validation</groupId>
	<artifactId>validation-api</artifactId>
	<version>2.0.1.Final</version>
</dependency>

view 및 resources | 🖇️

로그인 컨트롤러

@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;
    }
}

참고) 자바의 정석 | 남궁성과 끝까지 간다

0개의 댓글