😰프로젝트를 진행하던 중 실제 코드를 돌리게 되면 db에 저장이 되지 않는 문제가 발생하였습니다.
아래는 chatGpt를 이용하여 만든 예제입니다.
@Service
@Transactional(readOnly=true)
public class TransactionalService {
@Autowired
private UserRepository userRepository;
public void updateUser(Long userId, String newUsername) {
User user = userRepository.findById(userId);
user.setUsername(newUsername);
saveUser(user);
}
@Transactional
public void saveUser(User user) {
userRepository.save(user);
}
}
🤔여기서 문제가 되었던 부분은 updateUser를 불렀을때입니다.
트랜잭션
은AOP
기반으로 작동하기 때문에 해당 클래스의 메서드를 호출할 때AOP
프록시 객체가 먼저 호출됩니다. 따라서 updateUser()메서드 내에서 saveUser() 메서드를 직접 호출하면 같은 클래스 내의 다른 메서드이지만AOP
프록시를 우회하여 트랜잭션을 동작시키지 않을 수 있습니다.
더욱 문제가 되었던 것은 아래의 테스트 코드였습니다.
@SpringBootTest
@ActiveProfiles("test")
public class TransactionalServiceTest {
@Autowired
private UserRepository userRepository;
@Test
@Transactional
public void testUpdateUser() {
User user = new User("john.doe", "John Doe");
userRepository.save(user);
Long userId = user.getId();
String newUsername = "Jane Doe";
userRepository.updateUser(userId, newUsername);
User updatedUser = userRepository.findById(userId);
assertEquals(newUsername, updatedUser.getUsername());
}
}
😭테스트 코드를 만들어서 작성하여 돌리게 되면 이 코드는 제대로 동작하게 됩니다.
그래서 결국 테스트 코드에서 잡지 못하고 product 환경에서 저장이 되지 않는 문제가 발생하게 된 것입니다.
위 방법을 해결 하는 방법은 여러가지가 있습니다.
@Service
@Transactional(readOnly = true)
public class TransactionalService {
@Autowired
private UserRepository userRepository;
public void updateUser(Long userId, String newUsername) {
User user = userRepository.findById(userId);
user.setUsername(newUsername);
saveUser(user);
}
@Transactional
public void saveUser(User user) {
userRepository.save(user);
}
@Transactional
public void updateUserWithTransaction(Long userId, String newUsername) {
userRepository.updateUser(userId, newUsername);
}
}
@Transactional(readOnly=true)
-> @Transactional
로 바꾸기😎testUpdateUser() 메서드에서
@Transactional(readOnly=true)
를@Transactional
로 변경합니다. 이렇게 하면 테스트 메서드 내에서도 트랜잭션 범위 내에서 실행되어 데이터 변경이 커밋됩니다.
@Service
@Transactional(readOnly = true)
public class TransactionalService {
@Autowired
private UserService userService;
public void updateUser(Long userId, String newUsername) {
userService.updateUser(userId, newUsername);
}
}
@Service
@Transactional
public class UserService {
@Autowired
private UserRepository userRepository;
public void updateUser(Long userId, String newUsername) {
User user = userRepository.findById(userId);
user.setUsername(newUsername);
userRepository.save(user);
}
}
클래스를 분리 하게 되면 트랜잭션을 명시적으로 관리할 수 있으며, 클래스 간의 역할과 책임을 분리하여 코드를 더욱 모듈화하고 응집성을 높일 수 있습니다.
🧐위 테스트 코드는 편의를 위해
@Transactional
을 달아 놓아 쉽게 롤백을 하고 있습니다. 그러나 이로 인해 위와 같은 문제를 잡지 못하였는데 다른 방법으로는@Transactional
을 없애는 방법도 있습니다.만약
@Transactional
을 없애게 된다면 테스트 환경 전용 DB를 연결하고 테스트가 끝나고 나면 데이터를 삭제해주셔야 후에 문제 없이 동작하게 됩니다.
자기 자신을 호출 하는 방식으로 사용하는 방법도 있습니다만 이렇게 쓸 바에는 클래스를 분리하는 것이 더 효과적일 것이라고 생각이 듭니다.