본 글은 https://www.inflearn.com/course/ORM-JPA-Basic/dashboard 강의를 바탕으로 작성한 글입니다.
1. JPA란?
JPA는 자바 진영의 ORM 기술 표준압나다.
[ORM]
- Object-relational mapping(객체 관계 매핑)
- 객체는 객체대로 설계
- 관계형 데이터베이스는 관계형 데이터베이스대로 설계
- ORM 프레임워크가 중간에서 매핑
- 대중적인 언어에는 대부분 ORM기술이 존재
orm이라는 것을 사용하게 되면 객체는 객체대로 설계를 하고 관계형 데이터베이스는 관계형 데이터 베이스 대로 설계를 하게 됩니다.
그리고 중간에 ORM 프레임 워크를 통해 객체와 데이터베이스의 차이부분 만큼을 조절하여 해결을 하게 됩니다 .
이렇게 함으로써 데이터베이스의 객체설계와 java의 객체 설계간에 패러다임의 불일치를 해결해 줍니다.
JPA는 애플리케이션과 JDBC 사이에서 동작한다. 원래는 개발자가 직접 JDBC API를 사용하였다면 이제는 jpa가 대신 JDBCAPI를 이용하여 매핑을 처리해 줍니다 .
이렇게 까지만 보면 우리가 흔히 쓰던 JDBC 템플릿이나 마이바티스 같은 것들과 비슷하다고 생각 할 수 있다. 하지만 JPA는 그런 프레임 워크랑 차이점이 있습니다.
2. 객체 형식으로 데이터베이스 접근
예를 들어 우리가 회원 객체를 저장한다고 생각해보겠습니다. 그러면 멤버 객체를 회원 DAO에 넘긴 후 회원 DAO가 JPA에게 멤버 엔티티(멤버 객체)를 저장해 주라고 명령을 하게 되면 JPA가 알아서 이 회원 객체를 분석을 한 후 SQL문을 생성해줍니다. 그리고 JDBC API를 사용하여 쿼리문을 실행시켜 데이터 베이스에 적용까지 시켜줍니다. 마치 java에 collction을 이용하여 저장하듯이 한줄코드를 이용하여 데이터를 처리할 수 있습니다.

만약 우리가 조회를 한다고 했을 때도 JPA에게 엔티티의 id만 전달하면 jpa가 select쿼리를 다 만든 뒤 JDBC API를 사용하여 result set을 다 메핑해준 뒤 패러다임 불일치까지 해결하여 entity object를 다 만들어서 우리에게 반환을 해줍니다.
그럼 JPA를 사용해야 하는 이유를 더 자세하게 알아보겠습니다.
JPA를 사용해야 하는 가장 중요한 이유는 SQL 중심적인 개발에서 객체 중심적인 개발로 이동할 수 있습니다. 왜냐하면 JPA가 중간에서 데이터베이스와 객체간에 불일치를 다 해결해주기 때문입니다. 따라서 JPA를 이용하면 java에 컬랙션에 객체를 저장하듯이 데이터베이스에 데이터를 객체처럼 이용할 수 있다는 장점이 생기게 됩니다. 이렇게 됨으로써 당연히 생산성과 유지보수 또한 훨씬 늘어나고 용이하게 됩니다.
생산성 관점에서는 복잡한 쿼리문을 작성할 필요없이 한줄의 코드로 정리가 됩니다.
유지보수 관점에서도 과거에는 만약 Member테이블에 연락처가 추가된다고 가정을 해보면 모든 SQL문에 수정작업을 거쳐야 한다. 하지만 JPA는 필드만 추가하면 해결이 됩니다.
3. JPA에서의 상속관계

기존방식으로 아이템이랑 엘범을 슈퍼타입 서브타입으로 테이블을 설계하게 되면 아이템 객체 인스턴스를 데이터 베이스에 저장을 할려면 인서트 쿼리를 아이템 테이블과 Album테이블에 모두 넣어야 합니다. 데이터가 나뉘어있기 때문에 insert쿼리를 두개를 작성을 해서 데이터를 저장하게 됩니다. 하지만 jpa를 이용하여 상속 저장을 하게 되면 엘범데이터를 넣을 때 insert구문 하나로 아이템 테이블에도 넣고 엘범테이블에도 데이터를 넣을 수 있게 됩니다. 왜냐하면 객체 상속 관계를 이용하여 데이터를 처리하기 때문입니다.
조회할 때도 마찬가지 입니다. jpa를 이용하면 java colletion에서 가져가는 것처럼 가져가지게 됩니다. 엘범을 조회할려면 아이템과 엘범을 조인한 다음에 가져가야 했었는데 JPA가 자동으로 필요한 테이블을 다 조인을 처리해서 엘범 객체를 반환해줍니다.
객체 그래프탐색을 할 때도 JPA를 이용하여 연관관계를 참조상태로 쓸 수 있습니다. 그래서 엔티티계층을 신뢰할 수 있습니다.
만약 jpa에서 member.setTeam()이라고 세팅을 해놓고 멤버를 저장했다고 했다고 가정을 해보겠습니다. 그리고 나서 이제 jpa.find를 통해서 멤버를 조회합니다. 그러면 member.getTeam()을 이용하여 팀을 조회할 수 있습니다. 그러면 이러한 의문점이생기게 됩니다. 그 수많은 query가 다 조인이 되어서 탐색이 가능한건가?라는 의문점이 들게 됩니다. 하지만 jpa에서는 성능최적화까지 고민을 하여 지연로딩이라는 것을 포함한 여러가지 방법을 통해 그래프탐색이 어디든 조회할 수 있도록 가능합니다.
이렇게 되면 엔티티계층을 신뢰할 수 있게 됩니다.
JPA를 통해서 memberId 100번으로 2개를 조회한 뒤 비교를 해보겠습니다. jpa를 이용해 member1과 member2를 id값 100을 이용하여 조회했을 때 두 값을 ==연산자를 이용하여 비교하면 같다라는 결과값이 나오게 됩니다. 마치 java collection을 사용할 때 처럼 같은 id값을 조회했을 때 같은 인스턴스가 나오게 됩니다.
하지만 jpa에서 비교할 때는 전제가 있습니다. 동일한 트랜잭션에서 조회를 해야 엔티티의 같음을 보장 할 수 있습니다.
JPA는 성능 최적화도 제공을하게 됩니다.
1. 1차캐시와 동일성 보장입니다.
jpa는 같은 트랜잭션 안에서는 항상 같은 엔티티를 반환해줍니다. jpa가 데이터 처리에 중간 과정에 참여하게 되면서 2가지의 성능상 이점을 가지게 됩니다 첫번째는 버퍼로 라이팅이 가능해지고 두번째로는 캐싱이 가능해집니다. 캐싱을 통해 이미 조회된 정보는 다시 조회를 할때 조회과정을 모두 거치지 않아도 중간에 정보를 반환을 받아 제공할 수 있는 기능이 있습니다. 만약 jpa에서 멤버를 조회를 할 때 같은 멤버를 조회하게 됩니다.
만약에 memberId=100인 회원을 조회한다했을 때 첫번째 조회할 때는 SQL문을 실행시켜 조회를 하게 됩니다. 하지만 두번째 조회를 하게 될때는 SQL문을 실행시키지 않고 캐시를 이용하여 값을 조회하게 됩니다. 두번의 조회로직을 작성했지만 같은 회원에 관해서는 SQL문을 한번만 실행을 하게 되는 것입니다. 같은 트랜잭션에서만 실행이 됩니다.
String memberId = "100";
Member m1 = jpa.find(Member.class, memberId); //SQL실행
Member m2 = jpa.find(Member.class, memberId); //캐시사용
println(m1 == m2) //true
//SQL문은 1번만 실행된다.
2. 트랜잭션을 지원하는 쓰기 지원입니다.
트랜잭션을 커밋할 대까지 InsertSQL을 모읍니다. 그 뒤 JDBC BATCH SQL 기능을 사용하여 한번에 SQL문을 전송합니다.
transaction.begin() // 트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
// 여기까지는 INSERT SQL을 데이터베이스에 보내지 않는다.
// 커밋하는 순간 데이터 베이스에 INSERT SQL을 모아서 보낸다.
transaction.commit(); // 트랜잭션 커밋
위의 코드를 보시면 트랜잭션을 시작한 후 멤버 A,B,C 세개를 모두 저장을 합니다. 이렇게 하면 인서트 쿼리가 세번 다 따로 나가는게 아니라 버퍼를 이용하여 모아놓았다가 트랜잭션을 커밋할 때 한번에 네트워크로 보내고 트랜잭션을 커밋하여 데이터를 저장할 수 있게 됩니다. 이렇게 하면 네트워크 통신 비용이 줄어든다는 장점이 있습니다.
3. 지연로딩과 즉시로딩
지연로딩: 객체가 실제 사용될 때 로딩
즉시로딩: JOIN SQL로 한번에 연관도니 객체까지 미리 조회
데이터를 조회할 때 어떨때는 멤버를 쓰고 어떨때는 멤버와 연관되어 있는 팀을 사용하게 됩니다. 근데 로직을 짜다 보니까 멤버를 조회할 때 항상 팀이 같이 사용하게 됩니다. 이렇게 되면 멤버를 조회 할 때 팀을 한번 쿼리로 같이 조인으로 조회해서 가지고 오는게 성능상 이점이 있습니다. 하지만 반대로 또 다른 상황이 있습니다. 만약에 멤버를 조회할 때 팀을 거의 같이 안씁니다 그러면 연관되어 있는 팀을 같이 조회하는 것보다 멤버만 조회하는게 성능상 유리합니다. 이렇게 되면 연관된 것을 같이 미리 땡겨올 필요가 없습니다. 이런 것처럼 상황마다 데이터를 어떻게 가져와야 할지 다릅니다. 그래서 JPA는 지연로딩과 즉시 로딩이라는 두가지를 모두 지원합니다.
// 지연로딩
Member member = memberDAO.find(memberId); // SELECT * FROM MEMBER
Team team = member.getTeam();
String teamName = team.getName(); // SELECT * FROM TEAM
// 즉시 로딩
Member member = memberDAO.find(memberId); // SELECT M.* , T.* FROM MEMBER JOIN TEAM ...
Team team = member.getTeam();
String teamName = team.getName();
지연로딩은 객체가 실제 사용될 때 로딩이 되는 것입니다.
위에 코드 처럼 먼저 멤버가 시용될떄는 멤버만 조회를 합니다. 그다음에 팀이 필요해서 팀을 사용하면 팀을 조회하게 됩니다. 이때 member.getTeam을 통해 프록시를 초기화한 뒤 실제 팀 데이터를 가져와 값을 채워주게 됩니다. 이렇게 되면 이제 실제 팀의 이름을 넣을 수 있게 됩니다.
만약 팀과 멤버를 같이 조회하고 싶을 때는 즉시로딩이라는 전략을 사용하게 됩니다. 멤버를 조회하는 순간 팀도 같이 조회를 하게 됩니다.
JPA에서는 평상시에 지연로딩 전략을 사용하고 즉시로딩을 사용했을 때 성능상 이점이 있겠다고 생각이 들 때 즉시로딩으로 설정을 하여 사용을 합니다.