본문 바로가기
Back-end/java spring

[JPA] 엔티티 매핑

by kkkdh 2023. 1. 27.
728x90

객체와 테이블 매핑

객체와 DB의 테이블을 어떻게 매핑하는지부터 정리해 보자.

 

매핑을 하는 방법 자체는 매우 간단하다.

  • 객체와 테이블 매핑: @Entity, @Table
  • 필드와 컬럼 매핑: @Column
  • 기본 키 매핑: @Id
  • 연관관계 매핑: @ManyToOne, @JoinColumn

먼저 객체와 테이블을 매핑하는 방법부터 알아보자.

 

@Entity

  • @Entity가 붙은 클래스는 JPA가 관리하며, 엔티티라 한다.
  •  JPA를 사용해서 테이블과 매핑할 클래스는 @Entity가 필수로 붙어야 한다.
  • 주의
    • 기본 생성자 필수(파라미터가 없는 public 또는 protected type의 생성자)
    • final 클래스, enum, interface, inner 클래스에는 사용할 수 없다.
    • 저장할 필드에 final keyword 사용 X

@Entity name 속성

  • JPA에서 사용할 엔티티의 이름을 지정하기 위해 사용하는 속성이다.
  • 기본값: 클래스의 이름을 그대로 사용
  • 같은 클래스 이름이 없으면, 가급적으로 기본값을 사용한다.

@Table

  • name: 매핑할 테이블의 이름 (기본값으로 엔티티의 이름을 사용한다)
  • catalog: 데이터베이스 catalog 매핑
  • schema: 데이터베이스 schema 매핑
  • uniqueConstraints(DDL): DDL 생성 시에 유니크 제약 조건을 생성한다.

데이터베이스 스키마 자동 생성

Entity만 보면, 어떤 속성들을 갖춘 테이블이 생성되어야 할지 알 수 있기 때문에, JPA에서는 데이터베이스 스키마를 자동 생성하는 기능을 제공한다

 

개발 단계에서 로컬 환경에서 애플리케이션을 테스트하는데 도움이 되는 기능

  • DDL(create문)을 애플리케이션 실행 시점에 자동으로 생성한다.
  • 테이블 중심 → 객체 중심
  • 데이터베이스 방언(dialect)을 활용해서 데이터베이스에 맞는 적절한 DDL을 JPA가 알아서 생성해 준다.
  • 이렇게 생성된 DDL은 반드시 개발 장비에서만 사용해야 한다.
  • 생성된 DDL은 운영 서버에서는 사용하지 않거나, 필요한 경우 적절히 다듬은 후에 사용해야만 한다!

value에 작성되는 값에 따른 차이점
이렇게 create로 value를 변경해 준다.

예시 코드를 다시 실행해 보면,

아까와 다르게 hibernate에 의해 미리 생성된 테이블이 존재하는 경우 drop 한 뒤에 새로운 테이블을 생성하는 create문이 애플리케이션의 실행과 함께 실행됨을 확인할 수 있다.

 

이제 엔티티 설계만 잘해놓으면, DB에서 sql문을 계속 활용해 table을 개발 과정에서 바꿔줄 필요가 없게 된다.

이렇게 새로운 속성을 entity에서 추가하고, 이번에는 value를 update로 바꿔보자

update로 value를 변경하는 경우 추가하는 변경 사항에 대해서 alter table query를 만들어 실행해 준다.

지우는 건 잘못하다 table column 날리는 순간 큰일이나기 때문이라고 한다.

 

데이터베이스 스키마 자동 생성 시 주의할 점!!

  • 운영 장비에는 절대 create, create-drop, update를 사용하면 안 된다.
  • 개발 초기 단계에는 value를 create, update
  • 테스트 서버는 update 또는 validate (개발이나 테스트 서버는 이 정도를 권장한다고 함..)
  • 스테이징과 운영 서버는 validate 또는 none (그냥 사용하지 말아라)

개발이나 테스트 서버에서는 validate 까지는 괜찮은데, 운영에서는 절대 쓰지 않을 것을 권장

시스템이 잠깐만 멈춰도 굉장히 큰 문제가 되기 때문이다.

 

DDL 생성 기능

  • 제약 조건 추가 예시: 회원 이름은 필수, 10자를 초과 X
    • @Column(nullable = false, length = 10), 이렇게 위의 제약 조건을 특정 칼럼에 추가해 줄 수 있다.
  • unique constraint 추가
    • @Table(uniqueConstraints = {@UniqueConstraint(name = "NAME_AGE_UNIQUE", columnNames={"NAME", "AGE})})
  • DDL 생성 기능은 DDL을 자동 생성할 때만 사용되고, JPA의 실행 로직에는 영향을 주지 않는다. (런타임 환경에 영향을 주지 않는다.)

DDL 생성 기능은 애플리케이션에 영향을 주지 않는다. 데이터베이스에 영향을 줄 뿐이다. 

어노테이션에 작성된 속성 값을 보고, 애플리케이션의 런타임 환경에 영향을 주진 않는다. (DB에 생성해 주는 것만 도와줄 뿐이다.)


필드와 컬럼 매핑

필드와 컬럼을 매핑하는 법을 공부하기 위해 다음의 요구 사항을 추가로 구현해 보자.

  1. 회원은 일반 회원과 관리자로 구분해야 한다.
  2. 회원 가입일과 수정일이 있어야 한다.
  3. 회원을 설명할 수 있는 필드가 있어야 한다. 이 필드는 길이 제한이 없다.

Member class 코드를 위와 같이 바꿨습니다.

@Column 어노테이션의 name 속성의 value를 변경해서 table의 column 이름을 다른 식으로 부여할 수도 있고, age 속성 같이 다른 type으로 필드를 선언해도 가장 적절한 data type을 찾아서 매핑해 준다.

 

@Enumerated(EnumType.STRING)
private RoleType roleType;

위와 같이 Enumerated annotation을 사용해서 Enum type 필드를 선언할 수도 있다.

세 가지의 타입이 DB에는 보통 존재
그 중에서 TIMESTAMP로 매핑해준다.

보통 자바의 Date type 안에는 날짜+시간이 다 들어가 있는데, 보통의 DB는 (날짜, 시간, 날짜+시간) 형태로 type이 나뉜다.

 

예시에서는 TIMESTAMP로 타입을 매핑함을 명시해 주고 있다.

마지막으로 varchar를 넘어서는 큰 문자열을 넣고 싶은 경우 @Lob 어노테이션을 작성하면 된다.

코드 실행 결과 만들어지는 create query

코드를 실행하면, 위와 같은 create query를 JPA가 만들어 DB로 전송하게 된다.

Trasient 어노테이션

만약 Entity 객체에 DB와 연관 없이 사용하고 싶은 필드를 선언하고 싶은 경우에는 @Transient 어노테이션을 붙여주면 된다.

 

@Column

속성에 따라 부여할 수 있는 column의 constraints

위 표의 내용과 같이 속성을 적절하게 사용해서 column에 제약 조건들을 부여할 수 있다.

unique를 사용하지 않는 이유: 제약조건 명을 임의로 만들어 내는데, 실무에서는 적합하지 못하다.

그래서 unique 제약 조건을 부여하고 싶은 경우, @Table 어노테이션의 속성을 이용해서 부여한다고 한다. (여기서는 제약 조건의 이름까지 부여할 수 있다.)

 

@Enumerated

Enum 타입을 매핑할 때 사용한다.

  • value 속성의 값에 따른 차이점
    • EnumType.ORDINAL: enum 순서를 데이터베이스에 저장
    • EnumType.STRING: enum 이름을 데이터베이스에 저장
  • 기본값이 EnumType.ORDINAL
  • 주의!! EnumType.ORDINAL 사용 X

ORDINAL로 값을 지정해보자.
ORDINAL로 지정하면, Enum의 원래 문자열이 아닌 정수로 작성된다.
이렇게 추가해 보자.

그런데, 만약에 Enum 타입을 사용할 때, 그 안에 포함된 값이 다음과 같이 늘어난다면 어떻게 될까??

GUEST가 추가되었다.

이런 경우 이제는 GUEST가 0, USER가 1, ADMIN이 2가 되어 기존에 DB에 들어있던 값들이 잘못된 정보를 저장하게끔 바뀌게 되는 문제가 발생한다.

 

그래서 EnumType.ORDINAL을 사용하는 것에는 굉장히 큰 위험이 따른다..

STRING으로 쓰자.
앞선 문제가 사라진다.

그래서 위와 같이 그냥 EnumType.STRING을 사용하는 것이 바람직하다고 볼 수 있다.

 

@Temporal

날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용한다.

 

참고: LocalDate, LocalDateTime을 사용할 때는 생략이 가능하다. (최신 하이버네이트에서 지원해 주기 때문)

알아서 type에 맞게 hibernate가 생성해 준다.

위와 같이 hibernate 최신 버전을 사용하면, 알아서 매핑해 주기 때문에 그냥 사용하면 된다.

 

구버전을 사용하는 경우는 위 표와 같이 직접 지정해서 사용해줘야 한다.

 

@Lob

데이터베이스 BLOB, CLOB 타입과 매핑된다.

  • @Lob에는 지정할 수 있는 속성이 없다.
  • 매핑하는 필드 타입이 문자인 경우 CLOB이 매핑, 나머지는 BLOB이 매핑된다.
    • CLOB: String, char [], java.sql.CLOB
    • BLOC: byte [], java.sql, BLOB

 

@Transient

  • 필드를 매핑하고 싶지 않은 경우 사용
  • 데이터베이스에 저장 X, 조회 X
  • 주로 메모리상에 임시로 값을 보관하고 싶은 경우에 사용하는 어노테이션이다.
  • @Transient private Integer temp; 와 같이 사용

필드를 매핑하는 것은 이렇듯 별로 어려울 게 없다. 연관관계 매핑이 조금 어렵다고 한다.


기본 키 매핑

키 매핑에만 집중하기 위해서 앞서 작성한 내용을 전부 지워보자.

전부 지웠다.

  • @Id
  • @GeneratedValue

기본 키 매핑을 위해서 위의 두 가지 어노테이션을 사용할 수 있다.

 

기본 키를 매핑하는 방법

  • 직접 할당: @Id만 사용
  • 자동 생성 (@GeneratedValue 사용)
    • IDENTITY: 데이터베이스에 위임한다. MySQL
    • SEQUENCE: 데이터베이스 시퀀스 오브젝트를 사용, ORACLE
      • @SequenceGenerator가 필요
    • TABLE: 키 생성용 테이블 사용, 모든 DB에서 사용한다.
      • @TableGenerator 필요
    • AUTO: 방언에 따라 자동 지정, 기본값

하나씩 정리해 보자.

 

직접 할당은 그냥 @Id 어노테이션을 사용하는 걸로 끝이고, 기본 키 자동 할당 전략을 하나씩 살펴보자.

 

IDENTITY 전략 - 특징

MySQL dialect로는 auto_increment와 같다고 함

auto_increment(MySQL)로 지정하고 싶은 경우 IDENTITY를 사용한다고 정리하면 될 것 같다.

  • 기본 키의 생성을 데이터베이스에 위임하는 전략이다.
  • 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용한다.
  • JPA는 보통 트랜잭션의 커밋 시점에서 insert sql을 실행
  • 하지만, auto_increment는 DB에 insert sql을 실행한 이후가 되어서야, ID의 값을 확인할 수 있다.
  • 따라서 IDENTITY 전략em.persist() 시점(영속화 시점)에 즉시 insert sql을 실행하고, DB에서 식별자를 조회하는 방식을 취한다.

 

SEQUENCE 전략 - 특징

  • 데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 DB 오브젝트이다. (ex. 오라클의 시퀀스)
  • 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용한다.

sequence 전략을 사용하기 위해 매핑하는 방법

위 코드를 보면 알 수 있듯이, @SequenceGenerator 어노테이션을 사용해서 sequence 전략에 사용할 sequence 객체에 대한 속성을 지정할 수 있다.

@SequenceGenerator에 지정할 수 있는 속성들

위 속성들 중에서 allocationSize가 중요하다. 

 

sequence 객체 또한 DB에 의해 관리되는 대상이기 때문에, 영속성 컨텍스트에 객체를 영속화하는 시점에서 이미 sequence에서 id를 갖고 온 상태여야 한다.

 

그렇기 때문에, JPA에서 먼저 sequence의 다음 값을 조회한다.

값을 얻어와서 영속화하고자 하는 객체의 id에 값을 넣어준다.

이러한 방식 덕분에 sequence 전략은 transaction이 commit 되는 시점까지 buffering 되었다가 한 번에 insert query를 보낼 수 있는 구조가 가능하다. (IDENTITY는 불가능했다)

 

그런데, 이때에도 계속 네트워크를 통해 DB에서 sequence 객체를 통해 현재 id를 조회하는 것이 성능에 문제가 되지 않을까라는 생각이 든다.

 

이때, allocationSize의 값을 지정하는 이유가 등장한다. 이 속성의 값을 50으로 해놓는다면, 미리 sequence 객체에서 id를 50개까지 땡겨놓은 다음에 사용하도록 할 수 있다.

 

memory에서 가져온 만큼 쓰고, DB에서는 한 번에 업데이트되도록 하여 여러 번 DB에 접속하는 단점을 보완하게 된다.

(간단하게 말해서 애플리케이션에서 사용할 id를 DB에서 먼저 여러 개 땡겨와서 쓰는 구조라고 정리할 수 있다.)

 

TABLE 전략

  • 키 생성 전용 테이블을 하나 만들어 데이터베이스 시퀀스를 흉내 내는 전략이다.
  • 장점: 모든 데이터베이스에 적용이 가능 (테이블 만드는 거니깐)
  • 단점: 성능이 안 좋다

@TableGenerator 어노테이션으로 키 값을 생성할 테이블의 속성들을 지정할 수 있다.

 

권장하는 식별자 전략

  • 기본 키 제약 조건: not null, unique, 변하면 안 된다.
  • 서비스가 운영될 미래까지 이 조건을 충족하는 자연 키는 찾기 어렵다. 따라서 대리 키(대체 키)를 사용하자.
  • 예를 들어 주민등록번호도 기본 키로 사용하기에는 적절하지 못하다. (조건은 충족해도 법안의 변경으로 폐지되어 문제가 되었던 경우가 있었다고 한다)
  • 권장: Long type + 대체 키 + 키 생성 전략 사용

 

728x90

댓글