JPA 엔티티 매핑

영속성 컨텍스트는 모든 객체가 아닌 DB 테이블과 연결되는 객체인 엔티티만 관리한다.

오늘은 엔티티를 사용하는 방법과 Spring을 사용하지 않고 순수 자바에서의 JPA 세팅 방법에 대해 정리해보겠다.

 

JPA 세팅

Hibernate Dependency  원하는 버전의 JPA 의존성을 추가해준다. 

 

META-INF 폴더 하위에 persistence.xml을 생성해서 하이버네이트 설정을 적용할 수 있다. 이때 설정 정보에 있는 persistence-unit의 name을 통해 EntityManagerFactory 생성 시 적용할 persistence 설정을 주입할 수 있다.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2" xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
    <persistence-unit name="hello">
        <properties>
            <!-- 필수 속성 -->
            <property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="jakarta.persistence.jdbc.user" value="sa"/>
            <property name="jakarta.persistence.jdbc.password" value=""/>
            <property name="jakarta.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/jpashop"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>

            <!-- 옵션 -->
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments"  value="true"/>
            <property name="hibernate.hbm2ddl.auto" value="update" />
        </properties>
    </persistence-unit>

</persistence>
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

 

스키마 자동 생성 옵션

ddl.auto 속성의 값을 어떻게 주냐에 따라 애플리케이션 실행 시 DB DDL을 자동으로 세팅할 수 있다.

일반적으로 운영 서버에서는 DB가 날아갈 위험이 있으므로 none 또는 validate로 사용한다.

update 또한 DDL이 잘못 업데이트 되면 DB에 락이 걸려서 서비스에 장애가 발생할 수 있기 운영 서버에서는 사용하지 않는 것이 좋다.

테스트 환경이나 개발 환경에서 DDL을 미리 확인하고 테스트 해보기 위해서 create와 update로 사용하는 경우가 많다.

  • create: 전부 날리고 새로 생성
  • create-drop: 새로 생성하고 프로그램 종료 시 전부 삭제
  • update: 변경된 DDL 반영
  • validate: JPA 엔티티에 명시된 내용과 실제 DB DDL을 비교해서 체크해준다.
  • none: 아무것도 하지않음

 

엔티티 매핑

엔티티란 JPA에서 관리하는 테이블과 연결되는 객체로 테이블과 매핑하기 위한 정보를 입력해줘야한다.

public, protected인 기본 생성자가 필수로 존재해야하며, 변경이 가능해야하기 때문에 final 키워드 사용이 불가하고, enum, interface, inner 클래스에서 사용이 불가능하다.

 

@Entity

JPA가 관리할 엔티티로 지정한다. name 옵션을 통해서 엔티티 이름을 지정해 줄 수 있고, 기본 값은 클래스 명이다.

일반적으로 클래스 명이 겹치지 않는다면 기본 값으로 사용하는 것을 권장한다.

 

@Table

엔티티와 매핑할 테이블에 대한 설정을 할 수 있다. 테이블 이름을 지정하거나, 유니크 제약 조건 등을 사용할 수 있다.

@Entity 사용 시 @Table을 사용하지 않아도 클래스 이름과 동일한 이름의 테이블과 연결된다.

 

@Id

엔티티는 반드시 식별자를 가져야하기 때문에 @Id로 기본 키를 지정해주어야한다.

 

@GeneratedValue

기본 키를 자동으로 생성하기 위한 어노테이션으로 어떤 방식으로 키를 생성할 지 strategy 옵션으로 전략을 설정할 수 있다.

테이블의 키는 비식별 키를 사용하는 것이 안전하다. 식별 키로 주민번호를 썼을 때 나라에서 주민번호를 DB에 저장할 수 없게 정책을 바꾼다면 기존 데이터를 전부 수정해야한다. 또는 전화번호와 같이 고유하지만 변경 가능성이 있다면, 역시 수정의 번거로움이 발생한다. 따라서 절대 바뀔 일이 없고 아무런 의미도 갖지않은 비식별 키를 가지는 것이 좋다.

  • IDENTITY
    • Auto Increment로 객체 저장 시 ID 값이 1씩 증가한다.
    • 가장 간편하고 보편적으로 사용되는 방법이지만, 다음 ID 값을 알기 위해서는 DB에 접근해야한다는 단점이 있다. 
  • SEQUENCE
    • Oracle Sequence와 같이 시퀀스 객체를 만들어서 ID 값을 생성한다.
    • 엔티티 레벨에서 @SequnceGenerator를 선언해서 ID 생성 방식을 지정해줘야한다. 이때 allocation size를 지정해주면 해당 범위 만큼의 시퀀스를 미리 확보해서 사용이 가능하며, 이를 통해 DB에서 조회하지 않고, 서버에서 바로 다음 ID를 생성하여 성능을 개선할 수 있다.
    • 시퀀스를 확보는 DB에서 시퀀스 값을 allocation size 만큼 증가 시킨 후 서버에서 현재부터 증가된 값 만큼을 사용하는 방식으로 확보한 상태로 서버가 다운되면 해당 범위 만큼의 시퀀스를 날리는 것이다. 따라서 적당한 시퀀스 범위를 잡아야한다.

 

필드와 컬럼 매핑

@Column

엔티티의 필드에 연결할 컬럼에 대한 정보를 지정할 수 있다. 사용하지 않을 시 필드명과 같은 이름의 컬럼에 매핑된다.

name, insertable, updatable, nullable, unique, length, columnDefinition(직접 문자열로 DDL 쿼리 작성) 등 다양한 DDL 설정 옵션이 존재한다.

이름만 제대로 맞추면 매핑은 끝이다. 다만 다양한 옵션으로 DDL을 설정할 수 있는데, ddl-auto 값이 none, validate가 아니라면 반영이되고 , 반영하지 않더라도 IDE에서 ERD를 오가며 스키마를 확인할 필요 없이 그 자체로 문서화가 되기 때문에 명시해주는 것이 좋다.

  • unique 옵션은 유니크 키의 이름을 설정할 수 없다는 문제가 있다. 랜덤으로 이름이 생성되기 때문에 @Table에서 유니크 제약 조건을 걸어주는 것이 더 유용하다.
  • columnDefinition은 직접 DDL을 작성하기 때문에 특정 DB에 종속된다.

@Enumerated

DB에는 Enum 타입이 존재하지 않기 때문에 Enum의 이름 또는 순서를 DB에 저장하여 사용할 수 있다.

EnumType을 통해 설정할 수 있으며, ORDINAL이 순서이고, STRING이 이름을 저장한다. 

순서를 저장할 경우 Enum 코드가 변경됨에 따라 순서가 변경되면 기존 데이터를 전부 변경해야한다. 따라서 이름을 사용해야한다.

 

@Temporal

시간 정보를 저장할 때 사용한다. 최신 하이버네이트에서는 사용하지 않더라도 Date, LocalDate, LocalDateTime 타입을 통해 시간 저장 양식을 자동으로 맞춰준다. 하지만 예전 버전의 하이버네이트를 사용한다면 @Temporal을 통해 DATE, TIME, TIMESTAMP로 옵션을 지정해줄 수 있다.

 

@Lob

BLOB, CLOB 타입으로 필드의  타입에 따라 자동 매핑되며, 게시글 처럼 사이즈가 매우 큰 타입에 대해 사용한다.

 

@Transient

필드를 컬럼에 매핑하지 않고, 메모리에서만 사용하고 싶을 때 사용한다. 

 

 

예시 코드

@Entity
public class Employee {

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

    @Column(name = "full_name", nullable = false, length = 100)
    private String fullName;

    @Enumerated(EnumType.STRING)
    private Position position;

    @Temporal(TemporalType.DATE)
    private LocalDate hireDate;

    @Lob
    private String description;

    protected Employee() {}

    // 실제 사용하는 생성자
    public Employee(String fullName, Position position, LocalDate hireDate, String description) {
        this.fullName = fullName;
        this.position = position;
        this.hireDate = hireDate;
        this.description = description;
    }

    // Getter 및 Setter 생략 => 일반적으로 Setter는 변경이 필요한 필드만 생성하고, Getter는 모든 필드에 대해 생성한다.
}

 

 

 

 

연관관계 매핑

테이블은 외래 키를 통해 연관된 데이터를 조회하지만, 객체는 참조를 통해 연관 객체를 조회한다.  

이때 엔티티에서 외래 키를 필드로 가진다면, 객체를 객체스럽게 조회할 수 없다. 더 객체지향적인 코드를 만들 수 있다.

@Entity
public class Member {
    @Column(name = "MEMBER_ID")
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    //A. 연관관계 매핑을 하지 않았을 때 
    @Column(name = "TEAM_ID")
    private Long teamId;
    
    //B. team 객체를 불러올 때
    @JoinColumn(name = "TEAM_ID")
    @ManyToOne
    private Team team;
}

//A 방식을 사용했을 때의 조회 코드
Member member = em.find(Member.class, 1L);
Team team = em.find(TEAM.class, member.getTeamId());

//B 방식을 사용했을 때의 조회 코드
Member member = em.find(Member.class, 1L);
Team team = member.getTeam();

 

@ManyToOne

다대일 관계를 매핑하며, DB에서 Many 쪽이 외래 키를 가지고 있기 때문에 @JoinColumn으로 외래 키와 매핑해주어야한다.

가장 안정적이기 때문에 관계를 최대한 @ManyToOne으로 풀어내는 것이 좋다.

@Entity
private class Member{
    @Id @GeneratedValue
    private Long id;
    
    private String name;
    
    @JoinColumn(name = "team_id")
    @ManyToOne
    private Team team;
}

 

@OneToMany

일대다 관계를 매핑하며, One 쪽에는 외래 키가 없어서 매핑될 필드가 없다.

단독으로 사용될 때는 JPQL을 통해 받아온 값을 저장하여 사용한다. 하지만 다대일과 일대다를 동시에 사용해서 양방향으로 매핑을 한 상황이라면 외래 키를 가진 Many쪽에서 연결한 객체에 mappedBy로 매핑하여 사용할 수 있다.

이때 연관관계의 주인이라는 개념이 등장하는데, 외래 키를 가지고 있는 쪽이 주인이 되며, 변경 사항을 DB에 반영하기 위해서는 주인 쪽의 값을 변경해주어야한다.

예를 들어 멤버를 수정하기 위해서는 멤버 엔티티에서 수정해야하고, team의 members를 수정해도 실제 DB member 테이블에는 반영이 되지않는다.

양방향 매핑에서 엔티티에 대해 @ToString, @Data, JSON 변환 시 무한 참조에 의한 스택오버플로우가 발생할 수 있다.

@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team") // Member의 team 객체에 연결하고 있다.
    private List<Member> members = new ArrayList<>();	//new로 컬렉션을 초기화 해주어야 get() 호출 시 NPE를 방지할 수 있다.
}

 

@OneToOne

일대일 관계를 매핑하며, 다대일과 일대다와 유사하게 사용된다. 

@Entity
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToOne(mappedBy = "person")
    private Address address;
}

@Entity
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String street;

    private String city;

    @OneToOne
    @JoinColumn(name = "person_id")
    private Person person;
}

 

@ManyToMany

다대다 관계를 매핑하며, 다대다 관계에서는 중간의 매핑 테이블이 필요한데, 하이버네이트에서는 매핑 테이블을 자동으로 생성해준다.

하지만 자동으로 중간 테이블을 만들어서 처리해주기 때문에 중간 테이블을 개발자가 관리할 수 없다는 문제가 있다.

따라서 실무에서는 권장되지 않으며, 필요한 상황이 있다면 다음과 같이 매핑 테이블까지 선언해서 @OneToMany와 @ManyToOne을 조합해서 사용하는 것이 좋다.

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProducts = new ArrayList<>();
}

@Entity
public class Product {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "product")
    private List<MemberProduct> memberProducts = new ArrayList<>();
}

@Entity
public class MemberProduct {

    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;
}

//코드 출처: https://codeung.tistory.com/254

 

 

 

 

상속관계 매핑

관계형 데이터베이스에는 상속이 존재하지 않기 때문에 다양한 방법으로 객체의 상속을 표현하는데, JPA에서는 3가지 방법으로 상속관계를 표현할 수 있다.

@Inheritance를 사용해서 3가지 strategy를 적용할 수 있다.

 

JOINED

테이블이 정규화 되지만, 조인을 많이 사용해야한다는 특징이 있다. 가장 권장되는 전략이다.

 

SINGLE_TABLE

하나의 테이블에 모든 자식 요소의 필드를 넣는다. 조인이 없어서 빠르고 테이블이 단순하지만, null을 많이 허용하므로 테이블이 커져서 성능이 저하될 수도 있다. 성능이 중요할 경우 고려해볼 수 있다.

 

TABLE_PER_CLASS

테이블마다 부모의 모든 필드를 가지는 방법으로 테이블이 전부 흩어져있어 조회 시 모든 테이블을 조인하기 때문에 안쓰는 것이 좋다.

 

이미지를 보면 DTYPE 이라는 필드가 보이는데 DTYPE은 어떤 자식 엔티티인지 구분하기 위한 용도로 사용된다.

@DiscriminatorColumn으로 DTYPE 필드를 사용할 수 있고, 컬럼명을 지정할 수 있다. 기본값은 DTYPE이다.

@DiscriminatorValue로 DTYPE에 들어갈 값을 지정할 수 있다. 기본 값은 엔티티 이름이다.

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn //name으로 컬럼명 지정 default=DTYPE 
public class Item {
    @Column(name = "ITEM_ID")
    @Id @GeneratedValue
    private Long id;

    private String name;
    private int price;
}


@DiscriminatorValue("A")	//생략 시 Album으로 들어감
@Entity
public class Album extends Item{
    private String artist;
}

 

 

@MappedSupperClass

실제 상속을 표현하기 위한 것이 아닌, 단순히 중복되는 컬럼 정보에 대한 BaseEntity를 생성해서 객체에서 상속했을 때 사용한다.

즉, 테이블에서 상속을 표현하기 위한 것이 아닌 객체에서 상속을 사용하기 위한 어노테이션이다.

@Getter
@Setter
@MappedSuperclass
public abstract class BaseEntity {    
  private String createdBy;    
  private LocalDateTime createdDate;    
  private String lastModifiedBy;
  private LocalDateTime lastModifiedDate;
}

@Entity
public class Member extends BaseEntity {
	...
}

'개발 > Spring' 카테고리의 다른 글

JPA 프록시와 지연로딩  (1) 2024.01.28
JPA 영속성 전이와 고아 객체  (0) 2024.01.28
JPA 동작 과정  (0) 2024.01.26
JPA란 무엇인가?  (1) 2024.01.26
data.sql 파일로 테스트 데이터 적용하기  (0) 2024.01.22