[Springboot] 13 로그인 하기 (blog v2 인증) , ORM 적용, 단일쓰레드와 멀티쓰레드

김호정's avatar
Aug 21, 2024
[Springboot] 13 로그인 하기 (blog v2 인증) , ORM 적용, 단일쓰레드와 멀티쓰레드
 
  • blog v2는 인증 적용할 거임

개념 설명

 
  • 하이버네이트는 Object Relation Mapping 을 해준다
  • 하이버네이트는 오브젝트(Object)를 관리하는 기능이 있다.
  • A/T - DS - C - R
    • URL 요청이 들어오면 디스패쳐 서블릿이 어노테이션(리플렉션)을 쫙 훑어보고 맞는거 찾아줌
      notion image
    • CPU 는 먼저 메모리에 가서 데이터를 찾는다. 없으면 하드디스크로 가서 찾는다.
    • 하드디스크는 써클에서 조회할 때 x초 , 다음 써클로 이동할 때 x초 가 걸린다. 조회/ 이동 시간을 모두 합치면 맨 끝의 써클까지 이동할 때까지 시간이 많이 걸린다는 걸 알 수 있다.
    • 좀 더 가까운 곳에서 데이터를 찾아서 가져오는 것을 “캐싱” 이라고 한다. CPU가 자신의 레지스터를 먼저 조회하는 것도, 우리 프로젝트의 repository가 데이터베이스가 아닌 Persistence Context에서 데이터를 가져오는 것도 다 캐싱이다.
    •  
notion image
Board 객체가 HB(하이버네이트) 안에 5개 생김
fk로 설정한 user를 못넣게되니까 H2(데이터베이스)에 요청해서 select from user_tb를 실행시킴
user객체는 조회되어서 하이버네이트(엄밀히 말하면 Persistence Context)에 저장되고
user1 을 조회한 user1 객체가 거기 저장되는거임.
그래서 다시 user1 의 정보를 요청하게 되면 이미 조회한 user객체는 굳이 데이터베이스로
가서 select 문을 실행시키지 않고 Persistence Context에 있는걸 “캐싱”해서 쓴다!
 
 
우리 더미데이터 기준으로 보면
Board 3 객체부터는 user_tb select 를 한번 더 실행시킨다.
( 현재 Persistence Context 에는 User1 객체 만 들어있으니까!)
 
 
 
 

 

코드 작성

먼저 board 엔티티와 user 엔티티의 관계 설정을 해주자.
Board_tb의 외래키로 User 설정 → 왜 user_id 가 아닌 User 를 설정하는지는 아래에서 설명
Board_tb의 외래키로 User 설정 → 왜 user_id 가 아닌 User 를 설정하는지는 아래에서 설명
 
 
notion image
H2에 가서 확인해보면 Board_tb에 user_id ( integer )로 생성된 것을 확인할 수 있음
 
notion image
콘솔에서 확인해보아도 user_id integer 로 들어감
 
++
회원가입 만들 필요없이 더미 데이터 넣자.
더미 데이터 넣으면 바로 로그인 할 수 있으니까.
 
insert into user_tb(username, password, email, created_at) values ('ssar', '1234', 'ssar@nate.com', now()); insert into user_tb(username, password, email, created_at) values ('cos', '1234', 'cos@nate.com', now()); insert into user_tb(username, password, email, created_at) values ('love', '1234', 'love@nateSELECT * FROM BOARD_TB .com', now()); insert into board_tb(title, content, created_at, user_id) values ('제목1', '내용1', now(), 1); insert into board_tb(title, content, created_at, user_id) values ('제목2', '내용2', now(), 1); insert into board_tb(title, content, created_at, user_id) values ('제목3', '내용3', now(), 2); insert into board_tb(title, content, created_at, user_id) values ('제목4', '내용4', now(), 2); insert into board_tb(title, content, created_at, user_id) values ('제목5', '내용5', now(), 2);
 
data.sql 파일에 더미데이터 추가 ( 기존 board_tb insert 문에 user_id 추가해서 수정도 했음)
data.sql 파일에 더미데이터 추가 ( 기존 board_tb insert 문에 user_id 추가해서 수정도 했음)
 
 
++
💡
콘솔에 하이버네이트 쿼리문 실행되는거 이쁘게 보이게 하려면 application.properties 에 spring.jpa.properties.hibernate.format_sql=true 를 추가하자.
그럼 보기 편하게 나온다.
 
notion image
 
pretty
pretty
 
데이터 베이스에서 데이터가 추가된 것을 아래처럼 확인할 수 있다.
Board_tb 에 데이터가 잘 들어와 있다.
Board_tb 에 데이터가 잘 들어와 있다.
 
User_tb에 데이터가 잘 들어와 있다.
User_tb에 데이터가 잘 들어와 있다.
 

?) 왜 user_id 가 아닌 User를 fk로 설정하는가 ?

 
notion image
 
Board_tb 에 User 객체를 foreign key로 넣어두었다.
만약 board id가 1인 것을 조회해서 데이터를 가져오면 (Select * from board_tb)
user_id = 1인데 이 1이 위의 User 에 저장될 수 있나?
→ 위의 user 는 타입이 Object 이니까 1 ( integer ) 을 넣을 수 없다.
→ user_id (integer )를 찾아와서 어떻게 넣을 수 있을까 ? 타입이 다른데
그래서 하이버네이트가 이때 Select 를 한 번 더 때린다.
→ User 테이블에서 해당 user_id 를 조회해서 1을 찾아냄
Select * from User_tb where user_id = 1 이 실행되는 것
→ 왜 Select 를 한 번 더 하냐? 위에서 Select * from board_tb를 했는데 ?
→ Object relation Mapping을 하려고!
 
→ 한번 select 하면 하이버네이트에 해당 user 객체가 저장되는데 이후에 같은 user 정보를
select 해야 할 때가 오면 DB에 가서 조회하지 않고 캐싱해서 하이버네이트에 있는
해당 user 정보를 가지고 온다.
→ 같은 user 를 두번째로 조회할 때 부터는 캐싱을 하지, new Board 하고나서 select from user_tb 안한다.
→ 위의 data.sql의 데이터를 실행시키면 board_tb에 들어간 user_id 가 2 개 이니까 ( 1, 1, 2, 2, 2 )
select 문이 2번 실행된다
 
  • Object Relational Mapping이란?
 
 

FetchType.LAZY VS. FetcheType.EAGER

 
notion image
 
 
notion image
Lazy 로 설정한 경우
→ user 조회를 안한다! fk로 1(user_id)을 들고 있으니까 user 객체에 id 값만 넣어서 객체생성
→ Board (제목1, 내용1, 시간) & User( 1 ←user_id ) 이렇게 되어있음
→ 나중에 만약에 username이나 email이 필요하다?
→ 그때 select from user_tb 쿼리가 실행됨 ! (Lazy 하다)
Eager 로 설정한 경우
→ user 조회한다! (Select가 실행됨). select 로 모든 정보를 조회해서 user 객체에 넣어둔다.
→ Board (제목1, 내용1, 시간) & User( 1, ssar , 1234, ssar@nate.com, 시간) 이렇게 User
의 모든 데이터를 조회하면서 다가지고 와서 넣어둠
 
정말 그런가 ? 궁금하다
 
하면 테스트 코드로 가서 테스트해보자 !
 
BoardRepositoryTest 로 가서 findAll_test 매서드의 결과값을 확인해보자 !
 

Test

notion image
기존의 finaAll_test 매서드를 실행시켜보면, 지금은 select 가 1번만 실행된다.
 
지금 Board의 데이터는 아래와 같이 들어가 있다.

data.sql

notion image
 
0번 인덱스를 조회하면 제목5 데이터가 나온다.
아래서부터 0, 1, 2, 3, 4 이렇게 순서대로 실행된다.
 
위의 코드는 순수 findAll 이어서 제목5 부터 제목4, 제목3, 제목2, 제목1 이렇게 순서대로 콘솔에 출력된다.
 
 
 

레이지 로딩이란 ?

 
notion image
일단 지금 처음 조회할 때는 findAll 이 실행되고 List의 0 인덱스의 getId 하면 2가 나온다.
 
notion image
맨 아래에 insert into 한게 인덱스 0 에 들어있는 데이터이니 user_id 가 2 맞다.
user_id는 findAll 이 실행될 때 값이 가져와진다.
 
그런데 User의 username 이 필요하다. 지금은 user_id 밖에 없을텐데.
getUsername 을 하면 ?
 
boardList.get(0).getUser().geUername()
 
notion image
 
Eager 로 설정해두면 → boardRepository.findAll 할 때 select 문이 실행된다.
Lazy 로 설정해두면 → getUser().getUsername() 할 때 select 문이 실행된다.
(user_id 외의 정보가 필요할 때 그때서야 lazy 하게 select 문을 실행시킴)
 
현재 Board 의 User에 FetchType.LAZY 를 붙여놓았기 때문에 FindAll 매서드를 실행하면
select from user_tb 가 실행되지 않는다. 그냥 user_id 만 가지고 옴
select from board_tb 만 실행되는 것을 볼 수있다. 그리고 user_id 만 가지고 왔기 때문에 getUser().getId()하면 값이 출력된다.
select from board_tb 만 실행되는 것을 볼 수있다. 그리고 user_id 만 가지고 왔기 때문에 getUser().getId()하면 값이 출력된다.
 
notion image
Lazy 로 설정하고 위처럼 해보면 username이 필요할 때 select 문이 실행되는 것을 알 수 있다.
이것이 레이지로딩이다.
 
(”=========”) 바로 윗줄에서 getUser().getId() 를 하면 user_id 까지는 boardList 조회할때
가지고 왔으니까 select 문 실행안시키고 바로 출력할 수 있는데,
getUsername 하니 그제서야 select from user_tb 쿼리를 실행시킨다. ( → 레이지 로딩 )
 
 
notion image
Select 문이 1번 실행되었다. 왜 ? 인덱스 0 ,1, 2의 user가 cos로 동일하기 때문!
 
인덱스 0, 1, 2의 user_id 가 모두 동일하다
인덱스 0, 1, 2의 user_id 가 모두 동일하다
처음 cos user를 조회하고 나면 하이버네이트가 cos user 객체를 가지고 있게 된다.
그럼 2번째로 getUser 요청을 했을 때 데이터베이스로 가서 select 하지 않고,
더 가까운 곳에 있는 것(지금의 경우에는 Persistence Context에 해당)을 캐싱 하여 보여준다.
 
notion image
인덱스 3까지 조회하니 select 를 2번 한다.
왜? 인덱스 3의 user는 ssar 로, 기존에 조회하지 않아서 캐싱할게 없기 때문이다.
ssar 유저는 이때 처음 조회되는 거니까 데이터베이스에 가서 가져오라고 select 를 실행시킨다.
 
notion image
마찬가지로 각각 다른 유저의 username을 출력하려고 하니 각각 select 문을 실행시킴
각각 1회씩 처음으로 조회.
 

BoardRepository

쿼리를 작성해보자.
 
notion image
 
엔포드의 원칙에 따라 board_tb을 먼저 적는다.
N, fk, driving class
→ 조인할 User 랑 Board 를 놓고 보면 1 : N 의 관계이다. 그러면 board 가 N이 되고, board에 User가 fk 로 들어가 있어야한다. 그리고 board가 driving class가 된다 (엔포드)
→ 그래서 select * from 하고 나서 board_tb 이 먼저와야 한다!!!!
→ 이렇게 안하고 select * from user_tb 로 시작하면 board_tb의 모든 행을 조회하기 때문에 속도가
느려진다.
→ select * from board_tb 하면 fk에 해당하는 user_id를 찾아서 User_tb를 조회할 때,
딱 그 pk만 집어서 그 행만 조회하고 아래행은 더이상 조회하지 않기 때문에 속도가 빠르다.
→ 그러니 이 원칙을 지키자.
 
+
게시글을 적었는데 사람이 없는 경우는 없다 → 사람이 다 조회되어야 하니까 → user랑은 Inner 조인
게시글을 적었는데 댓글이 없는 경우가 있다 → 댓글은 다 조회되지 않아도 되니까
→ reply랑은 Left outer 조인
 
+
조인할때는 on 을 사용
 
여기 쿼리 좀 이상해서 뒤에 수정함. 문법만 보기!
여기 쿼리 좀 이상해서 뒤에 수정함. 문법만 보기!
Board 가 User 객체를 가지고 있기 때문에 (ORM) DTO를 안만들어도 된다.
 
쿼리만들고 test 에서 findById_test 실행
notion image
에러남
 
bt.* , ut.* 추가해서 쿼리 수정하고 다시 실행했는데 여전히 에러남.
 
뭐지.
 
H2가서 쿼리 잘 작성했는지 직접 실행시켜서 확인
 
notion image
조인되어서 데이터 잘 나오는데.
 
왜 repository 에서는 안되지 ?
 
Query query = em.createQuery("select b from Board b join fetch User u on b.user.id = u.id where b.id = :id", Board.class);
 
createQuery 로 바꿔서 JPQL 문법을 사용하는 걸로 수정했다.
 
이렇게 하고 findById_test 를 하니
notion image
에러는 안나지만 User 객체의 모든것이 아닌 user_ id 만 조회됨
 
지금 상세보기에서 User 객체의 username이 필요해서 조인을 사용한건데
User객체의 필드가 없어서 문제.
 
그래서 쿼리를 아래와 같이 수정함
notion image
"select b from Board b join fetch b.user where b.id = :id"
 
[ JPQL ]
 
  • Board (대문자 ) 자바 클래스 명
  • join fetch → inner 조인
  • left join fetch → left outter 조인
select b from Board b join fetch b.user 여기까지가 inner join!
 
JPQL 쓸때는on 절 ( ON 테이블1 어쩌고 = 테이블 2 어쩌고 )이 필요없음
 
select b from Board b join fetch b.user
→ b.user 까지가 inner join
 
위처럼 쿼리를 수정하고
notion image
 
→ Test 코드를 실행하면 이렇게 user id, created_at, email, password, username 모두 잘 select 된다 !
 
 
[ JPQL 장점 ]
 
마이그레이션(ex. MySql 쿼리 → MariaDb 쿼리로 바꾸는 것) 할 때 쉬워짐. 자기가 해주니까.
 
MySql 이랑 오라클이랑 Mssql 다 다르다. 네이티브쿼리 문법.
 
JPQL 을 쓰면 난 JPQL 로 작성하기만 하고 DB 에 연결할때
각 DB에 맞게 네이티브 쿼리로 변경되어 보내짐.
 
어떤 데이터베이스를 쓸지 안정해졌을때 JPQL로 작성하면 편안하다.
 

BoardRepository

 
notion image
 
findById 와 함께 findAll 도 JPQL 쿼리로 수정해주었다
 
! ) JPQL 쿼리쓸 때는 em.createQuery 써야한다. em.createNativeQuery 하면 빨간줄 뜸
 
 
 
게시글 상세보기에선 username이 필요하다.
 
Eager 로 바꾸고,
findbyId_test를 실행하면
 
통신을 2번하면 IO가 2번 일어남 → 맘에 안든다
 
 

 

Object Relation Mapping 이 없다면

 
 
notion image
 

UserRepository 생성

package shop.mtcoding.blog.user; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; import java.sql.Timestamp; @Repository public class UserRepository { @Autowired private EntityManager em; public User findById() { Query query = em.createNativeQuery("select * from user_tb where id = 1"); Object[] obs = (Object[]) query.getSingleResult(); System.out.println(obs[0]); System.out.println(obs[1]); System.out.println(obs[2]); System.out.println(obs[3]); System.out.println(obs[4]); User user = new User(); user.setId((Integer) obs[0]); user.setCreatedAt((Timestamp) obs[1]); user.setEmail((String) obs[2]); user.setPassword((String) obs[3]); user.setUsername((String) obs[4]); return user; } }
notion image
 
→ JPA 가 ORM 안하면 이렇게 Object 타입으로 받아와서 하나씩 하니씩 다 casting 해서 user 객체에 넣어줘야한다. → 매우 귀찮
 

UserRepositoryTest 생성

notion image
 
findById, getUsername 하면 결과값이 잘 나오는 것을 확인할 수 있다.
 
userRepository 에서 user.setUsername ((String) obs [4]); 로 user 객체에 obs[4] 에 있는 값을 username으로 setter 해주었는데 잘 들어간 것을 이렇게 Test 에서 확인.
 
그럼 이제 BoardRepository 에서 테스트해보자.
 

BoardRepository

public Board findByIdV2(int id) { Query query = em.createNativeQuery("select bt.id, bt.title, bt.content, bt.user_id, bt.created_at, ut.id u_id, ut.username, ut.password, ut.email, ut.created_at u_created_at from board_tb bt inner join user_tb ut on bt.user_id = ut.id where bt.id = ?", Board.class); query.setParameter(1, id); try { Board board = (Board) query.getSingleResult(); return board; } catch (Exception e) { e.printStackTrace(); // 익세션을 내가 잡은것 까지 배움 - 처리 방법은 v2에서 배우기 throw new RuntimeException("게시글 id를 찾을 수 없습니다"); } }
notion image
  • 이건 JPA가 ORM 해주는 코드
  • 그리고 여기서 inner join 을 해서 user까지 한꺼번에 불러왔기 때문에
  • 이 매서드 1번만 실행해서 getUser().getUsername() 할 수 있다.
→ Lazy 를 Eager 로 안 바꾸고 Join 이나 2번 select 문 실행 시켜서 user의 username을 get 할 수 있다.
 

Test

@Test public void findByIdV2_test() { int id = 1; Board board = boardRepository.findByIdV2(id); System.out.println(board.getUser().getUsername()); }
notion image
 
테스트 코드를 실행시키면
 
Hibernate: select bt.id, bt.title, bt.content, bt.user_id, bt.created_at, ut.id u_id, ut.username, ut.password, ut.email, ut.created_at u_created_at from board_tb bt inner join user_tb ut on bt.user_id = ut.id where bt.id = ? Hibernate: select u1_0.id, u1_0.created_at, u1_0.email, u1_0.password, u1_0.username from user_tb u1_0 where u1_0.id=? ssar
 
notion image
 
이렇게 실행된다.
INNER JOIN을 했기 때문에 select from board_tb 할 때 user 객체의 모든 정보를 조회해서 가지고 옴
 
어 근데 왜 select from user_tb 를 한번 더 실행하는 거지 ?
앞에 select 한 데이터를 가져다 쓰는거 아닌가?
 
하고 생각하고 선생님한테 물어봤더니
notion image
Repository 에서 쿼리를 이렇게 작성했는데,
 
board 와 user 의 id 와 createdAt 컬럼명이 동일해서 ( 테이블 조인시 컬럼명 동일하면 안됨 X)
user의 id에 u_id , user의 created_at 에 u_created_at 이라고 별칭을 지어줬다.
 
그렇게 되면 Board 에 담길때( Board board = (Board) query.getSingleResult(); 할때)
필드명이 각각 uId, uCreatedAt 이렇게 생성될텐데,
 
Persistance context 에 user 객체가 생성되고 값이 담겼다고 해도
uId, uCreatedAt 에 담겨있으니 getter 로 꺼내서 쓸 수가 없는것!
 
비록 uId에 값이 있다고는 하나 User 엔티티의 필드명이랑 다르게 생성되니
이 user 영속객체(db에서 가져와서 생성한 객체)는 죽은 객체가 된 것이다.
그렇게 되면 board.getUser().getId() 할수가 없는 상태니
getUser()를 실행하면 어찌되었든 User 객체를 한번 더 조회해서 가져온다.
uId, uCreatedAt 을 가진 영속 객체? User는 죽은객체다. 다시 select 가즈아.
uId, uCreatedAt 을 가진 영속 객체? User는 죽은객체다. 다시 select 가즈아.
 
 
notion image
 
실제로 board.getUser() 까지만 해도 user 객체가 조회된다.
→ select * from user_tb 실행됨
 
[지금까지 정리] v2 -> 인증
  1. fk 걸기
  1. 유저 더미데이터 추가 ( 회원가입 생략을 위해 )
  1. findAll (Repository) -> 하면 ORM 때문에 board 조회하면 user(fk) 도 조회된다.
    1. → list 에는 user를 조회할 필요가 없기 때문에 lazy 로 걸어두는게 맞다
      → detail 때문에 User 를 Eager 로 수정하면, list 에서도 select user_tb 가 실행되기
      때문에 안됨 (이런거 참을 수 없어)
  1. 그치만 게시글 상세보기 에서는 화면에 username이 필요한걸 ? → user 가 lazy 로 걸려있기 때문에 한방에 username 가져올 수 없다
  1. username을 가지고 올 수 있는 방법은 2개 가 있는데
    1. 1) view 에서 mustache 로 getter 2번 한다.(?) 이렇게 하면 2번 땡기기 때문에 NoNo
      2) 쿼리에서 직접 조인 해서 한번에 가져온다. Good
  1. findByIdv2 매서드 BoardRepository에 생성해서 해보자 → 동일한 필드에는 별칭 주기 (Board와 User의 Id, createdAt 필드명이 동일하기 때문에
    1. 쿼리 작성할 때 각각 별칭 붙이기) → Board.class 로 매칭 못함 (별칭이 붙어서 엔티티랑 필드의 이름이 달라졌기 때문에)
      → 그래서 데이터는 가져와 지지만 가져와서 getUser 하면 select from user_tb 가 한번 더 실행됨
      (사실 6번 좀 아리까리함)
      → 내가 직접 ORM 해야댐 → Object 로 받아서 각 필드 다 set 해주기.. (아래에서 해 볼 예정)
       
      findByIdV2 별칭 적어서 실행시키니 getUser 할 때 select 문이 한번 더 실행됨. 왜 그런지는 아래에서 설명
      findByIdV2 별칭 적어서 실행시키니 getUser 할 때 select 문이 한번 더 실행됨. 왜 그런지는 아래에서 설명
       
직접 ORM을 해보자. 얼마나 귀찮은지.
 

BoardRepository

public Board findByIdV2(int id) { Query query = em.createNativeQuery("select bt.id, bt.title, bt.content, bt.user_id, bt.created_at, ut.id u_id, ut.username, ut.password, ut.email, ut.created_at u_created_at from board_tb bt inner join user_tb ut on bt.user_id = ut.id where bt.id = ?"); query.setParameter(1, id); Object[] obs = (Object[]) query.getSingleResult(); System.out.println(obs[0]); System.out.println(obs[1]); System.out.println(obs[2]); System.out.println(obs[3]); System.out.println(obs[4]); System.out.println(obs[5]); System.out.println(obs[6]); System.out.println(obs[7]); System.out.println(obs[8]); System.out.println(obs[9]); return null; }
 
 
 
public Board findByIdV2(int id) { Query query = em.createNativeQuery("select bt.id, bt.title, bt.content, bt.user_id, bt.created_at, ut.id u_id, ut.username, ut.password, ut.email, ut.created_at u_created_at from board_tb bt inner join user_tb ut on bt.user_id = ut.id where bt.id = ?"); query.setParameter(1, id); Object[] obs = (Object[]) query.getSingleResult(); // 1 // 제목1 // 내용1 // 1 // 2024-08-21 12:49:35.197432 // 1 // ssar // 1234 // ssar@nate.com // 2024-08-21 12:49:35.194432 Board board = new Board(); board.setId((Integer) obs[0]); //board.setTitle(); System.out.println(obs[0]); System.out.println(obs[1]); System.out.println(obs[2]); System.out.println(obs[3]); System.out.println(obs[4]); System.out.println(obs[5]); System.out.println(obs[6]); System.out.println(obs[7]); System.out.println(obs[8]); System.out.println(obs[9]); return null; }
 
 
 
public Board findByIdV2(int id) { Query query = em.createNativeQuery("select bt.id, bt.title, bt.content, bt.user_id, bt.created_at, ut.id u_id, ut.username, ut.password, ut.email, ut.created_at u_created_at from board_tb bt inner join user_tb ut on bt.user_id = ut.id where bt.id = ?"); query.setParameter(1, id); Object[] obs = (Object[]) query.getSingleResult(); System.out.println(obs[0]); System.out.println(obs[1]); System.out.println(obs[2]); System.out.println(obs[3]); System.out.println(obs[4]); System.out.println(obs[5]); System.out.println(obs[6]); System.out.println(obs[7]); System.out.println(obs[8]); System.out.println(obs[9]); // 1 // 제목1 // 내용1 // 1 // 2024-08-21 12:49:35.197432 // 1 // ssar // 1234 // ssar@nate.com // 2024-08-21 12:49:35.194432 Board board = new Board(); User user = new User(); board.setId((Integer) obs[0]); board.setTitle((String) obs[1]); board.setContent((String) obs[2]); board.setCreatedAt((Timestamp) obs[4]); user.setId((Integer) obs[3]); user.setUsername((String) obs[6]); user.setPassword((String) obs[7]); user.setEmail((String) obs[8]); user.setCreatedAt((Timestamp) obs[9]); board.setUser(user); return board; }
 
 
직접 ORM 하는 코드인데 선생님이 공유해준거.
 
단점 : 코드가 매우 길어진다, 그리고 1 이 여러 값이 나왔는데 그게 board_id 의 1 인지
user_id 의 1인지 알 수 없다. 여기서 코드를 더 추가해서 어디서 온건지 알아낼 수는 있지만
암튼 매우 번거롭고 복잡해 진다.
 
위의 코드 한번 해보기..
 
나는 아래에 findByTitle 매서드로 ORM 을 직접 해보았다.
public void findByTitle(String title) { Query query = em.createNativeQuery("select * from board_tb where title = ?"); query.setParameter(1, title); Object[] obs = (Object[]) query.getSingleResult(); System.out.println(obs[0]); System.out.println(obs[1]); System.out.println(obs[2]); System.out.println(obs[3]); System.out.println(obs[4]); }
 
findByTtile 매서드를 만들어서
notion image
테스트 코드로 확인해보니
1
1
날짜
내용1
제목1
 
이 나와서 복사해가지고 repository 에 주석으로 붙여넣고
obs[] 배열에 있는 값을 board 와 user 객체에 넣어주었다.
 
위의 경우에도
 
역시 1 이 2번 출력되었는데 board_id 의 1인지
user_id 의 1인지 몰라서 그냥 때려 넣어버렸다.
notion image
 
 
 

Test

notion image
@Test public void findByTitle_test() { //given String title = "제목1"; //when Board board = boardRepository.findByTitle(title); //eye System.out.println(board.getUser().getId()); }
 
테스트 코드를 작성해서 실행시키면
notion image
 
이렇게 값이 잘 나온다.
 
이제 이 나온 값을 repository 로 들고가서
 
public Board findByTitle(String title) { Query query = em.createNativeQuery("select * from board_tb where title = ?"); query.setParameter(1, title); Object[] obs = (Object[]) query.getSingleResult(); System.out.println(obs[0]); System.out.println(obs[1]); System.out.println(obs[2]); System.out.println(obs[3]); System.out.println(obs[4]); /* 1 1 2024-08-21 14:21:41.34073 내용1 제목1 */ Board board = new Board(); User user = new User(); board.setId((Integer) obs[1]); board.setCreatedAt((Timestamp) obs[2]); board.setContent((String) obs[3]); board.setTitle((String) obs[4]); user.setId((Integer) obs[0]); board.setUser(user); return board; }
 
notion image
 
이렇게 주석으로 넣어주고
 
아래에 board, user 객체 만들어서 하나씩 넣어준다 ..
→ obs 1, 2, 3, 4 인덱스 에 어떤값이 들어왔는지 모르기 때문에 이렇게 찍어보고 확인해서 하나씩 casting 해서 넣어줘야 한다.
→ 같은 값이 2개 나오면 ( 위의 경우 1이 2개 나왔다 ) 이게 어디껀지 모를 수 있다.
 
⇒ 그래서 Object Relation Mapping을 알아서 해주는 JPA 가 편한 것이다.
 

 

로그인

화면부터 만들기
(실습은 선생님이 만들어둔 화면이 있으니 그거 복붙해서 쓰면 된다.)
(아래의 user.zip)
 
 
notion image
 
다운받아서 templates → user 디렉토리 만들어서 거기 넣기
 
notion image

JoinForm

{{> layout/header}} <div class="container p-5"> <!-- 요청을 하면 localhost:8080/join POST로 요청됨 username=사용자입력값&password=사용자값&email=사용자입력값 --> <div class="card"> <div class="card-header"><b>회원가입을 해주세요</b></div> <div class="card-body"> <form action="/join" method="post" enctype="application/x-www-form-urlencoded"> <div class="mb-3"> <input type="text" class="form-control" placeholder="Enter username" name="username"> </div> <div class="mb-3"> <input type="password" class="form-control" placeholder="Enter password" name="password"> </div> <div class="mb-3"> <input type="email" class="form-control" placeholder="Enter email" name="email"> </div> <button type="submit" class="btn btn-primary form-control">회원가입</button> </form> </div> </div> </div> {{> layout/footer}}
 

loginForm

{{> layout/header}} <div class="container p-5"> <!-- 요청을 하면 localhost:8080/login POST로 요청됨 username=사용자입력값&password=사용자값 --> <div class="card"> <div class="card-header"><b>로그인을 해주세요</b></div> <div class="card-body"> <form action="/login" method="post" enctype="application/x-www-form-urlencoded"> <div class="mb-3"> <input type="text" class="form-control" placeholder="Enter username" name="username"> </div> <div class="mb-3"> <input type="password" class="form-control" placeholder="Enter password" name="password"> </div> <button type="submit" class="btn btn-primary form-control">로그인</button> </form> </div> </div> </div> {{> layout/footer}}
 
++
V2 코드는 여기 참고
notion image
 

UserController 생성

package shop.mtcoding.blog.user; import jakarta.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @Controller public class UserController { @Autowired private UserRepository userRepository; @Autowired private HttpSession session; @PostMapping("/login") public String login(UserRequest.loginDTO loginDTO) { User sessionUser = userRepository.findByUsernameAndPassword(loginDTO.getUsername(), loginDTO.getPassword()); // request 에서 session 접근해서 값 꺼내와도 되지만 // http session 은 ioc 에 저장되어있으니 autowired 해서 꺼내쓰기 session.setAttribute("sessionUser", sessionUser); // 헬스장에 로그인하면서 session 락카에 보관. 여기서 session에 값이 저장됨 // 락카에는 객체를 다 넣을 필요가 없다. 그 유저의 pk만 넣어도 ㄱㅊ. 그 정보만 가지고 나중에 조회할 수 있기때문! return "redirect:/board"; } @PostMapping("/join") public String join(UserRequest.joinDTO joinDTO) { userRepository.save(joinDTO.toEntity()); // return "redirect:/login-form"; } @GetMapping("/join-form") public String joinForm() { return "user/join-form"; } @GetMapping("/login-form") public String loginForm() { return "user/login-form"; } }
notion image
  • java → user 패키지 → UserController 생성
  • 두 매서드 만들어서 회원가입, 로그인 화면 뜨는지 확인
 
@GetMapping(”/join-form”)
주소에 왜 user 이런거 안들어감 ?
 
/user/join-form 이 아니라 왜 /join-form 이지 ?
 
⇒ 인증하기 전의 주소에는 /user 이런거 쓰지말기!
/user/* → 인증이 필요해
/board/* → 인증이 필요해
 
그래서
 
/login-form
/login
/join-form
/join
 
위처럼 인증을 위한 주소 앞에는 엔티티명을 붙이지 않는다.
 
++
아래처럼 주소설계를 시작부터 잘못했으면
 
/user/login-form
/user/join-form
 
그 앞에다가 /api 붙여서 수습할 수는 있음 ^^;
/api/user/login-form
/api/user/join-form
 

 
header.mustache 가서 nav 바 (회원가입/로그인/로그아웃)추가해주기
notion image
 
 
userRepository 생성하기 ( 나는 이미 생성함 )
notion image
 
 
Test 에 UserRepositoryTest 생성하기 (나는 이미 생성함)
 
notion image
 
 
 
notion image
save 매서드 생성
 
 
notion image
테스트 코드를 작성
 
하이버네이트에 객체만 던져주면 자기가 알아서 Insert 함! 신기
 
 
 
notion image
왜 멀티쓰레드를 안쓰는가 요즘 ?
스프링도 아파치톰캣말고 네티?로 바꾸면 단일 스레드로 바뀜
 
스크립트 - 기생언어
html 은 자기만 있어도 작동하는데
자바스크립트는 html 이 있어야 작동함
 
html - actor
자바스크립트 - 대본
 
node.js → jvm(자바런타임환경)같은 자바스크립트 런타임환경
html 없이 자바스크립트가 작동함.
notion image
 
  • 컨텍스트 스위칭으로 여러개의 요청이 들어올 때 I/O까지 처리하면
시간이 너무 오래 걸림
  • 그래서 논 블럭킹 I/O 를 사용 한다 (아래)
notion image
notion image
  • 프로미스
  • NIO, 이벤트 루프
  • 단일쓰레드의 장점
 
지금하는 우리 프로젝트는 멀티쓰레드 → IO에 약하다.
그래서 단일 스레드로 만드는게 좋음 → 그게 노드 JS
 
파이썬 Jango ?
 
 
 
 
repository에 날짜 지우고
notion image
notion image
user 엔티티에 추가
 
 
 
notion image
 
notion image
회원가입 시도하면
 
 
notion image
성공하고 나서 로그인페이지로 이동하고
 
notion image
h2 가서 확인하면 성공적으로 데이터 들어가 있음
 
 
스프링은 파싱 전략이 2개
@RequestParam(”username”) String username 이렇게도 받을 수 있고
클래스로도 받을 수 있다 (DTO)
Data transfer object
 
서버는 request DTO, response DTO 를 가지고 있다.
정보를 과하게 가져오지 말고 화면에 맞는 정보만 가지고 오자.
 
notion image
 
UserRequest 생성
 
notion image
 
notion image
매서드 추가
 
 
notion image
 
 
컨트롤러에서 변수로 받던거 joinDto로 받도록 수정하자!
joinDto.toEntity 써주기
 
notion image
 
 
그럼 UserRepository 도 위와같이 수정할 수 있다.
save의 매개변수로 User user로 받아서 persist만 하면 insert 된다.
 
==
 
 
멀티 쓰레드는 IO 환경에 취약하다
MVC → 다 m이 있으니 → io 해야하니까 → 대기시간이 길어짐
 
NIO → None 블럭킹 I/O → IO 때문에 BLOCKING 되지 말라고
 
 
 
AJAX 호출할 때
자기할거하고 나중에 다 됐는지 확인하러감 (단일 쓰레드)
 
 
 
notion image
 
 
 

 
동기, 비동기
 
단일 스레드, 멀티 스레드

자 로그인 해보자

 
 

UserRepository

notion image
 
// 로그인 public User findByUsernameAndPassword(String username, String password) { // 조회 쿼리 Query query = em.createQuery("select u from User u where u.username=:username and u.password=:password", User.class); query.setParameter("username", username); query.setParameter("password", password); // 연관된 엔티티가 없으니까 object mapping (연관된 엔티티가 있으면 object relation mapping 을 함) // 배열로 받지않게 User.class 추가 User user = (User) query.getSingleResult(); return user; }
 
  • 로그인 매서드 findByUsernameAndPassword 만들기
 
userRepoitoryTest 가서 findByUsernameAndPassword_test 테스트
 
컨트롤러가서 로그인 매서드 완성
 
notion image
브라우저 키고
 
네트워크 클릭 → f5 해서 다시 reload 하기
 
그리고 localhost:8080/board 로 이동
 
notion image
그럼 네트워크에 이렇게 board 가 뜨는데
 
board를 클릭하면
응답헤더에서
notion image
content type text/html 인 것도 확인할 수 있고 뭐 여러가지를 확인할 수 있다.
 
* JSESSIONID는 락카를 한번이라도 건드려야 저장되니까 아직없음
(근데 나는 캡쳐하기 전에 로그인을 해서 Set-Cookie 에 JSESSIONID 가 있다 ㅎ)
 
notion image
 
로그인하면
응답헤더에 set-cookie 에 내 jsessionid가 들어가 있는데,
이걸 브라우저가 aapplication의 쿠키에 jsessionid 로 저장
notion image
 
이후에 요청하면
notion image
요청헤더에 cookie 에 jsessionid 를 넣고 요청함.
 
→ 이게 브라우저의 프로토콜! 쿠키에 저장! 다시 요청할 때 헤더에 가지고 감!
 

UserRepository

package shop.mtcoding.blog.user; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import java.sql.Timestamp; @Repository public class UserRepository { @Autowired private EntityManager em; // 로그인 public User findByUsernameAndPassword(String username, String password) { // 조회 쿼리 Query query = em.createQuery("select u from User u where u.username=:username and u.password=:password", User.class); query.setParameter("username", username); query.setParameter("password", password); // 연관된 엔티티가 없으니까 object mapping (연관된 엔티티가 있으면 object relation mapping 을 함) // 배열로 받지않게 User.class 추가 User user = (User) query.getSingleResult(); return user; } // repository 에는 기능명을 넣지말자 // join 이라고 하지말고 save 로 짓기 // 회원가입 @Transactional public void save(User user) { // 1. 비영속 user System.out.println("담기기전 : " + user.getId()); // 이때 id 는 null /* // 날짜 Timestamp now = Timestamp.valueOf(LocalDateTime.now()); User user = new User(); //pk 없이 user 객체 생성 user.setUsername(username); user.setPassword(password); user.setEmail(email); */ em.persist(user); // insert 쿼리가 날아감. id 가 없으니까 insert // persist 로 em에 담기면 영속 user 가 됨!!!! 담기면 insert 된다!! 동기화 된다고!!! System.out.println("담긴 후 : " + user.getId()); // user 가 영속객체가 되었음. getId 하면 id가 나옴 } // ORM 연습 public User findById() { Query query = em.createNativeQuery("select * from user_tb where id = 1"); Object[] obs = (Object[]) query.getSingleResult(); System.out.println(obs[0]); System.out.println(obs[1]); System.out.println(obs[2]); System.out.println(obs[3]); System.out.println(obs[4]); User user = new User(); user.setId((Integer) obs[0]); user.setCreatedAt((Timestamp) obs[1]); user.setEmail((String) obs[2]); user.setPassword((String) obs[3]); user.setUsername((String) obs[4]); return user; } }
 

UserController

package shop.mtcoding.blog.user; import jakarta.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @Controller public class UserController { @Autowired private UserRepository userRepository; @Autowired private HttpSession session; @PostMapping("/login") public String login(UserRequest.loginDTO loginDTO) { User sessionUser = userRepository.findByUsernameAndPassword(loginDTO.getUsername(), loginDTO.getPassword()); // request 에서 session 접근해서 값 꺼내와도 되지만 // http session 은 ioc 에 저장되어있으니 autowired 해서 꺼내쓰기 session.setAttribute("sessionUser", sessionUser); // 헬스장에 로그인하면서 session 락카에 보관. 여기서 session에 값이 저장됨 // 락카에는 객체를 다 넣을 필요가 없다. 그 유저의 pk만 넣어도 ㄱㅊ. 그 정보만 가지고 나중에 조회할 수 있기때문! return "redirect:/board"; } @PostMapping("/join") public String join(UserRequest.joinDTO joinDTO) { userRepository.save(joinDTO.toEntity()); // return "redirect:/login-form"; } @GetMapping("/join-form") public String joinForm() { return "user/join-form"; } @GetMapping("/login-form") public String loginForm() { return "user/login-form"; } }
 
아래의 UserRequest 는
DTO를 내부클래스로 관리하기 위해서 만든 것이다.
 
오늘 joinDto랑 loginDto를 만들었는데 (static 으로 만들어야 함)
joinDto의 toEntity 매서드는 DTO를 UserObject( User Entity ) 로 바꿔주는 역할을 한다.
insert 하는 건 toEntity로 만들어서 insert 하면 편리하다고 한다.
 
notion image
package shop.mtcoding.blog.user; import lombok.Data; // 내부클래스로 관리!! // 이렇게 하면 위험하지 않음 public class UserRequest { @Data // getter, setter, tostring 들고 있음 public static class joinDTO { // static 이니까 new 하기전에 이 클래스는 static 에 뜬다. private String username; private String password; private String email; // 매서드 만들기 // 이 매서드는 DTO -> UserObject (유저 엔티티) 로 바꾸는 역할을 함!! public User toEntity() { // insert 하는건 toEntity로 만들어서 넣으면 완전 편리하고 코드가 심플해짐!! // 아래에서 유저객체 만들어서 return 함! return User.builder().username(username).password(password).email(email).build(); } } // 요청 바디 데이터 받기 위해 dto 생성함!!! (dto 만드는 목적) // 이렇게 만들어두면 관리하기 편하다. // 나중에 사용하기도 편함 @Data // getter, setter, tostring 들고 있음 public static class loginDTO { // static 이니까 new 하기전에 이 클래스는 static 에 뜬다. private String username; private String password; } }
 
위의 UserController 에서 사용한 것을 볼 수있다.
 
++
UserRepositoryTest 를 생성해 테스트도 하자!
notion image

UserRepositoryTest

package shop.mtcoding.blog.user; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; @DataJpaTest // h2, em @Import(UserRepository.class) public class UserRepositoryTest { @Autowired private UserRepository userRepository; @Test public void findByUsernameAndPassword_test() { String username = "love"; String password = "1234"; User user = userRepository.findByUsernameAndPassword(username, password); System.out.println(user.getUsername()); System.out.println(user.getUsername()); } @Test public void save_test() { String username = "haha"; String password = "1234"; String email = "haha@nate.com"; //userRepository.save(username, password, email); } @Test public void findById_test() { //given //when User user = userRepository.findById(); //eye System.out.println(user.getUsername()); } }
 
 
Share article

keepgoing