@Transactional 내부 호출 문제

greenTea·2023년 6월 18일
1

문제 발생

😰프로젝트를 진행하던 중 실제 코드를 돌리게 되면 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 환경에서 저장이 되지 않는 문제가 발생하게 된 것입니다.


해결 방법

위 방법을 해결 하는 방법은 여러가지가 있습니다.

1. 메소드 분리

@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);
    }
}

2.@Transactional(readOnly=true) -> @Transactional로 바꾸기

😎testUpdateUser() 메서드에서 @Transactional(readOnly=true)@Transactional로 변경합니다. 이렇게 하면 테스트 메서드 내에서도 트랜잭션 범위 내에서 실행되어 데이터 변경이 커밋됩니다.

3. 클래스 분리

@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);
    }
}

클래스를 분리 하게 되면 트랜잭션을 명시적으로 관리할 수 있으며, 클래스 간의 역할과 책임을 분리하여 코드를 더욱 모듈화하고 응집성을 높일 수 있습니다.

4. 테스트 코드를 product 환경에 맞게 변경

🧐위 테스트 코드는 편의를 위해 @Transactional을 달아 놓아 쉽게 롤백을 하고 있습니다. 그러나 이로 인해 위와 같은 문제를 잡지 못하였는데 다른 방법으로는 @Transactional을 없애는 방법도 있습니다.

만약 @Transactional을 없애게 된다면 테스트 환경 전용 DB를 연결하고 테스트가 끝나고 나면 데이터를 삭제해주셔야 후에 문제 없이 동작하게 됩니다.

5. 자기 자신 호출

자기 자신을 호출 하는 방식으로 사용하는 방법도 있습니다만 이렇게 쓸 바에는 클래스를 분리하는 것이 더 효과적일 것이라고 생각이 듭니다.

profile
greenTea입니다.

0개의 댓글