JPA 왜 사용해야하는데?

nGyu·2022년 5월 6일
1
post-thumbnail

해당 글은 김영한님의 인프런 강의를 기반으로 작성되었습니다!

Spring을 하다보면 JPA에 관련된 이야기가 자주 언급이 되는데, 문득 JPA를 왜 써야하는지에 대한 의문이 들기 시작했다. 단순히 SQL을 작성하지 않고 바로 DB에 저장되기때문일까? 라는 생각도 함께 들어 김영한님의 강의를 보며 정리를 해보기로 했다.

SQL 중심적인 개발에서 객체 중심으로 개발

SQL중심적인 개발이란 무엇일까?
말 그대로 CRUD( CREATE, SELECT, UPDATE, DELETE) 코드가 무수히 많아지며
자바 객체를 SQL로 바꾸고 또 바뀐 SQL을 자바 객체로 바꾸고 등등.. 무수히 많이 반복되며 지루한 코드가 생겨날텐데 이를 SQL을 중심적으로 개발하기 때문에 생기는 문제이다.

하지만, 자바는 객체지향 언어이며 객체지향적으로 개발을 하는것이 좋다. JPA는 이렇게 SQL중심적인 개발에서 객체 중심개발로 전환을 할 수 있게 도와준다.

생산성의 증가

JPA를 사용하면 생산성이 기하급수적으로 상승한다! 그런데 왜 상승을 할까?

JPA와 CRUD

저장

일반적으로 저장을 할 때 SQL 문을 작성한다고 가정을 해보자.
이 때 해야하는게 INSERT INTO ~ VALUES ~ 이런식으로 작성을 할텐데 이 때 컬럼명을 다 가져오고 여기에 들어가야할 VALUES를 다 바인딩하고 조회해서 넣어주는 방식을 사용해야 한다.
하지만, JPA를 사용한다면 persist() 메소드를 이용하면 된다. jpa.persist([obejct]) 와 같은 형태이다.

조회

조회도 마찬가지이다. jpa.find() 메소드를 사용한다면 바로 조회가 가능하다.

수정

객체를 생성하고 그 객체에 object.set[Coulmn]([identifier])을 해주면 바로 수정이 된다.

삭제

jpa.remove([object])메소드를 이용해 삭제할 객체를 넣어주면 바로 삭제가 된다.

유지보수

만약, 컬럼을 하나 추가한다고 가정을 해보자.

Member라는 테이블에 tel이라는 연락처를 받을 수 있는 컬럼을 추가한다고 가정을 했을 때, 기존에 작성했던 SQL의 INSERT, SELECT, UPDATE를 하는 SQL에 전부 tel이라는 컬럼을 추가해야한다.

하지만, JPA는 컬럼이 추가되면 “어? 컬럼 추가했네? 이건 내가 처리할게!” 라며 JPA가 처리를 해준다.

패러다임의 불일치 해결

이 부분이 핵심이라 할 수 있다.

각 테이블 즉, 엔티티들은 다른 엔티티들과 연관관계를 맺고있는 경우가 많다. 하지만, 객체의 경우는 주소값을 이용해 관계를 맺고 테이블은 외래키를 이용해 관계를 맺는다. 이 둘만 보았을 때 엔티티와 객체는 전혀 다른 특징을 가지고 있는데 이를 패러다임의 불일치라고 한다.

상속

저장

B,C,D가 A를 상속받는것을 DB로 보면 B,C,D테이블이 A테이블을 참조한다고 할 수 있다.
이 때 B의 값을 INSERT하기 위해서는 B테이블과 A테이블에 모두 INSERT를 해 주어야 한다.

하지만, JPA를 사용한다면 persist 메소드를 한번만 호출하면 두 개의 INSERT를 하나로 줄일 수 있다는 것이다.

조회

B b = jpa.find( A.class , bId) 와 같이 코드를 작성한다면, JPA가 알아서 A와 B를 JOIN 하여 SQL을 작성해준다.

연관관계

이를 좀 깊게 들어간다면 내용들이 엄청나게 많아지는데 간단하게(?) 설명을 해보도록 하겠다.

member.setTeam(team); 을 하고 jpa.persist(member); 를 한다면 어떻게 될까?
setTeam을 했을 때 member의 team정보가 team 엔티티를 참조하고 있을 때 만약 B팀으로 수정한다고 하면 B의 PK를 가져와 member의 team정보의 FK를 B팀의 PK로 바꿔주는 INSERT문을 자동으로 생성해준다.

좀 다양한 내용들이 많긴한데 일단 개념자체는 이렇다.

객체 그래프 탐색

Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam();

이 코드를 한번 생각해보자 Member만을 조회했는데 Team객체에 Member객체 안에 있는 Team객체까지 가져올 수 있을까?

이를 좀 생각해보면 SELECT * FROM member; 만 했는데 team 엔티티까지 조인해서 가져오는 결과를 가져온다는것이다.

이게 과연 가능할까? 정답은 JPA라서 가능하다. JPA에는 지연로딩(LAZY)라는 기능이 존재하는데 이는 실제로 객체를 사용하기 전 까지 조회를 미루는 기능인데 이 기능때문에 위 코드같은 동작이 가능하다.

비교

String memberId = "100";

Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);

if(member1 == member2){~~}
--------------
class MemberDAO{
	public Member getMemeber(String memberId){
		String sql = "SELECT * FROM member WHERE memberId = ?";
		....
		return new Member(...);
	}
}

위 코드는 member1과 member2가 같을까? 아니다. 각각 새로운 객체를 생성하기 때문에 비교를 할 때 다르게 나온다.

객체끼리 비교를 할 때는 객체의 메모리 주소를 비교하기 때문이다.

String memberId = "100";
Member member1 = jpa.find(Member.class, memberId);
Member member2 = jpa.find(Member.class, memberId);

if(member1 == member2){~~}

하지만 위 코드에서 member1과 member2는 과연 같을까?
정답은 true이다. 왜일까?

JPA는 Collection에서 조회하는것처럼 동작을 해야하기 때문에 동일한 트랜잭션에서 조회한 엔티티는 같음을 보장하기 때문이다.

여기서 추가적인 개념이 또 들어가게 되는데 바로 캐싱이다. 이는 뒤에서 다루도록 해보겠다.

JPA의 성능 최적화 기능

1차 캐시와 동일성 보장

비교부분에서 설명했던 내용의 연장선이니 이어진다고 생각하면 좋다.

member1과 member2는 조회하는 구조가 똑같다. 그렇기 때문에 member1이 조회한 값을 그대로 캐싱을 한 상태이며, 이 후에 바로 member2를 조회하기 때문에 캐싱된 값을 가져다가 저장한다.

이러한 동일한 트랜잭션을 이용한다면 캐싱된 값을 사용할 수 있다는 이점도 존재한다.

이게 약간 생각을 잘 해야하는게 “트래픽이 많아지고 다른 유저가 동일한 값을 조회할 때 캐시가 일어나나?”라는것은 틀린것이다. 한 개의 메서드가 한 엔티티를 중복해서 조회를 해야하는 엄청나게 복잡한 쿼리에서 캐싱이 일어나는, 굉장히 짧은 순간의 캐싱이니 이 부분은 혼동하지 않았으면 한다.

이 외에는 약간의 조회 성능을 향상할 수 있다는 점이 있을 수 있다.

동일성 보장의 경우 DB Isolation Level이 Read Commit이어도 애플리케이션에서 Repeatable Read를 보장한다는 말인데, 이는 좀 어려워서 간단하게 언급하도록 하겠다.

DB Isolation Level은 총 4단계로 나뉘어지며, 레벨이 높아질수록 성능이 안좋아진다.
만약, 3단계를 2단계로 줄인다고 해도 JPA에서는 3단계인 Repeatable Read를 보장해준다는 의미이다.

이에 대해서 잘 몰라도 개발하는데 크게 지장은 없다고 하시는데, 나중에 시간이 남으면 DB Isolation Level과 동일성 보장에 대해서 공부를 좀 해봐야겠다.

쓰기 지연

트랜잭션을 commit 하기 전 까지 INSERT SQL을 모으고, 한번에 보낸다는것이다.

즉, begin()과 commit()사이에 10개의 persist가 있다면 INSERT SQL 10개를 모아 commit()때 한번에 보낸다는것이다.

지연 로딩과 즉시 로딩

지연 로딩 : 객체가 실제 사용될 때 로딩
즉시 로딩 : JOIN SQL로 한번에 연관된 객체까지 미리 조회

Member member = memberDao.find(memberId);
Team team = member.getTeam();
String teamName = team.getName();

이 두개는 모두 지연 로딩과 즉시 로딩 이 두가지를 선택해서 사용할 수 있는 로직이다.
memberDao 안에서 어떻게 로직을 처리하냐 즉, 어느 시점에서 쿼리를 보낼것이냐 라는 것이다.

“난 지금은 member만 조회할래 team은 조금있다 조회하지뭐”
라는 생각을 가진 로직이라면 1줄에서 SELECT FROM member가 생성되고 이 후에 3줄의 getName()을 할 때 SELECT FROM team 을 전달하게 된다.

“난 지금 member와 team을 조회할래”

라는 생각을 가진 로직이면 1번줄에서 바로 JOIN을 통해 값을 바로 가져온다.

이 차이점이라고 보면 된다. 이런걸 보면 JPA는 정말 신기한것같다 :)


정리

JPA가 우리가 작성해야할 SQL 을 대신 작성해준다고 할 수 있다.

그런데 JPA를 할 줄 안다고 RDB를 안해도 될까?
필자는 아니라고 보고, 김영한님도 RDB를 알아야 한다고 한다.

김영한님이 말씀하셧듯 성능의 90%는 RDB에서 나오고 JAVA는 Scala, TS등으로 대체가 가능하지만 RDB는 언어가 대체되는것만큼의 확률보다 현저히 낮다라고 언급을 하셨다.

결국 RDB를 잘 알아야 성능 최적화가 가능하고 이를 이용해서 어떻게 사용하냐에 따라 ORM을 작성하는 방법도 달라진다는 생각이 든다.

profile
지금보다 내일을, 모레를 준비하자

0개의 댓글