
<!-- SQL을 로그에 보여준다. -->
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>
<!-- DDL auto 설정 -->
<property name="hibernate.hbm2ddl.auto" value="create"/>
public static void executeCommit(EntityManager entityManager, Runnable action) {
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
try {
action.run();
} catch ( Exception e ) {
transaction.rollback();
log.error(e.getMessage(), e);
e.printStackTrace();
} finally {
transaction.commit();
}
}
반복되는 transaction 생성, 시작, 커밋 혹은 롤백 상황을 Util 함수로 만들어두었다.
Runnable은 run() 메서드 하나만 가진 함수형 인터페이스이다. 반환값과 파라미터가 없으며, 특정 함수를 실행하고 싶을 때 람다 형식으로 사용할 수 있게 된다.
@Test
@DisplayName("select test")
void select_test() throws Exception {
Member member = genMember(genMemberName());
executeCommit(entityManager, () -> {
entityManager.persist(member);
Member findMember = entityManager.find(Member.class, member.getId());
// 1차 캐시에서 가져오게 되는 것이다. (SELECT 문 X)
assertThat(findMember).isEqualTo(member);
log.info("member = {}", member);
log.info("findMember = {}", findMember);
});
executeCommit(entityManager, () -> {
Member findMember = entityManager.find(Member.class, member.getId());
assertThat(findMember.getId()).isEqualTo(member.getId());
findMember.setName("ADMIN"); // UPDATE 문 실행 O
});
executeCommit(entityManager, () -> {
Member findMember = entityManager.find(Member.class, member.getId());
assertThat(findMember.getName()).isEqualTo("ADMIN");
entityManager.detach(findMember);
findMember.setName("MEMBER"); // UPDATE 문 X
});
executeCommit(entityManager, () -> {
// member가 영속성 컨텍스트에서 준영속상태가 되었기 때문에 영속성 컨텍스트 안에 없다.
// 즉, DB에서 가져와야 한다. => SELECT문 실행
Member findMember = entityManager.find(Member.class, member.getId());
// DETACHED -> MANAGED
Member merge = entityManager.merge(member);
assertThat(merge).isEqualTo(findMember);
log.info("merge.getName() = {}", merge.getName()); // MEMBER
log.info("findMember.getName() = {}", findMember.getName()); // MEMBER
assertThat(findMember.getName()).isNotEqualTo("ADMIN");
assertThat(findMember.getName()).isEqualTo("MEMBER");
});
}
Q. 마지막 executeCommit() 에서 findMember 는 .find() 로 DB에서 가져온 객체라 이름이 ADMIN 이고, merge 를 한 merge 객체는 detached → managed로 된 애니까 이름이 MEMBER 이다.
근데 왜 assertThat(findMember.getName()).isNotEqualTo("ADMIN"); 이 테스트가 통과할까?
A. merge()는 member 객체의 값을 기반으로 새로운 영속 객체를 만들어서 반환한다.
✅< merge() 과정>
member.getId()로 DB나 1차 캐시에서 해당 ID를 가진 영속 객체를 찾는다. → 이름 “ADMIN”@Column(length = 100, nullable = false)
private String title;
@Column(columnDefinition = "TEXT")
private String contents;
@Enumerated(EnumType.STRING)
private Level membershipLevel;
@Column의 속성을 설정하여 구체적으로 DB 테이블의 컬럼을 설정해줄 수 있다.
length : VARCHAR 의 크기를 정한다.nullable = false : NOT NULL 로 설정한다.columnDefinition : 필드에서 타입이 String 이면 DDL-auto는 VARCHAR(255)로 선언해준다. 특별히 다른 문자열 타입으로 두고 싶다면 TEXT, LONGTEXT 등 지정해줄 수 있다.@Enumerated 의 속성을 통해 Enum 타입을 알려줄 수 있다.
락커와 락커의 주인은 1:1 관계이다. 그럴 때는 @OneToOne 관계 설정을 해주면 된다.
@Entity
public class Locker {
@Id
@Column(name="locker_id")
@GeneratedValue(strategy= GenerationType.IDENTITY)
private Long id;
private Integer number;
@OneToOne(mappedBy = "locker")
private LockerOwner owner;
}
@Getter
@Entity
public class LockerOwner {
@Id
@Column(name="locker_owner_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToOne
@JoinColumn(name="locker_id")
private Locker locker;
}
Player와 Team은 일대다 관계를 가지고 있다.
Player 입장에서는 N:1, Team 입장에서는 1:N의 관계를 가지고 있다.
이럴 때는 @OneToMany, @ManyToOne 연관관계를 주입해줄 수 있다.
@Getter
@Entity
@Table(name="players")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Player {
@Id
@Column(name="player_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(length=50)
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="team_id", nullable = false)
private Team team;
}
@ManyToOne 를 붙여준다.@JoinColumn을 붙이면 Player 테이블에 team_id 라는 외래키 컬럼을 만들게 된다. 그 team_id는 Team 테이블의 id 컬럼을 참조한다.nullable = false로 NOT NULL 설정을 해준다.@Entity
@Getter
@Table(name="teams")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {
@Id
@Column(name="team_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(length =50)
private String name;
// @OneToMany(mappedBy = "team", fetch = FetchType.LAZY)
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Player> players = new ArrayList<>();
}
mappedBy = "{필드명}" 을 명시하여, 외래키가 두 개 생기지 않도록 해준다. mappedBy는 연관관계의 주인을 나타내준다.fetch = FetchType.LAZY 로 지연 로딩을 하게끔 설정해준다. cascade = CascadeType.ALL : 연관된 엔티티도 함께 영속성 전이(persist/merge/remove 등) 시키고 싶을 때 사용하는 옵션이다. 부모 엔티티의 생명주기를 자식 엔티티에게도 같이 전이시키는 설정이다.orphanRemoval = true : 연관관계에서 자식 엔티티가 부모와의 관계가 끊기면,@OneToOne 이나 @OneToMany에만 적용될 수 있다. Q. CascadeType.ALL 을 하면 REMOVE도 전이가 되는데, orphanRemoval을 따로 설정해주는 이유가 뭘까?
A. 예를 들어 Cascade 설정으로는 Team 객체를 삭제했을 때, 연관관계에 있는 Player 객체도 삭제되는 것이다.
orphanRemoval 설정으로는 Team 객체의 players에서 선수를 한 명 제거하면, 그 선수는 Team과의 관계가 끊기게 된다. 그럴 경우, 그 선수 자체도 삭제되는 것이다.
✅ 정리
| 기능 | cascade = REMOVE | orphanRemoval = true |
|---|---|---|
| 부모 삭제 시 자식도 삭제 | ✅ | ✅ |
| 부모에서 자식 제거 시 삭제 | ❌ | ✅ |
지연 로딩은 데이터베이스에서 필요한 데이터만을 필요 시점에 로딩함으로써 애플리케이션의 성능을 최적화한다. 지연 로딩의 핵심 개념은 객체가 참조하고 있는 다른 객체나 컬렉션의 데이터를 처음부터 메모리에 로딩하지 않고, 해당 데이터에 실제로 접근하려는 시점에 로딩을 수행하는 것이다.
지연 로딩의 동작 방식은 프록시 패턴(Proxy Pattern)을 기반으로 한다. Hibernate에서는 실제 엔티티 객체 대신 프록시 객체를 반환하여, 이 프록시 객체가 해당 엔티티의 지연 로딩을 담당하게 한다. 프록시 객체는 엔티티에 접근하려는 시도가 있을 때 실제 데이터베이스 호출을 통해 데이터를 로드한다.
@OneToMany, @ManyToMany의 fetch 타입의 기본 값은 FetchType.LAZY이다.@ManyToOne, @OneToOne의 fetch 타입의 기본 값은 FetchType.EAGER이다.LazyInitializationException이 터진다.프록시 객체가 실제 데이터를 로딩(초기화)하려면 Hibernate 내부적으로 EntityManager과 연결되어 있어야 한다.즉, 영속성 컨텍스트가 있어야 하고 DB 커넥션도 열려 있어야 한다.
LazyInitializationException 과 같은 예외가 발생할 가능성이 있다. Hibernate는 데이터베이스 세션이 닫힌 후에는 지연 로딩을 수행할 수 없기 때문에, 세션이 닫힌 이후에 지연 로딩이 발생하는 상황에서는 예외가 발생하게 된다.
즉시 로딩은 특정 객체와 그 객체가 연관된 다른 객체들을 한꺼번에 로드하여 애플리케이션의 성능과 사용성을 향상시키는 중요한 기법이다. 즉시 로딩은 지연 로딩(Lazy Loading)과 대조적인 개념으로, 객체를 로드할 때 해당 객체와 연관된 모든 데이터를 함께 로드하는 방식이다.
try {
player = entityManager.find(Player.class, 1);
log.info("player.getName() = {}", player.getName());
entityManager.detach(player);
transaction.commit();
} catch ( Exception e ) {
transaction.rollback();
}
entityManager.close();
Team team = player.getTeam(); // 프록시 객체임.
log.info("team = {}", team);
assertThatThrownBy(
() -> {
team.getName();
}
).isInstanceOf(LazyInitializationException.class);
.find()로 player 객체를 가져왔을 때 이 Player 객체의 필드인 Team은 left join으로 가져온 실제 객체가 아니라 프록시 객체이다.
player 객체를 detached 해서 player는 영속성 컨텍스트에서 분리했다.
영속성 컨텍스트가 살아있어야 프록시 객체가 내부적으로 EntityManager를 통해 DB에 쿼리를 날려서 실제 데이터를 로딩(초기화)할 수 있다.
transaction.commit() 으로 트랜잭션을 끝내고, entityManager.close()으로 엔티티 매니저를 닫으면 프록시가 DB에 접근할 수 없게 된다.
player.getTeam()은 단순히 player 객체의 프록시 객체를 반환하기 때문에 문제가 없다.
하지만, team.getName()으로 실제 Team 객체에 접근하려고 하면 엔티티 매니저가 닫혔기 때문에 DB에 접근하여 실제 객체를 가져올 수 없게 된다.
-> LazyInitializatioinException 발생!
try {
player = entityManager.getReference(Player.class, 1);
// getReference 를 하면 entity 타입의 프록시 객체를 반환해준다.
} catch (Exception e){
transaction.rollback();
}
transaction.commit();
// entityManager.close();
log.info("player.getName() = {}", player.getName()); // 이 때 쿼리를 날려서 실제로 가져오는 것이다.
}
getReference 는 fetch 설정과 무관하게 지연 로딩을 강제하고 싶을 때 사용한다. entityManager.close() 를 주석처리 해뒀기 때문에 player.getName() 이 가능하다. 만약, 엔티티 매니저도 닫았다면 실제 객체에 접근할 수 없어 예외가 발생한다.try {
player = entityManager.getReference(Player.class, 1);
} catch (Exception e){
transaction.rollback();
}
transaction.commit();
entityManager.close();
boolean result =
entityManagerFactory.getPersistenceUnitUtil().isLoaded(player); // False
// 초기화가 됐는지 안됐는지 확인하는 것 (초기화가 됐느냐 -> 프록시냐 아니냐) entityManager.close()와는 관련 없다.
log.info("result = {}", result);
// player.getName(); 에러 터진다.
entityMangerFactory.getPersistenceUnitUtil().isLoaded() 메서드로 player 객체가 초기화(로딩) 됐는지 안됐는지 확인할 수 있다.Q. 그렇다면 프록시 객체의 필드에 접근하는, 쿼리를 던지는 메서드를 실행했을 경우, 그 이후로는 실제 객체가 되는 것인가? 그 때부터는 트랜잭션의 커밋, 엔티티매니저의 닫힘에 영향을 받지 않는가?
A. 맞다 ! 프록시 객체에서 .get~()의 메서드를 이용해 DB에 쿼리를 던지고 실제 객체를 얻어온 이후로는 실제 객체이다. 그 시점부터는 영향을 받지 않는다.
강의와 학생과 같은 관계는 다대다관계이다. 그럴 때는 @ManyToMany이다. 이럴 때는 누가 연관관계의 주인일까?
=> 이럴 때는 중간 테이블을 만들어서 연관관계를 일대다로 바꿔줘야 한다.
// Lecture.java
@ManyToMany(mappedBy = "lectureList")
private List<Student> studentList;
// Student.java
@ManyToMany
@JoinTable(
name="STUDENT_LECTURE_RELATION",
joinColumns = @JoinColumn(name="STUDENT_ID"),
inverseJoinColumns = @JoinColumn(name="LECTURE_ID")
)
private List<Lecture> lectureList;
@JoinTable을 두지 않으면 DDL-auto가 알아서 중간 테이블을 만들어준다. 하지만, 이 방법은 권장하지 않는다. 내가 그 중간 테이블 이름도 알지 못하고 그 테이블에 추가 컬럼도 넣어주지 못한다.@JoinTable을 두고 테이블을 만들어줄 수 있긴하다. 하지만, 다대다 관계일 때는 정규화를 다시 하든지, 중간 테이블을 두어 일대다, 다대일 관계로 다시 풀어주는 방법을 선택해야 한다.
어렵다 어렵다 어렵다 어렵다
아... 알면 알수록 되게 되게 재밌는데.. 되게 어렵고 되게 뭐가 많네.. 오늘 RBF하면서 머리 터지는 줄 알았다. 그치만 덕분에 merge 안 까먹을 듯 ㅎ.. ㅋ
앞으로의 커리큘럼을 알려주셨는데, 이거 다하고 jpql, QueryDSL, Spring Data JPA를 하고, Spring Security를 나간다고 하셨다.
그리고 사실 오늘 안 사실인데 JPA랑 Spring Data JPA랑 다른 거라고.. 크흠.. 난 똑같은 건 줄 알았다 ㅠ JPA 다 했는데 "Spring Data" JPA 나간다고 하셔서 찾아봤더니 다른 거더라고요.. 하핫 (머쓱)
되게 재밌는데 되게 어려운 거 아직 많이 남았다니까 걱정 많이 설렘 많이 ~ 하하 곧 1차 프로젝트 시작할 거라고 생각하니까 와.. 벌써 시간이?! 이런 생각도 많이 든다. 짧은 시간에 많은 걸 배웠구나 ! 1차 프로젝트에서 잘 써먹고 익숙해질 수 있었으면 좋겠다 !
아무튼 오늘은 금요일이다 ! FRIDAY 해피데이 :) 오늘은 밖에 좀 나가보고 싶어서.. 수업 끝나자마자 해지기 전에 호딱 광합성하려고 가볍게 공원에 산책하러 갔다 ㅎ 와.. 너무 행복하더라 정말 소소한 행복이란 이런 거다.. 이러면서 막 새삼스레 사진 찍으면서 산책했다. 그치만 다시 집에 돌아오니 할 거 쌓여있네 ㅎㅎㅎㅎ 오늘 TIL 하나 끝냈다 ㅎ.. 아자 ! :,)