Posts 학원 #68일차: JDBC, JDBC Driver, CRUD 구현
Post
Cancel

학원 #68일차: JDBC, JDBC Driver, CRUD 구현

JDBC

JDBC API와 MariaDB JDBC Driver

  • JDBC API: java.sql패키지에 있는 인터페이스

  • MariaJDBC 드라이버: 인터페이스를 구현하고 이 클래스를 보조하기 위해서 만들어진 클래스들의 묶음 (org.mariadb.jdbc.)

image

  • Driver<-Driver
    • 드라이버 정보, 커넥션 준비 메서드 규칙 정의
  • Connection <- MariaConnection
    • DBMS와 연결
    • Statement, PreparedStatement 객체 생성
  • Statement <- <MariaDbStatement
    • DBMS에 SQL을 전달(DBMS가 알 수 있도록 가공)
    • ResultSet 생성
  • PreparedStatement <- ServerSidePreparedStatement
    • DBMS에 SQL을 전달(DBMS가 알 수 있도록 가공)
    • ResultSet 생성
  • ResultSet <- SelectResultSet
    • DBMS에서 select를 실행한 결과를 한 개씩 가져온다.

각 객체는 new 연산자로 생성할 수 없고, 공장 객체를 통해야 하는 독특한 구조를 가지고 있다. Driver는 체인점 본사, Connection는 인테리어 업자, Statement, PreparedStatement는 지점, ResultSet은 지점에서 만드는 김밥이라고 할 수 있다. 김밥을 만들기 위해서는 일단 체인점 본사를 통해 인테리어 업자를 만나고, 이를 통해 지점을 만들어야 한다. 이러한 과정 없이 바로 new 김밥()을 할 수는 없다.

DriverManager와 Driver

image

인터페이스인지 직관적으로 알아볼 수 있도록 import하지 않고 늘여 쓰기도 한다.

드라이버를 직접 생성해서 사용하는 것이 아니라, 다음과 같은 절차를 거쳐야 한다.

  • DriverManager에게 JDBC URL을 알려주면서 Connection 객체를 달라고 요청한다.
  • 요청을 받은 DriverManager는 Driver를 찾아서 connect()를 호출한다. MariaDbConnection은 java.sql.Connection이라는 JDBC API 인터페이스의 구현체이다. URL에 맞는 드라이버에 대해 connect()를 호출한다.
    • URL에 맞는 드라이버를 찾지 못했으면 에러가 발생한다.
  • connect()를 호출하면 Driver구현체는 DBMS에 연결하는 객체인 Connection을 생성하여 리턴한다. Driver, DriverManager, App의 순으로 연쇄적으로 리턴된다.

JDBC 드라이버 준비

  • DBMS에 연결하기
    • MariaDB에 연결을 대행하는 클래스를 사용한다.
    • 이 클래스는 JDBC API규칙에 따라 작성되었고, JDBC Driver 파일(.jar)파일에 들어 있다.
    • 이 클래스를 사용하려면 먼저 이 JDBC Driver파일을 다운로드 받아 프로젝트의 ClassPath에 등록해야 한다.
  • 절차
    • 1) mvnrepository.com 또는 search.maven.org에서 mariadb jdbc driver를 검색한다.
    • 2) 라이브러리 정보를 build.gradle 파일에 설정한다.
    • 3) gradle을 이용하여 eclipse 설정 파일을 갱신한다. gradle eclipse
      • 다운로드 받지 않은 라이브러리가 있다면 자동으로 서버에서 받을 것이다.
      • 라이브러리 정보가 변경되었다면 해당 라이브러리를 서버에서 받을 것이다.
    • 4) 이클립스 프로젝트를 refresh 한다.
      • 프로젝트에 mariadb jdbc driver 라이브러리가 추가되었는지 확인한다.
  • JDBC 드라이버 로딩

    • java.sql.Driver 규칙에 따라 정의된 클래스를 로딩한다.

    • Driver 구현체는 JDBC에 대한 정보를 제공하고, DBMS에 연결작업을 수행한다.

    • JDBC: Java Database Connectivity: 자바에서 DB 프로그래밍을 하기 위해 사용하는 API

    • Driver 구현체는 DriverManager가 관리한다.

    • 따라서 접속할 DBMS의 Driver구현체를 생성하여 DriverManager에게 등록해야 한다.
  • DriverManager

    • java.sql.Driver 구현 객체를 관리하는 일을 한다.
    • DBMS 연결 요청이 들어오면 해당 DBMS의 Driver 구현체를 찾아 작업을 한다.

JDBC 드라이버 로딩 방법

방법 1: 직접 Driver 구현 객체를 생성하고 직접 등록하기

java.sql.Driver 구현체를 생성하여 JDBC 드라이버 관리자에 등록한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1) Driver 구현체의 인스턴스를 생성한다.
java.sql.Driver mariadbDriver = new org.mariadb.jdbc.Driver();

// 2) Driver 인스턴스를 드라이버 관리자에 등록한다.
DriverManager.registerDriver(mariadbDriver);
System.out.println("JDBC 드라이버 로딩 및 등록 완료!");

// DriverManager에 등록된 Driver 인스턴스를 확인한다.
// => DriverManager.getDriver(jdbcUrl);
// => jdbcUrl
// jdbc:[DBMS]://서버주소:포트번호/데이터베이스명
java.sql.Driver driver = DriverManager.getDriver("jdbc:mariadb:");
System.out.println(driver); 

// JDBC 드라이버 로딩 및 등록 완료!
// org.mariadb.jdbc.Driver@5674cd4d

방법2: Driver 구현 객체 생성과 자동 등록

Driver 객체를 생성하면 자동으로 DriverManager에 등록된다. Driver 구현체가 로딩될 때 static 블록에서 인스턴스를 만들어 등록하기 때문이다.

1
2
3
4
5
6
7
8
9
10
11
// 1) Driver 구현체의 인스턴스를 생성
// => Driver 구현체가 로딩될 때 인스턴스가 생성되기 때문에
// 굳이 다음과 같이 인스턴스를 생성할 필요가 없다.
new org.mariadb.jdbc.Driver();

// 2) DriverManager에 등록하지 않는다.

// DriverManager에 자동으로 등록되었는지 확인한다.
java.sql.Driver driver = DriverManager.getDriver("jdbc:mariadb:");
System.out.println(driver);
// 해당 드라이버가 등록되지 않았다면 에외가 발생할 것이다.

방법3: Driver 구현 클래스 로딩과 자동 등록

image

java.sql.Driver 인터페이스를 구현한 클래스를 로딩하면 해당 클래스에서 자신을 자동으로 DriverManager에 등록할 것이다.

다음은 MariaDB의 Driver 구현체의 코드 중 일부이다. 클래스가 로딩되면 static 블록은 무조건 실행된다. 이 static 블록에는 스스로 자신의 인스턴스를 만들어 DriverManager에 등록하는 코드가 들어 있다. 즉 Driver 클래스가 로딩되면 이러한 작업들이 실행된다.

1
2
3
4
5
6
7
8
9
10
11
public final class Driver implements java.sql.Driver {

  static {
    try {
      // static 블록에서 스스로 자기자신을 만들어서 등록시킨다.
      DriverManager.registerDriver(new Driver(), new DeRegister());
    } catch (SQLException e) {
      throw new RuntimeException("Could not register driver", e);
    }
  }
  //..

이 방식의 장점은 소스 코드에 클래스를 지정하는 대신 클래스 이름을 지정하기 때문에 실행할 때 다른 클래스로 쉽게 바꿀 수 있다. 따라서 특정한 Driver 구현체에 종속되지 않게 만들 수 있다.

Class.forName("fully-qualified class name(패키지명을 포함한 클래스명)")

  • 문자열 파라미터로 주어진 클래스를 찾아 메모리(Method Area)에 로딩한다.
  • 이 과정에서 static 블록이 실행될 것이고, 결국 Driver인스턴스가 생성되어 DriverManager에게 등록될 것이다.
  • 한 번 로딩된 클래스는 중복해서 로딩되지 않는다. 따라서 static 블록도 중복해서 실행되지 않는다. 따라서 같은 Driver 인스턴스가 여러 개 생성되어 DriverManager에 여러 개 등록되는 것이 아니다.
1
2
3
4
Class.forName("org.mariadb.jdbc.Driver");
// DriverManager에 자동 등록된 것을 확인해보자!
java.sql.Driver driver = DriverManager.getDriver("jdbc:mariadb:");
System.out.println(driver);

방법4: Driver 구현체 자동 로딩

image

DriverManager를 사용할 때, DriverManager는 다음 절차에 따라 Driver 구현체를 찾아서 자동으로 로딩한다.

  • 1) jdbc.drivers 시스템 프로퍼티에 지정된 구현체를 찾아 로딩한다.

    • jdbc.drivers=foo.bah.Driver:wombat.sql.Driver:bad.taste.ourDriver

    • 이때 각 Driver 구현체는 ‘system class loader’를 통해 로딩된다.

    • 시스템 프로퍼티는 JVM에 설정된 “key=value”이다.

    • 시스템 프로퍼티 꺼내는 방법

      • System.out.printf("java.home=%s\n", System.getProperty("java.home"));
    • 시스템 프로퍼티 설정 방법

      • (1) JVM을 실행할 때 JVM 옵션을 지정하는 방법

        • java -Djdbc.drivers=클래스명:클래스명:클래스명 Exam0140
      • (2)프로그램 코드에서 설정하는 방법

        System.setProperty("jdbc.drivers", "com.eomcs.jdbc.ex1.MyDriver");

  • 2) java.sql.Driver 클래스의 서비스 제공자를 찾아 로딩한다.
  • jar 파일 안에 META-INF/services/java.sql.Driver 파일을 찾는다.
    • 옛날 jar파일 같은 경우 이 java.sql.Driver파일이 없을 수도 있다. 없으면 자동이 안 되고, 인스턴스를 생성하거나 Class.forName을 해줘야 한다.
    • 있다면 Referenced Libraries에 있는 jar파일들을 하나하나 뒤져서 java.sql.Driver를 찾고, 이 파일에 적혀 잇는 클래스의 인스턴스를 생성해서 등록한다.
    • 이때 JVM은 ‘service-provider loading’ 절차에 따라 이 파일에 등록된 클래스를 로딩한다.
    • jar 파일에 해당 정보가 있다면, 1~3 방법처럼 개발자가 따로 java.sql.Driver 구현체를 명시적으로 등록하지 않아도 된다.
    • 예를 들어, mariadb JDBC 드라이버 jar 파일은 이 정보가 들어 있다. 다라서 java.sql.Driver를 구현한 클래스를 로딩하거나 생성할 필요가 없다.
1
2
3
4
5
6
7
8
9
10
11
public class Exam0141 {
  public static void main(String[] args) {
    try {
      java.sql.Driver driver = DriverManager.getDriver("jdbc:maradb:");
      System.out.println(driver);
      System.out.println("테스트!");
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

DB를 여러 개 사용하는 경우는 어떤 경우일까? MySQL의 데이터를 오라클로 옮기거나, 오라클에서 데이터베이스를 읽고 MS-SQL 데이터를 읽고 두 데이터를 합쳐서 오라클에 집어넣는 등의 작업을 할 때가 있다. 이 경우 드라이버를 여러 개 등록해서 써야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
java.sql.Driver mariadbDriver = new org.mariadb.jdbc.Driver();
java.sql.Driver oracleDriver = new oracle.jdbc.driver.OracleDriver();
java.sql.Driver mssqlDriver = new com.microsoft.sqlserver.jdbc.SQLServerDriver();
DriverManager.registerDriver(mariadbDriver);
DriverManager.registerDriver(oracleDriver);
DriverManager.registerDriver(mssqlDriver);
java.sql.Driver driver = DriverManager.getDriver("jdbc:mariadb://");
System.out.println(driver); //org.mariadb.jdbc.Driver@5674cd4d
java.sql.Driver driver2 = DriverManager.getDriver("jdbc:oracle:thin://");
System.out.println(driver2); //oracle.jdbc.OracleDriver@e9e54c2
java.sql.Driver driver3 = DriverManager.getDriver("jdbc:sqlserver://");
System.out.println(driver3); //SQLServerDriver:1

DBMS에 연결하기

DriverManager에게 DBMS와의 연결을 요청한다. 어느 서버에 접속할 것인지 정보를 제공해야 한다.

  • jdbc url: DBMS 서버 정보
    • jdbc:DBMS://서버주소:포트/데이터베이스명/
    • 포트번호를 지정하지 않으면 기본이 3306이다.
    • 포트번호는 dbms마다 다르다! MARIADB는 3306이다. 직접 서버를 만들 때도 이 포트번호는 안 쓰는 것이 좋다.
1
2
3
4
Connection con = DriverManager.getConnection(
  "jdbc:mariadb://localhost:3306/studydb", //jdbcURL
  "study", // username
  "1111"); // password
  • DriverManager는 등록된 java.sql.Driver 구현체 중에서 jdbc url에 지정된 Driver 객체를 찾는다.
    • 예: MariaDB: org.mariadb.jdbc.Driver 클래스의 객체
  • 그리고 DB 연결을 Driver 구현체에게 위임한다. 즉, Driver 객체의 connect()를 호출한다.
  • Driver 구현체(org.mariadb.jdbc.Driver 객체)는 DBMS와 연결한 후 소켓 정보를 갖고 있는 java.sql.Connection 구현체를 생성하여 리턴한다.

MariaDB의 Driver 구현체가 리턴한 Connection 객체를 확인해보자.

1
2
System.out.println(con.getClass().getName());
// org.mariadb.jdbc.MariaDbConnection

전체 코드는 다음과 같다. 파일과 마찬가지로 DBMS에 연결한 후 더이상 사용하지 않으면 연결을 해제해야 한다. 그러나 연결 해체하다가 발생된 예외는 무시한다. 이런 오류는 애플리케이션에서 처리할 방법이 없기 때문에 처리할 필요도 없다.

자원을 쓰고 close()를 호출하지 않으면 스레드를 계속 갖고 있는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Exam0210 {
  public static void main(String[] args) throws Exception {

    java.sql.Connection con = null;

    try {
      con = DriverManager.getConnection( //
          "jdbc:mariadb://localhost:3306/studydb", // jdbcURL
          "study", // username
          "1111" // password
          );

      System.out.println("DBMS와 연결됨!");
      System.out.println(con.getClass().getName());

    } finally {
      // 자원해제
      try {
        con.close();
      } catch (Exception e) {
        // 연결 해제하다가 발생된 예외는 무시한다.
      }
      System.out.println("DBMS와 연결 해제됨!");
    }
  }
}

자원을 해제할 때 호출하는 con.close()는 또 예외를 던지기 때문에 이에 대한 예외도 받아줘야 한다. 따라서 코드가 try catch ~ finally로 복잡하게 보인다. 다음과 같이 try-with-resource 문법을 사용하면 코드가 훨씬 간단해 보일 뿐만 아니라 try 블록을 벗어날 때 close()가 자동으로 호출된다.

1
2
3
4
5
6
7
8
9
10
11
12
public class Exam0220 {
  public static void main(String[] args) throws Exception {
    try (java.sql.Connection con = DriverManager.getConnection( //
        "jdbc:mysql://localhost:3306/studydb", // jdbcURL
        "study", // username
        "1111" // password
        );) {
      System.out.println("DBMS와 연결됨!");
    }
    System.out.println("DBMS와 연결 해제됨!");
  }
}

연결할 때 다음과 같이 주소를 URL, Id, password를 하나의 문자열로 합쳐 인자로 넘길 수 있다.

1
2
3
4
5
try (Connection con = DriverManager.getConnection("jdbc:mariadb://localhost:3306/studydb?user=study&password=1112")) {
  System.out.println(con.getClass().getName());
} catch (Exception e) {
  e.printStackTrace();
}

DBMS에 SQL문 보내기

  • executeUpdate(): INSERT/UPDATE/DELETE 등 DML 관련 SQL문 전송
    • 리턴값: 변경(insert/update/delete)된 데이터의 개수
  • executeQuery(): SELETE 등 DQL 관련 SQL문 전송
    • 리턴값: 서버에서 데이터를 가져오는 일을 할 객체

insert

image

1
2
3
4
5
6
7
8
try (Connection con = DriverManager.getConnection("jdbc:mariadb://localhost:3306/studydb?user=study&password=1111");
    java.sql.Statement stmt = con.craeteStatement();) {
  System.out.println(stmt.getClass().getName());
  int count = stmt.executeUpdate(
  "insert into x_board(title, contents) values('제목10', '내용10')");
} catch (Exception e) {
  e.printStackTrace();
}

select

image

  • executeQuery()
    • DBMS 서버에 select 문을 보낸다.
    • 리턴 값: java.sql.ResultSet 구현 객체
  • ResultSet
    • 결과가 아니라, **서버에서 결과를 가져오는 일**을 할 객체이다.
    • 즉 서버의 select 실행 결과를 가져올 때 사용하는 도구이다.
    • ResultSet객체의 메서드 next()를 사용하여 서버에서 select의 결과 한 레코드(row)를 가져온다.
      • 가져왔으면 true, 가져올 게 없다면 false를 리턴한다.
  • getXxx(컬럼 번호)
    • 인자는 select 문에 나열한 컬럼의 순서를 지정한다. 단 번호는 0부터가 아니라 1부터 지정한다.
    • select문에 wildcard(*)를 사용했다면, 테이블을 정의할 때 선언한 컬럼의 순서이다.
    • 컬럼의 타입과 상관 없이 getString()으로 값을 꺼낼 수 있다.
    • DBMS에 설정된 컬럼의 타입에 따라 값을 변환해서 받고 싶다면, 다음과 같이 해당 타입의 값을 리턴하는 getXxx()를 호출한다.
      • int, number: getInt()
      • char, varchar, text: getString()
      • date, datetime: getDate()
      • float: getFloat()
  • getXxx(컬럼명)
    • 컬럼의 번호를 지정하는 방식은 소스 코드를 읽을 때 매우 불편하다. 해당 번호가 어떤 칼럼을 가리키는지 알려면 select 문을 살펴봐야 하는 번거로움이 있다. 그래서 실무에서는 가능한 번호 대신 컬럼의 이름을 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public static void main(String[] args) throws Exception {
  try (java.sql.Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/studydb?user=study&password=1111");
       java.sql.Statement stmt = con.createStatement();
       java.sql.ResultSet rs = stmt.executeQuery("select * from x_board order by board_id desc");) {
    boolean isReceived = rs.next();
    
    // getXxx(컬럼번호)
    // 컬럼의 타입에 상관 없이 getString()으로 값을 꺼낼 수 있다.
    if (isReceived) {
      System.out.printf("%s, %s, %s, %s, %s\n",
                       rs.getString(1), // board_id
                       rs.getString(2), // title
                       rs.getString(3), // contents
                       rs.getString(4), // created_date
                       rs.getString(5)); // view_count
      //9, 제목10, 내용, 2020-10-29 14:27:28.0, 0
    }
    
    if (isReceived) {
        System.out.printf("%d, %s, %s, %s, %d\n",
                          rs.getInt(1), 
                          rs.getString(2), 
                          rs.getString(3), 
                          rs.getDate(4), 
                          rs.getInt(5));
      // 9, 제목10, 내용, 2020-10-29, 0
    }
    
    // getXxx(컬럼명)
    if (isReceived) {
        System.out.printf("%d, %s, %s, %s, %d\n",
                          rs.getInt("board_id"), 
                          rs.getString("title"), 
                          rs.getString("contents"), 
                          rs.getDate("created_date"), 
                          rs.getInt("view_count"));
      }
      // 9, 제목10, 내용, 2020-10-29, 0
  }
}

반복문을 사용하면 서버에서 여러 개의 데이터를 가져올 수 있다.

1
2
3
4
5
6
7
8
while (rs.next()) {
  System.out.printf("%d, %s, %s, %s, %d\n",
                    rs.getInt("board_id"), 
                    rs.getString("title"), 
                    rs.getString("contents"), 
                    rs.getDate("created_date"), 
                    rs.getInt("view_count"));
}

실행 결과는 다음과 같다.

1
2
3
4
5
6
7
8
9
9, 제목10, 내용, 2020-10-29, 0
8, 제목10, 내용, 2020-10-29, 0
7, 제목10, 내용, 2020-10-29, 0
6, 제목6, 내용, 2020-10-29, 0
5, 제목5, 내용, 2020-10-29, 0
4, 제목4, 내용, 2020-10-29, 0
3, 제목3, 내용, 2020-10-29, 0
2, 제목2, 내용, 2020-10-29, 0
1, 제목1, 내용, 2020-10-29, 0

update

아이디가 3보다 큰 애들의 조회수를 20만큼 증가시킨다.

1
2
3
4
int count = stmt.executeUpdate("update x_board set view_count = view_count + 20"
                               + " where board_id > 3");
System.out.printf("%s개 변경 성공!", count);
// 6개 변경 성공!

delete

1
2
3
int count = stmt.executeUpdate("delete from x_board where board_id = 9");
System.out.printf("%s 개 삭제 성공!", count); // 1개 삭제 성공!
// 다시 한 번 실행하면 삭제할 데이터가 없기 때문에 0을 리턴한다 (0개 삭제 성공!)

만약 삭제할 것이 없다면 삭제하지 않고 0을 리턴한다.

부모 테이블의 데이터를 지우려면, 부모 테이블의 데이터를 참조하는 자식 테이블의 데이터를 먼저 지워야 한다. 게시글을 참조하는 첨부 파일 데이터를 먼저 지운다. 참조되고 있는 데이터를 지우면 다음과 같은 에러가 뜬다.

x_board 무결성 제약조건에 결함이 발생하기 때문에 삭제할 수 없다.

1
`SQLIntegrityConstraintViolationException Cannot delete or update a parent row: a foreign key constraint fails (`studydb`.`x_board_file`, CONSTRAINT `fk_board_file` FOREIGN KEY (`board_id`) REFERENCES `x_board` (`board_id`))

DBMS는 개발자를 모든 파일 입출력 CRUD에서 해방시킨다.

image

게시판 관리

등록

다음과 같이 게시물을 등록하는 프로그램을 작성한다.

1
2
3
4
5
6
제목? aaa
내용? bbb
등록하시겠습니까?(Y/n) y
등록하였습니다.
등록하시겠습니까?(Y/n) n
등록을 취소 하였습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
String title = null;
String contents = null;

try (Scanner keyScan = new Scanner(System.in)) {

  // 사용자로부터 제목, 내용을 입력 받는다.
  System.out.print("제목? ");
  title = keyScan.nextLine();

  System.out.print("내용? ");
  contents = keyScan.nextLine();

  System.out.print("입력하시겠습니까?(Y/n) ");
  String input = keyScan.nextLine();

  if (!input.equalsIgnoreCase("y") && input.length() != 0) {
    System.out.println("등록을 취소 하였습니다.");
    return;
  }
}

try (Connection con = DriverManager.getConnection(
  "jdbc:mysql://localhost:3306/studydb?user=studydb?user=study&password=1111");
     Statement stmt = con.createStatement();) {
  String sql = String.format("insert into x_board(title, contents) values('%s', '%s')", title, contents);
  int count = stmt.executeUpdate(sql);
  System.out.printf("%d 개 입력 성공!", count);
}

목록

다음과 같이 게시물 목록을 출력한다.

1
2
3
번호, 제목, 등록일, 조회수
2, aaa, 2019-1-1, 2
1, bbb, 2018-12-31, 3

레코드에서 컬럼 값을 꺼낼 때 컬럼 번호를 지정하는 것보다 컬럼의 이름을 지정하는 것이 유지보수에 더 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try (Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/studydb?user=study&password=1111");
    Statement stmt = con.createStatement();
    ResultSet rs = stmt.executeQuery(
    "select * from x_board order by board_id desc")) {
  System.out.println("번호, 제목, 등록일, 조회수");
  while (rs.next()) {
    System.out.printf("%d, %s, %s, %d\n",
                     // 컬럼의 이름 지정
                     rs.getInt("board_id"),
                     rs.getString("title"),
                     rs.getDate("created_date"),
                     rs.getInt("view_count"));
  }
}

조회

다음과 같이 게시물을 조회하는 프로그램을 작성한다.

1
2
3
4
5
6
7
8
번호? 1
제목: aaa
내용: aaaaa
등록일: 2019-1-1
조회수: 2

번호? 100
해당 번호의 게시물이 존재하지 않습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
String no = null;
try (Scanner keyScan = new Scanner(System.in)) {
  System.out.print("번호? ");
  no = keyScan.nextLine();
}

try (Connection con = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/studydb?user=study&password=1111");
    Statement stmt = con.createStatement();
    ResultSet rs = stmt.executeQuery(
    "select * from x_board where board_id = " + no)) {
  if (rs.next()) {
    System.out.printf("제목: %s\n", rs.getString("title"));
    System.out.printf("내용: %s\n", rs.getString("contents"));
    System.out.printf("등록일: %s\n", rs.getDate("created_date"));
    System.out.printf("등록일: %s\n", rs.getInt("view_count"));
  } else {
    System.out.println("해당 번호의 게시물이 존재하지 않습니다.");
  }
} 

변경

다음과 같이 게시물을 변경하는 프로그램을 작성하라!

1
2
3
4
5
6
번호? 1
제목? xxx
내용? xxxxx
변경하였습니다.
(또는)
해당 번호의 게시물이 존재하지 않습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
String no = null;
String title = null;
String contents = null;

try (Scanner keyScan = new Scanner(System.in)) {
  System.out.print("번호? ");
  no = keyScan.nextLine();
  
  System.out.print("제목?");
  title = keyScan.nextLine();
  
  System.out.print("내용?");
  contents = keyScan.nextLine();
}


try (Connection con = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/studydb?user=study&password=1111");
    Statement stmt = con.createStatement()) {
  String sql = String.format(
  "update x_board set title='%s', contents='%s' where board_id=%s", title, contents, no);
  int count = stmt.executeUpdate(sql);
  
  if (count == 0) {
    System.out.println("해당 번호의 게시물이 존재하지 않습니다.");
  } else {
    System.out.println("변경하였습니다.");
  }
}

삭제

다음과 같이 게시물을 삭제하는 프로그램을 작성하라.

1
2
3
4
번호? 1
삭제하였습니다.
(또는)
해당 번호의 게시물이 존재하지 않습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
String no = null;

try (Scanner keyScan = new Scanner(System.in)) {
  System.out.print("번호? ");
  no = keyScan.nextLine();
}

try (Connection con = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/studydb?user=study&password=1111");
    Statement stmt = con.createStatement()) {
  
  // 게시글 첨부 파일 삭제
  stmt.executeUpdate("delete from x_board_file where board_id=" + no);
  
  // 게시글 삭제
  int count = stmt.executeUpdate("delete from x_board where board_id="+ no);
  
  if (count == 0) {
    System.out.println("해당 번호의 게시물이 존재하지 않습니다.");
  } else {
    System.out.println("삭제하였습니다.");
  }
}

Statement와 SQL 삽입 공격

입력 문자열에 SQL 명령을 삽입하여 프로그램의 의도와 다르게 데이터를 조작하는 행위이다. 사용자가 입력한 값을 가지고 SQL 문장을 만들 때 이런 문제가 발생한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class Exam0210 {

  public static void main(String[] args) throws Exception {
    String no = null;
    String title = null;
    String contents = null;

    try (Scanner keyboard = new Scanner(System.in)) {
      System.out.print("번호? ");
      no = keyboard.nextLine();

      System.out.print("제목? ");
      title = keyboard.nextLine();

      System.out.print("내용? ");
      contents = keyboard.nextLine();
    }

    try (Connection con = DriverManager.getConnection(
        "jdbc:mysql://localhost:3306/studydb?user=study&password=1111");
        Statement stmt = con.createStatement()) {
      int count = stmt.executeUpdate(
          "update x_board set title = '" + title + 
              "', contents = '" + contents + 
              "' where board_id = " + no);

      // 위에서 사용자가 입력한 값을 가지고 SQL 문장을 만들면 다음과 같다.
      //
      // update x_board set title = 'okok',
      // contents = 'test', view_count = 300, created_date = '2019-3-3'
      // where board_id = 1
      //

      if (count == 0) {
        System.out.println("해당 번호의 게시물이 존재하지 않습니다.");
      } else {
        System.out.println("변경하였습니다.");
      }
    }
  }
}

위의 예제를 실행해서 다음과 같이 입력하면 어떻게 될까?

1
2
3
번호? 1
제목? okok
내용? test', view_count = 300, created_date = '2019-3-3

위와 같이 입력한 값을 가지고 SQL 문장을 만들면 다음과 같다. 프로그램은 이렇게 완성된 SQL 문장을 곧바로 DBMS로 보내버린다.

1
2
3
update x_board set title = 'okok',
contents = 'test', view_count = 30, created_date = '2019-3-3' 
where board_id = 1

즉 개발자의 의도와 다르게 사용자가 변경해서는 안되는 컬럼 값까지 멋대로 데이터가 변경되는 문제가 발생한다.

This post is licensed under CC BY 4.0 by the author.

모두의 네트워크 #3장: 물리계층

HTML5 CSS3 웹 표준의 정석 #3장: 이미지와 하이퍼링크

Loading comments from Disqus ...