Notice
Recent Posts
Recent Comments
Link
«   2025/01   »
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
Tags
more
Archives
Today
Total
관리 메뉴

Joon's Space

[Spring] H2 데이터베이스, 순수JDBC, JdbcTemplate, JPA (5) 본문

Web/Spring

[Spring] H2 데이터베이스, 순수JDBC, JdbcTemplate, JPA (5)

Happy Joon 2022. 1. 26. 22:04

H2 데이터 베이스

- 개발이나 테스트 용도로 가볍고 편리하게 사용할 수 있는 DB

 

https://www.h2database.com

 

H2 Database Engine (redirect)

H2 Database Engine Welcome to H2, the free SQL database. The main feature of H2 are: It is free to use for everybody, source code is included Written in Java, but also available as native executable JDBC and (partial) ODBC API Embedded and client/server mo

www.h2database.com

 

보통 실무에서는 mysql 같은 DB를 사용하는데, 테스트용이기 때문에, 가벼운 h2 db를 사용한다. 

 

위 사이트에서 나의 경우에는 mac을 사용하기 때문에, all platform으로 받아서 다운로드한 경로의 

h2/bin/h2.sh 파일을 실행시켜준다! (chmod 755 h2.sh로 실행 권한 부여)

 

연결이 안될시 주소창의 아이피 부분을 -> localhost로 변경

 

이런 화면이 뜨면 성공!

JDBC URL은 여러 군데에서 접속하기 위해 소켓을 사용하는데, 이때 JDBC URL 은 jdbc:h2:tcp://localhost/~/test로 접속해 준다.

 

home 경로에 test.mv.db 파일이 생성 되었을 텐데, 만약 db에 문제가 생겼을 경우 지우고 연결을 해제하여 다시 h2.sh를 실행해 준다. 

 

이후에 sql 문장으로 필요한 member table을 생성 해 준다.

 

member에는 id, name이 필요한데 type은 각각 정수형, string형

drop table if exists member CASCADE;
create table member
(
 id bigint generated by default as identity,
 name varchar(255),
 primary key (id)
);

 

member table 생성 완료
member table 생성 확인

 

JDBC

위에서처럼 데이터 베이스 설정은 완료 되었는데, 그렇다면 이제 자바를 설치한 데이터 베이스에 연결시키는 작업이 필요하다. 이때 필요한 것이 jdbc이다. jdbc는 데이터베이스에서 자료를 쿼리하거나 업데이트하는 방법을 제공한다. (자바 api)

 

jdbc 환경설정

build.gradle 파일에 h2, jdbc 관련 라이브러리 추가

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	runtimeOnly 'com.h2database:h2'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

 

main/application.properties 에 database 경로와 import 한 driver를 적어준다.

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver

 

순수 jdbc 코드

hello.hellospring/repository/MemoryMemberRepository

package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.jdbc.datasource.DataSourceUtils;
import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class JdbcMemberRepository implements MemberRepository {
    private final DataSource dataSource;
    public JdbcMemberRepository(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    @Override
    public Member save(Member member) {
        String sql = "insert into member(name) values(?)";
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            conn = getConnection();
            pstmt = conn.prepareStatement(sql,
                    Statement.RETURN_GENERATED_KEYS);
            pstmt.setString(1, member.getName());
            pstmt.executeUpdate();
            rs = pstmt.getGeneratedKeys();
            if (rs.next()) {
                member.setId(rs.getLong(1));
            } else {
                throw new SQLException("id 조회 실패");
            }
            return member;
        } catch (Exception e) {
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }
    @Override
    public Optional<Member> findById(Long id) {
        ...
    }
    @Override
    public List<Member> findAll() {
       ...
    }

    @Override
    public void clearStore() {

    }

    @Override
    public Optional<Member> findByName(String name) {
        ...
    }
    private Connection getConnection() {
        return DataSourceUtils.getConnection(dataSource);
    }
    private void close(Connection conn, PreparedStatement pstmt, ResultSet rs)
    {
        try {
            if (rs != null) {
                rs.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        try {
            if (pstmt != null) {
                pstmt.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        try {
            if (conn != null) {
                close(conn);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    private void close(Connection conn) throws SQLException {
        DataSourceUtils.releaseConnection(conn, dataSource);
    }
}

getConnection, releaseConnection을 사용할 때, DataSourceUtils에서 가져와야 한다. (스프링을 사용할 때)

 

위 코드를 작성 후 configuration 작업을 해주어야 서버가 돌아간다. 

 

SpringConfig 파일

package hello.hellospring;

import hello.hellospring.repository.JdbcMemberRepository;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class SpringConfig {

    private DataSource dataSource;

    @Autowired
    public SpringConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
//        return new MemoryMemberRepository();
        return new JdbcMemberRepository(dataSource);
    }
}

이때 memberService와 연결되는 repository는 더 이상 MemoryRepository를 사용하지 않으므로 MemoryRepository에서 JdbcMemberRepository로 변경해 주어야 한다. 그리고 dataSource 변수를 추가해 dababase에 접근하게 해 준다.  (configuration 하는 과정에서 @Bean을 중복으로 추가하면 오류가 뜰 수 있기 때문에, 주의한다.)

 

h2 db에 접근하기 위해 username, password정보도 properties에 다음과 같이 추가로 작성해 주어야 회원 목록에서 오류가 나지 않는다. 

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

 

sql문을 작성해 insert into member (NAME) values("spring1"), insert into member (NAME) values("spring2")로 데이터로 추가하여 다시 앱을 실행하면 다음과 같이 회원목록에 추가된 데이터가 뜨게 된다.

 

데이터를 지우고 추가해서 id부분은 4,5 이다.

여기서 회원가입 기능을 통해서 jpa라는 이름을 추가해 주어도 데이터베이스에 잘 추가되는 것을 볼 수 있다. (h2 database에도 추가됨) 

앱을 껐다가 다시 켜도 다시 남아있는 것도 확인!

 

 

스프링 통합 테스트

 

memoryRepository를 이용했을 때가 아닌 h2 db를 연결한 service를 test 하려고 한다. 

 

MemberServiceIntegrationTest 파일을 생성 후, MemberServiceTest 파일에서 약간 수정해 준다.

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
//import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
//import org.junit.jupiter.api.AfterEach;
//import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import static org.junit.jupiter.api.Assertions.assertThrows;

@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

    @Test
    void 회원가입() {
        // given
        Member member = new Member();
        member.setName("hello");
        ...

memberService와 memberRepository를 불러올 때 @Autowired를 사용하고, MemberServiceIntegrationTest클래스 위에 @SpringBootTest와 @Transactional 데코레이션을 사용 

 

@SpringBootTest는 직접 spring을 실행시켜서 스프링 컨테이너와 테스트를 함께 실행시켜주는 역할을 한다.

 

여기서 @Transactional을 사용하는 이유는 test를 할 때, 이 데코레이션을 사용하면, db에 적용하지만 각 test commit 시키지 않고 다시 rollback을 시켜주기 때문에, test을 할 때 썼다 지웠다 하는 메서드를 작성하지 않아도 돼서 편리하게 test을 할 수 있게 해 주기 때문이다.

(@Transactional을 사용하면 test를 하여도, 위 hello라는 이름이 db에 남지 않는다.)

 

그리고 memory가 아니므로, afterEach, beforeEach로 더 이상 데이터를 초기화시켜 줄 필요가 없으므로 지워 준다. 

 

이번 테스트를 하는 과정에선, 실제 db에서 test를 하는 것이기 때문에 기존 Member 데이터는 방해가 될 수 있기 때문에 모두 지워준다.

 

성공적으로 test 완료

 

JdbcTemplate

스프링 JdbcTemplate은 Mybatis와 비슷하게 JDBC API에서 반복 코드를 제거해 주는 역할을 한다. (sql문은 직접 작성)

 

JdbcTemplateMemberRepository class 생성 후, MemberRepository를 implements 하여, 

 

- save

- findById

- findByName

- findAll 

 

총 4가지 method를 jdbcTemplate을 이용하여 구현 

public Member save(Member member) {

    // insert문 생성 ( "insert into member(id, name) values (id, name)" )
    SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
    jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");

    Map<String, Object> parameters = new HashMap<>();
    parameters.put("name", member.getName());

	// member에 name columns에 저장할 member의 name을 저장
    Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
    member.setId(key.longValue());
    return member;
}

SimpleJdbcInsert를 import 하여, insert문을 위와 같은 방식으로 생성해 준다. (SimpleJdbcInsert) 

public Optional<Member> findById(Long id) {
    List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id);
    return result.stream().findAny();
}

findById에서는 데이터의 조회 부분이므로, jdbcTemplate의 query 함수를 사용한다. query함수의 반환 타입은 list이기 때문에 list타입으로 저장하여, 해당 id에 대하는 member를 return 해준다. 두 번째, parameter로 콜백 함수를 하용하는데 이때, rowmapper를 사용

 

rowmapper는 다음과 같이 정의한다.

private RowMapper<Member> memberRowMapper() {
    return (rs, rowNum) -> {
        Member member = new Member();
        member.setId(rs.getLong("id"));
        member.setName(rs.getString("name"));
        return member;
    };
}

rowmapper란 내가 원하는 타입으로 반환할 수 있게 해주는 기능으로 예를 들면 select로 나온 여러 개의 값을 반환할 수 있을 뿐만 아니라, 사용자가 원하는 형태로도 얼마든지 받을 수 있다.  

 

위 rowmapper에서는 member object를 담고 있는 list가 반환될 것이다.

 

public Optional<Member> findByName(String name) {
    List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);
    return result.stream().findAny();
}

findByName은 query문과 parameter만 name으로 바꾸어 준다.

public List<Member> findAll() {
    return jdbcTemplate.query("select * from member", memberRowMapper());
}

findAll은 모든 member정보를 반환해주므로 따로 데이터를 처리해 줄 필요가 없다.

 

 

JPA

JPA를 사용하면 위 jdbctemplate을 보완하여, query문조차 작성할 필요가 없다. -> 생산성을 크게 높일 수 있다.

sql문을 작성 할 필요가 없기 때문에, sql, 데이터 중심 설계 -> 객체 중심의 설계로 패러다임이 전환될 수 있다.

 

build.gradle 파일에 JPA, h2 데이터 베이스 관련 라이브러리 추가

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
//	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	runtimeOnly 'com.h2database:h2'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

JPA 라이브러리에 jdbc 라이브러리가 포함되어 있기 때문에, 주석처리해주어 제거한다. 

 

JPA 관련 설정 추가 (application.properties)

spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none

application.properties 파일에 JPA관련 설정을 추가한다.

show-sql 은 JPA가 생성하는 sql문을 볼 수 있게 해 주고, hibernate.ddl-auto=none을 통해 JPA가 자동으로 테이블을 생성하는 기능을 꺼준다. 

 

JPA = 객체 + ORM 기술을 합친 개념. JPA가 객체를 관리하기 위해서 @Entity라는 annotation을 사용한다. 위에서 작성한 Member 객체에 @Entity를 사용하여 JPA와 연결시켜 준다.

package hello.hellospring.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY) // 자동적으로 db가 생성 = identity
    private Long id;

    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

@Id는 jpa에 직접 기본키를 매핑해 준다. 우리는 id column이 db가 자동적으로 생성해 주기 때문에 IDENTITY 방식을 사용한다. 

 

이제 JPA로 구동될 member repository class를 생성 해 준다.

 

- JpaMemberRepository class 생성 

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;

public class JpaMemberRepository implements MemberRepository{

    private final EntityManager em;

    public JpaMemberRepository(EntityManager em) {
        this.em = em;
    }

    @Override
    public Member save(Member member) {
        em.persist(member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        Member member = em.find(Member.class, id);
        return Optional.ofNullable(member);
    }
    @Override
    public Optional<Member> findByName(String name) {
        List<Member> result = em.createQuery("select m from Member m where m.name = :name", Member.class)
                .setParameter("name", name)
                .getResultList(); // 이름이 name변수에 담긴 member list를 반환 (이 시점에 flush)
        return result.stream().findAny(); // 1개라도 찾으면 반환
    }

    @Override
    public List<Member> findAll() {
        return em.createQuery("select m from Member m", Member.class)
                .getResultList(); // 모든 member정보 list 반환
    }

    @Override
    public void clearStore() {

    }
}

- save 과정에서 EntityManager 객체의 persist함수를 통해서 영구적으로 영속성을 부여하고 db에 저장한다.

- findById, findByName, findAll과 같은 조회 과정에서는 find 함수나 createQuery를 통해서 list를 반환하여 찾아준다. 

(findById과 같이 키를 이용해서 찾는 것이 아닌 findByName, findAll 과같이 키값으로 찾는 것이 아니라 중복 값이 있어 리스트를 반환할 때는 query문을 작성해야 한다.)

 

주의* h2 database 버전은 1.4.200으로 해 준다. 최신 버전의 h2 database에서 id값에 null값이 insert 되어 오류 발생

반응형