IT/Spring

스프링 @Transactional 에서 readOnly 설정을 하는 이유

yeTi 2024. 6. 18. 23:54

안녕하세요. yeTi입니다.
오늘은 스프링 프레임워크에서 트랜젝션을 관리하는 방법을 간략하게 언급하고 트랜젝션을 사용하지 않는 상황과 읽기 전용 트랜젝션을 사용할 때의 장단점을 말해보고자 합니다.

스프링프레임워크는 무엇일까요?

스프링 프레임워크는 엔터프라이즈 애플리케이션 개발을 간편하게 하기 위해 만들어진 강력한 프레임워크로 다양한 모듈과 기능을 제공합니다. 다음은 스프링 프레임워크의 주요 특징과 구성 요소입니다.

주요 특징

  1. 경량성: 스프링은 경량 프레임워크로 필요한 기능만 선택하여 사용할 수 있습니다. 만들어진 때에는 경량이었지만 요즘에는 경량이라고 말하기는 부적절한 면이 있습니다.
  2. 의존성 주입(Dependency Injection): 스프링은 객체 간의 의존성을 설정 파일이나 어노테이션을 통해 관리합니다.
  3. AOP(Aspect-Oriented Programming): 횡단 관심사(예: 로깅, 보안, 트랜잭션 관리 등)를 분리하여 코드의 모듈성을 높입니다.
  4. 트랜잭션 관리: 선언적 트랜잭션 관리를 통해 데이터베이스 트랜잭션을 쉽게 관리할 수 있습니다.
  5. MVC(Model-View-Controller): 스프링 MVC 모듈을 사용하면 웹 애플리케이션을 구조적으로 개발할 수 있습니다.
  6. 유연성: 다양한 기술과 쉽게 통합할 수 있습니다.

주요 모듈

  1. 스프링 코어(Core): IoC 컨테이너, 의존성 주입 기능을 제공합니다.
  2. 스프링 AOP: Aspect-Oriented 프로그래밍 기능을 제공합니다.
  3. 스프링 데이터: 데이터 접근을 쉽게 하기 위한 모듈로, JPA, MongoDB, Neo4j 등을 지원합니다.
  4. 스프링 MVC: 웹 애플리케이션 개발을 위한 모델-뷰-컨트롤러 아키텍처를 제공합니다.
  5. 스프링 시큐리티(Security): 애플리케이션의 보안을 관리합니다.
  6. 스프링 부트(Boot): 독립 실행형 애플리케이션을 빠르게 개발할 수 있도록 도와주는 프로젝트입니다.

스프링 프레임워크는 그 유연성과 확장성 덕분에 다양한 엔터프라이즈 애플리케이션 개발에 널리 사용되고 있습니다.

스프링에서 트랜젝션 관리

트랜잭션 관리는 데이터베이스 작업의 일관성과 신뢰성을 보장하기 위해 매우 중요한 요소입니다. 스프링은 선언적 트랜잭션 관리와 프로그래밍적 트랜잭션 관리를 모두 지원합니다.

선언적 트랜잭션 관리

선언적 트랜잭션 관리는 XML 설정 파일이나 어노테이션을 통해 트랜잭션을 관리하는 방법입니다. 이 방식은 코드와 트랜잭션 관리 로직을 분리할 수 있어 코드의 가독성과 유지보수성을 높여줍니다.

어노테이션 기반 트랜잭션 관리

스프링에서 가장 많이 사용되는 방법은 @Transactional 어노테이션을 사용하는 것입니다. 이 어노테이션을 사용하면 메서드나 클래스에 트랜잭션을 적용할 수 있습니다.

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class MyService {

    @Transactional
    public void performTransaction() {
        // 트랜잭션이 필요한 비즈니스 로직
    }
}

@Transactional 어노테이션은 트랜잭션 전파(propagation), 격리 수준(isolation level), 읽기 전용 여부(read-only), 롤백 조건(rollback rules) 등을 설정할 수 있습니다.

@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, readOnly = false, rollbackFor = Exception.class)
public void performTransaction() {
    // 트랜잭션이 필요한 비즈니스 로직
}

프로그래밍적 트랜잭션 관리

프로그래밍적 트랜잭션 관리는 PlatformTransactionManagerTransactionTemplate을 사용하여 코드 내에서 트랜잭션을 명시적으로 제어하는 방법입니다.

PlatformTransactionManager 사용

PlatformTransactionManager를 사용하면 트랜잭션 시작, 커밋, 롤백을 명시적으로 제어할 수 있습니다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

@Service
public class MyService {

    @Autowired
    private PlatformTransactionManager transactionManager;

    public void performTransaction() {
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

        TransactionStatus status = transactionManager.getTransaction(def);
        try {
            // 트랜잭션이 필요한 비즈니스 로직

            transactionManager.commit(status);
        } catch (Exception ex) {
            transactionManager.rollback(status);
            throw ex;
        }
    }
}

TransactionTemplate 사용

TransactionTemplate을 사용하면 트랜잭션 경계를 명확하게 설정할 수 있습니다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

@Service
public class MyService {

    @Autowired
    private TransactionTemplate transactionTemplate;

    public void performTransaction() {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                try {
                    // 트랜잭션이 필요한 비즈니스 로직
                } catch (Exception ex) {
                    status.setRollbackOnly();
                    throw ex;
                }
            }
        });
    }
}

스프링에서 트랜잭션 관리는 선언적 트랜잭션 관리가 주로 사용되며, 코드의 간결성과 유지보수성을 높여줍니다. 트랜잭션 관리를 통해 데이터베이스 작업의 일관성과 신뢰성을 보장할 수 있습니다.

선언적 트랜젝션에서 read only 속성의 역할

readOnly 속성은 트랜잭션이 오직 읽기 전용 작업만 수행될 것임을 명시하는 데 사용됩니다. 이 속성은 데이터베이스의 성능 최적화와 무결성 보장에 중요한 역할을 합니다.

readOnly 속성의 역할

  1. 성능 최적화: 데이터베이스에서 읽기 전용 트랜잭션으로 설정하면, 데이터베이스가 내부적으로 최적화를 할 수 있습니다. 예를 들어, 특정 데이터베이스 시스템에서는 읽기 전용 트랜잭션이 쓰기 잠금을 피하거나 캐시를 더 효과적으로 사용할 수 있습니다.

  2. 무결성 보장: 읽기 전용 트랜잭션으로 설정하면, 트랜잭션 내에서 데이터 수정이 시도될 경우 오류를 발생시켜 데이터의 무결성을 유지할 수 있습니다. 이는 실수로 데이터베이스를 변경하는 것을 방지하는 데 유용합니다.

사용 예

@Transactional 어노테이션에서 readOnly 속성을 사용하는 예는 다음과 같습니다:

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class MyService {

    @Transactional(readOnly = true)
    public List<MyEntity> fetchData() {
        // 데이터베이스에서 데이터를 읽어오는 로직
        // 이 메서드는 읽기 전용 트랜잭션 내에서 실행됩니다.
    }
}

위 예제에서 fetchData 메서드는 읽기 전용 트랜잭션으로 실행되므로, 이 트랜잭션 내에서 데이터 수정이 시도될 경우 예외가 발생할 수 있습니다.

실전 적용

실제로는 CRUD(생성, 읽기, 업데이트, 삭제) 작업 중 읽기 작업에 대해서만 readOnly = true를 설정합니다. 예를 들어, 다음과 같은 상황에서 유용합니다:

  1. 데이터 조회: 대규모 데이터를 조회하는 서비스 메서드.
  2. 보고서 생성: 데이터베이스에서 대량의 데이터를 읽어와 보고서를 생성하는 경우.
  3. 캐시 조회: 캐시에 저장된 데이터를 읽어오는 경우.

다음은 추가적인 예제입니다:

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class MyService {

    @Transactional(readOnly = true)
    public MyEntity getEntity(Long id) {
        // ID로 엔티티를 조회
        return entityManager.find(MyEntity.class, id);
    }

    @Transactional
    public MyEntity saveEntity(MyEntity entity) {
        // 엔티티를 저장
        return entityManager.persist(entity);
    }

    @Transactional
    public void deleteEntity(Long id) {
        // ID로 엔티티를 삭제
        MyEntity entity = entityManager.find(MyEntity.class, id);
        if (entity != null) {
            entityManager.remove(entity);
        }
    }
}

여기서 getEntity 메서드는 읽기 전용 트랜잭션으로 실행되며, 데이터베이스 수정 작업을 방지합니다. 반면 saveEntitydeleteEntity 메서드는 기본 트랜잭션 설정을 사용하여 데이터베이스에 쓰기 작업을 수행할 수 있습니다.

이러한 트랜잭션 관리 방식은 애플리케이션의 성능과 안정성을 높이는 데 도움이 됩니다.

트랜젝션을 사용하지 않는 것과 readonly 속성 비교

트랜잭션을 사용하지 않을 때의 성능 이점과 스프링 트랜잭션의 readOnly 속성을 사용하는 경우의 성능을 비교해 보겠습니다.

트랜잭션 없이 수행하는 경우

트랜잭션을 사용하지 않으면 데이터베이스의 트랜잭션 관리 오버헤드가 줄어들어 성능이 향상될 수 있습니다. 이는 특히 다음과 같은 상황에서 유리합니다:

  1. 데이터 일관성이 중요하지 않은 읽기 작업: 특정 작업이 데이터베이스의 일관성을 요구하지 않는다면, 트랜잭션을 생략함으로써 성능을 최적화할 수 있습니다.
  2. 단순 조회 작업: 복잡한 비즈니스 로직이 없이 단순히 데이터를 조회하는 작업에서는 트랜잭션 오버헤드를 피할 수 있습니다.

스프링 트랜잭션에서 readOnly 속성 사용

스프링 트랜잭션에서 readOnly 속성을 true로 설정하면, 트랜잭션은 여전히 사용되지만 읽기 전용 최적화를 통해 성능을 향상시킬 수 있습니다. 다음과 같은 이점이 있습니다.

  1. 데이터베이스 최적화: 많은 데이터베이스 시스템은 읽기 전용 트랜잭션을 감지하고 내부적으로 최적화를 수행합니다. 예를 들어, 잠금 경감, 캐싱 최적화 등.
  2. 일관성 유지: 읽기 전용 트랜잭션을 사용하면 데이터베이스의 일관성을 유지하면서도 성능 최적화를 할 수 있습니다.
  3. 보안성: 실수로 인한 데이터 수정이 방지됩니다.

성능 비교

  1. 트랜잭션 없음:

    • 장점: 트랜잭션 관리에 따른 오버헤드가 없기 때문에 순수 읽기 작업에서 성능이 더 빠를 수 있습니다.
    • 단점: 데이터 일관성과 무결성을 보장할 수 없습니다. 특히 동시성 제어가 필요한 경우 문제가 발생할 수 있습니다.
  2. 읽기 전용 트랜잭션 (readOnly = true):

    • 장점: 데이터베이스의 최적화 기능을 활용할 수 있으며, 트랜잭션 관리에 따른 일관성과 무결성을 유지할 수 있습니다. 다수의 데이터베이스 시스템에서 읽기 전용 트랜잭션을 효율적으로 처리할 수 있습니다.
    • 단점: 트랜잭션 관리 오버헤드가 약간 존재하지만, 읽기 전용 최적화를 통해 이를 최소화할 수 있습니다.

스프링에서 트랜잭션 없이 수행하는 방법

트랜잭션을 사용하지 않고 데이터베이스 작업을 수행하는 방법은 일반적으로 두 가지가 있습니다. 트랜잭션 관리 기능을 완전히 생략하는 방법과 트랜잭션을 명시적으로 제어하지 않는 방법입니다.

트랜잭션 관리 기능 생략

스프링에서 트랜잭션을 완전히 생략하고 수행하는 방법입니다. 이를 위해 @Transactional 어노테이션을 사용하지 않고, 트랜잭션 매니저를 설정하지 않으면 됩니다.

예제:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Service
public class MyService {

    @PersistenceContext
    private EntityManager entityManager;

    public MyEntity findEntity(Long id) {
        // 트랜잭션 없이 데이터베이스 작업 수행
        return entityManager.find(MyEntity.class, id);
    }

    public void saveEntity(MyEntity entity) {
        // 트랜잭션 없이 데이터베이스 작업 수행
        entityManager.persist(entity);
    }
}

위 코드에서는 @Transactional 어노테이션을 사용하지 않으므로 트랜잭션이 생성되지 않습니다. 이는 데이터베이스의 기본 트랜잭션 설정에 따르며, 데이터베이스에 따라 다르게 작동할 수 있습니다.

2. 명시적 트랜잭션 제어 생략

프로그래밍적 트랜잭션 관리를 사용하지 않고, 트랜잭션을 명시적으로 제어하지 않는 방법입니다. 이 방법은 트랜잭션 관리자를 전혀 사용하지 않는 경우에 해당합니다.

예제:

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class MyRepository {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public MyEntity findEntity(Long id) {
        // 트랜잭션 없이 데이터베이스 작업 수행
        String sql = "SELECT * FROM MyEntity WHERE id = ?";
        return jdbcTemplate.queryForObject(sql, new Object[]{id}, new MyEntityRowMapper());
    }

    public void saveEntity(MyEntity entity) {
        // 트랜잭션 없이 데이터베이스 작업 수행
        String sql = "INSERT INTO MyEntity (name, value) VALUES (?, ?)";
        jdbcTemplate.update(sql, entity.getName(), entity.getValue());
    }
}

위 코드에서는 JdbcTemplate을 사용하여 데이터베이스 작업을 수행합니다. JdbcTemplate을 사용할 때도 트랜잭션 관리자를 설정하지 않으면 트랜잭션이 생성되지 않습니다.

주의사항

트랜잭션 없이 작업을 수행하는 것은 다음과 같은 몇 가지 단점을 가질 수 있습니다:

  1. 데이터 일관성 문제: 동시성 문제가 발생할 수 있으며, 여러 작업이 동시에 수행되면서 데이터 불일치가 발생할 수 있습니다.
  2. 오류 처리: 트랜잭션이 없기 때문에 작업 중 오류가 발생해도 롤백되지 않아 데이터가 손상될 수 있습니다.
  3. 복구 불가능성: 데이터베이스 작업이 실패한 후에도 상태를 원래대로 되돌릴 수 없습니다.

따라서, 데이터 일관성과 무결성이 중요한 애플리케이션에서는 트랜잭션을 사용하는 것이 좋습니다. 트랜잭션 없이 작업을 수행하는 것은 주로 데이터 일관성이 크게 중요하지 않은 경우나 성능 최적화가 절대적으로 필요한 경우에만 고려해야 합니다.

결론

트랜잭션을 완전히 사용하지 않으면, 단순한 읽기 작업에서 성능이 더 나을 수 있지만, 데이터의 일관성과 무결성 문제가 발생할 가능성이 있습니다. 반면, 스프링의 readOnly 트랜잭션은 트랜잭션 관리 오버헤드를 줄이면서도 데이터 일관성을 유지할 수 있습니다. 이는 데이터 일관성이 중요한 엔터프라이즈 애플리케이션에서 더 적합한 선택이 될 수 있습니다.

따라서, 데이터 일관성이 중요하지 않고 순수한 성능을 추구하는 경우 트랜잭션을 생략할 수 있지만, 대부분의 경우 읽기 전용 트랜잭션을 사용하는 것이 성능과 일관성 모두에서 균형 잡힌 접근 방식일 것입니다.