🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [15]일차
🚀15일차에는 JDBC 사용과 JDBC 문법들, DAO (Data Access Object)와 DTO (Data Transfer Object)를 배울 수 있었다.
사담이지만 DAO와 DTO를 자세히 배우다보니 DDD도 떠오르는데 이전에 대외활동을 진행하면서
MVC패턴, TDD(테스트 주도 개발), DDD (Domain Driven Design)등에 대한 용어를 접했었다. 🥲
👀DDD는 보통 원인 해결에 초점을 맞추어 복잡한 문제 상황에서 해결하기 위한 방법론이다. (도메인, 서브도메인, 바운디드컨텍스트 를 구분하고 정의해야한다)
오늘 데이터베이스와 자바를 연결하는 과정이 신기하기도하고 새로운 개념이 많이 등장하여 회고 시간 전까지 따라가는데 급급했던 것 같다.
다행히 따라가면서도 내 언어로 정리하다보니 "블로그에 어느 부분을 자세히 기술해야할지, 지나가야할지" 등을 구분해낼 수 있게되었다.🚀
JDBC
- Java DataBase Connectivity의 약어
- 데이터베이스를 자바와 연결해주는 프로그램
- 다양한 종류의 관계형 데이터베이스(RDBMS)에 접속하고 SQL문을 수행하여 처리하는데에
사용되는 표준 SQL 인터페이스 API - 자바는 표준 SQL 인터페이스인 JDBC API를 제공
- 자바 SQL API를 살펴보면 SQL은 인터페이스인 것을 알 수 있다. 인터페이스를 구현해야하는 것이다.
JDBC 인터페이스
- 사용자 ↔ 인터페이스 ↔ 데이터베이스(MySQL이라는 DBMS사용)
- 만약 사용자가 인터페이스를 통하지 않고 MySQL에 접속하여 직접사용한다면
Oracle이라는 DBMS로 바꿀때 사용자의 코드가 모두 바뀌어야한다.
➡️인터페이스를 사용하면 DBMS를 바꿔도 사용자의 코드가 바뀔 일이 드물것이다. - 이렇게 사용자는 인터페이스만 사용하고, DB를 제공하는 벤더(=DB회사)들은 인터페이스를 구현한다
(DB접속, 결과값 반환 등)
👀데이터베이스 벤더
- 데이터베이스 관리 시스템(DBMS)을 개발하고 판매하는 기업
- 데이터베이스 관리에 필요한 다양한 서비스와 솔루션을 제공
- 오라클(Oracle),마이크로소프트(Microsoft),MySQL,PostgreSQL 등
- 데이터베이스 성능 모니터링, 데이터베이스 보안, 데이터베이스 백업 및 복구 등의 서비스를 제공
❓그렇다면 자바는 데이터베이스에 접속할 수 있는 API들(=구현체) 을 제공하는지?
➡️ 자바가 제공하지 않는다. 데이터베이스(ex. MySQL, Oracle…)에 접속할 수 있는 코드는
“각각의 DB 벤더들(=DB회사들)“이 제공한다.
- 만약 세탁기를 사용할때 다른 회사의 세탁기를 교체하더라도 사용방법이 비슷한 것과 마찬가지이다.
그렇다면 세탁기 회사는 일반적인 사용방법을 제공한다. (=세탁기 사용방법 인터페이스) - 인터페이스는 객체 생성이 되지 않기때문에 DB벤더들이 구현체를 제공한다 (=API)
- 따라서 JDBC를 구현하기위해서는 DB벤더가 제공하는 API (자바클래스 묶음=라이브러리)가 필요
JDBC 라이브러리 추가
- 💡JDBC를 사용하기 위해서는 “라이브러리”를 추가해야한다.
- IntelliJ에서 왼쪽 탭의 “External Libraries”에서 추가해야하고,
다양한 라이브러리를 추가해야할 수 있다.(스프링, MySQL, Oracle 등) - 각각의 DB벤더에 사이트에서는 라이브러리를 다운받을 수 있게 제공한다.
ex. MySQL은 Connector/J(=구현체)를 Java Jar파일로 제공
Maven / Gradle
- DB벤더 사이트에서 라이브러리를 직접 다운로드 하지 않아도
이 라이브러리를 편리하게 사용할 수 있도록 도와주는 도구 - Maven, Gradle같은 프로젝트로 생성하면,
직접 다운로드받아서 프로젝트에 추가하지 않아도 Maven, Gradle이 대신 수행
▶️실습 - IntelliJ에서 라이브러리 추가하기 (build gradle파일)
plugins {
id 'java'
}
group = 'org.example'
version = '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
}
test {
useJUnitPlatform()
}
- ⭐dependencies 부분 (=종속성) 에 필요한 라이브러리를 추가
- MySQL Workbench에서 (select version()) 을 통해 버전을 확인하고 해당하는 버전의 코드를 넣어준다.
- implementation 코드를 넣어줄 수 있다. (MySQL버전 9.1.0 사용)
- 이 코드가 구현체를 의미한다 (=API를 의미한다)
JDBC 문법
- Connection 객체 : 접속객체를 의미하며 추상화되어있다. DriverManager를 이용해 접속
- Statement객체 : SQL문 작성을 추상화해놓은 객체 SQL작성
- ResultSet 객체 : 결과값을 추상화한 객체
ex. hr데이터베이스에 있는 데이터를 ResultSet으로 추상화한 후 ResultSet 객체를 통해 결과를 가져옴 - 정리하자면,
[접속(connection객체) → SQL작성(statement객체) → 실행 → 결과 얻기(resultSet객체)]
▶️실습 - SQL 접속 확인
public static void main(String[] args) {
// 접속 확인!
// 1. 접속
String url = "jdbc:mysql://localhost:3306/scott"; // IP와 PORT를 말함 - 정해진 약속이 있음
String user = "like"; // USER NAME을 의미
String password = "lion"; // STORE IN VAULT의 패스워드 (MySQL Workbench기준)
Connection connection = null; // 접속이 추상화된 객체
connection = DriverManager.getConnection();
}
- DriverManager API를 살펴보면
- static으로 설정되어있어 객체로 만들 필요가 없음
- getConnection() : 이 메소드로 connection을 전달
- String을 이용해서 url을 작성하는데 이 url은 정해진 규칙이 있다.
String url = "jdbc:mysql://localhost:3306/scott";
- jdbc:mysql : jdbc와 mysql을 연결
- localhost : 어느 접속으로 할 것인지 - 로컬호스트
- 3306 : 포트번호를 의미
- scott : 데이터베이스 이름을 의미
- 이 정보들을 getConnection() 안에 매개변수로 넣어주는 것이다.
❓getConnection()오류가 발생할 경우
- 자바 SQL API를 살펴보면
main메소드에다가 throws Exception으로 예외처리를 넘겨주어야 getConnection() 오류가 해결
➡️SQLException이 발생하는 것인데 컴파일 시에는 체크하지 않고(=Unchecked Exception) 실행시에 발생하는 오류인 RuntimeException을 상속받지 않으므로,
컴파일 시 체크하는 (=Checked Exception) 예외에 해당된다.
따라서 컴파일러가 체크한 이 예외를 처리하지 않으면 컴파일 오류가 발생한다. 예외를 꼭 처리해주어야하는 것이다.
▶️실습 - getConnection() 오류 발생의 예외처리 넘기는 방법 2가지
- 예외처리를 단순히 넘기는 방법
public static void main(String[] args) throws Exception{
// ... 접속 부분코드 ...
if(connection != null){
System.out.println("접속 성공!");
}else System.out.println("접속 실패!");
connection.close();
}
- 예외처리를 직접 해주는 방법
Connection connectionExcept = null; // 접속이 추상화된 객체
try{
connectionExcept = DriverManager.getConnection(url, user, password);
if(connectionExcept != null){
System.out.println("접속 성공!");
}else System.out.println("접속 실패!");
}catch (SQLException e){
System.out.println(e);
}finally{ // 반드시 닫아야하므로 finally에 작성
// connection객체가 null이 아닐때만 닫아주는 것을 실행 (null 일때도 예외발생 가능성때문)
{if(connectionExcept != null){
try{
connectionExcept.close();
}catch (Exception e){
System.out.println(e);
}
}
}
❓실행시 한글깨짐 오류발생
➡️해결법 : IntelliJ에서 File → Settings → Build Tools검색 → Gradle → Build and run using과 run tests using을
Gradle대신 IntelliJ IDEA로 바꿔줌
❓MySQL 버전 8 이전에 발생할 수 있는 문제
// MySQL이 8 버전이후 부터는 생략해도 되는 부분
Class.forName("com.mysql.cj.jdbc.Driver"); // 드라이버 로딩 부분
- 드라이버 로딩 부분을 명시해주어야하지만 버전 8이후로는 이 코드가 생략되어도 실행되어도 문제가 되진 않는다.
- 중요한 코드이므로 기억해두는 것이 좋다.(=클래스를 인스턴스화 시켜주는 코드)
▶️실습 - INSERT 명령에 대한 메소드 구조 만들기
- 전체적인 구조
public static void main(String[] args) {
// 1. 필요한 객체가 무엇인지 [선언!]
// 3. 접속 [Connection객체 얻어오기!]
// 4. SQL 작성 [Statement 객체 얻어오기 및 사용!]
// 5. 실행
// 6. 결과 얻기 [ResultSet 객체 이용!]
// 2. 사용한 객체를 닫는다. [닫기!]
}
코드 작성 전에 먼저 MySQL에서 확인해보면
- INSERT 명령은 결과를 숫자로 반환하는 것을 알 수 있다.
- SELECT 명령은 데이터의 구조와 모양을 보여주는 것을 알 수 있다.
- 결과값을 표 형태로 보여주는데 이럴때 ResultSet 객체를 이용해야한다.
// 1. 필요한 객체가 무엇인지 [선언!]
Connection conn = null;
PreparedStatement ps = null;
try{
// 3. 접속 [Connection객체 얻어오기!]
// 4. SQL 작성 [Statement 객체 얻어오기 및 사용!]
// 5. 실행
// 6. 결과 얻기 [ResultSet 객체 이용!]
}catch(Exception e){
}finally{
// 2. 사용한 객체를 닫는다. [닫기!]
// 열리는 객체는 꼭 닫아줘야한다.
if(ps != null){
try{
ps.close(); // 닫는 것이 중요
}catch(Exception e){
System.out.println(e);
}
}
if(conn != null){
try{
conn.close(); // 닫는 것이 중요
}catch(Exception e){
System.out.println(e);
}
}
}
- 또한 이처럼 객체들이 만들어지지 않을 경우도 존재할 수 있으니 예외처리를 작성해야한다.
- 4번 SQL작성 부분에는
// 4. SQL 작성 [Statement 객체 얻어오기 및 사용!]
ps = conn.prepareStatement(sql);
- 이처럼 prepareStatement 메소드는 매개변수로 String sql 을 받는 것을 확인할 수 있다.
- 매개변수로 String sql이 필요하므로
// 1번방법)
// SQL문 선언 후
String sql = "";
// SQL문 작성
String sql = "insert into dept(deptno, dname, loc) values(70, 'INSERT TEST', '' ";
// 2번방법)
// 직접 SQL 넣어주기
ps = conn.prepareStatement("insert into dept(deptno, dname, loc) values(70, 'INSERT TEST', '' ");
- 이렇게 두가지 방법으로 String sql 매개변수에 값을 넣어줄 수 있다.
- 1번방법) 직접 매개변수 안에 SQL문을 작성해주는 경우
- 2번방법) 선언 부에 미리 선언해놓는 방법도 있다.
SQL문에서 values 안에서 PreparedStatement를 사용할때는 "바인딩"이라는 개념으로 값을 줄 수도 있다.
PreparedStatement
- PreparedStatement는 Statement의 인터페이스를 구현하고있고
- CallableStatement과 같은 Statement 종류도 있는 것을 알 수 있다.
▶️실습 - PreparedStatement 사용
// 2. SQL문 작성 - Statement 사용
int deptno = 90;
String dname = "lion";
String sql = "insert into dept(deptno, dname, loc) values(" + deptno + ", " + dname + ", '' ";
- 이렇게 String sql 안에 + 연산자를 사용해서 변수를 참조하게되면 오타로 인한 오류가 많이 발생할 수 있다.
- 또한 매번 다른 쿼리로 인식하여 가져온 후 실행하게되므로 성능이 잘 나오지 않는다.
// 2. SQL문 작성 - PreparedStatement 사용
String sql = "insert into dept(deptno, dname, loc) values(?,?,?);";
- 위와 같은 문제점을 개선하기 위해 PreparedStatement를 사용하게되면
값이 들어오는 부분에 ( ? ) 를 사용할 수 있다. - 이 ( ? ) 에 해당하는 값을 바인딩해주는 메소드를 제공하는 것이 PreparedStatement이다. > set()메소드
- ( ? ) 는 이 쿼리문을 DBMS시스템에 한 번 해석해서 가지고 있게되므로
쿼리를 한 번 번역해서 값으로 교체될 수 있는 기능을 가진다. - ⚠️( ? ) 에는 테이블명은 넣지 않는다.
- PreparedStatement의 메소드들 중 executeQuery() 와 executeUpdate()가 있다.
- MySQL Workbench에서 INSERT 명령을 수행해보면 int값을 반환하여 결과를 리턴해주므로
int를 반환타입으로 가지는 executeUpdate() 메소드를 호출한다.
CRUD [클래스] 만들기
▶️실습 - InsertTest 작성
- 예외처리로 "try-with-finally" 를 사용한다.
public static void main(String[] args) {
// 1. 필요한 객체가 무엇인지 [선언!]
Connection conn = null;
PreparedStatement ps = null;
String url = "jdbc:mysql://localhost:3306/scott";
String user = "like";
String password = "lion";
int deptno = 90;
// 값이 들어가는 부분에는 ? 를 쓴다 (PreparedStatement)
String sql = "insert into dept(deptno, dname, loc) values(?,?,?);";
try{
// 3. 접속 [Connection객체 얻어오기!]
conn = DriverManager.getConnection(url, user, password);
// 4. SQL 작성 [Statement 객체 얻어오기 및 사용!]
ps = conn.prepareStatement(sql);
ps.setInt(1, deptno); // 첫번째 (?)에는 int타입으로 deptno를 입력할 것
ps.setString(2, "교육부"); // 두번째 (?)에는 String타입으로 "교육부"를 입력할 것
ps.setString(3, "일산"); // 세번째 (?)에는 String타입으로 "일산"을 입력할 것
// 5. 실행
int count = ps.executeUpdate(); // 반환 값이 int
// 6. 결과 얻기 [ResultSet 객체 이용!]
System.out.println(count + " 건 입력했습니다.");
}catch(Exception e){
e.printStackTrace();
}finally{
// 2. 사용한 객체를 닫는다. [닫기 필수!]
if(ps != null){
try{
ps.close(); // 닫는 것이 중요
}catch(Exception e){
System.out.println(e);
}
}
if(conn != null){
try{
conn.close(); // 닫는 것이 중요
}catch(Exception e){
System.out.println(e);
}
}
}
}
- 데이터가 성공적으로 들어가는지 확인
- 오토커밋은 자동으로 설정되어있어서 scott 데이터베이스에 데이터가 바로 적용
- 똑같은 값을 INSERT 하게되면 catch블록에서 e.printStackTrace()로 예외가 발생
- MySQL Workbench로 확인해보면 [교육부, 일산]이 잘 들어가있다.
// 3. 접속 [Connection객체 얻어오기!]
conn = DriverManager.getConnection(url, user, password);
conn.setAutoCommit(false);
- 만약 들어간 값이 자동으로 COMMIT되어 데이터베이스에 변경사항이 적용되지 않고,
COMMIT 했을때만 들어가게하고 싶으면
setAutoCommit() 메소드를 이용하여 false를 시켜주면 된다. (오토커밋 해제)
ps.setInt(1, deptno);
ps.setString(2, "교육부");
ps.setString(3, "일산");
conn.commit();
- 만약 오토커밋을 해제되어있는 경우에는 데이터를 넣고난 후 commit()메소드로 COMMIT 명령을 수행한다.
▶️실습 - UpdateTest 작성
- 예외처리로 "try-with-resources" 를 사용한다.
- resources를 사용하는 이유 : 사용한 객체(conn, ps)는 닫아주어야하는데
자동 리소스 닫기 기능(resources)을 사용하는 것 - try블록 안에 close해야하는 자원들을 모두 선언 (자동으로 리소스를 닫아줌)
- close() 메소드 생략으로 코드가 간편해짐
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/scott";
String user = "like";
String password = "lion";
// UPDATE 쿼리문 작성, 값이 들어가는 부분에는 ? 를 쓴다. (PreparedStatement)
String sql = "update dept set dname=? where deptno=?";
int deptno = 80;
// try-with-resources
try( // resources 부분
Connection conn = DriverManager.getConnection(url, user, password);
PreparedStatement ps = conn.prepareStatement(sql);
){
ps.setString(1, "마케팅");
ps.setInt(2, 80);
int count = ps.executeUpdate();
System.out.println(count + " 건 수정했습니다.");
}catch(Exception e){
e.printStackTrace();
}
}
LOCK 락 발생
❓만약 UPDATE를 할때 수정이 되지 않는 경우
(오토커밋이 해제되어있는 상태에서 주로 발생)
- 데이터베이스는 각각 다른 세션을 만들 수 있다. (A사용자, B사용자)
- 각 세션 별로 "같은 데이터베이스"에서 UPDATE를 수행하게되면 락이 걸릴 수가 있다.
- ex.80번 부서의 데이터 수정을 진행한 A사용자와
80번 부서의 데이터를 수정하려하는 B사용자가 있을때
오토커밋이 설정되지 않아 A사용자의 수정사항이 데이터베이스에 반영되지 않은 경우이다.
(A사용자가 COMMIT or ROLLBACK을 결정하지 않음)
이렇게 사용자들이 UPDATE를 하는 경우 “기다리는 경우”가 있는데 “락”이 걸린 것이다.
(Update에 대한 클래스를 실행했을때 "n 건 수정했습니다.” 같은 문구가 출력되지 않고 무한정 기다리는 상태이면 “락”이 걸린 것이다.)
❓락이 걸리는 이유
➡️“DBMS가 가지는 정책”으로 인해 데이터베이스의 무결성을 유지하게되는데
데이터가 수정될때 이러한 정책으로 인해 DBMS는 “락”을 걸어놓을 수 있다.
❓데드락 DEAD LOCK
➡️두 세션 모두 이도저도 못하는 상태를 의미
이럴 경우 각 세션에서 COMMIT하거나 ROLLBACK하는 등의 명령을 수행하여 변경사항을 적용하거나 되돌려야한다.
➡️데드락이 걸린 상태가 되면 DBMS는 일정 시간 후에 데드락이 걸린 세션들의 접속을 끊게된다.
▶️실습 - DeleteTest 작성
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/scott";
String user = "like";
String password = "lion";
// DELETE 쿼리문 작성, 값이 들어가는 부분에는 ? 를 쓴다. (PreparedStatement)
String sql = "delete from dept where deptno=?";
int deptno = 55;
// try-with-resources
try( // resources 부분
Connection conn = DriverManager.getConnection(url, user, password);
PreparedStatement ps = conn.prepareStatement(sql);
){
// prepareStatement 문에는 ? <- 가 들어있으니 그 ? 를 표시해주어야한다.
ps.setInt(1, deptno);
int count = ps.executeUpdate();
System.out.println(count + " 건 삭제했습니다.");
}catch(Exception e){
e.printStackTrace();
}
}
- 55번 부서번호의 데이터가 잘 삭제된 것을 확인할 수 있다.
CRUD [메소드] 만들기
❓Dept 테이블에 대해서 입력, 수정, 삭제 하는 기능을 구현
➡️그 테이블의 CRUD를 수행하는 클래스를 "하나"만들고 클래스 안에 CRUD의 기능을 하는 메소드를 작성한다.
DAO - Data Access Object
⭐그러한 객체들을 잘 구현해는 패턴으로 DAO 라는 것을 사용
- dept 테이블이 있으면 deptDAO를 만들게되면
dept테이블에 대한 객체들을 DAO에 담아둘 수 있게 된다. - dept테이블에 대해 CRUD를 하고 싶을때는 deptDAO에서 찾으면 되는 것이다.
▶️실습 - dept테이블의 DAO 만들어보기
// 기본 CRUD - CREATE(생성), READ(조회), UPDATE(수정), DELETE(삭제)
// INSERT 메소드 만들기 - c
// UPDATE 메소드 만들기 - u
// DELETE 메소드 만들기 - d
- 각 메소드 별로 접속(Connection)과 닫는 코드(close)가 있어야하는데 그렇게 되면 코드가 중복될 수 있다.
- 접속과 닫는 기능을 하는 메소드 구현하면 좋을 것이다.
- ➡️특정 테이블에 대한 접속, 닫는 코드만 필요한 것이 아니므로
접속, 닫는 코드를 어느 테이블에서도 사용할 수 있도록 별도의 클래스를 만들어야할 것
→ 클래스 만들기 (DBUtil) (= 자주 사용하는 클래스명)
DTO - Data Transfer Object
- 데이터를 한꺼번에 담을 수 있게 도와준다.
- DTO는 데이터 전송 객체로 프로세스 간에 데이터를 전달하는 객체를 의미
- 데이터를 한꺼번에 가지고 다닐 가방 객체인 느낌이다.
public boolean addDept(){
return false;
}
- 만약 dept테이블에 데이터를 한 건 입력하는 메소드를 이렇게 작성한다했을때
- 누구나 사용할 수 있어야하고(public), 반환타입은 (int, void, boolean) 모두 가능할 것이다.
- 매개변수에는 PreparedStatement에서 만든 쿼리문의 ( ? ) 에 들어갈 값을 입력받아야한다.
- 각 테이블마다 매개변수는 다를 수 있기때문에 타입과 변수명을 정의하기가 어렵다.
이럴때 정의하는 것이 DTO 인 것이다.
비슷하게 VO (=Value Object)도 존재하는데 이 또한 "값만 담는 객체"이다.
❓VO와 DTO의 차이
➡️둘다 데이터를 전달하는 역할을 하지만 목적/속성에 차이가 있음
- DTO는 데이터 전송 목적으로 데이터 교환을 위해 사용하기때문에 getter, setter만 존재
주로 외부 시스템과 데이터 통신 시 사용, 중요한 정보는 노출시키지 않는 특징 - VO는 불변성을 가지며 특정 비즈니스 값을 가지게되므로 Read Only (읽기만 가능) 속성을 가짐
주로 데이터베이스에서 가져오는 데이터를 표현할때 사용 - ex. 웹화면 로그인 정보 >> DTO 처리
데이터베이스의 테이블 관련 데이터 >> VO 처리
▶️실습 - DeptDTO 만들기
public class DeptDTO {
private int deptno;
private String dname;
private String loc;
// Getter, Setter 부분
}
- DeptDTO에 테이블의 컬럼들을 (필드) 선언하고, Getter / Setter 메소드를 구현하면된다.
public boolean addDept(DeptDTO deptDTO){
return false;
}
- DeptDAO에서 INSERT명령을 수행하는 addDept 매개변수로는 데이터를 담은 가방인 DeptDTO를 넣을 수 있게된다.
▶️실습 - DBUtil 클래스 : 접속 메소드와 닫기 메소드 만들기
- 접속 메소드 - 1번 방법
// 첫번째 접속 방법 (connect)
public static Connection getConnection() throws Exception {
// 설정 값이 들어오지 않으면 기본값은 이 정보들을 사용할 것
String url = "jdbc:mysql://localhost:3306/scott";
String user = "like";
String password = "lion";
Connection connection = DriverManager.getConnection(url, user, password);
return connection;
}
- Connection 타입으로 반환
- Connection객체를 얻을때는 그 객체가 인스턴스화 될 필요가 없기때문에 편리하게 사용하기 위해
static로 선언하고 있다. (new 키워드로써 생성하지 않고 클래스로 직접 접근 가능) - 사용자에게 예외처리를 넘긴다 (throws)
- 첫번째 방법 접속 메소드 작성은 String으로 값을 직접 지정하여 넣는 방법이다.
- 접속 메소드 - 2번 방법
// 두번째 접속 방법 (connect) - 유저이름과 비밀번호를 입력받아 connection을 만들도록함
public static Connection getConnection(String user, String password) throws Exception {
String url = "jdbc:mysql://localhost:3306/scott";
Connection connection = DriverManager.getConnection(url, user, password);
return connection;
}
- 위의 1번방법인 getConnection() 메소드를 오버로딩한 사례이다.
- 설정값인 유저이름과 비밀번호를 매개변수로 입력받아 그 값을 connection으로 넣어주는 방법이다.
- 닫기 메소드
// 닫기 (close)
public static void close(Connection conn){
if(conn != null){
try{
conn.close();
}catch(SQLException e){
throw new RuntimeException(e);
}
}
}
// 닫기 (close) - Connection만 받는 close()메소드를 오버로딩하여 PreparedStatement 추가
public static void close(Connection conn, PreparedStatement ps){
if(ps != null){
try{
ps.close();
}catch(SQLException e){
throw new RuntimeException(e);
}
}
close(conn); // Connection 만을 받는 메소드를 호출하여 Connection을 닫아준다.
}
// 닫기 (close) - Connection과 PreparedStatement 을 받는 close()메소드를 오버로딩하여 ResultSet 추가
public static void close(Connection conn, PreparedStatement ps, ResultSet rs){
if(rs != null){
try{
rs.close();
}catch(SQLException e){
throw new RuntimeException(e);
}
}
close(conn, ps);
}
- 이렇게 3가지 경우를 오버로딩하여,
사용자가 어떤 객체를 닫을건지를 매개변수로 입력받아 그에 따라 각기 다른 메소드가 실행될 수 있도록 한다.
- 예를 들어 INSERT메소드에서 Connection과 PreparedStatement 객체를 닫고자할때
두 객체 (conn, ps) 를 매개변수로 갖는 두번째의 close()메소드를 호출해주면 될 것이다.
▶️실습 - INSERT 메소드
- DeptDTO클래스와 DBUtil클래스를 활용
// INSERT 메소드 만들기 - c (1. 리턴타입 boolean사용)
public boolean addDept(DeptDTO deptDTO){
boolean flag = false; // 제대로 동작했는지를 체크하기 위한 플래그
// 1. 필요한 객체 선언
Connection conn = null;
PreparedStatement ps = null;
String sql = "insert into dept(deptno, dname, loc) values(?,?,?);";
try{
// 2. 접속
conn = DBUtil.getConnection("KimJava", "k1234");
ps = conn.prepareStatement(sql);
// 첫번째 ? 값 : deptDTO클래스에서 deptno값을 꺼내기 위해 Getter메소드 사용
ps.setInt(1, deptDTO.getDeptno());
// 두번째 ? 값 : deptDTO클래스에서 dname값을 꺼내기 위해 Getter메소드 사용
ps.setString(2, deptDTO.getDname());
// 세번째 ? 값 : deptDTO클래스에서 loc값을 꺼내기 위해 Getter메소드 사용
ps.setString(3, deptDTO.getLoc());
// 3. 쿼리 실행
int count = ps.executeUpdate();
if(count == 1){ // 제대로 동작했을 경우 count값이 증가하므로 flag를 true로 변경
flag = true;
}
}catch(Exception e){
System.out.println(e);
}finally{
// 4. 선언한 객체 닫기
DBUtil.close(conn, ps);
}
return flag; // flag를 반환하여 제대로 동작했는지, 안했는지를 boolean형으로 반환
}
<유저의 INSERT 명령 수행>
DeptDAO deptDAO = new DeptDAO();
// 값을 담을 가방 만들기
DeptDTO deptDTO = new DeptDTO();
deptDTO.setDeptno(77);
deptDTO.setDname("TEST_DEPART");
deptDTO.setLoc("제주도");
// INSERT 테스트 실행
// 값을 담은 가방을 deptDAO 클래스 안의 addDept메소드의 인자로 전달
// boolean형 result에 담아 제대로 성공했는지 안했는지를 검사
boolean result = deptDAO.addDept(deptDTO);
if(result) System.out.println("DAO, DTO, DBUtil 성공!!");
else System.out.println("DAO, DTO, DBUtil 실패!!");
- 💡CRUD기능을 수행할 수 있는 메소드가 담긴 DeptDAO 클래스를 인스턴스로 만든다.
- 💡DeptDAO 클래스 안에 있는 addDept() 메소드의 인자로 보낼 값을 담기 위해
- 💡컬럼들과 Getter/Setter가 구현된 DeptDTO 클래스를 인스턴스로 만든다.
- Setter로 샘플데이터를 넣어주고, boolean 형으로 반환하는 addDept()메소드 안에 DTO를 넣어준다.
- DB에 [77번 부서의 TEST_DEPART, 제주도] 샘플데이터가 잘 담긴 것을 확인할 수 있다.
▶️실습 - UPDATE 메소드
// UPDATE (리턴타입 int), try-with-resources 사용
public int updateDept(DeptDTO deptDTO){
int resultCount = 0;
String sql = "update dept set dname=? where deptno=?";
try(
Connection conn = DBUtil.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
){
// 사용자로부터 deptDTO의 setter로 설정된 값들을 getter로 가져올 것임
ps.setString(1, deptDTO.getDname());
ps.setInt(2, deptDTO.getDeptno());
resultCount = ps.executeUpdate();
}catch(Exception e){
throw new RuntimeException(e);
}
return resultCount;
}
- int형 타입으로 반환하므로 resultCount 변수를 만들어
- ps.executeUpdate()메소드 실행에 따른 성공여부 int값을 입력받고 그 값을 메소드 종료 시 반환한다.
<유저의 UPDATE 명령 수행>
DeptDAO deptDAO = new DeptDAO();
// 값을 담을 가방 만들기
DeptDTO deptDTO = new DeptDTO();
// INSERT 코드 대신 UPDATE 코드
deptDTO.setDeptno(77);
deptDTO.setDname("INSERT TO UPDATE");
deptDTO.setLoc("제주도수정");
// count가 반환되므로 [숫자 + 건 수정됨] 출력
System.out.println(deptDAO.updateDept(deptDTO) + " 건 수정됨");
- UPDATE 요청한 것이 잘 수정된 것을 확인가능
▶️실습 - DELETE 메소드
// DELETE (리턴타입 void)
public void deleteDept(int deptno){
String sql = "delete from dept where deptno=?";
try(
Connection conn = DBUtil.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
){
// 사용자로부터 deptDTO의 setter로 설정된 값들을 getter로 가져온 후
// deptno에 해당하는 값을 삭제
ps.setInt(1, deptno);
ps.executeUpdate();
}catch(Exception e){
throw new RuntimeException(e);
}
}
- 매개변수로 deptno만 받아 삭제할 수 있게함
<유저의 DELETE 명령 수행>
DeptDAO deptDAO = new DeptDAO();
// 값을 담을 가방 만들기
DeptDTO deptDTO = new DeptDTO();
deptDAO.deleteDept(77);
- 어떠한 출력문도 지정하지 않았기에 프로세스 완료 문구만 출력
- Workbench로 확인해보면 [77번 부서의 내용]이 지워진 것을 확인할 수 있다.
▶️실습 - READ 클래스
- User가 SELECT 명령 수행하는 클래스
- 첫번째방법) 조건에 맞는 "한 건의 데이터만" 조회
public class SelectTest {
public static void main(String[] args) {
// 1. 선언
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null; // 결과를 담아줄 ResultSet (SELECT 연산)
String sql = "select deptno, dname, loc from dept where deptno=?";
try{
// 3. 접속 : DBUtil의 getConnection으로 Connection 객체 얻어오기
conn = DBUtil.getConnection();
// 4. 쿼리 작성
ps = conn.prepareStatement(sql);
ps.setInt(1, 10); // 부서번호가 10인 부서테이블의 deptno, dname, loc 조회
// 5. 쿼리 실행
rs = ps.executeQuery();
}catch(Exception e){
throw new RuntimeException(e);
}finally{
// 2. 선언 후에는 먼저 닫아야함 (닫는코드)
DBUtil.close(conn, ps, rs);
}
}
}
- String sql 안에 ( ? ) 을 지정하여 특정 행 (로우)를 가리키도록 지정한다.
만약 여러 건의 데이터를 지정할때는 이 부분을 빼주면 될 것이다. - finally블록에서는 DBUtil에서 ResultSet까지 닫을 수 있는 오버로딩된 close()메소드 호출
- 5번 쿼리 실행 단계에서 리턴타입 ResultSet이 반환됨
rs가 표와 같은 결과값을 가리키는 "주소값"이 담김 > 이 주소값을 통해서 결과를 얻어내야함 - 처음 커서는 데이터베이스의 전체 표를 가리키지만 커서를 특정 레코드(행)으로 옮겨서 그 값을 가져올 수 있음
- 이 커서를 옮기는 메소드가 "next()메소드"
- 다음행의 값이 유효하면 true를 리턴, 더이상읽을 행의 값이 없으면 false리턴
- 예외처리 부분 : rs는 결과가 없을 수도 있기때문에 rs.next()를 조건문으로 감싸준다.
- 두번째방법) 여러 건의 데이터 조회
public class SelectManyTest {
public static void main(String[] args) {
// 1. 선언
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null; // 결과를 담아줄 ResultSet (SELECT 연산)
// 2번방법 = ( ? ) 바인딩한 부분을 빼주면 전체 조회가 가능할 것
String sql = "select deptno, dname, loc from dept where deptno";
try{
// 3. 접속 : DBUtil의 getConnection으로 Connection 객체 얻어오기
conn = DBUtil.getConnection();
// 4. 쿼리 작성
ps = conn.prepareStatement(sql);
// 5. 쿼리 실행
rs = ps.executeQuery();
// 6. 결과 얻어오기
// 더이상 next()값이 없을때까지 반복
while(rs.next()){
System.out.println(rs.getInt(1));
System.out.println(rs.getString(2));
System.out.println(rs.getString(3));
}
}catch(Exception e){
throw new RuntimeException(e);
}finally{
// 2. 선언 후에는 먼저 닫아야함 (닫는코드)
DBUtil.close(conn, ps, rs);
}
}
}
- 여러 건의 데이터를 조회할때는 while문으로 처리해줄 수 있다.
▶️실습 - READ 메소드
- DeptDAO 클래스 안에 READ를 할 수 있는 메소드를 추가한다 (getDept())
- 첫번째 메소드) 조건에 맞는 "한 건의 데이터만" 조회하는 메소드
public DeptDTO getDept(int deptno){
DeptDTO deptDTO = new DeptDTO(); // 가방을 먼저 만들어야한다.
// 1. 선언
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
// 바인딩 할 값 = ?
String sql = "select deptno, dname, loc from dept where deptno=?";
try{
conn = DBUtil.getConnection();
ps = conn.prepareStatement(sql);
ps.setInt(1, deptno); // 값 바인딩
rs = ps.executeQuery(); // 값 얻기
if(rs.next()){
// 이 가방에는 ResultSet에서 첫번째 컬럼의 값을 Setter메소드의 인자로 전달
deptDTO.setDeptno(rs.getInt("deptno"));
deptDTO.setDname(rs.getString("dname"));
deptDTO.setLoc(rs.getString("loc"));
}
}catch(Exception e){
throw new RuntimeException(e);
}finally{
DBUtil.close(conn, ps, rs);
}
// 값을 담은 가방을 리턴함
return deptDTO;
}
- 한 건의 데이터를 조회하는 메소드로 DeptDTO 타입을 반환
<유저의 READ 명령 수행>
// 공통적인 인스턴스 생성 부분
DeptDAO deptDAO = new DeptDAO();
DeptDTO deptDTO = new DeptDTO(); // 값을 담을 가방 만들기
// -> 꺼내고 싶은 부서번호를 입력
DeptDTO resultDTO = deptDAO.getDept(10);
// System.out.println(resultDTO.getDname()); // dname꺼내서 출력해보기
System.out.println(resultDTO); // toString을 오버라이딩해서 출력
- 부서번호 10번에 해당하는 행(로우)를 출력한다.
- 두번째 메소드) 여러 건의 데이터를 조회하는 메소드
public List<DeptDTO> getDeptList(){
// List는 인터페이스이므로 자식클래스인 ArrayList로 인스턴스 생성
List<DeptDTO> deptList = new ArrayList<>();
// 1. 선언
Connection conn = null; // 접속 객체
PreparedStatement ps = null; // 명령 수행 객체
ResultSet rs = null; // 결과값 받을 객체
try{
conn = DBUtil.getConnection();
ps = conn.prepareStatement("select deptno, dname, loc from dept where deptno");
rs = ps.executeQuery();
// next()할때마다 다음 행(레코드)을 가리킨다. 가리킬게 있으면 true, 없으면 false
while(rs.next()){
DeptDTO deptDTO = new DeptDTO();
deptDTO.setDeptno(rs.getInt(1));
deptDTO.setDname(rs.getString(2));
deptDTO.setLoc(rs.getString(3));
// 리스트에 하나의 행(로우)에 담긴 1, 2, 3번 컬럼의 데이터가 추가
deptList.add(deptDTO);
// 그 다음 while문을 진행하면 list의 다음 부분에
// 그 다음 행에 담긴 1, 2, 3번 컬럼의 데이터가 추가
}
}catch(Exception e){
throw new RuntimeException(e);
}finally{
DBUtil.close(conn, ps, rs);
}
return deptList;
}
- DeptDTO 타입으로 반환하며, List를 사용하여 여러 건의 데이터를 담게 한다.
- while문을 진행할때마다 가방을 만들어 그 1번 가방(리스트)에 해당 로우의 값들을 담고,
- 다음 while문에서는 또 새로운 가방을 만들어 그 2번 가방(리스트)에 그 해당 로우의 값들을 담는 것이다.
- 이 가방(리스트)를 반환하게 되면 여러 건의 데이터가 담긴 값을 확인할 수 있게된다.
- ⚠️만약 while문 밖에서 선언된 경우 값을 담을 가방이 "하나"만 만들어진다.
이럴 경우 같은 데이터만 가리키므로 리스트에 값을 지속해서 추가해주어도 똑같은 데이터값만 나온다.
<유저의 READ 명령 수행>
List<DeptDTO> list = deptDAO.getDeptList();
for(DeptDTO dto : list){
System.out.println(dto);
}
- DeptDTO 타입으로 리스트를 만들고,
- 그 list 참조변수 안에 deptDAO 클래스의 getDeptList() 메소드로부터
- 반환받은 값(return deptList)를 넣는다.
- forEach 문으로 DeptDTO타입의 list로부터 DeptDTO타입의 객체를 하나 꺼내서 출력해준다.
반복하여 DeptDTO타입의 객체가 여러 건 출력되는 것이다.
💡회고로 정리하면서 내용을 익힐 것이 많았다. 특히 클래스로 선언한 CRUD와 메소드로 선언한 CRUD를 실습하고, 유저를 만들어 사용하는 실습을 하다보니 반환타입에 따라 가져오는 결과가 다른 것도 알 수 있었다.
DAO, DTO와 같은 개념을 통해 데이터베이스의 값을 CRUD 연산할 수 있는 것을 손쉽게 해볼 수 있어서 배운 것이 많았다🚀
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_17일차_''Java IO" (1) | 2024.12.24 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_16일차_''컬렉션 프레임워크" (0) | 2024.12.23 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_14일차_'DML, DDL, TCL' (0) | 2024.12.19 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_13일차_'조인과 서브쿼리' (0) | 2024.12.18 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_12일차_'MySQL+쿼리' (0) | 2024.12.17 |