❓왜 jpa를 써야 할까? : SQL 중심적인 개발의 문제점
- 현재의 애플리케이션 개발 언어 트렌드 → 객체 지향 언어(ex : [Java, Scala, ...])
- 현재 데이터베이스 세계의 헤게모니 → 관계형 DB(ex : [Oracle, MySQL, ...])
→ 개발자가 객체로 데이터를 가공해도 DB에 저장할 땐, 결국 SQL을 사용한다.
→ SQL 중심적인 개발 !
→ 객체를 관계형 DB에 관리한다는 것이 문제 !
❓무엇이 문제일까?
1. 무한 반복, 지루한 코드
기능하나 추가해서 테이블을 생성할 때마다 CRUD SQL을 다 만들어주어야 한다.
→ Jdbc, MyBatis가 매핑에 도움을 주기는 하지만 그래도 개발자가 다 짜야함!
Example : 회원 객체를 만들고 DB에 CRUD를 하는 기능이 있는 상태
1. 기존 회원 객체와 테이블 기능 쿼리 구현
/** 회원객체 **/
public clas Member {
private String memberid;
private String name;
}
/** 쿼리 **/
INSERT INTO MEMBER(MEMBER_ID, NAME, TEL) VALUES...
SELECT MEMBER_ID, NAME, TEL FROM MEMBER M
UPDATE MEMBER SET ...
2. 기획자가 전화번호 필드를 추가 해 달라고 한 상황
/** 회원객체 **/
public clas Member {
private String memberid;
private String name;
private String tel;
}
/** 쿼리 **/
INSERT INTO MEMBER(MEMBER_ID, NAME, TEL) VALUES...
SELECT MEMBER_ID, NAME, TEL FROM MEMBER M
UPDATE MEMBER SET ... TEL = ?
결론 : SQL에 의존적인 개발을 할 수밖에 없다!
2. 패러다임의 불일치 - 객체 vs 관계형 DB
: 객체지향과, 관계형 데이터 베이스는 서로 가지고 있는 사상이 다르다.
- 객체지향 개발 : 추상화, 캡슐화, 정보 은닉, 상속, 다향성 등 시스템의 복잡성을 제어하는 다양한 장치들을 제공
- 관계형 DB : 데이터를 잘 정규화해서 저장하는 것이 목표
객체를 영구 보관하는 다양한 저장소
object
RDB
: 현 헤게모니NoSQL
: 대안이 될 수 있지만, 아직까지는 main XFile
: 객체 100만건을 Object로 바꾼 다음, 검색을 해야하는데 현실적으로 불가능OODB
: 죽은지 오래
객체를 관계형 데이터베이스에 저장
객체를 SQL로 변환해서 RDB에 저장하는 과정은 누가 할까? 개발자가 !
→ 노가다 시작,,!
객체 vs 관계형 데이터베이스
1. 상속
- 객체의 상속관계를 DB에 밀어 넣으려면 어떻게 해야할까?
: 객체 상속관계와 그나마 제일 유사한 관계가 Table 슈퍼 타입 서브타입 관계이다. - 객체 상속 관계에서는 그저
extends
나implements
로 상속 관계를 맺고 캐스팅도 자유롭다.
하지만, 상속받은 객체(Album)를 데이터 베이스에 저장하려면 어떻게 해야할까?- 객체 분해 : Album객체를 Item과 분리한다.
- Item Table에 하나, Album 테이블에 하나 두개의 쿼리를 작성해서 저장한다.
- 그러면, Album 객체를 DB에서 조회하려면 어떻게 해야할까?
:ITEM
과ALBUM
을 조인해서 가져온 다음, 조회한 필드를 각각 맞는 객체에 매핑시켜서 가져와야 한다.
결론 : DB에 저장할 객체에는 상속 관계를 쓰지 않는다.
만약 자바 컬렉션에서 저장하면?
list.add(album);
자바 컬렉션에서 조회하면?
Album album = list.get(albumId);
/** 부모 타입으로 조회 후 다형성 활용 **/
Item item = list.get(albumId);
2. 연관관계
- 객체는 참조를 사용한다. (
member.getTeam();
) - 테이블은 외래 키를 사용한다. (
JOIN ON M.TEAM_ID = T.TEAM_ID
)
Member와 Team간에 참조가 가능한가?
- 객체
Member
→Team
은member.getTeam()
을 통해 가능,Team
→Member
는 참조할 객체가 없기 때문에 불가능하다. - 테이블
서로가 서로를 참조할 키(FK)가 있기 때문에 양측이 참조가 가능하다.Member
↔︎Team
- 객체를 테이블에 맞춰 모델링해보자.
class Member{
String id; //MEMBER_ID 컬럼 사용
Long teamId; //참조로 연관관계를 맺는다.
String username; // USERNAME 컬럼 사용
}
class Team{
Long id; //TEAM_ID 컬럼 사용
String name; //NAME 컬럼 사용
}
/*쿼리*/
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES ...
INSERT INTO TEAM(TEAM_ID, NAME) VALUES...
하지만, 위 코드는 객체지향적이지 못한 것 같다.
왜? → Member
에서는 Team
을 참조하는데 참조 값을 가져야 하지 않을까?
Long teamId -> Team team;
이런식으로 설계를 바꾼다면 외래키를 DB에 어떻게 저장할까?
member.getTeam().getId();
이렇게 참조되는 Team객체에서 id를 가져와서 넣어줘서 저장하면 된다. 하지만, 조회할 때는 어떻게 해야할까?
SELECT M.*, T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
public Member find(String memberId){
//SQL 실행
Member member = new Member();
//데이터터베이스에서 조회한 회원 관련 정보를 모두 입력
Team team = new Team();
//회원과 팀 관계 설정
member.setTeam(team);
return member;
}
자바 컬렉션에서는 ?
list.add(member);
Member member = list.get(memberId);
Team team = member.getTeam();
객체 그래프 탐색
이 그래프를 보면 Member 객체에서 엔티티 그래프를 통해 Category 까지도 접근이 가능해야 한다. 하지만 과연 Member
객체에서 Delivery
코드를 마음껏 호출할 수 있을까? ❌불가능 하다.❌
이유 : 처음 실행하는 SQL에 따라 탐색 범위가 이미 결정된다.
SELECT M.*, T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
member.getTeam(); //OK
member.getOrder(); //null
위의 코드를 보자. 처음에 Member와 Team을 가져온 뒤, 값을 반환했다. member.getTeam();
은 실행이 되지만, member.getOrder();
로 Order를 가져올 수 없다. → 엔티티 신뢰문제가 발생한다.
class MemberService{
...
public void process(){
Member member = memberDao.find(memberId);
member.getTeam(); //????
member.getOrder().getDelivery(); //????
}
}
- 이 코드에서 정말 자유롭게
member
안의 모든 참조를 자유롭게 참조할 수 있을까? - 실제로 모든 코드와 쿼리를 확인해보기전까지는 엔티티 그래프 검색이 어디까지 되는지 확신할 수 없다. 즉, 엔티티 신뢰문제가 발생한다.
모든 객체를 미리 로딩할 수 없기 때문에 대안으로 상황에 따라 동일한 회원 조회 메서드를 여러 번 생성한다.
memberDAO.getMember(); //Member만 조회
memberDAO.getMemberWithTeam(); //Member와 Team조회
memberDAO.getMemberWithOrderWithDelivery(); // Member, ORder, Delivery 조회
결론: 계층형 아키텍처에서 진정한 의미의 계층 분할이 어렵다.
3. 데이터 식별 방법
❓동일한 식별자로 조회한 두 객체 비교결과는?
String memberId = "100";
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);
member1 == member2; // false 다르다
class MemberDAO{
public Member getMember(String memberId){
String sql = "SELECT * FROM MEMBER WHERE MEMBER_ID = ?";
...
//JDBC API, SQL 실행
return new Member(...);
}
}
member
를 조회할 때마다new Member()
를 통해 새로운 객체를 만들어서 조회 하기 때문에 두 인스턴스 비교는 내용물이 같더라도 다르다.
자바 컬렉션에서 조회한 객체를 비교
String memberId = "100";
Member member1 = list.get(memberId);
Member member2 = list.get(memberId);
member1 == member2; // true 같다.
결론 : 객체지향적으로 모델링을 할 수록 매핑작업만 늘어나고 복잡해진다. 그렇다면, 객체를 자바 컬렉션에 저장하듯이 DB에 저장할 수 없을까? 고민하다 나온 것이 JPA이다.
JPA란?
Java Persistence API의 줄임말이다. 자바 진영의 ORM 기술 표준을 의미한다.
그럼, ORM은 뭘까?
ORM은 Object-relational mapping(객체 관계 매핑)이다.
객체는 객체대로 설계하고, 관계형 데이터베이스는 관계형 데이터베이스대로 설계를 한 다음, 중간의 어떤 차이들은 ORM 프레임워크가 매핑해준다. 대중적인 언어에는 대부분 ORM 기술이 존재한다.
JPA의 동작 과정
JPA는 애플리케이션과 JDBC 사이에서 동작을 한다.
자바 애플리케이션 있으면 그 안에 JPA가 있다. 여기서 개발자가 직접 JDBC API를 쓰는 게 아니라, JPA에게 명령을 내리면 JPA가 JDBC API를 사용해서 SQL을 호출하라고 만들어서 보내고 그 결과를 받아서 동작을 하게 된다.
JPA의 동작 과정 - 저장
예를 들어, MemberDAO에서 객체를 저장하고 싶으면 JPA에게 그냥 멤버 객체를 넘긴다. 그럼 JPA가 멤버를 분삭하고, 적정한 SQLㅇ르 생석하고, JDBC API를 사용해서 디비에 보내고 결과를 받는다. 여기서 중요한 것은 패러다임의 불일치를 해결한다는 것이다.
JPA의 동작 과정 - 조회
조회 과정에서는 객체를 다 매핑을 해준다는 것을 볼 수 있다. 여기서도 패러다임의 불일치를 해결해준다.
❓그러면 우리는 JPA를 왜 사용해야 할까?
: SQL 중심적인 개발에서 객체 중심으로 개발하기 위해서 사용한다.
1. 생산성 - JPA와 CRUD
- 저장:
jpa.persist(member)
- 조회:
Member member = jpa.find(memberId)
- 수정:
member.setName(“변경할 이름”)
- 삭제:
jpa.remove(member)
JPA를 쓴다는 것은 사상 자체가 자바 컬렉션에 객체를 넣었다 뺐다 하는 것 처럼 쓸 수 있다는 것!
2. 유지보수
- 기존: 필드 변경시 모든 SQL 수정해야한다.
- JPA: 필드만 추가하면 된다. (SQL은 JPA가 처리해줌 )
3. JPA와 패러다임의 불일치 해결
1.상속 관계
위에서 봤던 상속 관계 테이블을 다시 보자.
여기서 개발자가 할일은 다음 코드만 작성해주면 된다.
Album album = jpa.find(Album.class, albumId);
나머지 쿼리문은 JPA가 처리해준다.
SELECT I.*, A.*
FROM ITEM I
JOIN ALBUM A ON I.ITEM_ID = A.ITEM_ID
2. 연관관계, 객체 그래프 탐색
멤버에 새로운 팀을 세팅해준다음, persist를 통해 저장을 하면 DB에 저장이 된다.
- 연관관계 저장
member.setTeam(team);
jpa.persist(member);
- 객체 그래프 탐색find 해서 멤버 객체를 가져온 다음,
member.getTeam
해서 팀을 꺼내올 수 있다. 마치 자바 컬렉션에 넣었던 것처럼 활용할 수 있다.JPA를 쓰면, 멤버 객체를 믿을 수 있기 때문에, 객체 그래프 탐색을 자유롭게 할 수 있다. 즉, 신뢰할 수 있는 엔티티, 계층이 형성된 것이다.
class MemberService {
...
public void process() {
Member member = memberDAO.find(memberId);
member.getTeam(); //자유로운 객체 그래프 탐색
member.getOrder().getDelivery();
}
}
JPA와 비교하기
String memberId = "100";
Member member1 = jpa.find(Member.class, memberId);
Member member2 = jpa.find(Member.class, memberId);
member1 == member2; //같다.
동일한 트랜젝션에서 조회한 엔티티는 같음을 보장한다.
3. JPA의 성능 최적화 기능
- 1차 캐시와 동일성(identity) 보장해준다.
- 같은 트랜잭션 안에서는 같은 엔티티를 반환 → 약간의 조회 성능을 향상
- DB Isolation Level이 Read Commit이어도 애플리케이션에서 Repeatable Read 보장한다.
String memberId = "100"; Member m1 = jpa.find(Member.class, memberId); //SQL Member m2 = jpa.find(Member.class, memberId); //캐시 println(m1 == m2) //true
- 트랜잭션을 지원하는 쓰기 지연(transactional write-behind) : 버퍼링 기능
- INSERT
- 트랜잭션을 커밋할 때까지 INSERT SQL을 모은다.
- JDBC BATCH SQL 기능을 사용해서 한번에 SQL 전송한다.
transaction.begin(); // [트랜잭션] 시작 em.persist(memberA); em.persist(memberB); em.persist(memberC); //여기까지 INSERT SQL을 데이터베이스에 보내지 않는다. //커밋하는 순간 데이터베이스에 INSERT SQL을 모아서 보낸다. transaction.commit(); //[트랜잭션] 커밋
- INSERT
- UPDATE
- UPDATE, DELETE로 인한 로우(ROW)락 시간 최소화
- 트랜잭션 커밋 시 UPDATE, DELETE SQL 실행하고, 바로 커밋
transaction.begin(); // [트랜잭션] 시작 changeMember(memberA); deleteMember(memberB); 비즈니스_로직_수행(); //비즈니스 로직 수행 동안 DB 로우 락이 걸리지 않는다. //커밋하는 순간 데이터베이스에 UPDATE, DELETE SQL을 보낸다. transaction.commit(); // [트랜잭션] 커밋
- 지연 로딩(Lazy Loading)
- 지연로딩 : 객체가 실제 사용될 때 로딩한다.
- 즉시 로딩 : JOIN SQL로 한번에 연관된 객체까지 미리 조회한다.
결론 : ORM은 객체와 RDB 두 기둥위에 있는 기술이다. 따라서 객체지향과 RDB 두 개에 대해 충분한 지식을 가지고 있는 것이 중요하다
이 글은 김영한 님의 "자바 ORM 표준 JPA 프로그래밍 - 기본편" 강의를 듣고 정리한 내용입니다.