🚩 [자바 웹 개발 워크북 Ch.2] (1) : 2.1 JDBC 프로그래밍 준비+2.2 JDBC 구현

NtoZ·2023년 6월 5일
0

Study

목록 보기
5/9
post-thumbnail

🚩 2.1 JDBC 프로그래밍 준비

MariaDB 설치와 생성

  • MariaDB / Download

  • utf-8 설정

  • 기존에 mysql을 사용하고 있기 때문에 port번호가 겹치지 않도록 3306 -> 3336으로 변경했다.
    (mysql과 mariadb는 동일한 포트번호를 사용한다.)

SQL 에디터 : HeidiSQL

  • DB는 일반적으로 원격지에 떨어져 있는 경우가 많으므로 편집을 위한 별도의 프로그램이 필요.
    윈도우는 자동으로 함께 설치되며, 인텔리제이 얼티메이트 경우 사용 가능.

데이터베이스 webdb 생성 (97)

사용자 추가 및 점검 (98)

프로젝트 생성과 MariaDB 준비

JDBC 프로그램의 구조

  • JDBC 프로그램⭐은 'Java Database Connectivity'의 약자로 자바 프로그램과 데이터베이스를 네트워크 상에서 연결해 데이터를 교환하는 프로그램
  • JDBC 프로그램을 작성하려면 데이터베이스와 자바 프로그램 사이에서 네트워크 데이터를 처리하는 코드가 필요하다. 이 때 JDBC 드라이버가 이런 역할을 수행한다.

    (사진 출처 : DB 작업 - JDBC 1편)

JDBC 프로그램 작성 순서

  • JDBC 프로그램은 네트워크를 통해서 데이터베이스와 연결을 맺고, SQL을 전달해서 데이터베이스가 이를 실행하는 흐름이다.
    => 순서가 중요하다.
    ❶ 네트워크를 통해서 데이터베이스와 연결을 맺는 단계
    데이터베이스에 보낼 SQL을 작성하고 전송하는 단계
    ❸ (필요하다면) 데이터베이스가 보낸 결과를 받아서 처리하는 단계
    데이터베이스와 연결을 종료하는 단계

⭐테스트 프로그램 작성 -JDBC

  • import org.junit.jupiter.api.Assertions;
    import org.junit.jupiter.api.Test;
    JUnit을 활용하여 테스트 코드를 작성할 수 있다.

  • @Test : 테스트를 위한 메소드에 대한 정보 표기 public void이어야 하며, 파라미터가 존재하지 않아야 한다.

  • Assertions.assertEquals(변수1, 변수2) : 변수의 내용이 같은지 확인하는 메서드

  • testConnection() 테스트 코드

Class.forName("org.mariadb.jdbc.Driver");

        Connection connection = DriverManager.getConnection(
                "jdbc:mariadb://localhost:3336/webdb",
                "webuser",
                "webuser");

        Assertions.assertNotNull(connection);

        connection.close();
  • Class.forName("JDBC 드라이버클래스") : JDBC 드라이버 클래스를 메모리상으로 로딩. 해당 드라이버가 존재하지 않는 경우 예외 발생

  • Connection connection : java.sql.Connection은 데이터베이스와 네트워크를 연결하는 인터페이스이다.

  • DriverManager.getConnection("url", "user", "password") : DB 내의 여러 정보를 이용하여 특정 데이터베이스에 연결

    • jdbc:mariadb://localhost:3336/webdb jdbc 프로토콜을 사용하여 mariadb에 연결. localhost:3306은 네트워크 연결 정보, webdb는 연결하려는 DB 정보를 의미한다.
  • Assertions.assertNotNull() : DB와 정상적으로 연결이 된다면 Connection 객체는 null이 아닐 것이다.

  • connection.close() : DB와의 연결을 종료한다. JDBC 프로그램은 리소스 낭비를 방지하기 위해 DB 연결을 잠깐씩 맺고 종료한다. 반드시 작업이 완료되면 DB와의 연결을 종료해야 한다.

DB 테이블 생성 & DML

  • 106~108 : DB 테이블 생성 생략
  • 109~110 : INSERT
  • 111 : SELECT
  • 111 : UPDATE
  • 112 : DELETE

DML과 쿼리(select) 차이

  • DML : 몇 개의 데이터가 처리되었는지 숫자로 결과 반환
    • 몇 개의 row가 추가/변경/삭제 되었는지
  • SELECT : 데이터 반환
    • 레코드로 반환

JDBC 프로그래밍을 위한 API와 용어들

  • java.sql.Connection
    • Connection 인터페이스⭐는 데이터베이스와 네트워크 상의 연결을 목적으로 한다.
    • 실제 구현 클래스는 JDBC 드라이버 파일 내부의 클래스
    • Connection은 반드시 close()해야 한다.⭐ Connection.close()는 데이터베이스쪽에 연결을 끊어도 좋다는 신호를 주고 네트워크 연결 종료
    • try~catch~finally 수동 종료 또는 try~with~resources를 통해 자동 종료
    • Statement 또는 PreParedStatement와 같이 SQL을 실행할 수 있는 객체를 생성한다.
Connection connection = ...
PreparedStatement preparedStatement 
	= connection.prepareStatement("select * from tbl_todo”)
  • java.sql.Statement/PreparedStatement

    • JDBC에서 SQL을 데이터베이스로 보내기 위해서 Statement/PreparedStatement 타입을 이용
    • 그 외 프로시저 호출을 위한 CallableStatement 도 있다.

    • PreparedStatement : SQL문을 미리 전달하고 나중에 데이터를 보내는 방식
    • Statement : SQL 문 내부에 모든 데이터를 같이 전송하는 방식

    • 실제 개발에서는 SQL injection을 예방하기위해 PreparedStatement만을 이용하는 것이 관례
    • StatementConnection과 마찬가지로 close()를 통해서 종료해야 DB 내부의 메모리와 같이 사용했떤 자원들 즉각 정리됨.

    • setXXX() : setInt(), setString(), setDate()와 같이 다양한 타입에 맞게 데이터 세팅
    • executeUpdate() : DML(insert/update/delete)을 실행하고 결과를 int타입으로 반환 (영향 받은 row의 개수)
    • executeQuery() : 쿼리(select) 실행. ResultSet이라는 리턴 타입 이용
  • java.sql.ResultSet

    • PreparedStatement를 이용해서 insert/update/delete를 처리하는 DML의 경우 int로 반환된다. 반면에
      executeQueryselect를 실행한다면 DB에서 반환하는 레코드(행)을 읽어들이기 위해 ResultSet이라는 인터페이스가 이용된다. 이 인터페이스는 자바 코드에서 데이터를 읽기 때문에 getter 메소드를 사용해 필요한 타입으로 데이터를 읽는다.
    • ResultSet의 메소드 중 next()를 사용해서 다음 행(row)의 데이터를 읽을 수 있다. ResultSet은 순차적으로 데이터를 읽도록 구성되어 있다.
    • ResultSet도 네트워크를 통해 데이터를 읽으므로 사용이 끝난 후에는 반드시 close()를 해주어야 한다.
  • Connection Pool과 DataSource

    • JDBC는 필요한 순간에 잠시 DB와 네트워크로 연결하고 데이터를 보내고 받는 방식이다.
      DB와의 연결(Connection)을 맺는 작업은 시간과 자원 소모가 많다. 여러 번 SQL을 실행하면 성능 저하가 발생하는 것이다.
      JDBC는 Connection을 미리 생성해서 보관하며, 필요시에 꺼내쓰는 방식인 Connection Pool(커넥션 풀)⭐을 이용해 문제를 해결한다.

    • javax.sql.DataSource인터페이스는 커넥션 풀을 자바에서 API형태로 지원하며, 커넥션 풀을 이용하는 라이브러리는 모두 DataSource 인터페이스를 구현하므로 이를 활용해서 JDBC 코드를 작성하게 된다.
    • Connection Pool은 기작성된 라이브러리를 이용하는 경우가 많은데, DBCP, C3PO, HikariCP 라이브러리를 이용한다.
  • DAO(Data Access Object)

    • DAO는 데이터를 전문적으로 처리하는 객체이다.
      DB의 접근과 처리를 전담하는 객체를 의미한다.
    • DAO는 주로 VO를 단위로 처리한다.
    • DAO를 호출하는 객체는 DAO가 내부에 어던식으로 데이터를 처리하는지 알 수 없도록 구성한다.
  • VO(Value Object) 혹은 엔티티(Entity)

    • OOP는 데이터를 객체 단위로 처리한다.
      테이블의 한 행(row)는 자바 프로그램에서 하나의 객체가 된다.
    • DB에서 하나의 데이터는 엔티티(entity)라고 하며 자바에서는 이를 처리하기 위해 테이블과 유사한 구조의 클래스를 만들어서 객체로 처리한다.
      이것이 '값을 보관하는 용도'의 VO(Value Object)이다.
    • DTO는 각 계층을 오고 가는 데 사용되며, getter/setter를 모두 사용하지만 VO는 데이터베이서의 엔터티를 자바 객체로 표현되며 데이터 자체를 의미하여 getter만을 이용하는 경우가 대부분이다



🚩2.2. JDBC 구현

🏁 Lombok 라이브러리

  • Lombok features | 공식 문서 - feature

  • 인파 | IntelliJ - Lombok 설치 방법 & 오류 해결

    • 롬복(Lombok)⭐자바 클래스에서 반복적으로 작성되는 getter, setter, toString, 생성자 코드 등의 소스들을, 어노테이션(Annotation)을 사용하여 생략할 수 있도록 컴파일 시점에 자동으로 생성해주는 라이브러리이다.
  • 코끼리 | [JAVA] LOMBOK이란? LOMBOK 적용하는 방법

    • 편리성을 제공하는 라이브러리일수록 주의할 사항으로 API 설명과 내부 동작을 어느정도 숙지하고 사용해야 한다는 점 입니다. 예를 들어 Lombok의 @Data 어노테이션이나 @ToString 어노테이션으로 자동 생성되는 toString()메서드는 순환 참조 또는 무한 재귀 호출 문제로 인해 StackOverflowError가 발생할수도 있습니다. 물론 이 문제를 인지한 Lombok에서 해결할 수 있는 속성들을 제공하지만 롬복이 편리하다는 이유만으로 마구 사용한다면 여러가지 문제가 발생할 수 있다는 것입니다.
  • 자바 웹 개발 워크북 118p

    Lombok 라이브러리를 이용하면 간편해지는 작업들

    • getter/setter 관련 : @Getter, @Setter, @Data 등을 이용해서 자동 생성
    • toString() : @ToString을 이용한 toString()메소드 자동 생성
    • equals()/hashCode() : @EqualsAndHashCode를 이용한 자동생성
    • 생성자 자동 생성 : @ALlArgsConstructor, @NoArgsConstructor 등을 이용한 생성자 자동 생성
    • 빌더 생성 : @Builder를 이용하여 빌더 패턴 코드를 생성. (setter를 편리하게 함)
  • build.gradle에 롬복 라이브러리 의존성 추가 (추가 후에는 그래들 반드시 갱신)

repositories {
	mavenCentral()
}

dependencies {
	compileOnly 'org.projectlombok:lombok:1.18.28'
	annotationProcessor 'org.projectlombok:lombok:1.18.28'
	
	testCompileOnly 'org.projectlombok:lombok:1.18.28'
	testAnnotationProcessor 'org.projectlombok:lombok:1.18.28'
}

커넥션 풀 : HikariCP 설정

  • 코드 연구소 | DB 커넥션 풀(Connection pool)이란? HikariCP란?

    • 커넥션 풀은 데이터베이스와 연결된 커넥션을 미리 만들어 놓고 이를 pool로 관리하는 것이다. 즉, 필요할 때마다 커넥션 풀의 커넥션을 이용하고 반환하는 기법이다. 이처럼 미리 만들어 놓은 커넥션을 이용하면 Connection에 필요한 비용을 줄일 수 있다. 따라서 DB에 빠르게 접속할 수 있다.
    • 또한 커넥션 풀을 사용하면 커넥션 수를 제한할 수 있어서 과도한 접속으로 인한 서버 자원 고갈을 방지할 수 있으며 DB 접속 모듈을 공통화해 DB 서버의 환경이 바뀔 경우 유지보수를 쉽게 할 수 있다.
      :(사진 출처 - https://javarevisited.blogspot.com/2018/07/how-to-setup-jndi-database-connection-pool-tomcat-spring-example-tutorial.html#axzz83pG7yfSj)
    • HikariCP가벼운 용량과 빠른 속도를 가지는 JDBC의 커넥션 풀 프레임워크이다. SpringBoot를 사용해 본 사람이라면 한 번쯤은 이름을 들어본 적이 있을 것이다. SpringBoot는 커넥션 풀 관리를 위해 HikariCP를 사용한다.
  • 히카리CP 벤치마킹 테스트

    • DB 연결을 많이 할수록 HikariCP를 이용하는 것과 사용하지 않는 것에는 상당한 성능 차이가 발생한다. 특히 데이터베이스가 원격지에 떨어져 있는 경우에는 네트워크 연결에 더 많은 시간을 소비해야 하기 때문에 (히카리CP를 사용하고 안하고의) 차이가 더 커진다.
        - (자바 웹 개발 워크북 122p 발췌)
    dependencies {
        compileOnly('jakarta.servlet:jakarta.servlet-api:5.0.0')
        ... 생략...
    // https://mvnrepository.com/artifact/com.zaxxer/HikariCP
    implementation 'com.zaxxer:HikariCP:5.0.0'

Connection Pool 이용하기

  • import com.zaxxer.hikari.HikariConfig;
    import com.zaxxer.hikari.HikariDataSource;를 사용해 커넥션 풀을 구성한다.

    HikariConfig 객체를 생성한다.
    ❷ HikariConfig 객체에 setXXX()를 통해 드라이버클래스명, JdbcUrl, Username, Password 등을 구성하며, addDataSourceProperty("키"-"밸류")로 데이터소스속성을 추가한다.
    ❸ 위의 HikariConfig의 객체를 HikariDataSource 객체의 생성자에 담아 데이터소스 객체를 생성한다.
    Connection connection = ds.getConnection();으로 커넥션 객체를 얻어와 Connection 인터페이스에 담는다.
    connection.close(); //🔥 사용한 자원은 반드시 닫아준다.

TodoDAO와 (Lombok 의) @Cleanup

  • 블로그 | DAO (Data Access Object)
    • DAO는 데이터베이스나 외부 파일 시스템과 같은 영속성 메커니즘에 접근하여 데이터의 CRUD(Create, Read, Update, Delete) 처리를 담당하는 객체 혹은 그러한 패턴을 의미한다.
  • DAO(Data Acess Object)⭐는 커넥션풀을 이용하여 실제 SQL 처리를 전담한다.
  • 응용 프로그램 호출을 지속성 계층에 매핑함으로써 DAO는 데이터베이스 세부 정보를 노출하지 않고 데이터 작업을 제공하며,
    이러한 격리로 단일 책임을 지향한다.사진 출처: Data Access Object(DAO) pattern

ConnectionUtil 클래스

  • TodoDAO에서는 필요한 작업을 수행할 때 HikariDataSource를 이용하게 되므로 이에 대한 처리를 쉽게 사용할 수 있도록 ConnectionUtil 클래스를 enum으로 구성해서 사용한다.
package org.zerock.jdbcex.dao;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import java.sql.Connection;

//⭐ enum 클래스
public enum ConnectionUtil {

    INSTANCE; //⭐ 상수 하나로 싱글톤 패턴을 구현한다.

    //⭐ 싱글톤 패턴이므로 ConnectionUtil.INSTANCE.getConnection()에 의해서 하나의 ds가 반환됨.
    private HikariDataSource ds; // 히카리데이터소스 참조변수 ds

    ConnectionUtil() {
//⭐ HikariConfig 객체에 DB와 연결에 대한 구성정보를 담는다.
        //(드라이버, JDBCURL, 유저이름, 패스웓, 데이터소스속성)
        HikariConfig config = new HikariConfig();
        config.setDriverClassName("org.mariadb.jdbc.Driver");
        config.setJdbcUrl("jdbc:mariadb://localhost:3336/webdb");
        config.setUsername("webuser");
        config.setPassword("webuser");
        config.addDataSourceProperty("cachePrepStmts", "true");
        config.addDataSourceProperty("prepStmtCacheSize", "250");
        config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");

        //⭐ DB와 연결정보가 담긴 HikariConfig 객체를 ConnectionUtil 클래스의 히카리데이터소스 참조변수에 할당
        this.ds = new HikariDataSource(config);
    }
    
    public Connection getConnection() throws Exception {
        return ds.getConnection();
    }

}

다시 TodoDAO

  • 참고 : try~with~resource 구문 (JDK 1.7 추가)
    • try(){}catch(예외){예외처리} 로 사용하며 try() 내에 선언된 변수들이 자동으로 close()된다. (단 try() 내에 선언된 변수들은 모두 Closeable이라는 인터페이스를 구현한 타입들이어야 한다.)

TodoDAOTests 클래스 작성

  • DAO를 작성하면 항상 테스트 코드를 이용하여 동작에 문제가 없는지 확인해야 한다.
  • @BeforeEach⭐ : Junit 애너테이션 모든 테스트 전에 해당 메서드 호출
    (만약 해당 메서드에서 객체를 생성했다면 그 객체를 다른 메서드에 이용 가능)

Lombok의 @CleanUp

  • @CleanUp: try-with-resource보다 가독성 있는, 자원을 회수하는 애너테이션
    @CleanUp이 추가된 변수는 해당 메소드가 끝날 때 close()가 호출되는 것 보장

    단, @Cleanup을 이용하면 Lombok 라이브러리에 상당히 종속적인 코드를 작성하게 된다는 부담은 존재한다.

TodoDAO의 등록 기능 구현

  • void insert(TodoVO vo) throws Exception:
    todoVO 객체 ➡️ 데이터베이스 row에 추가

등록 기능 테스트 (TodoDAOTests.testInsert())

public void testInsert() throws Exception{
        TodoVO todoVO = TodoVO.builder()
                .title("Sample Title...")
                .dueDate(LocalDate.now())
                //finish 속성을 별도로 입력하지 않으면 default에 의해 0이 됨.
                .build();
        todoDAO.insert(todoVO);
    }
  • Lombok의 @Builder 사용 :
    해당 클래스 객체.builder()로 시작하여 .멤버변수명()으로 세팅한 다음 .build()로 마무리하여 데이터를 세팅한다.

    빌더 패턴생성자와 달리 필요한 만큼만 데이터를 세팅할 수 있다.

  • Lombok @Builder의 동작 원리 : 클래스 레벨, 생성자 레벨, Singular

    • 빌더 패턴에서는 한 번 설정한 속성을 여러번 메서드를 호출하여 다시 설정할 수 있어야 하기 때문에 final 키워드는 적합하지 않다.

TodoDAO의 목록 기능 구현

  • public List<TodoVO> selectAll() : select문의 결과로 받아오는 다수의 행들을 List<TodoVO>로 변환하여 반환한다.
    (select문 1개의 결과 : TodoVO
    select문 여러 개의 결과 : List<TodoVO>)

테스트 (TodoDAOTests의 testSelectAll())

TodoDAO의 조회 기능 구현

  • TodoVO selectOne(Long tno) : tno 제목 번호에 따른 단 하나의 레코드(row)를 자바 객체 TodoVO로 변환하여 얻어오기

  • String sql = "select * from tb1_todo where tno = ?"; : 동적쿼리

  • preparedStatement.setLong(1, tno); : 몇 번 게시글을 조회할지 그 인자(tno)를 넘겨 받기 때문에 동적쿼리의 와일드카드?를 인자로 넘겨받은 tno로 바꿔주어야 한다.

  • resultSet.next(); : while(resultSet.next())문을 사용할 필요가 없다. 왜냐하면 쿼리의 실행 결과로 단 하나의 row만 반환되기 때문이다.

테스트 (TodoDAOTests의 testSelectOne())

TodoDAO의 수정/삭제 기능 구현

  • 삭제 void deleteOne(Long tno)

  • 수정 void updateOne(TodoVO vo)

    • String sql = "update tb1_todo set title =?, dueDate = ?, finished = ? where tno = ?"; : update 테이블 set 열1=? , 열2=?, 열3=?, ... where 조건

    • preparedStatement.setString(1, vo.getTitle()); : 와일드 카드 4개에 부합하도록 총 4개의 데이터를 입력해주어야 한다. 그 중 첫 번째 ?는 인자로 전달된 vo의 제목이다.

참고&출처:

자바 웹 개발 워크북

profile
9에서 0으로, 백엔드 개발블로그

0개의 댓글