Backend 14

JPA Entity: 당신의 Java 객체가 데이터베이스에서 살아 숨 쉬는 이유

일반 Java 클래스와 Entity 클래스가 무엇이 다른지 모르겠다.

근거 · 교안 p88-p93

JPA Entity: 당신의 Java 객체가 데이터베이스에서 살아 숨 쉬는 이유 대표 이미지

1장: 객체의 ‘메모리 수명’과 ‘영속적 삶’의 차이

솔라의 손가락이 노트북 키보드 위를 무의미하게 떠돌다 멈췄다. 화면에는 방금 작성한 몇 줄의 자바 코드가 선명했다. 평범한 User 클래스. 이름과 이메일을 담는, 수없이 만들어 봤던 그런 종류의 것이었다. 하지만 솔라는 코드 너머의 무언가에 막혀 있었다. 마치 투명한 벽에 부딪힌 기분이었다.

public class User {
    private String name;
    private String email;

    // 생성자, getter, setter...
}

이 코드를 볼 때와, JPA 관련 예제에서 본 @Entity가 붙은 User 클래스를 볼 때, 솔라의 머릿속에서 두 이미지는 전혀 다른 종류의 것으로 느껴졌다. 하나는 익숙하고 만만했지만, 다른 하나는 보이지 않는 복잡한 규칙과 약속으로 똘똘 뭉친 존재 같았다.

“언니.”

나지막한 부름에, 책을 읽던 루나가 고개를 들었다. 솔라의 턱짓이 향하는 노트북 화면을 잠시 바라본 루나는, 말없이 일어나 솔라의 책상 옆으로 다가왔다. 그리고는 솔라의 코드나 화면이 아닌, 책상 한쪽에 놓인 메모지 뭉치와 펜을 조용히 가져왔다.

“이 User 클래스로 객체 하나를 만들어 봐, 머릿속으로 말고, 여기 이 종이에.”

솔라는 의아한 표정을 지었지만, 루나가 내민 펜을 받아들었다. 그리고는 메모지 한 장을 뜯어내어 조심스럽게 적었다.

User userA = new User("솔라", "sola@example.com");

“자, 여기. userA 객체가 만들어졌어. 메모리 어딘가에 있겠지.”

솔라는 종이를 책상 위에 올려놓았다. 루나는 그 종이를 가만히 내려다보더니 말했다.

“좋아. 그럼 이제 userA의 이름을 바꿔볼까?”

“간단하지. userA.setName("솔라 Kim");

솔라는 펜을 들어 종이에 적힌 “솔라” 위에 두 줄을 긋고, 그 옆에 “솔라 Kim”이라고 새로 썼다. 메모지에는 이제 작은 수정의 흔적이 남았다.

“잘했어. 지금 이 종이가 네 컴퓨터의 메모리라고 생각해 봐. 객체를 만들고, 그 객체의 상태를 바꿨어. 아주 자연스러운 일이지.”

루나의 말에 솔라는 고개를 끄덕였다. 여기까지는 전혀 문제 될 게 없었다. 늘 하던 일이었다.

“이제, 프로그램을 종료해 봐.”

”…….”

솔라의 손이 순간 멈칫했다. 프로그램 종료. 그건 곧 이 모든 작업이 실행되던 프로세스가 끝나는 것을 의미했다. 메모리에 할당되었던 모든 것이 사라지는 순간. 솔라는 잠시 망설이다, 책상 위에 놓여 있던 메모지를 손에 들어 천천히 구겼다. 작게 바스락거리는 소리를 내며 종이가 둥글게 뭉쳐졌다.

솔라는 손바닥 안의 구겨진 종이뭉치를 내려다보았다. 방금 전까지 ‘솔라 Kim’이라는 정보를 담고 있던 객체, userA의 마지막 모습이었다.

“사라졌네.” 솔라가 작게 중얼거렸다. “컴퓨터를 끄거나, 프로그램이 끝나면 그냥… 사라지는 거. 당연한 건데.”

“그래, 당연한 일이야.” 루나가 조용히 말을 받았다. “그게 바로 우리가 보통 ‘객체’라고 부르는 것들의 ‘메모리 수명’이지. 프로그램이 살아있는 동안에만 숨을 쉬고, 프로그램이 끝나면 함께 사라지는 운명.”

루나는 아무것도 쓰여있지 않은 새 메모지를 가리켰다. 그리고 솔라의 손바닥 안에 남은 구겨진 종이뭉치를 번갈아 보았다. 그 차이가 너무도 명확해서, 다른 설명이 필요 없을 정도였다.

솔라는 그제야 자신이 무엇을 놓치고 있었는지 어렴풋이 깨달았다. @Entity가 붙은 클래스를 보며 느꼈던 이질감의 정체. 그것은 단순히 애너테이션 하나가 더 붙어있다는 사실 때문이 아니었다.

솔라는 구겨진 종이를 책상 위에 내려놓고, 다시 노트북 화면을 바라봤다. 화면 속의 평범한 User 클래스가 이제는 다르게 보였다. 저것은 잠시 태어났다가 프로그램이 끝나면 미련 없이 사라질 존재. 일시적인 삶을 사는 존재였다.

“그럼…” 솔라가 입을 열었다. 이전과는 다른 무게가 실린 질문이었다.

“일반 자바 클래스랑 Entity 클래스는 뭐가 다른 거야? 어떻게 하면… 이 구겨진 종이처럼 사라지지 않고 계속 살아있게 만들 수 있는 건데?”

솔라의 질문은 더 이상 두 클래스의 문법적 차이를 묻고 있지 않았다. 그것은 객체의 일시적인 ‘메모리 수명’을 넘어, 어떻게 하면 ‘영속적인 삶’을 부여할 수 있는지에 대한, 근본적인 방법에 대한 질문으로 바뀌어 있었다. 그 연결고리를 만드는 ‘숨겨진 손’의 역할, 그것이 바로 솔라가 정말로 궁금했던 것이었다.

2장: @Entity: 클래스를 ‘영속 객체’로 선언하는 표식

솔라의 질문이 남긴 고요함 속에서, 루나는 구겨진 종이뭉치를 바라보고 있는 솔라의 시선을 따라갔다. ‘어떻게 하면 계속 살아있게 만들 수 있는가.’ 그 질문은 더 이상 코드의 문법에 대한 것이 아니었다. 그것은 존재의 방식에 대한 물음이었다.

루나는 말없이 솔라의 노트북을 자신의 쪽으로 살짝 돌렸다. 그리고는 화면에 떠 있는 User 클래스 코드를 그대로 복사했다. 이어서 새로운 파일 두 개를 나란히 열고, 각각의 파일에 똑같은 코드를 붙여넣었다. 이제 화면에는 왼쪽의 UserA.java와 오른쪽의 UserB.java, 두 개의 동일한 클래스가 떠 있었다.

// UserA.java
public class User {
    private String name;
    private String email;

    // ...
}
// UserB.java
public class User {
    private String name;
    private String email;

    // ...
}

솔라는 루나가 무엇을 하려는지 알 수 없어 잠자코 지켜보았다. 루나는 오른쪽 UserB.java 파일의 public class User 바로 윗줄에, 조용히 한 줄을 추가했다.

@Entity

단 한 줄의 추가. 하지만 그로 인해 두 클래스는 더 이상 동일하지 않았다. 하나는 평범한 User 클래스, 다른 하나는 @Entity라는 표식이 붙은 User 클래스가 되었다. 루나는 아무 말 없이 노트북을 다시 솔라에게로 돌려주었다.

“자, 이제 상상해 봐.” 루나가 입을 열었다. “JPA라는 이름의 ‘숨겨진 손’이 이 방에 들어와서 이 두 개의 클래스 설계도를 본다고.”

솔라는 눈을 가늘게 뜨고 화면을 응시했다. UserAUserB. 하나는 익숙했고, 다른 하나는 여전히 어색한 모자를 쓰고 있는 것 같았다.

“음… UserB 쪽을 먼저 쳐다보겠지? 자기만 아는 표식이 붙어 있으니까. 마치… 클럽 입장용 도장 같은 거?” 솔라는 나름의 비유를 들어 말했다. “근데 그게 다 아니야? 일단 알아보고 나면, 둘 다 똑같은 User 클래스잖아. 이름이랑 이메일 필드를 가진.”

솔라의 생각은 간단했다. @Entity는 JPA에게 ‘이 녀석이야!’라고 알려주는 이름표일 뿐, 클래스의 본질 자체를 바꾸지는 못할 거라는 믿음. 그건 그저 주석이나 꼬리표 같은 장식이라고 생각했다.

“클럽 입장용 도장이라.” 루나는 그 비유를 잠시 곱씹더니, 고개를 저었다. “그것보다는 ‘출생신고서’에 가까워.”

“출생신고서?”

“응. UserA 클래스는 그냥 종이에 그린 사람 그림 같은 거야. 얼마든지 그리고, 구겨서 버릴 수 있지. 누구도 그 그림이 사라졌다고 해서 찾으러 다니지 않아. 우리가 아까 구겨버린 메모지처럼.”

루나는 솔라의 책상 위, 구겨진 종이뭉치를 턱으로 가리켰다.

“하지만 UserB는 달라. @Entity라는 표식은, 이 클래스로부터 태어날 모든 객체는 이제부터 내가 관리하겠다는 JPA의 공식적인 선언이자, 약속이야. JPA에게 ‘이 클래스는 이제부터 평범한 객체가 아니라, 데이터베이스와 연결되어 영속적인 삶을 살아야 할 존재다’라고 알려주는 거지. 일종의 출생신고서를 제출해서, 국가 시스템에 정식으로 등록되는 아기처럼.”

솔라의 눈동자가 흔들렸다. 이름표가 아니었다. 장식은 더더욱 아니었다. 그것은 객체의 운명을 근본적으로 바꾸는, 일종의 ‘계약’이었다. 메모리 안에서만 살다 죽는 일시적인 존재에서, 시스템이 그 삶을 보장하고 관리하는 ‘영속적인 존재’로 신분을 바꾸는 절차였던 것이다.

솔라는 다시 화면의 두 클래스를 보았다. 이제는 완전히 다르게 보였다. 왼쪽의 UserA는 자유롭지만 고독하고 유한한 존재. 오른쪽의 @Entity가 붙은 UserB는 JPA라는 거대한 시스템의 관리를 받는, 책임과 권리를 동시에 지닌 존재.

“아…!” 솔라의 입에서 작은 탄성이 터져 나왔다. “그러니까 @Entity는 ‘JPA님, 이 클래스로 만드는 객체는 이제 당신이 책임져 주세요. 프로그램이 꺼져도 사라지지 않게, 데이터베이스에 잘 보관하고 관리해주세요’라고 요청하는 거구나. 연결고리의 시작점이네.”

‘JPA Entity는 DB 테이블과 연결되는 영속 객체다.’ 솔라의 머릿속을 맴돌던 불투명한 문장이 비로소 선명한 의미를 갖기 시작했다. @Entity라는 출생신고서를 제출함으로써, 비로소 객체는 ‘영속 객체’로서의 첫발을 내딛는 것이었다.

솔라는 잠시 생각에 잠겼다가, 새로운 질문을 찾아냈다. 호기심 많은 탐정처럼, 해결된 미스터리에서 다음 사건의 실마리를 발견한 것이다.

“알겠어. 이제 @Entity를 붙여서 만든 User 객체는 JPA가 관리하는 특별한 존재가 됐어. 그런데 말이야, 내가 User 객체를 백 개 만들면, 데이터베이스에도 백 개의 줄이 생기겠지? 전부 @Entity가 붙은 클래스로 만들었으니까 JPA가 다 관리해 줄 거고. 그럼 나중에 내가 ‘솔라’라는 이름을 가진 사용자를 찾고 싶을 때, JPA는 그 백 개의 줄 중에서 정확히 어떤 줄이 내가 찾는 ‘솔라’인지 어떻게 아는 거지? 모두 똑같은 User 타입인데.”

솔라의 질문은 정확했다. JPA가 객체들을 ‘관리’하기로 약속했다면, 그들을 서로 ‘구별’할 방법 또한 반드시 필요했다. 수많은 영속 객체들 사이에서 단 하나의 존재를 특정할 수 있는 유일한 표식. 영속성의 연결고리는 이제 새로운 국면을 맞이하고 있었다.

3장: @Id와 @GeneratedValue: DB 레코드를 고유하게 식별하는 열쇠

솔라의 마지막 질문은 허공에 맴돌지 않았다. 그것은 곧장 루나에게로 향하는 명확한 화살이었다. 수백 개의 똑같은 User 객체들 사이에서, JPA는 어떻게 단 하나의 ‘솔라’를 구별해내는가?

루나는 대답 대신, 키보드를 몇 번 두드려 빈 스프레드시트 창을 화면에 띄웠다. 격자무늬만 가득한, 텅 빈 디지털 종이었다. 솔라가 이전 장에서 봤던 UserA@Entity가 붙은 UserB 클래스 코드는 화면 한쪽으로 밀려나 있었다.

“여기에 방금 네가 말한 User 객체 백 개를 만든다고 생각해 봐.” 루나가 C열과 D열을 넓히며 말했다. “C열은 이름, D열은 이메일. 한 번 몇 개만 채워볼래?”

솔라는 잠시 어리둥절했지만, 루나의 의도를 파악하고 키보드를 잡았다.

ABCD
이름이메일
솔라sola@example.com
루나luna@example.com
솔라sola.dev@example.com

세 번째 행을 입력했을 때, 솔라의 손가락이 멈칫했다. 이름이 ‘솔라’인 데이터가 벌써 두 개였다.

“자, 이제 세 번째 줄에 있는 ‘솔라’의 이메일을 ‘sola.coder@example.com’으로 바꿔줘.” 루나가 요청했다.

솔라는 마우스 커서를 움직여 세 번째 행의 이메일 셀을 클릭하고 수정했다. 간단한 작업이었다.

“어떻게 그 줄이 내가 말한 ‘솔라’인 줄 알았어?”

“방금 내가 입력했으니까. 그리고 ‘sola.dev’ 이메일을 가진 솔라는 하나뿐이잖아.”

“좋아. 그럼 첫 번째 줄에 있는 ‘솔라’를 지워줘.”

솔라는 망설임 없이 첫 번째 데이터 행을 삭제했다. 너무나 명확한 지시였다.

“이상하지 않아?” 루나가 물었다. “자바 코드에서는 User 객체 백 개가 모두 똑같은 타입이라서 구별이 안 될 거라고 걱정했는데, 여기서는 아주 쉽게 구별하고 있네. 왜일까?”

솔라는 스프레드시트 화면을 뚫어지라 쳐다보았다. “그야… 여기서는 행 번호가 있으니까. 2번 행, 3번 행, 4번 행… 눈에 보이지 않는 순서가 있잖아.”

“맞아. 그리고 그 순서는 데이터를 추가하거나 삭제하면 계속 바뀌지. 믿을 만한 기준이 못 돼.” 루나는 말을 이으며, 솔라가 방금 삭제했던 데이터를 되살렸다. 그리고 A열의 이름을 ‘ID’로 바꾸었다.

“우리에겐 절대 변하지 않는, 각 줄을 고유하게 만들어 줄 무언가가 필요해. 주민등록번호처럼.”

루나는 첫 번째 데이터 옆 ID 셀에 ‘1’을 입력했다. 솔라는 즉시 의도를 알아채고 두 번째와 세 번째 데이터 옆에 각각 ‘2’와 ‘3’을 입력했다. 이제 테이블은 훨씬 안정적으로 보였다.

ABCD
ID이름이메일
1솔라sola@example.com
2루나luna@example.com
3솔라sola.dev@example.com

“이거네!” 솔라가 말했다. “이렇게 각 줄마다 고유한 번호만 있으면, 이름이 같아도 얼마든지 구별할 수 있잖아. DB가 알아서 이런 번호를 붙여주겠지. 그럼 우린 신경 쓸 필요 없는 거 아니야?”

솔라의 생각은 그럴듯했다. 데이터베이스가 자동으로 처리해주는 일이라면, 굳이 자바 코드에서까지 신경 쓸 필요는 없어 보였다.

그때 루나가 솔라의 UserB 클래스 코드 창을 다시 앞으로 가져왔다.

@Entity
public class User {
    private String name;
    private String email;

    // ...
}

“JPA에게 이 클래스는 ‘영속 객체’라고 출생신고까지 마쳤어. 그런데 이 설계도 어디에도 우리가 방금 만든 ‘ID’ 열에 대한 정보는 없어.” 루나는 코드에 새로운 필드 하나를 추가했다.

@Entity
public class User {

    private Long id; // 추가된 부분

    private String name;
    private String email;

    // ...
}

“일단 ID를 담을 변수는 만들었어. 하지만 이걸로 끝일까? JPA라는 ‘숨겨진 손’은 이 id 필드가 우리가 엑셀에서 그토록 중요하게 여겼던 바로 그 ‘고유 식별자’라는 사실을 어떻게 알까?”

루나는 private Long id; 바로 윗줄에 새로운 애너테이션을 추가했다.

@Id

“아.” 솔라의 입에서 작은 소리가 새어 나왔다. @Entity가 클래스 전체의 운명을 결정했듯이, @Id는 이 id 필드에 특별한 임무를 부여하는 표식이었다. 수많은 필드 중에서 바로 이 필드가 데이터베이스 테이블의 모든 행을 유일하게 구별하는 값, 즉 기본 키(Primary Key)임을 선언하는 것이었다.

“그리고 더 중요한 게 있어.” 루나는 말을 멈추지 않았다. “네가 ‘DB가 알아서 해주겠지’라고 했지? 맞아. 하지만 동시에 여러 사람이 데이터를 추가하려고 하면 어떻게 될까? 둘 다 자기가 ‘4번’이라고 생각하고 ID를 넣으려고 하면 충돌이 날 거야. 그래서 우리는 JPA에게 ID를 ‘어떻게’ 만들어낼 건지도 알려줘야 해.”

루나는 @Id 바로 아래에 또 다른 애너테이션을 추가했다.

@GeneratedValue(strategy = GenerationType.IDENTITY)

“이건 JPA에게 하는 위임장이야. ‘이 id 값은 내가 직접 정하지 않을 테니, 네가 데이터베이스의 방식을 따라서 알아서, 그리고 안전하게, 중복되지 않는 값으로 채워 넣어줘.’ 라고 구체적으로 지시하는 거지.”

솔라는 비로소 모든 조각이 맞춰지는 것을 느꼈다. 데이터베이스가 PK를 자동으로 생성해주니 자바 코드에서는 신경 끌 수 있다는 생각은 순진한 착각이었다. 자바 객체와 데이터베이스 테이블이라는 두 세계를 연결하는 JPA에게, 어느 필드가 그 ‘연결고리’의 핵심인지를 알려주는 @Id, 그리고 그 고유한 값을 어떻게 생성할 것인지 전략을 명시하는 @GeneratedValue가 모두 필요했다. 이것은 방치가 아니라, 정교한 ‘위임’이었다.

‘JPA Entity는 DB 테이블과 연결되는 영속 객체다.’

이 문장은 이제 솔라에게 전혀 다른 의미로 다가왔다. 영속 객체는 그저 데이터가 저장된 존재가 아니었다. @Id라는 명확한 신분증을 가지고, 데이터베이스 세상 속에서 자신의 고유한 위치를 보장받는 존재였다.

“알겠어. 이제 모든 User 객체는 고유한 ID를 가지게 됐어. 이걸로 JPA가 헷갈릴 일은 없겠네.” 솔라는 만족스러운 표정을 지었다. 그러다 문득, 스프레드시트의 C열 ‘이름’과 자바 클래스의 name 필드를 번갈아 보았다.

“그런데 언니, 만약에… 데이터베이스 테이블에는 user_name이라고 되어 있고, 내 클래스에는 그냥 name이라고 되어 있으면 어떡해? ID처럼 이름이 다르잖아. 이것도 JPA가 알아서 연결해 주나?“

4장: @Column: 객체 필드를 DB 컬럼에 맞춤 매핑하기

솔라의 질문이 끝나자마자, 루나는 대답 대신 스프레드시트 창을 다시 활성화했다. 이전 장에서 만들었던, ID와 이름, 이메일이 적힌 작은 표였다. 루나의 마우스 커서가 ‘이름’이라고 적힌 C열의 헤더를 망설임 없이 클릭했다.

루나는 ‘이름’이라는 단어를 지우고, 그 자리에 user_name이라고 새로 입력했다. 화면에는 이제 미묘한 불일치가 발생했다. 데이터베이스를 흉내 낸 스프레드시트의 열 이름은 user_name인데, 화면 한쪽에 보이는 User 클래스의 필드 이름은 여전히 name이었다. 두 세계의 약속이 어긋난 순간이었다.

ABCD
IDuser_name이메일
1솔라sola@example.com
2루나luna@example.com
3솔라sola.dev@example.com

솔라는 그 변화를 조용히 지켜보고 있었다. 방금 자신이 던졌던 질문이 눈앞에 구체적인 형태로 나타난 셈이었다.

“언니 말대로라면, 내 자바 클래스 필드 이름은 name인데, 만약 데이터베이스 테이블의 컬럼 이름은 user_name이라면 어떡해? ID처럼 이름이 서로 다른데. 이것도 JPA가 알아서 똑똑하게 연결해 주나?”

솔라의 목소리에는 일말의 기대가 섞여 있었다. @Entity@Id의 마법을 경험한 터라, JPA라면 이 정도의 이름 차이쯤은 관례(snake_case와 camelCase)를 파악해서 자동으로 해결해 줄지도 모른다고 생각한 것이다.

루나는 고개를 저으며 User 클래스 코드의 name 필드 위로 커서를 옮겼다.

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;

    // ...
}

“JPA는 똑똑하지만, 독심술사는 아니야.” 루나는 그렇게 말하며, private String name; 바로 위에 새로운 애너테이션을 한 줄 추가했다.

@Column(name = "user_name")

솔라의 눈이 커졌다. 너무나 명확하고 직접적인 지시였다. ‘이 name 필드는 데이터베이스의 user_name 컬럼과 연결하라.’ JPA의 추측에 기댈 필요 없이, 개발자가 직접 그 연결고리를 지정해주는 것이었다.

“아… 이것도 그냥 약속이구나. 이름이 다를 땐 다르다고 명시해주는 거.”

솔라는 @Column 애너테이션이 그저 이름이 다를 때를 위한 예비책 정도로 생각했다. 이름만 같다면 굳이 쓸 필요가 없는, 선택적인 장식이라고.

바로 그때, 루나는 솔라의 노트를 가져와 펜으로 간단한 스케치를 시작했다. 데이터베이스 테이블의 컬럼을 표현하는 두 개의 길쭉한 상자였다.

첫 번째 상자 위에 VARCHAR(10)이라고 적고, 두 번째 상자 위에는 TEXT라고 적었다.

User 객체에 ‘자기소개’ 필드를 추가했다고 가정해 보자. 자바 코드에서는 그냥 String 타입으로 만들었어. 그런데 데이터베이스 담당자가 한 명은 글자 수를 10자로 제한하고(VARCHAR(10)), 다른 한 명은 길이 제한 없이(TEXT) 만들었다면 어떻게 될까?”

루나는 솔라에게 펜을 건넸다. “여기에 ‘안녕하세요, 밝고 긍정적인 개발자 솔라입니다.’라는 문장을 넣어 봐.”

솔라는 펜을 들고 첫 번째 VARCHAR(10) 상자에 문장을 써 내려가기 시작했다. ‘안녕하세요, 밝’까지 쓰자 상자가 꽉 찼다. 더 이상 쓸 공간이 없었다. 반면, TEXT 상자에는 전체 문장을 여유롭게 다 적을 수 있었다.

“잘리겠네.” 솔라가 짧게 말했다. “혹은 에러가 나거나. 분명히 내 자바 객체에는 긴 문장을 잘 담았는데, 막상 데이터베이스에 저장하려고 하니 데이터가 유실되는 거야.”

그제야 솔라는 자신이 놓치고 있던 것을 깨달았다. 객체와 테이블의 연결은 단순히 이름만 맞춘다고 끝나는 것이 아니었다. 이름은 수많은 속성 중 하나일 뿐이었다. 문자열의 최대 길이, null 값 허용 여부, 유니크 제약 조건 등, 데이터베이스 컬럼이 가진 세부적인 규칙들을 자바 객체는 알지 못했다. 이 둘 사이의 미세한 균열이 데이터의 유실이나 오류를 낳을 수 있었다.

루나는 다시 User 클래스 코드를 가리켰다. 그리고 @Column 애너테이션을 수정했다.

@Column(name = "user_name", length = 10, nullable = false)
private String name;

@Column은 단순히 이름만 지정하는 표식이 아니야. 컬럼의 세부 명세서에 가깝지. 길이는 10자로 제한하고(length = 10), 절대 비어 있으면 안 된다(nullable = false)는 규칙까지 JPA에게 알려주는 거야. 이렇게 해야 JPA가 데이터베이스 테이블을 생성할 때 이 규칙을 반영해주거나, 데이터를 저장하기 전에 유효성을 검사할 수 있어.”

‘JPA Entity는 DB 테이블과 연결되는 영속 객체다.’

이 문장은 이제 솔라에게 훨씬 더 무거운 의미로 다가왔다. 영속 객체가 된다는 것은, 그저 데이터베이스에 저장된다는 뜻이 아니었다. 객체의 각 필드가 데이터베이스 컬럼의 어떤 제약 조건과 규칙 아래에 놓일 것인지 세밀하게 정의하고, 그 약속을 지키는 ‘책임’까지 포함하는 것이었다. @Column은 그 책임을 수행하기 위한 정교한 도구였다.

솔라는 자신이 완성한 User 클래스 코드를 쭉 훑어보았다. @Entity, @Id, @GeneratedValue, 그리고 이제 @Column까지. 객체의 영속적인 삶을 위한 출생신고서, 신분증, 그리고 상세 신상 명세서까지 모두 갖춰진 셈이었다.

“좋아. 이제 이 설계도만 있으면 JPA가 데이터베이스에 User 테이블을 완벽하게 만들고, 내 User 객체를 정확하게 저장하고, 또 꺼내 올 수도 있겠네.”

솔라는 자신감에 차서 말했다. 그러다 문득, 고개를 갸웃거렸다.

“그런데… 내가 객체를 만들 땐 new User(...) 하고 생성자를 쓰잖아. 그럼 JPA가 데이터베이스에서 데이터를 다시 읽어와서 User 객체로 만들 땐 어떻게 하는 거지? 걔도 생성자를 쓰나? 어떤 생성자를 어떻게 알고 쓰는 거지?”

솔라의 머릿속에 새로운 의문이 떠올랐다. JPA라는 ‘숨겨진 손’이 데이터베이스에서 가져온 이름, 이메일 같은 데이터 조각들을 어떤 방법으로 온전한 하나의 ‘객체’로 조립해내는지, 그 과정이 전혀 상상되지 않았다.

5장: 기본 생성자: JPA가 객체를 ‘숨겨진 손’으로 만드는 방법

솔라는 완성된 User 클래스를 보며 만족스러운 미소를 지었다. 출생신고서(@Entity), 고유한 신분증(@Id@GeneratedValue), 그리고 상세 신상 명세서(@Column)까지. 이 정도면 객체의 영속적인 삶을 보장하기 위한 서류 작업은 완벽해 보였다.

스스로의 코드에 자신감이 붙은 솔라는, 클래스를 좀 더 ‘개발자 친화적으로’ 다듬기로 했다. 객체를 생성할 때마다 setter를 여러 번 호출하는 것이 번거롭게 느껴졌기 때문이다. 그녀는 User 클래스에 이름과 이메일을 인자로 받는 생성자를 추가했다.

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "user_name", length = 10, nullable = false)
    private String name;

    private String email;

    // 새로 추가한 생성자
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    // ... getter, setter
}

이제 new User("솔라", "sola@example.com") 한 줄이면 객체 생성이 끝났다. 솔라는 깔끔해진 코드에 만족했다. 자바에서는 개발자가 생성자를 하나라도 만들면, 컴파일러가 자동으로 만들어주던 매개변수 없는 기본 생성자가 사라진다. 솔라는 어차피 쓰지도 않는 생성자이니, 없는 편이 더 낫다고 생각했다.

그 모습을 조용히 지켜보던 루나가 말했다. “좋아. 그 객체를 데이터베이스에 저장했어. 그리고 일주일이 지났다고 상상해 봐. 이제 데이터베이스에서 ID가 1번인 사용자의 정보를 다시 가져와서 User 객체로 만들어야 해.”

루나는 책상 위 레고 블록 상자에서 가장 기본적이고 네모난 2x4 브릭 하나와, 자동차 바퀴, 운전대, 창문처럼 생긴 특수 브릭들을 꺼내 솔라 앞에 흩어 놓았다.

“JPA라는 ‘숨겨진 손’이 저기 흩어진 데이터 조각들(ID 1, 이름 ‘솔라’, 이메일 ‘sola@example.com’)을 가지고 User 객체를 조립해야 해. 조립의 첫 단계는 뭘까?”

솔라는 자신 있게 답했다. “당연히 new User(...)를 호출해서 객체를 만드는 거지.”

“그 (...) 안에는 뭘 넣어야 하지?” 루나가 되물었다.

“음… 이름이랑 이메일?” 솔라는 말을 뱉고 나서야 자신의 대답이 어딘가 이상하다는 것을 깨달았다. “아니, 잠깐. JPA는 아직 데이터베이스에서 어떤 값을 읽어올지 모르잖아. 일단 객체부터 만들어야 그 안에 값을 채워 넣을 텐데…”

솔라의 생각은 꼬리를 물었다. 자신이 만든 User(String name, String email) 생성자를 사용하려면, 객체를 만드는 그 순간에 이미 nameemail 값을 알고 있어야만 한다. 하지만 JPA는 보통 일단 텅 빈 객체를 먼저 만들고, 그 후에 데이터베이스에서 읽어온 값들을 하나씩 채워 넣는 방식으로 동작한다. 순서가 맞지 않았다.

“네가 만든 생성자는 ‘자동차를 만들려면, 만드는 동시에 운전대와 바퀴를 꼭 끼워 넣어야 해!’라고 요구하는 것과 같아.” 루나가 레고 블록을 가리키며 말했다. “하지만 JPA라는 조립공은 훨씬 더 단순한 방식으로 일하고 싶어 해. 일단 어떤 부품이든 상관없이 조립을 시작할 수 있는 가장 기본적인 몸체, 즉 ‘빈 껍데기’를 먼저 만드는 거지.”

루나는 평범한 2x4 브릭을 집어 들었다. “조립공은 먼저 이 기본 브릭으로 뼈대를 만들어. 그런 다음에야 데이터베이스에서 가져온 ‘운전대’ 블록과 ‘바퀴’ 블록을 제자리에 끼워 맞추는 거야. 이게 수백, 수천 개의 각기 다른 종류의 객체를 일관된 방식으로 조립할 수 있는 비결이지.”

솔라는 그제야 자신이 저지른 실수를 깨달았다. 자신이 만든 생성자는 개발자인 ‘나’에게는 편리했지만, 프레임워크인 ‘JPA’에게는 당혹스러운 요구사항이었다. JPA는 어떤 복잡한 엔티티 클래스든 안정적으로 객체화하기 위해, 아무런 조건 없이 호출할 수 있는 공통의 진입점이 필요했다. 그것이 바로 매개변수 없는 ‘기본 생성자’였다.

“내가 쓰지 않는다고 해서, 필요 없는 게 아니었구나…” 솔라가 중얼거렸다. “이 기본 생성자는 나를 위한 게 아니라, JPA를 위한 거였어. JPA가 내 객체의 삶을 관리해주는 대신, 내가 JPA에게 제공해야 하는 최소한의 약속, 일종의 ‘비밀 통로’ 같은 거였네.”

솔라는 노트북으로 돌아가 User 클래스에 코드를 추가했다. 개발자인 자신이 실수로 사용하지는 못하게 접근 제어자를 protected로 바꾸고, 매개변수 없는 기본 생성자를 명시적으로 만들어주었다.

@Entity
public class User {

    // ... 필드들

    // JPA를 위한 기본 생성자
    protected User() {
    }

    // 개발자를 위한 생성자
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    // ...
}

‘JPA Entity는 DB 테이블과 연결되는 영속 객체다.’

마지막 퍼즐 조각이 맞춰지자, 솔라의 머릿속에서 흐릿했던 문장이 비로소 완벽한 그림으로 완성되었다. 객체의 영속적인 삶은 마법이 아니었다. 그것은 개발자와 프레임워크 사이의 정교한 계약이었다.

솔라는 자신이 만든 User 클래스의 전체 코드를 천천히 훑어 내려갔다.

@Entity: “JPA, 이 클래스는 당신이 관리해야 할 ‘영속 객체’입니다.” @Id, @GeneratedValue: “이 필드가 고유한 신분증(PK)이고, 값은 데이터베이스의 규칙에 따라 자동으로 만들어주세요.” @Column: “이 필드는 테이블의 이 컬럼에 연결되고, 이런 세부 규칙을 따릅니다.” protected User(): “그리고 당신이 데이터베이스에서 객체를 다시 만들어낼 때, 이 통로를 사용해주세요.”

솔라는 이제 더 이상 루나에게 질문을 던지지 않았다. 대신, 그녀는 스스로에게 설명하기 시작했다. 객체가 태어나고, 데이터베이스에 기록되고, 다시 현실 세계의 객체로 부활하는 전 과정. 그 영속성의 연결고리를 만드는 ‘숨겨진 손’의 모든 움직임이 선명하게 보였다. 그녀는 이제 새로운 도메인이 주어지더라도, 그에 맞는 영속 객체를 자신 있게 설계하고 각 요소의 필요성을 당당하게 설명할 수 있을 터였다. 모든 조각이 제자리를 찾은 완벽한 설계도가 그녀의 머릿속에 완성된 순간이었다.