본문 바로가기
IT 공부/자바와 웹 애플리케이션

[JPA] - 자바 진영의 데이터베이스 처리 기술 발전

by exdus3156 2024. 1. 9.

1. JPA와 Mybatis의 차이

1) JDBC의 문제는 무엇인가

우선 자바 애플리케이션이 내부적으로 데이터베이스와 상호 작용이 필요할 때, 사용할 수 있는 기술은 크게 보면 JDBC, MyBatis, JPA(with QueryDSL) 등이 있다는 점을 기억하자. 그리고 MyBatis와 JPA는 모두 JDBC의 단점을 해소하기 위해 사용되는 기술이라는 점도!

결국 JPA와 MyBatis를 이해하기 위해서는 JDBC의 단점이 무엇인지 아는 것으로 시작해야 한다.

 

자바 애플리케이션은, 특히 백엔드라면, 애플리케이션 로직에 따라 DB를 활용해야 하는 순간이 반드시 오기 마련이다. 그때 데이터베이스 상호작용의 상세 사항을 숨기고(정보 은닉), 적절한 캡슐화를 통해 자바 애플리케이션 입장에서는 자바 인터페이스에만 의존할 수 있도록 만든 것이 영속(persistance) 계층이다.

그렇다면 그 내부 구현을 어떻게 할 것인가?

JDBC가 있다. JDBC의 문제는 인터페이스 구현을 위해 내부적으로 SQL문법을 사용한다는 점, 그리고 PreparedStatement, ResultSet 등 복잡한 코드를 사용해 DB에서 처리한 결과 데이터를 개발자가 일일이 코딩해서 자바 애플리케이션이 내부적으로 받아들이는 객체(vo)로 변환해야 한다는 점이다.

이것은 코딩의 생산성 문제로 이어진다. 수많은 SQL 문법을 코드 내부로 적어야 하는데다, 값을 일일이 SQL 문법을 사용해 문자열로 입력해줘야 하고(PreparedStatement), 처리 결과를 다시 값 객체로 일일이 매핑해줘야 하기 때문이다.

 

2) MyBatis의 해결

MyBatis는 데이터 계층의 인터페이스(MyBatis에서는 Mapper라고 부른다)를 개발자가 구현하는 대신 개발자가 준 SQL문을 읽어들여 자동으로 그 구현체를 생성해주는 기술이다.

복잡하게 생각할 것이 하나도 없다. 단순하게 보자. MyBatis는 데이터 처리를 담당하는 Persistence 인터페이스를 SQL문을 보고 스스로 구현해주는 기능이다. 딱 이것이 본질이다.

코딩 생산성이 극도로 좋아지는데, PreparedStatement에서 일일이 값을 매핑했던 것을 단순하게 #{}과 같은 키워드로 자동 처리하고, 특히 ResultSet으로 데이터를 읽어 객체로 매핑할 필요 없이, resultType이 무엇인지 명시하는 것만으로도 자동으로 결과를 VO 객체에 실어준다.

인터페이스와 mapper.xml를 매핑하여 인터페이스의 구현체를 만드는 작업은 MyBatis의 SessionFactory 객체를 통해 만들 수 있다. 스프링 프로젝트에서는 SessionFactoryBean을 컨텍스트에 등록하기만 해도 Mapper 구현체가 자동 완성되어 스프링 빈으로 등록므로 사용 로직을 구성할 필요조차 없이 Mapper 객체를 주입받으면 끝이다.

물론 이 인터페이스가 데이터베이스와 연계해서 쿼리를 처리하려면 결국은 개발자가 SQL문을 작성하고, 이것을 인터페이스의 메소드와 일일이 매핑해야 한다. 하지만 JDBC가 SQL 문법을 내부로 작성하는데 비해, MyBatis는 이것을 xml 파일로 분리함으로써 자바 코드가 SQL을 쓰는 것에서 벗어난다. 

 

3) MyBatis의 문제와 JPA 표준의 해결

(※ 오해해서는 안 되는게, MyBatis와 JPA는 선택사항에 가깝지, JPA가 MyBatis의 업그레이드라는 말이 아니다!)

MyBatis는 현재도 많이 활용되는 라이브러리다. 그러나 굳이 단점을 따지자면, SQL문을 직접 작성한다는 것은 결국 특정 DB에 종속적인 코드를 낳는다는 점이며, 특히나 DB가 변하지 않는다고 한들 DB 스키마와 같은 테이블 설계 구조가 변경될 때에도 SQL과 인터페이스를 통째로 수정해야 한다는 점이다.

게다가 대부분의 코드가 사실상 간단한 CRUD에 불과하다는 점을 생각해보면 DB의 변경의 여파로 엄청난 양의 코드를 수정해야 하는데, 이 작업들이 사실상 반복 노동에 가깝다는 것을 알 수 있다. (이런 단순 코드를 boilerplate code라고 부른다)

JPA는 객체 자체를 완전히 DB에 매핑시켜 버린다. 이것은 자바 객체(클래스) 자체를 데이터로 바라보는 관점에 기반한다. 이것을 엔티티(entity)라고 부르는데, JPA는 엔티티와 데이터베이스의 데이터가 같다고 가정하고 이 둘을 동기화시키는 기술이다. 즉, 데이터베이스의 관점을 자바 애플리케이션 내부로 들고 오면서도, 해당 데이터베이스와 관련된 기술 세부 사항에서 벗어나는 것이다.

결론적으로 JPA의 궁극의 핵심은 @엔티티 객체가 데이터베이스와 자동 동기화된다는 것이다.

특정 DB에 종속되지 않도록 JPA는 자동으로 엔티티 객체를 토대로 DB와 동기화를 해준다. 이렇게 하는 것의 설계 상 이점은, 자바 애플리케이션이 자신의 입장에서 DB에 종속되지 않고 자유롭게 데이터 구조를 다룰 수 있다는 점이다. 

또한 간단한 CRUD 메소드를 자동으로 매핑해주고 구현해준다는 점에서 코드 생산성이 좋아진다. 대부분의 DB 작업이 CRUD라는 것을 생각하면 엄청난 생산성의 발전이다.

물론 간단한 CRUD가 그렇다는 것이다. 복잡한 쿼리 작업의 경우는 직접 JPQL과 같은 문법을 사용해 스스로 매핑해야 하며 이는 MyBatis와 아주 큰 차이는 아니다. 그러나 MyBatis보다 JPA와 같은 ORM 패러다임이 조금 더 최신 패러다임이다.

 

 


2. JPA 관련 기술들 정리

아주 간단하게나마 JPA와 관련된 기술들의 설정 및 목적과, 의도를 정리해보려고 한다.

 

1) 스프링부트의 JPA 설정

먼저 설정 이야기를 해보자. 사실 설정과 관련해서는 스프링부트에서 건드릴 것은 거의 없다. 스프링부트는 내부적으로 Hiberbate라는 JPA 구현체를 사용한다. 따로 JPA 라이브러리를 설정할 필요가 전혀 없다. 단지 개발자가 사용하는 데이터베이스의 JDBC Driver만 있으면 된다.

스프링부트는 내부적으로 JPA를 가지고 실행하지만, 만약 JDBC Driver가 무엇이며 데이터베이스 서버 URL과 사용자 아이디와 패스워드 정보를 알려주지 않으면 부팅에 실패한다.

정보는 위와 같이 클래스패스에 있는 application.properties에 정보를 등록하기만 하면 충분하다. 그리고 필수적인 정보 이외에도 추가적인 정보를 통해 JPA 기술이 수행되는 방식을 조정할 수 있다.

예를 들어, ddl-auto는 스프링부트를 실행할 때 엔티티의 설계에 따라 매번 DB 서버의 테이블 자체를 생성하거나 수정할지 결정할 수 있다. format_sql은 실행한 sql 문법을 조금 더 깔끔하게 출력하는 옵션이다. show-sql은 말 그대로 실행한 sql문법을 콘솔에 출력하라는 의미다.

엔티티를 설계하고 다음으로 해야 할 것은 이 엔티티를 식별하고 DB와 동기화하라는 어노테이션을 붙이는 것이다. (@EnableJpaAuditing)  JPA 설정은 이게 전부다.

 

 

2) JpaRepository 인터페이스

스프링은 MyBatis와 마찬가지의 기능을 제공해준다. JpaRepository 인터페이스는 엔티티와 관련된 여러 기본적인 처리들을 구현해서 그 구현체를 빈에 등록한다. 

역시나 스프링부트에서는 사용 방법이 극도로 간단하다.

그저 JpaRepository 인터페이스를 상속한 클래스를 선언만해도 스프링부트가 그것을 찾아서 구현체를 빈에 등록해준다. 빈에 등록된 객체이므로 @Autowired와 같은 어노테이션으로 객체를 찾아서 쓰면 끝이다.

사용은 매우 간단하다. 물론 마이바티스로 매퍼 인터페이스를 통해 사용하는 것과 다를 것이 없다. 하지만 마이바티스보다 스프링 JPA가 훌륭한 점은, CRUD나 paging과 같이 자주 사용되는 메소드를 미리 마련해줬다는 점에 있다.

(※ 참고로 JpaRepository는 스프링의 기술이다. JPA 표준은 아니다.)

 

3) 원하는 메소드가 JpaRepository에 없을 때는 쿼리 메소드

스프링 JpaRepository 인터페이스가 마련해준 메소드들은 기본적인 것들이다. 물론 이것으로도 충분할 때도 많겠지만, 조금 더 세밀한 sql 처리나 커스터마이징이 필요한 경우에는 곤란해진다.

스프링 JPA는 쿼리 메소드 기능을 지원해준다. 이것은 규칙에 따라 사용자가 메소드 이름을 생성하면 자동으로 해당 쿼리를 사용할 수 있는 메소드가 만들어지는 기능이다. 재밌는 기능이다.

문제는 복잡한 쿼리는 메소드가 너무 길어지기 때문에 그닥이라는 점이다. 하지만 JpaRepository가 제공하지 않는 기능 중에, 간단한 속성 이름만을 사용해서 findByName() 과 같은 메소드를 만들 때는 아주 유용하다.

 

4) 마찬가지 이유로 사용되는 @Query와 JPQL

복잡한 쿼리가 사용되는 메소드를 생성하고 싶을 때 쿼리메소드를 사용해도 되지만, 그 대신 사용할 수 있는 것이 @Query 어노테이션으로 직접 SQL 문을 작성하는 것이다.

여기서 SQL 문법은 특정 데이터베이스를 피하기 위해 추상화된 JPQL이라는 언어를 사용한다. 만약 특정 DB 문법을 사용하고 싶다면 아래와 같이 쓴다.

그러나 이 방법은 마치 MyBatis이 어노테이션을 활용하는 방식과 전혀 다를 것이 없다 그래서 단점도 똑같은데, SQL 문법이 코드 내부로 들어간다는 점이다. 게다가 어쩌면 MyBatis보다도 단점이 될 수 있다. 문자열로 SQL 문법이 고정되어 버리므로 동적 쿼리를 작성할 수 없기 때문이다.

 

5) Querydsl

Querydsl의 근본 목표는 바로 JPQL을 자바 객체지향 코드로 만들겠다는 것이다. 물론 JPQL 언어 자체가 JPA 표준의 일부이긴 하지만, 그렇다고 Querydsl이 JPA인 것은 아니다. JPA와는 별개의 기술이다. 단지 JPA를 사용할 때 JPQL과 관련한 문제를 해소하기 위해 등장했기 때문에 JPA와 함께 사용된다. 실무적으로 Querydsl은 JPA 인터페이스 뒤에서 'JPA의 아쉬운 부분을 보좌'하는 역할이다.

(※ Querydsl은 설정이 워낙 까다롭기로 소문난 기술이다. 사용 절차도 사실 꽤 까다롭다. 여기서는 QueryDsl 기술의 근본적인 목표만 포스팅했다. 설정과 관련된 내용은 포스팅 자체를 따로 올렸다. → 링크)

그런데 Querydsl이 왜 자바 코드로 SQL(JPQL)을 만드는 것이며, 왜 이것이 효과가 있다고 말하는 것일까?

JPA의 문제점은 복잡한 쿼리를 사용하려면 어노테이션으로 JPQL을 적어야 한다는 점과 함께, 그것을 문자열로 사용하는 만큼 동적 쿼리 작업을 수행하기 힘들다는 점에 있다.

사실 해당 쿼리가 언제나 그렇게 사용된다면야 문제는 없다. 하지만 애플리케이션을 만들다보면 입력 값에 따라 쿼리 자체를 바꿔야 할 때가 있다. 예를 들어, 특정 키워드가 입력되면 검색을 해야 하는 경우를 생각해보자. 만약 키워드가 입력되지 않는다면 where와 같은 SQL문을 쓰지 말아야 할 것이다. (링크)

또한 복잡한 쿼리를 문자열로 적는 것의 새로운 문제는 디버깅이 힘들다는 점이다.

모든 가능성을 고정된 문자열로 커버하기 위해서는 모든 경우의 수에 대한 모든 메소드와 SQL이 필요하다. 그러나 동적 쿼리를 작성해 SQL을 프로그래밍할 수 있다면 얘기는 달라진다.