스프링 DB 2편 - 데이터 접근 활용 기술 (6) - 데이터 접근 기술 - JPA
인프런 스프링 DB 2편 - 데이터 접근 활용 기술편을 학습하고 정리한 내용 입니다.
JPA 시작
스프링과 JPA는 자바 엔터프라이즈(기업) 시장의 주력 기술이다.
스프링이 DI 컨테이너를 포함한 애플리케이션 전반의 다양한 기능을 제공한다면, JPA는 ORM 데이터 접근기술을 제공한다.
JPA는 스프링 만큼이나 방대하고, 학습해야할 분량도 많다. 하지만 한번 배워두면 데이터 접근 기술에서 매우 큰 생산성 향상을 얻을 수 있다. 대표적으로 JdbcTemplate이나 MyBatis 같은 SQL 매퍼 기술은 SQL을 개발자가 직접 작성해야 하지만, JPA를 사용하면 SQL도 JPA가 대신 작성하고 처리해준다.
실무에서 JPA를 더욱 편리하게 사용하기 위해 스프링 데이터 JPA와 Querydsl이라는 기술을 함께 사용한다.
중요한 것은 JPA이다. 스프링 데이터 JPA, Querydsl은 JPA를 편리하게 사용하도록 도와주는 도구다.
JPA 설정
spring-boot-starter-data-jpa 라이브러리를 사용하면 JPA와 스프링 데이터 JPA를 스프링 부트와 통합하고, 설정도 아주 간단히 할 수 있다.
build.gradle에 다음 의존 관계를 추가한다.
1
2
3
4
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
//JdbcTemplate 추가
//implementation 'org.springframework.boot:spring-boot-starter-jdbc'
spring-boot-starter-data-jpa는 spring-boot-starter-jdbc도 함께 포함(의존)한다. 따라서 해당 라이브러리 의존관계를 제거해도 된다. 참고로 mybatis-spring-boot-starter 도 spring-boot-starter-jdbc 를 포함하기 때문에 제거해도 된다.
hibernate-core: JPA 구현체인 하이버네이트 라이브러리jakarta.persistence-api: JPA 인터페이스spring-data-jpa: 스프링 데이터 JPA 라이브러리
application.properties에 다음 설정을 추가하자.
1
2
3
#JPA log
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
org.hibernate.SQL=DEBUG: 하이버네이트가 생성하고 실행하는 SQL을 확인할 수 있다.org.hibernate.type.descriptor.sql.BasicBinder=TRACE: SQL에 바인딩 되는 파라미터를 확인할 수 있다.spring.jpa.show-sql=true: 참고로 이런 설정도 있다. 이전 설정은logger를 통해서 SQL이 출력된다. 이 설정은System.out콘솔을 통해서 SQL이 출력된다. 따라서 이 설정은 권장하지는 않는다. (둘다 켜면 logger , System.out 둘 다 로그가 출력되어서 같은 로그가 중복해서 출력된다.)
스프링 부트 3.0
1
2
3
#JPA log
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.orm.jdbc.bind=TRACE
스프링 부트 3.0 이상을 사용하면 하이버네이트 6 버전이 사용되는데, 로그 설정 방식이 달려졌다. 다음과 같이 로그를 설정해야 한다.
JPA 적용 1 - 개발
JPA에서 가장 중요한 부분은 객체와 테이블을 매핑하는 것이다. JPA가 제공하는 애노테이션을 사용해서 Item객체와 테이블을 매핑해보자.
Item - ORM 매핑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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을 생략할 경우 필드의 이름을 테이블 컬럼 이름으로 사용한다. 참고로 지금처럼 스프링 부트와 통합해서 사용하면 필드 이름을 테이블 컬럼 명으로 변경할 때 객체 필드의 카멜 케이스를 테이블 컬럼의 언더스코어로 자동으로 변환해준다.itemNameitem_name, 따라서 위 예제의@Column(name = "item_name")를 생략해도된다.
JPA는
public또는protected의 기본 생성자가 필수이다. 기본 생성자를 꼭 넣어주자.
1
public Item() {}
이렇게 하면 기본 매핑은 모두 끝난다. 이제 JPA를 실제 사용하는 코드를 작성해보자.
JpaItemRepositoryV1
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
@Slf4j
@Repository
@Transactional
public class JpaItemRepositoryV1 implements ItemRepository {
private final EntityManager em;
public JpaItemRepositoryV1(EntityManager em) {
this.em = em;
}
@Override
public Item save(Item item) {
em.persist(item); // persist : 영구히 저장한다
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
Item findItem = em.find(Item.class, itemId);
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
// 업데이트 저장?
// 안해줘도 된다. 더티 채킹 (트랜잭션 끝나는 시점에)
}
@Override
public Optional<Item> findById(Long id) {
Item item = em.find(Item.class, id);
return Optional.ofNullable(item);
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
String jpql = "select i from Item i";
Integer maxPrice = cond.getMaxPrice();
String itemName = cond.getItemName();
if (StringUtils.hasText(itemName) || maxPrice != null) {
jpql += " where";
}
boolean andFlag = false;
if (StringUtils.hasText(itemName)) {
jpql += " i.itemName like concat('%',:itemName,'%')";
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
jpql += " and";
}
jpql += " i.price <= :maxPrice";
}
log.info("jpql={}", jpql);
TypedQuery<Item> query = em.createQuery(jpql, Item.class);
if (StringUtils.hasText(itemName)) {
query.setParameter("itemName", itemName);
}
if (maxPrice != null) {
query.setParameter("maxPrice", maxPrice);
}
return query.getResultList();
}
}
private final EntityManager em: 생성자를 보면 스프링을 통해 엔티티 매니저(EntityManager)라는 것을 주입받은 것을 확인할 수 있다. JPA의 모든 동작은 엔티티 매니저를 통해서 이루어진다. 엔티티 매니저는 내부에 데이터소스를 가지고 있고, 데이터베이스에 접근할 수 있다.@Transactional: JPA의 모든 데이터 변경(등록, 수정, 삭제)은 트랜잭션 안에서 이루어져야 한다. 조회는 트랜잭션이 없어도 가능하다. 변경의 경우 일반적으로 서비스 계층에서 트랜잭션을 시작하기 때문에 문제가 없다. 하지만 이번 예제에서는 복잡한 비즈니스 로직이 없어서 서비스 계층에서 트랜잭션을 걸지 않았다. JPA에서는 데이터 변경시 트랜잭션이 필수다. 따라서 리포지토리에 트랜잭션을 걸어주었다. 다시 한번 강조하지만 일반적으로는 비즈니스 로직을 시작하는 서비스 계층에 트랜잭션을 걸어주는 것이 맞다.
참고: JPA를 설정하려면 EntityManagerFactory , JPA 트랜잭션 매니저(JpaTransactionManager), 데이터소스 등등 다양한 설정을 해야 한다. 스프링 부트는 이 과정을 모두 자동화 해준다.
설정 및 테스트 실행
JpaConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class JpaConfig {
private final EntityManager em;
public JpaConfig(EntityManager em) {
this.em = em;
}
@Bean
public ItemService itemService() {
return new ItemServiceV1(itemRepository());
}
@Bean
public ItemRepository itemRepository() {
return new JpaItemRepositoryV1(em);
}
}
ItemServiceApplication
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Slf4j
//@Import(MemoryConfig.class)
//@Import(JdbcTemplateV1Config.class)
//@Import(JdbcTemplateV2Config.class)
//@Import(JdbcTemplateV3Config.class)
//@Import(MyBatisConfig.class)
@Import(JpaConfig.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ItemServiceApplication.class, args);
}
@Bean
@Profile("local")
public TestDataInit testDataInit(ItemRepository itemRepository) {
return new TestDataInit(itemRepository);
}
}

테스트 확인 결과 동적 쿼리랑, Insert도 잘 나간다.
댓글남기기