JPA - JPA 엔티티의 Detached 상태가 되는 경우

📌 1. 트랜잭션이 종료되면서 Persistence Context가 닫힌 경우

✔️ findByUserId()로 조회한 ictInfo는 처음에는 Managed 상태

✔️ 하지만, 메서드 실행이 끝나면서 영속성 컨텍스트가 닫힘 → Detached 상태가 됨

✔️ 이후 변경을 해도 DB에 반영되지 않음!

@Service
public class ICTInfoService {

    private final ICTInfoRepository ictInfoRepository;

    public void process(String userId, String newLocalIp) {
        // 🔹 1. 데이터베이스에서 조회
        ICTInfo ictInfo = ictInfoRepository.findByUserId(userId)
                .orElseThrow(() -> new IllegalArgumentException("ICTInfo not found: " + userId));

        // 🔹 2. 트랜잭션이 끝나면서 ictInfo는 Detached 상태가 됨
        // 이후 수정해도 DB에 반영되지 않음
        ictInfo.setLocalIp(newLocalIp);

        // ❌ save()를 하지 않으면 DB에 반영되지 않음
    }
}

📌 2. detach() 또는 clear()를 사용하여 강제로 Detached 상태로 변경

  • ✔️ entityManager.detach(entity)를 호출하면 해당 엔티티가 Persistence Context에서 분리됨(Detached)

  • ✔️ 이후 setLocalIp(newLocalIp)을 호출해도 DB에 반영되지 않음

@Service
@RequiredArgsConstructor
public class ICTInfoService {

    private final ICTInfoRepository ictInfoRepository;
    private final EntityManager entityManager; // ✅ EntityManager 주입

    public void process(String userId, String newLocalIp) {
        // 🔹 1. 데이터 조회 (영속 상태)
        ICTInfo ictInfo = ictInfoRepository.findByUserId(userId)
                .orElseThrow(() -> new IllegalArgumentException("ICTInfo not found: " + userId));

        // 🔹 2. 강제로 Detached 상태로 변경
        entityManager.detach(ictInfo);

        // ❌ Detached 상태에서는 set()만으로 업데이트되지 않음!
        ictInfo.setLocalIp(newLocalIp);

        // save() 또는 merge() 필요
    }
}

📌 3. findById()로 가져온 후 다른 트랜잭션에서 수정하려는 경우

✔️ 첫 번째 findByUserId()로 조회한 ictInfo는 Detached 상태

✔️ updateIpInNewTransaction()에서 새로운 트랜잭션을 열어 같은 데이터를 수정

✔️ 그런데, 이전에 가져온 Detached 상태의 엔티티를 다시 save()하면 StaleObjectStateException 발생

@Service
@RequiredArgsConstructor
public class ICTInfoService {

    private final ICTInfoRepository ictInfoRepository;

    public void process(String userId, String newLocalIp) {
        // 🔹 1. findById()를 호출한 후 메서드가 종료되면 트랜잭션이 종료됨 (Detached)
        ICTInfo ictInfo = ictInfoRepository.findByUserId(userId)
                .orElseThrow(() -> new IllegalArgumentException("ICTInfo not found: " + userId));

        // 다른 트랜잭션에서 변경 시도
        updateIpInNewTransaction(userId, newLocalIp);

        // ❌ 이 상태에서 다시 save() 하면 StaleObjectStateException 발생 가능
        ictInfoRepository.save(ictInfo);
    }

    @Transactional
    public void updateIpInNewTransaction(String userId, String newLocalIp) {
        ICTInfo ictInfo = ictInfoRepository.findByUserId(userId)
                .orElseThrow(() -> new IllegalArgumentException("ICTInfo not found: " + userId));

        ictInfo.setLocalIp(newLocalIp);
    }
}

✅ Detached 상태 해결 방법

  • ✔️ 해결 방법 1: @Transactional 사용 (가장 추천!)

@Transactional
public void process(String userId, String newLocalIp) {
    ICTInfo ictInfo = ictInfoRepository.findByUserId(userId)
            .orElseThrow(() -> new IllegalArgumentException("ICTInfo not found: " + userId));

    ictInfo.setLocalIp(newLocalIp); // ✅ Dirty Checking으로 자동 업데이트됨
}
  • ✔️ 해결 방법 2: merge()를 사용하여 영속 상태로 변경

public void process(String userId, String newLocalIp) {
    ICTInfo ictInfo = ictInfoRepository.findByUserId(userId)
            .orElseThrow(() -> new IllegalArgumentException("ICTInfo not found: " + userId));

    ICTInfo managedEntity = entityManager.merge(ictInfo); // ✅ merge()로 다시 Managed 상태로 변경
    managedEntity.setLocalIp(newLocalIp);
}
  • ✔️ 해결 방법 3: save() 대신 saveAndFlush() 사용

public void process(String userId, String newLocalIp) {
    ICTInfo ictInfo = ictInfoRepository.findByUserId(userId)
            .orElseThrow(() -> new IllegalArgumentException("ICTInfo not found: " + userId));

    ictInfo.setLocalIp(newLocalIp);
    ictInfoRepository.saveAndFlush(ictInfo); // ✅ 즉시 DB 반영
}

참고: JPA에서 엔티티는 4가지 상태를 가집니다.

상태
설명

Transient (비영속, New)

DB에 저장되지 않은 새로운 객체

Managed (영속, Persistent)

JPA가 관리하는 상태

Detached (준영속, Detached)

트랜잭션 종료 후 관리되지 않는 상태

Removed (삭제, Removed)

삭제된 상태

Last updated