JDBC를 JDBC 템플릿을 이용하게 됨으로써 코드는 확 줄었지만 여전히 개발자가 sql문을 작성해야하는 숙제가 남아있다. 그런데 JPA를 사용하면 쿼리도 자동으로 작성해준다.
구글 트렌드에서 검색해보면 세계적으로 JPA가 압도적이고, 2015년 이후로 국내에서도 마이바티스보다 JPA가 보급이 많이 되었다.
스프링에서도 JPA를 굉장히 많이 지원하고 있으므로 추후에 김영한님의 자바 ORM 표준 JPA 프로그래밍 책을 사서 심도있게 다뤄볼 예정이다.
JPA(Java Persistence API)는 Java Persistence API의 줄임말로 자바 진영의 ORM 기술 표준이다. JPA는 인터페이스 모음이므로, 이 인터페이스를 구현한 실제 클래스가 필요하다. JPA를 구현한 실제 클래스에는 대표적으로 하이버네이트(Hibernate)가 있다.
ORM이란 무엇일까?
ORM(Object-Relational Mapping)은 이름 그대로 객체와 관계형 데이터베이스를 매핑 한다는 뜻이다. ORM 프레임워크는 객체와 테이블을 매핑 해서 패러다임의 불일치 문제를 개발자 대신 해결해준다.
따라서 객체 측면에서는 정교한 객체 모델링을 할 수 있고 관계형 데이터베이스는 데이터베이스에 맞도록 모델링하면 된다. 그리고 둘을 어떻게 매핑 해야 하는지 매핑 방법만 ORM 프레임워크에게 알려주면 된다.
JPA는 특정 DB에 종속되지 않는 기술이다.
하지만 DB마다 SQL 문법과 함수, 타입이 다르다.
이 차이를 해결하기 위해 방언(Dialect) 이 존재한다.
→ JPA가 사용하는 SQL을 해당 DB 문법에 맞게 변환해주어 차이를 흡수한다.
따라서 DB를 변경하더라도 코드 수정 없이 Dialect 설정만 바꾸고 DB를 교체할 수 있다.
| 구분 | MySQL | Oracle |
|---|---|---|
| 문자형 타입 | VARCHAR | VARCHAR2 |
| 문자열 자르기 | SUBSTRING() | SUBSTR() |
| 페이징 | LIMIT | ROWNUM |
JPA는 다양한 데이터베이스 환경에 맞게 여러 키 생성 전략을 제공한다.
각 전략의 동작 시점과 성능 특성을 이해하고, 사용하는 DB에 맞게 선택하는 것이 중요하다.
AUTO_INCREMENT를 사용하는 MySQL 등에 적합하다.em.persist() 시점에 즉시 INSERT 쿼리가 실행된다.private static void logic(EntityManager em) {
Board board = new Board();
em.persist(board);
System.out.println("board.id = " + board.getId());
}
// 출력: board.id = 1
@Entity
@SequenceGenerator(
name = "BOARD_SEQ_GENERATOR",
sequenceName = "BOARD_SEQ", // 실제 DB 시퀀스 이름
initialValue = 1,
allocationSize = 1
)
public class Board {
@Id
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "BOARD_SEQ_GENERATOR"
)
private Long id;
}
allocationSize 기본값이 50인 이유
단, DB에 직접 접근해 데이터를 삽입할 경우 시퀀스 값이 크게 증가한 것처럼 보일 수 있다.
hibernate.id.new_generator_mappings=true설정 시 위 최적화가 적용된다.
SELECTUPDATE 실행allocationSize를 활용해 최적화할 수 있다.@Entity
@TableGenerator(
name = "BOARD_SEQ_GENERATOR",
table = "MY_SEQUENCES",
pkColumnValue = "BOARD_SEQ",
allocationSize = 1
)
public class Board {
@Id
@GeneratedValue(
strategy = GenerationType.TABLE,
generator = "BOARD_SEQ_GENERATOR"
)
private Long id;
}
IDENTITY, SEQUENCE, TABLE 중 자동 선택한다.@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
| 전략 | 키 생성 주체 | 대표 DB | 장점 | 단점 |
|---|---|---|---|---|
| IDENTITY | DB | MySQL | 단순, 설정 간단 | 쓰기 지연 불가 |
| SEQUENCE | DB 시퀀스 | Oracle, PostgreSQL | 빠르고 안전 | DB 의존 |
| TABLE | 키 생성 테이블 | 모든 DB | 이식성 높음 | 가장 느림 |
| AUTO | JPA 자동 선택 | 모든 DB | DB 교체 용이 | 전략 예측 어려움 |
build.gradle 파일에 JPA와 데이터베이스(여기서는 H2) 관련 라이브러리를 추가한다.
jpa는 jdbc관련 라이브러리를 포함하고 있으므로 Jdbc 라이브러리는 지워도 된다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
// implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
resources/application.properties에 설정을 추가한다.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
이후에 엔티티에 매핑을 한다.
DB에서 ID를 생성(관리)하는 것을 설정하기 위해 IDENTITY를 지정해준다.
스프링은 EntotyManager를 만들어 인젝션(주입)을 해준다.
또한 JPA는 회원가입처럼 데이터가 변경이 일어날 때 항상 트랙젝션 안에서 실행 되어야하므로 service클래스나 회원가입 메소드위에 @Transactional을 붙여준다.
package jpabook.start;
import javax.persistence.*;
import java.util.Date;
@Entity
@Table (name="MEMBER")
public class Member {
@Id
@Column (name = "ID")
private String id;
@Column (name - "NAME")
private String username;
private Integer age;
//== 추가 ==
@Enumerated (EnumType. STRING)
private RoleType roleType; // 1
@Temporal (TemporalType. TIMESTAMP)
private Date createdDate; // 2
@Temporal (TemporalType. TIMESTAMP)
private Date lastModifiedDate; // 2
@Lob
private String description; //3
}
//Getter, Setter
package jpabook.start;
public enum RoleType {
ADMIN, USER
}
////////////////////////////////////////////////////////////
@Configuration
public class SpringConfig {
@PersistenceContext
private EntityManager em;
@Autowired
public SpringConfig(EntityManager em) {
this.em = em;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new JpaMemberRepository(em);
}
}
////////////////////////////////////////////////////////
public class JpaMemberRepository implements MemberRepository {
private final EntityManager em;
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
@Override
public Member save(Member member) {
em.persist(member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
@Override
public Optional<Member> findByName(String name) {
List<Member> result = em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name)
.getResultList();
return result.stream().findAny();
}
@Override
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList()
} //JPA 쿼리인데 select의 대상이 테이블이 아닌 객체를 대상으로 쿼리를 날리고 있다.
}
위에서 살펴본 바와 같이 JPA를 사용하기만 해도 코드가 줄어드는 기적을 볼 수 있었다.
그런데 한단계 더 나아가 스프링 데이터 JPA 프레임워크를 사용하면 리포지토리에 구형 클래스 없이 인터페이스만으로 개발을 완료할 수 있다.(마법과 다름없다.)
기존에 반복적으로 작성하면 CRUD기능도 스프링 데이터 JPA가 모두 제공한다.
따라서 개발자들은 핵심 비즈니스 로직을 개발하는데, 집중 할 수 있게 되었다.
이제는 스프링 데이터 JPA가 선택이 아닌 필수이다.
하지만 JPA를 편리하게 도와주는 라이브러리일 뿐이기 때문에 JPA를 공부하지 않으면 실무에서 활용하기가 어려울 것이다. (결국엔 모두 배워야 한다...)
위의 Repository코드를 스프링 데이터 Jpa를 사용하여 작성해보았는데 코드가 거의 없다시피 하는 것을 볼 수 있다.
CRUD기능은 인터페이스로 제공되고 findByName()처럼 메소드 명으로 조회 기능을 제공한다.
페이징 기능도 자동으로 제공한다.
Spring Data Jpa을 통해 EntityManger를 직접 다루지 않고도 JPA 기술을 사용할 수 있다.
import hello.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interFace SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
@Override
Optional<Member> findByname(String name);
}
실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적쿼리는 QueryDSL이라는 라이브러리를 사용하면 된다. QueryDSL을 사용하면 쿼리도 자바코드로 작성할 수 있고, 동적쿼리도 편리하게 작성할 수 있다. 이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용하거나 스프링 JdbcTemplate를 사용하면 된다.