하위 태스크 1

JDBC 드라이버 의존성 추가

build.gradle에 데이터베이스 드라이버 추가

In-memory 데이터베이스를 사용하기 위해 H2 데이터베이스 엔진을 의존성에 추가한다.

build.gradle:

dependencies {  
    implementation 'com.h2database:h2:2.2.224'  
    // ... 
}

이후 Gradle 프로젝트를 동기화한다.

하위 태스크 2

데이터베이스 연결 구현

DriverManager를 사용한 Connection 획득

DriverManager.getConnection 메서드를 호출해 Connection 객체를 획득한다. H2 데이터베이스를 In-memory 방식으로 사용하기 때문에 jdbc:h2:mem:으로 시작하는 URL을 쓴다.

public class Main {
    public static void main(String[] args) throws SQLException {
        String url = "jdbc:h2:mem:mydb";
        String user = "sa";
        String password = "";
        Connection conn = DriverManager.getConnection(url, user, password);
        
        System.out.println("Connection 객체 → " + conn);
        
        conn.close();
    }
}

main 메서드 실행 결과는 다음과 같다.

Connection 객체 → conn0: url=jdbc:h2:mem:mydb user=SA

하위 태스크 3

Member 테이블 생성

데이터베이스에 테이블 생성

데이터베이스에 id 열, name 열, email 열로 구성된 테이블을 생성한다. 생성된 테이블을 SHOW TABLES 문장으로 확인한다.

public class Main {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        Class.forName("org.h2.Driver");
 
        String url = "jdbc:h2:mem:mydb";
        String user = "sa";
        String password = "";
        Connection conn = DriverManager.getConnection(url, user, password);
 
        Statement stmt = conn.createStatement();
        stmt.execute("CREATE TABLE member (id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255), email VARCHAR(255))");
 
        ResultSet rs = stmt.executeQuery("SHOW TABLES");
 
        while (rs.next()) {
            System.out.println(rs.getString("TABLE_NAME"));
        }
 
        stmt.close();
        conn.close();
    }
}

main 메서드 실행 결과는 다음과 같다.

MEMBER

하위 태스크 4

Statement를 사용한 INSERT

기본 INSERT 작업 구현

member 테이블에 하나의 행을 삽입하는 INSERT 문장을 실행한다. 이후 member 테이블에서 영향을 받은 행을 출력한다.

public class Main {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        Class.forName("org.h2.Driver");
 
        String url = "jdbc:h2:mem:mydb";
        String user = "sa";
        String password = "";
        Connection conn = DriverManager.getConnection(url, user, password);
 
		// 테이블 생성
        Statement stmt = conn.createStatement();
        stmt.execute("CREATE TABLE member (id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255), email VARCHAR(255))");
 
		// 행 삽입
        int result = stmt.executeUpdate("INSERT INTO member (name, email) VALUES ('홍길동', '[email protected]')");
 
        System.out.println(result + "개의 행이 갱신되었습니다.");
 
        stmt.close();
        conn.close();
    }
}

main 메서드 실행 결과는 다음과 같다.

1개의 행이 갱신되었습니다.

하위 태스크 5

Statement를 사용한 SELECT

SELECT 및 ResultSet 처리 구현

member 테이블의 모든 데이터를 조회하는 문장을 실행한다. 이후 name 열의 값을 차례로 순회하며 출력한다.

public class Main {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        Class.forName("org.h2.Driver");
 
        String url = "jdbc:h2:mem:mydb";
        String user = "sa";
        String password = "";
        Connection conn = DriverManager.getConnection(url, user, password);
 
        // 테이블 생성
        Statement stmt = conn.createStatement();
        stmt.execute("CREATE TABLE member (id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255), email VARCHAR(255))");
 
        // 행 삽입
        stmt.executeUpdate("INSERT INTO member (name, email) VALUES ('홍길동', '[email protected]')");
 
        // 테이블 조회
        ResultSet rs = stmt.executeQuery("SELECT * FROM member");
        while (rs.next()) {
            System.out.println(rs.getString("name"));
        }
 
        stmt.close();
        conn.close();
    }
}

main 메서드 실행 결과는 다음과 같다.

홍길동

하위 태스크 6

Statement를 사용한 UPDATE/DELETE

UPDATE, DELETE 작업 구현

member 테이블을 생성하고 두 개의 행을 삽입한 시점의 member 테이블의 모습은 다음과 같다.

idnameemail
1홍길동[email protected]
2김철수[email protected]
id 열이 2인 행의 name김수호로 변경하고, id 열이 1인 행을 제거하는 SQL 문을 실행하는 문장을 추가했다.
public class Main {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        Class.forName("org.h2.Driver");
 
        String url = "jdbc:h2:mem:mydb";
        String user = "sa";
        String password = "";
 
        try (Connection conn = DriverManager.getConnection(url, user, password); Statement stmt = conn.createStatement()) {
            // 테이블 생성
            stmt.execute("CREATE TABLE member (id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255), email VARCHAR(255))");
 
            // 행 삽입
            stmt.executeUpdate("INSERT INTO member (name, email) VALUES ('홍길동', '[email protected]'), ('김철수', '[email protected]')");
 
            // 행 수정
            stmt.executeUpdate("UPDATE member SET name = '김수호' WHERE id = 2");
            stmt.executeUpdate("DELETE FROM member WHERE id = 1");
 
            // 테이블 조회
            try (ResultSet rs = stmt.executeQuery("SELECT * FROM member")) {
	            while (rs.next()) {
	                System.out.println(rs.getString("name"));
	            }
            }
        }
    }
}

main 메서드 실행 결과는 다음과 같다.

김수호

하위 태스크 7

PreparedStatement INSERT 구현

파라미터 바인딩을 사용한 INSERT

member 테이블에 데이터를 삽입하는 코드에서 Statement를 사용해 SQL 문을 실행하던 것을 PreparedStatement로 대체했다.

public class Main {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        Class.forName("org.h2.Driver");
 
        String url = "jdbc:h2:mem:mydb";
        String user = "sa";
        String password = "";
 
        try (
                Connection conn = DriverManager.getConnection(url, user, password);
                Statement stmt = conn.createStatement();
        ) {
            // 테이블 생성
            stmt.execute("CREATE TABLE member (id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255), email VARCHAR(255))");
 
            // 행 삽입
            try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO member (name, email) VALUES (?, ?)")) {
                pstmt.setString(1, "홍길동");
                pstmt.setString(2, "[email protected]");
                pstmt.executeUpdate();
 
                pstmt.setString(1, "김철수");
                pstmt.setString(2, "[email protected]");
                pstmt.executeUpdate();
            }
 
            // 테이블 조회
            try (ResultSet rs = stmt.executeQuery("SELECT * FROM member")) {
                while (rs.next()) {
                    System.out.println(rs.getString("name"));
                }
            }
        }
    }
}

main 메서드 실행 결과는 다음과 같다.

홍길동
김철수

하위 태스크 8

PreparedStatement SELECT 구현

파라미터 바인딩을 사용한 SELECT

member 테이블을 조회하는 SELECT 문에 PreparedStatement를 사용했다. WHERE 절에 파라미터를 사용해 id 열에 대한 조건을 설정할 수 있다. 다음 코드는 id 열이 1인 행을 탐색하고 name 열의 값을 출력한다.

public class Main {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        Class.forName("org.h2.Driver");
 
        String url = "jdbc:h2:mem:mydb";
        String user = "sa";
        String password = "";
 
        try (
                Connection conn = DriverManager.getConnection(url, user, password);
                Statement stmt = conn.createStatement();
        ) {
            // 테이블 생성
            stmt.execute("CREATE TABLE member (id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255), email VARCHAR(255))");
 
            // 행 삽입
            try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO member (name, email) VALUES (?, ?)")) {
                pstmt.setString(1, "홍길동");
                pstmt.setString(2, "[email protected]");
                pstmt.executeUpdate();
 
                pstmt.setString(1, "김철수");
                pstmt.setString(2, "[email protected]");
                pstmt.executeUpdate();
            }
 
            // 테이블 조회
            try (PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM member WHERE id = ?")) {
                pstmt.setLong(1, 1L);
                try (ResultSet rs = pstmt.executeQuery()) {
                    while (rs.next()) {
                        System.out.println(rs.getString("name"));
                    }
                }
            }
        }
    }
}

main 메서드 실행 결과는 다음과 같다.

홍길동

하위 태스크 9

Spring Data JDBC 설정

application.properties 설정 및 의존성 확인

build.gradle에 H2 의존성을 명시하고 Gradle 프로젝트를 동기화한다.

dependencies {
    implementation 'com.h2database:h2:2.2.224'
    // ...
}

application.properties를 H2 데이터베이스 사용 환경에 따라 수정한다.

spring.datasource.url=jdbc:h2:mem:mydb
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=org.h2.Driver

스프링 부트 실행 시 member 테이블을 생성하기 위해 resources 폴더 안에 schema.sql을 작성한다.

CREATE TABLE member (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(255),
    email VARCHAR(255)
);

하위 태스크 10

Member 엔티티 클래스 생성

@Table, @Id 어노테이션 사용

Member 클래스를 선언한다. @Table("MEMBER") 애노테이션은 Member 클래스가 데이터베이스의 member 테이블에 대응함을 알린다. @Id 애노테이션은 id 속성이 기본 키임을 알린다.

@Table("MEMBER") // H2 데이터베이스는 테이블 이름을 대문자로 관리한다.
@Data // Getter, Setter 등의 메서드를 만든다.
@AllArgsConstructor // 모든 속성을 생성자 매개변수로 하는 생성자를 만든다.
public class Member {
    @Id
    private Long id;
    private String name;
    private String email;
}

하위 태스크 11

MemberRepository 인터페이스 생성

CrudRepository 상속

CrudRepository 인터페이스를 상속 받는 MemberRepository 인터페이스를 생성한다. CrudRepository 인터페이스를 상속 받으면 이전 하위 태스크에서 구현했던 여러 JDBC 코드(조회, 삽입 등)를 대체할 수 있다.

public interface MemberRepository extends CrudRepository<Member, Long> {
}

하위 태스크 12

Repository를 사용한 CRUD

Spring Data JDBC를 통한 데이터베이스 작업

memberRepository 인스턴스로 CRUD 작업을 한다.

  • Create: Member 클래스로 홍길동, 김철수 인스턴스를 생성하고 데이터베이스에 삽입한다.
  • Read: member 테이블의 모든 행을 가져오고, 그 내용과 개수를 출력한다.
  • Update: 홍길동 인스턴스의 name 속성을 홍신우로 바꾼다.
  • Delete: 김철수 인스턴스를 제거하고 member 테이블에 남은 행 개수를 출력한다.
@Component
@RequiredArgsConstructor
@Slf4j
public class SpringJdbcApplication implements ApplicationRunner {
    private final MemberRepository memberRepository;
 
    @Override
    public void run(ApplicationArguments args) throws Exception {
        // Create
        Member gildongHong = new Member(null, "홍길동", "[email protected]");
        Member chulsuKim = new Member(null, "김철수", "[email protected]");
        memberRepository.save(gildongHong);
        memberRepository.save(chulsuKim);
 
        // Read
        var members = memberRepository.findAll();
        log.info("members = {}", members);
 
        long count = memberRepository.count();
        log.info("count = {}", count);
 
        // Update
        gildongHong.setName("홍신우");
        memberRepository.save(gildongHong);
        var sinwooHong = memberRepository.findById(1L);
        log.info("updatedGildongHong = {}", sinwooHong);
 
        // Delete
        memberRepository.delete(chulsuKim);
        long countAfterDelete = memberRepository.count();
        log.info("countAfterDelete = {}", countAfterDelete);
    }
}

스프링 부트 실행 결과는 다음과 같다.

members = [Member(id=1, name=홍길동, [email protected]), Member(id=2, name=김철수, [email protected])]
count = 2
updatedGildongHong = Optional[Member(id=1, name=홍신우, [email protected])]
countAfterDelete = 1