공부하는 입장이기 때문에 혹시 글에 잘못된 점이 있다면 댓글로 알려주시면 감사하겠습니다!
저번에 작성했던 글에 이어서 오늘은 JDBC를 작성하는데 사용되는 Factory 패턴과 PreparedStatment에 대한 글을 적으려고 한다.
PreparedStatement
저번에 작성했던 글을 보면 SQL문을 보낼 Statement 객체는 3가지 종류가 있다.
- Statement : SQl을 보내기 위한 통로. 인자가 없음.
- PreparedStatement : Statement와 동일한데 차이점은 인자값으로 SQL을 받기 때문에 특정한 SQL에 대한 통로라고 생각하면 된다.
- CallableStatement : PL/SQL을 호출할 때 사용
이 중에서 마지막 statement는 아직 PL/SQL을 배우지 않았기 때문에 제외하고 말하려고 한다.
Statement를 사용하면 네트워크 트래픽이 많이 발생하고, SQL-Injection 보안 이슈가 발생하게 된다. 이를 보완하기 위해서 PreparedStatement를 사용한다. PreparedStatement는 특정 SQL의 전용 통로라고 생각하면된다.
setString함수를 사용해서 데이터를 SQL 문자열로 자동 변환 해주며, 데이터만 전송하기 때문에 네트워크 트래픽도 감소한다. 또한 값으로 대체되기 때문에 SQL-Injection 보안 이슈도 해결된다. 그래서 PreparedStatement를 사용하는 방법을 작성해보려고한다.
우선 전에 사용했던 예제를 가져와서 사용해보려고 한다.
MemberDao.java파일에서 이 전까지는 전체조회하는 함수와 유저의 이름을 받아서 상세조회하는 함수만 구현했다.
그래서 일단은 저번에 동일한 방식으로 유저를 추가하는 함수를 만든다.
//회원 추가
public void addUser(User dto){
//1. JDBC Driver 로딩
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Connection conn = null;
Statement stmt = null;
try {
conn = DriverManager.getConnection(url, user, password);
stmt = conn.createStatement(); //3. SQL 실행 통로 형성
//SQL문
String sql = "insert into user values ('";
sql+= dto.getUserId() +"' ,'";
sql+= dto.getUserPw() +"','";
sql+= dto.getName() +"','";
sql+= dto.getPhone() +"','";
sql+= dto.getGrade() +"',";
sql+= dto.getAge() +")";
int result = stmt.executeUpdate(sql);
//5. SQL 결과 처리
if(result == 1) {
System.out.println("유저 등록 완료");
}else {
System.out.println("유저 등록 실패 ");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
우선 CUD 를 사용할 때는 executeUpdate를 R를 사용할 때는 executeQuery를 사용하기 때문에 유저를 추가하는 함수에서는 executeUpdate를 사용해줬다. executeUpdate의 반환값을 int인데 결과에 따라서 1 또는 0을 반환해준다.
SQL문을 작성하는 곳을 보면 전부다 + 기호를 사용해서 String을 붙여주고 있다. 이렇게 String을 사용하게 되면 작성하기에도 불편하고 시간도 오래걸리기 때문에 PreparedStatement를 사용해서 바꿔주려고 한다.
PreparedStatement를 사용하게 되면 통로를 생성할 때 sql을 인자로 가지고 생성한다. 그렇기 때문에 전용 통로가 생긴다고 말할 수 있다.
위의 코드에서 Statment를 선언하는 곳과 통로를 생성해주는 곳을 변경해준다.
PreparedStatement pstmt = null; //statement 선언부
pstmt = conn.prepareStatement(sql); //통로 생성부
그 다음이 이제 가장 유용한 것이 PreparedStatement을 사용하면 변수를 ?으로 대체 가능하다는 것이다. 아까는 + 을 사용해서 변수와 String을 연결해줬는데 PreparedStatement을 사용하면 우선
String sql = "insert into user values (?,?,?,?,?,?)";
이렇게 선언한다. 주의해아할 점은 ""안에 세미콜론은 포함되면 안된다. 에러남 !
총 6개의 인자가 들어갈 예정이고 이제 각 ? 에 맞는 변수를 매칭해주면 된다. 각 타입에 맞춰서 설정해준다. setString과 setInt 등
pstmt.setString(1, dto.getUserId());
pstmt.setString(2, dto.getUserPw());
pstmt.setString(3, dto.getName());
pstmt.setString(4, dto.getPhone());
pstmt.setString(5, dto.getGrade());
pstmt.setInt(6, dto.getAge()); //타입이 int이므로 setInt로
그러고나서 이제 execute함수를 실행해주면 된다.
int result = pstmt.executeUpdate();
여기서 중요한것은 executeUpdate안에 sql을 파라미터로 넣어주면 안된다. (방금하다가 나도 오류났다..)
Statement을 사용했을때에는 안에 sql을 파라미터로 넣어줬지만 preparedStatement는 이미 sql문을 사용해서 통로을 만들었기 때문에 다시 넣어줄 필요가 없다.
그리고 executeUpdate 의 반환값을 통해서 성공,실패 여부를 출력해주면 된다.
마지막에 자원해제도 해준다.
<전체 코드>
// 회원 추가
public void addUser(User dto) {
// 1. JDBC Driver 로딩
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = DriverManager.getConnection(url, user, password);
// SQL문
String sql = "insert into user values(?,?,?,?,?,?)";
pstmt = conn.prepareStatement(sql); // 3. SQL 실행 통로 형성
pstmt.setString(1, dto.getUserId());
pstmt.setString(2, dto.getUserPw());
pstmt.setString(3, dto.getName());
pstmt.setString(4, dto.getPhone());
pstmt.setString(5, dto.getGrade());
pstmt.setInt(6, dto.getAge()); // 타입이 int이므로 setInt로
int result = pstmt.executeUpdate();
// 5. SQL 결과 처리
if (result == 1) {
System.out.println("유저 등록 완료");
} else {
System.out.println("유저 등록 실패 ");
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (pstmt != null) {
try {
pstmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
추가적으로)
추가적으로 String보다는 StringBuilder를 사용하면 속도면에서 훨씬 빠르게 실행할 수 있다. 그렇기 때문에 String보다는 StringBuilder를 사용해서 sql문을 작성해주는 것이 속도를 개선시켜줄 수 있다.
StringBuilder sql = new StringBuilder();
sql.append("insert into user values(?,?,?,?,?,?)");
pstmt = conn.prepareStatement(sql.toString()); //prepareStatement의 파라미터는 String이기 때문에 toString을 사용해서 String으로 변환
pstmt.setString(1, dto.getUserId());
pstmt.setString(2, dto.getUserPw());
pstmt.setString(3, dto.getName());
pstmt.setString(4, dto.getPhone());
pstmt.setString(5, dto.getGrade());
pstmt.setInt(6, dto.getAge()); //타입이 int이므로 setInt로
prepareStatement의 파라미터는 String이기 때문에 toString을 사용해서 String으로 변환해줘야한다.
FactoryDao
이제 드라이버와 연결하고, DB서버에 접속, Result객체 생성과 모든 자원 해제를 간단하게 할 수 있는 Factory 패턴을 사용해보려고 한다.
지금까지 작성했던 함수를 보면 매 함수마다 자원을 할당하고 해제해줬다. 같은 일을 하는 코드가 중복되서 있으면 유지보수하기도 어렵고 지저분해보일 수 있다. 그러므로 중복된 코드는 따로 클래스를 생성해서 호출해서 사용하도록 하려고 한다.
우선 같은 패키지 안에 FactoryDao.java 파일 생성해준다.
JDBC의 순서를 다시 보면
1. JDBC Driver 로딩
2. DBMS 서버 연결
3. SQL 실행 통로 생성
4. SQL 실행 요청 및 결과 받기
5. SQL 실행 결과 처리
6. DB 자원 해제
이중에서 굵은 색으로 표시된 부분을 FactoryDao.java파일에 싱글톤 패턴을 사용해서 구현할 것이다.
우선 드라이버 로딩과 DB연결을 위한 String 변수 4개를 가져온다.
private String driver = "com.mysql.cj.jdbc.Driver";
private String url = "jdbc:mysql://127.0.0.1:3306/testdb?serverTimezone=UTC&useUniCode=yes&characterEncoding=UTF-8";
private String user = "[DB접속id]";
private String password = "[DB접속pwd]";
그리고 이제 싱글톤 패턴을 위한 instance함수와 getInstance함수를 선언해준다.
private static FactoryDao instance = new FactoryDao(); //싱글톤 패턴을 위한 객체 생성
public static FactoryDao getInstance() { //객체를 가져오기 위한 함수
return instance;
}
이제 3개의 함수를 만들것이다.
1. JDBC Driver 로딩 함수
//1. JDBC Driver 로딩
private FactoryDao() {
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
JDBC를 로딩하는 Class.forName(driver); 를 수행하는 함수를 만들어준다.
2. DB서버 연결 함수
//2. DB서버 연결
public Connection getConnection() throws SQLException {
return DriverManager.getConnection(url, user, password);
}
DriverManager.getConnection(url, user, password)을 수행해서 반환받은 Connection객체를 바로 리턴해준다.
3. 자원 해제 함수
//3. 자원 해제
public void close(Connection conn, Statement stmt, ResultSet rs) {
if(rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
3개의 객체를 받아서 만약 null이 아니라면 각각 close함수를 수행해서 자원을 해제해준다.
그런데 executeUpdate() 함수를 수행하는 경우에는 ResultSet객체를 사용하지 않는다. 그런 경우에는 어떻게 하면 될까?
오버로딩을 사용해서 해결 할 수 있다.
//3. 자원 해제 함수 오버로딩
public void close(Connection conn, Statement stmt) {
close(conn, stmt, null);
}
<전체 코드>
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class FactoryDao {
private String driver = "com.mysql.cj.jdbc.Driver";
private String url = "jdbc:mysql://127.0.0.1:3306/[db이름]?serverTimezone=UTC&useUniCode=yes&characterEncoding=UTF-8";
private String user = ;
private String password = ;
private static FactoryDao instance = new FactoryDao(); //싱글톤 패턴을 위한 객체 생성
public static FactoryDao getInstance() { //객체를 가져오기 위한 함수
return instance;
}
//1. JDBC Driver 로딩
private FactoryDao() {
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
//2. DB서버 연결
public Connection getConnection() throws SQLException {
return DriverManager.getConnection(url, user, password);
}
//3. 자원 해제
public void close(Connection conn, Statement stmt, ResultSet rs) {
if(rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
//3. 자원 해제 함수 오버로딩
public void close(Connection conn, Statement stmt) {
close(conn, stmt, null);
}
}
그렇다면 이제 Dao 클래스에서는 어떤식으로 사용하면 될까?
우선 FactoryDao객체를 가져온다. 싱글톤 패턴으로 구현되었기 때문에 다음과 같은 코드를 사용해서 객체를 사용한다.
private FactoryDao factory = FactoryDao.getInstance();
전체 코드를 보면서 하나씩 대체해가면 된다. 기존에 각 기능에 해당하는 부분을 FactoryDao에 포함된 함수로 대체해준다. 위에서 만들었던 유저 추가 함수부분만 보면 다음과 같다.
// 회원 추가
public void addUser(User dto) {
// 1. JDBC Driver 로딩 => factory 객체를 가져오면서 이미 수행됨
// try {
// Class.forName(driver);
// } catch (ClassNotFoundException e) {
// e.printStackTrace();
// }
Connection conn = null;
PreparedStatement pstmt = null;
try {
// conn = DriverManager.getConnection(url, user, password); //아래 문장으로 대체
conn = factory.getConnection();
// SQL문
String sql = "insert into user values(?,?,?,?,?,?)";
pstmt = conn.prepareStatement(sql); // 3. SQL 실행 통로 형성
pstmt.setString(1, dto.getUserId());
pstmt.setString(2, dto.getUserPw());
pstmt.setString(3, dto.getName());
pstmt.setString(4, dto.getPhone());
pstmt.setString(5, dto.getGrade());
pstmt.setInt(6, dto.getAge()); // 타입이 int이므로 setInt로
int result = pstmt.executeUpdate();
// 5. SQL 결과 처리
if (result == 1) {
System.out.println("유저 등록 완료");
} else {
System.out.println("유저 등록 실패 ");
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
factory.close(conn, pstmt);
}
}
만약 ResultSet 객체도 있다면 마지막에
factory.close(conn, pstmt, rs);
으로만 변경해주면 된다.
'개발 공부 > Java' 카테고리의 다른 글
[JAVA] JDBC란? 자바에서 JDBC 사용하기 (JAVA + MYSQL) (0) | 2022.03.19 |
---|