글 검색

공개된 글의 제목과 본문을 검색합니다.

    닫기 Esc

    1. 프록시#

    • 엔티티를 조회할 때 연관된 엔티티들이 항상 사용되는 것은 아니다.
    • 연관관계의 엔티티는 비즈니스 로직에 따라 사용될 때도 있지만 그렇지 않을 때도 있다.
    java
    // Member 엔티티
    @Entity
    public class Member{
    		private String userName;
    		
    		@ManyToOne
    		private Team team;
    		...
    }
    
    // Team 엔티티
    @Entity
    public class Team{
    		private String teamName;
    		...
    }
    java
    // CASE 1. Member, Team 객체 조회 필요
    public void printUserAndTeam(String memberId) {
    		Member member = em.find(Member.class, memberId);
    		Team team = member.getTeam();
    		System.out.println("회원 이름: " + member.getUsername());
    		System.out.println("소식팀: " + team.getName()); // team 객체 조회
    }
    
    // CASE 2. Member 객체 조회 필요
    public void printUser(String memberId) {
    		Member member = em.find(Member.class, memberId);
    		Team team = member.getTeam();
    		System.out.println("회원 이름: " + member.getUsername());
    }
    • JPA는 이런 문제를 해결하려고 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공한다.
      • 이것을 지연 로딩이라 한다.
    • 그런데 지연 로딩 기능을 사용하려면 실제 엔티티 객체 대상에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이것을 프록시 객체라 한다.

    1.1. 프록시 기초#

    • JPA에서 식별자로 엔티티 하나를 조회할 때 영속성 컨텍스트는 엔티티가 없으면 데이터 베이스를 조회한다.
      java
      Member member = em.find(Member.class,"member1");
    • 직접 엔티티를 조회하면 실제 사용 유무와 무관하게 데이터베이스에서 조회하게 된다.
      • 실제 사용 시점까지 데이터베이 조회를 미루고싶다면 EntityManager.getReference() 메소드를 사용하면 된다.

    1.1.1. 프록시의 특징#

    • 프록시는 실제 클래스를 상속받아 만들어지기 때문에 실제 클래스와 모양이 같다.
    • 사용자 입장에서는 프록시인지 아닌지 구분하지 않고 사용해도 된다.
    • 프록시 객체는 실제 객체에 대한 참조(target)를 보관한다.
      • 객체 메서드 호출 시 실제 객체의 메서드를 호출한다.

    1.1.2. 프록시 객체의 초기화#

    • 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
      • 이를 프록시 객체 초기화라 한다.

    1.1.3. 프록시의 특징#

    • 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
    • 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있다.
    • 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 한다.
    • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없으므로 em.getReference()를 호출해도 프록시가 아닌 실제 엔티티를 반환한다.
    • 초기화는 영속성 컨텍스트의 도움을 받아야 가능하다. 따라서 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태의 프록시를 초기화하면 문제가 발생한다. 하이버네이트는 org.hibernate.LazyInitializationException 예외를 발생시킨다.

    1.2. 프록시와 식별자#

    • 프록시 객체는 식별자 값을 가지고 있으므로 식별자 값을 조회하는 team.getId()를 호출 해도 프록시를 초기화하지 않는다.
      • 단, @Access(AccessType.PROPERTY)로 설정한 경우에만 초기화하지 않는다.
    • 엔티티 접근 방식을 필드 (@Access(AccessType.FIELD))로 설정하면 JPA는 getId() 메소드가 id만 조회하는 메소드인지 다른 필드까지 활용해서 어떤 일을 하는 메소드인지 알지 못하므로 프록시 객체를 초기화한다.
    • 프록시는 연관관계를 설정할때 DB 접근을 줄일 수 있다.

    1.3. 프록시 확인#

    • PersistenceUnitUtil.isLoaded(Object entity) 메소드를 사용하면 프록시 인스턴스의 초기화 여부를 확인할 수 있다.
    • 하이버네이트의 initialize() 메소드를 사용하면 프록시를 강제로 초기화할 수 있다.

    2. 즉시 로딩과 지연 로딩#

    • Member 테이블에 Team 테이블을 외래키로 가지고 있다고 할때 Member 엔티티를 호출하면 연관 엔티티인 Team 엔티티도 DB에서 같이 가져와야할까?
      • JPA는 엔티티의 조회 시점을 선택할수 있도록 해준다.
    • 즉시 로딩 : 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
      • 설정 방법 : @ManyToOne(fetch = FetchType.EAGER)
    • 지연 로딩 : 연관된 엔티티를 실제 사용할 때 조회한다.
      • 설정 방법 : @ManyToOne(getch = FetchType.LAZY)

    2.1. 즉시 로딩#

    java
    @Entity
    public class Member{
    		...
    		@ManyToOne(fetch = FetchType.EAGER)
    		@JoinColumn(name = "TEAM_ID")
    		private Team team;
    		...
    }
    
    // 즉시 로딩
    Member member = em.find(Member.class,"member1");
    Team team = member.getTeam(); // 객체 그래프 탐색
    • 즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용한다.
      sql
      SELECT
      	M.MEMBER_ID AS MEMBER_ID,
      	M.TEAM_ID AS TEAM_ID,
      	M.USERNAME AS USERNAME,
      	T.TEAM_ID AS TEAM_ID,
      	T.NAME AS NAME
      FROM
      	MEMBER M LEFT OUTER JOIN TEAM T
      		ON M.TEAM_ID=T.TEAM_ID
      WHERE
      	M.MEBER_ID='member1'
      • 여기서는 회원과 팀을 조인해서 쿼리 한번으로 두 엔티티를 모두 조회한다.

    2.2. 지연 로딩#

    java
    @Entity
    public class Member{
    	...
    	@ManyToOne(fetch = FetchType.LAZY)
    	@JoinColumn(name = "TEAM_ID")
    	private Team team;
    	...
    }
    
    // 지연 로딩
    Member member = em.find(Member.class,"member1");
    Team team = member.getTeam(); // 객체 그래프 탐색
    team.getName(); // 팀 객체 실제 사용
    • 지연 로딩은 실제 객체가 사용되지 전까지 로딩을 미룬다.
    • 실제 데이터가 필요한 순간이 되엇야 데이터베이스를 조회해서 프록시 객체를 초기화한다.

    2.3. 즉시 로딩, 지연 로딩 정리#

    • 즉시 로딩을 사용하면 처음부터 연관된 모든 엔티티를 영속성 컨텍스트에 올려두므로 현실적이지 않다.
    • 연관된 엔티티를 즉시/지연 로딩하는것이 좋을지는 상황에 따라 다르다.
    • 즉시 로딩
      • 연관 엔티티가 적고 연관 엔티티들이 거의 같이 사용된다.
    • 지연 로딩
      • 연관 엔티티가 굉장히 많고 거의 같이 사용되지 않는다.

    3. 지연 로딩 활용#

    • 사내 주문관리 시스템을 개발한다고 가정해보자
    • 얼마나 같이 사용되는지에 따라 로딩 전략을 구성한다.
      • Member - Team : 거의 같이 사용, 즉시 로딩, N:1
      • Member - Order : 서로 가끔 사용, 지연 로딩, 1:N
      • Order - Product : 거의 같이 사용, 즉시 로딩, N:1
    java
    @Entity
    public class Member{
    		@Id
    		private String id;
    		private String username;
    		private Integer age;
    		
    		@ManyToOne(fetch = FetchType.EAGER)
    		private Team team;
    		
    		@OneToMany(mappedBy = "member",fetch = FetchType.LAZY)
    		private List<Order> orders;
    		...
    }
    회원 조회
    회원 조회
    • 회원과 팀은 즉시로딩으로 설정되어 조회시 함께 조인되어 쿼리가 발생한다.
    • 반면 주문 내역은 지연 로딩이기 때문에 필요시 실제 DB에 내용을 질의한 뒤 초기화된다.

    3.1. 프록시와 컬렉션 래퍼#

    주문내역 조회
    주문내역 조회
    java
    Member member = em.find(Member.class,"member1");
    List<Order> orders = member.getOreders();
    System.out.println("orders = "+orders.getClass().getName());
    // orders = org.hibernate.collection.internal.PersistentBag
    • 하이버네이트는 엔티티를 영속 상태로 만들때 엔티티에 컬렉션이 있으면 컬렉션을 추적하고 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경하고 이를 컬렉션 래퍼라고 한다.
    • 엔티티를 지연 로딩하면 프록시 객체를 사용하는데 컬렉션(List orders)은 컬렉션 래퍼가 지연 로딩을 처리해준다.

    3.2. JPA 기본 페치 전략#

    • fetch 속성의 기본 설정값은 다음과 같다.
      • @ManyToOne, @OneToOne : 즉시 로딩
      • @ManyToMany, @OneToMany : 지연 로딩
    • JPA의 기본 페치 전략은 연관된 엔티티가 하나면 즉시 로딩을, 컬렉션이면 지연 로딩을 사용한다.
      • 하지만 추천하는 방법은 모든 연관관계에 지연 로딩을 사용하는 것이다.

    3.3. 컬렉션에 FetchType.EAGER 사용 시 주의점#

    • 컬렉션을 하나 이상 즉시 로딩하는 것은 권장하지 않는다.
      • 예를 들어 A 테이블을 N, M 두 테이블과 일대다 조인하면 SQL 실행 결과가 N 곱하기 M이 되면서 너무 많은 데이터를 반환할 수 있고 결과적으로 애플리케이션 성능이 저하될 수 있다.
        • 따라서 2개 이상의 컬렉션을 즉시 로딩으로 설정하는 것은 권장하지 않는다.
    • 컬렉션 즉시 로딩은 항상 외부 조인을 사용한다.
      • 데이터베이스 제약조건으로 내부 조인으로 인해 검색이 되지 않는 상황을 막을 수는 없다.
        • 따라서 JPA는 일대다 관계를 즉시 로딩할 때 항상 외부 조인을 사용한다.
    • FetchType.EAGER 설정과 조인 전략
      • @ManyToOne, @OneToOne
        • optional=false : 내부 조인
        • optional=true : 외부조인
      • @ManyToMany, @OneToMany
        • optional=false : 외부 조인
        • optional=true : 외부조인

    4. 영속성 전이: CASCADE#

    • 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶으면 영속성 전이 기능을 사용하면 된다.
    • JPA는 CASCADE 옵션으로 영속성 전이를 제공한다.

    4.1. 영속성 전이 : 저장#

    java
    @ManyToOne(optional = false, cascade = CascadeType.PERSIST)
    • 해당 옵션 사용 시 부모와 자식 엔티티를 한 번에 영속화할 수 있다.

    4.2. 영속성 전이 : 삭제#

    java
    @ManyToOne(optional = false, cascade = CascadeType.REMOVE)
    • 해당 옵션 사용 하여 부모 엔티티를 삭제할 경우 연관된 자식 엔티티도 함께 삭제된다.

    4.3. CASCADE의 종류#

    java
    public enum CascadeType {
        ALL, //모두 적용
        PERSIST, //영속
        MERGE, //병합
        REMOVE, //삭제
        REFRESH, //REFRESH
        DETACH //DETACH
    }
    • 여러 속성을 같이 사용할 수 있다.

    5. 고아 객체#

    • JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데 이것을 고아 객체(ORPHAN) 제거라 한다.
    • 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제되도록 가능하다.
    java
    @Entity
    public class Parent {
        @Id @GeneratedValue
        private Long id;
        
        @OneToMany(mappedBy = "parent", orphanRemoval = true)
        private List<Child> children = new ArrayList<Child>();
        ...
    }
    • 고아 객체 제거 기능은 영속성 컨텍스트를 플러시할 때 적용되므로 플러시 시점에 DELETE SQL이 실행된다.
    • 고아 객체 제거는 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다.
    • 고아 객체 제거에는 기능이 하나 더 있는데 개념적으로 볼때 부모를 제거하면 자식은 고아가 된다.
      • 부모를 제거하면 자식도 같이 제거할 수 있다.
      • CadecadeType.REMOVE를 설정하면 가능하다.

    6. 영속성 전이 + 고아 객체, 생명주기#

    • CascadeType.ALL + orphanRemoval = true를 동시에 사용하면 어떻게 될까?
      • 일반적으로 엔티티는 EntityManager.persist()를 통해 영속화되고 EntityManager.remove()를 통해 제거된다.
      • 이것은 엔티티 스스로 생명주기를 관리한다는 뜻이다.
      • 그런데 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다.
    • 영속성 전이는 DDD의 Aggregate Root개념을 구현할 때 사용하면 편리하다.
    전체 글 둘러보기