카테고리 없음
JPA 까먹지 말자! (2)
머룽
2023. 4. 23. 14:05
오늘은 저번시간에 이어서 JPA 까먹지 말자! (2) 를 시작해보자. JPA라 했지만 구현체는 hibernate 기준으로 설명하니 다른 구현체들은 조금씩 다를 수도 있으니 참고하면 되겠다. 또한 종종 hibernate 이야기도 있을 수도 있다.
@GeneratedValue strategy
JPA에서는 @GeneratedValue 어노테이션의 strategy 속성으로 기본키 전략을 설정할 수 있다. 물론 직접 기본키를 생성해주는 것도 좋지만 그보다는 자동생성 전략도 나쁘지 않게 생각된다. 필요하다면 비지니스 키를 따로 만들어서 직접 생성해주는 방법도 생각해 볼 수 있다. JPA에서는 기본키의 전략이 3가지가 있다.TABLE
, SEQUENCE
, IDENTITY
전략이다. AUTO 전략도 있지만 AUTO는 해당 벤더에 따라 위의 세가지중 하나를 선택하게 된다. 3가지의 전략은 따로 설명하지 않겠다.
근데 이때 알아야 할 것은 기본 키 전략이 IDENTITY
일경우에는 쓰기 지연이 동작하지 않는다. 예를들어 다음과 같다.
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
//..
}
@Transactional
public void saveAccount() {
Account account = new Account();
account.setName("wonwoo");
entityManager.persist(account);
}
위와 같이 작성할 경우에는 entityManager.persist(account)
이 메서드를 호출 할때 바로 insert 쿼리라 작성되어 DB에 날라간다. 하지만 TABLE
전략이나 SEQUENCE
전략은 Transaction
이 끝나고 flush 혹은 commit이 호출 될 때 insert 쿼리가 만들어져서 날라 가게 된다. 그 이유는 아마 데이터베이스에서 ID를 가져와야 하므로 insert 쿼리라 먼저 만들어져서 DB에 날라가는 것 같다.
만약 쓰기 지연 효과를 얻고 싶다면 TABLE
또는 SEQUENCE
전략으로 설계를 해야 한다.
@Basic LAZY
JPA 에서는 @Basic 어노테이션이라는 것이 있다. 하지만 우리는 거의 사용하지 않는다. 왜냐하면 사용할 일이 없기 떄문이다.. 기본 타입을 말하는 건데. 굳이 쓰지 않아도 암시적으로 JPA가 이거슨 기본타입이라고 설정해 주기 때문이다.public class Account {
@Id
@GeneratedValue
@Basic
private Long id;
@Basic
private String name;
}
public class Account {
@Id
@GeneratedValue
private Long id;
private String name;
}
위의 코드는 암묵적으로 동일하다. 그런데 우리는 기본타입을 Lazy 로딩 할 수 있다. @Basic 어노테이션에는 fetch
속성이 존재한다. @OneToMany
나 @OneToOne
기타 매핑하는 어노테이션에서 사용하는 fetch와 동일하다.
@Basic(fetch = FetchType.LAZY)
@Lob
private String content;
위와 같이 fetch 속성에 FetchType을 LAZY
설정해 주면 된다. 그럼 실제 content
가 사용 될때 쿼리가 작성되어 날라간다.
Account account = entityManager.find(Account.class, 1L);
System.out.println(account.getContent()); //LAZY
실제 쿼리를 보면 아래와 같다.
select
account0_.id as id1_0_0_,
account0_.name as name3_0_0_
from
account account0_
where
account0_.id=?
select
account_.content as content2_0_
from
account account_
where
account_.id=?
위 처럼 content를 사용할 때 한번 더 쿼리를 날린다. 근데 사용할 일이 있을까 싶다. content의 크기가 어마어마하게 크면 모를까..
근데 바로는 되지 않고 설정을 조금 해야한다. 여러방법이 있는 것 같은데 그 중에서 가장 쉬운 방법은 메이븐을 사용한다면 아래와 같이 plugin을 작성해야 한다.
<plugin>
<groupid>org.hibernate.orm.tooling</groupid>
<artifactid>hibernate-enhance-maven-plugin</artifactid>
<version>${hibernate.version}</version>
<executions>
<execution>
<configuration>
<failonerror>true</failonerror>
<enablelazyinitialization>true</enablelazyinitialization>
</configuration>
<goals>
<goal>enhance</goal>
</goals>
</execution>
</executions>
</plugin>
근데 설정을 좀 잘해야 될거 같다. 다른 프록시들과 조금 꼬이는 듯하다. 만약 사용할 경우에는 다른 프록시들이 잘되는지 테스트를 많이 해봐야 될 것 같다.
Criteria
요즘의 개발자들은 거의 사용하지 않겠지만 (필자 역시도 요즘개발자라) 예전 개발자분들은 익숙한 클래스일 듯 싶다. 요즘 Criteria 보다는 훨씬 쉬운 Querydsl 이라는게 있어 대부분이 Querydsl을 사용하지 Criteria를 사용하지는 않는 것 같다. Criteria와 Querydsl은 동일하게 JPQL을 편하게 작성하도록 만든 빌더 클래스들이다. Criteria는 JPA표준이고 Querydsl은 비표준이지만 훨씬 간단하고 알아보기 좋고 쉽다. 하지만 이런게 있다고만 알아두면 좋을 것 같다.CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<account> criteria = builder.createQuery(Account.class);
Root<account> root = criteria.from(Account.class);
criteria.select(root);
criteria.where(builder.equal(root.get("id"), 1L));
Account account = entityManager.createQuery(criteria).getSingleResult();
위와 같이 조금 어렵다. 뭔가 많은 작업을 해야 된다. 그리고 조금 알아보기도 힘든 것같다. 익숙하지 않아서 그런가? 그래도 사용하고 싶다면 사용해도 된다. JPA 표준이니까. 이것의 장점은 타입세이프해서 좋은 것 같다. 물론 root.get("id")
이 부분도 타입세이프하게 만들 수 있다.
참고로 spring data jpa 에서도 Criteria
를 사용한다. 당연히 그래야 했을 거고 그럴꺼라고 생각했다.
Proxy
JPA에서는 바로 쿼리를 날려서 가져오는 find 메서드와 나중에, 해당 객체를 사용할 떄 가져오는(Lazy) getReference 메서드가 존재한다.Account account = entityManager.find(Account.class, 1L);
Account account1 = entityManager.getReference(Account.class, 2L);
위와 같이 딱 두줄만 코드를 작성했을 때 쿼리는 몇번 날아갈까?
정답은 한번이다. getReference 메서드를 사용할 때는 실제 쿼리를 날리지 않고 프록시 객체만 전달 된다. 왜 그럼 굳이 proxy 객체만 전달하는 getReference
를 만들었을까?
예를들어 연관관계를 맺을 경우 account의 엔티티가 필요한 상황이라고 가정해보자. 물론 account의 엔티티들을 사용한다면 프록시 객체가 필요 없겠지만 단지 연관관계를 맺을 목적이라면 굳이 데이터베이스에 쿼리를 날릴 필요가 없다. 그럴 경우 이 프록시 객체를 사용하면 된다.
Account account = entityManager.getReference(Account.class, 2L);
Address address = new Address();
address.setAddress("seoul");
address.setAccount(account);
entityManager.persist(address);
위와 같이 작성했을 경우 insert 쿼리 한번만 데이터베이스에 날라가게 된다. 좀 더 성능적으로 최적화를 할 수 있다. 이런 경우에라면 정말 좋은 기능이지 않나 싶다.
참고로 다른 JPA의 구현체들은 어떤 프록시를 사용하는지 모르겠지만 hibernate 경우에는 javassist를 사용했다. 하지만 이 글을 쓰는 기준으로 최신버전인 5.3.7 버전은 bytebuddy로 proxy를 변경하였다. 아마도 5.3 이후부터 bytebuddy로 변경한 듯 싶다. 요즘에 code generation 으로 bytebuddy 를 많이 이용하는 것 같다.
또한 Spring data jpa 프로젝트에서도 해당 메서드를 사용가능하다. 현재 버전은 findById
이전버전은 findOne
메서드로 데이터베이스에서 바로 조회했다면 getOne
메서드로 해당 프록시 엔티티를 가져올 수 있다.
FlushMode
hibernate 경우에는 FlushMode 6가지 정도 지원하는데 JPA 스펙에는 2가지가 있다. 하이버네이트 API를 사용한다면 6개 모두 사용할 수 있겠지만 JPA API만 사용한다면 2가지 타입만이 존재한다.COMMIT
과 AUTO
가 JPA에서 지원해주는 FlushMode 타입이다.
일반적으로 JPA는 트랜잭션이 commit 되기 직전에 flush도 자동으로 된다. 또 한가지 자동으로 flush 가 될 때가 있는데 그때는 jpql이나 쿼리를 날릴 때 자동으로 flush가 호출 된다.
jpql이나 쿼리를 작성해서 날릴 때 flush 되는 설정이 FlushMode.AUTO 설정이다. 이것은 JPA의 기본값이다. 굳이 아래처럼 명시해주지 않아도 된다.
Account account = new Account();
account.setName("wonwoo");
entityManager.setFlushMode(FlushModeType.AUTO);
entityManager.persist(account);
entityManager.createQuery("select a from Account a", Account.class).getResultList();
위와 같이 코드를 작성할 경우 insert 쿼리는 jpql 쿼리를 날리 전에 먼저 데이터베이스에 날라간다.
insert
into
account
(content, name, id)
values
(?, ?, ?)
select
account0_.id as id1_0_,
account0_.content as content2_0_,
account0_.name as name3_0_
from
account account0_
만약 AUTO가 아닌 COMMIT으로 했을 경우에는 insert 는 commit 되고 날라가고 그전에 jpql이나 쿼리가 먼저 날라간다. 그래서 원하는 데이터가 나오지 않을 수도 있다.
entityManager.setFlushMode(FlushModeType.COMMIT);
아래는 FlushMode를 COMMIT으로 했을 떄 쿼리이다. select 쿼리가 먼저 데이터베이스에 날라갔다.
select
account0_.id as id1_0_,
account0_.content as content2_0_,
account0_.name as name3_0_
from
account account0_
insert
into
account
(content, name, id)
values
(?, ?, ?)
쿼리를 날릴때 마다 매번 flush를 하지 않아 성능은 더 좋겠지만 원하는 데이터가 나오지 않을 수도 있으니 상황에 맞게 고민을 해서 사용하면 되겠다.
@NamedQuery
필자는 잘은 사용하지 않지만 사용하면 나쁘지 않은 기능이다. 정적쿼리를 사용할 때 매우 유용한 어노테이션이다. 매번 동일한 쿼리를 작성하지 않고 해당 Name만 지정해줘서 쿼리를 날리면 된다.@Entity
@NamedQuery(name = "findByname", query = "select a from Account a where name = :name")
public class Account {
//...
}
위와 같이 @NamedQuery
어노테이션을 이용해서 정적 쿼리를 만들 수 있다. 아주 심플하다. 근데 많으면 보기 불편할거 같다.
List<account> accounts = entityManager.createNamedQuery("findByname", Account.class)
.setParameter("name", "wonwoo")
.getResultList();
매번 같은 쿼리를 작성하지 않고 해당 name만 작성해서 쿼리를 날릴 수 있으니 재사용성도 있다. 만약 여러개를 작성하고 싶다면 @NamedQueries
어노테이션을 이용하면 된다.
@Entity
@NamedQueries({
@NamedQuery(name = "findByname", query = "select a from Account a where name = :name"),
@NamedQuery(name = "findByEmail", query = "select a from Account a where email = :email")
})
public class Account {
//...
}
이것은 JPA2.1 기준이며 JPA 2.2부터는 좀 더 간편하게 @NamedQuery
어노테이션만 이용해도 된다.
@Entity
@NamedQuery(name = "findByname", query = "select a from Account a where name = :name")
@NamedQuery(name = "findByEmail", query = "select a from Account a where email = :email")
public class Account {
}
@NamedQueries
어노테이션을 사용하는 것보다 @NamedQuery
를 사용하는 것이 더 보기에 좋아 보인다. 하지만 JPA 2.2 부터 지원하니 그 전 사용자라면 @NamedQueries를 사용해야 한다.
오늘은 이렇게 JPA에 관련해서 두번째 시간을 가져봤다. 물론 필자도 JPA를 잘 사용하지 못한다. (나도 잘사용하고 싶다고오오..)
계속계속 사용해야 하는데 말이다.