숨 꾹참고 딥다이브 - JPA 역사편
Intro
🫧 “ORM이 이렇게까지 오기까지, 얼마나 숨을 참아야 했을까요?"
다시 돌아왔습니다. 한동안 휴식기를 좀 가졌습니다. 개인적으로 다사다난 한 8월을 보냈었어요.
경조사도 있었고, 정신을 차려보니 이미 8월이 사라 져 있었었죠.
JPA 에 대한 글을 한번 찐하게 써본 적이 없는 것 같아 글을 써볼까 합니다. JPA 많이들 쓰시죠.
처음 JPA 를 썼던 시기가 떠오릅니다. 잘 적응이 되지 않았던 기억이 많이 남아요. JDBC 로 DB 커넥션을 만들고 String Query 를 날리던 게 학부 시절의 기억이었는데...막상 그렇게 실무에서 업무를 할 일은 없었습니다.
실무에서 3년차를 진행하고있는 이 순간, JPA 를 얼마나 잘 알고 쓰고 있는 지 문득 궁금해졌습니다. 이번 포스팅도 가능하면 끝까지 딥다이브 해 보겠습니다.
☕ 커피 한잔 하면서 읽어주세요. 이번에도 딥다이브~! 달려보겠습니다.
JPA 소개
Java Persistance Api 는 자바진영을 대표하는 Object Relational Mapping (ORM) 입니다. Java 애플리케이션에서 데이터와 직접 맞닿아있는 부분을 담당하죠.
ORM 이라는 단어를 한번 면밀히 보겠습니다. 객체와 관계있는 데이터를 매핑 시켜주는 역할을 하는 게 ORM 이죠. 데이터베이스 내 테이블의 데이터를 읽어왔다면, 실제로 사용할 수 있는 객체로 매핑해주는 작업이 필요합니다. ORM 덕분에 우리는 이런 작업들을 수동으로 하지 않아도 됩니다.
ORM 의 선택지는 여러가지가 있고 JPA 는 그 중 하나입니다. MyBatis 를 쓸 수도 있고 Kotlin 베이스의 개발자라면 Exposed 를 사용할 수도 있죠. JPA 는 오랜 시간 사용되어서 레퍼런스가 많은 ORM 입니다. 어지간해서는 Spring 애플리케이션을 개발할 때 실패하지 않죠. 다만, JPA 그 자체가 가지는 단점도 있다보니 상황에 따라서 판단해서 사용하셔야 합니다.
이번 포스팅에서는 이런 기술적인 이야기보다는 역사에 대한 이야기를 할까 합니다. 아무리 이번 시리즈의 목적이 딥다이브라고 해도 너무 글 하나에서 내용을 방대하게, 깊게 다루면 피로도가 높아지더라구요.
JPA 역사 이야기
1. JDBC 의 시대
학부생 때 웹 애플리케이션 개발을 Java 를 통해 배워 본 사람이라면 아마 아래의 플로우로 데이터 접근을 처리했을 것입니다.
Connection -> Statement -> ResultSet -> while(rs.next()) {...}
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT * FROM MEMBER");
ResultSet rs = ps.executeQuery();
while (rs.next()) {
Member m = new Member();
m.setId(rs.getLong("id"));
m.setName(rs.getString("name"));
}
사실 이 프로세스는 2000년대 초반의 자바 진영의 데이터 접근 방식을 요약한 것입니다.
JSP 와 Servlet, 그리고 HTML, CSS, JS 로 웹 애플리케이션 수업을 들었던 적이 있습니다. 당시의 수업이 이론에 목적이 있었다는 것을 감안하면, 굳이 복잡한 Spring 을 사용하지 않았던 이유를 이해할 수 있었죠. (학부생 기준에선 충분히 복잡할 수 있다고 생각합니다. 사실 지금도 복잡해요...)
커넥션을 열고 닫는 것, 트랜잭션을 처리하는 것, SQL 을 작성하는 것, 그리고 데이터를 매핑하는 것 모든 것들이 처음에는 수동으로 코드를 작성하는 것에서 시작했습니다.
이 접근 방식의 문제가 있다면 크게 아래와 같습니다.
먼저 SQL 을 직접 관리해야 합니다. 당시에 QueryDSL 같은 게 있을 리가 없죠. 오직 String 으로 관리해야 합니다. 그 마저도 JDK 17 의 멀티라인 String 을 쓸 수 있었던 것도 아닐겁니다. 물론 지금도 이렇게 개발하시는 분도 계시겠지만, 2000년대 초반이라는 것을 기억하셔야 합니다. 우리가 정말 많이 쓰고 있는 LTS 인 JDK 8 이 2014년에 출시 된 버전이라는 것을 잊어서는 안됩니다...
다음으로 객체와 데이터베이스간에 불일치가 생길 수 있습니다. 물론 이건 지금도 비슷한게 아닌가 라고 생각할 수 있지만, 개발 과정에서 테이블 명세가 변경되는 경우 상당히 많은 코드를 수정해야 합니다. 최소한 ORM 을 쓰게 된다면, ORM 선에서 에러처리를 하거나 컬럼 하나를 무시하거나 등 대처를 할 수 있지만 현재의 구조에서는 쉽지 않죠.
종합적으로 비즈니스 로직보다 SQL 그 자체에 관리 포인트가 늘어나는 문제가 생깁니다. SQL 을 작성하는 것 자체가 문제는 아니겠지만, 정작 코드를 작성해야 할 시간에 SQL 을 수정하는 것은 개발 공수를 잡아먹는 일입니다.
물론 때로는 SQL 로 때우는게 나은 상황도 생기긴 합니다. 통계 처리를 할 때는 RDB 에 따라 사용할 수 있는 기능들을 써먹는 게 나을 수도 있죠.
지금 이야기하는 내용은 좀 결이 다른 내용이긴 합니다. SQL 그 자체가 객체로 작성된 게 아닌이상 데이터 타입만 봐서는 String 그 이상 그 이하도 아닌 게 현재 시점에서의 문제입니다.
수정사항이 생기든, 복잡하지만 한방으로 문제를 해결할 수 있는 쿼리를 쓰든 우리가 수정하고 관리하는 SQL 이 String 변수 하나에 전부 짱박혀있다고 생각해보면 이해가 좀 더 쉬울 것 같습니다. 조건에 따라 WHERE 문을 넣었다 빼고 싶은데 이 상황에선 어떻게 하면 좋을까요...?
이쯤 되면 누군가는 이런 생각을 할 수도 있을 것 같습니다.
아니 걍 이거 빌더같은걸로 해결 못하나?
SQL 관리에 객체를 활용해보자! 라는 생각을 누군가 하기 시작했고, 2001년 경 그 아이디어를 실현 한 프로젝트가 등장했습니다.
2. Hibernate 의 등장 (2001 ~)
Hibernate 는 우리가 이미 잘 알고 있는 ORM 입니다. 그 전에도 시도가 없었던 것은 아닌데, 바로 EJB 의 Entity Bean 입니다. 하지만 EJB 의 Entity Bean 은 복잡한 XML 설정과 Vendor 종속성이 있었습니다. (EJB 그 자체의 문제일 것 같습니다.)
Hibernate 는 최대한 단순하게 Annotation 베이스로 옵션을 지정할 수 있고, 애플리케이션 영역에서 트랜잭션을 컨트롤 할 수 있다는 차별점을 가졌습니다. 또한 1차, 2차 캐시 기능과 같은 심화 기능을 제공하죠.
가장 결정적으로 DB 중심이 아닌 애플리케이션의 객체 중심의 설계 철학을 가졌습니다. 애플리케이션 영역에서까지 DB의 의존성을 가져가지 않으려고 노력했다는 의미죠.
이전에는 DB 스키마 그 자체가 애플리케이션의 설계를 지배하는 것이 일반적인 상황이었습니다. Hibernate 는 테이블이 아닌 객체 모델 (entity) 를 우선했죠. 객체간의 연관관계가 곧 데이터의 관계가 될 수 있도록 설계되었습니다. Member.team, Order.items 와 같은 연관관계등이 그 예시입니다.
또한 JDBC 만 쓸 때와는 다르게, 객체의 상태 변화 그 자체를 추적하는 기능이 존재했습니다. JDBC 는 직접 UPDATE Query 를 날려줘야 하죠. Hibernate 에서는 그럴 필요가 없습니다.
member.setName("DevCow");
tx.commit(); // UPDATE 자동 실행 (Dirty Checking)
개발자는 덕분에 데이터 조작이 아니라 객체의 상태 전이 (state transition) 을 다루게 되었죠.
또한 객체간의 연관관계 그 자체가 데이터의 관계로 인해 데이터 행 (Row) 단위가 아닌 객체 그 자체를 중심으로 코드를 작성할 수 있게 되었습니다.
member.getTeam().getName(); // Lazy Loading으로 자동 조회
JDBC 였으면 Join Query 를 작성해서 날려야 합니다. 그렇게 조회 된 Row 들을 또 다시 가공해야 하죠. 이런 기능 덕분에 DB 테이블이 아닌 객체간의 네트워크가 사고의 단위가 될 수 있습니다.
Hibernate 는 크게 네 가지 핵심 기능을 가지고 있습니다.
- Lazy Loading : 필요한 시점에만 로딩
- Dirty Checking : 변경 자동 감지
- Session Cache : 동일 트랜잭션 내 동일 객체 보장
- HQL : SQL을 추상화한 객체지향 쿼리
3. JPA 의 탄생 (2006)
Hibernate의 성공을 눈여겨본 **Java Community Process (JCP)**는 각 벤더의 ORM 기술을 통합해 표준을 만들기로 결정합니다.
당시에 Hibernate 외에도 다른 ORM 들이 많이 시도되던 시기었습니다.
2006년, EJB 3.0과 함께 **JPA 1.0 (Java Persistence API)**이 발표됩니다. Hibernate 와 Toplink, Kodo 의 협의체에서 발표되었죠.
@Entity
class Member {
@Id @GeneratedValue
Long id;
String name;
}
이 코드 몇줄로 객체가 영속성 (persistence) 를 가지게 되었죠. 그동안의 수동 매핑이 이렇게 심플해졌습니다.
4. JPA 2.x - ORM의 정제기
그 이후로 JPA 는 계속해서 버전을 올리며 표준을 잡아 갔습니다. 간략한 패치노트는 아래와 같습니다.
-
Jpa 2.0 (2009)
- Criteria API로 타입세이프한 쿼리 제공
- ElementCollection, MapKey 등 컬렉션 매핑 강화
- 1차 캐시, Flush 모드 표준화
-
JPA 2.1 (2013)
- EntityGraph: Fetch 전략 동적 제어
- AttributeConverter: VO ↔ 컬럼 변환
- Stored Procedure 호출 지원
이 시기 부터는 Spring Data JPA 가 등장하기 시작했습니다. 우리가 익숙 한 Repository 인터페이스, CRUD 자동완성의 시대가 열린거죠.
Hibernate 는 여전히 JPA 의 구현체로 진화했습니다. 처음부터 JPA 의 구현체는 아니었겠지만, 표준을 잡아가며 사실상 JPA라는 표준의 구현체가 Hibernate 가 되는 구조가 굳어지게 되죠.
5. Jakarta Persistence - 표준의 계승 (2019 ~ )
2019년에 한 사건이 터집니다. Oracle 이 Java EE 를 Eclipse 재단에 이관하게 되었죠. 그 결과 모든 패키지들이 javax 에서 jakarta 로 변경되었습니다. 더이상 Java EE 가 아니라 Jakarta EE 를 따르게 되었교 JPA 3.0 의 시대가 오게 되었죠.
JPA 외의 ORM?
JPA 가 세상에 나온 지 시간이 꽤 오래 지났죠. 현재는 정말 다양한 선택지들이 존재합니다. Spring Data JPA 와 QueryDSL 의 조합은 국룰 조합으로 유명하죠. 하지만 그 외에도 jOOQ, Exposed (Kotlin) 과 같은 선택지도 존재합니다. 논블로킹 DB 접근이 필요한 경우엔 R2DBC 가 요구되기도 하죠.
개인적으로는 JPA + QueryDSL 의 조합을 가장 오래 사용했지만, 최근에는 Exposed 를 사용해보려고 노력중입니다. Java 그 자체의 단점을 Kotlin 이 많이 보완해주는 만큼 Java 에 맞게 최적화 된 Spring Data JPA 에서 벗어나보려고 조금씩 노력 해 보고 있습니다.
Outro
JPA 의 역사는 ORM 이라는 기술이 발전 해 나가고 표준화가 이뤄지는 과정 그 자체라고도 볼 수 있습니다. 많은 개발자들에게 객체지향 그 자체의 중요성을 집중할 수 있도록 정립 해 가는 과정을 쭉 돌아보면서, 실제로 개발자들의 코드 작성 패러다임을 상당히 많이 바꿨다는 것을 체감했습니다.
다음 편에서는 JPA 의 내부로 깊게 파고들어가는 작동원리 편입니다. 기대해주세요~🙋♂️
댓글
댓글 서비스가 아직 연결되지 않았습니다.