1. 엔티티
영구 도메인 객체. 즉, 도메인을 표현할 수 있는 객체라는 것이다.
=> 엔티티 클래스는 최상위 클래스여야하거나, static inner 클래스이어야 한다.(enum, record, interface면 안됌)
=> public, protected 접근제한자를 가진 기본 생성자가 있어야 한다.(런타임에 리플렉션을 사용한 객체)
=> 엔티티의 필드는 final이면 안된다.
=> 엔티티의 필드는 무조건 getter나 setter 등의 다른 메서드를 통해 접근해야한다. entitiy.필드 이런식으로 접근하면 안된다.
1-1. 리플렉션과 엔티티 값 설정
=> 상황: em.find를 해온 상태이다. ResultSet이 있겠다.
Constructor<Member> constructor = Member.class.getDeclaredConstructor();
constructor.setAccessible(true);
Member member = constructor.newInstance();
=> 리플렉션과 기본 생성자를 통한 객체 생성
Field field = Member.class.getDeclaredField("name");
field.setAccessible(true);
field.set(member, "홍길동");
=> 리플렉션을 이용한 필드 값 주입
이러한 방식으로 getter와 setter없이도 값을 주입해준다.
2. 엔티티 영속 관리
2-1. 엔티티 객체 생명 주기
2-1-1.엔티티 객체 생성
=> 새로운 엔티티가 생성됐을 시, 영속성 컨텍스트에 포함되지 아무런 식별자도 갖고 있지 않는다.
=> 후에 EntitiyManager API(JPA 인터페이스, Hibernate 구현)을 통해 영속 상태가 될 것이다.(EntityManager.persist())
2-1-2. 영속화 된 엔티티 객체(EntityManager.persist())
=> 영속되는 엔티티 객체는 @Id가 붙은 PK를 식별자로, 영속성 컨텍스트에 포함된다.(식별자 O, 영속성 컨택스트 O)
=> 엔티티가 persist()되면, 영속화된다. 이미 영속화된 객체는 persist해도 무시한다.
=> cascade=PERSIST 또는 cascade=ALL 어노테이션이 붙어있다면, 연관된 모든 객체가 영속화된다.
=> 준영속 상태인 객체에 persist()가 되면, EntityExistsException가 발생한다.
2-1-3. 삭제(EntityManager.remove()) - 영속성 컨텍스트에서 삭제
=> 제거된 엔티티는 일단 식별자가 있고, 커밋 시 데이터 베이스에서 삭제될 것이다.
=> cascade=REMOVE 나 cascade=ALL 일 경우 연관된 모든 객체가 삭제된다.
=> 만약, 준영속 상태의 객체에 remove()를 할 경우 IllegalArgumentException이 발생한다.
=> 영속 상태에서 삭제된 엔티티는 후에 flush나 commit 시 DB에서도 삭제된다.
**준영속 엔티티 객체는 PK를 식별자로 두며 영속성 컨텍스트에 포함되지 않는다.(식별자 O, 영속성 컨텍스트 X)**
2-2. 영속성 컨텍스트와 DB 동기화
JPA를 쓰는 순간 DB가 주 저장 장치가 아닌, JPA의 영속성 컨텍스트가 주 저장 장치가 되고, DB는 보조 저장 장치가 된다.(어떻게 보면 당연한 말이다.)
영속성 컨텍스트와 DB는 트랜잭션 Commit 시점에 동기화된다.
동기화는 flush()를 통해 할 수 있다.
그럼 flush()가 언제 실행되는지 살펴보자.
2-2-1. 양방향 연관관계 주의점
class Team {
@OneToMany(mappedBy = "team") // 비주인
List<Member> members;
}
class Member {
@ManyToOne // 연관관계의 주인
Team team;
}
위의 코드가 있다.
mappedBy는 FK가 없는 쪽에 써준다.
즉, 연관관계의 주인은 Member가 되고, 공식 문서에서는
"연관관계의 주인이 갖고 있는 참조에 따라 DB에 반영된다."고 하고 있다.
즉, Team의 members에 new Member()의 객체를 넣어준다고 해도, DB에는 반영이 안되는 것이다.
때문에, DB에 반영하고 싶은 INSERT, UPDATE가 있다면, 연관관계 주인(FK가 있는 쪽)의 필드값을 변경해주어야 한다.
이러한 구조 때문에 자주 보는 편의 메서드라는 패턴이 나온 것일 것이다.
@Entity
public class Team {
@Id
private Long id;
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL)
private List<Member> members = new ArrayList<>();
//양방향 편의 메서드 => 주인이 아닌 쪽에서 주인을 가져와 자신을 주인에게 주입
public void addMember(Member member) {
members.add(member);
member.setTeam(this); // 주인 쪽도 설정해줌!
}
}
위의 코드를 보면 Team에서 member를 가져와 this(team)을 넣어주는 것을 볼 수 있다.
=> 왜 이렇게 될까?
=> 연관관계의 주인은 FK를 갖고 있다.
=> 연관관계의 하인?은 FK를 갖고 있지 않다.
=> 연관관계의 하인이 List<Member>에 주인을 넣어줘도, 실제 DB에서 하인 쪽은 연관관계에 대한 어떠한 것도 할 수 없다.
=> 반대로, 주인 쪽은 FK를 추가해줌으로 연관관계를 만들어 줄 수 있다.
=> 단방향 연관관계는 애초에 연관관계 주인이 없기 때문에, 상관없다.
그렇다면, 결론은 "양쪽 엔티티가 양쪽에서 변경이 필요하다면 양방향 매핑이 필요하다."로 귀결될 것 같다.
2-2-2. 플러시 모드
연관관계의 주인에 의해 바뀐 엔티티 집합은 flush()를 통해 DB에 동기화된다.
이 플러시는 2가지의 모드가 있다.
2-2-2-1. EntityManager.setFlushMode(FlushModeType.AUTO)
=> flush()가 트랜잭션 커밋 전,후에 자유롭게 실행될 수 있다.
=> 즉, 영속성 컨텍스트 안에 스냅샷과 다른 엔티티(수정되었다는 것)가 있는 경우 flush()를 자유롭게 사용한다.
2-2-2-2. EntityManager.setFlushMode(FlushModeType.COMMIT)
=> flush()가 트랜잭션 커밋 바로 전에만 1번 수행한다.
2-2-2-3. EnitityManager.setFlushMode(FlushModeType.MANUAL)
=> @Transactional(ReadOnly = true)일 때, 하이버네이트는 이 모드로 설정한다.
=> flush()를 하지 않는다.
=> flush()가 되지 않으니, 스냅샷도 저장하지 않는다.(이 때문에, 메모리 상 이점이 있다.)
=> INSERT/UPDATE/REMOVE 등 쿼리를 막는다.
2-2-3. 트랜잭션
2-2-3-1. 어차피 flush를 안할꺼면 @Transactional(readOnly = true)을 안쓰는 것이, 더 낫지 않냐?
=> 영속성 컨텍스트의 생명주기는 트랜잭션이다.
=> 때문에, @Transactional의 주기 동안만 영속성 컨텍스트를 사용할 수 있다.
=> 영속성 컨텍스트를 사용하지 못하면, LAZY 로딩에 필요한 프록시도 당연히 사용하지 못한다.(LAZY 로딩 시도 시 LazyInitializationException 던짐)
=> 만일, DTO 매핑과 같이 LAZY 로딩도 필요 없는 서비스 로직이 있다면, 굳이 안써주는것이 더 효율적일 것이다.
=> 하지만, 유지보수성을 위해 그냥 붙여주는것이 좋겠다.
2-2-3-2. @Transactional 없이 저장할 수 있나?
=> JpaRepository의 save()메서드는 그저 persist()를 호출해준다.
=> 사실상, @Transactional을 쓰지 않아도 영속성 컨텍스트가 생성되지만, 아무것도 할 수 없는 유령 상태이다.
=> 때문에, 메모리만 잡아먹고 아무것도 해줄 수 없다.
=> Commit, flush() 둘 다 안되기 때문에, 이런 방식은 옳지 못한 방식이다.
'jpa 개념' 카테고리의 다른 글
List를 saveAll하기 (0) | 2024.12.28 |
---|---|
영속성 (0) | 2024.01.21 |
JPA 기본 (0) | 2024.01.12 |