본문으로 바로가기

프록시와 연관관계 & 값 타입

category SPRING/JPA 2022. 3. 21. 06:56
728x90
반응형
SMALL

프록시와 연관관계 정리

프록시

=>DB를 바로 조회하지 않고 실제 사용시점에 DB를 조회

연관된 객체를 마음껏 탐색하는 목적

방법은 2가지

1.즉시로딩

2.지연로딩

연관된 객체를 함께 저장 또는 삭제 하는 기능 지원

=>영속성 전이, 고아객체제거

객체 탐색시 연관된 객체를 함께 조회하거나 혹은 연관된 객체를 제외하고 내가 원하는 객체만 조회하고 싶어

=>지연 로딩의 등장

지연로딩

엔티티가 실제 사용될 때 까지 조회를 지연하는 방법

지연로딩을 사용하려면 실제 엔티티 객체 대신 DB조회를 지연할 수 있는 가짜 객체인 프록시 객체가 필요

프록시 기초

Member member = em.find(Member.class, "member1");

해당 구문은 영속성 컨텍스트에 엔티티가 없으면 바로 DB를 조회

Member member = em.getReference(Member.class, "member1");

em.getReference() 메소드를 사용하여 실제 사용 시점까지 조회를 미룬다.

해당 메소드 사용시 JPA는 DB를 조회하지 않고 DB 접근을 위임한 프록시 객체를 반환한다.

프록시 객체는 실제 엔티티 클래스를 상속받고있음

프록시 객체의 초기화

Member member = em.getReference(Member.class, "member1");
member.getName(); // 실제 사용시 프록시 초기화 하여 실DB 접근

  • 프록시 특징
  1. 프록시 객체는 처음 사용시 한 번만 초기화
  2. 프록시 초기화 시 실제 엔티티에 접근 가능
  3. 원본 엔티티를 상속받았기에 타입체크에 유의
  4. 영속성 컨텍스트에 찾는 엔티티가 존재하면 em.getReference()메소드를 써도 프록시 객체가 아닌 실제 엔티티를 반환
  5. 준영속 상태의 프록시를 초기화하면 문제가 발생
프록시 확인

내가 조회한 엔티티가 진짜 엔티티인지 혹은 프록시로 조회한 것인지 확인하는 방법

초기화 여부

boolean isLoad = em.getEntityManagerFactory()
                    .getPersistenceUnitUtil().isLoad(entity);
System.out.println(isLoad); //초기화 여부 확인                    

실제 or 프록시 확인

System.out.println(member.getClass.getName());

출력결과가 javassit.. 라고 나오면 프록시 이다.

즉시로딩 과 지연로딩

  • 즉시로딩

@ManyToOne(fetch = FetchType.EAGER)

엔티티를 조회할 때 연관된 엔티티도 함께 조회된다.

대부분의 JPA 구현채눈 즉시 로딩을 최적화 하기 위해 가능하면 조인 쿼리를 사용한다.

즉시 로딩시 내부조인이 아닌 외부조인이 사용된다.

WHY?

서로 연관관계인 회원과 팀이 있다는 가정 하에

회원 조회시 팀에게 소속되지 않은 회원까지 같이 조회할려면 이 상황에서 외부조인이 성능상 유리하다.

내부조인을 사용하려면 외래키에 NOT NULL 제약조건을 설정하면 내부조인이 사용된다.

결국 선택적 관계이면 외부조인 필수적 관계이면 내부조인이 사용되는 것이다.

내부조인

(optional = false)

외부조인

(optional = true)
  • 지연로딩

@ManyToOne(fetch = FetchType.LAZY)

연관된 엔티티를 실제 사용할 때 조회한다.

조회대상이 영속성 컨텍스트에 이미 있으면 프록시 객체가 아닌 실제 객체가 반환된다.

JPA 기본 페치 전략

연관된 엔티티가 하나면 즉시로딩

연관된 엔티티가 컬렉션이면 지연로딩

컬렉션을 로딩하는것은 비용이 많이들고 실수로 많은 데이터를 로딩할 수 있기때문

대게 추천 방법은 모든 연관관계에 지연 로딩을 사용하구 상황에 따라서 즉시로딩을 사용하는것

영속성 전이:CASCADE

부모엔티티 저장시 자식엔티티도 함께 저장되는것

부모

@Entity
public class Parent{

    @Id@GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
    private List<Child>children = new ArrayList<Child>();
    ...
}

자식

@Entity
public class Child{

    @Id@GeneratedValue
    private Long id;

    @ManyToOne
    private Parent parent;
    ...
}

CASCADE 저장코드

private static void saveWithCascade(EntityManager em){

    Child child1 = new Child();
    Child child2 = new Child();

    Parent parent = new Parent();
    child1.setParent(parent);
    child2.setParent(parent);

    parent.getChildren().add(child1);
    parent.getChildren().add(child2);

    //부모 저장 -> 연관 자식들 저장
    em.persist(parent);
}

영속성 전이:삭제

CascadeType.REMOVE 를 설정하고 부모를 삭제

만약 설정을 안한다면?

각각이 제거해야한다

Parent findParent = em.find(Parent.class, 1L);
Child findChild1 =  em.find(Child.class, 1L);
Child findChild2 =  em.find(Child.class, 2L);

em.remove(findChild1);
em.remove(findChild2);
em.remove(findParent);

고아객체

부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 고아 객체(ORPHAN)제거하고 한다.

고아 객체 제거 기능 설정

@Entity
public class Parent{

    @Id@GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "parent", orphanRemoval = true)
    private List<Child>children = new ArrayList<Child>();
    ...
}
Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0); // 자식엔티티를 컬렉션에서 제거

모든 자식엔티티 제거

parent1.getChildren().clear();

이 기능은 참조하는 곳이 하나일 때만 사용해야 한다.

삭제한 엔티티가 다른곳에서도 참조로 사용되고 있다면 문제가 발생한다.

그렇기 때문에 orphanRemoval @OneToOne, @OneToMany 에서만 사용가능하다.

개념적으로는 부모를 제거하면 자식도 같이 제거되기에 CascadeType.REMOVE를 설정한 것과 같다.

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

CascadeType.ALL + orphanRemoval = true를 동시에 사용하면?

엔티티는 스스로 생명주기를 관리한다.

em.persist() //영속화
em.remove() //영속화 제거

그러나 위의 두 옵션을 같이 사용하면?

자식을 저장하려면 부모에 등록하기만 하면된다(CascadeType)

Parent parent = em.find(Parent.class, parentId);
parent.addChild(child1);

자식을 삭제하려면 부모에서 제거하면 된다(orphanRemoval )

Parent parent = em.find(Parent.class, parentId);
parent.getChildren().remove(removeObject);

값 타입

JPA 데이터 타입은 크게 엔티티 타입 // 값 타입으로 나뉘어진다.

엔티티 타입은 @Entity로 정의하는 객체이고

값 타입은 int, Integer, String 처럼 단순히 값으로 사용하는 자바 기본타입이나 객체를 말한다.

차이점은

값 타입은 식별자가 따로 존재하지 않기에 추적할 수 없다.

값 타입은 3가지로 분류된다

  • 기본값 타입(자바 기본: int, double.. // 래퍼클래스: Integer // String)
  • 임베디드 타입
  • 컬렉션 값 타입

기본 값 타입

값 타입은 공유하면 안 된다.

임베디드 타입

새로운 값 타입을 직접 정의해서 사용

직접 정의한 임베디드 타입도 int, String 처럼 값타입이다.

@Entity
public class Member{

    @Id @GeneratedValue
    private Long id;
    private String name;

    @Embedded Period workPeriod;
    @Embedded Address homeAddress;
    //...
}
@Embeddable
public class Period {

    @Temporal(TemporalType.DATE) java.util.Date startDate;
    @Temporal(TemporalType.DATE) java.util.Date endDate;

    public boolean isWork(){
        //..
    }
}
@Embeddable
public class Address {

    @Column(name="city")
    private String city;
    private String street;
    private String zipCode;
}

@Embeddable : 값 타입을 정의하는 곳에 표시

@Embedded : 값 타입을 사용하는 곳에 표시

임베디드 타입은 기본 생성자가 필수이다.

임베디드 타입과 연관관계

@Entity
public class Member{

    @Embedded Address address;
    @Embedded PhoneNumber phoneNumber;
    //..
}

@Embeddable
public class Address{
    String street;
    String city;
    Strint state;
    @Embedded PhoneNumber phoneNumber;
}

@Embeddable
public class Zipcode{
    String zip;
    String plusFour;
}

@Embeddable
public class Zipcode{
    String zip;
    String plusFour;
}

@Embeddable
public class PhoneNumber{
    String areaCode;
    String localNumber;
    @ManyToOne PhoneServiceProvider provider;
    ..
}

@Entity
public class PhoneServiceProvider{
    @Id String name;
    ...
}

임베디드 타입과 null

member.setAddress(null);
em.persist(member);

값 타입과 불변 객체

  • 값 타입 공유 참조

member.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();

address.setCity("NewCity");
member2.setHomeAddress(address);

이러한 공유 참조로 인해 발생하는 버그는 정말 찾아내기 힘듬

member.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();

address.setCity("NewCity");
member2.setHomeAddress(address);

값 타입의 실제 인스턴스인 값을 공유하는 것은 위험하기에 값(인스턴스)를 복사해서 사용해야한다.

setCity() 같은 수정자 메소드를 모두 제거하자.

불변 객체

값 타입은 될수 있으면 불변객체로 설계해야만 한다.

생성자로만 값을 설정하고 수정자를 만들지 않으면 안된다.

@Embeddable
public class Address {

    private String city;

    protected Address(){}

    public Address(String city){this.city = city}

    public String getCity(){
        return city;
    }

}

불변 객체 사용

Address address = member1.getHomeAddress();

//회원1의 주소값을 조회해서 새로운 주소값을 생성
Address newAddress = new Address(address.getCity());
member2.setHomeAddress(newAddress);

여기선 Address는 불변객체이다. 새로운 객체를 생성해서 사용하므로 공유해도 부장용이 발생하지 않는다.

값 비교

  • 동일성(identity) 비교

인스턴스의 참조 값을 비교 == 사용

  • 동등성(Equiv) 비교

인스턴스 값을 비교 equals() 사용

값 타입은 동일성 비교를 하면 둘은 서로 다른 인스턴스이므로 결과는 거짓이다.

값 타입을 비교할 땐 equals() 를 통해 동등성 비교를 해야한다.

값 타입 컬렉션

값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 @ElementCollection, @CollectionTable 사용

@Entity
public class Member{

    @Id @GeneratedValue
    private Long id;

    @Embedded
    private Address homeAddress;

    @ElementCollection
    @CollectionTable(name = "FAVORITE_FOODS", "joinColumns =    
    @JoinColumn(name="MEMBER_ID"))
    @Column(name="FOOD_NAME")
    private Set<String> favoriteFoods = new HashSet<String>();

    @ElementCollection
    @CollectionTable(name = "ADDRESS", "joinColumns = @JoinColumn(name="MEMBER_ID"))
    private List<Address> addressHistory = new ArrayList<Address>();
}

@Embeddeable
public class Address {

    @Column
    private String city;
    private String street;
    private Srring zipcode;
    //...
}

@CollectionTable 생략하면 기본값을 사용해 매핑됨(엔티티이름_컬렉션 속성이름)

DB의 테이블은 컬럼안에 컬렉션을 포함할 수 없다. 따라서 @CollectionTable을 사용해서 추가한 테이블을 매핑해야한다.

그리고 favoriteFoods 처럼 값으로 사용되는 컬럼이 하나면 @Column을 사용해서 컬럼명을 지정할 수 있다.

addressHistory 또한 별도의 테이블을 사용해야 한다.

값 타입 컬렉션 등록

Member member = new Member();

member.setHomeAddress(new Address("통영","몽돌해수욕장","5029-2788"))

member.getFavoriteFoods().add("짬뽕");
member.getFavoriteFoods().add("짜장");
member.getFavoriteFoods().add("탕수육");

member.getAddressHistory().add(new Address("서울","강남","123-123"));
member.getAddressHistory().add(new Address("서울","강남","000-000"));

em.persist(member);

해당 코드에서는 member 를 영속성 컨텍스트에 등록시킬때 값타입들도 함께 등록된다.

값 타입 컬렉션도 폐치전략이 있다. LAZY가 기본 전략이다.

@ElementCollection(fetch = FetchType.LAZY)

값 타입 컬렉션 수정

Member member = em.find(Member.class, 1L);

//1. 임베디드 값 타입 수정
member.setHomeAddress(new Address("새로운도시","새로운해수욕장","123456"))

//2. 기본값 타입 컬렉션 수정
Set<String> favoriteFoods = member.getFavoriteFoods();
favoriteFoods.remove("탕수육");
favoriteFoods.add("치킨")

//3. 임베디드 값 타입 컬렉션 수정
List<Member> addressHistory = member.getAddressHistory();
addressHistory.remove(new Address("서울","기존주소","123-123"));
addressHistory.add(new Address("새로운도시","새로운 주소","123-456"));
  1. 임베디드 값 타입 수정사실 Member 엔티티를 수정하는 것과 같다.
  2. homeAddress 임베디드 값 타입은 Member 테이블과 매핑했으므로 Member 테이블만 update 한다.
  3. 기본값 타입 컬렉션 수정
  4. 자바의 String 타입은 수정할 수 없다. 그리하여 변경을 위해선 변경을 원하는 기존 대상을 제거하고 새롭게 추가해야한다.
  5. 임베디드 값 타입 컬렉션 수정

​ 값 타입은 불변해야하므로 기존 주소를 삭제하고 새 주소를 등록했다.

    값 타입은 equals, hashcode를 무조건 구현해야한다.

값 타입 컬렉션 제약사항

값 타입 컬렉션은 별도의 테이블에 보관된다.

값 타입은 식별자라는 개념이 없으므로 값을 변경시 DB에 저장된 원본 데이터를 찾기 어렵다.

이 때문에 값 타입 컬렉션에 변경사항 발생시 연관된 모든 데이터를 삭제하고 다시 저장한다.

실무에선 값 타입 컬렉션에 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신 일대다 관계를 고려해야한다

값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성하기 때문에 컬럼에 NULL 조건을 추가할수 없고 중복될수도 없다.

때문에 이를 해결하기 위한 방법은 일대다 + 영속성전이 + 고아객체제거 기능을 적용하면된다

@Entity
public class AddressEntity{

    @Id
    @GeneratedValue
    private Long id;

    @Embedded Address address;
    //...
}
@OneMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name="MEMBER_ID")
private List<AddressEntity> addressEntity = new ArrayList();
728x90
반응형
LIST

'SPRING > JPA' 카테고리의 다른 글

객체지향 쿼리언어  (0) 2022.03.26
jpa metamodel must not be empty! 테스트 에러  (0) 2022.03.24
JPA 상에서의 코드성데이터 처리방법중 하나  (0) 2022.03.21
다양한 연관관계 매핑 정리  (0) 2022.03.17
JPA 시작  (0) 2021.11.30