- blog v2는 인증 적용할 거임
개념 설명
- 하이버네이트는 Object Relation Mapping 을 해준다
- 하이버네이트는 오브젝트(Object)를 관리하는 기능이 있다.
- A/T - DS - C - R
- CPU 는 먼저 메모리에 가서 데이터를 찾는다. 없으면 하드디스크로 가서 찾는다.
- 하드디스크는 써클에서 조회할 때 x초 , 다음 써클로 이동할 때 x초 가 걸린다. 조회/ 이동 시간을 모두 합치면 맨 끝의 써클까지 이동할 때까지 시간이 많이 걸린다는 걸 알 수 있다.
- 좀 더 가까운 곳에서 데이터를 찾아서 가져오는 것을 “캐싱” 이라고 한다. CPU가 자신의 레지스터를 먼저 조회하는 것도, 우리 프로젝트의 repository가 데이터베이스가 아닌 Persistence Context에서 데이터를 가져오는 것도 다 캐싱이다.
URL 요청이 들어오면 디스패쳐 서블릿이 어노테이션(리플렉션)을 쫙 훑어보고 맞는거 찾아줌


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 엔티티의 관계 설정을 해주자.


H2에 가서 확인해보면 Board_tb에 user_id ( integer )로 생성된 것을 확인할 수 있음

콘솔에서 확인해보아도 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);

++
콘솔에 하이버네이트 쿼리문 실행되는거 이쁘게 보이게 하려면
application.properties 에 spring.jpa.properties.hibernate.format_sql=true 를 추가하자.
그럼 보기 편하게 나온다.

→

데이터 베이스에서 데이터가 추가된 것을 아래처럼 확인할 수 있다.


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

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


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

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

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

일단 지금 처음 조회할 때는 findAll 이 실행되고 List의 0 인덱스의 getId 하면 2가 나온다.

맨 아래에 insert into 한게 인덱스 0 에 들어있는 데이터이니 user_id 가 2 맞다.
user_id는 findAll 이 실행될 때 값이 가져와진다.
그런데 User의 username 이 필요하다. 지금은 user_id 밖에 없을텐데.
getUsername 을 하면 ?
boardList.get(0).getUser().geUername()

Eager 로 설정해두면 → boardRepository.findAll 할 때 select 문이 실행된다.
Lazy 로 설정해두면 → getUser().getUsername() 할 때 select 문이 실행된다.
(user_id 외의 정보가 필요할 때 그때서야 lazy 하게 select 문을 실행시킴)
현재 Board 의 User에 FetchType.LAZY 를 붙여놓았기 때문에 FindAll 매서드를 실행하면
select from user_tb 가 실행되지 않는다. 그냥 user_id 만 가지고 옴


Lazy 로 설정하고 위처럼 해보면 username이 필요할 때 select 문이 실행되는 것을 알 수 있다.
이것이 레이지로딩이다.
(”=========”) 바로 윗줄에서 getUser().getId() 를 하면 user_id 까지는 boardList 조회할때
가지고 왔으니까 select 문 실행안시키고 바로 출력할 수 있는데,
getUsername 하니 그제서야 select from user_tb 쿼리를 실행시킨다. ( → 레이지 로딩 )

Select 문이 1번 실행되었다. 왜 ? 인덱스 0 ,1, 2의 user가 cos로 동일하기 때문!

처음 cos user를 조회하고 나면 하이버네이트가 cos user 객체를 가지고 있게 된다.
그럼 2번째로 getUser 요청을 했을 때 데이터베이스로 가서 select 하지 않고,
더 가까운 곳에 있는 것(지금의 경우에는 Persistence Context에 해당)을 캐싱 하여 보여준다.

인덱스 3까지 조회하니 select 를 2번 한다.
왜? 인덱스 3의 user는 ssar 로, 기존에 조회하지 않아서 캐싱할게 없기 때문이다.
ssar 유저는 이때 처음 조회되는 거니까 데이터베이스에 가서 가져오라고 select 를 실행시킨다.

마찬가지로 각각 다른 유저의 username을 출력하려고 하니 각각 select 문을 실행시킴
각각 1회씩 처음으로 조회.
BoardRepository
쿼리를 작성해보자.

엔포드의 원칙에 따라 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 실행

에러남
bt.* , ut.* 추가해서 쿼리 수정하고 다시 실행했는데 여전히 에러남.
뭐지.
H2가서 쿼리 잘 작성했는지 직접 실행시켜서 확인

조인되어서 데이터 잘 나오는데.
왜 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 를 하니

에러는 안나지만 User 객체의 모든것이 아닌 user_ id 만 조회됨
지금 상세보기에서 User 객체의 username이 필요해서 조인을 사용한건데
User객체의 필드가 없어서 문제.
그래서 쿼리를 아래와 같이 수정함

"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
위처럼 쿼리를 수정하고

→ Test 코드를 실행하면 이렇게 user id, created_at, email, password, username 모두 잘 select 된다 !
[ JPQL 장점 ]
마이그레이션(ex. MySql 쿼리 → MariaDb 쿼리로 바꾸는 것) 할 때 쉬워짐. 자기가 해주니까.
MySql 이랑 오라클이랑 Mssql 다 다르다. 네이티브쿼리 문법.
JPQL 을 쓰면 난 JPQL 로 작성하기만 하고 DB 에 연결할때
각 DB에 맞게 네이티브 쿼리로 변경되어 보내짐.
어떤 데이터베이스를 쓸지 안정해졌을때 JPQL로 작성하면 편안하다.
BoardRepository

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

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;
}
}

→ JPA 가 ORM 안하면 이렇게 Object 타입으로 받아와서 하나씩 하니씩 다 casting 해서 user 객체에 넣어줘야한다. → 매우 귀찮
UserRepositoryTest 생성

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를 찾을 수 없습니다");
}
}

- 이건 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());
}

테스트 코드를 실행시키면
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

이렇게 실행된다.
INNER JOIN을 했기 때문에 select from board_tb 할 때 user 객체의 모든 정보를 조회해서 가지고 옴
어 근데 왜 select from user_tb 를 한번 더 실행하는 거지 ?
앞에 select 한 데이터를 가져다 쓰는거 아닌가?
하고 생각하고 선생님한테 물어봤더니

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 객체를 한번 더 조회해서 가져온다.


실제로 board.getUser() 까지만 해도 user 객체가 조회된다.
→ select * from user_tb 실행됨
[지금까지 정리]
v2 -> 인증
- fk 걸기
- 유저 더미데이터 추가 ( 회원가입 생략을 위해 )
- findAll (Repository) -> 하면 ORM 때문에 board 조회하면 user(fk) 도 조회된다.
→ list 에는 user를 조회할 필요가 없기 때문에 lazy 로 걸어두는게 맞다
→ detail 때문에 User 를 Eager 로 수정하면, list 에서도 select user_tb 가 실행되기
때문에 안됨 (이런거 참을 수 없어)
- 그치만 게시글 상세보기 에서는 화면에 username이 필요한걸 ? → user 가 lazy 로 걸려있기 때문에 한방에 username 가져올 수 없다
- username을 가지고 올 수 있는 방법은 2개 가 있는데
1) view 에서 mustache 로 getter 2번 한다.(?) 이렇게 하면 2번 땡기기 때문에 NoNo
2) 쿼리에서 직접 조인 해서 한번에 가져온다. Good
- findByIdv2 매서드 BoardRepository에 생성해서 해보자 → 동일한 필드에는 별칭 주기 (Board와 User의 Id, createdAt 필드명이 동일하기 때문에
쿼리 작성할 때 각각 별칭 붙이기)
→ Board.class 로 매칭 못함 (별칭이 붙어서 엔티티랑 필드의 이름이 달라졌기 때문에)
→ 그래서 데이터는 가져와 지지만 가져와서 getUser 하면 select from user_tb 가 한번 더 실행됨
(사실 6번 좀 아리까리함)
→ 내가 직접 ORM 해야댐 → Object 로 받아서 각 필드 다 set 해주기.. (아래에서 해 볼 예정)

직접 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 매서드를 만들어서

테스트 코드로 확인해보니
1
1
날짜
내용1
제목1
이 나와서 복사해가지고 repository 에 주석으로 붙여넣고
obs[] 배열에 있는 값을 board 와 user 객체에 넣어주었다.
위의 경우에도
역시 1 이 2번 출력되었는데 board_id 의 1인지
user_id 의 1인지 몰라서 그냥 때려 넣어버렸다.

Test

@Test
public void findByTitle_test() {
//given
String title = "제목1";
//when
Board board = boardRepository.findByTitle(title);
//eye
System.out.println(board.getUser().getId());
}
테스트 코드를 작성해서 실행시키면

이렇게 값이 잘 나온다.
이제 이 나온 값을 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;
}

이렇게 주석으로 넣어주고
아래에 board, user 객체 만들어서 하나씩 넣어준다 ..
→ obs 1, 2, 3, 4 인덱스 에 어떤값이 들어왔는지 모르기 때문에 이렇게 찍어보고 확인해서 하나씩 casting 해서 넣어줘야 한다.
→ 같은 값이 2개 나오면 ( 위의 경우 1이 2개 나왔다 ) 이게 어디껀지 모를 수 있다.
⇒ 그래서 Object Relation Mapping을 알아서 해주는 JPA 가 편한 것이다.
로그인
화면부터 만들기
(실습은 선생님이 만들어둔 화면이 있으니 그거 복붙해서 쓰면 된다.)
(아래의 user.zip)

다운받아서 templates → user 디렉토리 만들어서 거기 넣기

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 코드는 여기 참고

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";
}
}

- 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 바 (회원가입/로그인/로그아웃)추가해주기

userRepository 생성하기 ( 나는 이미 생성함 )

Test 에 UserRepositoryTest 생성하기 (나는 이미 생성함)


save 매서드 생성

테스트 코드를 작성
하이버네이트에 객체만 던져주면 자기가 알아서 Insert 함! 신기

왜 멀티쓰레드를 안쓰는가 요즘 ?
스프링도 아파치톰캣말고 네티?로 바꾸면 단일 스레드로 바뀜
스크립트 - 기생언어
html 은 자기만 있어도 작동하는데
자바스크립트는 html 이 있어야 작동함
html - actor
자바스크립트 - 대본
node.js → jvm(자바런타임환경)같은 자바스크립트 런타임환경
html 없이 자바스크립트가 작동함.

- 컨텍스트 스위칭으로 여러개의 요청이 들어올 때 I/O까지 처리하면
시간이 너무 오래 걸림
- 그래서 논 블럭킹 I/O 를 사용 한다 (아래)


- 프로미스
- NIO, 이벤트 루프
- 단일쓰레드의 장점
지금하는 우리 프로젝트는 멀티쓰레드 → IO에 약하다.
그래서 단일 스레드로 만드는게 좋음 → 그게 노드 JS
파이썬 Jango ?
repository에 날짜 지우고


user 엔티티에 추가


회원가입 시도하면

성공하고 나서 로그인페이지로 이동하고

h2 가서 확인하면 성공적으로 데이터 들어가 있음
스프링은 파싱 전략이 2개
@RequestParam(”username”) String username 이렇게도 받을 수 있고
클래스로도 받을 수 있다 (DTO)
Data transfer object
서버는 request DTO, response DTO 를 가지고 있다.
정보를 과하게 가져오지 말고 화면에 맞는 정보만 가지고 오자.

UserRequest 생성


매서드 추가

컨트롤러에서 변수로 받던거 joinDto로 받도록 수정하자!
joinDto.toEntity 써주기

그럼 UserRepository 도 위와같이 수정할 수 있다.
save의 매개변수로 User user로 받아서 persist만 하면 insert 된다.
==
멀티 쓰레드는 IO 환경에 취약하다
MVC → 다 m이 있으니 → io 해야하니까 → 대기시간이 길어짐
NIO → None 블럭킹 I/O → IO 때문에 BLOCKING 되지 말라고
AJAX 호출할 때
자기할거하고 나중에 다 됐는지 확인하러감 (단일 쓰레드)

동기, 비동기
단일 스레드, 멀티 스레드
자 로그인 해보자
UserRepository

// 로그인
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
테스트컨트롤러가서 로그인 매서드 완성

브라우저 키고
네트워크 클릭 → f5 해서 다시 reload 하기
그리고 localhost:8080/board 로 이동

그럼 네트워크에 이렇게 board 가 뜨는데
board를 클릭하면
응답헤더에서

content type text/html 인 것도 확인할 수 있고 뭐 여러가지를 확인할 수 있다.
* JSESSIONID는 락카를 한번이라도 건드려야 저장되니까 아직없음
(근데 나는 캡쳐하기 전에 로그인을 해서 Set-Cookie 에 JSESSIONID 가 있다 ㅎ)

로그인하면
응답헤더에 set-cookie 에 내 jsessionid가 들어가 있는데,
이걸 브라우저가 aapplication의 쿠키에 jsessionid 로 저장

이후에 요청하면

요청헤더에 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 하면 편리하다고 한다.

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 를 생성해 테스트도 하자!

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