Rails 개발자들이 자주 마주치는 데이터베이스 문제 중 하나가 바로 데이터 무결성(data integrity) 문제입니다. 적절한 유효성 검증과 더불어, 잘 설계된 트랜잭션 블록은 데이터가 일부만 생성되거나 업데이트되는 상황을 방지해 줍니다.
하지만 트랜잭션은 제대로 설계되지 않으면 애플리케이션에 악영향을 주거나, 심한 경우 데이터베이스 전체를 마비시킬 수도 있습니다.
이 글에서는 트랜잭션을 다룰 때 활용할 수 있는 모범 사례들을 소개합니다. 팁 자체는 간단하지만, 여러분의 트랜잭션이 견고하고 가독성 높으며 안전하게 동작하도록 만드는 데 큰 도움이 될 것입니다.
그럼 시작해 보겠습니다!
1. 가능하면 Bang 메서드(!)를 사용하세요
Rails에서 !가 붙은 메서드는 문제가 발생했을 때 확실하게 예외를 발생시켜 주기 때문에 안심하고 사용할 수 있습니다.
예를 들어 #save 메서드에는 save! 버전도 존재합니다. 컨트롤러처럼 예외를 발생시키고 싶지 않은 곳에서는 이 버전을 사용하는 편이 좋습니다.
그러나 이러한 접근 방식은 트랜잭션 안에서는 제대로 작동하지 않습니다. save를 사용하면 오류가 발생했을 때 프로세스를 롤백할 수 없기 때문입니다. 그래서 트랜잭션 안에서는 메서드의 ! 버전을 사용하는 것이 매우 중요합니다.
save를 사용한 코드에서는 membership 레코드가 생성되지 않았음에도 트랜잭션이 성공으로 처리되어, 결국 데이터베이스에는 불완전한 데이터 구조가 남게 됩니다.
반면 save!를 사용하면 ActiveRecord::RecordNotSaved 오류가 발생하여 트랜잭션이 정상적으로 롤백됩니다.
2. 트랜잭션 내 에러를 올바르게 처리하세요
트랜잭션에서 발생하는 에러를 다룰 때는 반드시 지켜야 할 몇 가지 규칙이 있습니다. 이 규칙을 따르면 가독성이 좋고 안정적으로 동작하는 코드를 작성할 수 있으며, 다른 개발자에게 혼란을 주거나 디버깅하기 어려운 기이한 동작을 만들 일도 없습니다.
ActiveRecord::StatementInvalid는 rescue로 처리하지 마세요
ActiveRecord::StatementInvalid는 데이터베이스 레벨에서 무언가 잘못되었을 때 발생하는 특수한 오류입니다. 이 오류는 절대 rescue로 감싸면 안 됩니다. 데이터베이스 쿼리에 문제가 생기면 항상 명시적으로 알림을 받아야 하기 때문입니다.
올바른 레벨에서 rescue를 사용하세요
트랜잭션 블록 내부에서 rescue를 사용하면 오류를 잡긴 하지만, 오류를 직접 처리해 버렸기 때문에 트랜잭션이 롤백되지 않습니다. 오류가 그대로 발생하도록 두고, 트랜잭션 블록 바깥에서 잡아야 합니다.
이렇게 하면 오류 발생 시 트랜잭션이 롤백되면서 동시에 오류도 포착할 수 있습니다. 트랜잭션의 기본 동작을 해치지 않으면서 트랜잭션 내부에서 발생한 오류를 처리하는 올바른 방법입니다.
일반적인 오류를 잡지 마세요
StandardError나 ArgumentError 같은 포괄적인 오류를 잡는 것은 피해야 합니다. 이는 가독성 있고 테스트하기 쉬운 코드를 위한 일반적인 원칙이기도 하지만, 트랜잭션 관점에서도 반드시 기억해야 할 사항입니다.
이런 오류들을 넓게 잡으면 코드의 다른 곳에서 발생한 오류까지 함께 묻히게 되어 디버깅이 훨씬 어려워집니다. rescue 지점과는 무관한 심각한 문제가 조용히 숨겨질 수도 있습니다.
ActiveRecord의 기본 롤백 오류를 현명하게 활용하세요
ActiveRecord는 트랜잭션 내부에서 조용히 롤백을 실행할 수 있게 해주는 특별한 오류 클래스를 제공합니다. ActiveRecord::Rollback 오류를 발생시키면 트랜잭션은 롤백되지만, 다른 오류와 달리 이 오류는 블록 밖으로 전파되지 않습니다. 이 특성을 잘 기억해 두고 상황에 맞게 활용하세요.
3. 트랜잭션을 사용하지 말아야 할 때를 파악하세요
모든 것이 그렇듯, 코드에서 트랜잭션을 과도하게 사용해서는 안 됩니다. 예를 들어 단 하나의 쿼리만 트랜잭션으로 감싸는 것은 흔한 실수입니다. 쿼리 하나가 실패하더라도 롤백할 대상이 없기 때문에 아무 의미가 없습니다.
또 다른 흔한 실수는 데이터베이스 호출과 관련 없는 코드를 트랜잭션으로 감싸는 것입니다. 트랜잭션은 블록 내부의 코드가 실행되는 동안 데이터베이스 연결을 계속 점유하기 때문에 이런 방식은 피해야 합니다. 가능한 한 블록 내부에는 데이터베이스 호출만 포함하도록 제한하세요.
4. 트랜잭션의 단점을 이해하세요
트랜잭션은 데이터베이스 내 데이터 무결성을 유지하는 데 큰 도움이 되지만, 단점 또한 인지해야 합니다. 예를 들어 트랜잭션 블록으로 감싸인 쿼리들은 단일 쿼리보다 더 많은 DB 리소스를 소모합니다.
트랜잭션 사용의 또 다른 단점은 코드가 복잡해진다는 점입니다. 잘못 사용하면 오히려 코드의 가독성을 떨어뜨릴 수 있습니다.
5. 트랜잭션 블록을 올바른 맥락에서 사용하세요
ActiveRecord를 상속받는 클래스라면 어디서든 transaction 메서드를 사용할 수 있습니다. 다만 어떤 버전을 사용하느냐도 중요합니다. 기능적인 관점에서는 큰 차이가 없을 수 있지만, 코드 가독성 측면에서는 분명한 차이가 있습니다.
transaction 메서드를 사용하는 대표적인 세 가지 방식은 다음과 같습니다.
여러 모델을 다루면서 인스턴스 메서드 호출과 클래스 호출을 블록 안에서 섞어 사용한다면 ActiveRecord::Base.transaction을 사용하는 것이 좋습니다.
특정 모델에 속한 코드를 주로 다룬다면 해당 클래스 레벨에서 transaction 메서드를 호출하세요.
모델 인스턴스를 대상으로 작업할 때는 인스턴스 레벨에서 transaction 메서드를 호출하는 것이 자연스럽습니다.
물론 이러한 규칙은 공식적인 것은 아닙니다. 코드를 더 읽기 쉽게 만들기 위한 제안일 뿐입니다.
마무리: Ruby on Rails 프로젝트의 트랜잭션 점검하기
Ruby on Rails에서 트랜잭션을 다루는 이번 팁들이 도움이 되었기를 바랍니다.
우리는 Rails 트랜잭션을 올바르게 설계함으로써 데이터 무결성을 높이고, 예상치 못한 부작용 없이 프로세스가 안정적으로 동작하도록 만드는 것의 중요성을 살펴보았습니다.
더 나아가, 체계적인 에러 처리 정책은 트랜잭션을 사용할 때만 유용한 것이 아니라 코드베이스 전반의 품질을 끌어올립니다. 코드에서 오류가 발생할 것으로 예상되는 다음 순간에도 이 점을 꼭 기억하세요.
지금이 바로 Ruby on Rails 프로젝트의 트랜잭션 설계를 점검하고 오류를 예방하기 좋은 시기입니다. 데이터베이스와 효율적이고 안정적으로 통신하도록 설계하여 애플리케이션을 한층 더 견고하게 만들어 보세요.
즐거운 코딩 되세요!
P.S. Ruby Magic의 글을 발행 즉시 읽고 싶으시다면 Ruby Magic 뉴스레터를 구독하고 어떤 글도 놓치지 마세요!
Paweł Dąbrowski
게스트 저자 Paweł은 오픈소스 애호가이자 성장을 추구하는 개발자로, 사람과 컴퓨터를 위해 글을 써온 경력이 10년 이상입니다. 점과 점을 연결하여 고품질 소프트웨어를 만들고, 사람과 기업 사이의 가치 있는 관계를 구축하는 데 주력하고 있습니다.
Paweł Dąbrowski의 모든 글 보기