JPA Basic 1. DDL과 @Entity

zdpk·2024년 3월 12일

JPA Basic

목록 보기
1/11
post-thumbnail

관계형 데이터베이스는 과거부터 현재까지 오랫동안 매우 널리 사용되어 그 안정성이 입증 되었으며, 서비스를 개발함에 있어서 필수불가결한 존재로 남아있다.

NoSQL, NewSQL 등 다양한 대안이 나오고 있지만, 현재로서는 RDB를 대체할 정도의 이점을 보여주지 못했으며, 보여준다고 하더라도 수많은 서비스에서 잘 사용되고 있는 RDB를 대체하려면 아주 오랜 시간이 걸릴 것이다.
예전엔 'NoSQL이 RDB를 대체할 것이다'라는 이야기가 있었지만, 지금은 보기 힘들고, 사실상 대체가 아닌 보완적인 관계 정도로 생각하면 된다.

고로 무엇을 개발하던 반드시 알아야 하는 존재라고 생각하면 편하다.

Jpa를 공부해보면 다양한 Annotation들이 나오고, Persistence Context에 의한 캐싱이 이루어지며, 로딩 전략에 따라 SQL이 나가는 시점이 달라지는 등, 처음 접한 입장에서 헷갈리기 쉽다.

이번 Jpa 시리즈에서는 매우 기초적인 SQL부터 각종 Annotation과 DB의 상호작용에 대해서 핵심 위주로 정리해보려고 한다.

  • 연관 관계
  • 로딩 전략
  • 페치 조인
  • Lazy Load 최적화

이미 Jpa를 봤던 사람들은 잘 알 수도 있는 내용이지만, 위 내용 중 헷갈리는 부분이 있다면 글을 읽어보는 것도 나쁘지 않을 것 같다.


Project Setting

프로젝트는 다음과 같이 설정했다.

lombok을 사용하여 보일러플레이트를 제거했으며, DB는 편의를 위해 h2를 사용하였다.

그 외에 Spring, Jpa도 추가했다.

// build.gradle
plugins {  
    id 'java'  
    id 'org.springframework.boot' version '3.2.3'  
    id 'io.spring.dependency-management' version '1.1.4'  
}  
  
group = 'x'  
version = '0.0.1-SNAPSHOT'  
  
java {  
    sourceCompatibility = '17'  
}  
  
configurations {  
    compileOnly {  
        extendsFrom annotationProcessor  
    }  
}  
  
repositories {  
    mavenCentral()  
}  
  
dependencies {  
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'  
    implementation 'org.springframework.boot:spring-boot-starter-web'  
    compileOnly 'org.projectlombok:lombok'  
    runtimeOnly 'com.h2database:h2'  
    annotationProcessor 'org.projectlombok:lombok'  
    testImplementation 'org.springframework.boot:spring-boot-starter-test'  
}  
  
tasks.named('test') {  
    useJUnitPlatform()  
}

application.yaml은 다음과 같이 설정했다.

# application.yaml
spring:  
  datasource:  
    # 메모리 상에서 h2 실행
    url: jdbc:h2:mem:db  
    username: sa
    password:  
    driver-class-name: org.h2.Driver  
  jpa:  
    hibernate:  
      # 매 번 @Entity가 붙은 테이블 자동으로 생성
      ddl-auto: create  
      
    # sql console 출력
    show-sql: true  
    properties:  
      hibernate:  
        # 가독성을 위해 sql 출력 시 포매팅
        format_sql: true  

DDL

Jpa를 검색해서 이 글을 읽고 있다는 것은, 기본적인 SQL을 알고 있을 가능성이 높다고 생각한다.

그럼에도 불구하고 잘 모르는 사람들을 위해 Jpa의 Entity에 대해 알아보기 전에, DDL, Data Definition Language에 대해서 간단하게 정리하는 시간을 가져보겠다.

많은 서적을 보면 SQL은 CREATE TABLE과 같이 대문자로 표기하는 경향이 있는데, 대소문자 구분이 없기 때문에 소문자로 작성해도 문제가 되지 않는다.(Keyword 이야기이며, Table, Column 등은 대소문자 구분)

SQL 자체가 매우 오래 되었기 때문에 Code Highlight 등이 되지 않는 환경을 고려하여 Keyword를 대문자로 표기하던 과거의 유산이 아직까지 이어지고 있는 것이 아닌가 생각한다.

최근 접한 서적 외의 예시에서는 키워드를 소문자로 표기하는 경우도 많았고, 현재 작성하는 이 글을 어떤 플랫폼에 올리더라도 SQL Highlight를 잘 해줄거라 믿기 때문에 모든 SQL을 소문자로 작성하겠다.


create table

create table user (
    id bigint primary key,
    name varchar(30),
    age int
);

user 테이블 작성 시, 위와 같이 create table DDL을 통해 테이블을 만들면 다음과 같은 모습이 될 것이다.

|     id |     name |     age |

DB의 테이블은 마치 Java의 Class와도 비슷하다.

public class User {
    long id;
    String name;
    int age;
}

테이블은 Record를 만들어내기 위한 틀,
Class는 instance를 만들어내기 위한 틀

그렇다면 Record는 instance와 비슷하다고 볼 수 있을 것이다.

이 점을 이용하여 Jpa는 Java로 작성된 코드만 보고, DDL을 자동으로 생성 및 호출하여 테이블을 만들어준다.


@Entity

이를 위해서는 어떤 Class가 테이블을 만드는 데 사용되어야 하는지 Jpa에게 알려줘야 한다.

모든 Class가 테이블이 된다면 문제가 될 것이기 때문이다.

익히 알고 있는 Controller, Service, Repository도 전부 테이블이 된다고 생각해보면 이해가 될 것이다.

알려주는 방법은 간단하다.

@Entity Annotation을 붙여주면 된다.

// User.java
@Entity
@Getter
public class User {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int age;
}

Jpa는 @Entity Annotation이 붙은 Class를 찾아서 Application이 실행될 때, Table로 만들어준다.

이를 위해서는 약간의 설정이 필요하다.

사실 이미 설정을 완료했다.

앞서 살펴본 application.yaml의 다음 부분이다.

# application.yaml
jpa:  
    hibernate:  
      # 매 번 @Entity가 붙은 테이블 자동으로 생성
      ddl-auto: create  

사실 application.yaml을 비워도 자동으로 설정이 들어가지만, 설정 내용이 보이지 않으면 헷갈리고, 버전에 따라 바뀔 수도 있는 부분이기 때문에 명시적으로 작성하는 것이 더 좋다고 생각한다.


@Column

대표적인 DDL이 테이블 생성 구문이었고, @Entity Annotation을 통해 Application 실행 시, 자동으로 Class를 테이블로 생성할 수 있다는 것을 알았다.

그런데 다음과 같이 user 테이블의 nameunique로 지정해 주려면 Jpa에서는 어떻게 해야 할까.

create table user (
    id bigint primary key,
    name varchar(30) unique,
    age int
);

간단하다.

@Entity로 지정된 클래스의 각 필드에 @Column Annotation을 붙여주고, 그 안에 원하는 속성을 명시하면 된다.

// User.java
@Entity
@Getter
public class User {
    @Id
    @GeneratedValue
    private Long id;
    @Column(unique = false)
    private String name;
    private int age;
}

즉, @Column은 Application 실행 시, 테이블 생성할 때 호출될 DDL의 옵션을 설정하는데 사용할 수 있다.

이 외에도 다음과 같이 name이라는 속성을 xxx로 주면

@Column(name = "xxx")
private String name;

클래스 상에서의 필드명은 name이지만, 테이블 Column 명은 xxx이 된다.

create table user (
    id bigint primary key,
    xxx varchar(30) unique,
    age int
);

nullablefalse로 설정하면,

@Column(nullable = false)
private String name;

테이블 생성 시, name Column에 not null이 붙는다.

create table user (
    id bigint primary key,
    name varchar(30) not null,
    age int
);

이 외에도 다양한 옵션이 있지만, 중요한 것은 @Entity@Column은 DDL에 영향을 준다는 것이다.

Java에서 Class와 Table이 유사한 역할을 하기 때문에 Class에 지정한 옵션이 Table 생성 시에 반영된다고 생각하면 좀 더 이해하기 수월할 것이다.


Jpa 오류 분석

이제 Application의 main 함수를 실행시켜보자.

그럼 다음과 같은 오류가 발생할 것이다.

drop table if exists user cascade " via JDBC [Syntax error in SQL statement "\000a    drop table if exists [*]user cascade "; expected "identifier";]

원인을 분석해보자.

ddl-autocreate로 설정했기 때문에 Application 실행 시, 자동으로 @Entity Annotation이 붙은 클래스에 대한 create table 구문을 만들어서 DB에 DDL을 내보낼 것이다.

오류에서 drop table구문이 보이는 것은 ddl-auto: create로 설정 시, 먼저 모든 테이블이 drop되고, 그 뒤에 create가 호출되기 때문이다.

여기까지 알더라도 Spring의 설정이 워낙 방대하기 때문에 원인을 파악하기 쉽지 않을 수 있다.

Jpa는 오류 메세지가 방대하고, 상황을 정확히 설명하지 않는 경우가 많은 것 같다.

오류 메세지를 쭉 내리다 보면 다음과 같은 구문이 보일 것이다.

Syntax error in SQL statement "\000a    create table [*]user (\000a  

drop이 아닌 create table을 하는 도중에 Syntax error가 발생했다는 의미다.

만약 DB를 조금 공부했다면 이 메세지를 보고 문제가 무엇인지 파악할 수도 있다.

우리가 만든 첫 번째 Table인 user가 DB 내부적으로 사용되는 Table과 이름이 충돌하여 발생한 것이라는 사실을 말이다.

그러나 우리가 사용하고 있는 것은 Jpa이기 때문에 Jpa에서는 어떻게 문제를 해결해야 할지는 또 다른 문제가 된다.

간단하게는 클래스명을 User가 아닌 UserTable 등의 충돌하지 않는 이름으로 바꾸는 방법이 있겠다.

그러나 좋은 방법은 아니라고 생각된다.

Spring은 DB와 독립적이기 때문에 이름이 DB 의존적인 것은 좋지 않다.

게다가 User가 반드시 RDB에 저장될 것이라는 보장이 없다.

나중에 User를 MongoDB 등으로 이전한다면 그 때는 Table이 아닌 Document라는 명칭이 된다.

Table이 1000개라면, 1000개의 클래스를 추적하여 변경할 필요가 있다.

게다가 Redis를 사용하여 캐싱을 한다면, User는 RDB에도 존재하고 Redis에도 존재할 수 있다.

그럴 경우에는 어떻게 표현할 것인가.

Java Application에서의 User는 그대로 표현하면서, DB에서 이름 충돌을 해결할 수 있는 방법을 떠올려보자.

create table "user" (
    id bigint primary key,
    name varchar(30),
    age int
);

PostgreSQL에서는 user""을 붙여서 "user"로 테이블명을 표현하면 이름 충돌을 해결할 수 있다.

create table `user` (
    id bigint primary key,
    name varchar(30),
    age int
);

MySQL에서는 위와 같이 표기할 수 있다.

Jpa에서도 이를 해결하기 위한 방법이 몇 개 있는데, 위와 같이 테이블명을 ""에 감싸주는 식으로 해결해보겠다.

@Entity
@Getter
@Table(name = "\"user\"")
public class User {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int age;
}

위와 같이 @Table Annotation에 name 속성을 넣어주면 된다.

"을 escape 처리 해주는 것을 잊지 말자.

이제 다시 Applicaiton을 실행하면, create table user가 아닌, create table "user"이 출력될 것이다.

클래스명은 User인 채로 유지하면서, 이름 충돌까지 피할 수 있다.

Jpa에 익숙한 사람은 간단한 문제라고 생각할 수도 있다.

그러나 처음 보는 사람에게는 오류 메세지만 보고 해결하기 어려울 수도 있고, DB, Jpa 모두를 조금은 알아야 해결할 수 있기 때문에 누군가에게는 매우 고통스러운 문제가 될 수도 있는 일이다.

앞으로 Jpa를 사용하면서 이러한 문제들을 많이 겪게 될 것이다.

DB와 Jpa를 모두 이해해야 문제를 해결할 수 있는 경우들이 많다는 의미이다.

그러니 DB, Jpa를 병행해서 학습할 것을 추천한다.

다음 장에서는 Persistence Context에 대해서 알아보겠다.

0개의 댓글