DB에서 중복 가입자 체크
동일한 회원명으로 가입하니까 PK 위반으로 터졌다. 이렇게 터지면 안되니까 ssar이라는 username이 DB에 있는지 조회해야한다 → 이건 유효성 검사라고 하지않고 비지니스 로직의 일부다 DB에서 검사하는거니까 컨트롤러의 책임이 아니기때문에 유효성 검사라고 하지 않는다
3번 Model에게 위임하기 전에 2번 동일 username 체크부터 해야한다… 동일한 username이 없다는 걸 확인한 후, DB에게 넘겨줘야 하니까!
중복 체크를 try-catch 문으로 묶으면, 중복된 username이 발견되면 예외가 발생하여 catch 블록으로 이동하게 된다. 이를 통해 예외를 처리하고, 중복된 데이터로 인한 예기치 않은 동작을 방지할 수 있지만... 이렇게는 사용하지 않는다!!
중복 체크 시, try-catch 사용 X
1. 오류의 원인이 많아서 구별하기 힘들다. 2. try-catch 문으로 예외를 처리하는 과정에서 서버와의 통신이 이루어지며, I/O가 발생한다
[ I/O 가 이뤄지는 걸 막기 위해서 ]
만약, 서버에서 유효성 검사를 안하고 디비에 보내면 디비에서 TRY-CATCH로 터뜨린 후, 서버에게 보내주겠지. 그럼 서버에서 오류를 잡겠지? → 통신. 즉, I/O를 한다는 말임 이게 내 컴퓨터가 아니라 다른 컴퓨터랑 통신한다고 가정해보자. 통신은 단순히 통신만 하지 않는다. 통신을 하면 insert를 시도하고, 하드디스크에 접근하고 등등 통신에 많은 과정이 벌어진다 느리겠죠? 애초에, 통신을 안하도록 문제를 먼저 잡고 시작했으면 I/O가 없어서 속도가 빠를텐데...
예시를 들어보자. 데이터 딕셔너리 (데이터베이스에서 사용되는 정보들을 저장하고 있는 "도서관") 라는 도서관에 Spring Book이라는 책이 I열 4번에 있다. 나는 이 책을 찾고싶다. 1. Spring Book 이라는 책을 'meta data'로 보고, (=보유하고 있는 책 목록으로 살펴보고) 책이 존재한다는 사실을 바로 알고 통신 없이 터뜨릴 수 있다. 기각한다? = I/O가 일어나지 않았다 = 미리 감지했다 = 빨리 파악 가능 2. Spring Book 이라는 책을 찾기 위해 내가 직접 I열 4번에 방문해서 직접 책이 있는 걸 확인한 후 터뜨린다. (DB에서 터뜨렸다!) = I/O가 일어났다 = 통신이 일어났다 = 미리 감지하지 못했다 = 느려 = 이게 다 돈이다
중복 체크를 위해 DB가 아니라 서버 쪽에서 INSERT 요청을 먼저 처리하는 것! 중복 체크를 위해 클라이언트에서 INSERT 요청을 서버로 보내기 전에 서버 쪽에서 해당 데이터가 중복되는지 미리 확인한다. 이를 위해 서버는 데이터베이스에 SELECT 요청을 보내어 중복 여부를 판단 함!
유니크 제약 조건이 걸려있는 컬럼에 대한 정보는 데이터 딕셔너리에서 찾을 수 있다
try-catch는 터뜨리고 나서 잡는 거니까 안 터뜨리고 잡을 수 있으면 안 터뜨리는게 좋다 (I/O 때문에 )
I/O가 아예 안 일어난다고 생각하면 안된다.
일단 중복 체크를 한다는 말은 DB에 SELECT 요청을 한다는 거니까.
! 터뜨리지 않는다는 점이 중요 ! 하다
왜 중복 체크 시 INSERT 요청을 DB에 보내지 않나
Write (Insert, Update, Delete)요청은 DB에 변형을 주기 때문이다
동시에 3명이 들어와서 /Join요청(insert)를 보내면 서버는 동시에 처리해야하기 때문에 Thread가 생긴다. 메서드는 실행(호출)시에 메모리에 뜨니까… join 메서드를 세명이서 동시에 때리면 c1 c2 c3의 join 스택이 생김 (객체는 1개나 스택은 3개) 이 join 메서드들이 각각 DAO를 동시에 치면... read는 동시에 들어와도 되지만 write가 한 번 끼는 순간 이상한 일이 일어난다 만약, 데이터베이스에 ssar만 있는 상태로 회원가입을 한다고 치자. select는 DB에 동시 접근이 가능하니까, DB에 cos라는 username이 있는지 세 명이 동시에 들어와서 셋 다 검증을 때린다. 현재 "cos" username은 존재하지 않기 때문에, 3명의 클라이언트는 SELECT 요청을 통해 "cos"가 없음을 확인! 이후 3명의 클라이언트가 동시에 "cos"를 INSERT(삽입)하려고 한다면, 프라이머리 키(primary key) 제약으로 인해 충돌이 발생한다. (3명 중 2은 wait가 걸린다) 이러한 상황에서 INSERT 작업에 대해 락(lock)이 걸리게 되고, 무결성과 정확성이 깨지는 것을 방지하기 위해 트랜젝션이 아이솔레이션(isolation), 즉 고립/격리 된다.
무결성은 기본 키(primary key)의 고유성을 보장하는 것을 의미
아이솔레이션(isolation)은 동시에 여러 트랜잭션이 실행될 때,
트랜잭션이 서로에게 영향을 주지 않고 독립적으로 실행되는 것
아이솔레이션(isolation) (고립)
데이터베이스에서의 write 작업은 동시에 여러 사용자가 접근하더라도 고립되어 있어 서로 영향을 주지 않고 작업을 수행 즉, 한 사용자가 데이터베이스에서 write 작업을 수행하는 동안 다른 사용자들은 그 작업이 완료될 때까지 대기 상태(wait)에 있어야 한다. 때문에 내가 select로 검증할 수 있는걸 검증하고, insert를 하는게 좋다… insert부터 때려박음 안 돼. 고립에 걸리니까...
"고립"은 데이터베이스에서 트랜잭션 실행 중에 다른 사용자들이 해당 트랜잭션의 작업이 완료될 때까지 대기하는 상태 "버퍼링"은 데이터를 메모리의 버퍼에 임시로 저장하는 것
유령데이터 (팬텀데이터)
트랜잭션에서 발생할 수 있는 일종의 이슈. 이러한 상황은 "Dirty Read"라고도 불린다. read 했을때, 트랜젝션을 안 걸고 가버리면 write했을때 데이터가 없는 경우가 있다... A와 B라는 두 명의 사용자가 동시에 동일한 계좌에서 금액을 출금하려 한다고 가정하자. A가 계좌의 잔액을 읽은 후, B가 동시에 해당 계좌에서 돈을 출금하고 잔액을 감소시킨다. 이때, A가 트랜잭션을 시작하고 잔액을 변경하기 전에 B의 트랜잭션이 완료되어 이미 잔액이 변경된 상태가 된다. 그러나 A는 아직 읽은 데이터인 변경되기 전의 잔액으로 작업을 수행하게 되므로, 실제 잔액과 일치하지 않는 결과를 얻게 되겠지요... -> 유령데이터
스프링은 트랜젝션을 잡고 있을 수 있다 (+트랜젝션의 원자성)
DB한테 write요청을 하면 다른 애들이 못쓰니까, 트랜젝션이 걸린다는 것까진 이해했죠? 그럼, 트랜젝션은 언제 끝날까? 바로 서버에게 응답하면 트랜젝션이 종료된다! 그러나 스프링은 '아직 트랜젝션이 끝나면 안되는데!' 하고 트랜젝션을 잡고 있을 수 있다. 보통 응답이 안 끊기게 버텨야할 때 그러는데... '내가 write를 여러번 해야할 때', '한 번에 안 끝날 때' (ex.이체) 그런다. 따라서 스프링과 같은 프레임워크는 트랜잭션을 종료하지 않고 유지하며, 응답이 완료된 후에 트랜잭션을 종료하는 방식으로 데이터 일관성과 응답 성능을 모두 유지할 수 있다.
[ 트랜젝션이 안되면… 예시 ]
a가 b한테 1000, 1000 > 0, 2000 으로 이체 요청을 할 것이다 write로 1000원을 b한테 입금 요청 여기서 트랜젝션이 끝나버리면... 동시에 요청한 제 3자가 가로채갈 수 있다 a : 나 천원 보냈어 b : ?? 안왔는데? c : (1000원 가져감) ㅎㅎ 개발자 ㄳ 이런 상황을 방지하기 위해... 데이터베이스는 해당 트랜잭션이 완료될 때까지 다른 사용자가 해당 데이터에 접근하지 못하게 lock을 걸어야한다. 입금 요청에 대한 응답이 온 후에야 B가 실제로 돈을 받았다는 정보를 전달하는 것!
[ 트랜젝션의 원자성 ]
업데이트를 한다는 것은 메모리 상태만 변경되었을 뿐이기 때문에, 해당 변경 사항을 영구적으로 기록하기 위해 커밋(commit)을 해줘야 한다. 만약 서버와 데이터베이스 간의 통신 중에 -1과 같은 문제가 발생한다면, 예를 들어 이체 중에 오류가 발생한다면 전체 작업을 롤백(rollback)해야 한다. 트랜잭션은 메모리에만 기록된 것이기 때문에, 아직 커밋되지 않은 상태
[ 언두 (undo) ]
이전 상태를 기억하고, 실패한 작업을 취소하고 다시 요청하는 것을 의미. 즉, 언두는 롤백
트랜잭션에서의 원자성은 "되면 되고, 안되면 처음부터 다시한다"라는 개념.
즉, 트랜잭션 내의 모든 작업이 한 번에 성공하거나, 작업이 하나라도 실패하면 처음부터 다시 시작하는 것! (트랜잭션의 일관성과 신뢰성을 보장하기 위함)
write 하는 것 자체가 트랜젝션이 만들어진다는 뜻
[ 서비스 레이어 = 트랜젝션을 관리하는 레이어 ] @Transactional
사용자 이름 체크 후 데이터베이스에 사용자 정보를 저장하는 경우, 두 작업을 하나의 트랜잭션으로 묶어서 실행하면, 중간에 오류가 발생해도 데이터의 일관성을 유지하고 롤백할 수 있음. (username 체크를 했을 때, 중복된 이름이 있으면 작업에 실패한거니까 완전히 처음부터 롤백이 가능해서 그러는 듯? 하나의 작업이 실패했으니 다음 작업으로 넘어가지 않고 처음부터 하는거지... 만약 중복된 값이 없으면 다음 로직인 DB연결 후 INSERT까지 쭉 실행되는거고...) 그러니까 이거 2개를 서비스 레이어로 묶어서 빼버려
[ @Transactional를 클래스 레벨에서 붙이면? ]
트랜젝션은 메서드나 클래스 레벨에서 적용할 수 있지만... 클래스 레벨에 @Transactional 어노테이션이 설정되어 있다면, 해당 클래스의 모든 메서드가 트랜잭션 내에서 실행된다. 그래서 모든 메서드 실행 전에 트랜잭션을 시작하고, 메서드가 완료되면 트랜잭션을 종료하는 과정을 거치게 되어서... 시간이 엄청 지연된다
[ @Transactional를 메소드 레벨에서 붙이면? ] - 독립적으로 실행!
특정 메서드에만 트랜잭션을 적용하고 싶다면, 해당 메서드에 @Transactional 어노테이션을 붙여준다. 다른 메서드와의 트랜잭션 연관성이 없고, 한 메서드의 실행 결과가 다른 메서드에 영향을 주지 않아야 할 때, 독립적인 트랜잭션을 사용하여 각각의 메서드를 독립적으로 처리한다 이렇게 독립적인 트랜잭션으로 설정하면, 한 메서드에서 예외가 발생해도 다른 메서드의 트랜잭션에는 영향을 주지 않는다
[ 메소드 만들기 - UserController 클래스 ]
사용자 이름(username)을 기준으로 데이터베이스에서 해당 사용자를 조회하는 코드 requestDTO에 담긴 사용자 이름을 기준으로 데이터베이스에서 해당 사용자를 조회하여 user 변수에 할당하는 역할. 이후에 user 객체를 사용하여 로그인, 회원가입 등의 기능을 처리할 수 있다.
[ UserRepository에서 쿼리문 변경해주자 ]
수업이 너무 빠름. 이 이후는 없음. 본인이 알아서 해볼 것
TIP!
@Transactiional = org. 그거 넣기
애초에 실패하는 트랜젝션을 만들지마라
insert하면 다른 애들이 고립이 걸리니까, (wait걸리니까..)
애초에 실패하는 트랜젝션을 만들지마라!
그니까, read할 때부터 트랜젝션을 걸어버려 → 절대 누가 못오겠지!
write할때 트랜젝션을 걸어버리면 그 찰나에 누가 write를 해버렸을 수도 있음
getsingleResult = 한 건 리턴될 때
getresultList = 여러 건 리턴될 때
* 리퀘스트와 세션은 메모리 관점으로 봐라
* 리퀘스트의 생성 주기는 짧다. 응답하면 끝! stateless
* 세션이 있으면 stateful. 애는 오래가죠. 클라이언트의 상태를 저장하죠
Share article