6 분 소요

 인프런 스프링 DB 1편 - 데이터 접근 핵심 원리편을 학습하고 정리한 내용 입니다.

데이터베이스 연결

애플리케이션과 데이터베이스를 연결해보자.

해당 connection패키지에 db 접근에 필요한 정보를 담을 ConnectionConst를 만들었다.

ConnectionConst

1
2
3
4
5
public abstract class ConnectionConst {  
    public static final String URL = "jdbc:h2:tcp://localhost/~/test";  
    public static final String USERNAME = "sa";  
    public static final String PASSWORD = "";  
}

데이터베이스에 접속하는데 필요한 기본 정보를 편리하게 사용할 수 있도록 상수로 만들었다.

이제 JDBC를 사용해서 실제 데이터베이스에 연결하는 코드를 작성해보자.

hello.jdbc.connection.DBConnectionUtil

1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j  
public class DBConnectionUtil {  
  
    public static Connection getConnection() {  
        try {  
            Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);  
            log.info("get connection = {}, class = {}", connection, connection.getClass());  
            return connection;  
        } catch (SQLException e) {  
            throw new IllegalStateException(e);  
        }  
    }  
}

DriverManager.getConnection(URL, USERNAME, PASSWORD)connection을 획득해서 리턴하는 코드다.

DriverManager가 URL같은 정보로 이 DB에 맞는 데이터베이스 드라이버를 찾아서 해당 드라이버가 제공하는 커넥션을 반환해준다. 여기서는 H2 데이터베이스 드라이버가 작동해서 실제 데이터베이스와 커넥션을 맺고 그 결과를 반환해준다.

DBConnectionUtilTest

1
2
3
4
5
6
7
8
9
@Slf4j  
class DBConnectionUtilTest {  
  
    @Test  
    void connection() {  
        Connection connection = DBConnectionUtil.getConnection();  
        assertThat(connection).isNotNull();  
    }  
}

실행 결과를 보면 class=class org.h2.jdbc.JdbcConnection 부분을 확인할 수 있다. 이것이 바로 H2 데이터베이스 드라이버가 제공하는 H2 전용 커넥션이다. 물론 이 커넥션은 JDBC 표준 커넥션 인터페이스인 java.sql.Connection 인터페이스를 구현하고 있다.

JDBC DriverManager 연결 이해

지금까지 코드로 확인해본 과정을 좀 더 자세히 알아보자.

  • JDBC는 java.sql.Connection 표준 커넥션 인터페이스를 정의한다.
  • H2 데이터베이스 드라이버는 JDBC Connection인터페이스를 구현한 org.h2.jdbc.JdbcConnection구현체를 제공한다.

DriverManager 커넥션 요청 흐름

JDBC가 제공하는 DriverManager는 라이브러리에 등록된 DB 드라이버들을 관리하고, 커넥션을 획득하는 기능을 제공한다.

  1. 애플리케이션 로직에서 커넥션이 필요하면 DriverManager.getConnection()을 호출한다.
  2. DriverManager는 라이브러리에 등록된 드라이버 목록을 자동으로 인식한다. 이 드라이버들에게 순서대로 다음 정보를 넘겨서 커넥션을 획득할 수 있는지 확인한다.
    • URL: 예) jdbc:h2:tcp://localhost/~/test
    • 이름, 비밀번호 등 접속에 필요한 추가 정보
    • 여기서 각각의 드라이버는 URL 정보를 체크해서 본인이 처리할 수 있는 요청인지 확인한다. 예를 들어서 URL이 jdbc:h2로 시작하면 이것은 h2 데이터베이스에 접근하기 위한 규칙이다. 따라서 H2 드라이버는 본인이 처리할 수 있으므로 실제 데이터베이스에 연결해서 커넥션을 획득하고 이 커넥션을 클라이언트에 반환한다. 반면에 URL이 jdbc:h2로 시작했는데 MySQL 드라이버가 먼저 실행되면 이 경우 본인이 처리할 수 없다는 결과를 반환하게 되고, 다음 드라이버에게 순서가 넘어간다.
  3. 이렇게 찾은 커넥션 구현체가 클라이언트에 반환된다.

JDBC 개발 - 등록

이제 본격적으로 JDBC를 사용해서 애플리케이션을 개발해보자.

여기서는 JDBC를 사용해서 회원(Member) 데이터를 데이터베이스에 관리하는 기능을 개발해보자.

1
2
3
4
5
6
drop table member if exists cascade; 
create table member ( 
	member_id varchar(10), 
	money integer not null default 0, 
	primary key (member_id) 
);

해당 테이블이 있어야 한다.

자 이제 도메인 패키지를 만들고 Member클래스를 만들어보자.

hello.jdbc.domain.Member

1
2
3
4
5
6
7
8
9
10
11
12
@Data  
public class Member {  
    private String memberId;  
    private int money;  
  
    public Member() {  
    }  
    public Member(String memberId, int money) {  
        this.memberId = memberId;  
        this.money = money;  
    }  
}

회원의 ID와 해당 회원이 소지한 금액을 표현하는 단순한 클래스이다. 앞서 만들어둔 member테이블에 데이터를 저장하고 조회할 때 사용한다.

가장 먼저 JDBC를 사용해서 이렇게 만든 회원 객체를 데이터베이스에 저장해보자.

MemberRepositoryV0 - 회원 등록

hello.jdbc.repository.MemberRepositoryV0

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**  
 * JDBC - DriverManager 사용  
 * */  
@Slf4j  
public class MemberRepositoryV0 {  
  
    public Member save(Member member) throws SQLException {  
        String sql = "insert into member(member_id, money) values (?, ?)";  
  
        Connection con = null;  
        PreparedStatement pstmt = null;  
  
        try {  
            con = getConnection();  
            pstmt = con.prepareStatement(sql);  
            pstmt.setString(1, member.getMemberId());  
            pstmt.setInt(2, member.getMoney());  
            pstmt.executeUpdate();  
            return member;  
        } catch (SQLException e) {  
            log.error("db error", e);  
            throw e;  
        } finally {  
            close(con, pstmt, null);  
        }  
    }  
  
    private void close(Connection conn, Statement stmt, ResultSet rs) {  
  
        if (rs != null) {  
            try {  
                rs.close();  
            } catch (SQLException e) {  
                log.error("error", e);  
            }  
        }  
  
        if (stmt != null) {  
            try {  
                stmt.close();  
            } catch (SQLException e) {  
                log.error("error", e);  
            }  
        }  
  
        if (conn != null) {  
            try {  
                conn.close();  
            } catch (SQLException e) {  
                log.error("error", e);  
            }  
        }  
  
    }  
  
    private Connection getConnection() {  
        return DBConnectionUtil.getConnection();  
    }  
}

커넥션 획득

  • getConnection() : 이전에 만들어둔 DBConnectionUtil를 통해서 데이터베이스 커넥션을 획득한다.

save() - SQL 전달

  • sql : 데이터베이스에 전달할 SQL을 정의한다. 여기서는 데이터를 등록해야 하므로 insert sql을 준비했다.
  • con.prepareStatement(sql) : 데이터베이스에 전달할 SQL과 파라미터로 전달할 데이터들을 준비한다.
    • sql : insert into member(member_id, money) values(?, ?)"
    • pstmt.setString(1, member.getMemberId()) : SQL의 첫번째 ? 에 값을 지정한다. 문자이므로 setString을 사용한다.
    • pstmt.setInt(2, member.getMoney()) : SQL의 두번째 ? 에 값을 지정한다. Int 형 숫자이므로 setInt를 지정한다.
  • pstmt.executeUpdate() : Statement를 통해 준비된 SQL을 커넥션을 통해 실제 데이터베이스에 전달한다. 참고로 executeUpdate()int를 반환하는데 영향받은 DB의 row수다. 여기서는 하나의 row 를 등록했으므로 1을 반환한다.

리소스 정리

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
private void close(Connection conn, Statement stmt, ResultSet rs) {  
  
    if (rs != null) {  
        try {  
            rs.close();  
        } catch (SQLException e) {  
            log.error("error", e);  
        }  
    }  
  
    if (stmt != null) {  
        try {  
            stmt.close();  
        } catch (SQLException e) {  
            log.error("error", e);  
        }  
    }  
  
    if (conn != null) {  
        try {  
            conn.close();  
        } catch (SQLException e) {  
            log.error("error", e);  
        }  
    }  
  
}

다음과 같이 혹시 모를 상황에 대비해서 모두 try ~ catch로 감쌌다.

쿼리를 실행하고 나면 리소스를 정리해야 한다. 여기서는 Connection, PreparedStatement를 사용했다. 리소스를 정리할 때는 항상 역순으로 해야한다. Connection을 먼저 획득하고 Connection을 통해 PreparedStatement를 만들었기 때문에 리소스를 반환할 때는 PreparedStatement를 먼저 종료하고, 그 다음에 Connection을 종료하면 된다. 참고로 여기서 사용하지 않은 ResultSet은 결과를 조회할 때 사용한다.

주의
리소스 정리는 꼭! 해주어야 한다. 따라서 예외가 발생하든, 하지 않든 항상 수행되어야 하므로 finally구문에 주의해서 작성해야한다. 만약 이 부분을 놓치게 되면 커넥션이 끊어지지 않고 계속 유지되는 문제가 발생할 수 있다. 이런 것을 리소스 누수라고 하는데, 결과적으로 커넥션 부족으로 장애가 발생할 수 있다.

참고
PreparedStatementStatement의 자식 타입인데, ?를 통한 파라미터 바인딩을 가능하게 해준다. 참고로 SQL Injection공격을 예방하려면 PreparedStatement를 통한 파라미터 바인딩 방식을 사용해야 한다.

참고로 자바 7부터try-with-resources를 사용하면 좀 더 깔끔하게 자원을 종료 시킬 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Member save2(Member member) throws SQLException {  
    String sql = "insert into member(member_id, money) values (?, ?)";  
  
    try (Connection con = getConnection();  
         PreparedStatement pstmt = con.prepareStatement(sql)) {  
         
        pstmt.setString(1, member.getMemberId());  
        pstmt.setInt(2, member.getMoney());  
        pstmt.executeUpdate();  
        return member;  
          
    } catch (SQLException e) {  
        log.error("db error", e);  
        throw e;  
    }  
}

이러면 finally에서 지저분하게 코드를 길게 안 써도 된다.

자바 9이상부터는 좀 더 간단하게 try-with-resources를 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public Member save3(Member member) throws SQLException {  
    String sql = "insert into member(member_id, money) values (?, ?)";  
  
    Connection con = getConnection();  
    PreparedStatement pstmt = con.prepareStatement(sql);  
      
    try (con; pstmt) {  
  
        pstmt.setString(1, member.getMemberId());  
        pstmt.setInt(2, member.getMoney());  
        pstmt.executeUpdate();  
        return member;  
  
    } catch (SQLException e) {  
        log.error("db error", e);  
        throw e;  
    }  
}

다음처럼 변수로 빼서 넣을 수 있다.

이제 테스트 코드를 사용해서 JDBC로 회원을 데이터베이스에 등록해보자.

MemberRepositoryV0Test - 회원 등록

hello.jdbc.repository.MemberRepositoryV0Test

1
2
3
4
5
6
7
8
9
10
11
class MemberRepositoryV0Test {  
  
    MemberRepositoryV0 repository = new MemberRepositoryV0();  
  
    @Test  
    void crud() throws SQLException {  
        //save  
        Member member = new Member("memberV0", 10000);  
        repository.save(member);  
    }  
}

실행해서 성공하면

다음과 같이 memberV0가 들어가 있는걸 볼 수 있다.

태그: ,

카테고리:

업데이트:

댓글남기기