Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- MSSQL
- dm_exec_requests
- ERROR_MESSAGE
- SQL
- 튜닝
- TRANCOUNT
- 재귀멤버
- transferTo
- 프론트컨트롤러
- 아이
- 진경혜
- spring
- 스프링
- renameTo
- 홈스쿨링
- 배치
- XWPF
- 요약
- 요청매핑
- SQLSTATE=42705
- 앵커멤버
- 함수
- 디스패처서블릿
- HWPF
- 자바
- 교육법
- getChannel()
- XACT_STATE
- git
- java
Archives
- Today
- Total
필기노트
김영한 스프링 강의 요약 - JPA, Querydsl 본문
반응형
목차
1. JPA, Querydsl 설정
2. Item - ORM 매핑
3. ItemRepositoryV2 (스프링 데이터 JPA)
4. ItemQueryRepositoryV2 (Querydsl)
5. ItemServiceV2
6. V2Config
7. 예외 변환
1. JPA, Querydsl 설정
1) build.gradle
plugins {
id 'org.springframework.boot' version '2.6.5'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
ext["hibernate.version"] = "5.6.5.Final"
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
//JdbcTemplate 추가
//implementation 'org.springframework.boot:spring-boot-starter-jdbc'
//MyBatis 추가
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
//JPA, 스프링 데이터 JPA 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
//Querydsl 추가
implementation 'com.querydsl:querydsl-jpa'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
//H2 데이터베이스 추가
runtimeOnly 'com.h2database:h2'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//테스트에서 lombok 사용
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}
tasks.named('test') {
useJUnitPlatform()
}
//Querydsl 추가, 자동 생성된 Q클래스 gradle clean으로 제거
clean {
delete file('src/main/generated')
}
2) IntelliJ IDEA - Q타입 생성 확인 방법
- Build -> Build Project 또는 Build -> Rebuild 또는 main(), 또는 테스트를 실행하면 된다.
- src/main/generated 하위에 hello.itemservice.domain.QItem이 생성되어 있어야 한다.
- 참고: Q타입은 컴파일 시점에 자동 생성되므로 버전관리(GIT)에 포함하지 않는 것이 좋다.
3) JPA 로그 설정
application.properties에 다음 설정을 추가하자.
main - application.properties
#JPA log
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.orm.jdbc.bind=TRACE
- org.hibernate.SQL=DEBUG : 하이버네이트가 생성하고 실행하는 SQL을 확인할 수 있다.
- org.hibernate.type.descriptor.sql.BasicBinder=TRACE : SQL에 바인딩 되는 파라미터를 확인 할 수 있다.
2. Item - ORM 매핑
package hello.itemservice.domain;
@Data
@Entity
public class Item {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "item_name", length = 10)
private String itemName;
private Integer price;
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
- @Entity : JPA가 사용하는 객체라는 뜻이다. 이 에노테이션이 있어야 JPA가 인식할 수 있다. 이렇게 @Entity가 붙은 객체를 JPA에서는 엔티티라 한다.
- @Id : 테이블의 PK와 해당 필드를 매핑한다.
- @GeneratedValue(strategy = GenerationType.IDENTITY) : PK 생성 값을 데이터베이스에서 생성하는 IDENTITY 방식을 사용한다. 예) MySQL auto increment
- @Column : 객체의 필드를 테이블의 컬럼과 매핑한다.
- name = "item_name" : 객체는 itemName이지만 테이블의 컬럼은 item_name 이므로 이렇게 매핑 했다.
- length = 10 : JPA의 매핑 정보로 DDL( create table )도 생성할 수 있는데, 그때 컬럼의 길이 값으로 활용된다. ( varchar 10 )
- @Column을 생략할 경우 필드의 이름을 테이블 컬럼 이름으로 사용한다. 참고로 지금처럼 스프링 부트와 통합해서 사용하면 필드 이름을 테이블 컬럼 명으로 변경할 때 객체 필드의 카멜 케이스를 테이블 컬럼의 언 더스코어로 자동으로 변환해준다.
- itemName -> item_name, 따라서 위 예제의 @Column(name="item_name")를 생략해도 된다.
- JPA는 public 또는 protected의 기본 생성자가 필수이다. 기본 생성자를 꼭 넣어주자.
3. ItemRepositoryV2 (스프링 데이터 JPA)
package hello.itemservice.repository.v2;
import hello.itemservice.domain.Item;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ItemRepositoryV2 extends JpaRepository<Item, Long> {
List<Item> findByItemNameLike(String itemName);
List<Item> findByPriceLessThanEqual(Integer price);
}
- ItemRepositoryV2는 JpaRepository를 인터페이스 상속 받아서 스프링 데이터 JPA의 기능을 제공하는 리포지토리가 된다.
- 기본 CRUD는 이 기능을 사용하면 된다.
- JpaRepository 인터페이스만 상속받으면 스프링 데이터 JPA가 프록시 기술을 사용해서 구현 클래스를 만들어준다. 그리고 만든 구현 클래스의 인스턴스를 만들어서 스프링 빈으로 등록한다. 따라서 개발자는 구현 클래스 없이 인터페이스만 만들면 기본 CRUD 기능을 사용할 수 있다.
- 그런데 이름으로 검색하거나, 가격으로 검색하는 기능은 공통으로 제공할 수 있는 기능이 아니다. 따라서 쿼리 메서드 기능을 사용하거나 @Query를 사용해서 직접 쿼리를 실행하면 된다.
- findAll() 코드에는 보이지 않지만 JpaRepository 공통 인터페이스가 제공하는 기능이다. 모든 Item을 조회한다. 다음과 같은 JPQL이 실행된다. select i from Item i
- findByItemNameLike() 이름 조건만 검색했을 때 사용하는 쿼리 메서드이다. 다음과 같은 JPQL이 실행된다. select i from Item i where i.name like ?
- findByPriceLessThanEqual() 가격 조건만 검색했을 때 사용하는 쿼리 메서드이다. 다음과 같은 JPQL이 실행된다. select i from Item i where i.price <= ?
쿼리 메서드 기능
스프링 데이터 JPA는 인터페이스에 메서드만 적어두면, 메서드 이름을 분석해서 쿼리를 자동으로 만들고 실행해주는 기능을 제공한다.
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
- 스프링 데이터 JPA는 메서드 이름을 분석해서 필요한 JPQL을 만들고 실행해준다.
- 물론 JPQL은 JPA가 SQL로 번역해서 실행한다. 물론 그냥 아무 이름이나 사용하는 것은 아니고 다음과 같은 규칙을 따라야 한다.
스프링 데이터 JPA가 제공하는 쿼리 메소드 기능
- 조회: find...By, read...By, query...By, get...By
- 예:) findHelloBy처럼 ...에 식별하기 위한 내용(설명)이 들어가도 된다.
- COUNT: count...By 반환타입 long
- EXISTS: exists...By 반환타입 boolean
- 삭제: delete...By, remove...By 반환타입 long
- DISTINCT: findDistinct, findMemberDistinctBy
- LIMIT: findFirst3, findFirst, findTop, findTop3
4. ItemQueryRepositoryV2 (Querydsl)
package hello.itemservice.repository.v2;
import static hello.itemservice.domain.QItem.item;
@Repository
public class ItemQueryRepositoryV2 {
private final JPAQueryFactory query;
public ItemQueryRepositoryV2(EntityManager em) {
this.query = new JPAQueryFactory(em);
}
public List<Item> findAll(ItemSearchCond cond) {
return query.select(item)
.from(item)
.where(
maxPrice(cond.getMaxPrice()),
likeItemName(cond.getItemName()))
.fetch();
}
private BooleanExpression likeItemName(String itemName) {
if (StringUtils.hasText(itemName)) {
return item.itemName.like("%" + itemName + "%");
}
return null;
}
private BooleanExpression maxPrice(Integer maxPrice) {
if (maxPrice != null) {
return item.price.loe(maxPrice);
}
return null;
}
}
- ItemQueryRepositoryV2는 Querydsl을 사용해서 복잡한 쿼리 문제를 해결한다.
- Querydsl을 사용한 쿼리 문제에 집중되어 있어서, 복잡한 쿼리는 이 부분만 유지보수 하면 되는 장점이 있다.
- Querydsl을 사용하려면 JPAQueryFactory가 필요하다. JPAQueryFactory는 JPA 쿼리인 JPQL을 만들 기 때문에 EntityManager가 필요하다.
- Querydsl에서 where(A,B)에 다양한 조건들을 직접 넣을 수 있는데, 이렇게 넣으면 AND 조건으로 처리된다. 참고로 where()에 null을 입력하면 해당 조건은 무시한다.
- 이 코드의 또 다른 장점은 likeItemName(), maxPrice()를 다른 쿼리를 작성할 때 재사용 할 수 있다는 점 이다. 쉽게 이야기해서 쿼리 조건을 부분적으로 모듈화 할 수 있다. 자바 코드로 개발하기 때문에 얻을 수 있는 큰 장점이다.
5. ItemServiceV2
package hello.itemservice.service;
@Service
@RequiredArgsConstructor
@Transactional
public class ItemServiceV2 implements ItemService {
private final ItemRepositoryV2 itemRepositoryV2;
private final ItemQueryRepositoryV2 itemQueryRepositoryV2;
@Override
public Item save(Item item) {
return itemRepositoryV2.save(item);
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
Item findItem = findById(itemId).orElseThrow();
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
@Override
public Optional<Item> findById(Long id) {
return itemRepositoryV2.findById(id);
}
@Override
public List<Item> findItems(ItemSearchCond cond) {
return itemQueryRepositoryV2.findAll(cond);
}
}
- ItemServiceV2는 ItemRepositoryV2와 ItemQueryRepositoryV2를 의존한다.
- 기본 CRUD와 단순 조회는 스프링 데이터 JPA가 담당하고, 복잡한 조회 쿼리는 Querydsl이 담당하게 된다.
- save() : repository.save(item) 스프링 데이터 JPA가 제공하는 save()를 호출한다. JPA가 만들어서 실행한 SQL을 보면 "insert into item (item_name, price, quantity) values (?, ?, ?)" id에 값이 빠져있는 것을 확인할 수 있다. PK 키 생성 전략을 IDENTITY로 사용했기 때문에 JPA가 이런 쿼리를 만들어서 실행한 것이다. 물론 쿼리 실행 이후에 Item 객체의 id 필드에 데이터베이스가 생성한 PK값이 들어가게 된다. (JPA가 INSERT SQL 실행 이후에 생성된 ID 결과를 받아서 넣어준다)
- update() : 스프링 데이터 JPA가 제공하는 findById() 메서드를 사용해서 엔티티를 찾는다. 그리고 데이터를 수정한다. 이후 트랜잭션이 커밋될 때 변경 내용이 데이터베이스에 반영된다. (JPA가 제공하는 기능이다.)
- findById() : repository.findById(itemId) 스프링 데이터 JPA가 제공하는 findById() 메서드를 사용해서 엔티티를 찾는다.
- @Transactional : JPA의 모든 데이터 변경(등록, 수정, 삭제)은 트랜잭션 안에서 이루어져야 한다. 조회는 트랜잭션이 없어도 가능하다. 변경의 경우 일반적으로 서비스 계층에서 트랜잭션을 시작하기 때문에 문제가 없다. 일반적으로는 비즈니스 로직을 시작하는 서비스 계층에 트랜잭션을 걸어주면 된다.
6. V2Config
package hello.itemservice.config;
@Configuration
@RequiredArgsConstructor
public class V2Config {
private final EntityManager em;
private final ItemRepositoryV2 itemRepositoryV2; //SpringDataJPA
@Bean
public ItemService itemService() {
return new ItemServiceV2(itemRepositoryV2, itemQueryRepository());
}
@Bean
public ItemQueryRepositoryV2 itemQueryRepository() {
return new ItemQueryRepositoryV2(em);
}
}
- SpringDataJpaItemRepository는 스프링 데이터 JPA가 프록시 기술로 만들어주고 스프링 빈으로도 등록해준다.
ItemServiceApplication
@Import(V2Config.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {}
7. 예외 변환
- 리포지토리에 @Repository 애노테이션만 있으면 스프링이 예외 변환을 처리하는 AOP를 만들어준다. 예외 변환 AOP 프록시는 JPA 관련 예외가 발생하면 JPA 예외 변환기를 통해 발생한 예외를 스프링 데이터 접근 예외로 변환한다.
- 스프링 데이터 JPA도 스프링 예외 추상화를 지원한다. 스프링 데이터 JPA가 만들어주는 프록시에서 이미 예외 변환을 처리하기 때문에, @Repository와 관계없이 예외가 변환된다.
- Querydsl은 별도의 스프링 예외 추상화를 지원하지 않는다. 대신에 JPA에서 학습한 것 처럼 @Repository에서 스프링 예외 추상화를 처리해준다.
REFERENCE
반응형
'김영한 강의 요약' 카테고리의 다른 글
김영한 스프링 강의 요약 - 트랜잭션 전파 (0) | 2024.05.04 |
---|---|
김영한 스프링 강의 요약 - 파일 업로드 (1) | 2024.03.16 |
김영한 스프링 강의 요약 - 자바 예외 이해 (0) | 2024.02.21 |
김영한 스프링 강의 요약 - @Transactional 이해 (0) | 2024.02.13 |
김영한 스프링 강의 요약 - 트랜잭션 이해 (0) | 2024.01.31 |
Comments