[JPA] JPA의 소개와 사용법
✔️ 01. JPA 소개
JPA는 자바 진영에서 만든 ORM 기술 표준이다.
그리고 스프링 진영에서도 스프링 프레임워크 자체는 물론이고 스프링 데이터 JPA라는 기술로 JPA를 적극적으로 지원한다.
또한 전자정부 표준 프레임워크의 ORM 기술도 JPA를 사용한다.
관계형 데이터베이스는 가장 대중적이고 신뢰 할만한 안전한 데이터 저장소이다.
데이터베이스에 데이터를 관리하려면 SQL을 사용해야 하는데
자바로 작성한 애플리케이션은 JDBC API를 사용해서 SQL을 데이터베이스에 전달한다.
public class MemberDAO {
public void save(Member member) {
String sql = "INSERT INTO MEMBER(MEMBER_ID, NAME) VALUES(?, ?)";
try {
Connection conn = DriverManager.getConnection("url", "user", "password");
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setLong(1, member.getId());
pstmt.setString(2, member.getName());
int result = pstmt.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
위 코드는 JDBC를 이용한 회원 등록 기능이다.
만약 컬럼이 추가된다면? 다른 테이블과 연관되어 있다면? 다른 기능을 추가한다면?
아마 복잡하고 반복되는 코드가 지속될 것이다.
물리적으로 SQL과 JDBC API를 데이터 접근 계층에 숨기는데 성공했을지는 몰라도
논리적으로는 엔티티와 아주 강한 의존관계를 가지고 있다.
또한 관계형 데이터베이스는 데이터 중심으로 구조화되어 있고 집합적인 사고를 요구한다.
그리고 객체지향에서 이야기하는 추상화, 상속, 다형성 같은 개념이 없다.
연관관계에서도 객체는 참조를 통해 다른 객체와 연관관계를 가지고 연관된 객체를 조회하지만
테이블은 외래 키를 사용해서 다른 테이블과 연관관계를 가지고 조인으로 연관된 테이블을 조회한다.
public void save(Member member) {
entityManager.persist(member);
}
public Team findTeam(Long memberId) {
Member member = entityManager.find(Member.class, memberId);
Team team = member.getTeam();
return team;
}
위 코드의 save 메서드는 JPA를 이용한 회원 등록 기능이다.
SQL문을 직접 작성할 필요도 없이 단 1줄만으로 똑같은 기능을 하고 있다.
또한 findTeam 메서드를 통해 연관된 객체 그래프를 탐색할 수 있다.
JPA는 team의 참조를 외래키로 변환해서 적절한 INSERT SQL을 데이터베이스에 전달하고
객체를 조회할 때 외래키를 참조로 변환하는 일도 처리해줌으로써 연관관계와 관련된 패러다임의 불일치를 해결해준다.
여기서 끝이 아니다 SQL을 직접 다루면 실행하는 SQL마다 객체 그래프의 탐색 범위가 정해진다.
하지만 JPA는 지연 로딩을 통해 실제 객체를 사용하는 시점까지 데이터베이스 조회를 미룸으로써 자유롭게 객체 그래프를 탐색할 수 있게 해준다.
이 때 JPA는 조회되는 객체가 같은 트랜잭션일 때 같은 객체가 조회되는 것 또한 보장해준다.
이걸 포함해서 JPA를 사용해야 하는 여러 이유가 있다.
- 생산성
지루하고 반복적인 코드와 CRUD용 SQL을 개발자가 직접 작성하지 않아도 된다.
이걸 통해 데이터베이스 설계 중심의 패러다임을 객체 설계 중심으로 역전시킬 수 있다.
- 유지보수
SQL과 JDBC API 코드를 JPA가 대신 처리해주므로 유지보수해야 하는 코드 수가 줄어든다.
- 패러다임의 불일치 해결
JPA는 상속, 연관관계, 객체 그래프 탐색, 비교하기와 같은 패러다임의 불일치 문제를 해결해준다.
- 성능
JPA를 사용하면 회원을 조회하는 SELECT SQL을 한 번만 데이터베이스에 전달하고 두 번째는 조회한 회원 객체를 재사용한다.
- 데이터 접근 추상화와 벤더 독립성
애플리케이션과 데이터베이스 사이에 추상화된 데이터 접근 계층을 제공해서
애플리케이션이 특정 데이터베이스 기술에 종속되지 않도록 한다.
- 표준
JPA는 자바 진영의 ORM 기술 표준이기 때문에 다른 구현 기술로 손쉽게 변경할 수 있다.
✔️ 02. JPA 시작
JPA는 RDB와 JAVA를 연결하기 위한 기술이므로 우선 RDB와 연결해야 한다.
//application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/{DB이름}?useSSL=false&serverTimezone=Asia/Seoul
username:
password:
RDB는 어떠한 것을 사용해도 상관없다. 왜일까?
JPA는 다양한 데이터베이스의 방언을 제공하므로 특정 데이터베이스에 종속되지 않는 기술이다.
RDB와의 연결을 한 후 JPA의 구현체인 hibernate 라이브러리를 추가해줘야 한다.
이 때, 라이브러리를 관리해주는 도구를 사용하는데 대표적으로 Maven과 Gradle이 있다.
레거시 프로젝트는 주로 Maven이고 성능은 Gradle이 압도적으로 좋다.
//Maven
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>5.6.10.Final</version>
</dependency>
//Gradle
implementation group: 'org.hibernate', name: 'hibernate-entitymanager', version: '5.6.10.Final'
hibernate 라이브러리는 다음과 같이 구성되어 있다.
- hibernate-core : 하이버네이트 라이브러리
- hibernate-entitymanager : 하이버네이트를 JPA 구현체로 동작하도록 하는 표준
- hibernate-jpa-2.1-api : JPA 2.1 표준 API를 모아둔 라이브러리
hibernate-entitymanager만 추가해줘도 나머지가 함께 추가된다.
(사실 spring-data-jpa 라이브러리를 추가하면 한꺼번에 추가된다.)
이제 하이버네이트의 속성을 설정할 수 있다.
- hibernate.show_sql : 하이버네이트가 실행한 SQL을 출력한다
- hibernate.format_sql : 하이버네이트가 실행한 SQL을 출력할 때 보기 쉽게 정렬한다
- hibernate.use_sql_comments : 쿼리를 출력할 때 주석도 함께 출력한다
- hibernate.id_new_generator_mappings : JPA 표준의 키 생성 전략 사용
본격적으로 JPA를 시작하려면 가장 먼저 Entity를 만들어 클래스와 테이블을 매핑해야 한다.
@Entity
@Table(name = "MEMBER")
public class Member {
@Id
@Column(name = "ID")
private String id;
@Column(name = "NAME")
private String username;
private String age;
}
- @Entity : 이 클래스를 테이블과 매핑한다고 JPA에게 알려준다.
- @Table : 매핑할 테이블 정보를 알려준다. (생략하면 클래스 이름을 사용)
- @Id : 프로퍼티를 테이블의 PK(기본키)에 매핑한다. (식별자 필드라고 한다.)
- @Column : 프로퍼티를 컬럼에 매핑한다. (생략하면 프로퍼티명을 사용)
만든 Entity를 이용해 애플리케이션을 개발해보도록 하자
public class JpaMain {
public static void main(String[] args) {
//엔티티 매니저 팩토리 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
//엔티티 매니저 생성
EntityManager em = emf.createEntityManager();
//트랜잭션 획득
EntityTransaction tx = em.getTransaction();
try {
//트랜잭션 시작
tx.begin();
//비즈니스 로직 실행
bizLogin(em);
//트랜잭션 커밋
tx.commit();
} catch(Exception e) {
//트랜잭션 롤백
tx.rollback();
} finally {
//엔티티 매니저 종료
em.close();
}
//엔티티 매니저 팩토리 종료
emf.close();
}
}
코드는 크게 3부분으로 나눠볼 수 있다.
- 엔티티 매니저 설정
JPA를 시작하기 위해 설정 정보를 토대로 엔티티 매니저 팩토리를 생성한다.
엔티티 매니저 팩토리는 생성 비용이 크기 때문에 애플리케이션에서 딱 한 번 생성하고 사용한다.
엔티티 매니저는 엔티티를 데이터베이스에 등록하고 DataSource를 통해 커넥션을 유지하면서CRUD가 가능하게끔 해준다.
엔티티 매니저 팩토리와 엔티티 매니저는 사용한 후 꼭 close()를 호출해 자원을 반납해야 한다.
- 트랜잭션 관리
JPA를 사용할 땐 항상 트랜잭션 안에서 데이터를 변경해야 한다.
트랜잭션 내에서 비즈니스 로직이 정상 동작하면 커밋하고 예외가 발생하면 롤백한다.
- 비즈니스 로직
public static void bizLogic(EntityManager em) {
String id = "id";
Member member = new Member();
member.setId(id);
member.setUsername("username");
member.setAge(20);
//등록
em.persist(member);
//수정
member.setAge(30);
//한 건 조회
Member findMember = em.find(Member.class, id);
//여러 건 조회
List<Member> members =
em.createQuery("select m from Member m", Member.class)
.getResultList();
//삭제
em.remove(member);
}
- 등록
엔티티를 저장하기 위해 persist() 메서드를 통해 영속성 컨텍스트에 넘겨준다.
JPA는 엔티티의 매핑 정보를 분석해 INSERT SQL을 만들어 데이터베이스에 전달한다.
- 수정
update()라는 메서드는 없다.
엔티티의 값이 변경되면 JPA가 변경을 감지하여 UPDATE SQL을 만들어 전달한다.
- 삭제
remove() 메서드를 통해 DELETE SQL을 만들어 데이터베이스에 전달한다.
- 한 건 조회
find() 메서드를 통해 엔티티의 타입과 PK를 통해 SELECT SQL을 만들어 전달한다.
- 여러 건 조회
JPA는 테이블이 아닌 엔티티를 대상으로 검색하기 위해 SQL을 추상화 한 JPQL을 사용한다.
그렇기 때문에 테이블이 아닌 클래스와 필드를 대상으로 쿼리한다.
JPA는 createQuery() 메서드를 통해 작성된 JPQL을 토대로 SQL을 만들어 전달한다.