Backend 16
ddl-auto와 테이블 자동 생성: '자동'의 비밀 파헤치기
Entity를 만들었을 뿐인데 DB 테이블이 자동으로 생기는 흐름이 낯설다.
근거 · 교안 p100-p103
1장: DB 테이블, 누가 ‘자동’으로 만들었을까요?
솔라는 자신의 모니터를 믿을 수 없다는 듯이 쳐다봤다. 화면 왼쪽에는 방금 막 작성한 User 클래스 코드가 떠 있었다. @Entity 어노테이션이 붙어있는, 지극히 평범한 자바 클래스였다. 화면 오른쪽에는 데이터베이스 관리 도구가 열려 있었다. 그리고 그 안에는, 방금 전까지 존재하지 않았던 USER 테이블이 선명하게 나타나 있었다.
솔라는 자신이 CREATE TABLE 같은 SQL 구문을 작성한 적이 없다는 것을 확실히 알고 있었다. 그저 애플리케이션을 실행 버튼을 눌렀을 뿐이다. 강의 자료에 적혀 있던 ‘Application 실행 시 Hibernate가 자동으로 테이블을 생성한다’는 문장이 머릿속을 맴돌았다. ‘자동’이라니. 마치 요술 지팡이라도 휘두른 것 같았다. 하지만 개발 세계에 요술은 없었다. 반드시 어딘가에, 자신이 모르는 연결 고리가 있을 터였다.
“언니, 이거 좀 이상해.”
솔라는 노트북을 들고 거실에 있는 루나에게 다가갔다. 루나는 조용히 책을 읽다 말고 솔라를 향해 고개를 들었다.
“내가 User라는 엔티티 클래스만 만들고 애플리케이션을 실행했는데, 데이터베이스에 저절로 USER 테이블이 생겼어. 내가 만들라고 한 적도 없는데.”
솔라의 목소리에는 설명할 수 없는 현상에 대한 짜증과 호기심이 뒤섞여 있었다. 마치 보이지 않는 유령이 자기 대신 일을 처리해 준 것만 같았다.
루나는 솔라의 노트북 화면을 잠시 들여다봤다. User.java 파일과 데이터베이스 클라이언트 화면을 차례로 훑어본 루나는 아무런 평가도 없이 질문을 던졌다.
“솔라, 우리 애플리케이션이 어떤 데이터베이스에 접속해야 하는지는 어떻게 알지?”
“응? 그야 당연히 설정 파일에 적어두니까…”
솔라의 말이 채 끝나기도 전에, 그녀의 눈이 동그래졌다. 무언가 번뜩 스쳐 지나간 것이다. 솔라는 재빨리 프로젝트 탐색기에서 application.yml 파일을 찾아 열었다. 데이터베이스 주소, 사용자 이름, 비밀번호 같은 익숙한 설정들이 눈에 들어왔다.
“우리가 뭔가 ‘자동으로’ 일어나길 바랄 때, 보통은 어딘가에 그렇게 해달라고 요청을 남겨두지 않을까? 마치 레스토랑에서 ‘주방장 특선 요리’를 주문하는 것처럼 말이야. 메뉴판에 적힌 재료만 보고도, 주방장이 알아서 요리를 완성해 내오도록 약속하는 거지.”
루나의 말에 솔라는 고개를 끄덕이며 설정 파일을 빠르게 훑어 내렸다. 주방장에게 보내는 비밀 주문서라도 찾는 사람처럼.
“여기서… ‘ddl’이라는 글자로 한번 찾아볼래?”
루나의 차분한 제안에 솔라는 곧장 검색 기능을 사용했다. Ctrl+F, ddl. 그리고 엔터. 화면이 번쩍이며 한 줄의 코드가 하이라이트되었다.
spring:
jpa:
hibernate:
ddl-auto: create
솔라는 잠시 숨을 멈췄다. 범인은 바로 여기 있었다. ddl-auto. 그리고 그 값인 create. 유령의 정체는 바로 이 설정 한 줄이었던 것이다. 모든 퍼즐 조각이 제자리를 찾는 느낌이었다.
“아…”
짧은 탄식이 솔라의 입에서 터져 나왔다.
“엔티티 클래스는 그냥 설계도 같은 거였구나. 그리고 이 ddl-auto 설정이, 애플리케이션이 시작될 때 하이버네이트라는 기술자에게 ‘이 설계도 보고 건물(테이블)을 새로 지어주세요’라고 말하는 명령이었던 거야. ‘자동’의 주체가 바로 이 설정이었어.”
솔라는 더 이상 ‘자동’이라는 단어가 마술처럼 느껴지지 않았다. 그것은 명백한 설정의 힘이었다. 엔티티 정의만으로는 아무 일도 일어나지 않는다. 애플리케이션이 실행되면서 이 설정 값을 읽고, 그 지시에 따라 데이터베이스 스키마를 생성하는 구체적인 과정이 있었던 것이다. 숨겨져 있던 연결 고리가 드디어 모습을 드러냈다.
자신의 오해를 깨달은 솔라는 한결 후련해진 표정으로 하이라이트된 코드를 다시 바라보았다. 이제 현상은 이해했다. 하지만 새로운 궁금증이 고개를 들었다.
“언니, 그럼 여기 create라고 되어 있는데… 혹시 다른 값도 있는 거야? 만약 create가 아니라 다른 거였다면 어떻게 됐을까? none이나 validate 같은 것도 본 것 같은데, 걔네들은 또 무슨 일을 하는 거지?”
2장: ddl-auto 옵션, ‘자동’을 어떻게 조종할까요?
솔라의 마지막 질문이 남긴 여운 위로, 루나는 대답 대신 작은 메모 패드와 펜을 들었다. 군더더기 없는 글씨체로 다섯 개의 단어를 세로로 적어 내려갔다.
createupdatevalidatenonecreate-drop
루나는 솔라와 자신 사이의 테이블 위에 메모지를 가만히 내려놓았다. 이것은 더 이상 추상적인 질문에 대한 단어의 나열이 아니었다. 솔라가 발견했던 ‘자동’의 배후, 그 설정의 힘을 직접 제어할 수 있는 일종의 ‘조종 패널’이었다.
“create가 무슨 일을 하는지는 이제 알게 됐지.” 루나가 create 단어를 펜 끝으로 가리키며 말했다. “하지만 그 방식엔 꽤나 날카로운 구석이 있어. 우리가 직접 확인해봐야 할 만큼.”
“날카로운 구석?”
솔라는 어리둥절한 표정으로 되물었다. 테이블을 만들어주는 착한 기능인 줄만 알았는데.
“지금 우리 USER 테이블은 데이터베이스에 이미 존재하고, ddl-auto 설정은 create로 되어 있어. 이 상태에서, 만약 User 엔티티에 새로운 정보를 추가해야 한다면 어떻게 될까? 가령 사용자 이메일 주소를 저장할 email 필드를 추가한다고 상상해봐. 그리고 애플리케이션을 다시 실행하면, 어떤 일이 벌어질 것 같아?”
솔라는 잠시 생각에 잠겼다. 그녀의 머릿속에서는 가장 그럴듯한 시나리오가 그려졌다.
“음… create는 ‘만든다’는 뜻이니까, 없는 걸 만들어주지 않을까? USER 테이블은 이미 있지만 email 컬럼은 없으니까, 하이버네이트가 알아서 컬럼을 추가해줄 것 같아. 그게 제일 합리적이잖아.”
그것은 지극히 상식적인 추론이었다. 솔라는 자신의 예측에 확신을 갖고 User.java 파일을 열어 private String email; 한 줄을 추가했다. 그리고 망설임 없이 애플리케이션 실행 버튼을 눌렀다. 하지만 잠시 후, 화면에 나타난 것은 성공적으로 추가된 컬럼이 아니라 새빨간 에러 메시지였다. 애플리케이션은 시작조차 못 하고 비정상적으로 종료되었다.
솔라가 당황해서 에러 로그를 살피자, 그 원인은 명확했다. Table "USER" already exists.
“아!” 솔라가 탄식했다. “컬럼을 추가해주는 게 아니었어. 그냥 CREATE TABLE 명령을 또 실행하려고 한 거구나. 테이블이 이미 있으니까 실패한 거고.”
create의 작동 방식은 솔라의 예상보다 훨씬 단순하고 직선적이었다. ‘있는지 없는지 보고 없으면 만든다’가 아니라, 그냥 ‘만든다’는 명령을 수행할 뿐이었다. 이미 존재하든 말든 상관없이. 솔라가 생각했던 ‘합리적인’ 방식과는 거리가 멀었다.
“바로 그게 create의 날카로운 점이야. 오직 처음, 아무것도 없을 때만 유용하지.” 루나가 말했다. “자, 이제 설정 파일로 가서 create를 update로 바꿔볼래? 다른 건 아무것도 건드리지 말고.”
솔라는 고개를 끄덕이며 application.yml 파일의 ddl-auto 값을 update로 수정하고 저장했다. 그리고 다시 애플리케이션을 실행했다. 이번에는 아무런 에러 없이 부드럽게 구동되었다. 솔라는 반신반의하며 데이터베이스 클라이언트로 시선을 옮겼다. USER 테이블의 구조를 확인하는 순간, 그녀의 눈이 커졌다.
ID, NAME 컬럼 옆에, EMAIL이라는 새로운 컬럼이 선명하게 추가되어 있었다.
“와…”
솔라는 자신도 모르게 감탄사를 내뱉었다. “update는 정말 똑똑하네. 자바 클래스랑 데이터베이스 테이블을 비교해서, 바뀐 부분만 정확하게 고쳐주는구나! create는 그냥 무작정 새로 지으려고만 하는데, 이건 진짜 ‘업데이트’를 해주는 거였어.”
두 옵션의 차이는 극명했다. 하나는 불도저처럼 밀어붙이는 방식이고, 다른 하나는 외과 의사처럼 정교하게 필요한 부분만 도려내고 붙이는 방식이었다. ‘자동’이라는 한 단어 뒤에 숨겨진 전혀 다른 두 개의 세상이었다. 이로써 솔라는 ddl-auto 옵션에 따라 스키마에 가해지는 영향이 어떻게 달라지는지 분석하고 예측할 수 있는 첫 단초를 얻었다.
루나는 솔라가 깨달음을 얻는 순간을 조용히 지켜보다가, 메모지에 적힌 다른 옵션들을 펜으로 가리켰다.
“나머지는 이 둘의 변형이거나 완전히 다른 목적을 가져. create-drop은 create처럼 테이블을 만들지만, 애플리케이션이 종료될 때 깨끗하게 삭제해줘. 매번 새로운 환경에서 테스트하고 싶을 때 유용하지. validate는 ‘검증 전문가’야. 테이블을 만들거나 바꾸지 않아. 대신 엔티티 클래스와 실제 DB 테이블이 일치하는지 검사만 해. 만약 다르면 에러를 내고 앱 실행을 막아버리지. 마지막으로 none은 말 그대로 아무것도 안 하는 거야. 하이버네이트에게 스키마에 관해서는 완전히 손 떼라고 말하는 것과 같아. 보통 실제 운영 서버에서는 이 옵션을 사용해.”
솔라는 다시 테이블 위의 메모지를 보았다. 이제 그 단어들은 더 이상 암호가 아니었다. 각자의 역할과 성격이 뚜렷한, 데이터베이스 스키마를 다루는 다섯 가지 전략이었다.
create: 맨땅에 새로 짓기 (이미 있으면 실패)update: 기존 건물 리모델링하기 (바뀐 부분만 수정)create-drop: 지었다가 바로 철거하기 (테스트용 임시 건물)validate: 설계도와 건물 대조하기 (검사만 수행)none: 아무것도 하지 않기 (전문가가 직접 관리)
제어와 예측의 열쇠를 손에 쥔 기분이었다. 하지만 한 가지 의문이 풀리자, 더 깊은 곳에 숨어있던 새로운 궁금증이 고개를 들었다.
솔라는 자신의 노트북 화면 속 User.java 파일과 데이터베이스 클라이언트를 번갈아 보았다.
“언니, 이제 옵션들은 알겠어. update가 email 필드를 보고 EMAIL 컬럼을 만들어준 것도 이해했고. 그런데… 이상한 점이 있어. 왜 하필 User 클래스가 USER 테이블이 된 거지? 그리고 email 필드는 왜 EMAIL 컬럼이 된 걸까? 만약 내가 테이블 이름을 MEMBER_LIST로, 컬럼 이름은 user_email로 하고 싶으면 어떻게 해야 해? 이것도 설정 파일 어딘가에 또 다른 비밀이 숨어있는 거야?”
3장: 내 Entity가 DB 테이블이 되는 과정: 어노테이션 활용법
솔라의 질문은 설정 파일 어딘가에 숨겨진 또 다른 비밀을 향하고 있었다. 하지만 루나는 application.yml 파일을 닫고, 대신 솔라의 노트북 화면에 떠 있는 User.java 파일을 가리켰다. 그리고는 키보드를 가져가 @Entity 어노테이션 바로 위에 새로운 코드를 한 줄 추가했다.
@Table(name = "MEMBER_LIST")
@Entity
public class User {
// ...
}
솔라가 미처 질문의 답을 예상하기도 전에, 루나는 해답의 실마리를 설정 파일이 아닌 엔티티 클래스 바로 그 자체에 올려놓았다. 마치 건물의 주소를 물었는데, 주소판을 새로 만들어 건물 정면에 달아주는 것과 같았다.
“아…” 솔라의 입에서 짧은 탄성이 터져 나왔다. 비밀은 설정 파일에 있는 게 아니었다. 바로 설계도 자체에 세부 지침을 적어 넣는 방식이었던 것이다. @Table. 너무나 직관적인 이름이었다. 테이블의 속성을 지정하는 주석. 솔라는 자신의 마지막 질문, “컬럼 이름은 user_email로 하고 싶으면 어떻게 해야 해?”를 떠올렸다. 그리고는 루나를 보지 않고도, 스스로 답을 찾아냈다.
솔라는 email 필드 위로 커서를 옮겨, 방금 루나가 했던 것처럼 새로운 어노테이션을 추가했다.
@Column(name = "user_email")
private String email;
“이거구나. 테이블은 @Table로, 컬럼은 @Column으로. 이렇게 직접 이름을 정해주는 거였어.”
솔라는 자신이 발견한 규칙에 만족하며 자신만만하게 말했다. 이제 모든 숨겨진 연결 고리를 찾아낸 것 같았다.
“좋아. 그럼 이제 ddl-auto가 update로 설정되어 있으니, 이대로 실행하면 하이버네이트가 똑똑하게 USER 테이블의 이름을 MEMBER_LIST로 바꾸고, EMAIL 컬럼 이름도 user_email로 바꿔주겠지?”
솔라는 자신의 추론을 증명하고 싶어 서둘러 애플리케이션 실행 버튼을 눌렀다. 이전 장에서 경험했던 update의 ‘스마트함’을 굳게 믿고 있었다. 애플리케이션은 에러 없이 깔끔하게 실행되었다. 솔라는 승리감에 도취되어 데이터베이스 클라이언트를 새로고침했다.
하지만 화면에 나타난 결과는 솔라의 예상을 완전히 빗나갔다. USER 테이블은 조금도 변하지 않은 채 그대로 남아있었고, 그 아래로 MEMBER_LIST라는 이름의 새로운 테이블이 생겨나 있었다. 새로 생긴 MEMBER_LIST 테이블은 솔라가 원했던 대로 user_email 컬럼을 가지고 있었지만, USER 테이블이 이름만 바꾼 것이 아니라 완전히 별개의 테이블이 생성된 것이다.
“어? 이게 뭐야… 왜 테이블이 두 개가 됐지? update가 이름은 못 바꾸는 거야?”
솔라의 자신감은 순식간에 혼란으로 바뀌었다. 외과 의사처럼 정교하다고 생각했던 update의 예상 밖의 행동에 당황했다.
“update는 기존 설계도와 건물을 비교해서, 설계도에 새로 생긴 방이 있으면 추가해주지. 하지만 기존에 있던 방의 이름을 바꾼다고 해서, 그걸 알아채고 원래 방의 명패를 바꿔주지는 않아. 그저 ‘이름이 다른 새로운 방’이 설계도에 생겼다고 판단하고, 새 방을 하나 더 만들 뿐이야.”
루나의 비유는 명확했다. update는 변경 사항을 감지하지만, ‘이름 변경’과 ‘새로운 항목 추가’를 구분할 만큼 섬세하지는 않았던 것이다. 하이버네이트 입장에서는 User 엔티티가 USER 테이블에 매핑된다는 정보가 사라지고, MEMBER_LIST 테이블에 매핑된다는 새로운 정보가 생겼을 뿐이었다. 과거의 연결 고리는 기억하지 못했다.
솔라는 잠시 생각에 잠겼다. 이제야 모든 조각이 맞춰졌다. ddl-auto 설정은 하이버네이트라는 기술자에게 ‘언제, 어떤 방식’으로 일할지를 지시하는 명령이었다. 그리고 엔티티 위의 @Table이나 @Column 같은 어노테이션은, 기술자가 사용할 설계도에 ‘무엇을, 어떻게’ 만들어야 하는지에 대한 구체적인 메모를 남기는 것과 같았다.
ddl-auto설정: 작업의 전략 (새로 지을까? 고쳐 지을까? 검사만 할까?)@어노테이션: 설계도의 세부 명세 (테이블 이름은 이걸로, 컬럼 이름은 저걸로!)
이 두 가지가 조합되어야 비로소 원하는 데이터베이스 스키마를 제어하고 예측할 수 있었다. 솔라는 더 이상 ‘자동’이라는 단어에 휘둘리지 않았다. 자동 생성의 모든 과정을 해부하고, 그 패턴을 진단할 수 있게 된 것이다.
이제 이 혼란스러운 상황을 바로잡을 차례였다. 솔라는 더 이상 루나에게 다음 행동을 묻지 않았다. 스스로 키보드를 잡고 움직이기 시작했다.
먼저, 데이터베이스 클라이언트에서 USER 테이블과 MEMBER_LIST 테이블을 모두 수동으로 삭제했다. 지저분해진 작업대를 깨끗이 치우는 작업이었다.
다음으로, application.yml 파일로 돌아가 ddl-auto 값을 update에서 create로 변경했다. 어차피 처음부터 깨끗하게 만들 것이므로, 혹시 모를 기존 상태와의 비교 과정이 필요 없는, 가장 확실한 방법을 선택한 것이다.
마지막으로, @Table(name = "MEMBER_LIST")와 @Column(name = "user_email") 어노테이션이 붙어있는 User.java 파일을 다시 한번 확인했다. 완벽한 설계도였다.
모든 준비를 마친 솔라는 심호흡 한번과 함께 애플리케이션을 실행했다. 잠시 후, 데이터베이스 클라이언트를 확인한 솔라의 얼굴에 환한 미소가 번졌다.
데이터베이스에는 오직 MEMBER_LIST라는 이름의 테이블 하나만이 존재했다. 그리고 그 테이블의 컬럼들은 ID, NAME, user_email로, 정확히 그녀가 의도했던 모습 그대로였다.
솔라는 자신이 작성한 @Entity 클래스와 어노테이션, 그리고 ddl-auto 설정을 번갈아 보았다. 더 이상 그것들은 마법 같은 ‘자동’의 결과물이 아니었다. 자신이 직접 규칙을 이해하고, 도구를 선택하고, 과정을 제어하여 만들어낸 명백한 결과물이었다. 솔라는 이제 새로운 엔티티를 만들 때, 그것이 데이터베이스에 어떤 모습으로 나타날지 정확히 예측하고 설계할 수 있는 자신감을 얻었다. 보이지 않던 연결 고리는 이제 그녀의 손안에 있는 명확한 설계 지침이 되어 있었다.