이 글은 H2 데이터베이스의 이전 버전 중 1.4.199 버전을 도커 컨테이너로 실행시키는 방법에 대한 글입니다. 가장 최신 버전을 설치하고자 하실 때에는 도커 이미지의 버전을 명시하지 않으시면 되고, 다른 이전 버전을 설치하고자 하실 때에는 버전을 달리 입력하시면 됩니다.
1) 의존성 추가
dependencies {
implementation org.hibernate:hibernate-entitymanager:4.3.10.Final
implementation com.h2database:h2:2.1.210
}
2) 도커를 통해 H2 설치
이미지 받기
저는 oscarfonts/h2 이미지를 받았습니다. 아래 링크는 도커 허브의 해당 이미지 주소입니다.
https://hub.docker.com/r/oscarfonts/h2
$ docker pull oscarfonts/h2:1.4.199
이미지가 모두 받아지면, 'docker images' 명령어를 통해 이미지 목록을 조회할 수 있습니다.
$ docker images
컨테이너 실행
$ docker run -d -p 1521:1521 -p 8082:8082 -v Users/user/Documents/docker-h2 -e H2_OPTIONS='-ifNotExists' --name=h2 oscarfonts/h2
- -d: 도커 컨테이너를 백그라운드에서 실행
- -p: 도커 컨테이너와 로컬 포트를 연결하는 옵션
- -v: h2 컨테이너 생성 후 데이터가 쌓이면 로컬 피씨 상에 저장하는 경로 지정
- H2_OPTIONS=”ifNotExists”: 새로운 테이블 생성이 가능하게 해줌
- 특정 버전의 이미지를 실행시킬 경우 이미지명 뒤에 ':버전' 명시해야 함
컨테이너를 실행시키는 명령이 제대로 수행되었는지 알고자 하면, 'docker ps' 명령어를 통해 실행 중인 컨테이너 목록을 조회할 수 있습니다.
$ docker ps
h2 컨테이너의 콘솔 실행
h2 컨테이너 내부에서 bash를 실행시킵니다.
$ docker exec -it h2 /bin/bash
Cf. 도커 컨테이너 접속
$ docker exec -it [container name] /bin/bash
도커를 통해 생성한 컨테이너에 접속한다는 것은 다른 말로는 컨테이너의 쉘을 실행시키겠다는 의미입니다. 컨테이너에 특정 명령을 수행하게 하기 위해서는 docker exec [container name]
명령어를 사용할 수 있는데, 여기에 컨테이너 이름과 실행시킬 프로그램을 전달해 해당 프로그램을 실행시킬 수 있습니다. 즉, 쉘을 실행시키기 위해서는 docker exec [container name] /bin/bash
와 같이 사용할 수 있습니다. 다만, 유의해야 할 점은 docker exec
명령을 입력할 때 -it
를 함께 입력해주어야 한다는 것입니다. 이는 STDIN 표준 입출력을 연다는 의미와 함께 tty(pseudo-TTY) 를 통해 접속하겠다는 의미를 가집니다.
h2 접속
아래 공식 문서에서 제공하는 새로운 데이터 베이스 생성 방법입니다. 쉘을 통해 생성하는 방식 외에도 서버를 실행시키고, 애플리케이션단에서 서버에 접근하는 방식도 있습니다. 이 글에서는 쉘을 통해 데이터베이스를 생성하는 방식을 활용했습니다.
http://www.h2database.com/html/tutorial.html#creating_new_databases
$ root@a39d99dc0df0:/opt/h2-data# cd /opt/h2/bin
$ root@a39d99dc0df0:/opt/h2/bin# ls -al
$ total 2496
$ drwxr-xr-x 2 root root 4096 Mar 21 2022 .
$ drwxr-xr-x 6 root root 4096 Mar 21 2022 ..
$ -rw-r--r-- 1 root root 2166760 Mar 13 2019 h2-1.4.199.jar
$ -rw-r--r-- 1 root root 98 Mar 13 2019 h2.bat
$ -rw-r--r-- 1 root root 109 Mar 13 2019 h2.sh
$ -rw-r--r-- 1 root root 105 Mar 13 2019 h2w.bat
이 때 `.jar` 파일의 org.h2.tools.Shell 클래스 파일의 main 메서드를 실행시켜 주면 됩니다.
main 메서드가 실행되면 url과 드라이버를 입력받습니다. 이 때 입력받은 url에 데이터베이스가 없다면 새로운 데이터베이스를 생성하게 됩니다.
username, password 설정을 할 수 있는데, 저는 데이터베이스와 관련된 공부를 위해 간단히 사용할 목적으로 설치하는 터라 username도 sa 그대로 사용하고, 패스워드도 입력하지 않았습니다.
$ root@3710cd4aef05:/opt/h2/bin# java -cp h2-2.1.210.jar org.h2.tools.Shell
데이터베이스에 접속하게되면 SQL문을 통해 DDL 또는 DML을 작성할 수 있습니다. 저는 예제로 MEMBER 테이블을 생성했습니다.
CREATE TABLE MEMBER(
ID INT PRIMARY KEY,
USERNAME VARCHAR(40),
AGE INT
);
3) 프로젝트에 연결하기
spring:
datasource:
url: jdbc:h2:tcp://localhost:1521/test
username: sa
password:
driver-class-name: org.h2.Driver
4) persistence.xml 설정
JPA는 persistence.xml을 통해 필요한 설정 정보를 관리할 수 있다. 이 파일이 [Class Path]/META-INF/persistence.xml 에 있으면 별도의 설정 없이 JPA가 인식할 수 있습니다.
persistence.xml로 관리되는 JPA 설정은 기본적으로 하나의 데이터베이스 당 하나의 영속성 유닛(persistence-unit)으로 구성됩니다. 각 영속성 유닛은 고유한 이름을 가집니다.
데이터베이스에 따라서 제공하는 기능과 문법이 상이합니다. 이렇게 SQL 표준과 다르거나 특정 데이터베이스에서만 제공하는 기능 등을 데이터베이스 방언(Dialect)라고 합니다. JPA는 특정 데이터베이스에 종속적인 기술이 아니며, 다양한 데이터베이스 방언을 지원합니다. 이 글에서는 H2 데이터베이스를 사용하기 때문에, 'org.hibernate.dialect.H2Dialect'를 작성합니다. 데이터베이스를 교체하는 경우 데이터베이스 방언을 변경하면 됩니다.
Cf. 하이버네이트가 제공하는 데이터베이스 방언
https://www.javatpoint.com/dialects-in-hibernate
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" version="2.1">
<persistence-unit name="test">
<properties>
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="javax.persistence.jdbc.user" value="sa"/>
<property name="javax.persistence.jdbc.password" value=""/>
<property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost:1521/test"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
<property name="hibernate.show_sql" value="true" />
<property name="hibernate.format_sql" value="true" />
<property name="hibernate.use_sql_comments" value="true" />
<property name="hibernate.id.new_generator_mappings" value="true" />
<property name="hibernate.hbm2ddl.auto" value="create" />
</properties>
</persistence-unit>
</persistence>
- hibernate.show_sql: 하이버네이트가 생성한 SQL 출력
- hibernate.format_sql: 하이버네이트가 실행한 SQL을 출력할 때 보기 좋게 정렬
- hibernate.use_sql_comments: 쿼리 출력시 주석도 함께 출력
- hibernate.id.new_generator_mappings: JPA 표준에 맞춘 새로운 키 생성 전략 사용
- hibernate.hbm2ddl.auto: 스키마 자동 생성(현재 예제에서는 생략해도 무관합니다.)
우측 블로그에서 잘 설명되어있습니다. (https://dkyou.tistory.com/14)
5) Entity 생성
Member 클래스와 ‘MEMBER’ 테이블 간의 맵핑 정보를 설정해주는 어노테이션들을 추가합니다.
- @Entity : 이 클래스가 테이블과 매핑되는 엔티티 클래스임을 알려줍니다.
- @Table : 이 엔티티 클래스가 매핑될 테이블 정보를 알려줍니다. (이 어노테이션 생략 시 엔티티 명을 이용해 테이블명과 매핑됩니다.)
- @Id : 기본키에 매핑합니다. (@Id가 사용된 필드를 식별자 필드라고 합니다.)
- @Column : 컬럼에 매핑합니다. 생략할 경우 필드명으로 컬럼명과 매핑합니다.
- Cf. 데이터베이스가 대소문자 구분을 하지 않을 경우 상관없지만, 대소문자 구분을 하는 경우에는 명시적으로 정의해주어야 합니다.
import javax.persistence.*;
@Data
@Entity
@Table(name = "MEMBER")
public class User {
@Id
@Column(name = "ID")
private int id;
@Column(name = "USERNAME")
private String userName;
@Column(name = "AGE")
private int age;
}
6) EntityManager 생성해 테이블에 접근하기
EntityManagerFactory를 생성할 때는 persistence.xml에 명시한 'persistence-unit' 명을 전달해야합니다. 이후 이 EntityManagerFactory로부터 EntityManager를 생성하고, 이 EntityManager로부터 트랜잭션을 할당받습니다. 이 트랜잭션을 실행했을 때 통신이 정상적으로 이루어진다면 콘솔에 JPA가 생성한 SQL문들이 출력될 것입니다.
주의할 점은, EntityManagerFactory를 생성하는 것은 곧 JPA를 동작시키는 기반이 되는 객체를 생성하고, JPA 구현체에 따라 커넥션 풀까지 생성하기도 합니다. 따라서, EntityManagerFactory를 생성하는 데에는 많은 비용이 소모됩니다. 그래서 EntityManagerFactory는 싱글톤 형태로 관리되어야 합니다.
EntityManager는 곧 데이터베이스와의 커넥션을 가지고 직접 데이터베이스에 CRUD하는 기능을 수행합니다. 데이터베이스 커넥션과 밀접한 관계가 있으므로 스레드 간에 공유되거나 재사용되어서는 안됩니다.
EntityManager를 통해 데이터베이스에 CRUD하는 작업들은 모두 트랜잭션이 시작되어서 종료되기 전 사이의 단계에 수행되어야 합니다. 만약 트랜잭션이 시작되지 않았거나 종료된 후에 작업을 수행하려하면 Exception이 발생하게 됩니다.
작업이 종료되면 EntityManager와 EntityManagerFactory 모두 close메서드를 통해 종료해주어야 합니다.
import javax.persistence.*;
import java.util.List;
public class JpaTest {
public static void main(String[] args) {
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("test");
EntityManager entityManager = entityManagerFactory.createEntityManager();
EntityTransaction entityTransaction = entityManager.getTransaction();
try {
entityTransaction.begin(); // 트랜잭션 열기
logic(entityManager); // 실제 처리가 수행되는 메서드
entityTransaction.commit(); // 커밋
} catch (Exception e) {
e.printStackTrace();
entityTransaction.rollback(); // 에러 발생시 트랜잭션 롤백
} finally {
entityManager.close();
}
entityManagerFactory.close();
}
public static void logic(EntityManager em) {
Member member = new Member();
member.setId(1);
member.setUsername("James");
member.setAge(20);
entityManager.persist(member);
// INSERT INTO MEMBER(ID, USERNAME, AGE) VALUES(1, 'James', 20);
member.setAge(25);
// UPDATE MEMBER SET AGE = 25 WHERE ID = 1;
Member result = entityManager.find(Member.class, id);
// SELECT * FROM MEMBER WHERE ID = 1;
List<Member> results = entityManager.createQuery("select m from Member m", Member.class).getResultList();
entityManager.remove(member);
// DELETE FROM MEMBER WHERE ID = 1;
}
}
- persist: 저장(insert)
- setter를 통한 값 변경: 수정(update)
- JPA에서는 어떤 객체의 값이 변경되었는지 추적하기 때문에 setter로 값을 변경한 뒤 따로 저장하는 로직을 수행하지 않아도 됩니다.
- find: 조회(select)
- remove: 제거(delete)
'Backend > Spring' 카테고리의 다른 글
Spring) .yaml(.yml) 파일 프로퍼티 불러오기 (0) | 2024.05.28 |
---|---|
Spring) Spring환경에서의 단위 테스트(JUnit5, Mockito) (0) | 2024.03.26 |
Spring + React 프로젝트 환경 구축 (0) | 2022.05.06 |
Spring) 실제 프로젝트가 실행되는 실제 경로 구하기 (0) | 2022.01.16 |
Spring) MyBatis 동적 쿼리(매개변수 작성 시 $와 #의 차이) (0) | 2022.01.16 |