1. 데이터베이스의 DAO (Data Access Object) 를 왜 만드나?
INSERT, DELETE, UPDATE는 요청하면 영향 받은 행 (숫자, INT)를 MAIN에 return 하고, SELECT는 테이블을 MAIN에 return 해준다. ★그러나 테이블 테이터를 자바가 바로 못받으니 자바 오브젝트로 파싱해서 리턴★ (DB는 자바가 아니고 다른 언어니까 파싱!) 즉, table을 받기 위해 테이블이랑 똑같이 생긴 클래스를 만들어야 한다. (String은 .메소드(. = 객체 연결 연산자)를 사용하지 못해서 XXX) JSON기억하죠? 그런걸 model 폴더에 만들 것
DAO에 SELECT, INSERT, DELETE, UPDATE 메소드를 만들어줄 것!
(재사용 하기 위함)
DAO와 DB는 요청해서 응답을 받으니까 소켓 통신이다
[ 공공데이터는 항상 JASON으로 응답 ]
공공데이터는 언어를 특정하지 않는다. url이 파이선으로 한 건지, 자바로한건지 잘 모르기 때문에 (=어떤 언어로든지 접근 가능해서) 공공데이터는 항상 jason으로 응답하고, 자바는 그걸 파싱해서 받는다
2. DAO 만들기
데이터베이스와 소프트웨어 간의 통신을 도와주는 중간 역할을 하는 객체 데이터베이스에 접근하여 CRUD(Create, Read, Update, Delete) 기능을 수행하고, 데이터베이스 트랜잭션 관리, 쿼리 실행, 데이터 매핑 등의 역할을 수행한다. (즉, 데이터베이스에 접근하여 데이터를 가져오거나 저장하는 역할)
이 DAO를 사용하여 재사용 할 수 있는 클래스로 만들자! SRP 단일 책임 원칙. 클래스의 책임을 만든다 = 레이어를 나눈다고 한다. (층을 나눠서 층마다 책임을 부여함)
이렇게 DAO(Data Access Object)를 만든다. → 만국공통. 무조건!!!
[ BankDAO에 메소드 ]
이렇게 DAO에 CRUD 메소드를 만들면,
이 그림대로 만들어진 것! !!DAO를 만들어야지 재사용 가능하니 꼭 만들자!!
3. model 패키지 만들기
[ model 패키지 ]
"model" 폴더는 데이터베이스와 관련된 클래스들을 모아두는 곳! 이 클래스는 테이블과 동일한 구조를 갖는 모델 클래스로 만들어지며, JSON 형식으로 데이터를 표현할 수 있다. 이 DAO 클래스는 일반적으로 "model" 폴더에 저장되어 관리됩니다.
* Table을 받기 위해, Table과 똑같이 생긴 Account 클래스를 model 패키지에 만들어줌.
* 그냥 막 만들지 말고, table 작성한 그 데이터로 만들어야 함. 이 데이터를 받을거라서!
* 지금 디폴트 생성자 있는 상태니까... 안써도 됨. 나중에 필요하면 다시 쓰러 오자
필드는 상태를 변경한다는 개념으로 쓰는게 아니라 파싱을 담는 항아리로 쓰인다.
JSON > ‘파싱 담기’ 참조
즉, Account는 DB에 Select한 데이터를 담기 위한 오브젝트!
* Timestamp = 날짜를 받을 땐 UTC로 받는 게 좋기 때문!
* createdAt
데이터베이스는 _ (언더스코어) 기법이지만 자바에선 카멜 표기법으로 써주기!
4. main에서 BankDAO 만들기
4-1. DELETE문
3-1. JDBC 자바와 마리아DB 연결에서 만들었던 그 코드를 재사용하기 위해 DAO를 만들려는 것 BankApp에 main에 있던 그 코드를 BankDAO의 deleteByNumber 메소드에 붙여넣는다.
이렇게 넣으면 무조건 number가 1인 계좌번호만 삭제되잖아. 그래서!! 메서드에 파라미터를 만들어준다.
만약, number에 2가 들어오면, getinstance()해서 소켓을 가져온다. (conn이 소켓이니까) 쿼리에 number 2를 넣는 코드 작성 (pstmt.setInt(1,number);) 그럼 쿼리를 소켓에 있는 버퍼에 담음 > 쿼리 완성 이제 플러시로 쏘자. (int num = pstmt.executeUpdate();)
데이터베이스가 number가 2번인 계좌를 리턴 할 때, 성공적이면 1을 리턴. 2가 없으면 0을 리턴. 때문에, return을 int num으로 받고, return 데이터 타입도 int로 바꿔준다. 만약 오류가 나면 catch로 넘어간다. throw하면 호출자한테 위임하게 되는데.... 그러면 너무 복잡해져서 오류 추적도 할 수 있는 e.printStackTrace()로 바꿀 것이다.
리턴 없어서 오류남. return 데이터 타입을 int로 바꿨으니, try를 탔을 때 reutrn값과 catch를 탔을 때의 return 값을 설정해줘야한다.
이제 오류나면 위임하는게 아니라 콘솔에 피범벅이 뜰 것!
근데 이런 식으로 return하는거 쌤은 별로 좋아하지 않지만…
우리는 초보자니 이렇게 사용하자.
deleteByNumber 메소드 코드
public class BankDAO { public int deleteByNumber(int number) { Connection conn = DBConnection.getInstance(); try { String sql = "delete from account_tb where number = ?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setInt(1,number); int num = pstmt.executeUpdate(); return num; } catch (Exception e) { e.printStackTrace(); } return -1; }
4-2. INSERT문
이제 insert 만들어 보자! insert 쿼리문에서 number는 AUTO_INCREMENT라 작성하지 않는다.
자바에서 'now'가 아니라 `LocalDate`와 같은 로컬 데이트 타입을 사용하면 한국 시간으로 처리될 수 있다. 데이터베이스 서버와 자바 서버가 서로 다른 시간대에 위치한다면, 시간 정보가 일치하지 않을 수 있다는 뜻! ex) 자바 서버가 한국에 위치하고 데이터베이스 서버가 미국에 위치한다면, 시간대의 차이로 인해 데이터베이스에 저장된 시간 정보와 자바에서 사용하는 로컬 데이트 타입의 시간 정보가 다를 수 있다는 뜻 -> 망함
데이터베이스 시간과 자바 서버 시간을 동기화하는 것이 중요
insert 했을 시, 1이 아니면 다 실패라고 볼 수 있다. 1이 아니면 제대로 적용된 게 아니기 때문! (0도 입력이 안 된거니까)
insert 메소드 코드
public int insert(String password, int balance) { Connection conn = DBConnection.getInstance(); try { String sql = "insert into account_tb(password, balance, created_at) values(?, ?, now())"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setString(1,password); pstmt.setInt(2,balance); int num = pstmt.executeUpdate(); return num; } catch (Exception e) { e.printStackTrace(); } return -1; }
4-3. UPDATE문
쿼리문을 이렇게 작성하는건 너무 위험하다. 항상 + 1000만 할 것인지?
updateByNumber 메소드 코드
public int updateByNumber(int balance, int number) { Connection conn = DBConnection.getInstance(); try { String sql = "update account_tb set balance = ? where number = ?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setInt(1,balance); pstmt.setInt(2,number); int num = pstmt.executeUpdate(); return num; } catch (Exception e) { e.printStackTrace(); } return -1; }
5. test에서 BankDAOTest 테스트 해보기 [ given, when, then ]
본 코드에서 코드를 (DELETE, INSERT, UPDATE만. 이 친구들은 INT로 받으니까 세트 격) 다 짰으니, 실행이 잘 되는지 test 해보자
테스트 코드를 짜는 방식은 여러 개가 있는데
우리는 앞으로 본 코드를 작성하고,
걔가 잘 돌아가는지 코드를 테스트 파일에 옮겨서 !실행만! 해볼 것이다.
(테스트 주도 개발. TDD)
메인 메서드의 동작을 확인하기 위해서
단위 테스트(test)를 통해 각각의 메서드를 독립적으로 테스트하는 것
일단 DBConnectionTest를 test 해보자
우선 연결이 잘 되는지부터 확인해 볼 것!
1. given - 파라미터 (매개변수)
test엔 파라미터를 작성할 수 없다. 때문에, 이 given 주석 아래에 작성할 것! 물론 본코드에 매개 변수가 없으면 해당 부분 생략 가능. (지금은 파라미터 받은게 없으니 생략!)
2. when - 본코드의 메서드 호출
test엔 return 타입을 받지 못한다. 때문에, when 주석 아래에 return 타입을 작성할 것!
커넥션을 리턴 받네. 당연함.. getInstance니까
이렇게, 리턴타입을 적어주면 끝!
3. then - 검증 (우리는 초보라 눈검증으로 할 것)
main에 작성된 코드가 이거임. 이제 이 커넥션이 null인지 아닌지만 (작동하는지, 안하는지만) 검증하면 된다.
성공했다! 이런걸 확인 하는게 then!
개발할 때는 printStackTrace()로 하고,
개발을 다하면 저 피범벅 메세지를 바꾸면 됨. throw로?
이제 test에서 DELETE 해보자!
1-1. given
given, when, then을 똑같이 해 볼 것이다. test엔 파라미터와 return 타입을 작성할 수 없다. 때문에, 이 주석들 아래에 작성할 것!
본코드에 매개 변수가 있는 것 확인!
여기에 본코드에 있는 매개변수 넣어주기
1-2. when
BankDAO가 static이 아니었으니까 new 해줘야함
return 타입 int인 것 확인한 후, 리턴 타입 적어주기
1-3. then
1이 아니면 다 실패한 거라고 치부할 것. result == 1
[ BankDAOTest deleteByNumber_test 전체 코드 ]
@Test public void deleteByNumber_test() { // given int number = 1; // when BankDAO dao = new BankDAO(); int result = dao.deleteByNumber(number); // then if(result == 1) { System.out.println("삭제 성공"); } else if (result == 0) { System.out.println(number + "번호를 찾을 수 없습니다."); } else { System.out.println("삭제 실패"); } }
test에서 UPDATE
number4의 balance가 100으로 바뀌었다. test 완료!
test 전체 코드
[ BankDAOTest ]
package dao; import org.junit.jupiter.api.Test; public class BankDAOTest { @Test public void deleteByNumber_test() { // given int number = 1; // when BankDAO dao = new BankDAO(); int result = dao.deleteByNumber(number); // then if(result == 1) { System.out.println("삭제 성공"); } else if (result == 0) { System.out.println(number + "번호를 찾을 수 없습니다."); } else { System.out.println("삭제 실패"); } } @Test public void insert_test() { //given String password = "535115"; int balance = 15000; //when BankDAO dao = new BankDAO(); int result = dao.insert(password, balance); //then if(result == 1) { System.out.println("insert 성공"); } else if (result == 0) { System.out.println(password + "와 " + balance + "를 넣을 수 없습니다"); } else { System.out.println("삭제 실패"); } } @Test public void updateByNumber_test() { //given int balance = 100; int number = 4; //when BankDAO dao = new BankDAO(); int result = dao.updateByNumber(balance, number); //then if (result == 1) { System.out.println("update 성공"); } else { System.out.println("업데이트 실패"); } } }
[ DBConnectionTest ]
package db; import org.junit.jupiter.api.Test; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBConnectionTest { @Test public void getInstance_test() { // given = 파라미터 // when Connection conn = DBConnection.getInstance(); //then if(conn == null) { System.out.println("실패"); } else { System.out.println("성공"); } } }
6. main 코드에서 실행
test가 완료가 되면 main 코드에 실행해보자.
import dao.BankDAO; import db.DBConnection; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.Scanner; public class BankApp { public static void main(String[] args) { //이거 BR로도 만들 수 있다 Scanner sc = new Scanner(System.in); System.out.print("삭제할 계좌번호를 입력해주세요 : "); int number = sc.nextInt(); //DAO한테 위임 BankDAO dao = new BankDAO(); //결과를 int로 안받으면 잘 됐는지 안됐는지 고객이 알 수가 없음 int result = dao.deleteByNumber(number); if (result == 1) { System.out.println("삭제 성공했습니다"); } else { System.out.println("삭제 실패했습니다"); } } }
삭제 됨! 정상 작동!
[ 한글이 깨질 때 해결 방법 ]
여기 들어가서 설정 해 주자!
bankapp은 bankDAO를 통해 데이터베이스(DB)에 접근한다. 그런데 이 bankDAO는 DB에 접근할 때 DBConnection을 활용하여 필요한 작업을 수행한다. (DBConnection의 도움을 받아 가는 것!) bankDAO는 DBConnection을 호출하여 DB에 접근하고, DB로부터 반환(return)된 결과를 받는다. 이를 통해 bankDAO는 데이터베이스와의 상호작용을 처리한다. 즉, bankDAO가 미들웨어 격
TIP!
Impl 이라고 이름 붙은 건 Implements BankDAOImpl = Impl = implements 구현체 만들어라. 라는 뜻!
과거에 적었던 코드랑 테이블을 참조해서 deleteByNumber 메소드의 내용을 구현하자 코드는 안 보고 적는 것 아니다. 1. DB소켓 가져오고 2. 버퍼에 쿼리담고 3. 플러시하고 이런 과정을 이해하자!
return 문을 사용하여 num 값을 반환하는 이유는
메서드가 호출된 곳에서 삭제된 레코드의 개수를 확인하고 처리 결과를 알기 위해서
즉,
insert()
메서드를 호출하여 결과를 int
변수 result
에 저장하는 것Share article