웹 개발/JPA 프로그래밍

엔티티(Entity)와 엔티티매니저(EntityManager)

오태림 2019. 1. 1. 16:07

이번에는 엔티티(Entity) 객체와 엔티티를 다루는 엔티티매니저(EntityManager) 객체에 대해 알아보겠습니다.


엔티티(Entity)

엔티티는 영속성을 가진 객체로 DB 테이블에 보관할 대상입니다. 즉 영속 컨텍스트에 속한 객체를 말합니다. 이러한 엔티티는 특정한 시점에 DB에 영향을 미치는 쿼리를 실행하게 됩니다.


이러한 역할을 하는 엔티티를 설정하는 방법은 2가지가 있는데 하나는 @Entity 애노테이션을 이용하는 것이고 다른 하나는 xml 설정을 이용하는 것입니다. 대부분 애노테이션을 이용하므로 애노테이션을 이용하여 엔티티를 설정하는 방법을 알아보도록 하겠습니다.



엔티티를 설정하기 위한 애노테이션


@Entity 애노테이션

 이전에 JPA에서는 쿼리를 자동으로 생성해준다고 했었습니다. JPA는 클래스 이름을 테이블 이름으로 사용하는데 아래의 Room 엔티티는 기본적으로 Room 테이블과 매핑됩니다.

@Entity
public class Room {
//...
}



@Table 애노테이션

그렇다면 클래스이름과 테이블의 이름이 다른 경우는 어떻게하면 될까요? @Table 애노테이션을 이용하면 클래스이름과 테이블 이름이 다르더라도 name 속성을 설정하여 테이블의 이름을 지정할 수 있습니다.

@Entity
@Table(name="room_info")
public class Room{
// ...
}



@Id 애노테이션

DB가 레코드를 구분하기 위해 주요키를 사용하는 것처럼 JPA는 엔티티의 @Id 애노테이션을 이용하여 식별자를 지정합니다. @Id 애노테이션을 지정한 필드 값은 엔티티를 식별하기 위한 식별자로 사용됩니다. 예를 들면 아래 코드에서 id = ? 쿼리에서 ? 자리에 @Id 설정한 필드가 사용됩니다.
public Optional findRoom(int roomId) {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpastart");
    EntityManager em = emf.createEntityManager();
    try {
        Room room = em.find(Room.class, roomId);  // SELECT ... FROM Room r WHERE id = ? 
        return Optional.ofNullable(room);
    } finally {
        em.close();
    }
}



@Basic 애노테이션

@Id 애노테이션을 제외한 나머지 영속 대상은 @Basic 애노테이션을 사용합니다. 아래의 Room 엔티티는 @Basic 애노테이션이 보이지 않는데 생략된 것으로 로 이해하시면 됩니다.
 
@Entity
@Table(name="room_info")
public class Room {
    @Id
    private String number;
    private String name;
    private String description;
}


@Enumerated 애노테이션

열거타입에 대한 매핑은 @Enumerated 애노테이션을 사용합니다. 

// 호텔 등급 열거형 public enum Grade { STAR1, STAR2, STAR3, STAR4, STAR5 }

//호텔 엔티티
@Entity
public class Hotel {
    @Id
    private String id;
    private String name;
    @Enumerated(EnumType.STRING)
    private Grade grade;
}

Hotel 엔티티를 보면 @Enumerated의 속성으로 EnumType.STRING 으로 지정하고 있습니다. 이 속성은 @Enumerated 애노테이션으로 설정한 열거형을 DB로 저장할 때 어떤 값으로 저장할지 결정합니다. EnumType.STRING으로 저장하는 경우 hotel 테이블에는 grade 필드에 "STAR1, STAR2, STAR3, STAR4, STAR5"란 문자열이 저장될 것이고 EnumType.ORDINAL을 사용하는 경우 grade필드에는 0~4(인덱스)가 저장될 것입니다. 여기서 인덱스0은 STAR1, 인덱스4는 STAR5를 가리킵니다. 열거형의 순서는 바뀔 가능성이 있기때문에 ORDINAL을 사용하는 것보다는 STRING을 사용하는 것이 유지보수에 더 유리할 것으로 보입니다.



@Column 애노테이션

@Column 애노테이션은 프로퍼티의 이름과 테이블의 칼럼 이름이 같다면 생략이 가능하지만, 다를 경우에는 @Column 애노테이션을 지정해줘야 합니다. 
@Entity
@Table(name="room_info")
public class Room {
    @Id
    private String number;
    private String name;
        
    @Column(name="description")
    private String desc;
}


좀 이후에 정리하겠지만 JPA는 INSERT, UPDATE, DELETE의 동작이 보통과 다르기 때문에 예상하지 못하거나 실수를 방지하기 위해 읽기 전용 매핑설정이  가능합니다. 읽기 전용 프로퍼티를 설정하는 방법은 insertable 속성과 updateble 속성을 false로 설정하면 됩니다. 읽기 전용 프로퍼티는 JPA가 자동으로 생성하는 쿼리에서 제외됩니다.

@Entity @Table(name="room_info") public class Room { //... @Column(name="id", insertable=false, updatable=false) private Long dbId; }



아래와 같이 insertable 속성과 updatable 속성을 false로 지정하면 JPA에서 쿼리를 생성할 때 해당 컬럼을 제외하는 것을 확인할 수 있습니다.

entityManager.persist(new Room("R202", "홍길동", "2번룸")); // insert into room_info (description, name, number) values (?, ?, ?);

- insert 제외 예시(id가 쿼리에서제외)

entityManager.getTransaction().begin();
Room room = entityManager.find(Room.class, "R202");
room.changeName("리뉴얼 2번룸"); // name을 바꾸는 메서드.
entityManager.getTransaction().commit(); 
// update room_info set description = ?, name = ? where number = ?;
- update 제외 예시(id가 쿼라에서 제외)



@Access 애노테이션

JPA는 DB에서 읽어온 데이터를 엔티티 객체에 매핑하거나, 엔티티 객체를 DB에 반영할 때 엔티티 클래스의 필드 또는 프로퍼티(get/set 메서드)를 사용합니다. 
 예시의 Room 엔티티를 살펴보면 필드에 @Id, @Column 애노테이션을 사용한 것이 아니라 프로퍼티에 사용하고 있습니다. 이렇게 프로퍼티에 애노테이션을 설정하면 DB에서 값을 읽어와 엔티티 객체에 전달할 때는 set메소드를 이용하고, 엔티티를 DB에 반영할 때는 get메소드를 이용하기 때문에 get/set 메소드를 모두 정의해야 합니다.


@Entity @Table(name="room_info") public class Room { private String number; private String name; private String desc; private Long dbId; @Id public String getNumber() { return number; } // setter.. @Column(name="description") public String getDesc(){ return desc; } // setter.. @Column(name="id", insertable=false, updatable = false) { public String getDbId() { return dbId; } // setter.. }


근데 일부는 필드에 일부는 프로퍼티에 설정해보았는데 각각 필드와 프로퍼티를 이용하여 매핑이 되는 것을 확인했습니다. @Access 애노테이션을 사용하여 접근 방식을 지정해야하는 필요성은 잘 모르겠는데 아마 클래스 단위로 접근 방식을 제어하기 위한 것이 아닌가 싶습니다. 

@Entity
@Table(name="room_info")
@Access(AccessType.PROPERTY) // or @Access(AccessType.FIELD)
public class Room {
    // @Id 만약 애노테이션을 필드에 사용하면 에러가 발생함..
    private String number;
    private String name;
    private String desc;
    private Long dbId;

    @Id
    public String getNumber() {
        return number;
    }
    // setter..
    @Column(name="description") 
    public String getDesc(){
        return desc;
    }
    // setter..
    @Column(name="id", insertable=false, updatable = false) {
    public String getDbId() {
        return dbId;
    }
    // setter..
}



@Transient 애노테이션

필드에 transient 키워드나 @Transient 애노테이션을 사용하면 영속대상에서 제외됩니다.
@Entity
@Table(name="room_info")
public class Room {
    @Id
    private String number;
    //...

    @Transient
    private long timestamp = System.currentTimeMills();
}


@Transient 애노테이션을 사용한 엔티티를 조회하면 아래와 같이 쿼리에서 해당 필드가 제외되는 것을 확인할 수 있습니다.

Room find = em.find(Room.class, room.getNumber());
//Hibernate: select room0_.number as number1_0_0_ from room_info room0_ where room0_.number=?



엔티티매니저(EntityManager)

엔티티매니저는 영속 컨텍스트에 접근하여 엔티티에 대한 DB 작업을 제공합니다. 엔티티 매니저의 메서드에 대해 알아보도록 하겠습니다.
 

find() 메서드

find() 메서드는 영속 컨텍스트에서 엔티티를 검색하고 없을 경우 DB에서 데이터를 찾아 영속 컨텍스트에 저장합니다. 여기서 식별자는 Entity 클래스에서 @Id 애노테이션으로 지정한 값을 사용해야 합니다. 

public find(Class entityClass, Object primaryKey)



persist() 메서드

persist() 메서드는 엔티티를 영속 컨텍스트에 저장 후 INSERT 쿼리를 실행합니다. 보통 커밋시점에 INSERT 쿼리를 실행하는데 바로 실행하는 경우도 있습니다. 이는 식별자 생성 방법에 따라 달라지는데 해당 내용은 이후에 정리하도록 하겠습니다.
public void persist(Object entity)


persist() 메서드는 트랜잭션 범위 내에서 실행해야 합니다. persist() 메서드는 실행시점에 영속 컨텍스트에 엔티티를 저장하고, 트랜잭션을 commit() 하는 시점에 insert 쿼리가 실행되므로 트랜잭션 범위에서 실행하지 않는다면 실제로 DB에 반영되지 않습니다.  

EntityManager entityManager = emf.createEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
try {
    transaction.begin();
    Hotel hotel = new Hotel("H101","서울호텔","STAR5");
    entityManager.persist(hotel);
    transaction.commit();
} catch(Exception e) {
    transaction.rollback();
    ...
} finally {
    entityManager.close();
}



remove() 메서드

remove() 메서드는 엔티티 클래스를 영속 컨텍스트에서 삭제 후 DELETE 쿼리를 실행합니다
public void remove(Object entity)

 

remove() 메서드 역시 트랜잭션 범위 내에서 실행되어야 하며 트랜잭션이 commit() 하는 시점에 delete 쿼리가 실행됩니다.

EntityManager em = emf.createEntityanager();
EntityTransaction transaction = em.getTransaction();

try {
    transaction.begin();
    
    Room room = em.find(Room.class, "R101");
    if(room != null) {
        em.remove(room);
    }
    transaction.commit();
} catch(Exception e) {
    transaction.rollback();
    ...
} finally {
    em.close();
}



엔티티 수정

엔티티 매니저는 별도의 update 메서드를 제공하지 않습니다. JPA는 트랜잭션 범위에서 엔티티 객체의 상태가 변경되면 이를 트랜잭션 커밋 시점에 반영한다. 이 사실을 모르고 사용하면 매우 위험할 것 같으니 주의가 필요합니다

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
try {
    transaction.begin();
    Room room = em.find(Room.class, "R101");
    if(room != null) {
        room.changeName("카프리");
    }
    transaction.commit();
} catch (Exception e) {
    transaction.rollback();
    //...
} finally {
    em.close();
}