Backend 13
H2, JPA 의존성, Datasource 설정 이해하기
DB를 쓰려면 코드만 작성하면 되는 줄 알았는데 driver, url, username, dialect, ddl-auto가 왜 필요한지 모르겠다.
근거 · 교안 p85-p87
1장: DB는 ‘외부 손님’: H2/JPA 의존성
솔라의 손가락이 노트북 화면 위를 맴돌았다. 코드 에디터에는 build.gradle이라는 이름의 파일이 열려 있었다. 수많은 설정 코드 속에서 솔라의 눈길은 유독 두 줄에 머물러 있었다.
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
“흐음….”
솔라는 알 수 없다는 표정으로 턱을 괴었다. 분명 데이터를 저장하기 위해 필요한 코드라고 들어서 따라 입력하긴 했는데, 볼수록 고개가 갸웃거려졌다. 옆에서 조용히 책을 읽던 루나가 솔라의 나지막한 탄식을 들었는지 시선을 돌렸다.
“왜 그래, 솔라?”
“언니, 이거 좀 이상하지 않아? 우리가 자바 코드를 짤 때, 문자열을 쓰고 싶으면 그냥 String을 쓰고, 리스트를 만들고 싶으면 ArrayList를 만들잖아. 따로 뭘 ‘추가’해주거나 하진 않았단 말이야.”
솔라는 화면의 두 줄을 손가락으로 가리켰다.
“그런데 왜 데이터베이스를 쓰려면 꼭 이런 코드들을 파일에 적어줘야 해? H2 데이터베이스나 JPA 같은 건 자바에 원래 포함된 기능이 아닌 거야?”
솔라의 질문에는 ‘당연히 포함되어 있어야 하는 거 아니야?’라는 순수한 의문이 담겨 있었다. 마치 계산기를 쓰는데 덧셈 기능을 따로 설치해야 한다고 말하는 것처럼 이상하게 들렸다.
루나는 잠시 생각에 잠기더니, 책을 덮고 솔라의 옆으로 다가왔다.
“좋은 질문이야. 그럼 이렇게 한번 생각해볼까? 솔라, 네가 지금 만들고 있는 애플리케이션이랑 H2 데이터베이스는… 하나의 프로그램일까, 아니면 완전히 별개의 프로그램일까?”
“음… 별개의 프로그램?”
솔라는 확신 없이 대답했다. 애플리케이션은 내가 실행하는 것이고, 데이터베이스는 어딘가에 따로 존재하는 데이터를 관리하는 프로그램이라는 막연한 이미지만 떠올랐다.
“맞아. 우리 집과 옆집처럼, 서로 다른 공간에 있는 별개의 존재지.”
루나는 솔라의 노트북 화면을 가리키며 말을 이었다.
“우리가 만든 애플리케이션은 ‘데이터를 저장하고 싶다’는 요청을 데이터베이스에게 보내야 해. 그런데 애플리케이션은 태어날 때부터 H2 데이터베이스와 대화하는 법을 알지는 못해. 세상에는 H2 말고도 MySQL, PostgreSQL처럼 수많은 데이터베이스가 있고, 각각 대화하는 방식이 미세하게 다르거든.”
“아… 그래서 통역사가 필요한 거구나!”
솔라의 눈이 반짝였다.
“정확해. build.gradle 파일에 추가한 ‘com.h2database:h2’라는 코드가 바로 그거야. 우리 애플리케이션에게 H2 데이터베이스라는 ‘외부 손님’과 대화하는 법을 알려주는 전담 통역사, 즉 ‘외부 자원 도우미’를 초대하는 주문인 셈이지.”
루나는 spring-boot-starter-data-jpa라고 적힌 다른 한 줄을 가리켰다.
“그럼 이건? 이것도 비슷한 거야?”
“비슷하면서도 조금 달라. 이건 더 똑똑한 도우미야. 우리가 자바 세상에서 사용하는 ‘객체’라는 개념과 데이터베이스 세상에서 사용하는 ‘테이블’이라는 개념은 모양이 완전히 달라. 이 둘 사이의 어색한 번역 작업을 대신해주는 아주 유능한 전문가라고 생각하면 돼.”
루나의 설명에 솔라는 고개를 끄덕였다. 막연하게 느껴졌던 코드 두 줄이 이제는 각각의 역할을 가진 전문가처럼 보이기 시작했다. 하나는 H2 데이터베이스와 직접 통신하는 실무 통역사, 다른 하나는 자바 객체와 데이터베이스 테이블 사이의 개념을 번역해주는 상위 레벨의 통역사인 셈이다.
“그럼 한번 실험해볼까?”
루나가 제안했다.
“만약 이 도우미들을 초대하지 않으면 어떻게 될까? build.gradle 파일에서 그 두 줄을 지우고 애플리케이션을 실행하면, 어떤 일이 벌어질까?”
솔라는 잠시 상상에 잠겼다. 외부 손님을 만나러 가는데, 통역사를 데려가지 않은 상황.
“아마… 우리 애플리케이션이 ‘데이터베이스랑 연결해줘!’라고 외쳐도, 아무도 그 말을 알아듣지 못하고 오류가 나겠지? ‘H2 데이터베이스와 대화하는 법을 모르겠습니다’ 라거나, ‘JPA가 뭔지 모르겠습니다’ 같은 메시지를 뱉어낼 것 같아.”
솔라의 대답에 루나는 만족스러운 미소를 지었다.
“바로 그거야. 이제 저 두 줄의 코드가 단순한 복사-붙여넣기용 주문이 아니라, 우리 프로젝트에 꼭 필요한 ‘외부 자원 도우미’들을 초대하는 명단처럼 보이지 않아?”
솔라는 다시 화면을 바라보았다. 아까와 똑같은 build.gradle 파일이었지만, 이제는 그 의미가 완전히 다르게 읽혔다. 애플리케이션이라는 본체만으로는 할 수 없는 일들을 처리하기 위해, 외부 전문가들을 프로젝트에 영입하는 과정. 데이터베이스는 자바 안에 내장된 당연한 기능이 아니라, 정중하게 대화법을 익혀서 소통해야 하는 독립적인 ‘외부 손님’이었던 것이다.
“알겠어. 외부에서 온 손님이니까, 그 손님과 대화할 전문가를 우리 편으로 만들어야 한다는 거구나. 의존성을 추가한다는 게 그런 의미였어.”
한 가지 의문이 풀리자, 솔라의 머릿속에는 곧바로 새로운 질문이 꼬리를 물었다.
“그런데 언니, 이제 전문가들(의존성)은 다 불렀잖아. 그럼 이 사람들이 알아서 다 해줘야 하는 거 아니야?”
솔라는 잠시 말을 멈추고 다음 설정 파일인 application.yml을 떠올렸다.
“근데 왜 우리는 또 그 전문가들한테 ‘손님은 저쪽 길로 가서 저 문으로 들어가야 하고요, 암호는 이거예요’ 하고 주소(url)나 이름(username) 같은 걸 일일이 알려줘야 하는 거지? 전문가라면서, 그 정도는 알아서 찾아가야 하는 거 아니야?”
2장: DB 접속은 ‘길 찾기’: Datasource 설정
솔라는 build.gradle 파일을 닫고, 프로젝트의 또 다른 핵심 설정 파일인 application.yml을 열었다. ‘외부 자원 도우미’들을 성공적으로 영입했으니, 이제 모든 게 순조롭게 풀릴 거라 생각했다. 하지만 화면에 나타난 몇 줄의 코드는 그녀의 기대를 다시 원점으로 돌려놓았다.
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
파일의 내용은 마치 초대장 주소를 적는 것 같았다. 하지만 솔라는 이 초대장이 마음에 들지 않았다. H2 데이터베이스와 대화할 전문가(의존성)를 이미 데려왔는데, 그 전문가에게 목적지 주소(url)와 신원(username)을 일일이 알려줘야 하는 상황이 부당하게 느껴졌다.
“언니, 이건 정말 이상해. 우리가 부른 전문가가 길치인가? 아니면 우리가 못 믿는 거야? H2 데이터베이스랑 일하라고 H2 전문가를 데려왔으면, 자기가 알아서 찾아가야 하는 거 아니야? 왜 우리가 또 목적지 주소를 알려주고, 심지어는 사용할 차종(driver-class-name)까지 지정해줘야 해?”
솔라의 목소리에는 투덜거림이 가득했다. 옆에서 그 모습을 지켜보던 루나는 솔라의 노트북 화면으로 시선을 옮겼다.
“그럼 그 전문가가 얼마나 똑똑한지 한번 시험해볼까?”
루나는 url이 적힌 줄을 턱으로 가리켰다.
“만약 솔라 네 말대로 전문가가 알아서 길을 찾을 수 있다면, 저 주소 한 줄쯤은 없어도 괜찮지 않을까?”
“그렇지! 내 컴퓨터 안에서 H2 데이터베이스를 쓰는 건데, 굳이 주소가 왜 필요해. ‘여기요, 여기!’ 하고 손짓하면 알아들어야지.”
솔라는 자신만만하게 url 설정 줄을 지워버렸다. 마치 불필요한 규제를 없애는 공무원이라도 된 듯한 기분이었다. 그리고 곧바로 애플리케이션을 실행했다.
결과는 처참했다. 몇 초 지나지 않아, 애플리케이션은 붉은색 오류 메시지를 토해내며 멈춰버렸다.
Description: Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
“‘url 속성이 지정되지 않았습니다’라고? 정말 하나도 못 알아듣네!”
솔라는 어이없다는 듯 웃으며 지웠던 코드를 다시 복구했다.
“좋아, 주소는 필수라고 치자. 택배 기사님도 주소는 필요하니까. 근데 이건 어때? username. 이름이 ‘sa’라니. 이게 내 이름도 아니고, 누구한테 허락을 맡는다는 거야? 어차피 내 컴퓨터인데.”
솔라는 이번엔 username: sa라고 적힌 부분을 삭제했다. ‘혼자 사는 집에 초인종 암호가 왜 필요해?’라고 생각하며 다시 애플리케이션을 실행했다. 이번에도 결과는 마찬가지였다. 아까와는 조금 다른, 하지만 명백한 실패를 알리는 오류 메시지가 나타났다.
Caused by: org.h2.jdbc.JdbcSQLInvalidAuthorizationSpecException: Wrong user name or password
“‘잘못된 사용자 이름이나 비밀번호’… 하, 정말 깐깐하네.”
솔라는 결국 항복하듯 키보드에서 손을 떼었다. 직접 겪어보니 명확해졌다. 애플리케이션이 데이터베이스에 연결하는 과정은 유능한 전문가에게 일을 맡기는 것과는 달랐다. 그것은 차라리, 보안이 삼엄한 건물에 처음 방문하는 것에 가까웠다.
“알 것 같아. 이 세 가지는… 건물에 들어가기 위한 필수 정보였구나.”
솔라가 천천히 입을 열었다.
“driver-class-name은 어떤 종류의 통신 수단(차량)을 이용해 건물에 접근할지 알려주는 거고, url은 건물의 정확한 주소. 그리고 username은 출입 허가를 받은 방문객의 이름인 거네. 이 세 가지 중에 하나라도 없으면, 정문에서부터 막히는 거였어.”
루나는 말없이 고개를 끄덕였다. 솔라는 이제 application.yml의 datasource 부분을 완전히 새로운 눈으로 바라보고 있었다. 불필요한 지시 사항의 나열이 아니라, 외부 자원이라는 목적지에 도달하기 위한 필수적인 ‘DB 연결 삼총사’처럼 보였다.
“이제 보니 이 세 개는 항상 같이 다녀야 하는 한 팀이었네. 외부 손님을 만나러 가는 길은 생각보다 훨씬 절차가 복잡했어.”
스스로 내린 결론에 만족한 솔라의 시선이 자연스럽게 파일의 아래쪽으로 향했다. 연결에 필요한 삼총사를 모두 확인했으니 이제 정말 끝이라고 생각했다. 하지만 거기에는 또 다른 낯선 설정들이 그녀를 기다리고 있었다.
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create-drop
“좋아, 이제 ‘DB 연결 삼총사’ 덕분에 건물 안으로 들어오긴 했는데… 이건 또 뭐야? dialect? ddl-auto? 손님을 만나기만 하면 되는데, 그쪽 지역 사투리를 써야 한다거나, 만날 때마다 방 구조를 새로 바꿔야 한다는 규칙이라도 있는 거야?”
3장: JPA는 ‘통역사/관리자’: Dialect & DDL-Auto
솔라는 application.yml 파일의 마지막 부분을 노려보고 있었다. ‘DB 연결 삼총사’의 존재를 납득하고 나니, 그 아래에 붙어있는 이 두 줄의 설정이 더더욱 거슬렸다. 건물 주소도 알고, 출입증도 받았는데, 안에 들어가서 나눌 대화의 ‘사투리’까지 지정하고, 만날 때마다 ‘방 구조’를 바꿀지 말지를 정해야 한다니.
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create-drop
‘이건 너무 과한 친절이야. JPA 정도 되는 전문가라면 이 정도는 알아서 해야 하는 거 아니야?’
솔라는 잠시 고민하다, database-platform이라 적힌 줄을 통째로 주석 처리해버렸다. H2 데이터베이스를 쓴다는 건 이미 build.gradle 파일과 datasource 설정에서 충분히 힌트를 줬다고 생각했다. 이 정도면 눈치껏 알아들을 때도 됐다.
“언니, 나 이거 없애 보려고. JPA가 얼마나 똑똑한지 한번 보자.”
솔라의 선언에 옆에 있던 루나가 흥미로운 눈빛으로 화면을 들여다봤다.
“어떤 일이 생길 것 같은데?”
“글쎄? H2 데이터베이스랑 연결은 됐으니까… JPA가 ‘어, 어느 나라 말로 대화해야 할지 모르겠네요’ 하면서 당황하려나? 그래도 표준어 비스무리한 걸로 어떻게든 대화하지 않을까?”
솔라는 반신반의하며 애플리케이션을 실행했다. 하지만 자신만만했던 예측과 달리, 애플리케이션은 시작부터 요란한 경고 메시지를 쏟아내더니 이내 멈춰버렸다.
Unable to determine Dialect without JDBC metadata
“‘JDBC 메타데이터 없이는 Dialect를 결정할 수 없습니다’… 라고? 결국 자기가 어떤 손님을 만나는지 모른다는 거네.”
솔라는 허탈한 웃음을 지으며 주석을 풀었다.
“그럼 이건 어때?”
루나가 이번에는 org.hibernate.dialect.H2Dialect의 H2 부분을 가리켰다.
“만약 우리가 만날 손님은 H2인데, JPA 통역사에게는 손님이 MySQL 출신이라고 거짓말을 하면 어떻게 될까?”
“다른 지역 사투리를 가르쳐주는 거네?”
솔라는 재미있겠다는 듯 H2Dialect를 MySQLDialect로 바꾸고 다시 애플리케이션을 실행했다. 이번에는 애플리케이션이 시작 단계에서 멈추지는 않았다. 하지만 잠시 후, 데이터베이스 테이블을 만들려는 시점에서 또다시 붉은 오류가 화면을 가득 채웠다. 오류 메시지는 H2 데이터베이스가 전혀 알아들을 수 없는 SQL 문법에 대한 불평이었다.
솔라는 그제야 깨달았다. JPA는 모든 데이터베이스에게 통하는 만국 공용어를 구사하는 게 아니었다. 오히려 정반대였다. JPA는 우리가 지정한 dialect에 맞춰, 해당 데이터베이스만이 알아들을 수 있는 아주 구체적이고 특화된 SQL 언어를 만들어내는 정교한 번역기였던 것이다.
“알겠다. dialect는 그냥 ‘힌트’가 아니라 ‘명령’이었어. JPA에게 ‘지금부터 네가 만드는 모든 SQL은 H2 데이터베이스의 문법을 철저히 따라야 해!’라고 알려주는 거구나. 마치 영어 번역가에게 영국식 영어를 쓸지, 미국식 영어를 쓸지 정확히 지정해주는 것처럼.”
첫 번째 의문을 해결한 솔라의 시선은 마지막 남은 설정, ddl-auto: create-drop으로 향했다.
“좋아, 사투리 문제는 해결됐어. 그럼 이건? create-drop. 만날 때마다 방을 만들고, 헤어지면 부숴버리라니. 너무 비효율적인 거 아니야? 손님이 쓸 방을 그냥 그대로 두면 안 돼?”
“그럼 그렇게 한번 해볼까? 이 설정을 validate로 바꿔봐. ‘방이 제대로 있는지 검사만 하라’는 뜻이야.”
루나의 제안에 솔라는 설정을 바꾸고 코드를 실행했다. 결과는 또다시 오류였다.
Schema-validation: missing table 'MEMBER'
“‘스키마 검증: MEMBER 테이블이 없습니다’… 아!”
솔라는 짧은 탄식을 내뱉었다. 너무나 당연한 결과였다. H2 데이터베이스는 mem, 즉 메모리 위에서 동작하기 때문에 애플리케이션이 꺼지면 모든 데이터와 테이블 구조가 사라진다. 그런 상태에서 애플리케이션을 다시 켜고 ‘테이블이 있는지 검사해줘’라고 요청했으니, 테이블이 없다는 오류가 나는 게 당연했다.
“create-drop은… 개발할 때 쓰라고 있는 거구나. 매번 테이블을 지우고 새로 만들어주니까, 내가 코드를 바꾸더라도 항상 깨끗한 상태에서 테스트해볼 수 있는 거네.”
솔라는 그제야 ddl-auto가 단순히 테이블을 만들고 지우는 기능이 아니라, 개발자의 ‘의도’를 반영하는 중요한 스위치라는 사실을 깨달았다. 테스트할 때는 create-drop이나 create를, 실제 운영 환경에서는 이미 만들어진 테이블 구조를 건드리지 않는 validate나 none을 사용해야 한다는 사실을 비로소 몸으로 이해한 것이다.
루나는 아무 말 없이 솔라가 스스로 결론을 내리도록 기다려주었다.
잠시 후, 솔라는 application.yml 파일을 처음부터 끝까지 천천히 스크롤하며 혼잣말처럼 중얼거렸다.
“이제 알겠어. 이 파일은 그냥 복사해서 붙여넣는 설정 모음이 아니었어.”
솔라는 마우스 커서로 파일의 각 부분을 가리키며 설명을 시작했다.
“맨 처음, 우리는 build.gradle에서 H2와 JPA라는 ‘외부 손님 도우미’들을 초대했지. 그리고 여기 datasource 설정은 그 도우미들에게 손님이 계신 곳의 주소와 출입증을 알려주는 ‘DB 연결 삼총사’였어.”
커서가 파일의 마지막 부분으로 이동했다.
“그리고 이 마지막 jpa 설정은, 건물 안으로 들어간 뒤에 필요한 ‘JPA 제어판’인 셈이야. dialect로는 손님의 출신지에 맞는 대화법을 지정해주고, ddl-auto로는 우리가 이 만남을 통해 무엇을 할 것인지, 즉 테이블 구조를 어떻게 다룰지 결정하는 거지.”
솔라는 확신에 찬 목소리로 말했다. 처음에는 의미 없는 규칙의 나열처럼 보였던 설정 파일이, 이제는 외부 데이터베이스라는 손님을 맞이하기 위한 하나의 잘 짜인 시나리오처럼 보였다. 각 설정은 저마다의 명확한 이유를 가지고 유기적으로 연결되어 있었다. 솔라는 더 이상 이 파일의 어떤 줄도 함부로 지우거나 바꾸지 않을 것이었다. 모든 줄의 존재 이유를 설명할 수 있게 되었기 때문이다.