테이블의 연관 관계와 객체의 연관 관계의 차이가 있기 때문에 직접 매핑하기에는 다소의 어려움과 귀찮음이 있다. JPA를 사용하면 이러한 테이블과 객체 사이의 매핑을 알아서 해주니 정말 편하게 사용할 수 있다. 이번에는 해당 내용을 직접 실습을 통해 살펴보도록 하겠다!
@Entity
@Getter
@Setter
@Builder
@Table(name = "users")
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(referencedColumnName = "id")
private Role role;
@Temporal(value = TemporalType.TIMESTAMP)
private Timestamp createdAt;
}
User 클래스
User와 Role은 다대일의 관계를 갖는다.
@Entity
@Getter
@Setter
@Builder
@Table(name = "roles")
@NoArgsConstructor
@AllArgsConstructor
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
Role 클래스
생성
생성한 객체간의 연관 관계를 설정하고 저장하면 테이블에도 외래키 값이 반영되어 저장된다.
@Test
//@Transactional
void create() {
// given
Role role = Role.builder().name("학생").build();
User user = User.builder().name("민수").role(role).createdAt(new Timestamp(System.currentTimeMillis())).build();
// when
log.info("----- 트랜잭션 시작 -----");
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
entityManager.persist(role);
entityManager.persist(user);
transactionManager.commit(status);
log.info("----- 트랜잭션 종료 -----");
// then
Assertions.assertThat("학생".equals(entityManager.find(User.class, user.getId()).getRole().getName())).isTrue();
}


데이터를 직접 확인해 보기 위해서 바깥쪽의 @Transactional 애너테이션을 잠시 비활성화 시켰다. 역할을 만들고 유저에 해당 객체를 포함시켰더니 알아서 roles 학생의 13이라는 값이 민수의 role_id에도 저장된 것을 볼 수 있다.
만약 민수의 role 값을 지정하지 않고 진행했다면 다음과 같이 role_id가 지정되지 않을 것이다.

주의해야할 점!!
트랜잭션 안에서 트랜잭션을 실행할 경우 처음 시작된 트랜잭션에 참여하게 되어 실제로는 해당 트랜잭션이 커밋될 때 내부 트랜잭션도 같이 커밋된다. 그래서 내부 트랜잭션을 롤백하는 상황을 구현할 때 transactionManager.rollback()을 해도 제대로 검증이 되지 않을 수 있다. 밑의 스크린샷을 보면 rollback() 호출 시에 rollback-only로 마킹만 하고 나중에 가서야 롤백을 하는데 이때는 아직 영속성 컨텍스트에 값이 남아 있기 때문에 select 쿼리를 날리지 않고 User를 조회한다. 그리고 최후에 가서야 롤백을 하기 때문에 DB에 저장이 되지 않는다.

조회
다른 테이블과 연결되어 있는 객체를 조회하면 해당 객체에 연결된 다른 객체를 참조하는 방식으로 접근할 수 있다.
@Test
void get() {
// given
Role role = entityManager.find(Role.class, 16);
User user = entityManager.find(User.class, 16);
// when
Role userRole = user.getRole();
// then
Assertions.assertThat(userRole.getId()).isEqualTo(role.getId());
}

테이블 간의 연결과 객체 간의 연결의 차이점에 대해서 배웠다. join으로 연결하는 방식은 이미 SQL 내에 접근 범위가 지정되어 있지만 객체 간의 참조는 연결하는 필드가 있는 이상 계속해서 참조가 가능하다. 그렇기 때문에 SQL을 사용하여 가져온 데이터를 객체에 매핑한다면 SQL로 가져온 테이블 이외의 것들은 객체간의 참조를 하지 못하고 null일 것이다. JPA는 개발자가 객체를 직접 테이블과 매핑하지 않아도 사용하는 시점에 자동으로 조인 쿼리를 만들어서 제공해준다. 테스트도 잘 성공한다. 다만 조회는 트랜잭션 안에서 하지 않았는데 작동하는 것을 알 수 있다. 이 부분은 검색을 해보니 맞긴 한데 자세한 내용은 나중에 더 공부하고 다시 봐야겠다. 밑의 영한님 답변 참고...
@Transactional 어노테이션 질문드립니다 - 인프런
안녕하세요 영한님Jpa 로 단순 조회기능을 이용하게될때Service Layer 에서 @Transactional(readOnly=true) 를 메소드에 선언해서 사용했었는데요테스트하다보니 @Transactional 어노테이션 없이 사용해도 조회
www.inflearn.com
수정
단순히 객체를 수정하기만 하면 커밋 시에 해당 내용이 자동으로 반영된다.
@Transactional
@Commit
@Test
void update() {
// given
String newName = "지토";
User user = entityManager.find(User.class, 16);
// when
user.setName(newName);
// flush() 호출해도 영속성 컨텍스트가 비워지는건 아님!
entityManager.flush();
//entityManager.clear();
// then
Assertions.assertThat(entityManager.find(User.class, 16).getName()).isEqualTo(newName);
}


이번에는 @Transactional 애너테이션을 사용해보자. 테스트에서는 해당 애너테이션을 사용하면 자동으로 rollback이 된다. 그러므로 커밋되게 하려면 @Commit을 붙여주면 된다. DB에서 직접 보고 싶으니 커밋을 해야 수정 결과가 반영된다.
수정은 단순히 객체를 조회하고 변경하면 된다. 단, 아무 객체나 되는 것이 아니고 영속성 컨텍스트에서 관리하고 있는 객체만 해당된다. 이를 영속이라고 한다. 그리고 또한 영속 상태에 있는 객체는 주로 id라고 사용하는 식별자가 무조건 있어야 하며, 중간에 이를 변경해서는 안 된다.
실행 결과를 보면 영속 상태인 객체를 변경했더니 update 쿼리가 실행되었다. 여기서 알 수 있는 점은 flush()를 실행한다고해도 결과가 바로 DB에 반영되지 않는다는 점이다. 이 부분에 관해서는 자세한 내용을 파악하지 못했다... 이유가 뭘까?
테스트 결과가 모두 실행되고 나서야 커밋이 되기 때문에 검증을 하는 마지막 라인에서 조회를 하지만 이미 영속성 컨텍스트에 있는 객체이기 때문에 해당 캐시에서 데이터를 가져온다. 그렇기 때문에 다시 select 쿼리를 날리진 않는다. 만약 clear() 메서드를 실행한다면 캐시가 비었기 때문에 select 쿼리로 레코드를 다시 가져올 것이다.

clear()를 실행했더니 update 쿼리 이후에 select 쿼리를 실행하는 것을 알 수 있다. 그런데 또 여기서는 clear() 이전에 flush()를 해야 원래 생각하던 대로 정상 동작한다... 아무래도 이후의 작업 종류에 따라 최적화를 해주는 것 같다.
삭제
객체를 삭제하면 DB에서도 삭제된다.
@Transactional
@Commit
@Test
void remove() {
// given
User user = entityManager.find(User.class, 16);
// when
entityManager.remove(user);
// then
Assertions.assertThat(entityManager.find(User.class, user.getId())).isNull();
}

remove()를 호출하면 해당 객체를 영속성 컨텍스트와 DB에서 삭제한다. 영속 상태에서 영속 상태가 아니게 되는 것을 준영속이라고 한다.
의문인 점은 삭제한 후에 다시 찾을 때 영속성 컨텍스트에 없는 것은 알고 있는데 왜 다시 DB에 있는지 select 쿼리를 날리지 않는 것일까? 내부적으로 무언가 삭제한 객체를 기억하는 장치가 있는 것 같다. -> 찾아보니 삭제 목록이라는 것이 있다고 한다.
그럼 정말 remove()를 호출한 이후에 영속성 컨텍스트에서 제거될까?
@Transactional
@Commit
@Test
void remove() {
// given
User user = entityManager.find(User.class, 18);
// when
entityManager.remove(user);
log.info("영속성 컨텍스트에 있는가? = {}", entityManager.contains(user));
// then
Assertions.assertThat(entityManager.find(User.class, user.getId())).isNull();
log.info("삭제함");
}

contains() 메서드를 이용해서 영속성 컨텍스트에 있는지 확인을 해봤다. 결과를 보니 false가 나왔다. 바로 삭제된 것을 알 수 있다. 이후에는 해당 객체를 이용하지 않는 것이 좋다.
'공부 > JPA' 카테고리의 다른 글
| [JPA] OneToOne에서의 N + 1 문제 (0) | 2024.02.27 |
|---|---|
| [JPA] 프록시 (Proxy) (0) | 2024.02.24 |
| [JPA] 다대다 연관 관계에서 복합키 사용하기 (0) | 2024.02.21 |
| [JPA] 연관 관계 중 인조키 vs 복합키 에 대한 고민 (0) | 2024.02.20 |
| [JPA] 영속성 컨텍스트 (Persistence Context) 테스트하기 (1) | 2024.02.14 |