> ## Content Index
> Fetch the complete content index at: https://www.popit.kr/llms.txt
> Use this file to discover other available public pages before exploring further.

# JPA 변경 감지와 스프링 데이터
- URL: https://www.popit.kr/jpa-change-detection-spring-data/
- Published: 2018-11-17T02:33:25.000Z
- Updated: 2018-11-17T02:33:25.000Z
- Author: 유 영모
- Tags: #소프트웨어, Spring, Spring Data, JPA, Hibernate, 변경감지, Dirty Checking

[JPAJava Persistence API](https://en.wikipedia.org/wiki/Java%5FPersistence%5FAPI?ref=popit.kr)는 엔티티에 변경이 일어나면 이를 감지하여 자동으로 데이터베이스에 반영한다. 이런 특징에 익숙지 않은 상태에서 [스프링 데이터Spring Data JPA](https://spring.io/projects/spring-data-jpa?ref=popit.kr)를 사용하다 보면 예상치 못한 지점에서 SQL update 구문을 보는 경우가 있다. 이 글은 JPA 변경 감지가 무엇이고 스프링 데이터 JPA와는 어떤 관계가 있는지 설명한다.

# JPA 변경 감지Dirty Checking

JPA는 엔티티 매니저Entity Manager가 엔티티를 조회/저장/삭제/수정한다. [엔티티 매니저의 API](https://docs.oracle.com/javaee/7/api/javax/persistence/EntityManager.html?ref=popit.kr)를 살펴보면 조회(find), 저장(persist), 삭제(remove)는 제공하지만 이상하게도 수정 API는 찾아볼 수 없다. 그 이유는 엔티티 매니저가 엔티티가 변경이 일어나면 이를 자동 감지하여 데이터베이스에 반영하기 때문인데 이것을 변경 감지라고 한다. 간단한 JPA 코드로 확인해 보자.

```java
EntityManager entityManager = entityManagerFactory.createEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
// 트랜잭션 시작
transaction.begin();
 
// 엔티티 조회
DataSource dataSource = entityManager.find(DataSource.class, 1l);
// 엔티티 변경
dataSource.setName("changed name..");
 
// 트랜잭션 커밋
transaction.commit();

```

![image2018-11-15_9-59-7](https://www.popit.kr/content/images/wp/2018/11/image2018-11-15_9-59-7.png)

명시적으로 update 같은 것을 호출해 주지 않아도 수정한 엔티티가 데이터베이스에 반영되었다. 왜냐하면 dataSource.setName()을 통해 엔티티를 변경했고 이를 엔티티 매니저가 감지하여 트랜잭션을 커밋(transaction.commit()) 시점에 데이터베이스에 반영한 것이다. **엔티티 매니저는 트랜잭션을 커밋 하거나 엔티티 매니저의** [**flush 메서드**](https://docs.oracle.com/javaee/7/api/javax/persistence/EntityManager.html?ref=popit.kr#flush--)**를 호출하게 되면 변경 사항을 데이터베이스에 반영한다.**  다만 변경 감지는 영속성 컨텍스트Persistence Context가 관리하는 영속 상태의 엔티티에게만 해당한다. 이 글에서 영속성 컨텍스트나 생명 주기에 대해 다루지 않는다. 이에 대한 설명은 [김영한 님의 발표 자료](https://www.slideshare.net/zipkyh/ksug2015-jpa3-jpa?ref=popit.kr)에 잘 나와 있다.

# 스프링 데이터 JPA

일반적으로 애플리케이션에서 데이터베이스에 접근할 때에는 별도의 계층을 두고 해당 계층에서는 흔히 말하는 [CRUDCreate, Read, Update and Delete(생성, 조회, 수정, 삭제)](https://en.wikipedia.org/wiki/Create,%5Fread,%5Fupdate%5Fand%5Fdelete?ref=popit.kr)를 제공한다. [스프링 프레임워크Spring Framework](https://spring.io/projects/spring-framework?ref=popit.kr)에서는 리파지토리를 JPA로 아래와 같이 구현하여 사용할 수 있다.

```java
@Entity
public class Item {
  @Id
  @GeneratedValue
  private Long id;
  //...
}
 
@Repository
public class ItemRepository {
 
  @PersistenceContext
  private EntityManager entityManager;
 
  @Transactional
  public void save(Item item) { entityManager.persist(item); }
  public void delete(Long id) { ... }
  public Item findOne(Long id) { ... }
  // ...
}
```

문제는 비슷한 코드가 반복된다는 것이다. 예를 들어 Order 엔티티를 추가하는 경우 OrderRepository를 만들고 ItemRepository와 유사한 CRUD 코드를 만들어야 한다. 스프링 데이터 JPA는 반복되는 코드 없이 쉽게 JPA 리파지토리를 만들 수 있다. 리파지토리를 만들 때 인터페이스 구현체 없이 아래처럼 인터페이스만 상속한다.

```java
import org.springframework.data.jpa.repository.JpaRepository;
public interface ItemRepository extends JpaRepository<Item, Long> {
}
 
public interface OrderRepository extends JpaRepository<Order, Long> {
}
```

# JPA 변경 감지와 스프링 데이터

연관 관계가 없는 2개(DataSource, IngestionHistory) JPA 엔티티와 스프링 데이터 JpaRepository를 상속하여 만든 리파지토리가 있다.

```java
import javax.persistence.Entity;
 
@Entity
public class DataSource {
  @GeneratedValue
  private Long id;
  private String ingestionInfo;
  // ...
}
 
@Entity
public class IngestionHistory {
  @GeneratedValue
  private Long id;
  // ...
}
 
import org.springframework.data.jpa.repository.JpaRepository;
public interface IngestionHistoryRepository extends JpaRepository<IngestionHistory, Long> {
}
 
public interface DataSourceRepository extends JpaRepository<DataSource, Long> {
}
```

스프링 컨트롤러(DataSourceController)에서는 DataSourceRepository로 DataSource 엔티티를 조회하고 필드 값을 변경한다. 변경한 엔티티를 스프링 서비스(EngineIngestionService)로 위임하는데 서비스 내부에서는 처리 결과로 IngestionHistory를 반환할 뿐 리파지토리를 사용하지 않는다. 마지막으로 반환받은 IngestionHistory 엔티티를 IngestionHistoryRepository로 저장한다.

```java
@RepositoryRestController
public class DataSourceController {
  @Autowired
  DataSourceRepository dataSourceRepository;
 
  @Autowired
  IngestionHistoryRepository ingestionHistoryRepository;
 
  @Autowired
  EngineIngestionService engineIngestionService;
   
  @RequestMapping(path = "/datasources/{id}/data", method = { RequestMethod.PUT })
  public @ResponseBody ResponseEntity<?> appendDataSource(
      @PathVariable("id") String id,
      @RequestBody String ingestionInfo) {
    // 1. 엔티티 조회
    DataSource dataSource = dataSourceRepository.findOne(id);
    // 2. 엔티티 변경
    dataSource.setIngestionInfo(ingestionInfo);
 
    // 3. 스프링 서비스 호출
    Optional<IngestionHistory> ingestionHistroy = engineIngestionService.doIngestion(dataSource);
 
    // 4. 호출 결과 저장
    ingestionHistoryRepository.save(ingestionHistroy.get());
 
    return ResponseEntity.noContent().build();
  }
}
```

코드를 실행하면 데이터베이스 테이블에는 어떤 변화가 있을까?

![image2018-11-8_7-54-46](https://www.popit.kr/content/images/wp/2018/11/image2018-11-8_7-54-46-1024x656.png)

JPA가 DataSource의 변경을 감지하여 마지막에 update을 실행하여 데이터베이스에 반영하였다.

스프링 프레임워크 같은 컨테이너 환경에서 사용하는 경우 엔티티 매니저가 여러 개 존재할 수 있는데 **변경 감지는 엔티티 매니저별로 수행한다.** **같은 스레드Thread에서 스프링 데이터가 제공하는 리파지토리들은 하나의 엔티티 매니저를 공유한다. 그래서 위의 코드에서 두 개의 리파지토리가 사용하는 엔티티 매니저는 동일하다.** 

![image2018-11-9_8-42-16](https://www.popit.kr/content/images/wp/2018/11/image2018-11-9_8-42-16.png)

위의 코드를 그림으로 나타내면 아래와 같다.

![스크린샷 2018-11-15 오전 11.52.32](https://www.popit.kr/content/images/wp/2018/11/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA-2018-11-15-%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB-11.52.32.png)

# 트랜잭션 커밋은 어디에서?

앞서 리파지토리를 만들 때 스프링 데이터 JPA의 JpaRepository 인터페이스만 상속하였다. 스프링 데이터에서 기본 구현체를 제공해 주기 때문이다.

```java
import org.springframework.data.jpa.repository.JpaRepository;
public interface IngestionHistoryRepository extends JpaRepository<IngestionHistory, Long> {
}
 
public interface DataSourceRepository extends JpaRepository<DataSource, Long> {
}
```

스프링 데이터 JPA에서 제공하는 JpaRepository 인터페이스의 기본 구현체는 SimpleJpaRepository이다. SimpleJpaRepository의 save 메서드에는 스프링 [Transactional 어노테이션](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Transactional.html?ref=popit.kr)이 붙어 있다. 이 의미는 이미 트랜잭션이 중이라면 참여하고 트랜잭션이 없다면 트랜잭션을 새로 시작하라는 의미이다.

![image2018-10-23_8-26-53](https://www.popit.kr/content/images/wp/2018/11/image2018-10-23_8-26-53.png)

결국 DataSourceController는 트랜잭션이 없기 때문에 ingestionHistoryRepository.save를 수행하면 트랜잭션이 시작되고 오류가 없으면 트랜잭션이 자동으로 커밋한 것이다.

# 코드 순서를 변경해 보면...

컨트롤러 코드 순서를 좀 바꿔보자.

```java
@RepositoryRestController
public class DataSourceController {
  // ...
  @RequestMapping(path = "/datasources/{id}/data", method = { RequestMethod.PUT })
  public @ResponseBody ResponseEntity<?> appendDataSource(
      @PathVariable("id") String id,
      @RequestBody String ingestionInfo) {
    DataSource dataSource = dataSourceRepository.findOne(id);
    Optional<IngestionHistory> ingestionHistroy = engineIngestionService.doIngestion(dataSource);
    ingestionHistoryRepository.save(ingestionHistroy.get()); // 트랜잭션 시작 및 종료 커밋됨
    // 코드 위치 변경
    dataSource.setIngestionInfo(ingestionInfo);
    return ResponseEntity.noContent().build();
  }
}
```

코드를 실행해 보면 이전과 달리 **update가 일어나지 않는다.**

![image2018-10-25_21-23-33](https://www.popit.kr/content/images/wp/2018/11/image2018-10-25_21-23-33.png)

그 이유는 앞서 언급했던 것처럼 엔티티 매니저의 데이터 베이스 반영 시점이 트랜잭션을 커밋 하거나 엔티티 매니저의 [flush 메소드](https://docs.oracle.com/javaee/7/api/javax/persistence/EntityManager.html?ref=popit.kr#flush--)를 호출하는 경우이기 때문이다. 위의 코드에서는 dataSource를 변경하기 이전에 이미 커밋(ingestionHistoryRepository.save) 되었다.

# 참고 자료

- [https://projects.spring.io/spring-data-jpa/](https://projects.spring.io/spring-data-jpa/?ref=popit.kr)
- [Ksug2015 - JPA3, JPA 내부구조](https://www.slideshare.net/zipkyh/ksug2015-jpa3-jpa?ref=popit.kr)