1. 배열로 보는 리스트 생성

ArrayList(이하 리스트)는 자바에서 배열을 기초로 만들어진 동적 배열이다.

리스트를 처음 선언할 때, 외부적으로는 사이즈가 0으로 보이지만, 실제로는 10개의 요소를 담을 수 있는 배열 공간을 내부적으로 할당받는다.

즉, 내부적으로 [0,0,0,0,0,0,0,0,0,0] 형태의 배열을 가지고 시작한다.

ArrayList<Object>  = new ArrayList<>(); // -> 내부 배열생성 [0,0,0,0,0,0,0,0,0,0]

 

 

사용자는 필요에 따라 아래와 같이 초기 배열의 크기를 조절 할 수 있다.

ArrayList<Object>  = new ArrayList<>(3); // -> 내부 배열생성 [0,0,0]

 

2. 리스트의 크기가 가변적일 수 있는 이유

 

리스트는 내부 배열의 크기를 넘어서는 요소들을 추가할 때,

현재 배열 크기의 1.5에서 2배에 해당하는 새로운 배열을 생성하고 기존의 값들을 이로 복사하여 옮긴다.

 

이 과정을 통해 리스트는 필요에 따라 그 크기가 유동적으로 변할 수 있다.

arrayList.add(6)
[1,2,3,4,5] -> [1,2,3,4,5,6,0,0,0,0]

 

3. 리스트의 삭제 - 리스트의 크기는 자동으로 줄어들지 않는다.

리스트는 배열 기반이지만, 고정 크기 배열과 달리 특정 인덱스의 값을 제거하는 기능을 제공한다.

 

예를 들어, {1,2,3,4,5}의 리스트에서 3을 제거하면, 내부적으로는 [1,2,4,5,0,0,0,0,0,0]로 업데이트되며,

리스트의 값들은 {1,2,4,5}로 변경된다.

 

이러한 삭제 연산은 제거한 값의 뒤에 존재하는 인덱스 값들을 앞으로 옮겨야 한다.

따라서, 배열의 크기 N에 비례하는 시간이 소요되므로 시간 복잡도는 O(N)이다.

 

리스트는 값을 추가 할 때에는 자동으로 그 크기가 늘어나지만, 값을 제거한다고 해서 리스트의 내부적 배열 크기가 줄어들지는 않는다.

배열 크기를 줄이기 위해서는 아래 메서드를 사용하여 리스트의 현재 크기에 맞는 새로운 배열을 생성하고 값을 이동시킬 수 있다.

arayList.trimToSize() // 내부 배열의 크기를 리스트의 사이즈에 맞춘다.
[1,2,3,4,5,0,0,0,0,0] -> [1,2,3,4,5]

 

배열을 생성하려면 통상 3가지 단계를 거치게 된다.

 

  1. 선언 - 배열에 대한 참조를 생성한다.
  2. 배열의 인스턴스화 - 배열을 생성한다.
  3. 초기화 - 배열의 셀에 값을 할당한다.

이를 단계별로 자세히 알아보자.

 

1. 선언(Declaration)

배열에 대한 참조 변수를 생성한다.
이 때 실제 배열 객체는 메모리에 생성되지 않는다. 선언은 타입과 배열을 나타내는 대괄호([]), 그리고 참조 변수 이름으로 구성된다.

int[] arr;

 

메모리에는 아직 배열은 할당되어있지 않다.

2. 인스턴스화(Instantiation)

new 키워드와 함께 배열의 실제 메모리 공간을 할당한다.
배열의 각 요소는 자동으로 해당 타입의 기본값으로 초기화된다. (예를 들어, int 타입의 배열 요소는 0으로 초기화된다).

이 과정에서 배열의 첫 번째 요소([0])에 해당하는 물리적 메모리 주소가 참조 변수에 저장된다.

int[] arr;
arr = new int[3];

2-1 왜 배열은 0부터 시작을 할까?

컴퓨터는 배열의 첫 번째 요소의 메모리 주소를 참조하여 시작점으로 삼는다. 이 주소를 '기준 주소(base address)'라고 부르며, 배열의 다른 요소들은 이 기준 주소로부터의 상대적 위치(offset)로 찾을 수 있다.


즉, 기준점(0)으로 위치를 계산하기 때문이다.

물리주소를 가진 첫번째 요소를 기준으로(본인이기 때문에 0) 계산한다.

3.초기화(Initialization)

배열의 각 요소에 구체적인 값을 할당한다.
이 과정은 명시적으로 각 요소에 값을 할당하거나, 배열 선언과 동시에 값을 초기화하는 문법을 사용하여 진행할 수 있다.

//순차적으로
int[] arr;
arr = new int[3];
arr[0] = 10;
arr[1] = 10;
arr[2] = 10;

//한번에
int[] arrAtOnce = {10,10,10};

3-1 재미있는 사실 : 순차적으로 초기화하느냐, 한번에 초기화하느냐에 따라서 성능이 달라진다?

순차적으로 초기화할 때는 각 배열 요소에 대해 값을 하나씩 할당해야 하므로, 요소의 개수에 비례해서 시간이 소요된다. 

 

반면에 일괄 초기화 구문을 사용하면 순차적 초기화와 초기화 방식은 같지만, 컴파일러나 런타임 환경에 따라 좀 더 최적화를 해주기때문에 성능이 조금 더 빨라진다. 이는 배열의 크기가 커지면 커질수록 성능의 차이가 벌어지게 된다.

JPQL은 JPA 에서 엔터티 객체를 조회하기 위한 쿼리 언어다. 관계형 데이터베이스의 SQL과 유사하지만, 객체 지향적 특성을 가진 쿼리 언어다.

JPQL의 주요 목적은 데이터베이스 테이블이 아닌 엔터티 객체를 대상으로 쿼리를 수행하는 것이다.

쿼리 API

TypedQuery, Query

  • TypedQuery: 반환 형식이 명확한 경우 사용. 반환될 엔티티나 값의 자료형을 지정할 수 있음.
  • Query: 반환 형식이 명확하지 않은 경우 사용. 주로 다양한 형식의 값을 한 번에 반환할 때 사용.

JPQL의 쿼리는 TypedQuery 와 Query 가 존재한다. 반환형이 존재하면 TypedQuery 그렇지 않으면 Query이다.

 

타입쿼리는 반환형의 클레스가 명확할 때 적용한다.
query1은 Member라는 반환형이 명확하니 Member.class를 추가하였고,
query2도 반환형이 m.userName이라는 문자열이므로 String.class를 반환한다.


query3는 숫자, 문자열 모두 포함하므로 반환형이 명확하지 않다. 따라서 반환 클레스를 명시하지 않는다.

즉, TypedQuery가 아닌 Query이다. 

TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class);  
TypedQuery<String> query2 = em.createQuery("select m.userName from Member m", String.class);  
Query query3 = em.createQuery("select m.userName, m.age from Member m");

결과 조회 API

  • query.getResultList() : 결과가 하나 이상일 때, 리스트 반환
    • 결과가 없으면 빈 리스트 반환
  • query.getSingleResult() : 결과가 정확히 하나일 때
    • 결과가 없으면 : javax.persistence.NoResultException
    • 둘 이상이면 : javax.persistence.NonUniqueResultException
Member member = new Member();  
member.setUserName("member1");  
member.setAge(10);  
em.persist(member);  
  
TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class);  
List<Member> resultList = query1.getResultList();  
for (Member member1 : resultList) {  
System.out.println("member1.getUserName() = " + member1.getUserName());  
}  
Member singleResult = query1.getSingleResult();  
System.out.println("singleResult.getUserName() = " + singleResult.getUserName());

 

EnttiyManager.createQuery() 를 그냥 반환하면 TypedQuery 혹은 Query가 나오기 때문에 이 쿼리 타입들에서 한번더 값을 조회 해야 한다.

다중 객체를 조회하는 getResultList(), 단일 객체를 조회하는 getSingleResult()를 사용하면 원하는 엔티티의 타입을 얻을 수 있다.

 

null이 나와도 예외없이 반환하는 getResultList와는 달리, getSingleResult는  값이 null이라면 NoResultException이 발생하고, 둘 이상이라면 NonUniqueResultException이 발생한다. 

Spring Data JPA에서는 단일 반환일 때 Optional을 반환하여 예외가 터지지 않게 한다.

 

파라미터 바인딩 - 이름 기준, 위치 기준

JPQL에 원하는 파라미터를 넣을때에는 이름을 기준으로 넣는것과, 순서를 기준으로 넣는방법이 있다.

 

이름기준

 

where문에 =: 파라미터명 을 추가한다.

이후  setParameter에서 기존에 입력한 파라미터명과, 실제 데이터를 넣는다.

Member singleResult = em.createQuery("select m from Member m where m.userName = :username", Member.class)  
.setParameter("username", "member1")  
.getSingleResult();  
System.out.println("singleResult = " + singleResult.getUserName());

 

순서기준 (비추천, 순서가 밀릴 위험)


where 절의 = : 파라미터 
대신 = ?숫자 로 변경하면 된다.
밑의 파라미터도 문자열대신 숫자로 변경

다만, 중간에 값이 추가되면 값이 그대로 밀릴 수 있으므로 권장하지 않는다.

Member singleResult = em.createQuery("select m from Member m where m.userName = ?1", Member.class)  
.setParameter(1, "member1")  
.getSingleResult();

 

프로젝션(SELECT)

  • SELECT 절에 조회할 대상을 지정하는 것
  • 프로젝션 대상 : 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)
  • SELECT m FROM Member m -> 엔티티 프로젝션
  • SELECT m.team FROM Member m -> 엔티티 프로젝션
  • SELECT m.address From Member m -> 임베디드 타입 프로젝션
  • SELECT m.username, m.age FROM Member m -> 스칼라 타입 프로젝션 

관계형 데이터베이스의 경우에는 스칼라 타입만 조회가 가능한데, JPQL의 경우 엔티티, 임베디드 타입도 조회 가능하다.

스칼라 타입이란, 우리가 일반적으로 DB에서 데이터를 조회할 때 반환되는 기본형 값들을 말한다. 

 

엔티티 프로젝션


엔티티 프로젝션을  하면 10개든 20개든 엔티티들이 전부 영속성 컨텍스트에서 관리된다.
다음과 같이 age가 10인 member를 영속한 후, 쿼리를 날리고 캐시를 비웠다.

Member member = new Member();  
member.setUserName("member1");  
member.setAge(10);  
em.persist(member);  
  
em.flush();  
em.clear();  
  
List<Member> resultList = em.createQuery("select m from Member m", Member.class)  
.getResultList();  
  
Member findMember = resultList.get(0);  
findMember.setAge(20);

엔티티 프로젝션으로 member를 조회하면, 수정을 했을 때 JPA에서는 변경을 감지하고 update 쿼리를 실행한다

Hibernate: 
    /* update
        for com.example.jpa.jpql.Member */update member 
    set
        age=?,
        team_id=?,
        user_name=? 
    where
        id=?

 

엔티티 프로젝션(참조 엔티티의 경우)

참조 엔티티를 조회할 때에는 당연하지만 반환 클레스도 참조 클레스로 변경해야한다.

 

첫번째 방식은 join 유무가 한눈에 파악하기 어려우므로

두번째 방식처럼 명시적으로 join을 표시해주는것이 좋다.

List<Team> worstResult = em.createQuery("select m.team from Member m", Team.class)  
.getResultList();  
  
List<Team> bestResult = em.createQuery("select t from Member m join m.team t", Team.class)  
.getResultList();

참조 엔티티를 반환형으로 지정했지만 쿼리 자체는 멤버에서 찾으므로 join 쿼리가 나간다.

Hibernate: 
    /* select
        m.team 
    from
        Member m */ select
            t1_0.id,
            t1_0.name 
        from
            member m1_0 
        join
            team t1_0 
                on t1_0.id=m1_0.team_id

 

 

임베디드 타입 프로젝션

임베디드 타입은 엔티티가 아닌 값이기 때문에  select의 주체가 되지 못한다.
즉, Entity.EmbeddedType 으로 조회해야 한다.

그렇기에  두번째 코드는 컴파일 에러가 나타난다.

 

스칼라 타입 프로젝션

반환타입을 제거하면 된다.

List resultList = em.createQuery("select m.userName, m.age from Member m")  
.getResultList();

이 스칼라 타입은 어떻게 조회될까?

SELECT m.username, m.age FROM Member m

1. Query 타입으로 조회

아래 코드는 반환타입 따로 지정하지 않아 반환타입 없이 그냥 List로만 반환되지만,

실제로는 내부적으로 Object[] 형식을 취한다고 한다.

실제로 조회해보면 다음과 같다.

Member member = new Member();  
member.setUserName("member1");  
member.setAge(10);  
em.persist(member);  
  
List resultList = em.createQuery("select m.userName, m.age from Member m")  
.getResultList();  
  
for (Object o : resultList) {  
log.info("o = {}",o);  
log.info("o.getClass = {}",o.getClass());
}

class [Ljava.lang.Object; 이 Object[]  를 의미한다.

o = [member1, 10]
o.getClass = class [Ljava.lang.Object;

2. Object[] 타입으로 조회

Member member = new Member();  
member.setUserName("member1");  
member.setAge(10);  
em.persist(member);  
  
List<Object[]> result = em.createQuery("select m.userName, m.age from Member m")  
.getResultList();  
  
for (Object[] objects : result) {  
log.info("objects={}", Arrays.toString(objects));  
}

3. new 명령어로 조회
- 단순 값을 DTO로 바로 조회
- SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m
- 패키지명을 포함한 전체 클래스명 입력
- 순서와 타입이 일치하는 생성자 필요

 

memberDTO 추가, toString을 오버라이딩 한다.

public class MemberDTO {  
private String username;  
private int age;  
  
public MemberDTO(String username, int age) {  
this.username = username;  
this.age = age;  
}  
  
@Override  
public String toString() {  
return "MemberDTO{" +  
"username='" + username + '\'' +  
", age=" + age +  
'}';  
}  
}

이후 조회문에서 DTO의 패키지 주소를 전부 적어서 mebmerDTO 객체를 추가한다.

Member member = new Member();  
member.setUserName("member1");  
member.setAge(10);  
em.persist(member);  
  
List<MemberDTO> resultList = em.createQuery("select new com.example.jpa.jpql.MemberDTO(m.userName, m.age) from Member m", MemberDTO.class)  
.getResultList();  
  
for (MemberDTO memberDTO : resultList) {  
log.info(memberDTO.toString());  
}

다음처럼 DTO에 감싸져서 온다.

MemberDTO{username='member1', age=10}

마지막 방법이 제일 좋아보이지만 DTO로 조회하려면 패키지명을 전부 적어야하는 불편함이 있다. 문자열로 조회를 하기때문에 오타를 찾기도 어렵다.

하지만 이는 QueryDSL로 대처가 가능하다.

'Java' 카테고리의 다른 글

해시코드  (0) 2024.01.13

1. 기본값 타입

1)  JPA의 타입 분류

 

JPA에서는 타입을 크게 2가지로 분류하는데

@Entity로 정의 하는 객체인 엔티티 타입

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

 

값 타입은 기본값 타입, 임베디드 타입, 컬렉션 값 타입으로 나뉜다.

 

기본값 타입자바 기본타입, 래퍼클래스, String을 포함 하는 값 타입이고

임베디드 타입은 복합 값 타입 이라고 하며, 사용자가 직접 정의하는 값 타입이며

컬렉션 값 타입기본값, 임베디드 타입을 컬렉션으로 묶은것이 컬렉션 값 타입이다.

 

2) 값 타입 분류

 

1 - 기본값 타입

  • 예 : String name, int age
  • 생명주기를 엔티티에 의존
    • 예 : 회원을 삭제하면 이름, 나이 필드도 제거됨
  • 값 타입은 공유하면 x
    • 예: 회원 이름 변경 시 다른 회원의 이름도 함께 변경되면 안됨
  • 참고 : 자바의 기본 타입은 절대 공유X
    • - int double 같은 기본 타입은 절대 공유 X
    • - 기본 타입은 항상 값을 복사함
    • - Integer 같은 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능하능한 객체이지만 변경 X

int, double같은 기본 타입은 절대 공유되지 않는다. 그렇기에 값타입으로 썼을 때 안전하다.

@Test  
void valueTest() {  
int a = 10;  
int b = a;  
  
a = 20;  
  
System.out.println("a = " + a);  
System.out.println("b = " + b);  
}

------------------------------------
a = 20
b = 10

a와 b는 저장공간을 따로 가지고 있어서

b의 값은 a가 10일때 초기화되고
a는 20으로 할당되어

a = 20 , b = 10이 된다.

이렇듯 기본형은 값이 공유되지 않는다.

그래서 부수효과가 일어나지 않는다.

래퍼 클래스같은 경우,  참조값을 공유하지만, 변경이 불가능하기때문에 안전하다.

 

 

2 - 임베디드 타입

  •  새로운 값 타입을 직접 정의할 수 있음
  • JPA는 임베디드 타입(embedded type) 이라고 함
  • 주로 기본 값 타입을 모아 만들어서 복합 값 타입이라고도 함
  • int, String과 같은 값 타입
  • @Embeddable : 값 타입을 정의하는 곳에 표시
  • @Embedded : 값 타입을 사용하는 곳에 표시
  • 기본 생성자 필수

2-1 임베디드 타입의 장점

  • 재사용
  • 높은 응집도
  • Period.isWork() 처럼 해당 값 타입만 사용하는 의미 있는 메서드를 만들 수 있음
  • 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존함 (값 타입이기 때문에)

임베디드 타입은 객체이기 때문에 데이터 뿐만 아니라 메서드까지 가지고 있기 때문에 거기서 오는 이득이 많다.

임베디드 타입 자체에는 @Embeddable을, 임베디드 타입을 사용할 엔티티의 속성에는 @Embedded를 붙인다.

@Entity  
@Getter  
@Setter  
public class Member {  
@Id  
@GeneratedValue  
@Column(name = "member_id")  
private Long id;  
@Column(name = "username")  
private String username;  
@Embedded  
private Period workPeriod;  
@Embedded  
private Address homeAddress;


@Embeddable  
@Getter  
@Setter  
@NoArgsConstructor  
public class Address {  
private String city;  
private String street;  
private String zipcode;  
  
public Address(String city, String street, String zipcode) {  
this.city = city;  
this.street = street;  
this.zipcode = zipcode;  
}  
}


@Embeddable  
@Setter  
@Getter  
@NoArgsConstructor  
public class Period {  
private LocalDateTime startDate;  
private LocalDateTime endDate;  
  
public Period(LocalDateTime startDate, LocalDateTime endDate) {  
this.startDate = startDate;  
this.endDate = endDate;  
}  
}

이러한 값 타입은 불변객체여야 하기 때문에 Setter대신 생성자를 통해 새로운 값을 만들어야 한다.

 

int 같은 기본형 타입은 애초에 참조가 존재하지 않아서 늘 새로운 값이 저장되므로 중간에 어떠한 값이 바뀌더라도 그것을 이용한 다른 값들이 변경되지 않지만,

 

임베디드 타입은 객체이므로 참조를 통해 값을 가져오기 때문에 중간에 값이 변경되면 그것과 연관되는 다른값들도 부수효과가 일어나기 때문에, 이러한 값타입들은 불변객체여야 한다.

Member member =new Member();  
member.setHomeAddress(new Address("city","street","1000"));  
member.setWorkPeriod(new Period(LocalDateTime.now(),LocalDateTime.now()));  
em.persist(member);

 

이처럼 따로 테이블이 생성되지 않고 member의 컬럼으로써 동작한다.

Hibernate: 
    create table member (
        end_date datetime(6),
        member_id bigint not null,
        start_date datetime(6),
        team_id bigint,
        city varchar(255),
        street varchar(255),
        username varchar(255),
        zipcode varchar(255),
        primary key (member_id)
    ) engine=InnoDB
Hibernate: 
    /* insert for
        com.example.jpa.domain.Member */insert 
    into
        member (city,street,zipcode,username,end_date,start_date,member_id) 
    values
        (?,?,?,?,?,?,?)

 

2-2 임베디드 타입과 테이블 매핑

  • 임베디드 타입은 엔티티의 값일 뿐이다.
  • 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
  • 객체와 테이블을 아주 세밀하게(find-grained) 매핑하는 것이 가능
  • 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많음

2-3 @AttributeOverride

@AttributeOverride임베디드타입의 속성을 재정의하며, 엔티티 내에서 같은 임베디드 타입을 쓸 때 컬럼이 중복되지 않게 컬럼명을 변경할 수 있다.

@Entity  
@Getter  
@Setter  
public class Member {  
@Id  
@GeneratedValue  
@Column(name = "member_id")  
private Long id;  
@Column(name = "username")  
private String username;  
@Embedded  
private Period workPeriod;  
@Embedded  
private Address homeAddress;  
@Embedded  
@AttributeOverrides({  
@AttributeOverride(name="city",  
column=@Column(name = "work_city")),  
@AttributeOverride(name="street",  
column=@Column(name = "work_street")),  
@AttributeOverride(name="zipcode",  
column=@Column(name = "work_zipcode"))  
})  
private Address workAddress;  
  
}

이렇게 임베디드 타입인 homeAddress의 컬럼과, workAddress의 컬럼이 구분되어 쿼리가 작성된다.

Hibernate: 
    /* insert for
        com.example.jpa.domain.Member */insert 
    into
        member (city,street,zipc5de,username,work_city,work_street,work_zipcode,end_date,start_date,member_id) 
    values
        (?,?,?,?,?,?,?,?,?,?)

참고 : 임베디드 타입의 값이 null 이면, 매핑한 컬럼 값은 null이 된다.

 

3) 값 타입과 불변 객체

1 - 값 타입 공유 참조

 

임베디드 타입은 여러 엔티티에서 동시에 같은 인스턴스를 참조할 수 있다. 

Address address = new Address("city", "street", "1000");  
  
Member member1 =new Member();  
member1.setUsername("member1");  
member1.setHomeAddress(address);  
em.persist(member1);  
  
Member member2 =new Member();  
member2.setUsername("member2");  
member2.setHomeAddress(address);  
em.persist(member2);  
  
member1.getHomeAddress().setCity("newCity");

member1의 Address의 값을 변경했으나, address라는 참조를 member2도 같이 사용하므로 값이 공유된다.

결론적으로 같은 임베디드 타입 값을 공유하였기 때문에 사이드 이팩트가 난 것이다.

Hibernate: 
    /* update
        for com.example.jpa.domain.Member */update member 
    set
        city=?,
        street=?,
        zipc5de=?,
        username=?,
        end_date=?,
        start_date=? 
    where
        member_id=?
Hibernate: 
    /* update
        for com.example.jpa.domain.Member */update member 
    set
        city=?,
        street=?,
        zipc5de=?,
        username=?,
        end_date=?,
        start_date=? 
    where
        member_id=?

그렇기에 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 굉장히 위험하다.
만약 이렇게 2개가 동시에 수정되는것을 의도하고자 한다면 임베디드 타입이 아닌 엔티티로 승격시켜야 한다.

 

2 - 값 타입 복사

 

 위처럼 같은 실제 인스턴스를 공유하는것은 위험하기 때문에

같은 값을 넣으려면 새로운 인스턴스를 만들어서 기존의 값을 복사해야 한다.

 

Address address1 = new Address("city", "street", "1000");  
  
Member member1 =new Member();  
member1.setUsername("member1");  
member1.setHomeAddress(address1);  
em.persist(member1);  
  
Address address2 = new Address(address1.getCity(), address1.getStreet(), address1.getZipcode());  
Member member2 =new Member();  
member2.setUsername("member2");  
member2.setHomeAddress(address2);  
em.persist(member2);  
  
member1.getHomeAddress().setCity("newCity");

 

3 - 객체 타입의 한계

  • 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
  • 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이다.
  • 자바 기본 타입에 값을 대입하면 값을 복사한다.
  • 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.
  • 객체의 공유 참조는 피할 수 없다.

기본 타입

int a = 10;
int b= a; // 기본 타입은 값을 복사
b = 4;

기본타입은 = 을 하면 값이 복사가 된다.
그래서 b의 값을 4로 변경하여도 a의 값은 유지된다.

 

객체 타입

Address a = new Address("old");
Address b = a; // 객체 타입은 참조를 전달
b. setCity("New")

여기서는 a,b 모두 같은 인스턴스를 가르키기 때문에 값이 변경되는 순간 a의 값도 변경된다.

 

4 - 불변 객체

  • 객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단
  • 값 타입은 불변 객체(immutable object)로 설계해야 함
  • 불변 객체 : 생성 시점 이후 절대 값을 변경할 수 없는 객체
  • 생성자로만 값을 설정하고 수정자(Setter)를 만들지 않으면 됨
  • 참고 : Integer, String은 자바가 제공하는 대표적인 불변 객체

값을 변경하려면

address 자체를 통으로 새로 만들고, homeAddress 자체를 newAddress로 변경한다.

ddress address1 = new Address("city", "street", "1000");  
  
Member member1 =new Member();  
member1.setUsername("member1");  
member1.setHomeAddress(address1);  
em.persist(member1);  
  
Address newAddress = new Address("newCity", address1.getStreet(), address1.getZipcode());  
member1.setHomeAddress(newAddress);

 

4) 값 타입의 비교

 값 타입은 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야 한다.

  • 동일성(identity) 비교 : 인스턴스의 참조 값을 비교, == 사용
  • 동등성(equivalence) 비교 : 인스턴스의 값을 비교, equals() 사용
  • 값 타입은 a.equals(b)를 사용해서 동등성 비교를 해야함
  • 값 타입의 equals() 메서드를 적절하게 재정의(주로 모든 필드 사용)

address1.equals(address2)는 true가 나올까? false가 나온다.

Address address1 = new Address("city", "street", "1000");  
Address address2 = new Address("city", "street", "1000");  
  
System.out.println("(address1 == address2) = " + (address1 == address2));  
System.out.println("(address1 eq address2) = " + (address1.equals(address2)));

 

왜냐하면 equals의 디폴트 옵션이 == 비교이기 때문이다.

(address1 == address2) = false
(address1 eq address2) = false

우리는 지금까지 String을 사용했을 때 equals로 비교하면 비교가 잘 되었는데 어째서 false가 나온것일까?

이유는 String 객체 내부에는 이미 equals 가 오버라이드 되어 있지만, 우리가 커스텀한 임베디드 타입은

오버라이드 되어있지 않기 때문이다. 

 

그래서 String내부의 equals ()를 보면

아래처럼 == 비교로 구성된 것을 알 수 있다.

== 비교는 기본형을 비교 할 때에는 값을 비교하기 때문에 가능하다.

public static boolean equals(byte[] value, byte[] other) {
        if (value.length == other.length) {
            for (int i = 0; i < value.length; i++) {
                if (value[i] != other[i]) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

그러므로 Address 임베디드 타입을 비교하고자 한다면 equals를 오버라이딩 해야한다.

또한 hashCode도 같이 오버라이딩을 해줘야 한다.

 

A와 B의 값이 동등하다면, 이 둘의 해쉬코드도 같다고 봐야 하기 때문이다.

또한, HashMap, HashSet 같은 컬렉션들은 해당 타입의 해쉬코드 값을 기준으로 객체를 저장하기 때문이다.

 

참고로 A와 B가 서로 상관없는 타입이어도 해쉬코드는 같을 수 있다.

@Override  
public boolean equals(Object o) {  
if (this == o) return true;  
if (o == null || getClass() != o.getClass()) return false;  
Address address = (Address) o;  
return Objects.equals(city, address.city) && Objects.equals(street, address.street) && Objects.equals(zipcode, address.zipcode);  
}

@Override
    public int hashCode() {
        return Objects.hash(city, street, zipcode);
    }

 

5) 값 타입 컬렉션

  • 값 타입을 하나 이상 저장할 때 사용
  • @ElementCollection, @CollectionTable 사용
  • 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
  • 컬렉션을 저장하기 위한 별도의 테이블이 필요함
  • 참고 : 값 타입 컬렉션은 영속성 전이(CASCADE) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.

관계형데이터베이스는 기본적으로 컬렉션을 담을수 있는 구조가 없다.


그래서 이러한 컬렉션 구조를 표현하려면
개별적으로 테이블을 생성해야 한다

값타입 테이블에는 식별자가 따로 존재하면 엔티티가 되어버리기 때문에 따로 두지 않는다.

@ElementCollection과 @CollectionTable을 이용한 엔티티구현

@Entity  
@Getter  
@Setter  
public class Member {  
@Id  
@GeneratedValue  
@Column(name = "member_id")  
private Long id;  
  
@Column(name = "username")  
private String username;  
  
@Embedded  
private Address homeAddress;  
  
@ElementCollection  
@CollectionTable(name = "favorite_food", joinColumns =  
@JoinColumn(name = "member_id")  
)  
@Column(name = "food_name")  
private Set<String> favoriteFoods = new HashSet<>();  
  
@ElementCollection  
@CollectionTable(name = "address", joinColumns =  
@JoinColumn(name = "member_id"))  
private List<Address> addressesHistory = new ArrayList<>();  
  
}

실행시켜보면 다음처럼 쿼리가 나온다


member

Hibernate: 
    create table member (
        member_id bigint not null,
        team_id bigint,
        city varchar(255),
        street varchar(255),
        username varchar(255),
        zipcode varchar(255),
        primary key (member_id)
    ) engine=InnoDB

 

favoriteFoods

Hibernate: 
    create table favorite_food (
        member_id bigint not null,
        food_name varchar(255)
    ) engine=InnoDB

 

address

Hibernate: 
    create table address (
        member_id bigint not null,
        city varchar(255),
        street varchar(255),
        zipcode varchar(255)
    ) engine=InnoDB

다음과 같이 테스트 코드를 작성한다.

값 타입 저장

Member member = new Member();  
member.setUsername("member1");  
member.setHomeAddress(new Address("homeCity","street","1000"));  
  
member.getFavoriteFoods().add("치킨");  
member.getFavoriteFoods().add("족발");  
member.getFavoriteFoods().add("피자");  
  
member.getAddressesHistory().add(new Address("oldCity1","street","1000"));  
member.getAddressesHistory().add(new Address("oldCity2","street","1000"));  
  
em.persist(member);

 

흥미로운 점은 값 타입 컬렉션을 따로 영속하지 않아도 member만 영속하니까 다른 테이블들도 저장이 되었다. 즉 라이프사이클이 다른 테이블임에도 불구하고 member와 같이 돌아가고 있는것이다.


값 타입 컬렉션도 본인 스스로 라이프 사이클이 없다.
값 타입 컬렉션도 결국은 값 타입이기 때문에 라이프 사이클을 엔티티에 의존한다.

 

또한 이런 값 타입 컬렉션은 기본이 지연로딩이라서 실제 데이터를 요청하기 전 까지는 데이터를 부르지 않는다.

 

참고로  컬렉션들은 대부분 대상을 찾을 때 equals 를 사용한다.  그래서 완전히 똑같은 equals 대상을 넣어주면 해당 값을 지울 수 있다.
그렇기 때문에 값 타입은 equals와 hashcode를 구현해 놓아야 한다.

 

값 타입 리스트 수정

Member member = new Member();  
member.setUsername("member1");  
  
member.getAddressesHistory().add(new Address("oldCity1","street","1000"));  
member.getAddressesHistory().add(new Address("oldCity2","street","1000"));  
  
em.persist(member);  
  
  
em.flush();  
em.clear();  
System.out.println("========================================");  
Member findMember = em.find(Member.class, member.getId());  
  
findMember.getAddressesHistory().remove(new Address("oldCity1","street", "1000"));

-------------------------------------------------------------------------------------
HashSet(FavoriteFoods) 경우 수정 코드 :
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");

 쿼리를 보면  해당 멤버에 소속된 addressHistory의 값 전체를 다 지웠다. 그리고 이상하게도 insert문이 2번 나왔다.

Hibernate: 
    select
        m1_0.member_id,
        m1_0.city,
        m1_0.street,
        m1_0.zipcode,
        m1_0.username 
    from
        member m1_0 
    where
        m1_0.member_id=?
Hibernate: 
    select
        a1_0.member_id,
        a1_0.city,
        a1_0.street,
        a1_0.zipcode 
    from
        address a1_0 
    where
        a1_0.member_id=?
Hibernate: 
    /* one-shot delete for com.example.jpa.domain.Member.addressesHistory */delete 
    from
        address 
    where
        member_id=?
Hibernate: 
    /* insert for
        com.example.jpa.domain.Member.addressesHistory */insert 
    into
        address (member_id,city,street,zipcode) 
    values
        (?,?,?,?)
Hibernate: 
    /* insert for
        com.example.jpa.domain.Member.addressesHistory */insert 
    into
        address (member_id,city,street,zipcode) 
    values
        (?,?,?,?)

1 -  값 타입 컬렉션의 제약사항

  • 값 타입은 엔티티와 다르게 식별자 개념이 없다.
  • 값은 변경하면 추적이 어렵다.
  • 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 데이터를 모두 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 함: null 입력 x, 중복 저장 x

값 타입 컬렉션에서 수정을 하려면 결국 해당 id에 대한 값을 모두 지우고 다시 리스트에 있는 값을 저장하기 때문에 효율적이지 않다.

 

결론적으로 정말 단순한 것 외에는 실무에서 그대로 사용하기는 어렵다

 

2 - 값 타입 컬렉션 대안

  • 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대 다 관계를 고려
  • 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
  • 영속성 전이(CASCADE) + 고아 객체 제거를 사용해서 값 타입 컬렉션처럼 사용

임베디드 타입 Address를 Entity로 승격

@Entity
@Getter
@Setter
@Table(name = "address")
@NoArgsConstructor
public class AddressEntity {
    @Id @GeneratedValue
    private Long id;

    private Address address;

    public AddressEntity(Address address) {
        this.address = address;
    }

    public AddressEntity(String city, String street, String number) {
        this.address = new Address(city,street,number);
    }
}

addressHistory에 @CollectionTable 대신 @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) 설정

@Entity
@Getter
@Setter
public class Member {
    @Id
    @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    @Column(name = "username")
    private String username;

    @Embedded
    private Address homeAddress;

    @ElementCollection
    @CollectionTable(name = "favorite_food", joinColumns =
        @JoinColumn(name = "member_id"))
    @Column(name = "food_name")
    private Set<String> favoriteFoods = new HashSet<>();

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "member_id")
    private List<AddressEntity> addressesHistory = new ArrayList<>();

}

 

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

JPA - 즉시로딩과 지연로딩, CASCADE와 고아객체  (1) 2023.10.10
JPA - 프록시  (0) 2023.10.10
JPA - 상속관계 매핑, @MappedSuperclass  (0) 2023.10.04
JPA - 연관관계  (0) 2023.10.01
JPA - 기본 키 매핑 : @GeneratedValue  (0) 2023.09.23

1. 지연로딩

JPA에서 지연 로딩(Lazy Loading)은 연관된 엔티티나 컬렉션을 즉시 로딩하지 않는 전략을 의미한다. 대신, 실제로 해당 데이터에 접근이 필요할 때 (예: 프로퍼티의 값을 가져오는 경우) 데이터베이스에서 해당 데이터를 로드한다.

 

연관관계에 FetchType.LAZY  속성을 추가

package com.example.jpa.domain;  
  
import jakarta.persistence.*;  
import lombok.Getter;  
import lombok.Setter;  
import lombok.extern.java.Log;  
  
import java.util.ArrayList;  
import java.util.List;  
  
@Entity  
@Getter  
@Setter  
public class Member extends BaseEntity {  
@Id  
@GeneratedValue  
@Column(name = "member_id")  
private Long id;  
@Column(name = "username")  
private String username;  
@ManyToOne(fetch = FetchType.LAZY)  
@JoinColumn(name = "team_id")  
private Team team;  
}

테스트 코드로 Team의 클래스명과, 쿼리를 확인해 보면

Team team = new Team();  
team.setName("teamA");  
em.persist(team);  
  
Member member1 = new Member();  
member1.setUsername("member1");  
member1.setTeam(team);  
em.persist(member1);  
  
em.flush();  
em.clear();  
  
Member member = em.find(Member.class, member1.getId());  
  
System.out.println("member.getTeam().getClass() = " + member.getTeam().getClass());

System.out.println("============================================================");  
member.getTeam().getName();  
System.out.println("============================================================");

Member 엔티티는 Team을 Join 하지 않고 조회했다가,

실제 Team의 데이터를 조회할 때 조회 쿼리가 나가는것을 알 수 있다.

클래스도 확인해보면 실제 객체가 아닌 프록시 객체를 반환한다.

Hibernate: 
    select
        m1_0.member_id,
        m1_0.created_by,
        m1_0.created_date,
        m1_0.last_modified_by,
        m1_0.last_modified_date,
        m1_0.team_id,
        m1_0.username 
    from
        member m1_0 
    where
        m1_0.member_id=?
member.getTeam().getClass() = class com.example.jpa.domain.Team$HibernateProxy$k43PADH3
============================================================
Hibernate: 
    select
        t1_0.team_id,
        t1_0.created_by,
        t1_0.created_date,
        t1_0.last_modified_by,
        t1_0.last_modified_date,
        t1_0.name 
    from
        team t1_0 
    where
        t1_0.team_id=?
============================================================

비즈니스 로직 상 대부분 메인 엔티티만 사용하고 참조 엔티티는 많이 쓰지 않을때 쓰는게 좋다. 라고 하지만

실무에서는 대부분  FetchType.LAZY 만 쓰인다고 한다.

 

정리

  • 모든 연관관계에 지연 로딩을 사용하라
  • 실무에서 즉시 로딩을 사용하지 마라
  • JPQL fetch 조인이나, 엔티티 그래프 기능을 사용하라
  • 즉시 로딩은 상상하지 못한 쿼리가 나간다.

 

2. 즉시로딩

지연로딩은 조회 시 가짜 프록시 객체를 만들어 조회하지만

즉시 로딩은 모든것에 Join을 걸고 조회한다

 

이전 엔티티 코드에서 fetchType.LAZY -> fetchType.EAGER 로 변경해 준 후 동일한 테스트코드를 실행하면

지연로딩과는 다르게 최초 조회시 모든 데이터를 불러오고,

구분선(===...)사이에는 아무것도 조회하지 않는것을 알 수 있다.

데이터를 모두 가지고 오기 때문에 프록시 객체가 아닌 실제 객체를 반환한다.

Hibernate: 
    select
        m1_0.member_id,
        m1_0.created_by,
        m1_0.created_date,
        m1_0.last_modified_by,
        m1_0.last_modified_date,
        t1_0.team_id,
        t1_0.created_by,
        t1_0.created_date,
        t1_0.last_modified_by,
        t1_0.last_modified_date,
        t1_0.name,
        m1_0.username 
    from
        member m1_0 
    left join
        team t1_0 
            on t1_0.team_id=m1_0.team_id 
    where
        m1_0.member_id=?
member.getTeam().getClass() = class com.example.jpa.domain.Team
============================================================
============================================================

엔티티와 참조 엔티티가 대부분 같이 사용된다고 한다면 즉시 로딩을 사용한다고 한다.

 

주의점

데이터베이스 입장에서 한두개 정도는  조인이라고 해서 크게 느리진 않다. 
하지만 만약에 수십개라면 완전 다른 차원의 이야기다.

다 즉시로딩으로 10개씩 되어있다고 생각해보자 
뭐 하나 할때마다 전부 조인되서 나간다.

그래서 실무에서 테이블이 복잡하게 얽혀있을 때에는 전부 지연 로딩 으로 설정해야 한다.

 

예를들어 JPQL로 테스트 코드를 작성하면

Team team = new Team();  
team.setName("teamA");  
em.persist(team);  
  
Member member1 = new Member();  
member1.setUsername("member1");  
member1.setTeam(team);  
em.persist(member1);  
  
em.flush();  
em.clear();  
  
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();

 

처음에는 엔티티 객체에 대한 쿼리가 나오고, 그 후에 참조 객체에 대해서 한번 더 조회 쿼리가 나온다.
그렇기에, 조회하려는 엔티티 객체가 10개 라면, 엔티티 쿼리 1개 + 참조 엔티티 쿼리 10개 의 쿼리가 출력된다. 

em.find같은 것들은 JPA가 내부적으로 최적화를 해 주지만 JPQL은 쿼리 그대로 나가기 때문이다.

 

Hibernate: 
    /* select
        m 
    from
        Member m */ select
            m1_0.member_id,
            m1_0.created_by,
            m1_0.created_date,
            m1_0.last_modified_by,
            m1_0.last_modified_date,
            m1_0.team_id,
            m1_0.username 
        from
            member m1_0
Hibernate: 
    select
        t1_0.team_id,
        t1_0.created_by,
        t1_0.created_date,
        t1_0.last_modified_by,
        t1_0.last_modified_date,
        t1_0.name 
    from
        team t1_0 
    where
        t1_0.team_id=?

Team과 Member를 하나씩 더 추가해보자

Team teamA = new Team();  
teamA.setName("teamA");  
em.persist(teamA);  
  
Team teamB = new Team();  
teamB.setName("teamB");  
em.persist(teamB);  
  
Member member1 = new Member();  
member1.setUsername("member1");  
member1.setTeam(teamA);  
em.persist(member1);  
  
Member member2 = new Member();  
member2.setUsername("member2");  
member2.setTeam(teamB);  
em.persist(member2);  
  
em.flush();  
em.clear();  
  
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();

쿼리를 보면 엔티티에 대한 쿼리 1개와 참조 엔티티에 대한 2개의 쿼리가 나가는것을 볼 수 있다.

Hibernate: 
    /* select
        m 
    from
        Member m */ select
            m1_0.member_id,
            m1_0.created_by,
            m1_0.created_date,
            m1_0.last_modified_by,
            m1_0.last_modified_date,
            m1_0.team_id,
            m1_0.username 
        from
            member m1_0
Hibernate: 
    select
        t1_0.team_id,
        t1_0.created_by,
        t1_0.created_date,
        t1_0.last_modified_by,
        t1_0.last_modified_date,
        t1_0.name 
    from
        team t1_0 
    where
        t1_0.team_id=?
Hibernate: 
    select
        t1_0.team_id,
        t1_0.created_by,
        t1_0.created_date,
        t1_0.last_modified_by,
        t1_0.last_modified_date,
        t1_0.name 
    from
        team t1_0 
    where
        t1_0.team_id=?

하지만 지연로딩으로 설정한다면 아래와 같이 하나의 쿼리로 끝이 난다.

 /* select
        m 
    from
        Member m */ select
            m1_0.member_id,
            m1_0.created_by,
            m1_0.created_date,
            m1_0.last_modified_by,
            m1_0.last_modified_date,
            m1_0.team_id,
            m1_0.username 
        from
            member m1_0

 

정리

  • 가급적 지연 로딩만 사용(실무에서)
  • 즉시 로딩을 적용하면 예상치 못한 SQL이 발생
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩 -> LAZY로 설정
  • @OneToMany, @ManyToMany 는 기본이 지연로딩

3. CASCADE(영속성 전이)

CASCADE특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만드는 기능이다.

 

Parent 와 Child라는 엔티티가 있을 때 연관관계에 cascade 속성을 추가한다.

package com.example.jpa.domain;  
  
import jakarta.persistence.*;  
import lombok.Getter;  
import lombok.Setter;  
  
import java.util.ArrayList;  
import java.util.List;  
  
@Entity  
@Getter  
@Setter  
public class Parent {  
@Id  
@GeneratedValue  
@Column(name = "member_id")  
private Long id;  
private String name;  
@OneToMany(mappedBy = "parent",cascade = CascadeType.ALL)  
private List<Child> childList = new ArrayList<>();  
  
public void addChild(Child child){  
childList.add(child);  
child.setParent(this);  
}  
}



package com.example.jpa.domain;  
  
import jakarta.persistence.*;  
import lombok.Getter;  
import lombok.Setter;  
  
@Entity  
@Getter  
@Setter  
public class Child {  
@Id  
@GeneratedValue  
@Column(name = "member_id")  
private Long id;  
private String name;  
  
@ManyToOne(fetch = FetchType.LAZY)  
@JoinColumn(name = "parent_id")  
private Parent parent;  
}

Child 엔티티를 영속시키지 않고 Parent 엔티티만 영속시킨다.

 
Child child1 = new Child();  
child1.setName("child1");  
Child child2 = new Child();  
child2.setName("child2");  
  
Parent parent = new Parent();  
parent.addChild(child1);  
parent.addChild(child2);  
  
em.persist(parent);

실제 쿼리를 보면 Parent 엔티티만 영속시켰지만 Child에 대한 쓰기 쿼리가 출력된다.

Hibernate: 
    /* insert for
        com.example.jpa.domain.Parent */insert 
    into
        parent (name,member_id) 
    values
        (?,?)
Hibernate: 
    /* insert for
        com.example.jpa.domain.Child */insert 
    into
        child (name,parent_id,member_id) 
    values
        (?,?,?)
Hibernate: 
    /* insert for
        com.example.jpa.domain.Child */insert 
    into
        child (name,parent_id,member_id) 
    values
        (?,?,?)

이처럼 부모 엔티티를 저장할때 부모 엔티티에 저장되어있는 자식 (정확하게는 부모 엔티티의 컬렉션 안에 있는)엔티티도 전부 저장하는 기능이다.

주의

  • 영속성 전이는 연관관계를 매핑하는 것과 아무 관련 없음
  • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐이다.

 

4. 고아객체

고아 객체부모 엔티티와 연관관계가 끊어진 자식 엔티티를 의미하는데 

연관관계 속성 중 orphanRemoval라는 옵션을 true로 하면, 고아 객체들을 자동 삭제해준다.

package com.example.jpa.domain;  
  
import jakarta.persistence.*;  
import lombok.Getter;  
import lombok.Setter;  
  
import java.util.ArrayList;  
import java.util.List;  
  
@Entity  
@Getter  
@Setter  
public class Parent {  
@Id  
@GeneratedValue  
@Column(name = "member_id")  
private Long id;  
private String name;  
@OneToMany(mappedBy = "parent",cascade = CascadeType.ALL, orphanRemoval = true)  
private List<Child> childList = new ArrayList<>();  
  
public void addChild(Child child){  
childList.add(child);  
child.setParent(this);  
}  
}

childList에서 첫번째 child를 제거하는 테스트코드를 작성

Child child1 = new Child();  
child1.setName("child1");  
Child child2 = new Child();  
child2.setName("child2");  
  
Parent parent = new Parent();  
parent.addChild(child1);  
parent.addChild(child2);  
  
em.persist(parent);  
  
em.flush();  
em.clear();  
  
Parent findparent = em.find(Parent.class, parent.getId());  
findparent.getChildList().remove(0);

쿼리를 확인해보면 delete 쿼리가 나가고 있다.

Hibernate: 
    select
        p1_0.member_id,
        p1_0.name 
    from
        parent p1_0 
    where
        p1_0.member_id=?
Hibernate: 
    select
        c1_0.parent_id,
        c1_0.member_id,
        c1_0.name 
    from
        child c1_0 
    where
        c1_0.parent_id=?
Hibernate: 
    /* delete for com.example.jpa.domain.Child */delete 
    from
        child 
    where
        member_id=?

실제 DB

 

parent

child

id가 1번인 데이터가 삭제된것을 볼 수 있다.

 

주의

  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
  • 참조하는 곳이 하나일 때사용해야함!
  • 특정 엔티티를 개별 소유할 때 사용

영속성 전이 + 고아 객체

  • CascadeType.ALL + orphanRemovel=true
  • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
  • 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음
  • 도메인 주도 설계(DDD)의 AggreGrate Root 개념을 구현할 때 유용하다고 한다.

 

CascadeType.ALL 혹은 REMOVE와 orphanRemovel이 기능적으로 비슷해보이지만

orphanRemoval은 연관관계가 끊어진 엔티티를 삭제하는 반면, CascadeType.REMOVE는 부모 엔티티가 삭제될 때 연관된 자식 엔티티도 함께 삭제한다.

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

JPA - 값 타입  (0) 2023.10.12
JPA - 프록시  (0) 2023.10.10
JPA - 상속관계 매핑, @MappedSuperclass  (0) 2023.10.04
JPA - 연관관계  (0) 2023.10.01
JPA - 기본 키 매핑 : @GeneratedValue  (0) 2023.09.23

1. EntityManager.find() vs EntityManager.getReference()

이하 EntityManager = em

 

JPA에는 em.find 말고도 em.getReference라는 이름 그대로 참조를 가져오는 메서드가 제공된다.

em.find는 데이터베이스를 통해서 실제 엔티티 객체를 바로 조회하고

em.getReference는 데이터베이스 조회를 미루는 가짜 엔티티 객체를 조회한다.

 

결론적으로 DB의 쿼리가 안나가는데 객체가 조회가 된다.

 

1) em.find()

Member member = new Member();  
member.setUsername("hello");  
  
em.persist(member);  
  
em.flush();  
em.clear();  
  
Member findMember = em.find(Member.class, member.getId());  
System.out.println("findMember = " + findMember.getId());  
System.out.println("findMember = " + findMember.getUsername());

em.find() 시점에서 실제 데이터를 불러오는 쿼리를 날린다.

Hibernate: 
    select
        m1_0.member_id,
        m1_0.created_by,
        m1_0.created_date,
        m1_0.last_modified_by,
        m1_0.last_modified_date,
        t1_0.team_id,
        t1_0.created_by,
        t1_0.created_date,
        t1_0.last_modified_by,
        t1_0.last_modified_date,
        t1_0.name,
        m1_0.username 
    from
        member m1_0 
    left join
        team t1_0 
            on t1_0.team_id=m1_0.team_id 
    where
        m1_0.member_id=?
findMember = 1
findMember = hello

2) em.getReference()

위 코드에서 em.find() 메서드만 em. getReference() 로 변경했다.

Member findMember = em.getReference(Member.class, member.getId());

getId() 부분까지는 이미 엔티티를 조회하는 시점에서 getId() 값을 가지고 있기 때문에 조회하지 않다가,

실제 데이터의 값이 필요하게 되는 getUserName() 시점에 조회쿼리를 날려 값을 조회한다.

findMember.getId() = 1
Hibernate: 
    select
        m1_0.member_id,
        m1_0.city,
        m1_0.street,
        m1_0.zipc5de,
        m1_0.username,
        m1_0.work_city,
        m1_0.work_street,
        m1_0.work_zipcode,
        m1_0.end_date,
        m1_0.start_date 
    from
        member m1_0 
    where
        m1_0.member_id=?
findMember.getUsername() = hello

그렇다면 getReference()의 Member 객체는 뭘까?

 

아래 쿼리를 추가하여 실행하면,

System.out.println("findMember = " + findMember.getClass());

진짜 Member객체가 아닌, 프록시 객체라는것을 알 수 있다.

findMember = class com.example.jpa.domain.Member$HibernateProxy$gPJPsc5E

이 프록시 객체의 내부에는 target이라는 것이 있는데, 이것이 진짜 엔티티를 가르킨다.

초기에는 껍데기만 있고, ID값만 들고있는 가짜가 반환된다.

2. 프록시 특징

엔티티의 프록시 객체는 어떻게 생겼냐 하면, 프록시 객체는 실제 객체의 taget이라는 참조를 보관한다.

예를들어, Member엔티티 프록시 객체에서 getName을 호출하면, 

프록시 객체는 실제 target에 있는 엔티티의 getName을 대신 호출한다.

그런데 처음에는 타겟이 없다.
왜냐하면 이걸 실제 DB에서 조회한 적이 없기 때문이다.

 

프록시의 특징은 실제 객체 엔티티를 상속받아서 만들어진다.
그래서 실제 클래스와 겉모양이 같다.
이는 하이버네이트가 내부적으로 여러 프록시 라이브러리를 이용해 만든다.

 

정리

  • 실제 클래스를 상속 받아서 만들어짐
  • 실제 클래스와 겉 모양이 같다.
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨(이론상)
  • 프록시 객체는 실제 객체의 참조(target)을 보관
  • 프록시 객체를 호출하면 객체는 실제 객체의 메소드 호출

3. 프록시 객체의 초기화

1) 프록시 객체가 초기화되는 과정

프록시 객체를 getReference로 가져왔을 때, member.GetName을 호출하면, 
target에는 Member의 값이 처음에는 없었다가, JPA가 영속성 컨텍스트에 Member 값을 요청한다.

그러면 영속성 컨텍스트가 DB를 조회해서 실제 엔티티 객체를 생성한다. 그리고 프록시 객체의 target에 연결을 시켜준다.
즉, 프록시 내부에는  Member target 이라는 진짜 객체 변수가 있다고 보면 된다.

그래서 프록시 객체에서 getName을 했을 때, target의  getName을 통해 실제 객체에 있는 getName이 반환된다.

 

2) 한번 초기화된 프록시는 다시 DB를 조회할 필요가 없다.


영속성 컨텍스트에서 초기화를 요청하는 부분이 중요하다.
프록시에 값이 없을 때 진짜 값을 달라고 요청하는 것이다. 
DB를 통해서 진짜 값을 가지고 와서 진짜 엔티티를 만드는 과정을 의미한다. 
그리고 이것이 초기화를 의미한다.

그리고 한번 초기화 되면 target에 객체 값이 걸리기 때문에 다시 DB를 조회할 일은 없다.
그래서 getName을 2번 요청하더라도 쿼리는 한번만 나간다.

 

em.getReference()로 userName 조회 시 쿼리

Hibernate: 
    select
        m1_0.member_id,
        m1_0.created_by,
        m1_0.created_date,
        m1_0.last_modified_by,
        m1_0.last_modified_date,
        t1_0.team_id,
        t1_0.created_by,
        t1_0.created_date,
        t1_0.last_modified_by,
        t1_0.last_modified_date,
        t1_0.name,
        m1_0.username 
    from
        member m1_0 
    left join
        team t1_0 
            on t1_0.team_id=m1_0.team_id 
    where
        m1_0.member_id=?
findMember.getUsername() = hello
findMember.getUsername() = hello

3) 실제 객체와 프록시 객체의 타입 비교 

 

또한 타입비교를 할 때 == 비교를 사용하지 말고 instance of를 사용해야한다.

왜냐하면 같은 객체끼리의 비교는 괜찮지만, em.find()와, em.getReference()를 == 비교한다면 false가 나올 수 있기 때문이다.

 Member member1 = new Member();
        member1.setUsername("hello1");
        em.persist(member1);

        Member member2 = new Member();
        member2.setUsername("hello2");
        em.persist(member2);

        em.flush();
        em.clear();

        Member findMember1 = em.find(Member.class, member1.getId());
        Member findMember2 = em.getReference(Member.class, member2.getId());

        System.out.println("findMember1.getClass() = " + findMember1.getClass());
        System.out.println("findMember2.getClass() = " + findMember2.getClass());
        System.out.println("findMember1 == findMember2 : " + (findMember1.getClass() == findMember2.getClass()));

이 경우엔 실제 객체와 프록시 객체가 비교되므로 false가 나온다.

== 연산자는 객체의 참조를 비교한다. 즉, 두 객체가 메모리 상에서 동일한 위치에 있는지를 확인하는 것이다.

 

그렇기 때문에, 실제 엔티티 객체와 그 엔티티의 프록시 객체가 서로 다른 메모리 위치에 있다면

== 연산자로 비교했을 때 결과는 false가 된다.

findMember1.getClass() = class com.example.jpa.domain.Member
findMember2.getClass() = class com.example.jpa.domain.Member$HibernateProxy$pNqeG26x
findMember1 == findMember2 : false

 

4) 영속성 컨텍스트에 엔티티가 이미 있을 때 em.getReference()

 

같은 id를 참조하는 경우,  이미 find() 하여 1차 캐시에 엔티티가 저장되어있는 상태라면, getReference() 하더라도 프록시가 아닌, 실제 객체를 반환한다.

Member member1 = new Member();  
member1.setUsername("hello1");  
em.persist(member1);  
  
  
em.flush();  
em.clear();  
  
Member findMember1 = em.find(Member.class, member1.getId());  
System.out.println("findMember1.getClass() = " + findMember1.getClass());  
  
Member findMember1Refer = em.getReference(Member.class, member1.getId());  
System.out.println("findMember1Refer.getClass() = " + findMember1Refer.getClass());


첫번째 이유는 1차캐시에 이미 실제 엔티티가 존재하기 때문이고, 
두번째 이유는 JPA는 같은 엔티티의 같은 id라면 라면 항상 == 비교를 true를 보장하기 때문이다.

findMember1.getClass() = class com.example.jpa.domain.Member
findMember1Refer.getClass() = class com.example.jpa.domain.Member

 

JPA는 같은 엔티티와 id라면 항상 == 비교가 참이 되도록 보장해주기 때문에, getReference()후 find()를 하게 되면 

둘 다 프록시 객체로 조회된다.

Member member1 = new Member();  
member1.setUsername("hello1");  
em.persist(member1);  
  
  
em.flush();  
em.clear();  
  
Member refMember = em.getReference(Member.class, member1.getId());  
System.out.println("refMember.getClass() = " + refMember.getClass());  
  
Member findMember = em.find(Member.class, member1.getId());  
System.out.println("findMember.getClass() = " + findMember.getClass());  
  
System.out.println("refMember == findMember : " + (refMember == findMember));

그렇기에 재미있게도 refMember.getClass()가 호출되기 직전에 조회쿼리를 호출하지만, 프록시 객체로 조회되는것을 볼 수 있다.

refMember.getClass() = class com.example.jpa.domain.Member$HibernateProxy$a1lJoMHB
Hibernate: 
    select
        m1_0.member_id,
        m1_0.created_by,
        m1_0.created_date,
        m1_0.last_modified_by,
        m1_0.last_modified_date,
        t1_0.team_id,
        t1_0.created_by,
        t1_0.created_date,
        t1_0.last_modified_by,
        t1_0.last_modified_date,
        t1_0.name,
        m1_0.username 
    from
        member m1_0 
    left join
        team t1_0 
            on t1_0.team_id=m1_0.team_id 
    where
        m1_0.member_id=?
findMember.getClass() = class com.example.jpa.domain.Member$HibernateProxy$a1lJoMHB
refMember == findMember : true

5) hibernate.LazyInitializationException - 영속성 컨텍스트에 값이 없거나 컨텍스트 자체가 닫힌 경우

 

프록시 객체에서 username을 호출하면 프록시 객체에서 영속성 컨텍스트에게 실제객체를 호출해달라고 요청하는데,

 영속성 컨텍스트에게 호출하기 전에 준영속을 시켜버리면 영속성 컨텍스트에는 캐시가 없기 때문에

 JPA에서는 실제 엔티티를 불러올수 없게 된다. close(), clear() 모두 동일하다.

 

Member refMember = em.getReference(Member.class, member1.getId());  
System.out.println("refMember.getClass() = " + refMember.getClass());  
  
em.detach(refMember);  
  
refMember.getUsername();

 

Hibernate: 
    /* insert for
        com.example.jpa.domain.Member */insert 
    into
        member (created_by,created_date,last_modified_by,last_modified_date,username,member_id) 
    values
        (?,?,?,?,?,?)
refMember.getClass() = class com.example.jpa.domain.Member$HibernateProxy$PJu5GIAI

could not initialize proxy [com.example.jpa.domain.Member#1] - no Session
org.hibernate.LazyInitializationException: could not initialize proxy [com.example.jpa.domain.Member#1] - no Session

 

정리

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

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

JPA - 값 타입  (0) 2023.10.12
JPA - 즉시로딩과 지연로딩, CASCADE와 고아객체  (1) 2023.10.10
JPA - 상속관계 매핑, @MappedSuperclass  (0) 2023.10.04
JPA - 연관관계  (0) 2023.10.01
JPA - 기본 키 매핑 : @GeneratedValue  (0) 2023.09.23

상속관계 매핑

 

객체에는 상속관계가 존재하지만, DB에는 상속이 존재하지 않는다.

하지만 DB에는 슈퍼- 서브타입 이라는 모델링 기법이 존재하는데, 이것이 객체의 상속관계와 유사하다.

 

상속관계 매핑이란, 객체의 상속, 구조와 DB의 슈퍼타입, 서브타입을 매핑하는것이다.

 

슈퍼-서브타입 모델링으로 공통된 속성들을 하나로 묶거나, 하나로 합칠 수 있다.

이렇게 슈퍼타입, 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법들이 JPA에서 세 가지가 존재한다.

 

1. 데이터를 정규화시켜서 각각의 테이블로 변환하는 조인 전략

 

비즈니스적으로 중요하고 복잡할 때 쓴다.

기본적으로 세 전략 중 이 전략을 많이 사용한다.

슈퍼타입으로 사용할 상위 엔티티 생성 후 @Inheritance(strategy = InheritanceType.JOINED) 를 클레스에 추가한다.

또한, @DiscriminatorColumn 라는 어노테이션을 추가하여 테이블상에 DTYPE 이라는것을 추가하는데,

조인된 서브타입 테이블의 이름을 저장해 데이터를 구분하는데 도움을 준다.

(name속성을 변경하여 컬럼명을 DTYPE 외에도 다양하게 사용할 수 있다. )

상속받은 엔티티 클레스에 @DiscriminatorValue(value = "M") 를 추가해주면,(@DiscriminatorValue("M") 도 가능)

엔티티 명 대신 설정한 값인 M이 들어간다.

 

이후 아래처럼 하위 엔티티에서 상위 엔티티를 상속받으면 된다.

결론적으로 아래 코드에서 변경점 하나 없이 @ Inheritance의 stategy 속성 변경만으로 테이블 구조가 달라진다.

package com.example.jpa.domain;  
  
import jakarta.persistence.*;  
import lombok.Getter;  
import lombok.Setter;  
  
import java.util.ArrayList;  
import java.util.List;  
  
@Entity  
@Getter  
@Setter  
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
public class Item {  
  
@Id @GeneratedValue  
@Column(name = "item_id")  
private Long id;  
  
private String name;  
private int price;  
private int stockQuantity;  
@ManyToMany(mappedBy = "items")  
private List<Category> categories = new ArrayList<>();  
}

package com.example.jpa.domain;  
  
import jakarta.persistence.Entity;  
  
@Entity  
public class Book extends Item{  
private String author;  
private String isbn;  
}

package com.example.jpa.domain;  
  
import jakarta.persistence.Entity;  
@Entity  
public class Album extends Item{  
private String artist;  
}

package com.example.jpa.domain;  
  
import jakarta.persistence.Entity;  
  
@Entity  
public class Movie extends Item{  
private String director;  
private String actor;  
}

상위 테이블과 하위 테이블 모두를 접근해야하므로 쿼리가 2번 나간다는 특징이 있다.

 

  • 장점
    • 테이블 정규화
    • 외래 키 참조 무결성 제약조건 활용 가능
    • 저장공간 효율화
  • 단점
    • 조회 시 조인을 많이 사용, 성능 저하
    • 조회 쿼리가 복잡함
    • 데이터 저장 시 INSERT SQL 2번 호출

가장 정석적인 전략이다.

 

2. 여러 테이블을 하나라 통합하는 단일 테이블 전략

 

논리 모델을 한 테이블로 구성하는 전략이다.

데이터도 얼마 안되고 너무 단순하고 확장할 일도 없을때 쓰면 좋다고 한다.

 

위에 상술한 @Inheritance 속성을 InheritanceType.SINGLE_TYPE으로 변경하면 된다.

Hibernate: 
    create table item (
        price integer not null,
        stock_quantity integer not null,
        item_id bigint not null,
        dtype varchar(31) not null,
        actor varchar(255),
        artist varchar(255),
        author varchar(255),
        director varchar(255),
        isbn varchar(255),
        name varchar(255),
        primary key (item_id)
    ) engine=InnoDB

위 코드들을 보면, 각자의 세부 엔티티들은 상위 엔티티인 item을 확장했고, 

그 item 테이블 쿼리를 보면 테이블에 세부 엔티티가 전부 들어간것을 볼수 있다.

  • 장점
    • 조인이 필요없으므로 일반적인 조회 성능이 빠름
    • 조회 쿼리가 단순함
  • 단점
    • 자식 엔티티가 매핑한 컬럼은 모두 null 허용
    • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있는 상황에 따라서 조회 성능이 느려질 수 있다.

3. 슈퍼타입으로 모으지 않고 서브타입에 각각 속성들을 포함시키는 구현 클래스마다 전략

 

@Inheritance 속성을 InheritanceType.TABLE_PER_CLASS 로 변경하면 된다.

상위 테이블을 없애고 각각의 하부 테이블에 상위테이블 값들을 넣는 방식이다.

참고로, 이 전략에서는 @DiscriminatorColumn 이 적용되지 않는다.

상위 테이블인 ITEM이 사라졌기 때문에, 구분할 필요가 없기 때문이다.

 

이 전략은 단순하게 값을 넣고 뺄 때는 딱 좋은데,  상위타입으로 엔티티를 find하게되면 문제가 생긴다.

타입을 구분할 수 없기 때문이다.

Item findItem = em.find(Item.class, movie.getId());  
log.info("findItem={}",findItem);

 

조회시 쿼리가 너무 복잡해진다.

Hibernate: 
    select
        i1_0.id,
        i1_0.clazz_,
        i1_0.name,
        i1_0.price,
        i1_0.stock_quantity,
        i1_0.artist,
        i1_0.author,
        i1_0.isbn,
        i1_0.actor,
        i1_0.director 
    from
        (select
            price,
            stock_quantity,
            id,
            artist,
            name,
            null as author,
            null as isbn,
            null as actor,
            null as director,
            1 as clazz_ 
        from
            album 
        union
        all select
            price,
            stock_quantity,
            id,
            null as artist,
            name,
            author,
            isbn,
            null as actor,
            null as director,
            2 as clazz_ 
        from
            book 
        union
        all select
            price,
            stock_quantity,
            id,
            null as artist,
            name,
            null as author,
            null as isbn,
            actor,
            director,
            3 as clazz_ 
        from
            movie
    ) i1_0 
where
    i1_0.id=?

 

  • 장점
    • 서브 타입을 명확하게 구분해서 처리할 때 효과적
    • not null 제약조건 사용 가능
  • 단점
    • 여러 자식 테이블을 함께 조회 할 때 성능이 느림(UNION SQL)
    • 자식 테이블을 통합해서 쿼리하기 어려움

@MappedSuperclass

테이블과는 관계 없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할을 한다.

주로 등록일, 수정일, 등록자, 수정자 같이 전체 엔티티에서 공통적으로 사용하는 정보를 모을 때 사용한다.

 

package com.example.jpa.domain;  
  
import jakarta.persistence.MappedSuperclass;  
import lombok.Getter;  
import lombok.Setter;  
  
import java.time.LocalDateTime;  
@Getter  
@Setter  
@MappedSuperclass  
public abstract class BaseEntity {  
private String createdBy;  
private LocalDateTime createdDate;  
private String lastModifiedBy;  
private LocalDateTime lastModifiedDate;  
}
------------------------------------
public class Team extends BaseEntity {...}

 참고 : @Entity 클래스는 엔티티나 @MappedSuperClass로 지정한 클래스만 상속가능하다.

 

  • 특징 
    • 상속관계 매핑이 아님
    • 엔티티 X, 테이블과 매핑 X
    • 부모 클래스를 상속받는 자식 클래스에 매핑 정보만 제공한다.
    • 조회, 검색 불가(em.find(BaseEntity) 불가)
    • 직접 생성해서 사용할 일이 없으므로 추상 클래스 권장

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

JPA - 즉시로딩과 지연로딩, CASCADE와 고아객체  (1) 2023.10.10
JPA - 프록시  (0) 2023.10.10
JPA - 연관관계  (0) 2023.10.01
JPA - 기본 키 매핑 : @GeneratedValue  (0) 2023.09.23
JPA - 플러시, 준영속 상태  (0) 2023.09.20

 연관관계 매핑이란, JPA에서 객체 간의 관계를 데이터베이스 테이블 간의 관계와 매핑하는 것을 말한다.

 

연관관계 매핑에 대해서는 고려해야할점이 크게 3가지가 존재한다.

1. 단방향, 양방향

테이블

  • 외래키 하나로 조인 가능
  • 방향이라는 개념이 없음

객체

  • 참조용 필드가 있는 쪽으로만 참조 가능
  • 한쪽만 참조하면 단방향
  • 양쪽이 서로 참조하면 양방향

2. 연관관계 주인

  • 외래키를 관리하는 참조를 의미
  • 주인의 반대편은 단순 조회만 가능
  • 테이블은 외래키 하나로 두 테이블이 연관관계를 맺음
  • 객체의 양방향 관계는 A->B, B->A처럼 참조가 2군데

3. 다중성

엔터티 간의 관계의 다중성을 나타낸다 . A 엔터티에서 B 엔터티로의 참조가 여러 개가 가능한지 , 혹은 하나만 참조가 가능한지, 아니면 서로가 서로 여러개를 참조 할 수 있는지 등을 설정한다.

N:1

단방향

N 쪽인 엔터티가 외래키를 가지며, 이를 연관관계의 주인으로 설정한다.

@JoinColumn 어노테이션을 사용하여 연관관계의 외래키 컬럼명을 지정할 수 있다.

양방향

양방향인 경우에는 참조된 엔티티 변수에 @ManyToOne(mappedBy = "엔티티변수명") 을 추가한다. 이 때, mappedBy 속성에는 연관관계의 주인이 아닌 반대편 엔터티의 필드명을 지정한다.

1:N

1 쪽인 엔티티가 연관관계의 주인이 된다.

DB의 1:N 관계에는 언제나 N쪽에 외래키가 존재하지만, 엔티티에서는 1쪽에 외래키가 존재할 수 있기 때문에 가능한 방법이다.

하지만 이런 DB와 객체의 관계차이 때문에 설계, 운영에 어려움이 많아, 일반적으로 권장되지 않는 방법이다.

또한, 연관관계 관리를 위해 추가적으로 UPDATE SQL이 실행된다.

단방향

N:1과는 반대로 @OneToMany가 붙은 변수에 @JoinColumn을 추가하고, 외래키 컬럼명을 지정한다. 또한 @JoinColumn을 추가하지 않으면 조인 테이블 방식(중간 테이블 생성)을 사용한다.

양방향

1:N 양방향은 JPA 스펙상 존재하지는 않지만 읽기전용 필드를 사용해서 양방향처럼 구현이 가능하다.

N쪽에도 @JoinColumn을 추가한다. 추가로 N쪽에는 연관관계 주인이 되는것을 막기 위해 속성에 insertable, updatable을 모두 false로 설정하여 읽기전용으로 만든다.

1:1

주 테이블과 대상 테이블 어느쪽에도 외래키를 선택할 수 있다.

외래키에 유니크 제약조건을 걸어야 1:1이 된다.

주 테이블에 외래키 단방향

@OneToOne, @JoinColumn을 이용하여 연결한다.

주 테이블에 외래키 양방향

1:1 관계에서 양방향 관계는 참조된 엔티티 변수쪽에 마찬가지로 @OneToOne을 추가하고, mappedBy설정을 해주면 된다.

대상 테이블에 외래키 단방향

지원하지 않는다.

 

 

대상 테이블에 외래키 양방향

연관관계의 주인을 대상 테이블쪽 엔티티로 잡아서 구현한다.

개발자 입장에서는 주 테이블에 외래키가 있는것이 성능적으로 좋다. 주 테이블의 값을 조회할 때 대상테이블을 확인하는데 있어서 주 테이블만 확인하면 되기 때문이다.

반대로 대상 테이블에 외래키가 존재한다면 주테이블 엔티티만으로는 확인 할 수 없으므로 무조건적으로 대상테이블을 조회해야 한다. 그러므로 지연로딩으로 설정해도 항상 즉시로딩된다.

 

N:N

실무에서는 권장하지 않는다

 

데이터베이스에서의 N:N

관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.

연결 테이블을 추가해서 1:N, N:1 관계로 풀어내야 한다.

객체에서의 N:N

객체에서는 컬렉션을 사용하여 N:N 관계를 표현 할 수 있다.

단방향

@ManyToMany 어노테이션과, 중간 테이블 생성을 위한 @JoinTable을 추가한다.

양방향

@ManyToMany.(mappedBy = "xxx")를 추가한다.

 

@ManyToMany의 한계

쿼리도 복잡하고 중간 테이블에 무엇인가 추가 정보를 넣기가 불가능하기 때문에 실무에서는 사용하기 어렵다.

 

@ManyToMany의 한계 극복

@ManyToMant, @JoinTable로 중간테이블을 생성하는 대신,

중간 테이블을 엔티티로 승격시키고, @ManyToMany를 @ManyToOne, @OneToMany로 변경한다.

중간 테이블쪽을 @ManyToOne으로 잡고 연결이 필요한 테이블들을 @OneToMany로 잡는다.

 

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

JPA - 프록시  (0) 2023.10.10
JPA - 상속관계 매핑, @MappedSuperclass  (0) 2023.10.04
JPA - 기본 키 매핑 : @GeneratedValue  (0) 2023.09.23
JPA - 플러시, 준영속 상태  (0) 2023.09.20
JPA - 영속성컨텍스트(2)  (0) 2023.09.20

JPA는 기본적으로 1차 캐시에 @Id로 키를 가지고 @Entity로 값을 가진다. 

 

그렇기에 1차 캐시에 엔티티를 저장하려면 @Id의 값이 필수적인데, @GeneratedValue는 자동으로 이 id값을 생성해준다.

 

@GeneratedValue

GenerationType.IDENTITY:

  • id값(기본 키)을 DB에서 생성한다.
  • JPA는 기본적으로 트랜잭션이 끝난 직후 데이터가 DB에 반영되지만, IDENTITY인 경우에는 엔티티를 영속시킬 때 쿼리가 DB에 반영된다.

GenerationType.IDENTITY은 왜 엔티티가 영속될때 쿼리가 DB에 반영되는가?

  • IDENTITY는 DB에서 id값을 넘겨받는다. 즉, DB에 넘어가기 전 까지는 id값이 존재하지 않는다.
  • JPA의 1차 캐시는 id값을 가지고 있어야 저장할수 있으므로, DB에 id값을 받아지 않는 한 저장할 수 없다.
  • 그렇기 때문에 JPA에서 IDENTITY 설정을 사용 할 경우에 영속화를 하는 시점에 미리 쿼리를 날려서 id를 조회하여 1차 캐시에 엔티티를 저장한다. 

GenerationType.SEQUENCE:

  • DB에서 id값을 가져오지만 IDENTITY와 다르게 다량으로 id값을 가져올 수 있다.
  • 차이점은 IDENTITY와 다르게 영속을 하더라도 트랜잭션이 끝난 시점에 DB에 반영된다.
  • @SequenceGenerator 로 시퀀스를 따로 설정할 수 있다.(따로 설정하지 않을 시 hibernate_sequence의 값을 사용한다)

GenerationType.SEQUENCE는 어떻게 IDENTITY와 다른가?

  •  @SequenceGenerator의 속성 중, allocationSize 라는 속성은 DB에서 sequence값을 한번에 몇개씩 가져올지를 정한다.
  • 이 allocationSize의 기본값은 50인데,  1부터 51까지의 시퀀스값을 db에서 생성하여 미리 JPA에 가져와서 어플리케이션이 종료될 때 까지 가져온다는 의미다.
  • 이렇게 가져온 시퀀스 값을 엔티티를 영속할 때 마다 하나씩 부여하여 1차 캐시에 저장할 수 있다. 
  • 동시성 이슈없이 다양한 문제들을 해결 할 수 있다고 한다.

GenerationType.TABLE

  • DB의 시퀀스를 흉내내는 키 전용 생성 테이블을 생성한다.
  • 실제 운영에서 사용하기에는 기존의 DB 생성 규칙과 다를 수 있으므로 쓰기가 어렵다.
  • 모든 DB에 적용할 수 있다는 장점이 있지만, 보통은 테이블 전략을 잘 사용 하지 않는다고 한다.

결론

전략은 SEQUENCE를 사용하자

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

JPA - 상속관계 매핑, @MappedSuperclass  (0) 2023.10.04
JPA - 연관관계  (0) 2023.10.01
JPA - 플러시, 준영속 상태  (0) 2023.09.20
JPA - 영속성컨텍스트(2)  (0) 2023.09.20
JPA - 영속성 컨텍스트(1)  (0) 2023.09.18

1. 플러시

플러시란 영속성 컨텍스트의 변경내용을 데이터베이스에 반영하는것을 의미한다.

 

플러시가 일어나면 어떤일이 일어나는가?

1. 변경감지가 일어난다. 그리고

2. 수정된 엔티티의 쿼리를 지연 SQL 저장소에 등록한다. 그리고

3. 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송한다.

4. 플러시를 보낸다고 해서 커밋이 되는것은 아니다.

 

영속성 컨텍스트를 플러시하는 방법

em.flush() - EntityManager의 flush를 직접 호출하는 방법.

member200이라는 맴버객체를 생성하고, em.persist를 한다고 하면,

현재 member 객체는 영속성 컨텍스트, 즉 1차 캐시에 저장된 상태이다.

또 동시에 지연 SQL 저장소에 객체를 생성하는 쿼리가 저장된다.

이후 em.flush()를 하게 되면 

트랜잭션이 커밋되기 전에 member 생성 쿼리가 데이터베이스에 호출된다.

 

트랜잭션 커밋 - 트랜직션을 커밋하면 자동으로 플러시가 호출.

위 코드처럼 따로 flush를 호출하지 않는다면 실제 플러시가 실행되는 시점은 커밋이 실행되는 시점이다.


JPQL 쿼리 실행 - 플러시 자동 호출

JPA 기본설정이 JPQL을 실행하면 무조건 플러시가 호출되게 되어있다.

플러시의 모드옵션을 설정해서 변경할 수 있지만, 크게 도움이 되지 않는다고 한다.

 

정리

1. 플러시는 영속성 컨텍스트를 비우지 않는다.

2. 플러시는 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화 한다.
3. 트랜잭션이라는 작업 단위가 중요하기 때문에 커밋 직전에만 동기화 하면 된다.

 

2. 준영속 상태

영속 -> 준영속

영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detatched)되는 것
영속성 컨텍스트가 제공하는 기능을 사용 못한다.

준영속 상태로 만드는 방법

em.detach(entity); 

특정 엔티티만 준영속 상태로 전환한다.

150번 객체를 em.find로 찾았다. 이렇게되면 영속성컨텍스트에 150번 맴버객체가 등록된다.

이후 맴버의 이름을 "AAAA"로 변경했다. 

본래대로라면 커밋이 되었을 때 JPA에서 변경감지가 일어나 업데이트 쿼리가 생성되어야 정상이지만

현재는 em.detach를 사용하여 맴버 엔티티를 준영속 상태로 전환했다. 즉 영속성 컨택스트에서

맴버 엔티티를 빼내었다는 이야기이므로 em.detach(member)가 일어난 시점 이후로에는

영속성 컨텍스트에 맴버 객체가 없으므로 변경감지가 일어나지 않고

결론적으로 조회 쿼리만 남게 되는 것이다.

 

em.close();

영속성 컨텍스트를 완전히 초기화시킨다.

em.detach(entity)와 같은 결과를 반환한다.

 

em.close();

영속성 컨텍스트를 종료시킨다.

위와 동일.

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

JPA - 상속관계 매핑, @MappedSuperclass  (0) 2023.10.04
JPA - 연관관계  (0) 2023.10.01
JPA - 기본 키 매핑 : @GeneratedValue  (0) 2023.09.23
JPA - 영속성컨텍스트(2)  (0) 2023.09.20
JPA - 영속성 컨텍스트(1)  (0) 2023.09.18

+ Recent posts