이번 포스팅에서는 엔티티매니저와 영속컨텍스트에 대해서 알아보려한다. 그전에 엔티티와 엔티티매니저가 무엇인지 알아야 하는데 이에 대해서는 이전에  정리한 내용이 있으니 참고하길 바란다.

+ JPA


JPA를 이용하여 개발한 프로그램 코드는 보통 다음과 같은 패턴으로 개발된다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
EntityManager emf = Persistence.createEntityManagerFactory("jpastart");
EntityManager em = emf.createManager();
EntityTransaction transaction = em.getTransaction();
 
try {
    transaction.begin();
    Sight sight = em.find(Sight.class1);
    sight.setDetail(new SightDetail('오전9~오후5시''연중무휴''100여대 주차가능'));
    transaction.commit();
catch(Exception e) {
    transaction.rollback();
    throw e;
finally {
    em.close();
emf.close();
}
cs


엔티티매니저를 생성한 이후 트랜잭션을 시작하고 엔티티매니저를 사용하여 엔티티(Sight)를 검색하고 명소정보(SightDetail)를 지정한 이후 트랜잭션을 커밋한다. 얼핏 일반적인 jdbc를 이용한 프로그래밍 패턴과 비슷하지만 JPA는 큰 차이점이 있다.



영속 컨텍스트

앞서 JPA의 가장 큰 특징은 영속성(Persistence)이다. JPA를 사용해 검색한 DB매핑정보(엔티티)는 메모리(영속 컨텍스트)에 저장되고 이러한 엔티티는 영속객체라 부른다. JPA는 매번 데이터베이스에 접근하는 것이 아니라 엔티티매니저를 사용하여 메모리상에 작업을 한후 트랜잭션이 커밋되는 시점에 데이터베이스에 반영되는 구조를 가지고 있다. 


엔티티, 엔티티매니저와 영속 컨텍스트의 관계는 어느정도 이해가 됐을 것 같은데 간단히 정리하면 아래의 그림과 같다.


프로그램에서 엔티티매니저를 이용하여 데이터를 검색요청하면 영속 컨텍스트에 데이터가 없을 경우 DB에서 검색하여 영속 컨텍스트에 엔티티로 저장한다. 이후에 같은 식별자를 가지는 데이터를 요청할 경우에는 DB에서 검색하는 것이 아니라 영속 컨텍스트에서 검색한 결과를 반환한다.



전체적인 흐름은 위와 같고 먼저 엔티티매니저에 대해 자세히 알아보도록 하자.



EntityManager(엔티티매니저)


앞서 엔티티매니저는 영속컨텍스트에 저장되어 있는 엔티티를 관리하기위한 객체라고 설명했었다. 엔티티매니저를 관리하는 주체에 따라 종류가 구분된다

 종류

설명 

애플리케이션 관리 엔티티매니저 

 애플리케이션에서 직접 emf를 생성하고 관리

컨테이너관리 엔티티매니저 

 JBoss EAP, 웹로직, TomEE와 같은 JEE 컨테이너가 EMF를 생성하고 관리


전자는 앞선 코드에서 처럼 프로그램 코드에서 직접 EntityManager를 직접 생성하고 종료하는 방식을 말한다. 반면 컨테이너 관리 엔티티매니저는 프로그램 코드가 아닌 JEE 컨테이너[각주:1]에서 EntityManager의 라이프사이클(생성~종료)을 관리한다. 

 아래의 예제에서 보이다시피 EntityManager에 대한 생성과 종료에 대한 코드가 없다. JEE 컨테이너는 @Transactional 애노테이션이 적용된 메서드를 트랜잭션 범위에서 실행한다. 즉 메소드가 실행될때 트랜잭션이 시작(begin)되고 메서드가 종료될 때 트랜잭션이 완료(commit)되는 것이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
@PersistenceContext EntityManager em;
 
@Transactional
public void withdraw(String email) {
    User user = em.find(User.class, email);
    if(user == null) {
        throw new UserNotFoundException();
    }
    
    em.remove(user);
}
cs



트랜잭션

컨테이너관리 엔티티매니저를 사용했을 때 트랜잭션에 대해 상세한 제어가 필요한 경우가 있다. 애플리케이션에서 관리하는 앤티티매니저의 경우는 개발자가 직접 트랜잭션을 제어할 수 있기 때문에 큰 어려움은 없겠지만 컨테이너가 관리하는 엔티티매니저의 경우는 전파방법에 대해 알아두어야 한다.

그전에 먼저 트랜잭션 종류에 대해 알아보자. JPA는 자원 로컬(Resource Local) 트랜잭션 타입과 JTA(Java Transaction Api) 타입의 두 가지 트랜잭션 타입을 지원한다. 트랜잭션 타입은 persistence.xml 파일의 transaction-type 속성값을 설정하면 된다. 

먼저 자원 로컬 트랜잭션은 JPA가 제공하는 EntityTransaction을 이용하는 방식이다. 아래의 코드는 JPA가 제공하는 자원 로컬 트랜잭션을 사용한 예시이다.
1
2
3
4
5
6
7
8
9
10
11
12
EntityManager em = emf.createEntityManager();
Transaction tx = em.createTransaction();
try {
    tx.begin();
    //...
    tx.commit();
catch(Exception e) {
    tx.rollback();
finally {
    em.close();
}
 
cs


다음은 JTA 트랜잭션 방식인데 JTA 트랜잭션을 사용하면 JPA에서 트랜잭션을 관리하지 않는다. 대신 EntityManager를 JTA 트랜잭션에 참여시켜 트랜잭션을 관리한다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
UserTransaction utx = (UserTransaction)new InitialContext().lookup("java:comp/UserTransaction");
utx.begin(); // JTA 트랜잭션 시작
 
EntityManager em = emf.createEntityManager();
em.joinTransaction(); // JTA 트랜잭션에 참여
 
try {
    // ...
    utx.commit();
catch(Exception e) {
     utx.rollback();
finally {
    em.close()
}
 
cs


위의 코드에서 5라인의 joinTransaction()을 호출하지 않으면 엔티티의 변경이 있을 경우 실제 DB에 반영되지 않는다. 그 이유는 JPA는 트랜잭션 내에서의 변경만 DB에 반영하는데 JTA트랜잭션 방식을 사용하면 엔티티매니저는 트랜잭션에 관여하지 않고 엔티티매니저가 JTA 트랜잭션에 참여하지 않았으니 실제 DB에 반영되지 않는 것이다. 


엔티티매니저의 영속 컨텍스트 전파


  1. Enterprise급 개발에 필요한 모든 기술을 담고 있는 기술 (트랜잭션, JSP, 서블릿 등..)을 사용할 수 있는 환경 [본문으로]

'웹 개발 > JPA 프로그래밍' 카테고리의 다른 글

@Embeddable  (0) 2019.02.19
JPA 식별자 생성 방식  (0) 2019.01.29
엔티티(Entity)와 엔티티매니저(EntityManager)  (0) 2019.01.01
JPA란  (0) 2018.12.29

@Embeddable 애노테이션을 지정한 클래스를 밸류 클래스라고 합니다. 밸류 클래스란 int, double 처럼 하나의 값을 나타내는 클래스를 말합니다. 보통 주소(address)라는 값을 저장하기 위해서는 아래와 같이 String 변수에 저장하여 관리할 것입니다. 

1
String address = "서울시 강동구 천호동 113-12번지 개발아파트 110동 1101호";
cs


하지만 주소 역시 값을 분리하여 저장하는 경우도 많습니다. address1과 address2,, zipCode가 합쳐져야 하나의 정확한 의미를 나타내는데 변수를 분리하여 관리하기 때문에 불편한 점도 있을 것입니다. 이러한 경우에 "밸류 클래스"를 사용하여 값이 가지는 의미를 강화시킬 수 있습니다.


1
2
3
String address1 = "서울시 강동구 천호동";
String address2 = "개발아파트 110동 1101";
String zipCode = "113-12";
cs


밸류 클래스

밸류 클래스는 여러개의 값(address1, address2, zipCode)을 가지지만 개념적으로 하나의 값(주소)을 표현합니다. 또한 다른 밸류 객체와 식별하기 위한 식별자를 가지지 않는 것이 특징입니다. 아래는 주소라는 값을 나타내는 Address 클래스입니다. 변수를 address1, address2, zipCode로 각각 관리했을 때보다 좀 더 의미가 뚜렷한 것 같지않으신가요? 

1
2
3
4
5
class Address {
    String address1;
    String address2;
    String zipCode;
}
cs


하지만 밸류 클래스는 보통 값으로 사용되기 때문에 equals 메소드와 hashCode 메소드를 오버라이딩하여 사용해야 합니다. 


@Embeddable 애노테이션과 밸류 매핑

먼저 @Embeddable 애노테이션은 설정한 클래스가 다른 엔티티의 일부로 저장될 수 있음을 설정하는 애노테이션입니다. 즉 @Embeddable 애노테이션을 설정한 밸류 클래스는 다른 엔티티에 포함될 수 있다는 것이죠. 밸류를 포함하는 엔티티에서는 @Embedded로 밸류 클래스임을 설정합니다. 호텔(엔티티)과 주소(밸류)의 설정을 살펴보시면 이해가 될 것 입니다.

1
2
3
4
5
6
7
8
create table jpastart.hotel (
    id varchar(100not null primary key,
    name varchar(50),
    grade varchar(255),
    zipcode varchar(5),
    address1 varchar(255),
    address2 varchar(255)
engine innodb character set utf8;
cs


호텔 엔티티
1
2
3
4
5
6
7
8
9
10
11
@Entity
public class Hotel {
    @Id
    private String id;
    private String name;
    @Enumberated(EnumType.STRING)
    private Grade grade;
 
    @Embedded
    private Address address;
}
cs

주소 밸류클래스

1
2
3
4
5
6
7
8
@Embeddable
public class Address{
    private String zipCode;
    private String address1;
    private String address2;
 
    //...
}
cs



그럼 엔티티매니저를 사용하여 Hotel을 검색하면 어떻게 될까요? address1, address2, zipCode는 자동으로 Hotel 엔티티안의 Address 밸류클래스로 매핑되기 때문에 호텔의 get메소드를 호출하여 사용할 수 있습니다. 


1
2
3
4
5
Hotel hotel = em.find(Hotel.class"H100-01");
Address address = hotel.getAddress();
// address.getAddress1(); 서울시 강동구 천호동
// address.getAddress2(); 개발호텔
// address.getZipCode();  113-12
cs



@Embeddable 접근 타입

@Embeddable 매핑한 대상은 기본적으로 엔티티의 접근 방법( 필드 접근타입 또는 프로퍼티 접근 타입 )을 따릅니다. 엔티티에서 프로퍼티 접근 타입(getter / setter)를 사용한다면 @Embeddable 매핑한 밸류 클래스 역시 프로퍼티 접근 타입을 사용하기 때문에 getter, setter 메소드를 정의해야 합니다.



@AttributeOverrides

관광지라는 엔티티가 있고 이 엔티티는 한글로 된 주소, 영어로 된 주소 2개를 가진다고 해보자. 

1
2
3
4
5
6
7
8
9
10
create table jpastart.sight (
    id varchar(100not null primary key,
    name varchar(50),
    kor_zipcode varchar(5),
    kor_address1 varchar(255),
    kor_address2 varchar(255)
    eng_zipcode varchar(5),
    eng_address1 varchar(255),
    eng_address2 varchar(255)
engine innodb character set utf8;
cs


위의 sight 테이블을 엔티티로 매핑하면 아래와 같은데 Address 밸류 클래스는 address1, address2, zipcode를 필드로 설정하고 있다. Sight 엔티티에서 Address 밸류클래스를 중복 선언하였기 떄문에  필드가 중복됐다는 JPA의 에러 메세지를 볼 수 있을것이다. 

1
2
3
4
5
6
7
8
9
@Entity
class Sight {
    private String id;
    private String name;
    @Embedded
    private Address korAddress;
    @Embedded
    private Address engAddress;
}
cs



@AttributeOverrides 또는 @AttributeOverride 애노테이션을 사용하면 위와 같은 문제를 해결할 수 있다. 이 두 애노테이션은 @Embedded로 매핑한 값 타입의 매핑 설정을 재정의할 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Entity
class Sight {
    private String id;
    private String name;
    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name="zipcode", column=@Column(name="kor_zipcode"),
        @AttributeOverride(name="address1", column=@Column(name="kor_address1"),
        @AttributeOverride(name="address2", column=@Column(name="kor_address2")
    })
    private Address korAddress;
    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name="zipcode", column=@Column(name="eng_zipcode"),
        @AttributeOverride(name="address1", column=@Column(name="eng_address1"),
        @AttributeOverride(name="address2", column=@Column(name="eng_address2")
    })
    private Address engAddress;
}
cs


위와 같이 설정하면 Address 밸류 클래스 필드값의 컬럼 설정을 재정의할 수 있다. 기존에는 address1, address2, zipcode가 중복되어서 오류가 발생하였지만 @AttributeOverrides, @AttributeOverride 애노테이션을 이용하여 컬럼이 중복되지 않도록 재정의하였다. 


데이터를 객체를 모델링하다보면 밸류 클래스의 중첩이 일어날 수 있는데 JPA에서는 중첩 @Embeddable도 지원된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Entity
public class City {
    // ...
    
    // 도시 연락처정보
    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name="address.zipcode", column=@Column(name="city_zip")),
        @AttributeOverride(name="address.address1", column=@Column(name="city_addr1")),
        @AttributeOverride(name="address.address2", column=@Column(name="city_addr2"))
    })
    private ContractInfo contractInfo;
}
 
@Embeddable
public class ContractInfo {
    @Column(name="ct_phone")
    private String phone;
 
    @Column(name="ct_email");
    private String email;
 
    @Embedded
    // City 클래스에서 재정의 안했을 경우 ContractInfo 클래스에서도 
    //@AttributeOverrides({
    //    @AttributeOverride(name="zipcode", column=@Column(name="ct_zip")),
    //    @AttributeOverride(name="address1", column=@Column(name="city_addr1")),
    //    @AttributeOverride(name="address2", column=@Column(name="city_addr2"))
    //})
    private Address address;
}
 
 
cs



밸류를 다른 테이블에 저장하기

sight 테이블을 Sight 엔티티와 Address 밸류로 매핑한 것처럼 하나의 테이블을 엔티티와 밸류로 저장할 수도 있지만, 
엔티티와 밸류를 각각의 테이블로도 저장할 수 있다. UML로 표현하면 아래와 같은 구조를 가진다



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Embeddable
public class SightDetail {
    @Column(name="hours_op")
    private String hourcOfOperation;
    private String holidays;
    private String facilites;
}
 
@Entity
@SecondaryTable(
    name ="sight_detail",
    pkJoinColumns= @PrimaryKeyJoinColumn(     //sight_detail의 sight_id(기본키=외래키)가 sight테이블의 id(식별자)를 참조함
        name = "sight_id", referencedColumnName = "id")
public class Sight {
    // 생략..
 
    // table = sight_detail은 데이터를 해당 테이블에서 가져오겠다는 의미
    @Embedded
    @AttributeOverrides({
        @AttributeOverride(
            name="hoursOfOperation",
            column=@Column(name="hours_op", table="sight_detail")),
        @AttributeOveride( 
            name="holidays",
            column=@Column(table="sight_detail")),
        @AttributeOverride(
            name="facilities",
            column=@Column(table="sight_detail"))
    })
    private SightDetail detail;
}
cs




'웹 개발 > JPA 프로그래밍' 카테고리의 다른 글

EntityManager 와 영속 컨텍스트  (0) 2019.03.03
JPA 식별자 생성 방식  (0) 2019.01.29
엔티티(Entity)와 엔티티매니저(EntityManager)  (0) 2019.01.01
JPA란  (0) 2018.12.29

이번 포스팅에서는 JPA에서 식별자를 생성하는 방식에 대하여 알아보도록 하겠습니다. 식별자 생성 방식에는 애플리케이션에서 직접 생성하는 방식(특별한 식별자 규칙이 있는 경우)과 JPA 에서 생성하는 방식이 있습니다. JPA가 식별자를 생성하는 방식은 다시 식별 칼럼 방식, 시퀀스 방식, 테이블 방식이 있습니다.


JPA에서 식별자를 생성하는 방식


식별 칼럼 방식

JPA가 식별 칼럼을 이용해서 식별자를 생성하려면 @Id 애노테이션 대상에 추가적인 설정이 필요합니다. @GeneratedValue 애노테이션을 추가하고 strategy 값을 설정하면 됩니다.

@Entity @Table(name="hotel_review") public class Review { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Long id; }


GenerationType.IENDTITY 는 식별자 생성을 데이터베이스의 식별자 생성 방식을 사용하여 식별자를 생성하는 방법입니다. 데이터베이스에서 기본키(식별자)를 auto_increment로 생성한다면 이에 의존하여 엔티티의 식별자를 생성하는 것이죠. 


사실 데이터베이스의 식별 칼럼이라는 것을 잘 모르겠어서 아래에서 정리할 "시퀀스 방식"과 큰 차이점을 모르겠네요.. 글을 읽으신 선배님이 계시다면 가르침 부탁드립니다 ㅠㅠ 


시퀀스[각주:1] 방식

@SequenceGenerator 애노테이션을 이용하면 데이터베이스의 시퀀스를 이용하여 식별자를 생성할 수 있습니다. 

    @Id
    @SequenceGenerator(
        name = "review_seq_gen",
        sequenceName = "hotel_review_seq",
        allocationSize = 1
    )
    @GeneratedValue(generator="review_seq_gen")
    private Long id;



@SequenceGenerator의 name으로 설정한 값은 @GeneratorValue 애노테이션에서 generator로 사용됩니다. sequenceName으로 설정한 값은 실제 데이터베이스의 시퀀스이며, allocationSize는 시퀀스 증가값을 설정합니다. 참고로 allocationSize의 기본값은 50인데 1로 사용해야 별탈없이 사용이 가능한데 다음에 기회가 된다면 정리하도록 하겠습니다.


시퀀스 방식을 사용하는 경우는 persist() 시점에 insert 쿼리를 실행하지 않고 시퀀스 관련 쿼리만 실행하여 식별자를 생성합니다.


1
2
3
transaction.begin();
Review review = new Review("KR-S-01"5"최고입니다", new Date());
entityManager.persist(review); // select hotel_review_seq.nextval from dual 
cs



테이블 방식

식별자를 구분하기 위해 사용할 주요키 칼럼과 다음 식별자로 사용할 숫자를 보관할 컬럼을 가지는 테이블을 이용하는 방법입니다. 아래의 id_gen 테이블은 entity 컬럼에서 엔티티를 구분하기위한 용도로 사용되고 nextid 컬럼은 해당 엔티티의 다음 식별자로 사용된다.


1
2
3
4
create table id_gen(
    entity varchar(100not null primary key,
    nextid int
engine innodb character set utf8;
cs


그럼 테이블 방식으로 엔티티의 식별자를 생성해보도록 하겠습니다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
public class City {
    @Id
    @TableGenerator(name = "idgen",
        table = "id_gen",
        pkColumnName = "entity",
        pkColumnValue = "city",
        valueColumnName = "nextid",
        initialValue = 0,
        allocationSize = 1)
    @GeneratedValue(generator = "idgen")
    private Long id;
}
cs


  • name : 테이블 생성기의 이름을 지정합니다. @GeneratedValue의 generator 속성값으로 사용됩니다.
  • table: 식별자를 생성할 때 사용할 테이블을 지정합니다.
  • pkColumnName: 식별자 생성용 테이블의 주요키 칼럼을 지정합니다.
  • pkColumnValue: 주요키 칼럼에 사용할 값을 지정합니다. 각 @TableGenerator마다 다른 값을 사용해야하며 보통 엔티티 클래스의 이름을 사용합니다.
  • valueColumnName: 생성할 식별자를 갖는 칼럼을 지정합니다.
  • initialValue: 식별자의 초기값을 지정합니다. 
  • allocationSize: 식별자를 해당값만큼 증가시킵니다. 이 값 역시 1로 설정해야 합니다.


1
2
3
4
5
6
7
8
9
10
11
transaction.begin();
 
City city = new City("서울");
entityManager.persist(city);
 
transaction.commit();
 
 
//select tbl.nextid from id_gen tbl where tbl.entity='city' for update
//insert into id_gen (entity, nextid) values ('city', 1) // 레코드가 존재하지 않을 때 식별자 테이블에 insert, 이미 식별자가 존재하는 경우는 쿼리 
//update id_gen set nextid=2 where nextid=1 and entity='city'
cs



위의 코드는 persist() 메서드를 실행하는 시점에 식별자 테이블에서 식별자를 구하고 다음에 식별자로 사용할 값을 업데이트하는 것을 확인할 수 있습니다.

  1. 순차적으로 증가하여 유일한 값을 생성해주는 객체 [본문으로]

'웹 개발 > JPA 프로그래밍' 카테고리의 다른 글

EntityManager 와 영속 컨텍스트  (0) 2019.03.03
@Embeddable  (0) 2019.02.19
엔티티(Entity)와 엔티티매니저(EntityManager)  (0) 2019.01.01
JPA란  (0) 2018.12.29

이번에는 엔티티(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();
}




'웹 개발 > JPA 프로그래밍' 카테고리의 다른 글

EntityManager 와 영속 컨텍스트  (0) 2019.03.03
@Embeddable  (0) 2019.02.19
JPA 식별자 생성 방식  (0) 2019.01.29
JPA란  (0) 2018.12.29


실무에서 JPA를 사용하고 있는데 올바르게 사용하고 있는지 확신이 들지않아 최근에 공부를 시작했습니다. "JPA 프로그래밍 입문"이란 책을 읽고 공부하며 제가 이해한 내용을 정리하는 곳이니 참고용으로만 봐주시면 감사할 것 같습니다. 


JPA(Java Persistence API)란 무엇인가? 

JPA란 DB 테이블과 자바 객체 사이의 매핑을 처리해주는 ORM[각주:1]이란 기술의 표준입니다. 따라서 JPA에는 객체와 DB 사이의 매핑을 어떻게 설정하고 어떻게 동작해야하는지 기술하고 있습니다. 즉 자바의 클래스와 DB의 테이블을 매핑하는 기술이란 뜻입니다.


JPA를 살펴보다면 하이버네이트(hibernate)란 말을 많이 접하게 됩니다. 그렇다면 하이버네이트란 무엇일까요?


JPA 프로바이더

하이버네이트란 JPA 프로바이더의 한 종류입니다. JPA 프로바이더는 JPA의 표준을 실제로 구현하고 있습니다. 쉽게 말하자면 JPA는 DB와 자바 객체를 매핑하기 위한 인터페이스(API)를 제공하고 JPA 프로바이더는(하이버네이트) 이 인터페이스를 구현한 것이죠.


JPA 특징

  1. 자바 객체와 DB 테이블 사이의 매핑 설정을 통해 SQL을 생성합니다. 보통 JDBC를 사용하여 개발을 하다보면 코드가 비슷한 형태로 반복되게 됩니다.
    DB커넥션을 구하고, 쿼리를 작성하고, 파라미터를 설정하고 실행한 결과를 자바 객체에 설정합니다.
     여기서 문제점은 테이블의 컬럼명이 추가, 삭제, 변경이 된다면 관련되어 있는 모든 쿼리를 수정해야 할 것인데 JPA는 매핑 설정만 변경하면 됩니다.
    또한 실행한 쿼리를 자바 객체에 설정해줘야하는데 JPA는 자바 객체로 매핑하여 검색할 수 있습니다. 즉 유지보수가 쉽습니다.

  2. 객체를 통해 쿼리를 작성할 수 있는 JPQL(Java Persistence Query Language)를 지원합니다.

  3. JPA는 성능 향상을 위해 지연 로딩이나 즉시 로딩과 같은 몇가지 기법을 제공하는데 이것을 잘 활용하면 SQL을 직접 사용하는 것과 유사한 성능을 얻을 수 있습니다. 하지만 잘못이해하고 사용하는 JPA는 성능을 크게 감소시킬 수 있습니다.

JPA 설정

JPA는 기본적으로 클래스패스에 있는 META-INF/persistence.xml 파일을 설정 파일로 사용합니다. 다음은 일반적인 persistence 파일입니다.

<persistence-unit> 태그의 name은 영속 단위를 설정하고 transaction-type은 JPA가 사용할 트랜잭션을 설정합니다. <class> 태그는 영속 관리할 클래스를 지정하며 <exclude-unlisted-classes>속성을 true로 설정하면 <class>태그에서 설정하지 않은 클래스들은 영속 관리 대상에서 제외됩니다. 




JPA 영속성

앞에서부터  "영속성"이란 반복되고 있는데 영속성이란 무엇일까요? JPA에서는 @Entity 애노테이션을 붙인 클래스를 "엔티티"라고 부릅니다. 엔티티는 다음에 자세히 알아보도록 하겠습니다. 

영속 컨텍스트는 엔티티를 담고있는 집합인데 JPA는 이 영속 컨텍스트에 속한 엔티티를 DB에 반영합니다. 즉 프로그램에서는 엔티티를 영속 컨텍스트에 
속한 엔티티를 검색하거나 삭제 또는 추가, 변경하는 작업을 하면 영속 컨텍스트의 내용이 DB에 반영되는 것이죠.


하지만 프로그램에서는 영속 컨텍스트에 직접 접근할 수 없고 EntityManager를 통해서만 영속 컨텍스트에 접근할 수 있습니다. 






  1. Object-Relational Mapping [본문으로]

'웹 개발 > JPA 프로그래밍' 카테고리의 다른 글

EntityManager 와 영속 컨텍스트  (0) 2019.03.03
@Embeddable  (0) 2019.02.19
JPA 식별자 생성 방식  (0) 2019.01.29
엔티티(Entity)와 엔티티매니저(EntityManager)  (0) 2019.01.01

+ Recent posts