프록시와 연관관계 관리
JPA의 즉시 로딩과 지연 로딩을 이해하기 위해서는 먼저 Proxy에 대해 이해해야합니다.
프록시
프록시 클래스는 실제 클래스를 상속받아서 만들어지며 겉 모양이 같습니다.
다만 실제 값이 필요할때까지 DB 조회를 미룰 수 있어서, 한 Entity와 연관된 다른 Entity들을 모두 가져올 필요 없을때 프록시를 사용합니다.
프록시 객체는 실제 객체의 참조를 보관을 해서, 애플리케이션에서 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출하게 됩니다.
Team team = new Team();
team.setName("teamA");
Member member = new Member();
member.setName("memberA");
member.setTeam(team);
em.persist(team);
em.persist(member);
em.flush(); // SQL 보냄
em.clear(); // 영속성 Context 1차 캐시 초기화
Member referMember = em.getReference(Member.class, member.getId()); // 프록시 객체 가져옴
System.out.println("referMember = " + referMember.getId() + ": " + referMember.getName());
// referMember.getId() 값은 메모리에 있던 값이기 때문에 DB 조회를 하지 않는다.
// referMember.getName() 값을 요청할 때 비로소 DB에 SELECT query를 보내서 값을 가져온다.
프록시 객체가 메서드를 호출하기 위해 실제 객체의 참조를 갖기 위해서 영속성 컨텍스트를 통해 DB를 조회합니다.
가져온 정보로 실제 Entity를 생성하고, 프록시 객체가 해당 Entity를 가르키도록 설정합니다.
그리고 프록시 특징은 다음과 같습니다.
프록시 객체는 처음 사용할 때 한번만 초기화
프록시 객체 초기화시, 프록시 객체가 실제 Entity로 바뀌는것이 아닌 참조를 통해 동작
프록시 객체는 원본 Entity를 상속받는 상태이므로 타입체크시
==
대신instance of
를 사용하는것이 좋음실제 Entity를 조회 후 프록시 객체를 조회하는 경우
Member findMember = em.find(Member.class, member.getId()); System.out.println("findMember.getClass() = " + findMember.getClass()); // 실제 Entity Member referMember = em.getReference(Member.class, member.getId()); System.out.println("referMember.getClass() = " + referMember.getClass()); // 실제 Entity System.out.println("isEqualClass ? = " + (referMember.getClass() == findMember.getClass())); // print "isEqualClass ? = true"
프록시 객체를 조회후 실제 Entity를 조회하는 경우JPA는 한 트랜잭션 내에서 실제 Entity 객체와 프록시 객체의 비교 연산 동작의
완전성을 보장하기 위해, 프록시 객체 조회 후 실제 Entity를 조회하는 경우라도,
두 객체가 모두 프록시 객체를 반환받도록 합니다.System.out.println("instanceof = " + (referMember instanceof Member)); // true System.out.println("instanceof = " + (findMember instanceof Member)); // true
따라서 두 객체의 클래스 타입은 동일
Member referMember = em.getReference(Member.class, member.getId()); System.out.println("referMember.getClass() = " + referMember.getClass()); // 프록시 객체 Member findMember = em.find(Member.class, member.getId()); System.out.println("findMember.getClass() = " + findMember.getClass()); // 프록시 객체 System.out.println("isEqualClass ? = " + (referMember.getClass() == findMember.getClass())); // print "isEqualClass ? = true"
영속성 컨텍스트에 찾고자 하는 Entity가 이미 있다면,
em.getRefrence()
하더라도 실제 Entity가 반환됨영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태인 경우, 프록시 객체를 초기화
하려하면 Exception이 발생함Member referMember = em.getReference(Member.class, member.getId()); em.detach(referMember); // 영속성 Context에서 분리 System.out.println("referMember = " + referMember.getId() + ": " + referMember.getName());
Member referMember = em.getReference(Member.class, member.getId());
System.out.println("isLoaded ? = " + emf.getPersistenceUnitUtil().isLoaded(referMember));
// print "isLoaded ? = false"
Hibernate.initialize(referMember); // 프록시 객체 강제 초기화
System.out.println("isLoaded ? = " + emf.getPersistenceUnitUtil().isLoaded(referMember));
// print "isLoaded ? = true"
System.out.println("referMember = " + referMember.getId() + ": " + referMember.getName());
// 강제 초기화를 이미 했기 때문에 getName()하더라도 DB에 query가 보내지지 않고 1차 캐시에서 값을 가져옴
즉시 로딩과 지연 로딩
지연 로딩
Member member = em.find(Member.class, 1L); // Member의 id, name Field만을 가져옴 System.out.println("member = " + member.getId() + ": " + member.getName()); Team team = member.getTeam(); // 연관관계에 관한 값을 요청할 경우, 그때서야 DB에 query를 보내서 team Field를 가져옴 System.out.println("team = " + team.getId() + ": " + team.getName());
@Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String name; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "TEAM_ID") private Team team; }
즉시 로딩
Member member = em.find(Member.class, 1L); // Member의 모든 Field를 다 가져옴 System.out.println("member = " + member.getId() + ": " + member.getName()); Team team = member.getTeam(); // 단순히 member 객체의 Field에서 참조 System.out.println("team = " + team.getId() + ": " + team.getName());
@Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String name; @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "TEAM_ID") private Team team; }
실무에서는 가급적 지연 로딩만 사용하는게 권장
즉시 로딩을 적용하면 예상치 못한 SQL이 발생하고, 특히 JPQL에서 N+1 문제를 일으킴
**@ManyToOne
, @OneToOne
의 경우 기본값이 즉시 로딩이므로, 지연 로딩으로 설정해야**함
영속성 전이
특정 Entity를 영속 상태로 만들때, 연관된 Entity도 함께 영속 상태로 만들고 싶을때 사용하는 방법.
영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없고, 단지 Entity를 영속화할때
연관된 Entity도 함께 영속화 하는 편리함을 제공함.
고아 객체
부모 Entity와 연관관계가 끊어진 자식 Entity를 의미
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
List<Member> members = new ArrayList<>();
public void addMember(Member member) {
this.members.add(member);
member.setTeam(this);
}
}
Member member1 = new Member();
Member member2 = new Member();
Team team = new Team();
team.setName("teamA");
team.addMember(member1);
team.addMember(member2);
em.persist(team);
// Team의 members Field는 전이 설정이 되어있기 때문에, team을 영속화하면 list에 속한 member도 영속화된다.
team.getMembers().remove(0);
// 부모 Entity에서 첫번째 자식 Entity와의 연관관계를 끊었으므로 member1은 고아 객체가 된다.
// orphanRemval = true 설정이 되어있기 때문에 고아 객체는 자동으로 삭제된다.
영속성 전이와 고아 객체의 생명주기
- 두 개념은 특정 Entity만이 해당 Entity를 소유하는 경우에만 사용
그렇지 않은 경우, 다른 Entity에서 예상치 못하게 추가되거나 삭제될 수 있기 때문 - 두 개념을 모두 사용하면 부모 Entity를 통해서 자식의 생명주기를 관리할 수 있게 되어
도메인 주도 설계의Aggregate Root
개념을 구현할 때 유용
값 타입
JPA 데이터 타입은 크게 2가지
- Entity 타입
@Entity
로 정의하는 객체- 데이터가 변해도 식별자를 통해 지속해서 추적 가능
- 값 타입
- 단순히 값으로 사용하는 자바 기본 타입 / 객체
- 식별자가 없고 값만 있으므로 변경시 추적 불가
- 값 타입을 소유한 Entity에 생명주기를 의존
기본 값 타입
,Embedded 타입
,Collection 값 타입
등으로 분류
기본 값 타입
int age; // 자바 기본 타입(primitive type)
Integer count; // Wrapper 클래스
String name;
자바 기본 타입, Wrapper 클래스, String 등이 있고, 기본 값 타입의 생명주기는 Entity에 의존
값 타입은 외부에 공유하면 안됨
Embedded 타입
주로 기본 값 타입을 모아서, 새로운 값 타입을 직접 정의하는 것을 의미
재사용이 가능하고 응집도가 높음
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
public Address() {
}
}
@Entity
public class Member {
...
@Embedded
private Address homeAddress;
}
값 타입을 정의하는 곳에 @Embeddable
그리고 값 타입을 사용하는 곳에 @Embedded
로 표현
Embedded 타입으로 사용할 클래스에는 기본 생성자가 필수로 존재해야함
Embedded 타입은 Entity의 값일 뿐이므로, 매핑하는 테이블은 변함이 없어야 함
불변 객체
값 타입을 여러 Entity에서 공유하면 예상치 못한 부작용이 발생할 수 있음
자바 기본 타입에 값을 대입하면 항상 복사가 되지만, Embedded 타입과 같이 직접 정의 한 값
타입은 객체 타입이기 때문에 값을 대입하면 참조 값이 공유됨
공유되더라도 값을 바꿀 수 없도록 불변 객체로 설정함으로써 부작용을 막을 수 있음
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
public Address() {
}
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
private void setCity(String city) {
this.city = city;
}
private void setStreet(String street) {
this.street = street;
}
private void setZipcode(String zipcode) {
this.zipcode = zipcode;
}
}
불변 객체는 생성 시점 이후 절대 값을 변경할 수 없는 객체
생성자로만 값을 설정하고, Setter를 만들지 않거나 private으로 구현 가능
Collection 값 타입
값 타입을 하나 이상 저장할 때, List나 Set과 같은 Collection을 사용함
하지만 DB에는 Collection을 하나의 테이블에 저장할 수 없기 때문에 Collection을 저장하기
위한 별도의 테이블이 필요
@Entity
public class Member {
...
@Embedded
private Address homeAddress;
@ElementCollection
@CollectionTable(
name = "ADDRESS",
joinColumns = @JoinColumn(name = "MEMBER_ID")
)
private List<Address> addressHistory = new ArrayList<>();
}
Collection을 위한 테이블은 원래 Entity의 PK를 기준으로 JOIN 함
하지만 Collection 값 타입은 다음과 같은 제약 사항때문에 권장하지 않음
- Entity와는 달리 식별자 개념이 없음
- 변경시 추적하기 어려움
- Collection에 변경 사항이 발생하면, 주인 Entity와 연관된 모든 데이터를 삭제하고,
값 타입 Collection에 있는 현재 값을 모두 다시 저장 - Collection을 매핑하는 테이블은 null값을 허용하면 안되고, 중복 저장 방지를 위해
모든 Column을 묶어서 PK를 구성해야함
실무에서는 1:N 연관관계 설정을 권장
@Entity
public class AddressEntity {
@Id
@GeneratedValue
private Long id;
private Address address;
}
@Entity
public class Member {
...
@Embedded
private Address homeAddress;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
}
'BE > Java' 카테고리의 다른 글
[Spring Security] 스프링 시큐리티에 발을 담가보자 (개념정리) (2) | 2023.12.19 |
---|---|
JPA Mapping - JPA 연관관계 매핑에 대해서 알아보자 (0) | 2023.07.29 |
JPQL ( Java Persistence Query Language ) - JPQL에 대해서 알아보자 (0) | 2023.07.23 |
JPA (Java Persistence API) - JPA에 대해서 알아보자 (0) | 2023.07.23 |
DipsatcherServlet이란? (0) | 2023.07.21 |