반응형

@JsonManagedReference와 @JsonBackReference 어노테이션 사용

  • Jackson 2.0 버전 이전에 순환 참조를 해결하기 위해 사용했던 어노테이션입니다.
  • @JsonManagedReference
    • 양방향 관계에서 정방향 참조할 변수에 어노테이션을 추가하면 직렬화에 포함된다.
  • @JsonBackReference
    • 양방향 관계에서 역방향 참조로 어노테이션을 추가하면 직렬화에서 제외된다.

Customer 객체에서 Order 객체에 @JsonManagedReference를 추가하고 Order에서는 Customer 객체에 @JsonBackReference 어노테이션을 추가하여 직렬화에서 Customer 객체를 제외 시켰습니다.

@Setter
@Getter
@ToString
public class Customer {
    private int id;
    private String name;
    @JsonManagedReference //serialized될 때 포함됨
    private Order order;
}

@Setter
@Getter
@ToString(exclude = "customer”) //toString() 실행시에도 무한 재귀가 발생하여 제외시킨다
public class Order {
    private int orderId;
    private List<Integer> itemIds;
    @JsonBackReference //serialization에서 제외된다
    private Customer customer;
}

@Test
public void infinite_recursion_해결책_JsonManagedReference_JsonBackReference() throws JsonProcessingException {
    Order order = new Order();
    order.setOrderId(1);
    order.setItemIds(List.of(10, 30));

    Customer customer = new Customer();
    customer.setId(2);
    customer.setName("Frank");
    customer.setOrder(order);
    order.setCustomer(customer);

    log.info("customer(toString) : {}", customer);
    log.info("customer(serialized json) : {}", objectMapper.writeValueAsString(customer));
    log.info("order(serialized json) : {}", objectMapper.writeValueAsString(order)); //customer정보는 제외된다
}

실행 화면

@JsonBackReference 어노테이션 선언으로 Order 객체에서 Customer 객체에 대한 정보는 빠지게 됩니다.

@JsonIdentityInfo - 추천 방식

  • Jackson 2.0 이후부터 새롭게 추가된 어노테이션입니다. @JsonIdentityInfo 어노테이션을 추가해서 직렬화에 포함 시킬 속성 값을 'property' 속성에 지정합니다.
  • @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class)
    • generator = ObjectIdGenerators.PropertyGenerator.class 클래스는 순환 참조시 사용할 Id를 생성하는데 사용되는 클래스이다.
  • @JsonIdentityInfo(property="id")
    • property 속성은 해당 클래스의 속성 이름을 지정한다.
    • 예제에서는 id는 Customer#id를 가리키고 직렬화/역직렬화 할 때 Order#customer의 역참조로 사용된다.

@JsonIgnore

제일 간단하게 해결할 수 있는 방법은 직렬화 할 때 순환 참조 되는 속성에 @JsonIgnore 어노테이션을 추가하여 직렬화에서 제외시키는 방법입니다.

@JsonIgnore
private Customer customer;

해당 customer 필드는 직렬화에서 제외되어 json 형태에 표시되지 않습니다.

반응형
반응형

@JoinColumn

  • 외래 키를 매핑할 때 사용한다.
속성 설명 기본 값
name 해당 Entity에서 매핑할 외래 키 이름 필드명 +_+ 참조하는 테이블의 기본 키 컬럼명
referencedColumnName 외래 키가 참조하는 대상 테이블의 컬럼명 참조하는 테이블의 기본 키 컬럼명
foreignKey(DDL) 외래 키 제약조건을 직접 지정할 수 있다. 이 속성은 테이블을 생성할 때만 사용한다.  
unique, nullable, insertable, updatable, columnDefinition, table @Column의 속성과 같다.  

@ManyToOne

  • 다대일 관계 매핑
속성 설명 기본 값
optional false로 설정하면 연관된 엔티티가 항상 있어야 한다. true
fetch 글로벌 패치 전략을 설정한다. @ManyToOne=FetchType.EAGER, @OneToMany=FetchType.LAZY
cascade 속성 전이 기능을 사용한다.  
targetEntity 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다.  

@OneToMany

  • 다대일 관계 매핑
속성 설명 기본 값
mappedBy 연관관계의 주인 필드를 선택한다.  
fetch 글로벌 패치 전략을 설정한다. @ManyToOne=FetchType.EAGER, @OneToMany=FetchType.LAZY
cascade 속성 전이 기능을 사용한다.  
targetEntity 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다.  
반응형
반응형

지연 로딩(LAZY)

내부 메커니즘은 위의 그림과 같다.
로딩되는 시점에 Lazy 로딩 설정이 되어있는 Team 엔티티는 프록시 객체로 가져온다.
후에 실제 객체를 사용하는 시점에(Team을 사용하는 시점에) 초기화가 된다. DB에 쿼리가 나간다.
- getTeam()으로 Team을 조회하면 프록시 객체가 조회가 된다.
- getTeam().getXXX()으로 팀의 필드에 접근할 때 쿼리가 나간다.

대부분 비즈니스 로직에서 Member와 Team을 같이 사용한다면?

  • 이런 경우 LAZY 로딩을 사용한다면 SELECT 쿼리가 따로따로 2번 나간다.
  • 네트워크를 2번 타서 조회가 이루어 진다는 이야기이므로 손해다.
  • 이때는 JPQL의 fetch join을 통해서 해당 시점에 한방 쿼리로 가져와서 쓰도록 한다.

즉시 로딩(EAGER)

fetch 타입을 EAGER로 설정하면 된다. (@ManyToOne, @OneToOne... @XXXToOne은 기본이 EAGER)
대부분의 JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회하려고 한다.
이렇게 하면 실제 조회할 때 한방 쿼리로 다 조회해온다.
실행 결과를 보면 Team 객체도 프록시 객체가 아니라 실제 객체이다.

프록시와 즉시 로딩 주의할 점

  • 실무에서는 가급적 지연 로딩만 사용한다.
  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다.
    • @ManyToOne이 5개 있는데 전부 EAGER로 설정되어 있다고 해보자.
      • 조인이 5개 일어난다. 실무에선 테이블이 더 많다
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다
    • 실무에서 복잡한 쿼리를 많이 풀어내기 위해서 JPQL을 많이 사용한다.
    • EAGER는 반환하는 시점에 다 조회가 되어 있어야 한다. 따라서 Member를 다 가져오고 나서 그 Member와 연관된 Team을 다시 다 가져온다.
    • ex) 멤버가 2명이고 팀도 2 개다. 각각 다른 팀이다.
Team team1 = new Team();
team1.setName("teamA");
em.persist(team1);
​
Team team2 = new Team();
team2.setName("teamB");
em.persist(team2);
​
Member member1 = new Member();
member1.setUsername("memberA");
em.persist(member1);
member1.changeTeam(team1);
​
Member member2 = new Member();
member2.setUsername("memberB");
em.persist(member2);
member2.changeTeam(team2);
​
em.flush();
em.clear();
​
List<Member> members = em
                .createQuery("select m from Member m", Member.class)
  .getResultList();
​
tx.commit();

실행 결과를 보면

  1. 멤버를 조회해서 가져온다
  2. Member들의 Team이 채워주기 위해 TEAM을 각각 쿼리 날려서 가져온다.

여기서 N+1의 문제가 나온다.
-> 쿼리 1개를 날렸는데 그것 때문에 추가 쿼리가 N개 나간다는 의미이다.

Hibernate: 
    /* select
        m 
    from
        Member m */ select
            member0_.id as id1_4_,
            member0_.createdBy as createdB2_4_,
            member0_.createdDate as createdD3_4_,
            member0_.lastModifiedBy as lastModi4_4_,
            member0_.lastModifiedDate as lastModi5_4_,
            member0_.age as age6_4_,
            member0_.description as descript7_4_,
            member0_.locker_id as locker_10_4_,
            member0_.roleType as roleType8_4_,
            member0_.team_id as team_id11_4_,
            member0_.name as name9_4_ 
        from
            Member member0_
Hibernate: 
    select
        team0_.id as id1_8_0_,
        team0_.createdBy as createdB2_8_0_,
        team0_.createdDate as createdD3_8_0_,
        team0_.lastModifiedBy as lastModi4_8_0_,
        team0_.lastModifiedDate as lastModi5_8_0_,
        team0_.name as name6_8_0_ 
    from
        Team team0_ 
    where
        team0_.id=?
Hibernate: 
    select
        team0_.id as id1_8_0_,
        team0_.createdBy as createdB2_8_0_,
        team0_.createdDate as createdD3_8_0_,
        team0_.lastModifiedBy as lastModi4_8_0_,
        team0_.lastModifiedDate as lastModi5_8_0_,
        team0_.name as name6_8_0_ 
    from
        Team team0_ 
    where
        team0_.id=?

결론

  • 실무에서는 가급적 LAZY 로딩 전략을 사용하자.
  • 실무에서 대부분 MEMBER와 TEAM을 함께 사용한다면 JPQL의 fetch join을 통해서 해당 시점에 한방 쿼리로 가져와서 사용하라.
  • @ManyToOne, @OneToOne과 같이 @XXXToOne 어노테이션들의 기본이 즉시 로딩(EAGER)이다.
  • @OneToMany, @ManyToMany는 기본이 지연 로딩(LAZY)이다.
반응형
반응형

1. Entity

- DB의 테이블과 매칭 되는 개념

ex) DB 구조
CREATE TABLE Student (
s_number BIGINT(20) NOT NULL AUTO_INCREMENT,
name varchar(255) not null,
);

ex) Entity 구조

@Entity
public class Student{

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(nullable = false)
    private long sNumber;

    @Column(nullable = false)
    private String name;

    // getter, setter...

}

2. EntityManager

Entity를 관리하는 역할을 수행하는 클래스이다.
엔티티 매니저 내부에 영속성 컨텍스트(Persistence Context)라는 걸 두어서 엔티티들을 관리한다.

영속성 컨텍스트(Persistence Context)

영속성: 비휘발성
컨텍스트: 하나의 환경, 공간으로 이해

영속성 컨텍스트: 영속성 컨텍스트를 관리하는 모든 EntityManager가 초기화 및 종료되지 않는 한 Entity를 영구히 저장하는 환경

EntityManager와 영속성 컨텍스트의 Entity 관리

public class Join{

    public void save(String name){
        Student s = new Student();
        s.setName(name);

        // 엔티티 매니저가 있다고 가정.
        EntityManager em;
        EntityTransaction tx = em.getTransaction();

        try{
            // 엔티티 매니저에서 수행하는 모든 로직은 트랜잭션 안에서 수행되야 한다.
            tx.begin();

            // 이렇게 하면 해당 엔티티 매니저의 영속성 컨텍스트에 위에서 만든 student 객체가 저장된다.
            // 이제 student 엔티티는 엔티티 매니저의 관리 대상이 되고, 영속성을 가졌다고 말할 수 있다.
            em.persist(s);

            // 트랜잭션을 커밋한다.
            tx.commit();
        }catch(Exception e){
            // 오류가 났다면 트랜잭션을 롤백 시켜줘야한다.
            tx.rollback();
        }finally{
            // 더 이상 사용하지 않는 자원이므로 엔티티 매니저를 종료시켜줘야 한다.
            em.close();
        }

    }
}

트랜잭션(Transaction)

  • 하나의 작업 단위
    ex) 상품 결제
  1. 상품 재고 조회
  2. 사용자의 잔고 조회
  3. 상품 재고 -
  4. 사용자 잔고에서 해당 금액을 뺌
  5. 주문 완료

위 5가지 작업중 하나라도 오류가 발생하면 전체가 오류라고 보고 맨 처음 상태로 돌려야한다.
이렇게 오류가 났을 때 처음 상태로 돌아가는(rollback) 작업의 단위를 트랜잭션이라 한다.
트랜잭션이 모두 정상 수행됬을 때는 commit을 수행해서 작업 내용을 실제 DB와 엔티티 매니저에 반영한다.

쓰기 지연 SQL 저장소

  • 영속성 컨텍스트 안에는 쓰기 지연 SQL 저장소라는 공간이 따로 존재한다.
public class Join{
    public void save(String name){

        Student s1 = new Student();
        s.setName(name);

        Student s2 = new Student();
        s.setName(name);

        // 엔티티 매니저가 있다고 가정
        EntityManager em;
        EntityTransaction tx = em.getTransaction();

        try{
            // 엔티티 매니저에서 수행하는 모든 로직은 트랜잭션 안에서 수행되야 한다.
            tx.begin();

            //쿼리는 전송되지 않는다.
            em.persist(s1);
            em.persist(s2);

            // 커밋하는 시점에 쿼리가 전송된다.
            tx.commit();
        }catch(Exception e){
            // 오류가 났다면 트랜잭션을 롤백 시켜줘야한다.
            tx.rollback();
        }finally{
            // 더 이상 사용하지 않는 자원이므로 엔티티 매니저를 종료시켜줘야 한다.
            em.close();
        }

    }
}

트랜잭션을 사용하지 않는다고 가정하고코드 동작을 살펴보자.

  1. 쿼리가 두 번 날아갈 것이다.
  2. 만약에 s2를 저장하는 시점에서 롤백을 해야할 때는 날리지 않아도 될 s1 에 대한 삽입 쿼리를 날리게 된 격이다.

하지만 쓰기 지연 SQL 저장소 및 트랜잭션에 의해 아래와 같이 동작한다.

  1. 트랜잭션이 커밋 되기 직전까지 모든 쿼리문은 영속성 컨텍스트 내부의 쓰기 지연 SQL 저장소에 저장된다.
  2. 트랜잭션이 커밋되는 순간 모든 쿼리가 한 방에 날아간다.
  3. 만약 트랜잭션 내부에서 오류가 나서 롤백을 해야한다면 애초에 날리지도 않을 쿼리를 날리지도 않는다.

3. EntityManagerFactory

  • 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하므로 스레드 간에 절대 공유하면 안된다.
  • 동시성(Concurrency):
    사용자가 체감하기에는 동시에 수행하는 거처럼 보이지만 사실 사용자가 체감할 수 없는 짧은 시간단위로 작업들을 번갈아가면서 수행되는 것이다.
    ex) 각 스레드들이 동시에 동작하는 거 같지만 알고 보면 스레드들이 아주 짧은 시간마다 번갈아가면서 작업을 수행하고 있는 것이다.
  • 병렬(Parallelism)
    우리가 생각하는 진짜 동시에 실행하는 개념을 생각하면 된다.
    실제로 동시에 여러 작업이 수행되는 개념
    각 스레드들이 동시에 동작한다.

내가 데이터를 수정하고 있는데 다른 스레드에서 해당 데이터를 미리 수정해버리면 안되기 때문에
엔티티 매니저는 하나를 공유하면 안 되고, 상황에 따라 계속 만들어줘야한다.
엔티티 매니저를 만드는 것이 엔티티 매니저 팩토리이다.

public class Join{
    public void save(String name){

        Student s1 = new Student();
        s1.setName(name);

        // META-INF/persistence.xml에서 이름이 db인 persistence-unit을 찾아서 엔티티 매니저 팩토리를 생성
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("db");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        try{
            // 엔티티 매니저에서 수행하는 모든 로직은 트랜잭션 안에서 수행되야 한다.
            tx.begin();

            // 이렇게 하면 해당 엔티티 매니저의 영속성 컨텍스트 위에서 만든 student 객체가 저장된다.
            // 이제 student 엔티티 매니저의 관리 대상이 되고, 영속성을 가졌다고 말할 수 있다.
            em.persist(s1);

            // 트랜잭션을 커밋한다.
            tx.commit();

        }catch(Exception e){
            // 오류가 났다면 트랜잭션 롤백 수행
            tx.rollback();
        }fianlly{
            // 더 이상 사용하지 않는 자원이므로 엔티티 매니저를 종료시켜줘야 한다.
            em.close();
        }

    }
}

Entity Manager Factory에서 제품(Entity Manager)를 찍어내는 개념
Entity Manager Factory는 Entity Manager와 달리 여러 스레드가 동시에 접근해도 안전하다.
단순히 Entity Manager만 찍기 때문

Entity Manager Factory를 짓는 비용은 크다
따라서 Entity Manager Factory는 DB당 하나 밖에 사용하지 않는다.

반응형

+ Recent posts