Home JPA 기본
Post
Cancel

JPA 기본

엔티티 매핑

0. 들어가기 전, JPA의 DB 스키마 자동 생성

JPA는 DDL을 애플리케이션 실행 시점에 자동 생성해준다.

또한 DBMS마다 조금씩 다른문법을 활용하여 DBMS에 적잘한 DDL을 생성한다.

이렇게 생성된 DDL은 개발단계에서만 사용해야한다.


1. 객체와 RDB를 어떻게 매핑해야하는가??

우선 객체와 테이블과 매핑해야 하는 요소를 정리하자면 다음과 같다.

AppAnnotationDB
객체@Entity, @Table테이블
필드@Column컬럼
기본 키@Id기본 키
연관관계@ManyToOne, @JoinColumn연관관계

2. 객체와 테이블을 매핑하려면?

@Entity 어노테이션이 붙은 클래스는 JPA가 관리한다. 이를 엔티티라고 한다.

여기서 주의해야할 점이 있는데

기본 생성자가 필수적으로 요구된다. (파라미터가 없는 public, protected –> @NoArgsArgument를 무의식중에 달았던 이유이다.) JPA 스펙상 요구하는 것이다. (JPA의 구현체에서 이에 대한 리플렉션, 프록싱 등… 을 사용한다고 한다.)

final 클래스, enum, interface, inner 클래스에는 사용하지 않아야한다.

저장할 필드에 final 키워드를 붙이면 안된다.


3. 필드와 컬럼을 매핑하려면?

일반적인 자료형의 필드는 다음과 같이 매핑하면 된다. (여기서 name은 생략해도됨)

@Column(name = “name)

Enum 타입의 필드는 다음과 같이 매핑하면 된다.

@Enumerated(EnumType.STRING)

이 때 EnumType.ORDINAL, EnumType.STRING 이 있는데, 이는 각각 Enum 순서와, Enum 이름을 데이터베이스에 저장하는 것이다.

또한 ORDINAL이 기본값이기 때문에 되도록이면 EnumType.STRING으로 매핑된 내용을 인지할 수 있게 하는 것이 좋다.

날짜/시간 같은 필드는 다음과 같이 매핑하면 된다.

@Temporal(TemporalType.TIMESTAMP)

이미지파일 등과 같이 큰 컨텐츠를 담는 타임은 다음과 같이 매핑한다.

@Lob

JPA에 매핑하고 싶지 않은 컬럼은 다음과 같은 어노테이션을 붙여준다.

@Transient


3.1. @Column 더 자세히 알아보기

@Column 어노테이션은 여러가지 속성 정보를 담을 수 있다.

속성 1. name

필드와 매핑할 테이블의 컬럼 이름을 나타낸다. 기본값은 객체의 필드 이름으로 하되 카멜케이스로 작성한 경우, _로 매핑된다.

속성 2. insertable, updatable

등록, 변경 가능 여부에 대한 속성이다. 기본값은 두 속성 모두 TRUE이다.

속성 3. nullable (DDL)

null 값의 허용 여부를 설정한다. false로 설정하면 DDL 생성 시에 not null 제약조건이 붙는다.

속성 4. unique (DDL)

@Table의 uniqueConstraints와 같지만 한 컬럼에 간단히 유니크 제약조건을 걸 때 사용한다.

속성 5. columnDefinition

데이터베이스의 컬럼 정보를 직접 줄 수 있다. (varchar(100), int, etc…)

속성 6. length (DDL)

문자 길이 제약조건을 건다. String 타입에만 사용 가능하며 기본값은 255이다.

속성 7. precision, scale (DDL)

BigDecimal 타입에서 사용한다.(BigInteger도 사용 가능)

precision은 소수점을 포함한 전체 자릿수

scale은 소수의 자리수를 나타낸다.

또한 이 옵션은 double, float 타입에는 적용되지 않는다.


4. 기본 키 매핑

4.1. 매핑 어노테이션

  • @Id
  • @GeneratedValue

기본 키를 직접 할당할 경우

@Id 만 사용한다.

기본 키를 자동 할당할 경우

@Id, @GeneratedValue 를 사용한다.

여기서 @GeneratedValue는 여러 옵션을 추가할 수 있다.

IDENTITY

자동할당에 대한 동작을 데이터베이스에 위임한다.(MySQL auto_increment 일 때 사용하자)

또한 아래와 같은 특징이 있다는 것읏 알아둬보자.

AUTO_INCREMENT는 데이터베이스에 INSERT Query를 날린 후에야 ID를 알 수 있다.

그렇기 때문에 이 전략은, em.persist()로 유저 생성 후 즉시 INSERT를 한 후에야 user.getId() 와 같은 문구로 ID를 다룰 수 있게 된다.

SEQUENCE

데이터베이스 시퀀스 오브젝트를 사용하여 자동할당한다.(ORACLE 환경에서 사용하자)

또한, @SequenceGenerator 가 필요하다.

TABLE

키 생성용 테이블을 사용한다.(모든 DBMS에서 사용가능하다.) 또한, @TableGenerator가 필요하다.

성능은 좀 아쉬운 방법이다.

AUTO

각 DBMS 문법에 맞게 자동지정된다.이 옵션은 기본값이다.


연관관계 매핑

단방향 연관관계 매핑 - 객체지향 패러다임과 맞지 않은 상황

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

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

    @Column(name = "TEAM_ID")
    private Long teamId;
}

@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;

    @Column
    private String name;
}

위 코드는 연관관계에 대한 내용을 객체로 이어붙인 것이 아닌, 값 객체를 통해 이어 붙여져 있다. 객체지향스럽지 않다고 볼 수 있는 코드이다.

그럼 어떻게 객체지향스럽게 연관관계를 매핑할 수 있는걸까?


단방향 연관관계 매핑 - 객체지향 패러다임에 부합할 수 있도록

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

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

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team teamId;
}

@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;

    @Column
    private String name;
}

위와 같이 코드를 작성하면 우리가 흔히 객체를 다루듯이 Team 내부에 존재할 Getter/Setter 등 도메인 메서드를 사용할 수 있게 된다.

한마디로 정리하면, 위와 달리 데이터 중심으로 모델링 한 것이 아니기 때문에 협력 관계를 구성할 수 있게 된 것이다.

단방향 연관관계는 사실 크게 어려운 개념도 아니고 사용하기에도 어렵지 않다.


양방향 연관관계, 양방향 연관관계의 주인

Member에서 Team을 사용하고,

Team에서 Member를 사용할 수 있도록 하기 위한 것이 양방향 연관관계이다.

DB는 사실상 양방향으로, 외래키를 통해서 양 테이블을 왔다갔다 할 수 있다. (구체적으로는, 딱히 방향이라는 개념은 없음)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

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

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team teamId;
}

@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;

    @Column
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

mappedBy가 꼭 필요한 속성인가???

ㅇㅇ 필요하다.

객체는 연관관계가 될 수 있는 상황이 2가지가 있다.

Member -> Team

Team -> Member

따라서 사실상 단방향 연관관계가 2개인 것으로 볼 수 있다. (이걸 억지로 양방향이라고 하는 것임)

테이블은 그냥 양방향임(사실 방향 그런거 없고 외래키를 알면 양쪽을 다 알 수가 있음)

아니 그래서 mappedBy가 꼭 필요한 것인가를 이야기 해보자면

Member라는 객체안에 Team을 변경하고 싶은 상황이 있다고 해보자.

근데 Team이라는 객체에 Member를 List로 받고있으니..

Team 객체에 있는 리스트에서 해당 멤버를 제거할지?

Member 객체에 있는 Team을 변경을 할지?

뭐가 맞는지 모르겠다. 둘다 해버릴까?

여기서 하나의 규칙이 등장한다. 위와 같은 두 가지 고민이 생기는 점에 대해서

하나의 객체에서만 외래키를 관리(등록, 수정)하는 “주인” 이라는 역할을 부여하는 것이다.

이 때, 주인이 아닌 연관관계를 mappedBy를 사용하는 것이다.

또한 주인이 아닌 연관관계에서는 외래키를 관리할 수 없으므로, 읽기 작업만 가능하게 된다.

음.. 그럼 누구를 주인으로 해야할까? 둘 중 아무나 하면되나??

외래키가 있는 곳을 주인으로 하면된다. (@ManyToOne 어노테이션이 붙은, 테이블로 따지면 외래키가 존재하는 객체 (1:N 에서 N이 속하는 테이블))


양방향 연관관계 매핑 시 주의할 점

연관관계의 주인이 아닌 컬럼에서 set으로 외래키를 설정하지 말아야한다!

1
2
3
4
5
6
7
8
Team team = new Team();
team.setName("TeamA");

Member member = new Member();
member.setName("member1");

// 문제가 발생하는 부분!!
team.getMembers().add(member);

이래버리면, 연관관계의 주인이 아닌 곳(앞으로 하인이라고 하겠다.)에서 연관관계에 대한 정보를 넣어주려고 하기 때문에

읽기 전용인 하인 측에서는 뭐 아무것도 할 수가 없다.

그래서 다음과 같이 변경해야한다.

1
2
3
4
5
6
7
8
9
10
11
Team team = new Team();
team.setName("TeamA");

Member member = new Member();
member.setName("member1");

// 문제가 발생하는 부분!!
// team.getMembers().add(member);

// 문제가 발생하는 부분!! --> 해결하기
member.setTeam(team);

근데 사실 더 정답에 가까운 방법이 있다. 양 쪽에 값을 넣어주는 것이다.

객체지향적으로 설계하기 위해서는 이 방법이 조금 더 낫다는 것인데 왜그런지 살펴보자.

바로 영속성 컨텍스트의 1차 캐시에 들어있는 내용을 조회할 수도 있는 상황 때문이다.

조금 더 자세히 말하자면, 나는 분명히 member.setTeam(team); 라는 문구로 Member의 Team을 지정해줬는데

team.getMembers(); 를 실행시키면 1차 캐시에 들어있던 비어있는 members를 가져오게 될 수 있다는 것 때문이다.

물론 이러한 문제는 엔티티 매니저의 flush(), clear()를 실행시키면 해결할 순 있으나 영속성 컨텍스트를 다 비워버리는 것 자체가 좀 별로긴 하다.

이것보단 차라리 team.getMembers().add(member); 로 직접 동기화를 시켜주는 편이 더 낫다.

뭐 객체지향 적으로도 이게 더 어울리는 코드이긴하다.

테스트 코드를 짤 때에도 문제가 생김!!!

마찬가지로 영속성 컨텍스트로 인한 값 동기화 문제가 발생할 수 있으므로 양쪽에 셋팅하자.

연관관계 편의 메서드를 만들어 놓으면 양쪽에 값을 넣는 과정에서의 실수를 줄일 수 있음

1
2
3
4
public void setTeam(Team team) {
    this.team = team;
    team.getMembers().add(team);
}

물론 Setter가 그리 좋은거는 아니지만 예시로 참고하자.

Setter를 자제하려면 그냥 다음과 같이 전용 메서드를 하나 만들면 되는 것이다.

1
2
3
4
public void changeTeam(Team team) {
    this.team = team;
    team.getMembers().add(team);
}

근데 꼭 양방향을 해야하나? 애초에 팀이라는 테이블에 멤버목록을 굳이 넣지 않아도, 멤버 테이블에 팀 정보를 넣어놓으면 될텐데.. (상의해보자)

이처럼 굳이 양방향을 안해도 충분히 요구사항을 만족시킬 수 있을거같은데 실수나 버그의 여지가 많은 양방향을 하는 이유는?

양방향 매핑 시 무한루프에도 조심해야한다.

ToString 메서드를 만들 때에도, 서로가 가진 객체 참조를 호출하게된다.

Member 호출 : Team Team 호출 : Member

그럼 Member -> Team -> Member -> … 이런 무한 루프가 발생할 수 있다.

JSON 생성 라이브러리, Lombok 에서도 문제가 발생하는데 일단 중요한 점은

Entity를 Controller에서 절대 반환하지 말자.

왜냐하면, Entity가 바뀌면 API 스펙이 바뀌는 기이한 현상이 발생하게 된다.

따라서 꼭 DTO를 통해 반환하도록 하자. 그리고 이를 통해 JSON 생성 라이브러리의 무한루프 문제도 해결될 수 있다.

양방향 매핑은 최~~ 대한 하지말자. 단방향 매핑으로 설계를 끝내버리는게 최선이다.

그럼 최대한 자제해야하는 양방향 매핑은 언제 써야하는가?

역방향으로 탐색해야하는 경우가 있을 수 있긴하다. 그럴땐 불가피하게 쓰긴 해야하는데 웬만하면 단방향 연관관계를 잘 셋팅하면 해결된다.


프록시

JPA내에서, 진짜 객체 조회 vs 가짜 객체 조회

1
2
em.find(); // 진짜 쿼리를 날리고, 그 정보에 대한 객체를 가져옴
em.getReference(); // 실제 쿼리는 안나감, 가짜 객체를 가져옴

가짜 객체라는게 무엇인가…

em.getReference()를 실행시키면, Hibernate는 데이터베이스의 조회를 미루는 가짜 엔티티를 조회하는데 이 가짜 엔티티의 구성은 다음과 같다.

1
2
3
4
5
6
7
class Proxy {
    // 실제 객체를 가리키는 참조 값
    Entity target = null;

    // Getter 메서드
    getter();
}

이 프록시의 특징은 실제 클래스를 상속 받아서 만들어진다 따라서 위의 코드를 조금 더 정교하게 작성하면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Proxy extends Member {
    // 실제 객체를 가리키는 참조 값
    Entity target = null;

    // Getter 메서드
    getter();
}

class Member {
    long id;
    String name;

    getter();
}

따라서 실제 클래스와 겉 모양이 똑같아서 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지않고 사용해도 되긴한다. (이론상 된다는 것 주의할 점이 있긴하다.)

프록시 객체는 실제 객체의 참조를 보관한다.

프록시 객체를 호출하면 프록시 객체는 실제 객체의 메서드를 호출하게 되어있다. (상속 관계 및 위임)

프록시가 만들어지고 사용되는 과정

1
2
Member member = em.getReference(Member.class, "id1");
member.getName();
  1. 현재 member는 프록시 객체이기 때문에 getName()메서드를 사용하기 위해선 실제 엔티티를 생성해야한다.
    1. 위/아래의 과정은 프록시가 고유하게 지닌, 실제 객체를 가리키는 참조 값이 없을 때의 상황을 말한다.
  2. 따라서 프록시 객체실제 엔티티의 동작을 사용할 경우에는 영속성 컨텍스트에 member를 초기화하도록 한다.
  3. 이렇게 하면, 영속성 컨텍스트는 실제 DB에 들어가 해당 member를 꺼내온 후 실제 엔티티를 생성해준다.
  4. 실제 엔티티가 생겼으면, 그 엔티티에 대한 getName()메서드를 수행한 후 값을 반환한다.

프록시의 특징

  1. 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
  2. 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것이 아니다. 실제 엔티티가 만들어짐으로써 프록시 객체가 참조할 수 있는 값이 생기는 것이다.
  3. 프록시 객체는 원본 엔티티를 상속 받는다. (타입 체크 시 instance of 를 사용해야한다. == 으로 타입비교 절대 금지!!!!!!)
  4. 영속성 컨텍스트에 이미 찾고자 하는 엔티티가 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다.
    1. JPA는 같은 트랜잭션(영속성 컨텍스트)내에서 같은 타입을 조회 시 반드시 True를 반환하도록 명세되어있기 때문이다.

Member findMember = em.find();

Member refMember = em.getReference();

System.out.println(refMember == findMember);

위 결과는 무엇이 나와야 하는가? –> True 이다.

find 메서드로 영속성 컨텍스트에 엔티티가 들어가있기 때문에 getReference()로 프록시 객체를 반환하는 것이 아닌, 영속성 컨텍스트의 객체를 반환하기 때문이다.

// ——–

Member refMember = em.getReference();

Member findMember = em.find();

System.out.println(refMember == findMember);

위 결과는 무엇이 나와야 하는가? –> True 이다.

근데, findMember는 실제 엔티티를 가져오는데, 어떻게 프록시 객체를 가져오는 타입 비교가 True가 될 수 있을까?

프록시 객체를 한 번 가져오면, 그 다음 find를 해도 프록시 객체를 가져오도록 만들어져 있기 때문이다.

  1. 영속성 컨텍스트의 동작 범위를 벗어난 준영속 상태일 때 프록시 초기화 요청 시 문제가 발생한다.

트랜잭션 주기 == 영속성 컨텍스트의 주기와 거의 비슷하다. 따라서 트랜잭션 문제가 조금 잘 못 된다면

Cloud not Initialize Proxy 라는 에러가 뜰 것인데 이 때 이 문제를 떠올리면 된다.

프록시 확인하기

프록시 인스턴스의 초기화 여부 확인

PersistenceUnitUtil.isLoaded(Object Entity);

프록시 클래스 확인 방법

entity.getClass.getName()

프록시 강제 초기화 (JPA 표준에는 강제 초기화가 명세되어있지 않다. (Hibernate의 추가기능이다.))

Hibernate.initialize(entity);

혹은

member.getName() 등으로 초기화한다.


Member를 조회할 때, Team도 함께 조회해야하는가?

흠… Member의 정보만 필요한데 Team까지 끌고오는건 딱히 좋은건 아닌 것 같다.

해결책 1. 지연로딩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

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

    // 지연로딩 구문
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team teamId;
}

@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;

    @Column
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

위와 같이 지연로딩에 대한 어노테이션 속성을 넣어주면, JPA는 프록시 객체를 가져오게 된다.

즉, Team에 대한 실제 엔티티를 쿼리를 만들어서 조회하는 것이 아니라, 참조값이 비어있는 껍데기 객체인 프록시 객체를 가져오는 것이라는 말이다.


프록시와 아주 밀접한 관계가 있다. 지연 로딩 / 즉시 로딩!

지연 로딩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

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

    // 지연로딩 구문
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team teamId;
}

Member m = em.find(Member.class, member1.getId());
System.out.println(m.getTeam().getClass());

// 지연로딩 시 (프록시 객체를 가져왔을 때) 실질적으로 쿼리가 나가는 구문
m.getTeam().getName();

위와 같이 지연로딩을 설정한 후 Member가 가진 Team의 객체를 조회하면, 프록시 객체가 반환된다.

따라서 위와 같이 getTeam.getName()처럼 Team에 대한 직접적인 행위가 필요할 때 쿼리가 나간다. (프록시 객체가 초기화된다.)

즉시 로딩

비즈니스 로직을 수행할 때, Member.getTeam().getName()와 같이 Team과 Member를 함께 쓰는 구문이 많은 경우에는 지연로딩이 그리 좋진않다.

매번 쿼리를 두번씩 쏴야하기 때문이다.

이 상황에선 쿼리를 덜 쓰고 비즈니스 로직을 수행할 수 있도록 도와주는 옵션이 즉시로딩이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

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

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID")
    private Team teamId;
}

Member m = em.find(Member.class, member1.getId());
System.out.println(m.getTeam().getClass());

즉시 로딩 옵션을 설정하면, em.find() 구문이 시작할 때부터 Member와 Team을 조인하여 가져오기 때문에 프록시 객체가 아닌 실제 객체를 가져온다.

SUWIKI EvaluatePost <-> Lecture <-> ExamPost 관계에서도 이 내용을 적용해볼만 할듯?

즉시 로딩 웬만하면 쓰면 안됨!!

  • 전혀 예상하지 못한 SQL이 나가게됨 (Join 이 섞이고 섞여서 엄청나게 복잡한 쿼리가 생김)
  • 애초에 Join을 해서 가져오는거라 꽤나 많이 느림
  • 즉시 로딩은 N + 1 문제를 발생시킨다.

N + 1 이 무엇이고 어떻게 발생하는가?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

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

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID")
    private Team teamId;
}

List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();

위와 같은 JPQL 쿼리를 쏘는 상황에서 N + 1 문제가 발생하게 된다.

즉시 로딩은 위에서 언급했듯이, 엔티티 객체에 접근 즉시 연관된 테이블까지 조회하여 객체를 완전 초기화 한다고 했다.

그렇기 때문에 멤버가 100명이고 모두 속한 팀이 다르면.. 그 멤버들이 속한 100개의 팀을 모두 조회하기 위해 100방의 쿼리가 나간다.

쿼리가 나가는 횟수를 줄이려고 즉시로딩을 썼는데 더 나가게 된다.

그리고 이 현상을 N + 1 문제라고 한다.

여기서 N 은 연관관계 때문에 추가로 발생한 쿼리, 1 은 내가 실행하고자 하는 쿼리이다.

N+1 문제는 지연 로딩으로 어느정도 해결은 된다! (완전히 해결되진 않음)

  1. 페치 조인
    1
    
    List<Member> members = em.createQuery("select m from Member m join fetch m.team", Member.class).getResultList();
    

    간단하게 페치 조인의 예시를 보면 위와 같다.

페치조인은 SQL에서 지원하는 문법이 아니며 JPQL에서 지원하는 문법임을 알아야한다. 또한 일반 JOIN문이랑 도대체 무슨 차이가 있는가? 함은 다음과 같다.

일반적인 조인

select * from member join member.team 을 했을 때는 member의 관한 테이블만 조회한다. 그렇기 때문에 member가 가진 연관관계에 대한 정보를 알려면 그에 대한 쿼리를 또 날려야한다.

이 추가적인 쿼리를 날리면서 N + 1 문제가 발생하게 되는 것이다.

select * from member join fetch member.team 을 했을 때는 member와 각 member 가 가진 team 에 대한 정보도 한번에 가져온다. 이렇게 되면 각 멤버의 Team을 따로 조회하지 않고 영속화 시킬 수 있기 때문에

추가적인 쿼리가 발생하지 않게 된다.

  1. 엔티티 그래프 (어노테이션)
  2. 배치 사이즈

@ManyToOne, @OneToOne은 기본 값이 즉시 로딩이다.

위 두 개의 어노테이션은 기본 속성이 즉시 로딩이므로 LAZY 옵션으로 변경하는 것을 꼭 잊지말자.

@OneToMany 혹은, @ManyToMany는 기본이 LAZY 로딩이므로 딱히 안해줘도 되지만… 그냥 습관적으로 해두는게 좋을 것 같다.


페치조인 더 자세히 보기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import java.util.List;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;

@SpringBootTest
@Transactional
class MemberRepositoryTest {

    @Autowired
    MemberRepository memberRepository;

    @Autowired
    TeamRepository teamRepository;

    @Autowired
    EntityManager manager;

    @BeforeEach
    void init() {
        Team teamJohn = new Team("John");
        Team teamDiger = new Team("Diger");
        Member Diger = new Member("Diger Kim");
        Member John = new Member("John Jeong");

        teamRepository.save(teamJohn);
        teamRepository.save(teamDiger);

        Diger.setTeam(teamDiger);
        John.setTeam(teamJohn);

        memberRepository.save(Diger);
        memberRepository.save(John);

        // 이 구문이 없어서 엄청 고생했다.
        // save() 메서드 내에 persist 구문이 있는데 이를 수행하게되면 영속성 컨텍스트에 들어가게 되서
        // FetchJoin이든, Join이든 차이가 없게 된다.
        manager.clear();
    }

    @Test
    void readMemberWithFetchJoin() {
        System.out.println("1----------------------------");
        List<Member> members = memberRepository.readMemberWithFetchJoin();
        for (Member member : members) {
            System.out.println("member = " + member.getTeam().getName());
        }
        System.out.println("1----------------------------");
    }

    @Test
    void readMemberWithJoin() {
        System.out.println("2----------------------------");
        List<Member> members = memberRepository.readMemberWithJoin();
        for (Member member : members) {
            System.out.println("member = " + member.getTeam().getName());
        }
        System.out.println("2----------------------------");
    }
}

출력 결과

img.png

img.png


페치 조인과 N + 1

일단 페치 조인은 양방향 연관관계에서만 사용 가능한가에 대한 궁금증이 있었지만, 위의 코드를 양방향으로 매핑하지 않았음에도 페치 조인 쿼리가 나간 것을 보면

그건 아닌 것으로 알 수 있다.

페치조인과 관련된 주제로는 N + 1이라는 문제가 있다.

N + 1이라는 문제는 본질적으로 로딩 방식에 따라 발생하는 문제가 아니다. EAGER 로딩을 사용하여도 N + 1 문제는 발생하기 마련이다.

프록시라는 개념이 본질적인 N + 1의 문제가 된다. A, B, C 라는 객체가 있다고 하자.

지연 로딩은 조회하고자 하는 객체인 A를 제외한, 연관관계로 엮여있는 B, C들은 프록시 객체로 가져온다.

그래서 A라는 엔티티를 조회하고 그 객체에서 B, C에 접근하게 되면 B, C의 실제 객체를 가져와야하기 때문에 쿼리가 2방 더 나가게 된다.

즉시 로딩은 A객체를 조회할 때도 B, C를 프록시가 아닌 진짜 객체를 가져오기 때문에 A 객체에 접근하여 B, C에 접근해도 추가적인 쿼리가 나가지 않는다.

하지만, 즉시 로딩은 A만 사용하고 싶을때에도 B, C를 조인으로 모두 불러온다.

이것이 즉시 로딩의 큰 문제점 중 하나이다.

그렇기 때문에 무조건적으로 모든 객체를 불러오는 것이 아닌 선택적으로 불러올 수 있도록 지연 로딩을 사용하라는 것이다.

지연 로딩은 앞서 언급했듯이 프록시 객체를 불러오는 것에 의하여 추가적인 쿼리(N + 1)가 나가기 때문에 이를 보완할 방법이 필요한데 이 방법 중 하나가 페치 조인인 것이다.

N + 1 문제는 로딩 방식으로 인한 발생한 본질적인 문제가 아니다. 프록시 조회냐 실제 엔티티 조회냐에 따라 영속성 컨텍스트와 연관되어 있는 문제인 것이다.


영속성 전이 (CASCADE)

영속성 전이 (CASCADE) 란?

특정 엔티티를 영속 상태로 만들고 싶다. 근데 이 때 관련된 엔티티도 영속 상태로 만들고 싶다.

그럴 때 영속성 전이를 사용하면 된다.


영속성 전이는 연관관계랑 아무 관계가 없다.

CASCADE의 주요 종류는 다음과 같다.

  • ALL : 영속성 전이 모두 적용
  • PERSIST : 영속성 전이를 엔티티 저장 시에만 적용
  • REMOVE : 영속성 전이를 엔티티 제거 시에만 적용

근데 이걸 언제 쓰지? 매번 써도 되나?

하나의 부모가 하위 클래스를 관리할 때는 써도 된다. (게시판 - 첨부파일)

두개의 부모가 하위 클래스를 관리할 때는 쓰면 안된다. (게시판 - 첨부파일 - 댓글)


고아 객체

부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제해주는 것

orphanRemoval 옵션을 걸고, 자식 엔티티를 지우면, 부모 엔티티에도 그 관리하던 자식 엔티티의 내용을 지운다.


값 타입

엔티티 타입

@Entity 타입으로 정의하는 객체

데이터가 변해도 식별자로 추적이 가능하다.

값 타입

int, Integer 처럼 단순히 값으로 사용하는 객체

식별자가 없어 값이 변경해도 추적이 불가능하다.

값 타입 분류

기본 값 타입 (primitive)

int, double, char etc …

생명 주기를 엔티티에 의존한다. (당연하게도 어떤 클래스에서 필드로 사용하는 것이기 때문이다.)

래퍼 타입 (wrapper)

Integer, Double, String etc …

래퍼 타입은 값이 공유가 된다.

1
2
Integer a = new Integer(10);
Integer b = a;

위와 같은 코드가 있으면 a의 값이 20, 30 등으로 변경된다 하면, b의 값 또한 변경된다. (wrapper타입은 해당 객체에 대한 참조값을 가지기 때문이다.)

따라서 참조값을 공유는 가능하지만, 변경은 되지 않는다.

a.setValue() –> 이런 식으로 변경이 안되니까!

임베디드 타입 (embedded)

내가 커스텀한 클래스를 값으로써 사용하는 것이다.

새로운 값 타입을 직접 정의해서 사용하는 것으로 JPA 이를 Embedded 타입이라고 한다.

회원 엔티티가 있다고 해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long id;

    private String username;

    private LocalDateTime startDate;
    private LocalDateTime endDate;

    private String city;
    private String street;
    private String zipcode;
}

여기서, startDate, endDate를 캡슐화 하고 싶을 때 임베디드 타입에 관한 어노테이션이 사용된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Embedable //임베디드 값 타입 이라는 것을 알리는 어노테이션을 꼭 달아야한다.
public class Period {
    private LocalDateTime startDate;
    private LocalDateTime endDate;

    // 이 때 기본 생성자는 필수적이다.
    public Period() {

    }
}

@Embedable //임베디드 값 타입 이라는 것을 알리는 어노테이션을 꼭 달아야한다.
public class Address {
    private String city;
    private String street;
    private String zipcode;

    // 이 때 기본 생성자는 필수적이다.
    public Address() {

    }
}

@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long id;

    private String username;

    @Embedded
    private Period period;

    @Embedded
    private Address address;
}

임베디드 타입은 잘 느껴지다싶이 재사용이 가능해진다. 또한 잘 설계되었다고 말하는 프로젝트에서는 엔티티보다 임베디드 타입을 위한 클래스가 더 많은게 대부분이다.

만약 하나의 임베디드 타입을 하나의 엔티티에서 여러개를 사용해야할 땐 어떻게 해야할까?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long id;

    private String username;

    @Embedded
    private Period period;

    @Embedded
    private Address homeAddress;

    @Embedded
    @AttributeOverrides(value = AttributeOverride(name = "city", column = @Column(EMP_START)))
    private Address workAddress;
}

컬렉션

List


값 타입 비교

값 타입은 인스턴스가 달라도 그 내부의 값이 같으면 같은 것으로 취급한다.

1
2
3
4
5
6
7
8
9
10
int a = 10;
int b = 10;

a == b --> true


Address a = new Address("서울");
Address b = new Address("서울");

a == b --> false

자바에서는 == 비교가 참조 값을 비교하는 것이다.

따라서 위 예제에서의 a, b 는 Predefined된 10이라는 같은 참조 값을 가리키기 때문에 같다고 나오지만

아래 예제에서의 a, b 는 각 다른 인스턴스 생성하여 그에 대한 참조값을 가리키기 때문에 다르다고 나온다.

동일성(identity 비교) vs 동등성(equivalence 비교)

  • 동일성은 인스턴스의 참조 값을 비교한다. 이때 비교에 필요한 연산자는 == 을 사용한다.
  • 동등성은 인스턴스의 값을 비교한다. equals() 메서드를 사용하며 재정의를 필요로 할 때도 있다.
    • 기본 equals()메서드는 == 비교이다. 따라서 동등성을 비교하고자 하지만 동일성 비교가 되기 때문에 이를 원한다면 오버라이드 해야한다.
This post is licensed under CC BY 4.0 by the author.