본문 바로가기

SpringBoot/Blog프로젝트 with JPA &데어프로그래밍님

21.1.7 TIL - 트랜잭션과 서비스, 데이터베이스 격리수준

※본 포스팅은 데어프로그래밍님 스프링부트 블로그프로젝트 수강후 작성한 내용입니다.

 

[트랜잭션과 서비스]

회원가입을 처리하기 위해 service라는 패키지를 만들어서 UserService class를 만들었다.

해당 클래스의 코드는 아래와같다.

package com.cos.blog.service;

import javax.transaction.Transactional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.cos.blog.model.User;
import com.cos.blog.repository.UserRepository;

@Service //이 어노테이션이 있어야 스프링이 컴포넌트 스캔을 통해서 Bean에 등록해준다.IoC를 해준다는 뜻
public class UserService {
	
	@Autowired
	private UserRepository userRepository;
	
	@Transactional
	public void 회원가입(User user) {
		userRepository.save(user);
	}
}

회원가입의 경우 insert만 해주면 되기 때문에 코드가 간단하다.

 

서비스가 왜 필요한가? 를 보기 전에 트랜잭션이란 무엇인지를 먼저 생각해보자.

 

트랜잭션이란 목적에 맞는 일을 처리하기 위한 가장 작은 일의 단위이다. 데이터의 변경이 다 끝나면 commit하는 용도로 트랜잭션을 시작하기 때문에 보통 데이터의 변경이 있을때만 트랜잭션을 사용한다. 따라서 select는 트랜잭셕으로 사용하지 않는다.(예외의 경우는 아래에서 격리수준 설명할때!) 스프링에서는 @Transactional annotation을 붙여서 사용한다. 예를들어 입금을 한다고 해보자. 그러면 돈을 insert하는 것이 하나의 트랜잭션이다. 돈을 송금하다고 해보자. 그러면 내 돈을 update하고 상대방의 돈도 update해야한다. 이 경우 목적에 따라 내 돈을 update하는 하나의 일이 트랜잭션이 될 수도 있지만 송금의 경우 두가지 일이 모두 일어나야 하기 때문에 두개의 트랜잭션을 묶어서 하나의 트랜잭션이라고 할 수 있다. 이렇게 하나의 일을 처리하기 위해 여러개의 트랜잭션이 모여서 하나의 트랜잭션이 되는것을 스프링에서는 서비스라고 한다.

 

서비스를 사용하는 이유

1. 트랜잭션을 관리하기 위해서

2. '서비스'라는 의미때문

 - 서비스라는것은 하나의 서비스, 하나의 기능이 되어야한다. 그 서비스에 있는 트랜잭션이 모두 오류가 없이 성공해야 일을 마쳤다고 할 수 있는 것이다. (이래야 commit한다) 만약 그 서비스내의 트랜잭션 중 하나라도 오류가 있다면 일을 잘 수행하지 못한 것이기 때문에 전체를 다 rollback해줘야한다.

 예를들어 송금서비스에서 내 돈을 update하는 것은 성공했지만 상대방의 돈을 update하는것은 실패했다면 송금에 실패한 것이기 때문에 다 rollback해주어야한다

 

[데이터베이스 격리수준]

데이터베이스 격리수준이라는 것은 데이터베이스에서 여러개의 트랜잭션이 있을때 이를 관리하는 방법을 말한다.

이 포스팅에서는 오라클, mysql의 격리수준을 보고 다음 포스팅에서 스프링부트의 격리수준을 보는것으로 하자.

 

[오라클의 격리수준 - read commit]

오라클의 기본 격리수준은 read commit이다. 이는 read를 하는데 commit된 것만 read를 한다는 것이다. 

예를들어 사용자 A,B가 있고, 기존에 DB에 id=1, name = rachel 이라는 데이터가 있다고 해보자. A는 트랜잭션을 시작해서 update문을 수행해서 DB의 데이터를 id=1, name=yejin으로 바꾸고 아직 트랜잭션을 종료하지 않았다.(=commit하지 않았다) 이때 B가 id=1인 데이터를 select하면 B는 rachel이라는 데이터를 얻게된다. B가 select해오는 데이터는 undo에 있는 데이터를 가지고오게 되는데 아직 A가 commit하지 않아서 undo에 있는 데이터가 변경되지 않았기 때문이다. A가 트랜잭션을 종료해서 commit된 후에 B가 select를 하면 yejin이라는 데이터를 얻는다.

이렇게 B가 트랜잭션을 시작하지 않은 상태에서 select한 결과가 변경되는것은 문제가 되지 않는다. 하지만 B가 select하고 이 결과들을 연산해서 insert를 하려고 트랜잭션을 시작한 경우, 트랜잭션 내에서 select의 결과가 변경되는 것은 문제가 된다. 이를 데이터의 부정합(정합성이 깨졌다)라고 한다. 트랜잭션 내에서 select를 했을 때 데이터가 보였다 안보였다하는 문제도 발생할 수 있는데 이를 Phantom Read라고 한다. 이 문제점 때문에 repeatable read를 사용한다. 이것은 mysql의 격리수준에서 살펴보자.

 

[MySQL의 격리수준 - InnoDB스토리지 엔진]

MySQL의 기본격리수준은 InnoDB스토리지 엔진으로 repeatable read이상을 사용한다. 따라서 데이터의 부정합이 발생하지 않는다. 즉, 내가 하나의 트랜잭션을 시작하고 나서 select했을 때 그 트랜잭션 내에서의 select의 결과가 동일하다.

repeatable read는 다음과 같이 동작한다고 생각하면 된다.

1. Transaction에 번호를 붙인다. 나중에 시작된 transaction의 번호가 더 높다.

2. Transaction이 종료되어 undo를 변경할 때 몇번 transaction이 무엇으로 데이터를 변경한 것인지 로그를 남긴다.

3. Transaction이 select할 때 자기의 번호보다 낮은 트랜잭션의 undo log만 보고 select한다. 따라서 트랜잭션이 시작하고 끝날 때 까지 select하는 데이터가 바뀌지 않게되는 것이다.

 

참고로 이러한 데이터의 정합성을 위해 스프링에서 코드를 짤 때 select만 수행할 때도 @Transactional을 붙인다. 자세한건 다음시간에!

댓글