일단 JPQL로 한번 짜보고, 똑같은 걸 QueryDSL로 짜서 비교해 볼거임.
@Test
public void startJPQL(){
String sql = "select m from Member m where m.username = :username";
Member result = em.createQuery(sql, Member.class)
.setParameter("username", "member1")
.getSingleResult();
assertThat(result.getUsername()).isEqualTo("member1");
}
뭐 그냥 JPQL이다.
select
m1_0.member_id,
m1_0.age,
m1_0.team_id,
m1_0.username
from
member m1_0
where
m1_0.username=?
@Test
public void startQuerydsl(){
JPAQueryFactory query = new JPAQueryFactory(em);
QMember m = QMember.member;
String usernameForFind = "member1";
Member result = query.select(m)
.from(m)
.where(m.username.eq(usernameForFind))
.fetchOne();
assertThat(result.getUsername()).isEqualTo(usernameForFind);
}
QueryDSL이다.
select
m1_0.member_id,
m1_0.age,
m1_0.team_id,
m1_0.username
from
member m1_0
where
m1_0.username=?
QueryDSL은 사용하기 위해서는 JPAQueryFactory라는 것이 필요하다.
이건 내부적으로, 예를 들어 저렇게 fetchOne()하면 쿼리를 보내고 가져오는, 저런 EntityManager를 내부적으로 사용하기 위해 EntityManager를 인자로 받는다.
QMember는 https://qwefdg3.tistory.com/905
이걸 보면 알다 시피 Member 엔티티에 대한 정보를 저장해 놓은 클래스? 라고 보면 된다.
그러니까 일단, QueryDSL에서는 저게 Member라는 엔티티를 지칭한다고 보면 된다.
보면 이제,
뭐 똑같다.
쿼리 팩토리를 시작으로
select m from m where m.username = "member1"
하고 fetchOne()
fetchOne 부분이 em에서 getSingResult 이런거와 동일하다고 볼 수 있다.
그리고 여기서 좀 중요한 부분이,
파라미터 바인딩을 해 주는데,
저게 내부적으로 jdbc의 prepare statement방식,
https://qwefdg3.tistory.com/81
을 사용하기 때문에 sql에서 뭐 "select * from member where member.username = " + param
이런 식으 + 로 문자열을 이어붙이는 게 아니라 바인딩 시키는 거라 인젝션 공격에서 안전하다.
jpa의 setParameter도, QueryDSL의 인자방식도 이 prepare statement 방식을 사용한다.
장점이,
메소드? 그러니까 문자열이 아닌 자바코드로 짜기 때문에 컴파일 시점에서 오류를 다 잡아준다는 점, 뭐 파라미터 바인딩도 그냥 인자로 넣어주면 내부적으로 다 jdbc의 prepareStatement()함수 이용해서 다 해주고,
또 코드 어시스턴스 기능도(emmet) 좋다. 그냥 있지 않을까? 하면 다 자동완성해서 리스트로 보여준다.
그리고, 저거 JPAQueryFactory 필드로 빼도 된다고 한다.
그러니까 이게 생각을 해 보면
Member result = query.select(m)
.from(m)
.where(m.username.eq(usernameForFind))
.fetchOne();
뭐지? JPAQueryFactory에 계속 뭔가를 해 나가는 것이므로, 뭔가 JPAQueryFactory가 상태를 저장하게 되지는 않을까? 할 수도 있는데, 당장 저 select 메소드만 들어가 봐도,
public <T> JPAQuery<T> select(Expression<T> expr) {
return query().select(expr);
}
@Override
public JPAQuery<?> query() {
if (templates != null) {
return new JPAQuery<Void>(entityManager.get(), templates);
} else {
return new JPAQuery<Void>(entityManager.get());
}
}
new로 준다.
보통 저렇게 시작하는 쿼리들이 아마 new로 줄 것이다.
그래서 동시성 문제는 괜찮을 것이다.
JPAQueryFactory 자체가 상태를 저장하고 있지 않을 것이고,
사용될 때 마다 저렇게 query()메소가 실행되고 시작되며 저것만 사용하게 될 것이다.
그리고 EntityManager 내부적으로 동시성 문제도 해결된다고 한다.
이 부분은, 결국 QueryDSL도 QueryDSL > JPA > JdbcTemplate 내부적으로 Jdbc Template를 사용하는데,
Jpa, 즉 EntityManager를 활용하는 Spring Data Jpa, QueryDSL은 모두 트랜잭션 범위에서 진행되어야 하는데,
Jdbc는 트랜잭션의 접근에 대해
https://qwefdg3.tistory.com/569
그러니까,
트랜잭션이 시작되면 트랜잭션 매니저가 그 커넥션을 트랜잭션 동기화 매니저 라는 곳에 보관해 놓는다.
즉, 그 커넥션이 커넥션 풀 중에서 사용중 이라는 상태가 되는 것이다.
저 커넥션이 트랜잭션 동기화 매니저의 쓰레드 로컬이라는 곳에 저장이 되는데, 쓰레드 로컬이란 그 쓰레드만 접근할 수 있는 저장소이다.
그렇기에 동시성 문제에서, 다른 쓰레드가 접근하게 저 커넥션을 사용할 수는 없다.
그 커넥션은 커넥션 풀에서 '사용중'이라는 꼬리표가 붙은 상태이고, 그러므로 다른 쓰레드에서 호출할 때 커넥션풀에서 커넥션을 요청했을 때 그 커넥션은 주어지지 않으며, 또한 쓰레드 로컬이란 해당 쓰레드에서만 접근할 수 있는 저장소에 저장되므로, 다른 쓰레드가 접근할 수 있는 여지가 없다.
그래서 커넥션이 쓰레드 별로 구분이 된다.
저렇게 커넥션에 대한 동시성은 해결이 되었는데,
또 영속성 컨텍스트에 대한, 즉 EntityManager 자체에 대한 동시성 문제가 해결이 되지 않았다.
이 부분은
여기에 대략 나와있다.
보니까 스프링 워크가 의존성을 주입해 줄 때, EntityManager 그 자체가 아니라 프록시로 한번 씌운 EntityManager를 준다는 것이다.
실제로 em.getClass() 해서 출력해 보니까
엔티티 : class jdk.proxy2.$Proxy117
이렇게 프록시로 나왔다.
아마 저 프록시가 영속성 컨텍스트를 관리하고 있을테고, EntityManager에 대한 로직은 저 프록시 내부에 있는 실제 싱글톤 Bean인 EntityManger를 통해 수행하겠지.
'스프링데이터 + JPA > QueryDSL' 카테고리의 다른 글
9. 검색 조건 쿼리 (0) | 2023.11.28 |
---|---|
8. Q타입 (0) | 2023.11.28 |
6. 예제 도메인 모델 (0) | 2023.11.28 |
5. H2 DB 설치 및 설정 (0) | 2023.11.27 |
4. 연관 라이브러리 (0) | 2023.11.27 |