JPA 소개
Last updated
Last updated
객체를 관계형 DB에 관리해야 한다. 그러다보니 SQL 중심으로 개발이 된다. 모든 쿼리를 다 짜고 자바 객체를 SQL로, SQL을 자바 객체로 변환하는 지루한 코드를 무한 반복해야한다.
객체는 필드와 메서드를 잘 캡슐화해서 사용하는 것이, RDBMS는 데이터를 잘 정규화 해서 저장하는 것이 목표다. 즉 서로 다른 성격이기 때문에 사용하기가 힘들다. 결국 개발자가 SQL 매퍼의 일을 하게 된다.
상속
객체에는 있지만 데이터베이스에는 없다.
연관 관계
객체는 참조를 통해 데이터를 가져오지만 데이터베이스는 PK, FK로 조인을 해서 가져온다.
데이터 타입
테이터 식별 방법
객체의 상속 관계를 비슷하게 구현할 수 있는 것이 슈퍼 타입, 서브 타입이라는 물리 모델이다.
이걸 테이블에 저장하려면 객체를 분해한 다음 insert into item
, insert into album
이렇게 쿼리를 여러 번 쳐야 한다. 조회를 한다고 하면 join을 가지고 album이나 movie에 맞게 만들어줘야 한다. 이렇게 번잡해지는 문제가 있다.
만약 DB 데이터가 아니라 자바 컬렉션이라면 위처럼 쉽게 할 수 있다. 하지만 RDMBS에 저장하고 꺼내는 순간 개발자가 일일이 손대야 한다.
객체는 참조를 사용한다.
테이블은 외래 키를 사용해서 join한다.
문제는 Team에서 Member 객체를 조회할 수 없다는 것이다. 테이블은 PK만 있으면 반대로도 조인할 수 있다.
테이블처럼 객체에 외래키인 teamId를 넣어서 Team을 참조하게 만들었다고 해보자.
코드로 구현하면 위와 같이 매핑할 수 있을 것이다.
근데 객체답게 설계한다면 사실 Member는 teamId가 아니라 team을 참조해야 하는 것 같다.
DB에 insert하려고 하니 team_id가 필요하다. member에서 getTeam()
을 통해 id를 찾아준다.
문제는 조회다. 멤버와 팀을 각각 조회해서 연관 관계를 코드로 직접 설정해줘야 하는 번거로움이 있다.
컬렉션에 넣으면 팀이 다 한 곳에 들어가니까 연관된 데이터가 한 줄에 딸려온다. 객체지향으로 하면 이렇게 편하게 쓸 수 있다. 하지만 DB에 넣는 순간 이게 망가져버린다.
객체는 레퍼런스만 있으면 어디든 쭉쭉 갈 수 있어야 한다.
하지만 처음 실행하는 SQL에 따라 검색할 수 있는 범위가 제한되어 버린다.
처음에 데이터를 가져올 때 member와 team을 대상으로 가져왔다면, getTeam()은 데이터가 있지만 order는 비어있기 때문에 꺼낼 수가 없다.
이것은 결국 Entity 신뢰 문제로 번진다. 개발자가 코드 상으로는 team과 order에 접근할 수 있지만 실제 데이터를 확인하기 전까지는 진짜 그런지 알 수가 없다.
레이어드 아키텍쳐는 그 다음 계층을 신뢰하고 있어야 하는데 이게 깨지는 것이다.
그렇다고 쓰지도 않는 객체를 미리 다 끌어올 수도 없는 일이다. 그래서 경우의 수에 따라 위처럼 조회하는 메서드를 여러 개 만들어야 한다.
물리적으로는 서비스와 DAO 계층이 이렇게 나뉘어 있어도 논리적으로는 연결되어 있는 문제가 있다. 즉, 진정한 의미의 계층 분할이 어렵다.
두 객체는 식별자가 같아도 SQL 실행 결과를 new Member()
를 통해 새로 만들기 때문에 비교하면 다르게 나온다.
하지만 컬렉션에서 조회한다면 둘은 참조값이 같기 때문에 같게 나온다.
이렇다보니 객체 지향 설계를 하면 할 수록 매핑 작업만 늘어난다. 사람들은 객체를 자바 컬렉션에 저장하듯 DB에 저장하는 방법을 고민했고 이에 대한 대안이 JPA다.
객체는 객체답게, RDB는 RDB 답게 설계하면 ORM 프레임워크가 매핑해준다.
JPA는 애플리케이션과 JDBC 사이에서 동작한다. 자바 애플리케이션이 JPA에게 명령하면 JPA가 JDBC API를 이용해 SQL를 실행한다.
JPA에게 Member 객체를 넘기면 해당 Entity를 분석해서 적절한 쿼리를 생성한다. 그 쿼리를 JDBC API에게 보내고 결과를 받는다.
중요한 건, 쿼리를 개발자가 아니라 JPA가 만든다는 것이다.
조회할 때도 PK만 보내면 JPA가 적절한 쿼리를 만들어 보내고 결과를 매핑해준다.
이렇게 ORM은 패러다임의 불일치를 해결해준다.
옛날에도 EJB라는 ORM이 있었지만 성능이나 기능이 좋지 않아 잘 쓰이지 않았다. 그걸 개선한 게 하이버네이트고 자바 진영에서 하이버네이트 개발자를 데려와 표준으로 만든 것이 JPA다.
JPA는 인터페이스의 모음이다. JPA 2.1 표준 명세를 구현한 구현체는 하이버네이트, EclipseLink, DataNucleus 세 가지가 있다. 대부분은 하이버네이트를 사용한다.
SQL 중심적인 개발에서 객체 중심으로 개발 가능
데이터 접근 추상화와 벤더 독립성
표준
저장, 조회, 수정, 삭제에 대한 SQL이 필요없고 그냥 만들어져 있는 메서드를 쓰기만 하면 된다.
특히 수정은 내가 원하는 값만 넣으면 DB로 수정 쿼리를 보내준다. 리스트에 있는 값을 수정해서 다시 리스트에 넣는 행위를 하지 않듯 JPA는 이것을 자동으로 해준다.
예전에는 개발자가 쿼리를 하나하나 수정해야 했다.
이제 필드만 추가하면 SQL은 JPA가 처리한다.
JPA는 상속과 같은 패러다임 불일치 문제도 해결해준다. persist()에 자식만 넣으면 부모와 자식 모두 쿼리를 날려준다.
조회할 때도 상속 관계의 Entity에 대해 알아서 join 해준다.
연관 관계를 지정했다면 Member만 조회해도 연관 관계가 있는 Team도 함께 조회할 수 있다. 마치 자바 컬렉션에 넣었던 것처럼.
이전엔 쿼리가 Member, Team만 불러왔기 때문에 Order를 불러올 수 없었다. 하지만 이제 객체 그래프를 자유롭게 탐색할 수 있다.
JPA에는 지연 로딩이 있어서 해당 데이터가 필요할 때 SQL을 쳐서 가져올 수 있다.
같은 트랜잭션 안에서 조회한 Entity는 같다는 걸 보장한다.
시스템에 중간 계층이 있다면 캐시 등을 이용해 성능을 높일 수 있다. JPA도 이 중간 계층에 해당한다.
만약 로직이 너무 복잡해서 중간 중간에 계속 같은 멤버를 조회하는 일이 생긴다면, 처음 조회한 m1은 SQL로 가져오지만 같은 PK이 값으로 다시 조회하면 캐시에서 가져온다.
여기서 캐시는 일반적인 캐시가 아니라 한 트랜잭션 안에서만 보장되는 짧은 캐시다. 실무에서 성능적으로 크게 도움은 안되지만 매커니즘을 알아두자.
똑같은 쿼리가 3개가 있다면 네트워크도 3번 타야 해서 느리다. JDBC Batch 기능이 있긴 하지만 아주 복잡해서 쓰기가 힘들다.
JPA는 일단 메모리에 쌓았다가 커밋되는 순간 같은 쿼리는 한 네트워크로 보낸다. 트랜잭션이란 기능이 있기 때문에 일단 메모리에 요청을 쌓았다가 커밋하는 순간에 네트워크로 보낸다.
Member 객체를 찾을 땐 Member 객체만 가져오고, 그 안에 속한 Team 데이터는 필요할 때 쿼리를 날려서 가져오는 것이 지연 로딩이다. 그만큼 쿼리가 많이 나가는 단점이 있다.
즉시 로딩은 join을 통해 한 방에 가져오는 기능이다. Member를 가져올 때 Team을 같이 가져온다. Team을 실제 가져오는 시점에는 이미 로딩된 데이터를 사용한다.
실무에서는 일단 지연 로딩으로 해놓고 최적화가 필요한 부분에 즉시 로딩을 사용한다.