문제 및 해결

[Spring JPA] 영속성 동작하지 않을 때의 원인과 해결법

0and24 2024. 12. 16. 16:18

이전에 작업했던 프로젝트를 마이바티스에서 JPA로 전환하는 작업 중 데이터를 호출한 후 값을 변경했지만, 실제 데이터베이스에는 save()를 직접 사용하지 않으면 변경된 값이 저장되지 않는 문제가 발생했습니다.

JPA를 사용하면서 트랜잭션 어노테이션을 생략한 실수를 통해 영속성 컨텍스트와 트랜잭션의 상호작용을 깊이 이해할 수 있었습니다. 이 글에서는 그 경험을 바탕으로 트랜잭션 어노테이션이 필요한 이유와 깨달은 점을 공유합니다.


1. 문제가 발생한 상황

JPA를 사용하는 메서드에 평소처럼 데이터를 수정하는 작업을 수행했습니다. 코드 예시는 다음과 같습니다:

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    public void updateUser(Long userId, String newName) {
        User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("User not found"));
        user.setName(newName);
        // 변경 사항을 저장하지 않음
    }
}

반면, 직접 save()를 사용하여 데이터를 저장하면 변경 사항을 저장합니다. ( 이런식으로 문제를 해결할 수 있지만 이 방법은 심각한 문제를 야기 할 수 있습니다. 밑에 제가 그 이유를 설명해보겠습니다.)

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    public void updateUser(Long userId, String newName) {
        User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("User not found"));
        user.setName(newName);
        userRepository.save(user); //정상적으로 변경 값 저장
    }
}

답: 여기에서 이런 문제가 발생한 이유는 단순하게 @Transactional을 붙이지 않아서 그렇습니다.


2. 왜 트랜잭션 어노테이션이 필요한가?

트랜잭션은 JPA의 영속성 컨텍스트와 밀접하게 연결되어 있습니다. 트랜잭션이 활성화되지 않으면 다음과 같은 문제가 발생할 수 있습니다:

  1. 영속성 컨텍스트 종료: 트랜잭션이 없으면 메서드 실행이 끝나는 즉시 영속성 컨텍스트가 종료됩니다. 따라서 변경된 엔티티는 더 이상 관리되지 않고, 데이터베이스에 반영되지 않습니다.
  2. 데이터 일관성 부족: 여러 작업을 수행하는 동안 트랜잭션이 없다면 일부만 성공하고 나머지는 실패하는 상황이 발생할 수 있습니다. 이는 데이터 무결성을 위협합니다.
  3. 지연 로딩 문제: 트랜잭션이 없으면 영속성 컨텍스트가 관리되지 않아 LazyInitializationException과 같은 문제가 발생할 수 있습니다.
  4. 중복 호출 문제: 트랜잭션 없이 save를 여러 번 호출하면 매번 데이터베이스와 직접 상호작용하게 됩니다. 이는 다음과 같은 부작용을 초래할 수 있습니다:
    • 성능 저하: 영속성 컨텍스트가 변경 사항을 일괄 처리하지 않으므로, 각 호출마다 데이터베이스 쓰기 작업이 발생해 성능이 떨어집니다.
    • 일관성 문제: 한 호출이 실패하면 이미 저장된 변경 사항을 되돌릴 방법이 없어 데이터 불일치가 발생할 수 있습니다.
    • 코드 복잡성 증가: 각 호출의 성공 여부를 개별적으로 관리해야 하므로 코드 유지보수가 어려워집니다.

3. 문제 해결: 트랜잭션 어노테이션 추가

위 문제를 해결하기 위해 메서드에 @Transactional 어노테이션을 추가했습니다. 수정된 코드는 다음과 같습니다:

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Transactional
    public void updateUser(Long userId, String newName) {
        User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("User not found"));
        user.setName(newName);
    }
}

이제 트랜잭션이 활성화되어 영속성 컨텍스트가 변경된 엔티티를 추적하고, 메서드가 종료될 때 데이터베이스에 변경 사항을 반영합니다.


4. 깨달은 점

이번 경험을 통해 다음과 같은 중요한 점을 배웠습니다:

  1. 트랜잭션은 필수적이다: JPA는 읽기 작업에서는 트랜잭션 없이도 동작할 수 있지만, 데이터 변경 작업(INSERT, UPDATE, DELETE)에는 반드시 트랜잭션이 필요합니다. 트랜잭션이 없으면 영속성 컨텍스트와 데이터베이스 간의 동기화가 보장되지 않습니다.

  2. 트랜잭션 범위를 명확히 설정해야 한다: 서비스 계층에서 트랜잭션을 명시적으로 관리하는 것이 가장 바람직합니다.

  3. 코드 리뷰와 테스트의 중요성: 이런 실수는 코드 리뷰나 테스트를 통해 사전에 발견할 수 있었을 것입니다.

5. 트랜잭션 어노테이션 사용 시 주의사항

  • 읽기 전용 트랜잭션: 읽기 전용 작업에는 @Transactional(readOnly = true)를 사용하여 성능을 최적화할 수 있습니다.
  • 예외 처리: 트랜잭션이 롤백되지 않는 상황을 방지하려면 커스텀 예외가 RuntimeException을 상속받도록 해야 합니다.

이번에 운이 좋게? 이런 문제가 발생하여 원인과 해결방법을 찾아봤었는데요.이번 실수는 저에게 JPA의 동작 원리를 깊이 이해하는 계기가 되었습니다.

혹시나 이 글이 이해가 안되거나 어렵다면 이전에 작성했던 JPA에 관한 글을 참고 하시면 좋을 것 같습니다.

2024.12.10 - [Spring] - JPA (Java Persistence API)란?

 

JPA (Java Persistence API)란?

JPA(Java Persistence API)는 Java 개발자가 데이터베이스 작업을 객체 지향적으로 처리할 수 있도록 지원하는 표준 사양입니다. SQL을 직접 작성하는 대신, 데이터베이스 테이블과 자바 객체(Entity)를 매

0and24.tistory.com