본문 바로가기

우아한테크코스/미션 정리

[코드리뷰 정리] Level 1. 체스 미션 - JDBC

2021-04-10글

JDBC (Java DataBase Connectivity)

JDBC는 DB에 접근할 수 있도록 Java에서 제공하는 API로, 모든 Java의 Data Access 기술의 근간이다.
모든 Persistence Framework는 내부적으로 JDBC API를 이용한다.

JDBC를 통한 DB 연결

1. 드라이버 로드

try {
  Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
  System.err.println(" !! JDBC Driver load 오류: " + e.getMessage());
  e.printStackTrace();
}

제공되는 클래스를 인스턴스화해서 내부적으로 저장해 메모리에 드라이버를 사용할 수 있게한다.
이 메소드를 통해 드라이버를 사용할 수 있도록 초기화 한다.
생성자를 통한 인스턴스 생성과 같다고 볼 수 있다.

2. Connection 객체 생성

DriverManager 클래스는 드라이버를 통해 Conection 객체를 만든다.
Connection은 DB와 연결하는 객체로 DB와 연결하는 통로이며, 이를 통해 쿼리를 전달하고 결과값을 반환받는다.

try {
  con = DriverManager.getConnection("jdbc:mysql://" + server + "/" + database + option, userName, password);
  System.out.println("정상적으로 연결되었습니다.");
} catch (SQLException e) {
  System.err.println("연결 오류:" + e.getMessage());
  e.printStackTrace();
}

3. Statement / PreparedStatement 객체 생성

Statement 객체를 통해 insert 쿼리를 작성하면 아래와 같이 각각의 값을 콤마와 따옴표로 구별해야하기 때문에
가독성도 떨어지고 잘못 입력할 확률도 커진다.

statement = connection.createStatement();
String query = "INSERT INTO piece(color, name, position, chessGameId) VALUE ('" + color + "','" + name +  "','" + position + "','" + chessGameId "')";
resultCount = statement.executeUpdate(insertQuery);

PreparedStatement 객체를 사용하면 다음과 같이 속성 값을 ?로 설정하고 set을 통해 설정하면, 자동으로 쿼리를 완성시켜준다.

String query = "INSERT INTO piece(color, name, position, chessGameId) VALUE (?, ?, ?, ?)";
try (Connection connection = dbManager.getConnection();
     PreparedStatement pstmt = connection.prepareStatement(query)) {
  for (Piece piece : pieces) {
    pstmt.setString(1, piece.color().name());
    pstmt.setString(2, piece.name());
    pstmt.setString(3, piece.position().key());
    pstmt.setInt(4, chessGameId);
    pstmt.executeUpdate();
  }
} catch (SQLException e) {
  e.printStackTrace();
}
  • executeQuery() : select를 통한 정보를 조회하고, 쿼리를 전송 후 결과 객체 반환한다.
  • executeUpdate(String query) : Insert, Update, Delete 쿼리를 전송하고 변경된 레코드 수를 반환한다.

4. ResultSet 객체 반환

ResultSet 객체는 select 쿼리의 결과값을 모두 가지고 있는 객체이다.

while (rs.next()) {
                Piece piece = PieceFactory.findByInfo(rs.getString("color"),
                        rs.getString("name"), rs.getString("position"));
                pieces.add(piece);
            }
  • boolean next() : 결과 레코드가 존재하면 true, 없으면 false
  • boolean previous() : 이전 레코드로 이동 (가장 첫 행이면 false)
  • boolean first() : 처음 위치로 이동 (레코드가 없으면 false)
  • boolean last() : 마지막 위치로 이동 (레코드가 없으면 false)
  • String getString(String colLabel) : 현재 커서 위치의 컬럼명에 해당하는 문자열 반환
  • int getInt(String colLabel) : 현재 커서 위치의 컬럼명에 해당하는 정수값 반환
  • String getString(int colIndex) : 컬럼 인덱스에 해당하는 문자열 반환 (1부터 시작)
  • int getInt(int colIndex) : 컬럼 인텍스에 해당하는 정수값 반환 (1부터 시작)

5. 자원 해제

DB 관련 작업을 하면서 Connection, Statement / PreparedStatement, ResultSet 객체를 사용했다.
DB 관련 처리 작업이 완료되었다면, 사용했던 객체들을 메모리에서 해제해주어야 한다.
만약 해당 자원들을 해제해주지 않으면 다음과 같은 문제점이 발생할 수 있다.

  • Connection pool을 사용하지 않은 상태에서 Connection을 닫지 않으면 DBMS에 연결된 새로운 Connection을 생성할 수 없다.
  • Statement / PreparedStatement를 해제하지 않으면, 생성된 갯수가 증가하면서 더 이상 새로운 Statement / PreparedStatement를 생성할 수 없다.

이번 미션에서는 try-with-resource를 통해 자원 close 처리 개선을 하였다.
이는 try 블록의 소괄호 안에서 자원 해제가 필요한 객체를 할당하면 try catch 절이 종료되면서
자원 해제가 필요한 connection과 preparedstatement를 자동으로 close되는 try-catch 문이다.
preparedstatement를 해제하면 현재(가장 최근에 생성한) ResultSet도 자동으로 해제된다.

public List<Piece> findAllByChessGameId(int chessGameId) {
  List<Piece> pieces = new ArrayList<>();
  String query = "SELECT color, name, position FROM piece WHERE chessGameId = ?";
  try (Connection connection = dbManager.getConnection();
       PreparedStatement pstmt = connection.prepareStatement(query)) {
    pstmt.setInt(1, chessGameId);
    ResultSet rs = pstmt.executeQuery();

    while (rs.next()) {
      Piece piece = PieceFactory.findByInfo(rs.getString("color"),
                                            rs.getString("name"), rs.getString("position"));
      pieces.add(piece);
    }
  } catch (SQLException e) {
    e.printStackTrace();
  }
  return pieces;
}

SQL Exception에 대하여

image

checked exception인 SQLException에 대해 어떻게 처리를 할지 고민하다가 결국 외부로 던져주기만 했었다.
휴의 피드백대로 지금은 DAO에서 stacktrace만 출력하도록 변경하였다.
추후에 휴가 SQLException에 대한 참고 자료를 보내주셨다.

[Toby spring] 사라진 SQLException에 따르면 DAO에서 발생하는 예외를 계속해서 외부로 던져주는 것도 하나의 방법이지만,
이렇게 되면 예외가 발생한 DAO를 사용한 Service에서 왜 SQLException이 발생했는지 알 수 없기 때문에
아래와 같이 SQLException을 포장한 예외의 의미를 담은 커스텀 예외를 만들어 던져주는 것도 하나의 방법이 될 수 있다.
이렇게 되면 의미가 분명한 예외로 전달해서 처리할 수 있게 된다.

try {
  //...
} catch (SQLException e) {
  throw DuplicatePieceIdException(e);
}

try {
  //...
} catch (SQLException e) {
  throw DuplicatePieceIdException().initCause(e);
}

또 자료에 따르면 JDBCTemplate을 사용하면 이 내부에서 SQLException을 처리해준다고 하니 차차 개념을 알아가자.

참고 자료