[Spring Boot] 데이터베이스 격리수준: Database Isolation Level

[Spring Boot] 데이터베이스 격리수준: Database Isolation Level

트랜잭션

일이 처리되기 위한 가장 작은 단위

트랜잭션들이 모여 하나의 트랜잭션을 이룰 수 있고, 서비스가 된다.

하나의 작업을 수행하기 위해 필요한 데이터베이스의 연산들을 모아놓은 것


DB 격리수준

  • 오라클 기본 격리수준 (Read Committed)

READ COMMIT

T1: 트랜잭션 시작 → Update문 (테이블 내의 정보 수정 - 222.Busan -> 222.Jeju)

T2: T1이  Update하는 동안 그 부분을 Select를 하면 T2는 수정되기 전의 정보를 Select한다.
222.Busan (Undo영역 수정 전)

-------------------------

T1: Update한 것을 Commit  (Undo영역이 수정 됨)

T2: T1이 commit하기 전에는 수정하기 전 정보를 Select하게 되고, Commit 이후에 Select하면 수정 된 정보를 볼 수 있다.
222.Jeju (Undo영역 수정 됨)

READ COMMIT 정합성 문제

T2는 트랜잭션을 실행하지않고, Select만 한다.
Busan이 나오다가 갑자기 Jeju가 나와버렸다.

만약에, T2입장에서 T1과 비슷한 시기에 트랜잭션을 실행하고, Select를 계속 한다.
그리고 마지막에 그 Select한 결과들을 모아 Insert연산을 하고 Commit을 한다면...

중간에 데이터가 변경되어 결과가 예상과 달라져버리면 문제가 된다.

이런 문제는 금전적인 처리에서 주로 발생한다.

총 3번 각각 만원씩 Select를 할려고 했는데, 마지막 Select에서 2만원이 나오면,

예상은 3만원 Insert를 하려고 했는데, 4만원이 Insert - Commit이 되어버린다.

정합성이 깨진다 = 부정합, 똑같은 Select에서 다른 것이 나왔을 때

PHANTOM READ(데이터가 보였다 안보였다), 아에 결과가 없을 때

이를 해결하기 위해 REPEATABLE READ방식을 사용해야한다.


  • MySQL 기본 격리수준 (Repeatable Read 이상) –> 부정합 발생 X

transaction Id부여

T2가 먼저 Transaction 시작(Id=10)
222를 Select하면 Busan

T1이 Transaction 시작 (Id=12)
222.Busan --> Jeju upate
commit

하지만 T2입장에서는 아직 자신의 Transaction이 종료되지 않았기 때문에, 항상 동일한 결과를 보여준다.
그래서 시작했을 때는 Busan이 나왔으므로, 끝까지 Busan이 나온다.

자신의 Transaction Id보다 작은 Undo로그를 보고 select한다.

Spring에서는

CRUD

C(Insert), U(Update), D(Delete) --> commit이 필요하므로
@Transactional 붙인다.

R(Select)는 보통 붙이지 않는데, 정합성을 위해 꼭 @Transactional을 붙여서
트랜잭션을 타게 해준다.
[Spring Boot] 데이터베이스 Update 하는 법

[Spring Boot] 데이터베이스 Update 하는 법

데이터베이스 Update하기

웹 브라우저에서 회원 수정을 하는 경우를 생각하며,

@PutMapping을 이용해서 주소를 만들어줬다. 이때 적은 주소는 @GetMapping의 주소와 동일한데, 스프링부트에서는 알아서 Get, Put을 구별해준다.

첫 번째 방법 (Save함수 사용)

주소에서 id를 받아온다. 이 id는 데이터베이스에 저장 된 id값을 불러오기 위함이다.

그리고 @ReqeustBody를 이용하여 Json 데이터를 요청하여, 이를 JavaObject로 변환한다.

SpringBoot에서는 MessageConverter가 Jackson 라이브러리를 사용하여 자동적으로 변환해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// email, password
@PutMapping("/dummy/user/{id}")
public User upadteUser(@PathVariable int id, @RequestBody User requestUser) { // Json 데이터를 요청 => Java Object(MessageConverter의 Jackson라이브러리로 변환해서 받음

User user = userRepository.findById(id).orElseThrow(() -> {
return new IllegalArgumentException("수정에 실패하였습니다.");
});
user.setPassword(requestUser.getPassword());
user.setEmail(requestUser.getEmail());
// save함수는 id를 전달하지 않으면 insert를 해주고,
// save함수는 id를 전달하면 해당 id에 대한 데이터가 있으면 update를 해주고
// save함수는 id를 전달하면 해당 id에 대한 데이터가 없으면 insert를 한다.

userRepository.save(user);
return null;
}

Select 때와 마찬가지로, 잘못 된 (없는) id값을 받았을 때를 방지하기 위해 IllegalArgumentException을 throw 할 수 있도록 한다.

정상적인 user객체에 수정하고자 했던 데이터(password와 email)을 set으로 수정해준다.

그리고 save함수를 이용하여 update해준다.


Save 함수, save 함수를 사용할 때

id를 전달하지 않으면 insert를 해주고,

id를 전달하고, 해당 id에 대한 데이터가 있으면 update를 해준다.

id를 전달하지만, 해당 id에 대한 데이터가 없으면 insert를 해준다.


두 번째 방법

@Transactional라는 annotaion을 사용해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// email, password
@Transactional
@PutMapping("/dummy/user/{id}")
public User upadteUser(@PathVariable int id, @RequestBody User requestUser) { // Json 데이터를 요청 => Java Object(MessageConverter의 Jackson라이브러리로) 변환해서 받음

User user = userRepository.findById(id).orElseThrow(() -> {
return new IllegalArgumentException("수정에 실패하였습니다.");
});
user.setPassword(requestUser.getPassword());
user.setEmail(requestUser.getEmail());

// 더티체킹
return user;
}

첫 번째 방법과 크게 다른 것은 없지만, @Transactional을 이용하면, Save함수를 사용하지 않아도 된다.

이를 Dirty Checking (더티체킹) 이라고 한다.

updateUser라는 함수가 실행될 때, Transaction이 실행되고, return이 될때 Transaction이 자동으로 종료되며, 자동 commit이 된다.


영속성 컨텍스트와 더티체킹에 대해서 공부해보자!

영속성 컨텍스트

[Spring Boot] 데이터베이스 Select 하는 법과 에러체크

[Spring Boot] 데이터베이스 Select 하는 법과 에러체크

데이터베이스의 데이터를 Select할 때 잘못 된 인수가 들어가면 어떻게 해야할까


데이터베이스 Select하기

User table을 select하기에 앞서,

UserRepository라는 인터페이스 파일을 새로 만들고, 그 UserRepository는 JpaRepository를 상속하고 있다.

1
2
3
// 자동으로 bean등록이 된다. --> @Repository 생략가능
public interface UserRepository extends JpaRepository<User, Integer> {
}

그리고 select 기능을 넣어줄 클래스파일에 DI를 해주고,

User의 객체를 return 받을 수 있게, 메소드를 받들어주고, 주소를 넣어준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class DummyControllerTest {

@Autowired // 의존성주입(DI)
private UserRepository userRepository;

// {id} 주소로 파라미터를 전달 받을 수 있음.
// http://localhost:8000/blog/dummy/user/3
@GetMapping("/dummy/user/{id}")
public User detail(@PathVariable int id) {
User user = userRepository.findById(id)
...
return user;
}

이 때, 만약 id부분에 DB에 없는 값이 들어가면 어떻게 해야할까… 라는 생각이 들 수 있다.

이 경우, user가 null이 되고, 결국 null값을 반환해주므로 프로그램에 문제가 생길 수 있다.

그래서 findById() 는 Optional로 User객체를 감싸서 반환해준다. 이후 우리가 null인지 아닌지 판단해서 사용하면 된다.

findById().orElseGet()

id값이 유효하면 그대로 user를 반환해주면 되지만, 유효하지 않을 경우 orElseGet을 타서 user에 User()빈 객체를 넣어줄 것이다. 이건 그냥 null과 다르다

orElseGet()에 넣을 수 있는 파라미터는 Supplier의 타입(인터페이스)이고, 이 타입의 Generic부분에 ?가 되어있는데 익명 객체를 넣어준다. 그리고 함수 get을 Override 해준다.

(인터페이스는 new할 수 없기 때문에, 익명 클래스를 만들어줘야한다. )

1
2
3
4
5
6
7
User user = userRepository.findById(id).orElseGet(new Supplier<User>() {
// 데이터베이스에 값이 없는 경우 orElseGet을 타서 user에서 User()...빈 객체를 넣어줌,null이 아님
@Override
public User get() {
return new User();
}
});

findById().orElseThrow()

하지만 findByID()에서 만약 id가 null일 경우 IllegalArgumentException 를 throw하라고 적혀있다.

1
2
3
4
5
6
User user = userRepository.findById(id).orElseThrow(new Supplier<IllegalArgumentException>() {
@Override
public IllegalArgumentException get() {
return new IllegalArgumentException("해당 유저는 없습니다. id: " + id);
}
});

or 람다식을 이용해서…

1
2
3
User user = userRepository.findById(id).orElseThrow(()->{
return new IllegalArgumentException("해당 유저는 없습니다. id: " + id);
});

결과

Spring에는 AOP라는 기능이 있다.

만약 Illegal이 있을경우, 에러를 가로채서 에러페이지로 넘어갈 수 있게 만들 수 있을 것이다.

추가

요청: 웹브라우저

user 객체: 자바 오브젝트

@RestController: html파일이 아닌 data를 리턴해주는 controller

이 경우 웹브라우저가 이해할 수 있는 데이터 (Json)으로 변환해줘야하는데, SpringBoot는 MessageConter가 자바 오브젝트가 리턴될 시 자동으로 Jackson라이브러리를 호출해서 user 오브젝트를 Json으로 변환해서 브라우저에게 전달한다.