개발자로 후회없는 삶 살기
JPA PART.JPA 환경 구축 및 실습 본문
서론
※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.
https://www.inflearn.com/roadmaps/149
본론
실무에서 jpa를 바로 도입하면 망하는 이유는 매핑을 제대로 못해서 그렇습니다. 이번 강의에서는 객체와 테이블을 제대로 설계하고 매핑하는 방법과 jpa 동작방식을 정확히 배웁니다.
1) 객체와 테이블을 제대로 설계하고 매핑하는 방법
2) 복잡한 관계 매핑
3) jpa 동작방법
jpa가 제대로 동작하는 방법을 이해하지 못하면 너무 추상화된 기술이기에 에러 해결을 못합니다. jpa가 내부적으로 어떻게 동작하는지 jpa가 어떤 sql을 만들어 내는지, 이 sql을 jpa가 언제 실행하는지 이해해야 합니다. 또한 실무 노하우와 성능까지 고려하여 jpa를 완전 정복해보겠습니다.
실무에서 sql을 작성하는 수많은 시간과 jpa 동작 방식을 몰라서 삽질하는 무수한 시간을 줄일 수 있을 것입니다. 단순한 sql 작성으로 시간을 낭비하지 않을 수 있고 남는 시간에 더 많은 테스트 코드, 설계, 코드리뷰를 고민할 수 있습니다.
- jpa 어플리케이션 개발
프로젝트를 생성하고 스프링 없이 jpa를 사용하여 동작하는 어플을 만들어 jpa의 감을 잡아보겠습니다.
-> 프로퍼티스 설정
persistence.xml에 프로퍼티스 설정할 것을 넣습니다. 반드시 /resources/META-INF 하위에 만들어야합니다.
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="hello">
<properties>
<!-- 필수 속성 -->
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="javax.persistence.jdbc.user" value="sa"/>
<property name="javax.persistence.jdbc.password" value=""/>
<property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<!-- 옵션 -->
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>
<!--<property name="hibernate.hbm2ddl.auto" value="create" />-->
</properties>
</persistence-unit>
</persistence>
jpa을 쓸 건데 이름은 뭐를 쓸 것인지 unit name을 넣습니다. 보통 DB 하나당 한개 만들며 지금 한개의 DB만 사용하여 하나 명시합니다. DB 접속을 위해 접속 정보를 넣어줘야합니다.
- jpa 구동 방식
jpa가 어떻게 동작하냐면 jpa는 Persistence라는 클래스가 있는데 여기서 프로퍼티스를 읽어서 EntityManagerFactory라는 클래스를 만듭니다. 팩토리는 프로퍼티스에 DB 접근 정보가 있으니 DB에 수정, 저장, 조회, 삭제 할 수 있을 것입니다. 공장에서 em을 만들어서 사용합니다.
-> 동작 테스트
jpa가 동작하는 지를 보려면 JpaMain이라는 클래스를 만들어서 확인하면 됩니다.
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
em.clear();
emf.close();
}
}
Main 클래스에 Persistence를 선언하여 라이브러리가 잘 들어왔고 환경 버전은 잘 맞췄는지 확인하는 것입니다. 환경 셋팅 확인은 매니저를 꺼내서 실행해보면 됩니다. 매니저는 공장에서 꺼내서 쓸 수 있습니다. createEntityManagerFactory()를 하면 UnitName을 넘기라고 하는데 여기에 xml에 작성한 이름을 적고 공장을 선언합니다. 이제 공장에서 매니저를 꺼내서 확인해보면 동작 확인이 끝납니다.
위 로그처럼 쭉쭉쭉 뭔가가 올라가면 연결이 된 것이고 커낵션과 관련된 동작을 하고 있습니다.
=> 테이블 만들고 동작 테스트
위는 라이브러리 확인이고 실제 동작을 위해 테이블을 만들고 엔터티와 맵핑을 해서 jpa가 동작하는지 보겠습니다.
회원 테이블을 만들고 이와 맵핑할 엔터티를 만듭니다. 이 둘의 맵핑을 통해 jpa 실행을 볼 것입니다.
-> 회원 엔터티
Member 클래스를 만듭니다.
@Entity
public class Member {
@Id
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@Entity를 꼭 붙어야하는데 jpa가 로딩될 때 jpa를 사용하는 애구나라고 jpa가 인식하고 jpa가 관리하는 엔터티로 됩니다. 테이블과 같이 id와 name을 넣고 id를 pk로 합니다.
-> 멤버 객체를 DB에 저장
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
Member member = new Member();
member.setId(1L);
member.setName("A");
em.persist(member);
tx.commit();
em.clear();
emf.close();
}
}
공장은 app 로딩 시점에 딱 하나만 만들어야 합니다. 그리고 실제 db에 저장하는 것은 트랜잭션 단위인 고객의 구매나, 거래 등이 일어날 때마다 매니저를 만들어서 실행합니다. 즉 공장은 한 번만 만들어지고 트랜잭션이 일어날 때마다 매니저를 만듭니다. > main에서 회원을 DB에 저장합니다. 회원에 데이터를 넣고 em.persist(회원)을 하면 저장이 끝납니다.
> 근데 실행해보면 저장이 안됩니다. jpa는 데이터를 변경하는 모든 작업을 트랜잭션 안에서 해야합니다. 쉽게 생각해서 트랜잭션을 하는 행위가 db 커낵션을 얻고 DB에 접근하는 것이라고 보면 됩니다.
트랜잭션을 만들고 실행해보면 드디어 쿼리가 나오고 DB에 데이터가 들어갑니다. 보면 지금 개발자가 쿼리를 만든게 없는데 jpa가 엔터티 맵핑 정보를 보고 넣어주는 것입니다.
@Transactional
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
Member member = new Member();
member.setId(2L);
member.setName("A");
em.clear();
emf.close();
}
}
@Transactional을 붙이고는 안 되는 것을 보니 스프링을 사용하지 않아서 프록시 클래스가 생성이 안되어서 그런 것 같습니다.
-> 설명
@Entity
@Table(name = "user")
public class Member {
개발자는 sql을 작성한 적이 없는데 jpa가 맵핑 정보를 봐서 쿼리를 작성하고 실행해 주었습니다. 엔터티와 테이블을 연결한 적이 없는데 관례상 생략을 하면 엔터티 이름과 같은 테이블과 jpa가 맵핑해줍니다. 맵핑할 테이블 이름을 지정해주고 싶으면 @Table을 하면 됩니다.
컬럼도 어노테이션을 붙이면 맵핑할 수 있고 생략하면 필드 명으로 맵핑이 되며 jpa는 어노테이션으로 테이블과 필요한 매핑을 다 합니다.
-> 문제 발생
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
// 트랜잭션 시작
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member();
member.setId(3L);
member.setName("A");
em.persist(member);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
문제가 발생했을 때 롤백할 수 있도록 트랜잭션 코드를 수정합니다. 또한 fin에 매니저를 닫아줘야하고 매니저가 내부적으로 커낵션을 물고 동작해서 반드시 매니저를 사용하면 닫아줘야 합니다. 매니저는 트랜잭션을 할 때마나 만들어진다고 했는데 위 코드처럼 동작하는 것입니다. 전체 어플이 끝나면 공장도 닫아야 합니다. 실제로는 스프링이 이것을 다 @트랜잭션으로 다 해줍니다. 그 후 was가 내려갈 때 공장도 닫아줍니다.
-> 수정
try {
Member member = em.find(Member.class, 1L);
member.setName("HelloJPA");
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
1번 회원을 수정해봅니다. 매니저를 컬랙션처럼 생각하면 되는데 find해서 클래스, pk로 맴버를 찾아오고 set하면 수정이 됩니다. 매니저를 객체를 저장해주는 컬랙션처럼 보라고 했는데 이처럼 저장한 객체를 가져와서 값을 넣으면 컬랙션처럼 데이터가 수정됩니다.
로그를 보면 update 쿼리가 남고 실제 DB에 데이터가 바뀝니다. 자바 객체에 값만 바뀌었는데 실제 DB에 값이 바뀌는 것이 정말 신기합니다. jpa를 통해서 엔터티를 가져오면 jpa가 엔터티를 관리하고 변경이 됐나 안됐나 트랜잭션 커밋하는 시점에 다 체크를 하고 커밋 직전에 쿼리를 만들고 커밋하여 update 쿼리가 나갑니다. 삭제도 컬랙션처럼 remove()를 하면 DB에 데이터가 실제로 사라집니다.
정말 간편해집니다. 이전에 가장 간편해진 버전인 탬플릿을 사용할 때 sql을 작성하고 executeQuery나 update 메서드를 해서 그래도 jdbc로 DB에 접근해서 sql을 해서 DB에 데이터를 접근하는 느낌을 받을 수 있었는데 jpa를 이용하면 sql을 안 쓰는 것처럼 자바 메서드로 객체를 다뤄서 DB에 데이터를 접근할 수 있습니다. 이게 자바 컬랙션을 다루도록 설계되어서 그렇습니다.
- 주의점
공장은 웹 서버가 올라온 순간 DB 당 하나만 생성이되고 매니저는 고객의 요청이 올 때마다 만들어지고 버려집니다. 그래서 매니저는 쓰레드간에 절대로 공유하면 안 됩니다. 얘를 하나 만들어서 여러 쓰레드에서 같이 쓰면 장애가 납니다. 마치 트랜잭션을 커낵션을 가져오는 것이라고 보면 커낵션을 한 번 쓰고 버리는 것처럼 매니저도 한 번 쓰고 버려야 합니다. jpa의 모든 데이터 변경은 트랜잭션 안에서 실행이 되기 때문입니다.
- jpql 소개
단순한 조회는 어떻게 할까요? find()로 하면 됩니다.
그런데 where을 사용해서 검색 조건을 넣는 예를들어 "나이가 18살 이상인 회원을 조회"하고 싶으면 어떻게 할까요? 그러면 jpql을 써야합니다. ✅
현업에서의 개발의 고민은 테이블이 정말 많고 필요하면 조인도 해야하고 원하는 데이터를 최적화해서 가져와야 하고 필요하면 통계성 쿼리도 날려야하는데 이런 걸 어떻게 할지가 고민입니다.
try {
List<Member> resultList = em.createQuery("select m from Member as m", Member.class)
.getResultList();
for (Member member : resultList) {
System.out.println("member name = " + member.getName());
}
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
jpa에서는 이것을 jpql로 합니다. 여러개의 행 조회는 createQuery()에 조회 쿼리를 작성하고 getResultList()를 하면 됩니다. jpa도 쿼리를 사용할 수 있는 것이고 이 쿼리로 복잡한 쿼리를 할 수 있는 것입니다. 근데 잘 보면 sql과 약간 다릅니다. 이게 jpa 입장에서는 코드를 짤 때 절대로 테이블을 대상으로 짜지 않습니다. 이건 멤버 객체를 대상으로 쿼리를 한 것입니다. "맴버 객체를 다 가져와"로 find를 객체 전부 다에 하는 것처럼 보면 됩니다.
로그를 보면 select에 모든 필드가 다 있습니다. 작성한 jpql은 m으로 멤버 엔터티를 넣었는데 모든 필드를 넣도록 적용이 됩니다.
try {
List<Member> resultList = em.createQuery("select m from Member as m", Member.class)
.setFirstResult(5)
.setMaxResults(8)
.getResultList();
for (Member member : resultList) {
System.out.println("member id = " + member.getId());
}
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
이게 어떤 메리트가 있을까요? 예를들어서 페이징을 하고 싶으면 setFirst와 Max로 1번부터 10개 가져오는 것을
Limit와 offset이 자동으로 적용이 됩니다. jpql이 객체를 대상으로 하는 객체 지향 쿼리라서 여러가지 기능들을 자바 코드로 다 제공을 해줍니다.
-> 설명
jpa를 사용하면 결국 엔터티 객체를 중심으로 개발이 되는데 문제는 검색 쿼리로 현업에서 일할 때 조인을 엄청 많이 하고 쿼리를 안 쓸 수 없습니다. 데이터를 단건이 아니라 검색을 해야 할 때 예를들어 "상위 10프로 회원을 가져와" 등 쿼리로 검색을 해야합니다.
DB 데이터를 필터링해서 가져와야 하는 건데 테이블에서 가져오면 jpa 사상이 깨지는 거라서 테이블이 아닌 객체를 대상으로 쿼리를 하는 문법이 들어간 것입니다. 결국은 쿼리를 날려야하는데 그러면 DB에 종속적으로 개발을 해야하니 객체를 대상으로 쿼리를 하는 jpql이 제공되는 것입니다. 쿼리 DSL까지 하면 자바로 쿼리를 하면서 신나게 개발을 할 수 있습니다.
'[백엔드] > [JPA | 학습기록]' 카테고리의 다른 글
JPA PART.고급 매핑 (0) | 2023.08.20 |
---|---|
JPA PART.다양한 연관관계 매핑 (0) | 2023.08.17 |
JPA PART.연관관계 매핑 기초 (0) | 2023.08.12 |
JPA PART.엔터티 매핑 (0) | 2023.08.01 |
JPA PART.영속성 관리 (0) | 2023.06.11 |