학습 자료

껐다 켜면 사라진다 — 데이터베이스와 H2


Article

13강 마지막에 서버를 껐다 켰다.

터미널
  • $ curl http://localhost:8080/books
  • []

손님이 등록한 책이 전부 사라졌다. store 는 그냥 자바 객체였고, 프로그램이 죽으면 메모리도 함께 사라진다.

어딘가에 적어둬야 한다. 그 "어딘가"가 데이터베이스다.

그냥 파일에 적으면 안 되나

된다. 실제로 해볼 수도 있다. 다만 금방 곤란해진다.

  • "가격이 20000원 이상인 책만 찾아줘" → 파일을 처음부터 끝까지 읽어야 한다
  • 두 사람이 동시에 책을 넣으면 → 한 명의 글이 다른 사람 글을 덮어쓴다
  • 파일을 쓰는 도중에 정전이 나면 → 반쯤 적힌 파일이 남는다

데이터베이스는 이런 일을 대신 해주는 프로그램이다. 우리는 "무엇을 달라"고 말만 하면 된다. 그 말이 SQL 이다.

표, 행, 열

데이터베이스는 데이터를 로 저장한다. 엑셀 시트를 떠올리면 거의 맞다.

13강의 Map 과 데이터베이스의 표

자바의 Map

13강

  • Map<Long, Book> store
  • → 1, 2, 3
  • → Book 객체
  • 객체 안에 title, price 가 들어 있다
  • 프로그램이 죽으면 사라진다

데이터베이스의 표

이번 강의

  • book 이라는 이름의 표(table)
  • 행(row) → 책 한 권
  • 열(column) → id, title, price
  • 열마다 타입이 정해져 있다
  • 파일에 남는다
Map 의 키가 표의 id 열이 된다. 13강에서 붙인 그 번호표다.

H2 — 설치할 것이 없다

데이터베이스도 프로그램이니 어디선가 돌아야 한다. 보통은 따로 설치해서 켜둔다.

H2 는 그럴 필요가 없다. 자바로 만들어진 데이터베이스라서, 우리 앱이 뜰 때 같이 뜬다. jar 하나가 전부다.

build.gradle 에 세 줄을 더한다.

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-webmvc'
	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	implementation 'org.springframework.boot:spring-boot-h2console'
	runtimeOnly 'com.h2database:h2'
	...
}

8강에서 의존성을 추가하는 것은 자동설정 후보를 추가하는 것이라고 했다. jdbc 를 넣었으니 Spring 이 데이터베이스에 연결하는 일을 알아서 준비한다.

어디에 저장할지 알려준다

application.properties 에 접속 정보를 적는다.

spring.datasource.url=jdbc:h2:file:./data/bookdb
spring.datasource.username=sa
spring.datasource.password=

spring.sql.init.mode=always
spring.h2.console.enabled=true

jdbc:h2:file:./data/bookdb파일에 저장하겠다는 뜻이다. ./data/bookdb.mv.db 라는 파일이 생긴다.

sa 는 H2 의 기본 사용자 이름이고, 비밀번호는 비워둔다. 우리 컴퓨터 안에서만 도는 데이터베이스라 괜찮다.

표를 만든다

표는 미리 만들어둬야 한다. src/main/resources/schema.sql 파일을 만들면 Spring 이 기동할 때 실행해준다.

CREATE TABLE IF NOT EXISTS book (
    id    BIGINT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    price INT NOT NULL
);

한 줄씩 읽어보면 뜻이 그대로 나온다.

CREATE TABLE 읽기
적은 것
IF NOT EXISTS이미 있으면 만들지 마라 (두 번째 기동 때 에러가 안 난다)
id BIGINTid 열은 큰 정수. 자바의 Long 에 해당한다
AUTO_INCREMENT값을 안 주면 1, 2, 3… 알아서 붙인다
PRIMARY KEY이 열이 행을 구별하는 번호표다. 중복될 수 없다
VARCHAR(255)길이가 변하는 글자. 최대 255자
NOT NULL비워둘 수 없다
AUTO_INCREMENT 가 13강에서 우리가 손으로 하던 nextId++ 를 대신한다.

브라우저로 들여다본다

앱을 띄우면 로그에 이런 줄이 찍힌다.

기동 로그
  • INFO --- H2ConsoleAutoConfiguration : H2 console available at '/h2-console'.
  • Database available at 'jdbc:h2:file:./data/bookdb'

브라우저로 http://localhost:8080/h2-console 에 들어가면 로그인 화면이 나온다. JDBC URL 칸에 위 로그의 주소를 그대로 넣고, 사용자명 sa, 비밀번호는 비운 채로 접속한다.

접속하면 왼쪽에 BOOK 표가 보이고, 오른쪽 창에 SQL 을 쳐서 실행할 수 있다.

SQL 은 네 문장이면 시작할 수 있다

여기서 놀라운 대응이 나온다.

같은 것을 세 가지 말로 부른다
하고 싶은 일HTTP 메서드 (3강)SQL
만든다POSTINSERT
읽는다GETSELECT
고친다PUTUPDATE
지운다DELETEDELETE
CRUD 라는 말이 여기서 나왔다. 우리가 13강에서 만든 API 가 곧 이 네 문장으로 번역된다.

직접 실행해봤다. 결과는 전부 실제 출력이다.

INSERT — 행을 넣는다.

INSERT INTO book (title, price) VALUES ('자바', 20000);
INSERT INTO book (title, price) VALUES ('스프링', 32000);

id 를 안 적었다. AUTO_INCREMENT 가 붙여준다.

SELECT — 행을 읽는다.

SELECT * FROM book;
결과
  • ID | TITLE | PRICE
  • 1 | 자바 | 20000
  • 2 | 스프링 | 32000
  • (2 rows, 14 ms)

* 는 "모든 열"이라는 뜻이다. 조건을 걸 수도 있다.

SELECT title, price FROM book WHERE price >= 30000;
결과
  • TITLE | PRICE
  • 스프링 | 32000
  • (1 row, 12 ms)

12강에서 ?minPrice=30000 으로 걸렀던 그 일이다. 그때는 자바 스트림으로 걸렀고, 이제는 데이터베이스가 거른다.

UPDATE — 행을 고친다.

UPDATE book SET price = 18000 WHERE id = 1;
결과
  • (Update count: 1, 6 ms)

한 줄이 바뀌었다고 알려준다.

DELETE — 행을 지운다.

DELETE FROM book WHERE id = 1;
결과
  • (Update count: 1, 7 ms)

없는 것을 건드리면

없는 행을 지워봤다. 그리고 없는 행을 조회해봤다.

결과
  • sql> DELETE FROM book WHERE id = 99;
  • (Update count: 0, 7 ms)
  • sql> SELECT * FROM book WHERE id = 99;
  • ID | TITLE | PRICE
  • (0 rows, 13 ms)

에러가 아니다. 지운 게 0건이고, 읽은 게 0건일 뿐이다.

두 가지가 겹쳐 보인다. 자바 편 16강에서 map.get() 이 없는 키를 주면 조용히 null 을 돌려줬다. 그리고 3강에서 DELETE 를 두 번 보내도 결과가 같다고 했다. 데이터베이스도 같은 태도다.

없는 것을 없다고 알려주는 일은 여전히 우리 몫이다. 13강에서 if 문으로 404 를 돌려줬던 그 자리다.

이제 껐다 켜본다

앱을 껐다. 다시 켰다. 그리고 다시 조회했다.

앱을 재시작한 뒤
  • sql> SELECT * FROM book;
  • ID | TITLE | PRICE
  • 1 | 자바 | 20000
  • 2 | 스프링 | 32000
  • (2 rows, 13 ms)

남아 있다. data/bookdb.mv.db 라는 파일에 적혀 있으니 프로그램이 죽어도 상관없다.

13강이 남긴 문제가 풀렸다.

그런데 API 는 아직 빈손이다

같은 순간에 API 를 불러봤다.

터미널
  • $ curl http://localhost:8080/books
  • []

여전히 빈 배열이다.

당연하다. 데이터베이스에는 책이 두 권 있지만, 우리 BookController 는 아직 자기 Map 을 들여다보고 있다. 둘은 서로를 모른다.

지금 상태
  1. BookControllerMap<Long, Book> 을 본다
  2. ??? 이 사이를 이을 것이 없다
  3. book 표행이 두 개 들어 있다
자바 객체와 표는 생김새가 다르다. 누군가 번역해줘야 한다.

Book 은 객체다. 필드가 있고 메서드가 있다. book 표는 행과 열이다. 둘은 모양이 다르다.

SELECT * FROM book 의 결과를 List<Book> 으로 바꾸고, new Book("자바", 20000)INSERT 문으로 바꾸는 일. 그 번역을 누가 할 것인가.

정리하면

데이터베이스는 데이터를 표로 적어두는 프로그램이고, H2 는 설치 없이 앱과 함께 뜬다.

다시 짚어보기
  1. 01

    표 · 행 · 열

    표는 Map 자리, 행은 책 한 권, 열은 필드 하나. id 열이 13강의 번호표다.

  2. 02

    H2 는 의존성 세 줄이면 뜬다

    따로 설치하지 않는다. jdbc:h2:file: 로 파일에 저장한다.

    spring.datasource.url=jdbc:h2:file:./data/bookdb
  3. 03

    SQL 네 문장이 CRUD 다

    INSERT · SELECT · UPDATE · DELETE. HTTP 의 POST · GET · PUT · DELETE 와 하나씩 짝이 맞는다.

  4. 04

    WHERE 를 빠뜨리면 전부 바뀐다

    SQL 은 되묻지 않는다. 없는 행을 지워도 에러가 아니라 Update count: 0 이다.

  5. 05

    파일에 적었으니 껐다 켜도 남는다

    13강의 문제가 풀렸다. 다만 API 는 아직 데이터베이스를 모른다.

객체와 표 사이를 잇는 일을 ORM 이라고 부른다. Object(객체) - Relational(관계형 데이터베이스) - Mapping(연결).

자바에서 그 일을 하는 표준이 JPA 다. 그리고 JPA 는 우리에게 표식 하나를 요구한다.

@Entity
public class Book { ... }

@Entity 는 **"이 클래스는 표 하나에 대응한다"**는 표식이다. 자바 편 21강에서 배운 대로, 표식 자체는 아무 일도 하지 않는다. 누군가 읽어야 뜻이 생긴다.

그런데 이 표식을 붙이려면 먼저 답해야 할 질문이 있다.

내용이 똑같은 책 두 권은, 같은 책인가 다른 책인가.

자바 편 14강에서 우리는 Bookequals 를 오버라이딩해 제목이 같으면 같은 책이라고 답했다. 내용으로 같음을 정한 것이다.

데이터베이스는 그 답을 거부한다. 같은 제목의 책이 서점에 두 권 꽂혀 있을 수 있기 때문이다. 왜 그런지, 그리고 그래서 id 가 왜 필요한지를 15강에서 본다.

껐다 켜면 사라진다 — 데이터베이스와 H2 — 디코드랩(DCODELAB)