13 min read

<JPA> 엔티티 매핑 기본

본격적으로 JPA에서의 엔티티 매핑 방법을 알아보기에 앞서 대표적으로 어떤 종류의 매핑이 존재하는지 알아봅시다.

  1. 객체와 테이블 매핑 : @Entity,@Table
  2. 필드와 컬럼 매핑 : @Column
  3. 기본 키 매핑 : @Id
  4. 연관 관계 매핑 : @ManyToOne,@JoinColumn 등등

4번을 제외한 내용들은 해당 포스팅에서 다룰 예정이며,4번 연관관계 매핑과 관련된 내용은 다음 포스팅에서 단독으로 다룰 예정입니다.

이번 포스팅의 목적은 단순히 JPA 문법을 써서 DB로부터 데이터를 받아오는 방법을 알려주는데 있지 않습니다.단순히 문법을 사용해 어플리케이션을 구동하다, 이러한 문법의 디테일한 부분을 알고 싶으신 분들이 참조해주시면 좋을 것 같습니다.

데이터 베이스 스키마 자동 생성 기능 정리

JPA 프로젝트를 여러번 작성해보신 분들은 create,update 등 hibernate.hbm2ddl.auto의 값을 다양하게 설정해보신 적이 많을 것입니다.이러한 속성 값들은 정리해드리겠습니다.

  1. create : 기존 테이블 삭제 후,다시 생성(drop 후 create)
  2. create-drop : 테이블 생성 후, 종료 시점에 drop
  3. update : 변경지점만 반영(alter 쿼리)
  4. validate : 엔티티와 테이블이 정상 매핑 되었는지 확인
  5. none : 사용안함
  • 종류는 위와 같습니다.제가 가장 헷갈렸던 부분은 validate를 사용하게 될 경우,우리가 직접 디비에 ddl 쿼리를 날려줘야하는것인가? 였습니다. 그렇습니다.예를 들어 Member라는 엔티티에 age라는 column이 새로 생성된 경우 직접 쿼리를 날려 디비에 생성시켜줘야 validate를 통과하여 App이 구동됩니다.
  • 근데 다음과 같은 생각이 듭니다.솔직히 update문 쓰면 진짜 편하게 alter 전부해주고 우리는 코드만 수정하면 되는데 왜 안쓸까?디비를 조금 더 공부해보시면 알 수 있는데 alter 쿼리를 날리게 될 경우,실시간 서버에서 DB Lock이 걸리는 경우가 있습니다.이렇게 될 경우 해당 디비에 접근하지 못해 데이터를 가져오지 못하는 불상사가 생깁니다.결론적으로 운영서버에서는 none으로 잡아주시고 개발 서버에서도 validate로 잡아주시거나 익숙하지 않으면 update를 사용해주시면 될것 같습니다.

객체와 테이블 매핑

  • @Entity는 JPA를 사용해서 테이블과 매핑할 클래스에 붙여주는 어노테이션입니다.즉,JPA가 관리해주는 클래스입니다.
  • 기본 생성자가 필수적으로 클래스에 존재해야합니다. 만일 외부패키지에서 해당 클래스의 무분별한 생성을 막고 싶다면 protected 접근 제어자를 사용해서 default constructor를 만들어 줍시다.
  • @Table@Entity로 설정된 엔티티와 매핑할 테이블을 지정해줍니다.
  • 주로 엔티티와 매핑할 테이블의 이름은 테이블과 다르게 지정할때 name 속성과 함께 사용합니다.
@Table(name = "MBR")
@NoArgsConstructor
@Entity
class Member{
	//...
}
  • 실제로 Table의 name값을 위와 같이 줄 경우,아래와 같은 테이블이 생성됩니다.

필드와 컬럼매핑

지금부터 필드값과 컬럼을 매핑시킬때 사용하는 어노테이션들에 대해 알아봅시다.

  1. @Column: 컬럼매핑
  • 아래의 표와 같은 기능을 가집니다.
  • 여기서 unique 속성을 필드값의 Column에 넣어줄 경우 아래와 같이 constraint를 발생시킵니다.무슨 제한 조건인지 전혀 알아볼 수 없죠
  • 그래서 위와 같은 맹점을 방지하기 위해 주로 @Table의 uniqueConstraints 속성을 사용합니다.아래와 같은 방식으로 사용합니다.
  • @Table(uniqueConstraints = @UniqueConstraint( name = "NAME-AGE-UNIQUE",
    columnNames = {"NAME", "AGE" )})
  1. @Temporal: 날짜 타입 매핑
  • 해당 어노테이션은 LocalDate와 LocalDateTime을 사용할 경우에는 생략 가능합니다.
  1. @Enumerated: enum 타입 매핑
  • 자바 enum 타입을 컬럼과 매핑시킬 때 사용하는데 중요한 유의점이 하나 있습니다.해당 어노테이션의 value 속성값을 잘 지정해줘야하는데 default 값은 EnumType.ORDINAL로 지정되어 있습니다.하지만 해당 속성을 사용하면 enum의 값들이 String이 아닌 index로 매핑되게 됩니다.그러면 직관적이지 않을 뿐더러 만약 enum의 타입의 갯수가 변경될 경우 유연성있게 유지보수를 할 수 없습니다.
  • 예를 들어 Member에 RoleType이라는 Enum 타입을 설정합시다.현재 Enumerated의 value값은 기본값인 EnumType.ORDINAL일 것입니다.여기서 ADMIN으로 RoleType을 설정하고 persist를 진행해줍니다.이후 DB값을 확인해봅시다.
public enum RoleType {
    ADMIN,USER
}
@Entity
@Table(name = "MBR")
public class Member {
    @Id
    private Long id;

    private String name;

    @Enumerated
    private RoleType roleType;
}
  • RoleType의 순서 대로 ADMIN은 0번째이기 때문에 0이 할당됨을 볼 수 있습니다.
  • 만약 여기서 아래와 같이 RoleType을 변경해볼까요?그리고 SUPER로 지정후 persist하게 되면 어떻게 될까요?
public enum RoleType {
    SUPER,ADMIN,USER
}
  • 예상하실 수 있듯이 똑같이 0으로 RoleType이 지정됩니다.이러한 문제점 때문에 무조건 Enum을 사용할 경우 EnumType.STRING을 사용하셔야 합니다.
  1. @Lob:BLOB,CLOB 매핑
  2. @Transient:특정 필드를 컬럼에 매핑하지 않음(매핑 무시)
  • 데이터베이스에 해당 필드를 저장하고 싶지 않고,메모리상에 임시로 저장하고 싶을 때 사용합니다.

기본 키 매핑

  • 직접 할당 : @Id만 사용해서 할당,하지만 사용할 일이 없다고 보면 됩니다.

자동할당

대부분의 경우 자동할당을 통해 엔티티의 Id값을 매핑해줍니다.자동할당을 시켜주는 전략 4가지를 알아봅시다.

  1. strategy = GenerationType.AUTO
  • JPA 설정 방언에 따라 자동으로 지정해주는 전략입니다.디폴트값이 AUTO로 지정되어 있습니다.
  1. strategy = GenerationType.IDENTITY
  • 기본 키 생성을 DB에 위임하는 방식입니다.
  • 주로 MySQL,PostgreSQL에서 사용합니다.
  • 실제로 DB에 날라가는 insert문이 특이하게 commit 시점이 아니라 엔티티의persist()시점에 날라갑니다.
  • 코드로 예를 들어 봅시다.구분을 위해 persist()메서드 앞 뒤로 구분선을 넣어주고 실행시켜보면 아래와 같은 결과가 나옵니다.
			Member member = new Member();
            member.setName("brido");

            //1차 캐시에 저장됨
            System.out.println("========Before=======");
            em.persist(member);
            System.out.println("========After=========");

            tx.commit();
  • 이는 JPA의 1차캐시에서 엔티티를 영속성으로 관리하려면 PK값인 Id값을 알아야 하기 때문에 발생한 현상입니다.즉,Id 값이 null이면 영속성으로 취급하지 못하니까 우선적으로 DB에 먼저 쿼리를 날려 Id 값을 지정한 후,해당 Id값을 통해 1차 캐시에 엔티티를 저장하는 것입니다.
  • 그러면 Id값을 DB로부터 가져와야하는데 select 쿼리는 왜 날라가지 않았나?라는 의문을 가지실수도 있는데 이는 JDBC Driver가 위와 같은 경우에 대비해서 바로 Id값을 리턴받을 수 있게 해놓아서 발생하지 않습니다.
  • 정리하면, em.persist()시점에 insert 쿼리가 발생하고 DB에서 PK값을 얻어 옵니다.
  1. strategy = GenerationType.SEQEUNCE
  • 시퀀스의 경우, oracle을 공부해신 분들이면 종종 보셨을 것 같습니다.간단한 정의는 유일한 값을 순서대로 생성하는 특별한 DB 오브젝트라고 이해하시면 됩니다.
  • 시퀀스를 기본키 매핑의 전략으로 삼을 경우 아래와 같은 설정을 거쳐야 합니다.
@Entity
@SequenceGenerator(
name = “MEMBER_SEQ_GENERATOR",
sequenceName = “MEMBER_SEQ", //매핑할 데이터베이스 시퀀스 이름
initialValue = 1, allocationSize = 50)
public class Member {
	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE,
					generator = "MEMBER_SEQ_GENERATOR")
	private Long id;
  • 사실 위와 같은 설정을 거치지 않고 아래와 같이 그저 전략만 SEQUENCE로 줘도 작동은 합니다.
@Entity
public class Member {
	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE)
	private Long id;
  • 하지만 우선 시퀀스의 이름이 동일하게 아래와 같이 설정됩니다.그래서 구분을 위해

@SequenceGenerator를 통해 시퀀스를 생성해줍니다.

  • 그러면 시퀀스는 DB에 있는 값이고,IDENTY 방식과 마찬가지로 엔티티를 영속성 관리 해주려면 DB에 우선적으로 접근해서 식별자를 가져와야하지 않을까요?
  • 쿼리만 나가지 않을 뿐 비슷한 방식을 취합니다.IDENTITY와 동일한 코드에서 전략만 SEQUENCE로 바꾼 경우 아래와 같은 결과가 나옵니다.
  • 그러면 동시에 여러개의 엔티티를 persist할 경우 매번 디비에 들어갔다 나오면 비효율적이지 않을까요?
  • 이러한 점을 보완하기 위해 allocationSize에 50이라는 값(디폴트가 50)이 들어가 있습니다.
  • 쉽게 설명하면 설정값만큼 해당하는 크기의 Id값을 DB로부터 땡겨오는 것입니다.
  • 직접 코드를 통해 봅시다.아래와 같이 persist할 객체를 만들어 줍니다.
 			Member member = new Member();
            Member member1 = new Member();
            Member member2 = new Member();
            Member member3 = new Member();
            Member member4 = new Member();

            member.setName("brido0");
            member1.setName("brido1");
            member2.setName("brido2");
            member3.setName("brido3");
            member4.setName("brido4");

            //1차 캐시에 저장됨
            System.out.println("========Before=======");
            em.persist(member);
            em.persist(member1);
            em.persist(member2);
            em.persist(member3);
            em.persist(member4);
            System.out.println("========After=========");

            tx.commit();
  • 5개의 객체가 있으니 언급한 비효율을 해결하지 못하면 5번의 시퀀스 call이 발생해야 합니다.하지만 2번의 call을 통해 해결됩니다.첫번째 call은 시퀀스를 처음 사용할때 시작값인 1로 설정해주는 것이고,두번째 call을 통해 한번에 50만큼 퍼올려서 객체들의 Id값들을 설정해줍니다.
  • 50만큼 퍼올렸으니,영속성 컨텍스트에 한번에 50개까지는 넣을 수 있습니다.
  • 여기서 혼동하는 부분이 있는데 지금 퍼올린 50만큼의 Id값은 한번의 트랜잭션에 모두 실려야하는 경우입니다. 트랜잭션마다 시퀀스 값 자체가 하나씩 증가하도록 하려면 allocationSize의 값은 무조건 1로 설정해야합니다.
  • 비교를 위해 allocationSize를 1로 설정하고 할 경우에 대한 결과입니다.
  • 한 트랜잭션 내에 다량의 엔티티의 Id값을 할당할 경우에 대한 성능 최적화는 실패했지만 그 다음 트랜잭션의 Id값이 6부터 증가하는 것을 볼 수 있을 것입니다.
  • 두번째 트랜잭션을 그대로 반영하고 싶으시면 update 전략으로 하시면 됩니다.
  1. strategy = GenerationType.TABLE
  • 키 생성 전용 테이블을 하나 만들어서 DB의 시퀀스 객체와 동일한 역할을 수행하게 하는 전략입니다.
  • 모든 DB에 적용 가능하다는 장점이 있지만,테이블을 만드는 지라 성능에 Trade-Off관계가 있습니다.

자동할당 - UUID

  • UUID는 컴퓨터가 가지는 범용 고유 식별자로써 쉽게 말하면 컴퓨터가 Id값을 알아서 안 겹치게 만들어주는것이라고 보면됩니다.
  • 아래와 같은 간단한 코드를 구현 가능합니다.
@Entity
public class MyEntity {
    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid",strategy = "uuid")
    private String id;
	//...
}
  • 회사 내부의 규칙 또는 요구사항으로 UUID를 적용해 달라는 경우도 있으니 한번 정도 간단히 구현해 보시면 좋을 것 같습니다.