스프링부트 강좌 59강(블로그 프로젝트) - 스프링작동원리 복습
제일 처음에 톰켓이 시작된다. 시작이 되면 필터들이 올라온다. 여기 필터에는 다양한 필터들이 있다. 권한, 인증, 한글 인코딩 등, 입구에 들어올 때 미리 걸러내야 할 것을 걸러준다. 그리고 필터들이 메모리에 올라오고, 디스패쳐가 메모리에 올라온다. 역할은 사용자들이 주소를 요청할 것이다. / 요청을 할 수도 있고, /admin, /user 등등. 디스패쳐가 이 주소를 확인을 해서 적절한 컨트롤러에게 요청을 해준다. 디스패쳐는 중간에 어떤 주소가 들어오는지 확인하고 주소에 맞는 컨트롤러를 요청해주는 역할을 한다.
컨트롤러 얘들이 메모리에 뜨는데 조금 특이한게,
서비스, JPA repository, 영속성 컨텍트스가 메모리에 뜬다. 얘들은 요청시에마다 메모리에 뜬다.
사용자가 1명이 요청을 하면 컨트롤러, 서비스, JPA repository, 영속성 컨텍트스 이런것들이 하나가 뜬다.
또 다른 사용자가 요청을 하면 기존에 떠있는 애들을 재사용하는 게 아니라, 쓰레드를 하나 추가를 해서 요청이 들어올때마다 쓰레드가 하나씩 만들어지면서 이 4개가 지속적으로 뜨게 되어있다. 하나로 유지되는 게 아니다.
컨트롤러, 서비스 JAP repository, 영속성 컨텍스트도 메모리에 뜨는데 지금 안뜨고, 언제 뜨나면...?
파란색으로 체크한 얘들은 일단 대기를 한다. request 요청이 안왔기 때문이다. 근데 DataSource라는 얘는 딱 1개가 뜬다.
컨트롤러, 서비스, JPA repository, 영속성 컨텍스트는 사용자 요청시 마다 쓰레드가 하나씩 만들어지면서 뜨는 애들.
데이터소스는 톰켓이 시작이 되면 미리 떠있는다. 이것은 데이터베이스와 직접적인 연결이 되어있다.
그리고 viewResolver, 인터셉터도 메모리에 뜬다.
얘들이 어떤 일들을 하는지 시나리오를 만들어서 확인해보자.
만약 사용자가 로그인 요청을 한다고 하자. 사용자가 request 요청을 하는데 당연히 그럼 톰켓이 시작이 되어있어야 한다. 디스패쳐도 마찬가지이다. 어떤 주소를 요청하는지에 따라 분기를 해주기 때문이다.
request: http://localhost:8000/login (POST)
그럼 body에 데이터가 담겨서 들어오는데
Body: username, password
가 날라올 것이다. 이것을 http 바디에 들고 요청이 들어온다. 필터를 거치고 나서 필터에서 필터링 되는 것들은 다 필터링 되고 디스패쳐가 이런 주소가 요청이 들어왔네?..
어떤 컨트롤러를 디스채쳐가 메모리에 띄어주냐면 /login이라는 주소가 있는 얘를 매모리로 띄어줄 것이다. PostMapping으로 가지고 있는 어떤 메서드가 있는 것을 메모리에 띄운다. 그럼 컨트롤러가 username과 password를 받는다.
컨트롤러는 이것을 받는 역할에서 끝난다. 어떤 주소 요청이 왔을 때, 그 주소에 대한 함수를 하나 만들어서 그 함수에 데이터를 받는 역할을 한다. 받고 나서 자기 역할은 끝났으니까 서비스에게 넘긴다.
서비스에게 login 요청을 하도록 한다.
login을 해달라! 요청을 하면 service에서 로그인이라는 서비스가 시작된다. 그럼 서비스에서 로그인이 시작되었으니까
로그인이 하기 위해서는 데이터베이스에 질의를 해야한다. 셀렉트 질의를 할 것이다. 이 유저네임과 패스워드가 있는 사용자가 있나요? 하고 셀렉트 요청을 한다.
그럼 jpa repository에게 요청한다. select 해줘..
그래서 username과 password가 있는 데이터가 있는지 서비스가 jpa repository에게 요청을 위임을 한다.
jpa repository는 자기가 들고있는 함수를 호출을 할 것이다. 예를 들어
Select * from user where username=? and password=?
라고 물어볼 것이다. 셀렉트로 이렇게 요청을 하면 해당 요청에 대한 영속성 컨텍스트가 있는지 확인을 한다. 그럼 이제 셀렉트를 하는데 유저 테이블에서 유저 네임과 유저 패스워드가 일치하는 어떤 얘가 있는지 확인 했으니까 유저라는 오브젝트가 영속성 컨텍스트 안에 있는지 존재하는지 물어본다.
근데 최초요청이기 때문에 영속성 컨텍스트는 비워져 있을 것이다. 그러니까 DataSource에게 디비에 직접 물어봐달라고 요청을 한다.
DataSource가 사용자 요청마다 메모리에 떠있는 게 아니라 딱 한개만 떠있는다. 디비에 질의요청을 한다. 질의요청을 해서 실제 셀렉트를 해봤는데 그 결과가 있으면 거기에 대한 응답을 해줘야 한다.
이때 영속성 컨텍스트에 해당 유저 오브젝트가 만들어진다. 그리고 이렇게 만들어지고 나서 응답을 해줄 것이다. 응답을 repository에게 유저 오브젝트를 준다. 이걸 받아서 다시 service에게 돌려준다.
그럼 서비스에서 user 오브젝트가 널인지 아닌지 체크를 하면 된다. null 이 로그인이 안될 것이고 null이 아니면 이런 유저가 해서 컨트롤러에게 유저 오브젝트가 null 이 아니니까 로그인 처리를 해야 한다고 본다.
그럼 세션에 이 유저 정보를 등록한다. 이런 것들을 서비스에서 다 코드를 만들어놔야 한다.
널이 아니면 세션에 유저 오브젝트를 등록하고,
그 사람에게 로그인을 요청했으니까 인증이 필요한 페이지로 이동할 수 있게 도와줘야 한다. / (main) 페이지로 이동해라고 해서 컨트롤러는 마지막에 페이지로 이동하게 된다.
이때 컨트롤러가 어떤 컨트롤러인지가 중요하다. 만약 rest 컨트롤러이면 데이터를 응답하는 컨트롤러이다.
일반적인 컨트롤러이면 html 페이지를 만들어서 응답하는 컨트롤러이다.
/ 로 이동한다는 것은 페이지를 응답해주는 컨트롤러이다. 이때는 View Resolver 가 작동한다.
View Resolver의 역할은 페이지를 만들어서 응답을 해준다. 이건 언제 작동할까?
컨트롤러가 일반적인 컨트롤러일때 View Resolver가 항상 작동한다.
그럼 컨트롤러에서 마지막에 return 되는 값이 "home"; 가 된다.
View Resolver가 home 이라는 페이지라는 페이지를 찾아서. home 이라는 페이지는 jsp 페이지이다. 이 jsp 페이지를 html로 만들어서 사용자에게 응답을 해준다.
만약 rest 컨트롤러이면 View Resolver 가 작동하지 않는다. 그럼 return 될때 home이라는 것은 home이라는 message 자체를 응답해준다. 요청한 사람에게 ...
인터셉터는 무엇일까?
만약에 요청을 /user/1 (1번 유저의 개인정보를 보고싶은) 을 했는데
함수() {
}
함수가 시작되면 1번 정보의 데이터를 만들어서 그 데이터를 응답해줄 것이다. 근데 모든 사람에게 1번 유저의 개인정보를 유출하면 안된다.
따라서 함수가 실행되기 전에 지금 들어온 이 사용자가 세션이 있는지 먼저 확인을 한다.
유저라는 오브젝트가 1번 유저인지 확인을 한다. 이 함수가 실행되기 전에..! 같은 유저면 정보를 보여주고, 다른 유저는 정보를 보여주면 안된다. 이러한 권한 체크를 인터셉터에서 할 수 있다.
인터셉터는 필터와 다르다.
필터는 애초에 요청이 들어올 때 그 사람을 걸러내는 역할을 한다면 인터셉터는 그 함수가 실행되기 직전에 이 함수가 어떤 개인정보를 주는 함수라고 하면 이 함수가 실행되기 전에 인터셉터가 낚아채서 권한이 있는지 확인해주는 것이다.
함수가 실행되기 전, 뒤에 인터셉터가 낚아채서 잠깐 자기가 해야 하는 일을 한다. 권한처리, 인증처리 등등을 말이다.
<정리>
톰켓이 시작되면 필터와 디스패쳐가 메모리에 뜬다. 그리고 데이터소스와 인터셉터 세션 등이 메모리에 뜬다.
뜬 상태에서 사용자가 리퀘스트 요청을 한다. 컨트롤러가 만들어지면서 (메모리에 뜨면서) 해당 메서드가 실행된다. 컨트롤러는 바디 데이터를 받아서 서비스에게 넘긴다. 이 두가지 데이터로 서비스 니가 알아서 로그인 서비스를 시작해라..
그럼 서비스는 로그인 서비스를 시작한다. 이 사용자가 있는지 없는지를 데이터베이스에서 셀렉트 해야 안다. 따라서 jpa repository에게 던져서 셀렉트 한번 해서 확인해달라고 한다. 그럼 jpa 가 영속성 컨텍스트에게 물어본다. 만약 들고 있으면 있는 오브젝트로 바로 응답받으면 되는데 없으면 데이터소스에 넘겨서
디비에게 질의해서 유저 정보가 있는지 확인해봐달라고 한다. 있으면 유저 정보를 계속 리턴 받아 결국 서비스에서 널인지 아닌지 체크를 한다. 널이면 너 권한 없어~ 로그인 없어라고 응답, 널이 아니면 로그인이 가능한 사람이니까 로그인 처리를 하기 위해서 세션에 등록을 한다. 그 다음에 사용자에게 어떤 페이지로 가게 하는 것은 홈페이지마다의 로직이 있을 것이다. 알아서 짜면 된다.
회원가입 요청 상황
http://localhost:8000/join
body : username, password, addr, email 등등
회원가입은 데이터베이스에 insert , 즉 값을 집어넣는 요청이다.
이 요청이 들어오면 디스패쳐가 컨트롤러를 만들 것이다. 컨트롤러에서 회원가입이라는 메서드를 찾을 것이다.
회원가입() {
}
회원가입 메서드가 이 네개의 바디 데이터를 받아 서비스에게 회원가입을 해달라고 요청을 할 것이다. 요청이 오는 순간부터 트랜잭션이 시작된다. 서비스 단에서 시작된다. 트랜잭션만 시작되는 게 아니라
컨트롤러 단부터 JDBC가 연결이 된다. 쉽게 말하면 데이터베이스에 세션이 만들어진다는 말, 즉 디비와 연결이 된다는 말이다. 디비와 연결이 되었고, 서비스 단으로 넘어가는 순간 트랜잭션이 시작된다. jap 레파지토리에 인서트 해달라고 요청해줄 것이다.
최초 인서트니까 영속성 컨텍스트에는 아무것도 없을 것이다. 그러면 db에 질의할 것이다. insert 해달라고..!
그럼 디비에 실제 유저 정보가 들어갈 것이다. 인서트가 될 것이다. 인서트가 되고 나서 다시 응답, 응답을 할 것이다. 정상적으로 회원가입 했으면 서비스가 끝나면서 컨트롤러로 돌아갈 것이다. 서비스가 끝나는 순간에 트랜잭션이 종료가 된다.
트랜잭션이 종료가 된다는 것은 디비에 인서트한 정보가 원래 실제로는 디비 서버의 메모리에 남겨져 있다.
실제 인서트가 된 것이 아니다. 서비스단에 끝나서 컨트롤러로 넘어가는 순간 트랜잭션이 종료되면서 실제로 커밋 요청이 이루어진다.
그래서 서비스 단에서는 트랜잭션이 시작되고, 서비스가 끝날때 트랜잭션이 종료되어서 트랜잭션을 관리할 수 있다. 스프링 부트가 갖고 있는 일종의 규칙이다.
트랜잭션이 종료될 때 실제 커밋이 되고 이는 영구히 해당 데이터가 저장된다는 뜻이다.
커밋 되기 전에는 내가 저장한 데이터가 메모리에만 남겨있다는 것이다. 실제로 저장이 된게 아니다. 서비스가 끝나기 전에 롤백을 시킬 수도 있기 때문이다.
정상적으로 회원가입 요청이 되었으면 실제 디비에 그 값이 딱 들어갔을 것이다. 그럼 컨트롤러는 데이터를 응답해줄 수 있고 혹은 회원가입이 잘 되었으니까 로그인 페이지로 돌아가게 할 수 있다. 로그인 페이지로 돌아가게 하려면 View Resolver가 호출이 될 것이다. 그래서 로그인 페이지를 만들어서 사용자에게 응답해줄 것이다.
만약 로그인이 잘되었다는 메시지만 응답하고 싶다면 View Resolver는 작동하지 않는다.
회원가입했을 때 로그인 처리를 바로 해주고 싶다면 세션에 유저정보를 등록하면 된다.
이런 것들은 프로그램을 짜기 나름일 것이다.
Q. 그렇다면 서비스는 트랜잭션을 관리하는 일 밖에 하지 않나요?
송금요청 상황
A가 B에게 500만원을 송금하는 요청을 했다고 하자.
컨트롤러
함수에서는
a, b라는 유저정보, 그리고 500만원 송금 이 세가지를 서비스에게 준다.
그럼 서비스가 해야할일은 1개가 아니다.
a 업데이트 요청을 할 것이다. jpa 는 a 오브젝트가 있는지 영속성 컨텍스트에서 확인을 해볼 것이다. 그때 있으면 데이터베이스로 가지 않고 이 오브젝트를 업데이트하고 끝낸다. 그리고 나서 돌아오고 돌아오고 서비스 단에서 끝나는 순간 트랜잭션이 종료된다.
이 a 오브젝트에 업데이트 된 내용이 자동으로 db쪽으로 flush 되어서 commit이 요청된다. 실제로 디비에 반영된다. 만약 영속성 컨텍스트가 없으면 db에 실제로 update 요청이 일어날 것이다. 업데이트 요청으로부터 응답이 오고, 응답이 와서 서비스단에서 정상으로 떨어졌다.
근데 A만 업데이트해서 컨트롤러에 돌려주면 안된다. 500만원을 송금했다는 것은 a의 급액이 천만원이였다면 500만원으로 깎고, b도 업데이트 해야 한다. b라는 유저에 500만원을 플러스 해줘야 하기 때문이다. 여기서 다시 jpa로 가야한다. update 요청을 해야한다.
즉 update 요청을 두번 하는 것이다.
그러면 b오브젝트가 영속성 컨텍스트에 있는지 확인하고 없으면 또 디비에 실제로 인서트 요청을 할 것이고 정상적으로 응답이 되면 다시 jpa repository -> service 에서 b업데이트까지 다 끝났을 것이다.
그러면 2개의 업데이트가 정상적으로 끝났다. 그리고 이때 컨트롤러에 딱 들어오면 자동으로 트랜잭션이 종료되니까 커밋이 된다. 둘다 업데이트 되었으니 정상적으로 성고했다는 뜻..
근데 만약에 a업데이트는 정상적으로 되었는데, b업데이트가 뭔가 문제가 생겨서 비정상적으로 되었다. 그럼 그럴때는 어떻게 해야할까?
서비스 단에서는 a, b모두 정상적으로 끝났을 때 커밋을 해야 한다. 근데 만약 b가 실패했을 경우 이때 롤백처리를 해야 한다. 이런 처리를 서비스 단에서 하면 된다.
서비스라는 얘는 데이터베이스 질의를 한번만 할 수 있지만 조금 큰 서비스는 데이터베이스 요청을 100번, 50번을 할 수 있다.
하나의 서비스라는 것은 모든 요청이 다 정상적으로 끝나야 성공하는 것이다. 그렇기 때문에 서비스단부터 트랜잭션이 시작되는 것이다.
트랜잭션 처리를 서비스 단에서 한다.
서비스는 정밀하게 말하면 하나의 기능을 담당한다. 그리고 이 하나의 기능을 수행하기 위해서는 여러개의 데이터베이스 요청이 있을 수 있다. 따라서 여러가지 데이터베이스 요청을 하나의 패키지로 담고 있는 역할을 한다.
정말 길었던 강의.....
-이 글은 유투버 겟인데어의 스프링 부트 강좌를 바탕으로 정리한 내용입니다.-