껐다 켜면 사라진다 — 데이터베이스와 H2
Article
13강 마지막에 서버를 껐다 켰다.
- $ curl http://localhost:8080/books
- []
손님이 등록한 책이 전부 사라졌다. store 는 그냥 자바 객체였고, 프로그램이 죽으면 메모리도 함께 사라진다.
어딘가에 적어둬야 한다. 그 "어딘가"가 데이터베이스다.
그냥 파일에 적으면 안 되나
된다. 실제로 해볼 수도 있다. 다만 금방 곤란해진다.
- "가격이 20000원 이상인 책만 찾아줘" → 파일을 처음부터 끝까지 읽어야 한다
- 두 사람이 동시에 책을 넣으면 → 한 명의 글이 다른 사람 글을 덮어쓴다
- 파일을 쓰는 도중에 정전이 나면 → 반쯤 적힌 파일이 남는다
데이터베이스는 이런 일을 대신 해주는 프로그램이다. 우리는 "무엇을 달라"고 말만 하면 된다. 그 말이 SQL 이다.
표, 행, 열
데이터베이스는 데이터를 표로 저장한다. 엑셀 시트를 떠올리면 거의 맞다.
자바의 Map
13강
Map<Long, Book> store- 키 → 1, 2, 3
- 값 → Book 객체
- 객체 안에 title, price 가 들어 있다
- 프로그램이 죽으면 사라진다
데이터베이스의 표
이번 강의
book이라는 이름의 표(table)- 행(row) → 책 한 권
- 열(column) → id, title, price
- 열마다 타입이 정해져 있다
- 파일에 남는다
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
);
한 줄씩 읽어보면 뜻이 그대로 나온다.
| 적은 것 | 뜻 |
|---|---|
IF NOT EXISTS | 이미 있으면 만들지 마라 (두 번째 기동 때 에러가 안 난다) |
id BIGINT | id 열은 큰 정수. 자바의 Long 에 해당한다 |
AUTO_INCREMENT | 값을 안 주면 1, 2, 3… 알아서 붙인다 |
PRIMARY KEY | 이 열이 행을 구별하는 번호표다. 중복될 수 없다 |
VARCHAR(255) | 길이가 변하는 글자. 최대 255자 |
NOT NULL | 비워둘 수 없다 |
브라우저로 들여다본다
앱을 띄우면 로그에 이런 줄이 찍힌다.
- 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 |
|---|---|---|
| 만든다 | POST | INSERT |
| 읽는다 | GET | SELECT |
| 고친다 | PUT | UPDATE |
| 지운다 | DELETE | DELETE |
직접 실행해봤다. 결과는 전부 실제 출력이다.
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 을 들여다보고 있다. 둘은 서로를 모른다.
- BookControllerMap<Long, Book> 을 본다
- ??? 이 사이를 이을 것이 없다
- book 표행이 두 개 들어 있다
Book 은 객체다. 필드가 있고 메서드가 있다. book 표는 행과 열이다. 둘은 모양이 다르다.
SELECT * FROM book 의 결과를 List<Book> 으로 바꾸고, new Book("자바", 20000) 을 INSERT 문으로 바꾸는 일. 그 번역을 누가 할 것인가.
정리하면
데이터베이스는 데이터를 표로 적어두는 프로그램이고, H2 는 설치 없이 앱과 함께 뜬다.
- 01
표 · 행 · 열
표는
Map자리, 행은 책 한 권, 열은 필드 하나.id열이 13강의 번호표다. - 02
H2 는 의존성 세 줄이면 뜬다
따로 설치하지 않는다.
jdbc:h2:file:로 파일에 저장한다.spring.datasource.url=jdbc:h2:file:./data/bookdb - 03
SQL 네 문장이 CRUD 다
INSERT·SELECT·UPDATE·DELETE. HTTP 의POST·GET·PUT·DELETE와 하나씩 짝이 맞는다. - 04
WHERE를 빠뜨리면 전부 바뀐다SQL 은 되묻지 않는다. 없는 행을 지워도 에러가 아니라
Update count: 0이다. - 05
파일에 적었으니 껐다 켜도 남는다
13강의 문제가 풀렸다. 다만 API 는 아직 데이터베이스를 모른다.
객체와 표 사이를 잇는 일을 ORM 이라고 부른다. Object(객체) - Relational(관계형 데이터베이스) - Mapping(연결).
자바에서 그 일을 하는 표준이 JPA 다. 그리고 JPA 는 우리에게 표식 하나를 요구한다.
@Entity
public class Book { ... }
@Entity 는 **"이 클래스는 표 하나에 대응한다"**는 표식이다. 자바 편 21강에서 배운 대로, 표식 자체는 아무 일도 하지 않는다. 누군가 읽어야 뜻이 생긴다.
그런데 이 표식을 붙이려면 먼저 답해야 할 질문이 있다.
내용이 똑같은 책 두 권은, 같은 책인가 다른 책인가.
자바 편 14강에서 우리는 Book 의 equals 를 오버라이딩해 제목이 같으면 같은 책이라고 답했다. 내용으로 같음을 정한 것이다.
데이터베이스는 그 답을 거부한다. 같은 제목의 책이 서점에 두 권 꽂혀 있을 수 있기 때문이다. 왜 그런지, 그리고 그래서 id 가 왜 필요한지를 15강에서 본다.