@Transactional
이 있으면 해당 메서드가 실행되기 전에 트랜잭션 AOP
가 먼저 동작한다.@Controller
class HelloController {
@Autowired HelloService helloService;
public void hello(){
Member member = helloService.logic(); // 반환된 Member 엔티티는 준영속 상태
}
}
@Service
class HelloService {
@PersistenceContext
EntityManager em;
@Autowired Repository1 repository1;
@Autowired Repository2 repository2;
// 메소드를 호출할 때 트랜잭션을 먼저 시작
@Transactional
public void logic() {
repository1.hello();
// member는 영속상태 : 현재 트랜잭션 범위 안에 있으므로 영속성 컨텍스트의 관리를 받는다.
Member member = repository2.findMember();
return member;
}
// 트랜잭션 종료 : 트랜잭션 커밋, 영속성 컨텍스트 종료, 조회한 member는 이제부터 준영속 상태
}
@Repository
class Repository1 {
@PersistenceContext
EntityManager em;
public void hello(){
em.xxx(); // 영속성 컨텍스트 접근
}
}
@Repository
class Repository2 {
@PersistenceContext
EntityManager em;
public void findMember(){
return em.find(Member.class, "id1"); // 영속성 컨텍스트 접근
}
}
트랜잭션이 같으면 같은 영속성 컨텍스트에 접근한다.
트랜잭션이 다르면 다른 영속성 컨텍스트에 접근한다.
서비스 계층에서 트랜잭션을 걸게되면 서비스 로직이 끝나는 시점에 트랜잭션이 종료되면서 영속성 컨텍스트도 함께 종료된다.
따라서, 조회한 엔티티가 서비스나 리포지토리 계층에서는 영속 상태로 관리가 되지만, 컨트롤러나 뷰 같은 프리젠테이션 계층에서는 준영속 상태가 된다.
Order
와 Member
는 ManyToOne
에 지연로딩이 걸려있는 상태Order
객체는 준영속 상태이기 때문에 지연로딩이나 변경감지가 작동하지 않는다.order.getMember()
로 조회한 Member
객체는 프록시 객체이다.member.getName()
으로 초기화를 시도하지만, 준영속 상태이기 때문에 지연로딩이 작동하지 않고class OrderController {
public String view(Long orderId) {
Order order = orderService.findOne(orderId);
Member member = order.getMember();
member.getName(); //지연 로딩 시 예외 발생
}
}
Order
를 불러올 때 연관된 Member
도 실제 객체로 함께 불러온다.@Entity
public class Order{
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.EAGER)
private Member member; // 주문 회원
}
Order order = orderService.findOne(orderId);
Member member = order.getMember();
member.getName(); //이미 로딩된 엔티티
order
와 member
가 모두 필요할 수 있지만, 화면 B에서는 order
만 필요할 수 있다.member
까지 같이 호출해야 하는 상황이 발생한다.JPA를 사용하면서 가장 조심해야 할 성능 이슈 (최우선 최적화 대상)
order
를 엔티티 매니저를 통해 조회했을 때는 join
을 통해 하나의 쿼리로 조회한다.
하지만, JPQL
을 통해 조회하면 JPQL
은 글로벌 페치 전략을 참고하지 않고 SQL
을 생성하기 때문에
일단 order
에 대한 쿼리만 생성
Order.member
의 전략이 즉시 로딩이므로 order
를 로딩하는 즉시 연관된 member
도 로딩해야 함.
연관된 member
를 영속성 컨텍스트에서 찾고 없으면 조회 쿼리를 날린다.
이때, 조회 쿼리를 order
수만큼 날리는 문제가 발생한다.
Order order = em.find(Order.class, 1L);
//SQL
select o.*, m.*
from Order o
left outer join Member m on o.MEMBER_ID=m.MEMBER_ID
where o.id = 1
------------------------------------------------------------------------
List<Order> orders = em.createQuery("select o from Order o", Order.class)
.getResultList();
//SQL
select * from Order // JPQL로 실행된 SQL
select * from Member where id = ? // EAGER로 실행된 SQL
select * from Member where id = ? // EAGER로 실행된 SQL
select * from Member where id = ? // EAGER로 실행된 SQL
select * from Member where id = ? // EAGER로 실행된 SQL
SQL JOIN
을 사용해서 페치 조인 대상까지 함께 조회한다.JPQL:
select o
from Order o
join fetch o.member
SQL:
select o.*, m.*
from Order o
join Member m on o.MEMBER_ID = m.MEMBER_ID
member.getName()
처럼 실제 값을 호출하는 시점에 초기화가 된다.class OrderService{
@Transactional
public Order findOrder(id){
Order order = orderRepository.findOrder(id);
order.getMember().getName(); //프록시 객체를 강제로 초기화
return order;
}
}
프리젠테이션 계층과 서비스 계층 사이에 파사드 계층을 하나 더 두는 방법
뷰를 위한 프록시 객체 초기화는 이곳에서 담당한다.
파사드 계층을 도입해서 프리젠테이션 계층과 서비스 계층 사이의 논리적 의존성을 분리할 수 있다.
class OrderFacade {
@Autowired OrderService orderService;
public Order = orderService.findOrder(id);
// 프레젠테이션 계층이 필요한 프록시 객체를 강제로 초기화
order.getMember().getName();
return order;
}
class OrderService{
public Order findOrder(id){
return ordeRepository.findOrder(id);
}
}
뷰를 개발할 때 필요한 엔티티를 미리 초기화 해두는 방법은 생각보다 요류가 발생할 가능성이 높다.
왜냐하면, 필요한 엔티티가 초기화된 상태인지 아닌지 확인하는 것은 상당히 번거롭고 놓치기 쉽기 때문이다.
영속성 컨텍스트를 뷰까지 열어둔다는 뜻이다.
이렇게 되면 뷰에서도 지연 로딩을 사용할 수 있다.
위의 방식에서 단점을 보완해 비즈니스 계층에서만 트랜잭션을 유지하는 방식의 OSIV
@Transactional
로 트랜잭션을 시작할 때 미리 생성해둔 영속성 컨텍스트를 찾아와서class MemberController {
public String viewMember(Long id) {
Member member = memberService.getMember(id);
member.setName("XXX"); // 보안상의 이유로 고객 이름을 XXX로 변경했다.
memberService.biz(); // 비즈니스 로직
return "view";
}
}
class MemberService {
@Transactional
public void biz() {...}
}
스프링 OSIV의 특징
스프링 OSIV의 단점
OSIV vs FACADE vs DTO
OSIV를 사용하는 방법이 만능은 아니다
Collection<Member> members = new ArrayList<>();
@OneToMany(mappedBy = "parent")
Collection<Child> children = new ArrayList<>();
//또는
@OneToMany(mappedBy = "parent")
List<Child> children = new ArrayList<>();
@OneToMany(mappedBy = "parent")
Set<Child> children = new HashSet<>();
@OrderColumn
을 추가하면 순서가 있는 특수한 컬렉션으로 인식한다.PersistentList
를 사용한다.@OrderColumn(name = "POSITION")
POSITION
컬럼에 보관한다.POSITION
컬럼은 Comment
테이블에 매핑된다.@Entity
class Board{
@Id @GeneratedValue
private Integer id;
@OneToMany(mappedBy = "board")
@OrderColumn(name = "POSITION")
private List<Comment> comments = new ArrayList<>();
}
@Entity
class Comment{
@Id @GeneratedValue
private Integer id;
@ManyToOne
@JoinColumn(name = "BOARD_ID")
private Board board;
}
@OrderColumn
은 Board
엔티티에서 매핑하므로 Comment
는 POSITION
의 값을 알 수 없다.List
를 변경하면 연관된 많은 위치 값을 변경하기 위한 추가 쿼리 발생POSITION
값이 null
이면 컬렉션을 순회할 때 NullPointerException
발생@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
@OrderBy("username desc, id asc")
private Set<Member> members = new HashSet<Member>();
...
}
boolean
타입 필드를 데이터베이스에 숫자 대신 Y, N 으로 저장할 수 있다.varchar
타입이어야 한다.@Entity
public class Member {
@Id
private String id;
private String username;
@Convert(converter=BooleanToYNConverter.class)
private boolean vip;
...
}
AttributeConverter<from, to>
를 구현해야 한다.convertToDatabaseColumn()
convertToEntityAttribute()
@Converter
class BooleanToYNConverter implements AttributeConverter<Boolean, String>{
@Override
public String convertToDatabaseColumn(Boolean attribute){
return (attribute != null && attribute) ? "Y" : "N";
}
@Override
public Boolean convertToEntityAttribute(String dbData){
return "Y".equals(dbData);
}
}
@Converter(autoApply = true)
를 적용한다.@Converter(autoApply = true)
class BooleanToYNConverter implements AttributeConverter<Boolean, String>{
// ...
}
PostLoad
refresh
를 호출한 후(2차 캐시에 저장되어 있어도 호출된다.)PrePersist
persist()
메소드를 호출해서 엔티티를 영속성 컨텍스트에 관리하기 직전에 호출된다.PreUpdate
flush
나 commit
을 호출해서 엔티티를 데이터베이스에 수정하기 직전에 호출된다.PreRemove
remove()
메소드를 호출해서 엔티티를 영속성 컨텍스트에서 삭제하기 직전에 호출된다. 또한 삭제orphanRemoval
에 대해서는 flush
나 commit
PostPersist
flush
나 commit
을 호출해서 엔티티를 데이터베이스에 저장한 직후에 호출된다. 식별자가 항상 존재한다. 참고로 식별자 생성 전략이 IDENTITY
면 식별자를 생성하기 위해 persist()
를 호출하면서persist()
를 호출한 직후에 바로 PostPersist
가 호출된다.PostUpdate
flush
나 commit
을 호출해서 엔티티를 데이터베이스에 수정한 직후에 호출된다.PostRemove
flush
나 commit
을 호출해서 엔티티를 데이터베이스에 삭제한 직후에 호출된다.엔티티에 이벤트가 발생할 때마다 어노테이션으로 지정한 메소드가 실행된다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String name;
@PostLoad
public void PostLoad(){
System.out.println("PostLoad");
}
@PrePersist
public void prePersist(){
System.out.println("prePersist");
}
@PreUpdate
public void PreUpdate(){
System.out.println("PreUpdate");
}
@PreRemove
public void PreRemove (){
System.out.println("PreRemove ");
}
@PostPersist
public void PostPersist (){
System.out.println("PostPersist ");
}
@PostUpdate
public void PostUpdate (){
System.out.println("PostUpdate ");
}
@PostRemove
public void PostRemove(){
System.out.println("PostRemove ");
}
}
리스너는 대상 엔티티를 파라미터로 받을 수 있다.
반환 타입은 void로 설정
@Entity
@EntityListeners(DuckListener.class)
public class Duck {
...
}
public class DuckListener {
@PrePersist
private void prePersist(Object obj) {
...
}
// 위와 같이 나머지 이벤트 오버라이딩
}
name
: 엔티티 그래프의 이름attributeNodes
: 함께 조회할 속성 선택@NamedAttributeNode
를 사용하고 그 값으로 함께 조회할 속성을 선택하면 된다.@NamedEntityGraph(name = "Order.withMember", attributeNodes = {
@NamedAttributeNode("member")
})
@Entity
public class Order {
@Id
@GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "MEMBER_ID")
private Member member;
}
em.getEntityGraph()
를 통해서 찾아오면 된다.javax.persistence.fetchgraph
사용, 값으로 찾아온 엔티티 그래프를 사용EntityGraph graph = em.getEntityGraph("Order.withMember");
Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", graph);
Order order = em.find(Order.class, orderId, hints);
Order
→ OrderItem
→ Item
과 같이 연달아서 엔티티 그래프를 조회할 경우 사용Order → Member
, Order → OrderItem
, OrderItem → Item
@NamedEntityGraph(name = "Order.withAll", attributeNodes = {
@NamedAttributeNode("member"),
@NamedAttributeNode(value = "orderItems", subgraph = "orderItems")
},
subgraphs = @NamedSubgraph(name = "orderItems", attributeNodes = {
@NamedAttributeNode("item")
})
)
@Entity
public class Order {
...
}
setHint
를 통해 힌트를 추가해주면 된다.List<Order> resultList =
em.createQuery("select o from Order o where o.id = :orderId",
Order.class)
.setParameter("orderId", orderId)
.setHint("javax.persistence.fetchgraph", em.getEntityGraph("Order.withAll"))
.getResultList();
createEntityGraph()
메소드를 사용하면 된다.EntityGraph<Order> graph = em.createEntityGraph(Order.class);
graph.addAttributeNodes("member");
Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", graph);
Order order = em.find(Order.class, orderId, hints);
//subgraph 사용
EntityGraph<Order> graph = em.createEntityGraph(Order.class);
graph.addAttributeNodes("member");
Subgraph<OrderItem> orderItems = graph.addSubgraph("orderItems");
orderItems.addAttributeNodes("item");
Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", graph);
Order order = em.find(Order.class, orderId, hints);
fetchgraph
는 지정한 속성만 함께 조회loadgraph
는 지정한 속성뿐만 아니라 즉시로딩으로 설정된 연관관계도 함께 조회한다.