Transactional 정리

@Transactional이란?

✔️ 개념

  • 하나의 트랜잭션 단위로 모든 작업이 성공하면 커밋, 하나라도 실패하면 롤백.

  • 주로 Service 계층에서 사용됨.

💚 테스트에서 @Transactional을 사용하면?

✔️효과

  • 테스트 메서드에서 @Transactional을 붙이면, 테스트가 끝난 뒤 DB 변경사항이 자동으로 롤백됨.

  • 즉, 테스트가 DB에 영향을 주지 않음 -> 항상 깨끗한 상태 유지 가능.

    • 테스트 프레임워크 레벨에서 트랜잭션을 무조건 롤백하도록 되어 있다.

@Transaction은 프록시 기반 AOP로 동작한다.

Spring이 만든 Proxy 객체를 통해 호출될 때만 트랜잭션이 적용된다.

@Service
@Transactional
public class MyService {
    public void save() {
        // 트랜잭션 시작
    }
}

Spring Context (ApplicationContext)
   └─ MyService proxy (@Transactional 감싼 가짜 객체)
          └─ 내부적으로 진짜 MyService 호출

Spring Container(ApplicationContext)에 등록된 프록시를 통해 호출될 때만 트랜잭션이 적용된다.

✅ 예시 1: 정상 작동하는 경우 (외부 Bean 간 호출)

@Component
public class Controller {
    @Autowired
    private MyService service;

    public void handleRequest() {
        service.save(); // ✅ 프록시를 통해 호출 → 트랜잭션 O
    }
}

❌ 예시 2: 트랜잭션이 동작하지 않는 경우 (자기 자신 내부 호출)

@Service
public class MyService {
    
    @Transactional
    public void outer() {
        inner(); // ❌ 직접 호출 → 프록시 통과 X → 트랜잭션 적용 안 됨
    }

    @Transactional
    public void inner() {
        // 여기에 트랜잭션 안 걸림
    }
}

✅ 해결책

내부 호출에서도 트랜잭션이 작동하도록 하려면?

@Autowired
private MyService self;

public void outer() {
    self.inner(); // ✅ ApplicationContext에서 주입된 프록시 → OK
}

@Transactional(readOnly = true) readOnley = true : 읽기 전용

CRUD에서 CUD 동작 X / only READ만 가능

  • JPA에서 CUD 스냅샷 저장, 변경감지를 하지않기때문에 (성능 향상)

  • CQRS를 하는 이유

    • COMMAND 와 READ를 분리하자

    • READ라는 행위가 보통 압도적으로 많다.

  • Master DB/ Slave DB( 복제본) 분리해서 Read는 Slave에서 , CUD는 Master DB에

@Transactional(readOnly = true)
    public List<ProductResponse> getSellingProducts() {

        List<Product> products = productRepository.findAllBySellingStatusIn(ProductSellingStatus.forDisplay());
        return products.stream()
                .map(product -> ProductResponse.of(product))
                .collect(Collectors.toList());

    }

    // 동시성 이슈 때문에
    // 실제 서비스에서는 UUID 활용하는게 좋을 수도
    @Transactional
    public ProductResponse createProduct(ProductCreateRequest request) {
        String latestProductNumber = productRepository.findLatestProduct();

        Product product = request.toEntity(request,createNextProductNumber(latestProductNumber));

        Product savedProduct = productRepository.save(product);

        ProductResponse productResponse = ProductResponse.of(savedProduct);

        return productResponse;
    }

    private String createNextProductNumber(String latestProductNumber) {
        if (latestProductNumber == null) {
            return "001";
        }
        int nextNumber = Integer.parseInt(latestProductNumber) + 1;
        return String.format("%03d", nextNumber);
    }

Last updated