반응형
1. AutoCommit 비활성화 처리한 JDBC 프로그램 작성법
TransactionControlApp.java
package xyz.itwill.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
//JDBC 프로그램은 기본적으로 AutoCommit 기능이 활성화 처리되어 있어 SQL 명령(DML)이 전달되어
//실행되면 자동으로 커밋 처리
// => 프로그램 실행시 예외가 발생한 경우 예외 발생전에 전달되어 실행된 SQL 명령에 대한 롤백 처리 불가능
//그래서 JDBC 프로그램에서는 AutoCommit 기능을 비활성화 처리하여
//예외 발생 없이 프로그램이 정상적으로 실행된 경우 커밋 처리하고 예외가 발생된 경우 롤백 처리하는 것을 권장
//원래 아래의 방법처럼 AutoCommit 기능을 비활성화처리하고 명령 실행시키는 것이 **정석 방법**이지만,
//예외발생마다 수동 커밋과 롤백처리하는 것이 귀찮아 실제적으로 잘 사용하지는 않음
//STUDENT 테이블에 저장된 학생정보 중 학번이 [2000]인 학생의 이름을 [임꺽정]으로 변경하는 JDBC 프로그램 작성
public class TransactionControlApp {
public static void main(String[] args) {
Connection con=null;
Statement stmt=null;
try {
Class.forName("oracle.jdbc.driver.OracleDriver");
String url="jdbc:oracle:thin:@localhost:1521:xe";
String username="scott";
String password="tiger";
con=DriverManager.getConnection(url, username, password);
//Connection.setAutoCommit(boolean autoCommit) : AutoCommit 기능의 사용여부를 변경하는 메소드
// => false : AutoCommit 기능 비활성화, true : AutoCommit 기능 활성화(기본)
con.setAutoCommit(false);
stmt=con.createStatement();
String sql="update student set name='임꺽정' where no=2000";
int rows=stmt.executeUpdate(sql);
//if(con!=null) throw new Exception();//인위적인 예외 발생
if(rows>0) {//조작된 행이 있는 경우
System.out.println("[메세지]"+rows+"명의 학생정보를 변경 하였습니다.");
} else {//조작된 행이 없는 경우
System.out.println("[메세지]해당 학번의 학생정보를 찾을 수 없습니다.");
}
//Connection.commit() : Connection 객체에 의해 전달된 모든 SQL 명령에 대한 커밋 처리 메소드
con.commit();
} catch (ClassNotFoundException e) {
System.out.println("[에러]OracleDriver 클래스를 찾을 수 없습니다.");
} catch (SQLException e) {
System.out.println("[에러]JDBC 관련 오류 = "+e.getMessage());
} catch (Exception e) {
System.out.println("[에러]프로그램에 예기치 못한 오류가 발생 하였습니다.");
try {
//Connection.rollback() : Connection 객체에 의해 전달된 모든 SQL 명령에 대한 롤백 처리 메소드
con.rollback();
} catch (SQLException e1) { }
} finally {
try {
if(stmt!=null) stmt.close();
if(con!=null) con.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
2. ConnectionFactory클래스 만들고 사용
ConnectionFactory.java
package xyz.itwill.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
//Connection 객체를 생성하여 반환하거나 JDBC 관련 객체를 전달받아 제거하는 기능을 제공하는 클래스
// => JDBC 프로그램 작성에 필요한 공통적인 명령들을 메소드로 제공
// => 프로그램의 생산성이 향상되고 유지보수의 효율성 증가
public class ConnectionFactory{
//1.
//ConnectionFactory의 getConnection()메소드 : Connection 객체를 생성하여 반환하는 메소드
//=> 간단하게 잠깐 쓰기 위해서 정적메소드로 만듦
public static Connection getConnection(){
Connection con=null;
try {
Class.forName("oracle.jdbc.driver.OracleDriver");
String url="jdbc:oracle:thin:@localhost:1521:xe";
String username="scott";
String password="tiger";
con=DriverManager.getConnection(url, username, password);
} catch (Exception e) {
System.out.println("[에러]Connection 객체를 생성할 수 없습니다.");
}
return con;
}
//2.
//ConnectionFactory의 close()메소드 : JDBC 관련 객체를 전달받아 제거하는 메소드
//=> 오버로드 선언
public static void close(Connection con) {
try {
if(con!=null) con.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void close(Connection con, Statement stmt) {
try {
if(stmt!=null) stmt.close();
if(con!=null) con.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void close(Connection con, Statement stmt, ResultSet rs) {
try {
if(rs!=null) rs.close();
if(stmt!=null) stmt.close();
if(con!=null) con.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
3. ConnectionFactory사용
ConnectionFactoryApp.java
package xyz.itwill.jdbc;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
//DEPT 테이블에 저장된 모든 부서정보를 검색하여 출력하는 JDBC 프로그램 작성
public class ConnectionFactoryApp {
public static void main(String[] args) {
//JDBC 관련 객체를 저장하기 위한 참조변수는 try 영역 외부에서 선언
// => try 영역을 포함한 모든 영역에서 참조변수를 이용해 객체 사용 가능
Connection con=null;
Statement stmt=null;
ResultSet rs=null;
try {
//🔥1+2.
//😋미리만든 getConnection()메소드 덕분
//DBMS서버에 접속한 Connection 객체를 반환받아 저장하기
con=ConnectionFactory.getConnection();
//🔥3.
//Connection 객체로부터 SQL 명령을 전달할 수 있는 Statement 객체를 반환받아 저장하기
stmt=con.createStatement();
//🔥4.
//Statement 객체를 이용하여 SQL 명령을 전달하여 실행하고 실행결과를 반환받아 저장하기
//Statement.executeQuery(String sql): SELECT 명령을 전달하여 실행하기 위한 메소드
//=>실행결과로 검색행이 저장된 ResultSet 객체 반환
String sql="select * from dept order by deptno";
rs=stmt.executeQuery(sql);
//🍓ResultSet커서 위치에 처리행이 있는 경우 계속 반복 실행해라~
while(rs.next()) {
//🍓컬럼값을 반환받아서 변수에 저장없이 바로 출력하는 중..
System.out.println("부서번호 = "+rs.getInt("deptno")+", 부서이름 = "+rs.getString("dname")+", 부서위치 = "+rs.getString("loc"));
}
} catch (SQLException e) {
System.out.println("[에러]JDBC 관련 오류 = "+e.getMessage());
} finally {
//🔥6.
//😋미리만든 close()메소드 덕분
//사용했던 모든 JDBC객체 삭제하기
ConnectionFactory.close(con, stmt, rs);
}
}
}
4. ResultSet의 부가적인 정보를 가지고 있는 ResultSetMetaData객체
ResultSetMetaDataApp.java
- ResultSet객체 관련 부가적인 정보를 가지고 있음
- 검색행의 부가적인 정보들, 특히 컬럼과 관련된 정보들이 저장되어있음
- vs DatabaseMetaData 클래스 도 있지만 잘 안씀..
package xyz.itwill.jdbc;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
//STUDENT 테이블에 저장된 모든 학생정보를 검색하여 출력하는 JDBC 프로그램 작성
public class ResultSetMetaDataApp {
//정석은 try-catch이지만 일단 떠넘겨 프로그램 작성함
public static void main(String[] args) throws SQLException {
//🔥1+2.
//😋미리만든 getConnection()메소드 덕분
//DBMS서버에 접속한 Connection 객체를 반환받아 저장하기
Connection con=ConnectionFactory.getConnection();
//🔥3.
//Connection 객체로부터 SQL 명령을 전달할 수 있는 Statement 객체를 반환받아 저장하기
Statement stmt=con.createStatement();
//🔥4.
//Statement 객체를 이용하여 SQL 명령을 전달하여 실행하고 실행결과를 반환받아 저장하기
//Statement.executeQuery(String sql) : SELECT 명령을 전달하여 실행하기 위한 메소드 - 실행결과로 검색행이 저장된 ResultSet 객체 반환
String sql="select * from student order by no";
//String sql="select no,name from student order by no";
ResultSet rs=stmt.executeQuery(sql);
//🍓ResultSet커서 위치에 처리행이 있는 경우 계속 반복 실행해라~
while(rs.next()) {
//🍓컬럼값을 반환받아서 변수에 저장없이 바로 출력하는 중..
System.out.println("학번 = "+rs.getInt("no")+", 이름 = "+rs.getString("name"));
}
//학번 = 1000, 이름 = 홍길동
//학번 = 2000, 이름 = 임걱정
System.out.println("==========================");
//🍎ResultSet.getMetaData() : 검색행(ResultSet 객체)에 대한 부가적인 정보를 저장하는 ResultSetMetaData 객체를 반환하는 메소드
ResultSetMetaData rsmd=rs.getMetaData();
//🍏ResultSetMetaData.getColumnCount() : 검색행의 컬럼 갯수를 반환하는 메소드
int columnCount=rsmd.getColumnCount();
System.out.println("검색행의 컬럼 갯수 = "+columnCount);
//검색행의 컬럼 갯수 = 5
System.out.println("==========================");
for(int i=1;i<=columnCount;i++) {
//🍏ResultSetMetaData.getColumnLabel(int columnIndex) : columnIndex 위치의 컬럼명을 반환하는 메소드
String columnLabel=rsmd.getColumnLabel(i);
//🍏ResultSetMetaData.isNullable(int columnIndex) : columnIndex 위치의 컬럼에 대한 NULL 허용 유무값(0 또는 1)을 반환하는 메소드
int isNull=rsmd.isNullable(i);
String nullResult="NULL";
//🍏ResultSetMetaData.columnNoNulls : NULL를 허용하지 않는 값을 표현하는 상수(Constant) - 정수값 : 0
if(isNull==ResultSetMetaData.columnNoNulls) {
nullResult="NOT NULL";
}
//🍏ResultSetMetaData.getColumnTypeName(int columnIndex) : columnIndex 위치의 컬럼에 대한 오라클 자료형을 반환하는 메소드
String columnTypeName=rsmd.getColumnTypeName(i);
//🍏ResultSetMetaData.getColumnDisplaySize(int columnIndex) : columnIndex 위치의 컬럼에 대한 출력크기를 반환하는 메소드
int columnDisplaySize=rsmd.getColumnDisplaySize(i);
System.out.println("컬럼명 = "+columnLabel);
System.out.println("NULL 허용 유무 = "+nullResult);
System.out.println("컬럼 자료형 = "+columnTypeName);
System.out.println("컬럼 출력크기 = "+columnDisplaySize);
System.out.println("---------------------------------------");
}
//컬럼명 = NO
//NULL 허용 유무 = NOT NULL
//컬럼 자료형 = NUMBER
//컬럼 출력크기 = 5
//--------------------------------------
//컬럼명 = NAME
//NULL 허용 유무 = NULL
//컬럼 자료형 = VARCHAR2
//컬럼 출력크기 = 50
//--------------------------------------
//컬럼명 = PHONE
//NULL 허용 유무 = NULL
//컬럼 자료형 = VARCHAR2
//컬럼 출력크기 = 20
//--------------------------------------
//컬럼명 = ADDRESS
//NULL 허용 유무 = NULL
//컬럼 자료형 = VARCHAR2
//컬럼 출력크기 = 100
//--------------------------------------
//컬럼명 = BIRTHDAY
//NULL 허용 유무 = NULL
//컬럼 자료형 = DATE
//컬럼 출력크기 = 7
//--------------------------------------
//🔥6.
//😋미리만든 close()메소드 덕분
//사용했던 모든 JDBC객체 삭제하기
ConnectionFactory.close(con, stmt, rs);
}
}
5. Statement객체의 execute(sql) | executeUpdate(sql) | executeQuery(sql)
ExecuteApp.java
package xyz.itwill.jdbc;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class ExecuteApp {
//정석은 try-catch이지만 일단 떠넘겨 프로그램 작성함
public static void main(String[] args) throws SQLException {
//🔥1+2+3.
//😋미리만든 getConnection()메소드 덕분
//DBMS서버에 접속한 Connection 객체를 반환받아 저장하기
Connection con = ConnectionFactory.getConnection();
//Connection 객체로부터 SQL 명령을 전달할 수 있는 Statement 객체를 반환받아 저장하기
Statement stmt = con.createStatement();
//💙방법1.
//🔥4.
/*
//Statement 객체를 이용하여 SQL 명령을 전달하여 실행하고 실행결과를 반환받아 저장하기
//Statement.**executeUpdate**(String sql) : DML명령(INSERT,UPDATE,DELETE) 또는 DDL명령(CREATE,DROP)을 전달하여 실행하기 위한 메소 - 실행결과로 조작행의 갯수를 정수값(int)으로 반환
String sql1 = "update student set name = '임꺽정' where no=2000";
int rows = stmt.executeUpdate(sql1);
System.out.println("[결과]" + rows + "명의 학생정보를 변경 하였습니다.");
System.out.println("==================================");
//Statement.**executeQuery**(String sql) : SELECT 명령을 전달하여 실행하기 위한 메소드 - 실행결과로 검색행이 저장된 ResultSet 객체 반환
String sql2 = "select * from student order by no";
ResultSet rs = stmt.executeQuery(sql2);
while(rs.next()) {
System.out.println("학번 = "+ rs.getInt("no")+", 이름 = " + rs.getString("name"));
}
System.out.println("==================================");
*/
//or
//💙방법2.
//🔥4.
int choice = 1; //임의로 설정하여 UPDATE 명령이 실행될 수 있도록 만들 것임
String sql = "";
if(choice == 1) {
sql = "update student set name = '임꺽정' where no=2000";
} else {
sql = "select * from student order by no";
}
//Statement.**execute**(String sql) : SQL 명령을 전달하여 실행하는 메소드 - boolean 반환
//=> false 반환 : DML 명령(INSERT,UPDATE,DELETE) 또는 DL 명령(CREATE,DROP)을 전달하여 실행한 경우의 반환값
//=> true 반환 : SELECT 명령을 전달하여 실행한 경우의 반환값
boolean result = stmt.execute(sql);
//🏡
//🏡"SELECT 명령"을 전달하여 실행한 경우
if(result) {
//🍀Statement.getResultSet() : Statement객체로 전달되어 실행된 SELECT 명령에 대한 검색결과를 저장한 ResultSet 객체를 반환하는 메소드
ResultSet rs = stmt.getResultSet();
//🍓ResultSet커서 위치에 처리행이 있는 경우 계속 반복 실행해라~
while(rs.next()) {
//🍓컬럼값을 반환받아서 변수에 저장없이 바로 출력하는 중..
System.out.println("학번 = "+ rs.getInt("no")+", 이름 = " + rs.getString("name"));
}
//🔥6.
//🔥미리만든 close()메소드 덕분
//사용했던 모든 JDBC객체 삭제하기
ConnectionFactory.close(con, stmt, rs);
//🏡"DML 명령 또는 DDL 명령"을 전달하여 실행한 경우
}else {
//🍀Statement.getUpdateCount(): Statement객체로 전달되어 실행된 DML 명령에 대한 조작행의 갯수를 정수값으로 반환하는 메소드
//=>DDL명령(CREATE,DROP)은 0을 반환(왜? 조작된 행이 없으니까)
int rows = stmt.getUpdateCount();
//🔥5.
//반환받은 SQL 명령의 실행결과를 이용하여 출력하기
//사용자에게 결과 알려주기
System.out.println("[결과]" + rows +"명의 학생정보를 변경 하였습니다.");
//🔥6.
//😋미리만든 close()메소드 덕분
//사용했던 모든 JDBC객체 삭제하기
ConnectionFactory.close(con, stmt);
}
}
}
6. SqlMinusApp 프로그램
SqlMinusApp.java ( like SqlPlus 프로그램)
package xyz.itwill.jdbc;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
//키보드로 SQL 명령을 입력받아 DBMS 서버에 전달하여 실행하고 결과를 출력하는 JDBC 프로그램 작성
// => 키보드로 입력 가능한 SQL 명령은 INSERT,UPDATE,DELETE,SELECT 명령만 가능하도록 작성
// => SQL 명령을 반복적으로 입력받아 실행되도록 작성
// => SQL 명령 대신 [EXIT]를 입력하면 프로그램 종료 - 대소문자 미구분
// => 키보드로 입력한 SQL 명령이 잘못된 경우 에러 메세지 출력
public class SqlMinusApp {
public static void main(String[] args) throws Exception {
//키보드로 SQL 명령(문자열)을 입력받기 위한 입력스트림 생성
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
Connection con = ConnectionFactory.getConnection();
Statement stmt = con.createStatement();
ResultSet rs = null;
System.out.println("SqlMinus 프로그램을 실행합니다.(종료:exit)");
while(true) {
//키보드로 SQL 명령을 입력받아 변수에 저장
System.out.print("SQL> ");
//키보드로 입력받은 문자열의 앞과 뒤의 모든 공백 제거
String sql = in.readLine().trim();
//키보드 입력값이 없는 경우 반복문을 처음부터 다시 실행
if(sql==null || sql.equals("")) continue;
//키보드 입력값이 [EXIT]인 경우 반복문 종료 - 프로그램 종료
if(sql.equalsIgnoreCase("exit")) break;
try {
//🏡"SELECT 명령"을 전달하여 실행한 경우
if(stmt.execute(sql)) {
//🍀Statement.getResultSet()메소드
rs = stmt.getResultSet();
//🍊검색행이 있는 경우(resultSet 커서의 다음 처리행이 있다면)
if(rs.next()) {
ResultSetMetaData rsmd = rs.getMetaData(); //🍎ResultSetMetaData 객체를 반환받아 저장
int columnCount = rsmd.getColumnCount(); //🍏SELECT 명령에 대한 검색행의 컬럼 갯수를 반환받아 저장 = 검색대상의 갯수
System.out.println("===============================");
for(int i = 1; i <= columnCount; i++) { //🍏SELECT 명령에 대한 columnIndex 위치의 컬럼명을 반환받아 출력 = 검색대상의 이름
System.out.print(rsmd.getColumnLabel(i)+"\\\\t");
}
System.out.println();
System.out.println("===============================");
do { //🍏반복문을 사용하여 검색행의 컬럼값을 반환받아 저장
for(int i = 1; i <= columnCount; i++) {
String columnValue = rs.getString(i);
//🍏컬럼의 오라클 자료형이 [DATE]인 경우
if(rsmd.getColumnTypeName(i).equals("DATE")){columnValue = columnValue.substring(0,10);}
//🍏컬럼값이 없는 경우
if(columnValue==null) {columnValue=" ";}
//🍏컬럼값 출력
System.out.print(columnValue+"\\\\t");
}
System.out.println();
}while(rs.next());
//🍊검색행이 없는 경우(resultSet 커서의 다음 처리행이 없다면)
} else {
System.out.println("검색된 결과가 없습니다.");
}
//🏡"INSERT, UPDATE, DELETE 명령"을 전달하여 실행한 경우
} else {
//🍀Statement.getUpdateCount()메소드
int rows = stmt.getUpdateCount();
System.out.println(rows+"개의 행을 "+sql.trim().substring(0,6).toUpperCase()+"하였습니다.");
}
}catch(SQLException e) {
//키보드로 입력받아 전달해 실행된 SQL 명령에 문제가 있는 경우 SQLException 발생
System.out.println("SQL 오류 = "+ e.getMessage());
}
}
//🔥6.
//😋미리만든 close()메소드 덕분
//사용했던 모든 JDBC객체 삭제하기
ConnectionFactory.close(con, stmt, rs);
System.out.println("[메세지]SQLMinus 프로그램을 종료합니다.");
}
}
7. 다음행으로만 이동 가능한 resultSetCursor VS 이동 방법 설정가능하고 처리행의 대한 조작 설정가능한 ResultSetCursor
ResultSetCursorApp.java
package xyz.itwill.jdbc;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class ResultSetCursorApp {
public static void main(String[] args) throws SQLException {
//🖤Connection객체 생성
Connection con = ConnectionFactory.getConnection();
//🖤Statement 객체생성방법 1
//Connection.createStatement() : Statement 객체를 생성하여 반환하는 메소드
//Statement 객체 : SQL 명령을 현재 접속중인 DBMS 서버에 전달하여 실행하는 기능을 제공하는 객체
Statement stmt = con.createStatement();
String sql = "select * from student order by no";
//Statement.executeQuery(String sql) : SELECT 명령을 전달하여 실행하고 검색결과를 ResultSet 객체로 반환하는 메소드
// => ResultSet 객체는 내부적으로 커서(Cursor)를 사용하여 행단위로 처리
// => ResultSet 커서는 다음행으로만 이동 가능하며 커서 위치의 행에 대한 조작 불가능
ResultSet rs = stmt.executeQuery(sql);
//ResultSet.next() : ResultSet 커서를 다음행으로 이동하는 메소드
// => 이동된 커서 위치에 처리행이 없는 경우 [false]를 반환하고 처리행이 있는 경우 [true]를 반환
while(rs.next()) {
//ResultSet.getRow() : ResultSet 커서가 위치한 처리행의 행번호(RowIndex)를 반환하는 메소드
//=> 반환값 : int **처리행의 행번호(RowIndex)** ex. ResultSet.getRow(1) ResultSet.getRow(2)
//ResultSet.getXXX(string columnLabel) : ResultSet 커서가 위치한 처리행의 컬럼값을 반환하는 메소드
//=> 반환값 : String **처리행의 컬럼값** ex. ResultSet.getString("name") ResultSet.getInt("no")
System.out.println(rs.getRow()+"행 : 학번 = "
+rs.getInt("no")+", 이름 = "+rs.getString("name"));
}
ConnectionFactory.close(con, stmt, rs);
System.out.println("==============================================================");
//🖤Connection객체 생성
con=ConnectionFactory.getConnection();
//🖤Statement 객체생성방법 2
//Connection.createStatement(int resultSetType, int resultSetConcurrency) : Statement 객체를 생성하여 반환하는 메소드
// => 매개변수에 전달되는 값에 따라 Statement 객체로 생성되는 ResultSet 객체의 사용방법을 다르게 설정
// => ResultSet 커서의 이동 또는 ResultSet 커서가 위치한 처리행의 대한 조작 설정 가능
//[int resultSetType] : ResultSet 커서의 이동 관련 속성값(ResultSet 인터페이스의 상수)을 전달하여 설정
// => ResultSet.TYPE_FORWARD_ONLY : ResultSet 커서를 다음행으로만 이동 가능 - 기본값
// => ResultSet.TYPE_SCROLL_INSENSITIVE : ResultSet 커서를 원하는 행으로 이동 가능 - 데이타베이스 변경 미반영 **(조작행 미반영)**
// => ResultSet.TYPE_SCROLL_SENSITIVE : ResultSet 커서를 원하는 행으로 이동 가능 - 데이타베이스 변경 반영 **(조작행 반영 -** resultSet 객체 내에서 행의 삽입삭제변경이 가능한데, 이것이 반영된다는 뜻**)**
//[int resultSetConcurrency] : ResultSet 커서 위치의 처리행의 조작 관련 속성값(ResultSet 인터페이스의 상수)을 전달하여 설정
// => ResultSet.CONCUR_READ_ONLY : ResultSet 커서 위치의 처리행에 대한 조작 불가능 - 기본값
// => ResultSet.CONCUR_UPDATABLE : ResultSet 커서 위치의 처리행에 대한 조작 가능
stmt=con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); //원하는 곳으로 이동 가능!, 조작은 불가능!
sql="select * from student order by no";
//ResultSet 커서는 ResultSet 객체의 BOF(Before Of File)영역에 위치
rs=stmt.executeQuery(sql);
rs.first(); //**ResultSet.first()** : ResultSet 커서를 첫번째 행으로 이동하는 메소드
System.out.println(rs.getRow()+"행 : 학번 = "+rs.getInt("no")+", 이름 = "+rs.getString("name"));
rs.last(); //**ResultSet.last()** : ResultSet 커서를 마지막 행으로 이동하는 메소드
System.out.println(rs.getRow()+"행 : 학번 = "+rs.getInt("no")+", 이름 = "+rs.getString("name"));
rs.absolute(2); //**ResultSet.absolute(int rowIndex)** : ResultSet 커서를 원하는 위치의 행으로 이동하는 메소드
System.out.println(rs.getRow()+"행 : 학번 = "+rs.getInt("no")+", 이름 = "+rs.getString("name"));
System.out.println("===========================================");
rs.beforeFirst(); //**ResultSet.beforeFirst()** : ResultSet 커서를 BOF(Before Of File) 영역으로 이동하는 메소드
while(rs.next()) { System.out.println(rs.getRow()+"행 : 학번 = "+rs.getInt("no")+", 이름 = "+rs.getString("name")); }
System.out.println("===========================================");
rs.afterLast(); **//ResultSet.afterLast()** : ResultSet 커서를 EOF(End Of File) 영역으로 이동하는 메소드
while(rs.previous()) { //ResultSet.previous() : ResultSet 커서를 이전행으로 이동하는 메소드
System.out.println(rs.getRow()+"행 : 학번 = "+rs.getInt("no")+", 이름 = "+rs.getString("name"));
}
ConnectionFactory.close(con, stmt, rs);
System.out.println("==============================================================");
//🖤Connection객체 생성
con=ConnectionFactory.getConnection();
//🖤Statement 객체생성방법 3
stmt=con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); //원하는 곳으로 이동 가능!, 조작도 가능!, 조작행 반영!
//ResultSet 커서가 위치한 처리행에 대한 "조작이 가능"한 경우 SELECT 명령의 검색대상으로 [*] 사용 불가능
sql="select no,name,phone,address,birthday from student order by no";
rs=stmt.executeQuery(sql);
rs.absolute(2);
//ResultSet.updateXXX(String columnLabel, XXX columnValue) : ResultSet 커서가 위치한 처리행의 컬럼값을 변경하는 메소드 - XXX : Java 자료형
rs.updateString("name", "임꺽정");
//ResultSet.updateRow() : 변경행을 ResultSet 객체에 적용하는 메소드 - 실제 테이블의 행에 대한 컬럼값 변경 처리
rs.updateRow();
while(rs.next()) { System.out.println(rs.getRow() + "행 : 학번 = "+rs.getInt("no")+" , 이름 = "+rs.getString("name")); }
//1행 : 학번 = 1000 , 이름 = 홍길동
//2행 : 학번 = 2000 , 이름 = 임걱정
//3행 : 학번 = 3000 , 이름 = 전우치
/*
rs.absolute(3);
//ResultSet.moveToInsertRow() : ResultSet 커서 위치에 새로운 행을 삽입하는 메소드 - 삽입행 다음에 존재하는 기존행은 자동으로 다음행으로 이동
rs.moveToInsertRow();
//삽입행의 컬럼값 변경
rs.updateInt("no", 4000);
rs.updateString("name", "일지매");
rs.updateString("phone", "010-1321-2411");
rs.updateString("address", "부산시 사하구");
rs.updateString("birthday", "2000-12-31");
//ResultSet.insertRow() : 삽입행을 ResultSet 객체에 적용하는 메소드 - 실제 테이블의 행에 대한 삽입 처리
rs.insertRow();
while(rs.next()) { System.out.println(rs.getRow() + "행 : 학번 = "+rs.getInt("no")+" , 이름 = "+rs.getString("name")); }
//1행 : 학번 = 1000 , 이름 = 홍길동
//2행 : 학번 = 2000 , 이름 = 임걱정
//3행 : 학번 = 3000 , 이름 = 전우치
//4행 : 학번 = 4000 , 이름 = 일지매
*/
/*
rs.absolute(4);
//ResultSet.insertRow() : ResultSet 커서가 위치한 처리행을 삭제하여 ResultSet 객체에 적용하는 메소드 - 실제 테이블의 행에 대한 삭제 처리
rs.deleteRow();
*/
rs.beforeFirst();
while(rs.next()) { System.out.println(rs.getRow()+"행 : 학번 = "+rs.getInt("no")+", 이름 = "+rs.getString("name")); }
//1행 : 학번 = 1000 , 이름 = 홍길동
//2행 : 학번 = 2000 , 이름 = 임걱정
//3행 : 학번 = 3000 , 이름 = 전우치
ConnectionFactory.close(con, stmt, rs);
System.out.println("==============================================================");
}
}
8. Statement객체의 단점 및 장점
StatementApp.java
package xyz.itwill.jdbc;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
//Statement 객체
//=> 현재 접속중인 DBMS 서버에 SQL 명령을 전달하여 실행하기 위한 기능을 제공하는 객체
//💛장점
//하나의 Statement 객체를 이용하여 다수의 SQL 명령을 전달하여 실행 가능
//🖤단점
//SQL 명령의 Java 변수값을 포함할 경우 문자열 결합 기능 사용
// => 문자열 결합을 이용할 경우 가독성 및 유지보수의 효율성 감소
// => InSQL 해킹 기술(값 대신 부분적인 SQL 명령을 포함시키는 해킹 기술)에 취약
// => 만약 관리자 아이디가 admin이고, 관리자 비번을 몰라도 Statement객체로 만들었다면 [' or '1'='1 ]만 작성하면 비번이 맞지 않아도 인증이 됨. 관리자로 로그인이 되어 사이트 조작이 가능함
//Statement객체 권장할 때
//사용자 입력값이 없는 프로그램 만들 때 , 보안적인 요소가 필요 없을 때
//ex) Sql Plus 프로그램 - sqldeveloper
//Statement객체 비권장할 때 - statement 객체를 상속받은 **PreparedStatement객체 이용**
//웹프로그램
//특히 사용자로부터 값을 입력받아야 하는 JDBC 프로그램
public class StatementApp {
public static void main(String[] args) throws Exception {
//키보드로 학생정보를 입력받아 STUDENT 테이블에 삽입하고 STUDENT 테이블에 저장된 모든
//학생정보를 검색하여 출력하는 JDBC 프로그램 작성 - Statement객체 이용한 단점1
/*
//1.
//키보드로 학생정보를 입력받기 위한 입력스트림 생성
//=>키보드 입력스트림을 문자로 가공처리 후 문자열로 확장 - 스트림의 다단계 연결
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
//2.
//키보드로 학생정보를 입력받아 저장
System.out.println("<<학생정보 입력>>");
System.out.print("학번 입력 >> ");
int no = Integer.parseInt(in.readLine()); //키보드로 문자열을 입력받아 정수값으로 변환해 저장
System.out.print("이름 입력 >> ");
String name = in.readLine();
System.out.print("전화번호 입력 >> ");
String phone = in.readLine();
System.out.print("주소 입력 >> ");
String address = in.readLine();
System.out.print("생년월일 입력 >> ");
String birthday = in.readLine();
//사용자에게 받은 입력값에 대한 검증이 필요하지만 일단 패스!
System.out.println("==================================================");
//3.
//입력된 학생정보를 STUDENT테이블의 행으로 삽입 처리
Connection con = ConnectionFactory.getConnection();
Statement stmt = con.createStatement();
//🖤단점 - 문자열 결합을 이용할 경우 가독성 및 유지보수의 효율성 감소
String sql1 = "insert into student values("+no+",'"+name+"','"+phone+"','"+address+"','"+birthday+"')";
//💛장점 - 다수의 SQL 명령 전달가능
int rows = stmt.executeUpdate(sql1);
System.out.println("[결과]"+rows+"명의 학생정보를 삽입 하였습니다.");
System.out.println("==================================================");
//4.
//STUDENT테이블에 저장된 모든 학생정보를 검색하여 출력
String sql2 = "select * from student order by no";
//💛장점 - 다수의 SQL 명령 전달가능
ResultSet rs = stmt.executeQuery(sql2);
System.out.println("<<학생정보 출력>>");
while(rs.next()) {
System.out.println("학번 = "+rs.getInt("no")+", 이름 = "+rs.getString("name"));
}
ConnectionFactory.close(con,stmt,rs);
System.out.println("==================================================");
*/
//키보드로 이름을 입력받아 STUDENT 테이블에 저장된 학생정보 중 해당 이름의 학생정보를
//검색하여 출력하는 JDBC 프로그램 작성 - Statement객체 이용한 단점2
//1.
//키보드로 학생정보를 입력받기 위한 입력스트림 생성
//=>키보드 입력스트림을 문자로 가공처리 후 문자열로 확장 - 스트림의 다단계 연결
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
//2.
//키보드로 학생정보를 입력받아 저장
System.out.println("<<학생정보 검색>>");
System.out.print("이름 입력 >> ");
String name = in.readLine();
//사용자에게 받은 입력값에 대한 검증이 필요하지만 일단 패스!
System.out.println("==================================================");
//3.
//STUDENT테이블에 저장된 학생정보 중 해당 이름의 학생정보를 검색하여 출력
Connection con = ConnectionFactory.getConnection();
Statement stmt = con.createStatement();
String sql="select * from student where name='"+name+"' order by no";
//🖤단점 - InSQL 해킹 기술(값 대신 부분적인 SQL 명령을 포함시키는 해킹 기술)
//String sql="select * from student where name='' or '1'='1' order by no";
//해석) [name = null string] 이거나 [무조건 참]인 경우
ResultSet rs = stmt.executeQuery(sql);
//4.
System.out.println("<<검색결과>>");
if(rs.next()) { //sql에 where절이 들어가기 때문에 if문 & do-while문 권장
do {
System.out.println("학번 = "+rs.getInt("no")+", 이름 = "+rs.getString("name"));
} while (rs.next());
}else {
System.out.println("검색된 학생정보가 없습니다.");
}
ConnectionFactory.close(con, stmt, rs);
System.out.println("==================================================");
}
}
9. PreparedStatement객체의 단점 및 장점
PreparedStatementApp.java
package xyz.itwill.jdbc;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
//**PreparedStatement 객체**
//=> 현재 접속중인 DBMS 서버에 SQL 명령을 전달하여 실행하기 위한 기능을 제공하는 객체
//=> 사용자 입력값에 의해 SQL 명령이 조작될 때 사용!!
//💛장점
// => InParameter를 사용하여 값을 전달받아 SQL 명령 작성
// => InParameter를 사용하므로 가독성 및 유지보수의 효율성 증가
// => InSQL 해킹 기술 무효화 - 사용자 입력값은 SQL 명령에서 무조건 문자값으로 처리됨
//🖤단점
//=> 하나의 PreparedStatement 객체는 저장된 하나의 SQL 명령만 전달하여 실행 가능
//=> 즉, 서로 다른 SQL 명령을 10번 보내고 싶으면 PreparedStatement객체 10개 필요함ㅠ
//PreparedStatement객체 권장할 때
//사용자 입력값이 있을 때 있는 프로그램 만들 때
//보안적인 요소가 필요할 때
//웹프로그램
//특히 사용자로부터 값을 입력받아야 하는 JDBC 프로그램
public class PreparedStatementApp {
public static void main(String[] args) throws Exception {
//키보드로 학생정보를 입력받아 STUDENT 테이블에 삽입하고 STUDENT 테이블에 저장된 모든
//학생정보를 검색하여 출력하는 JDBC 프로그램 작성
/*
//1.
//키보드로 학생정보를 입력받기 위한 입력스트림 생성
//=>키보드 입력스트림을 문자로 가공처리 후 문자열로 확장 - 스트림의 다단계 연결
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
//2.
//키보드로 학생정보를 입력받아 저장
System.out.println("<<학생정보 입력>>");
System.out.print("학번 입력 >> ");
int no = Integer.parseInt(in.readLine());//키보드로 문자열을 입력받아 정수값으로 변환해저장
System.out.print("이름 입력 >> ");
String name = in.readLine();
System.out.print("전화번호 입력 >> ");
String phone = in.readLine();
System.out.print("주소 입력 >> ");
String address = in.readLine();
System.out.print("생년월일 입력 >> ");
String birthday = in.readLine();
//사용자에게 받은 입력값에 대한 검증이 필요하지만 일단 패스!
System.out.println("==================================================");
//3.
//입력된 학생정보를 STUDENT테이블의 행으로 삽입 처리
Connection con = ConnectionFactory.getConnection();
//Connection.prepareStatement(String sql) : Connection 객체로부터 SQL 명령이 저장된 PreparedStatement 객체를 반환하는 메소드
// => PreparedStatement 객체에 저장된 SQL 명령에서는 ?(InParameter) 기호 사용
//InParameter : Java 변수값을 제공받아 SQL 명령의 문자값으로 표현하기 위한 기능
// => 즉, "어떤값인지는 모르겠으나, Java 변수값으로 전달될거야~" 의 뜻
// => 반드시 모든 InParameter에 Java 변수값을 전달받아야 완전한 SQL 명령 완성
//PreparedStatement.setXXX(int parameterIndex, XXX value) : PreparedStatement 객체에 저장된 SQL 명령의 InParameter에 Java 변수값을 전달하는 메소드
// => XXX : Java 자료형
// => parameterIndex : InParameter의 위치값(첨자) - 1부터 1씩 증가되는 정수값
// => 반드시 setXXX() 메소드를 호출하여 모든 InParameter에 Java 변수값을 전달
String sql1="insert into student values(?,?,?,?,?)";
PreparedStatement pstmt = con.prepareStatement(sql1);
pstmt.setInt(1, no);
pstmt.setString(2, name);
pstmt.setString(3, phone);
pstmt.setString(4, address);
pstmt.setString(5, birthday);
//**PreparedStatement**.**executeUpdate()** : DML 명령 또는 DDL 명령을 전달하여 실행하는 메소드 - 조작행의 갯수를 정수값으로 반환
int rows=pstmt.executeUpdate();
System.out.println("[결과]"+rows+"명의 학생정보를 삽입 하였습니다.");
System.out.println("=================================");
//4.
//STUDENT테이블에 저장된 모든 학생정보를 검색하여 출력
String sql2 = "select * from student order by no";
//🖤단점 - 저장된 하나의 SQL 명령만 전달하여 실행 가능
//🖤단점 - prepareStatement 객체를 다시 만들어 SELECT명령 반환받아 저장해야함
pstmt = con.prepareStatement(sql2);
//PreparedStatement.executeQuery() : SELECT 명령을 전달하여 실행하는 메소드 - 모든 검색행이 저장된 ResultSet 객체 반환
ResultSet rs=pstmt.executeQuery();
System.out.println("<<학생정보 출력>>");
while(rs.next()) {
System.out.println("학번 = "+rs.getInt("no")+", 이름 = "+rs.getString("name"));
}
ConnectionFactory.close(con, pstmt, rs);
System.out.println("==============================================================");
*/
//키보드로 이름을 입력받아 STUDENT 테이블에 저장된 학생정보 중 해당 이름의 학생정보를
//검색하여 출력하는 JDBC 프로그램 작성
//1.
//키보드로 학생정보를 입력받기 위한 입력스트림 생성
//=>키보드 입력스트림을 문자로 가공처리 후 문자열로 확장 - 스트림의 다단계 연결
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
//2.
//키보드로 학생정보를 입력받아 저장
System.out.println("<<학생정보 검색>>");
System.out.print("이름 입력 >> ");
String name = in.readLine();
//사용자에게 받은 입력값에 대한 검증이 필요하지만 일단 패스!
System.out.println("==================================================");
//3.
//STUDENT테이블에 저장된 학생정보 중 해당 이름의 학생정보를 검색하여 출력
Connection con = ConnectionFactory.getConnection();
//💛장점 - InSQL 해킹 기술 무효화 (SQL 명령에서 무조건 문자값으로)
String sql="select * from student where name= ? order by no";
PreparedStatement pstmt = con.prepareStatement(sql);
pstmt.setString(1, name);
ResultSet rs = pstmt.executeQuery();
//4.
System.out.println("<<검색결과>>");
if(rs.next()) { //sql에 where절이 들어가기 때문에 if문&do-while문 권장
do {
System.out.println("학번 = "+rs.getInt("no")+", 이름 = "+rs.getString("name"));
}while(rs.next());
}else {
System.out.println("검색된 학생정보가 없습니다.");
}
ConnectionFactory.close(con, pstmt, rs);
System.out.println("==================================================");
}
}
10. PreparedStatement객체의 일괄처리
AddBatchApp.java
package xyz.itwill.jdbc;
import java.sql.Connection;
import java.sql.PreparedStatement;
//JDBC 프로그램에서 하나의 PreparedStatement 객체를 사용하여 다수의 행을 삽입,변경,삭제 처리를 하기 위해
//일괄처리(Batch)영역에 SQL 명령을 저장하여 한번에 전달하여 실행 기능 제공
//=> 한꺼번에 명령 전달 가능
//=> 하지만 insert, delete, update만 가능
//=> 앞으로 만들 웹프로그램에서 잘 사용하지는 않음
public class AddBatchApp {
public static void main(String[] args) throws Exception {
//1.
Connection con = ConnectionFactory.getConnection();
//2.
String sql = "insert into student values(?,?,?,?,?)";
PreparedStatement pstmt = con.prepareStatement(sql);
//3.
//한명(일지매)의 학생정보를 STUDENT테이블의 행으로 삽입 처리
pstmt.setInt(1, 4000);
pstmt.setString(2, "일지매");
pstmt.setString(3, "010-2317-9741");
pstmt.setString(4, "서울시 종로구");
pstmt.setString(5, "1999-08-15");
pstmt.addBatch(); //PreparedStatement.addBatch()메소드 : PreparedStatement 객체에 저장된 SQL 명령을 Batch 영역에 추가하는 메소드
//4.
//한명(장길산)의 학생정보를 STUDENT테이블의 행으로 삽입 처리
pstmt.setInt(1, 5000);
pstmt.setString(2, "장길산");
pstmt.setString(3, "010-4879-6765");
pstmt.setString(4, "서울시 도봉구");
pstmt.setString(5, "1999-05-05");
pstmt.addBatch();//insert명령 Batch영역에 추가
//5.
//PreparedStatement.executeBatch() : Batch 영역에 저장된 SQL 명령을 전달하여 실행하는 메소드 - 조작행의 갯수를 int 배열로 반환
int[] result = pstmt.executeBatch(); //SQL 명령을 전달하여 실행
//6.
System.out.println("[메세지]" + result.length + "개의 행을 삽입하였습니다.");
ConnectionFactory.close(con, pstmt);
}
}
11. 저장프로시저를 이용하는 CallableStatement객체
- 저장프로시저 생성
--DELETE_STUDENT를 호출하면 아래의 명령을 실행하겠어요~
--IN모드의 매개변수 : VNO IN STUDENT.NO%TYPE (IN 생략가능) --> 값을 전달받아 PL/SQL에서 쓰는 매개변수 ( IN 파라미터)
--OUT모드의 매개변수 : VNAME OUT STUDENT.VNAME%TYPE --> 외부로 제공하기 위해 쓰는 매개변수 (OUT 파라미터)
-- DELETE_STUDENT 저장프로시저를 만들어주세요 + DELETE_STUDENT를 호출하면 아래의 명령을 실행하겠어요
--(VNO 인 파라미타 와 VNAME 아웃 파라미타)
CREATE OR REPLACE PROCEDURE DELETE_STUDENT( VNO IN STUDENT.NO%TYPE, VNAME OUT STUDENT.NAME%TYPE) IS
BEGIN
SELECT NAME INTO VNAME FROM STUDENT WHERE NO=VNO;
IF SQL%FOUND THEN
DELETE FROM STUDENT WHERE NO=VNO;
COMMIT;
END IF;
EXCEPTION
WHEN OTHERS THEN -- 만약 예외가 발생된다면
VNAME := NULL; -- VNAME에 NULL을 저장해주세요
END;
/
//Procedure DELETE_STUDENT이(가) 컴파일되었습니다.
CallableStatementApp.java
package xyz.itwill.jdbc;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.Types;
//키보드로 학번을 입력받아 STUDENT 테이블에 저장된 학생정보 중 해당 학번의 학생정보를 삭제하는
//JDBC 프로그램 작성 - 저장 프로시저를 호출하여 학생정보 삭제 처리
public class CallableStatementApp {
public static void main(String[] args) throws Exception {
//1.
//키보드로 학번을 입력받기 위한 입력스트림 생성
BufferedReader in=new BufferedReader(new InputStreamReader(System.in));
//2.
System.out.println("<<학생정보 삭제>>");
System.out.print("학번 입력 >> ");
int no=Integer.parseInt(in.readLine());
System.out.println("================================================");
//3.
Connection con=ConnectionFactory.getConnection();
/*
CREATE OR REPLACE PROCEDURE DELETE_STUDENT
(VNO IN STUDENT.NO%TYPE, VNAME OUT STUDENT.NAME%TYPE) IS
BEGIN
SELECT NAME INTO VNAME FROM STUDENT WHERE NO=VNO;
IF SQL%FOUND THEN
DELETE FROM STUDENT WHERE NO=VNO;
COMMIT;
END IF;
EXCEPTION
WHEN OTHERS THEN
VNAME := NULL;
END;
/
*/
//4.
//Connection.prepareCall(String sql) : 저장 프로시저를 호출하는 명령을 전달하여 실행하기 위한 CallableStatement 객체를 반환하는 메소드
// => 저장 프로시저를 호출하는 명령 : {call 저장프로시저명(?,?,...)}
// => 저장 프로시저에서 사용한 InParameter에는 반드시
//setXXX() 메소드로 값을 전달하거나(In모드)
//registerOutParameter() 메소드를 이용하여 값을 제공받아 사용(Out모드)
//=> 필요한값은 인매개변수, 처리결과는 아웃매개변수 이용하기
//=> resultSet객체 반환은 없음
//=> 프로그램은 저장 프로시저만 호출하면 되고, 결과만 받아서 출력하면 됨!
//=> 처리는 프로시저 내 명령들이 해줄테니.. 그래서 좋긴 함
String sql="{call delete_student(?,?)}";
CallableStatement cstmt=con.prepareCall(sql);
//5.
//InParameter에 setXXX()메소드로 값 전달하기
//CallableStatement.setXXX(int parameterIndex, XXX value) : 저장 프로시저의 IN 모드의 매개변수에 Java 변수값을 전달하기 위한 메소드
cstmt.setInt(1, no);
//6.
//InParameter에 registerOutParameter()메소드를 이용하여 값 제공받아 사용하기
//CallableStatement.registerOutParameter(int parameterIndex, int sqlType) : 저장 프로시저의 OUT 모드의 매개변수에 저장된 값을 제공받기 위한 메소드
// => sqlType : SQL 자료형 - Types 클래스의 상수(Constant) 사용
cstmt.registerOutParameter(2, Types.NVARCHAR);
//7.
//명령 전달해 실행
//CallableStatement.execute() : 저장 프로시저 호출 명령을 전달하여 실행하는 메소드
cstmt.execute();
//8.
//OUT모드의 매개변수로 제공받은 값 반환
//CallableStatement.getXXX(int parameterIndex) : 저장 프로시저의 OUT 모드의 매개변수로 제공받은 값을 반환하는 메소드
String name=cstmt.getString(2);
if(name==null) {
System.out.println("[메세지]해당 학번의 학생정보를 찾을 수 없습니다.");
} else {
System.out.println("[메세지]"+name+"님을 삭제 하였습니다.");
}
//9.
ConnectionFactory.close(con, cstmt);
}
}
반응형
'jdbc' 카테고리의 다른 글
[jdbc] 6. 학생관리 프로그램 (0) | 2024.05.14 |
---|---|
[jdbc] 5. DBCP 프로그램 (0) | 2024.05.14 |
[jdbc] 3. CRUD기능을 구현하는 JDBC 프로그램 작성법 (0) | 2024.05.13 |
[jdbc] 2. (java.lang패키지) Class객체(클래즈) (0) | 2024.05.12 |
[jdbc] 1. JDBC 프로그램 작성을 위한 환경설정 (0) | 2024.05.12 |