Java/JPA 관련

JPA소개

jwKim96 2022. 5. 10. 21:10

이 글은 자바 ORM 표준 JPA 프로그래밍을 보고 작성한 글 입니다.
혹시 잘못된 내용이 있을 경우 편하게 댓글 달아주시면 감사하겠습니다.

SQL 중심적인 개발의 문제

SQL 중심적인 개발을 주로 하던 시절에는 CRUD 쿼리 개발을 중심으로 해야했습니다.

  • INSERT, UPDATE, SELECT, DELETE 등등의 쿼리 작성
  • 데이터가 담겨있는 자바 객체의 속성을, 쿼리 파라미터로 설정하는 로직
  • 쿼리 결과를 다시 자바 객체로 변환하는 로직 작성
    등등

비슷하면서 귀찮은 작업들이 정말 많았고, 테이블이 수정될 경우 모든 SQL을 수정해야 하는 경우도 있었습니다.
그래서 전체 개발시간 중 쿼리를 작성하거나, 쿼리와 관련된 자바 코드를 작성하는 시간이 차지하는 비중이 아주 많았습니다.

패러다임의 불일치

객체 설계를 하고 RDB를 하거나, RDB를 설계하고 객체 설계를 하려고 보면 해결하기 어렵거나, 불편한 부분이 있습니다.
그건 우리의 관심사인 자바 객체RDB 사이의 패러다임의 불일치 때문입니다.

1. 상속

image

위는 Item과 하위 Album, Movie, Book의 객체 관계, 테이블 관계를 나타낸 그림입니다.
그림으로만 보면 상속관계를 잘 이용한 객체 관계와 슈퍼타입 서브타입을 잘 이용한 테이블 구조 입니다.

여기서 개발자가 이 구조로 INSERT, SELECT 로직을 작성한다고 생각해봅시다.

INSERT 로직

  1. ITEM 테이블 INSERT쿼리 작성
  2. ALBUM, MOVIE, BOOK 테이블 INSERT쿼리 작성
  3. ITEM에 대한 로직과 하위 객체에 대한 로직을 함께 호출하도록 개발
  4. // ex) Album 추가하는 로직 class ItemService { ... public void saveAlbum(Album album) { itemRepository.saveItem(album); // ITEM 테이블 INSERT 쿼리 로직 itemRepository.saveAlbum(album); // ALBUM 테이블 INSERT 쿼리 로직 } ... }

개발자가 신경써서, 두 테이블 모두에 입력하도록 개발해야합니다.
그리고 여기서 만약 Item을 상속하는 하위 객체가 더 추가된다면, 이와 비슷한 코드를 또 작성해야 합니다.

SELECT 로직

  1. Item 하위 객체 각각에 대한 Join SQL 작성
  2. 쿼리 결과를 하위 객체에 주입하는 로직 각각 작성

분명 그림으로만 봤을때는 좋은 구조로 보였는데, 막상 개발을 해보면 불편함이 너무 많습니다.
그래서 DB에 저장할 객체는 상속 관계를 잘 쓰지 않았다고 합니다.

만약 이를 자바 컬렉션에 저장한다면 어떨까요?

// 컬렉션에 저장하는 로직
class ItemService {
    ...
    public void saveAlbum(Album album) {
        itemRepository.save(album);
    }
    ...
}

class ItemRepository {
    List<Item> items = new ArrayList<>();
    public void save(Item item) {
        items.add(item);
    }
}

자바 컬렉션에 저장하면, 다형성을 활용할 수 있기 때문에 각 하위 객체에 대한 로직을 작성할 필요가 없습니다.
또한 Item의 새로운 하위 객체가 추가되더라도, ItemRepository의 로직은 전혀 수정하지 않아도 되죠.

2. 연관관계

객체RDB는 각자 참조외래 키를 사용하여 연관관계를 표현합니다.

먼저, 객체의 참조는 단방향으로만 관계를 맺을 수 있습니다.

// 참조는 단방향으로만
class Team {
    Long id;
    String name;
}

class Member {
    Long id;
    String name;
    Team team;
}

위 예제의 Member를 보면, 자신이 포함된 Team을 참조하고 있습니다.
하지만, Team은 자신에게 소속된 Member가 누가 있는지 알 수 없습니다.

반면에 RDB에서는 외래 키로 양방향으로 관계가 형성됩니다.
(예제 간소화를 위해 create table 쿼리 생략)

/* 외래키 하나로 양방향으로

Team
- id, PK
- name

Member
- id, PK
- name
- team_id, FK

*/

/* id가 1인 Member가 소속된 Team 정보 */
SELECT T.* 
  FROM MEMBER M
  JOIN TEAM T
    ON M.team_id=T.id
   AND M.id=1

/* id가 1인 Team에 소속된 Member 정보 */
SELECT M.* 
  FROM TEAM T
  JOIN MEMBER M
    ON T.id=M.team_id
   AND T.id=1

불편하다...

그래서 선배 개발자들은 객체지향을 잘 활용하여 코드를 작성 하며, 귀찮은 RDB 관련 작업을 줄일 방법을 모색합니다.

JPA - Java Persistence API

JPA의 등장배경

들어가기에 앞서 잠시 등장배경을 살펴보겠습니다.

EJB - Enterprise Java Bean

예전에는 EJB라는 공통되는 업무로직을 제공하는 기술이 있었습니다.
EJB를 사용하면 개발자들이 작성해야하는 단순 반복 코드를 줄일 수 있었습니다.
하지만 단점도 많이 있었는데요.

  • 객체지향적이지 않음(상속해야하는 수많은 인터페이스)
  • 구조가 복잡함
  • EJB 컨테이너 안에서만 동작할 수 있음
  • 컨테이너 로드 속도가 느림
  • 테스트가 매우 어렵거나 불가능함
  • 이식성이 안좋음

이러한 단점들에 불만을 가졌던 한 개발자는 Hibernate라는 오픈소스 프로젝트를 시작하게 됩니다.

Hibernate

개빈 킹(Gavin King)이라는 개발자가 EJB 보다 더 편리하게 사용할 수 있는 Hibernate라는 기술을 개발하기 시작했습니다.
이 오픈소스 프로젝트는 점차 많은 개발자들의 관심을 받으며 많은 기능이 추가되고, 많은 개발자들이 이를 사용하게 됩니다.
그래서 자바 표준 진영에서는 개빈 킹과 함께 하이버네이트의 구조를 많이 차용하여 JPA를 만들게 됩니다.

JPA란?

image

JPA는 자바에서의 ORM 기술 표준으로, JPA는 인터페이스의 모음 입니다.
특히 패러다임의 불일치를 해결하여 객체 설계와 RDB 설계의 차이로 발생하는 귀찮은 일들과 단순 CRUD를 대신 수행합니다.
그리고 JPA의 구현체는 Hibernate, EclipseLink, DataNucleus 등등 여러가지가 있는데요.
이 중에서 Hibernate는 초기에 시장을 선점하였고, Java ORM 분야의 80% 이상을 점유하게 됩니다.

JPA를 사용해야하는 이유

1. SQL 중심 개발 → 객체 중심 개발

반복적이고 단순한 SQL과 CRUD 로직 개발을 자동화 해주어 객체 중심으로 개발 가능합니다.
그리고 객체RDB의 패러다임의 불일치를 해결하여, 객체 중심적으로 설계 가능합니다.

2. 생산성

아래와 같이 객체를 Collection에 저장하여 사용하는 것 처럼 CRUD를 할 수 있어, 생산성이 기하급수적으로 높아집니다.

  • 저장 : em.persist(member);
  • 조회 : Member member = em.find(member);
  • 수정 : member.changeName("JPA"); // 객체를 변경하면 DB에 반영이 된다.
  • 삭제 : em.remove(member);

em : EntityManager 클래스로 객체 정보를 읽어서 DB에 쿼리를 날리고 결과를 반환하는 역할을 함
사실 위 설명 정확한 설명은 아니지만, 여기서는 간단하게 이렇게 이해하고 넘어가시면 됩니다.

3. 유지보수

유지보수 측면에서도, 장점이 아주 많은데요.
만약 필드 하나가 추가된다면, 기존에 SQL 중심 개발 방식에서는 많은 작업들이 필요했습니다.

  • 객체에 속성 추가
  • SQL 모두 수정
  • INSERT 쿼리 파라미터 추가
  • SELECT 쿼리 결과 객체에 주입하는 로직에 해당 컬럼 추가
  • 등등...

하지만, JPA를 사용한다면 한가지 작업만 하면 됩니다.

  • 객체에 속성 추가

4. 패러다임의 불일치 해결

잠시 위에서 언급했었던, 패러다임의 불일치 문제의 상속과, 연관관계 문제도 JPA가 완벽히 해결합니다.

1. 상속

image

위와 같은 상속관계가 있을 때, JPA가 귀찮은 모든 작업을 해주기 때문에 개발자가 할 일이 엄청나게 줄어들게 됩니다.

// JPA가 대신해주는 일들

/* 저장 */
// 개발자가 할일
Album album = new Album();
em.persist(album);

// JPA가 처리하는 일
- album의 클래스 Album의 정보를 읽어 INSERT SQL을 생성
    - ITEM 테이블 INSERT SQL 생성
     - ALBUM 테이블 INSERT SQL 생성
- 생성된 SQL DB로 전송

/* 조회 */
// 개발자가 할일
Album album = em.find(Album.class, albumId);

// JPA가 처리하는 일
- Album 클래스 정보를 읽어 SELECT SQL을 생성
    - ITEM, ALBUM 테이블을 JOIN 하여 SELECT SQL 생성
- 생성된 SQL DB로 전송
- 반환된 결과 Album 객체에 주입
- Album 객체 반환

위 처럼 귀찮은 작업들을 모두 JPA가 처리하여 객체로 반환해주기 때문에, 개발자는 중요한 비즈니스 로직 개발에 집중할 수 있습니다.

5. 성능 최적화

  1. 1차 캐시와 동일성 보장
  2. 트랜잭션을 지원하는 쓰기 지연
    1. INSERT 시
      • 트랜잭션 커밋 전까지 INSERT SQL을 모음
      • JDBC BATCH SQL 기능을 사용하여 한번에 SQL 전송
    2. UPDATE 시
      • UPDATE, DELETE로 인한 ROW LOCK 시간 최소화(커밋 시 한번에 실행하기 때문에)
      • 트랜잭션 커밋 시 UPDATE, DELETE SQL 실행하고, 바로 커밋함
  3. 지연 로딩 & 즉시 로딩
    • 객체가 실제 사용될 때 로딩(SELECT)
    • JOIN SQL로 연관된 객체를 미리 조회
    // 지연 로딩(프록시 기술을 이용함) 
    Member member = em.find(Member.class, id); // Team 연관 관계가 있어도, 일단 Member만 조회 
    Team team = member.getTeam(); // Team을 상속한 Proxy 객체를 반환 
    String teamName = team.getName(); // DB에서 Team 정보를 가져와서 값 반환(최초 1번만)
    
    // 즉시 로딩 
    Member member = em.find(Member.class, id); // Member, Team JOIN 하여 조회 
    Team team = member.getTeam(); // Team 객체 반환 
    String teamName = team.getName(); // 객체에서 정보를 반환

지연 로딩즉시 로딩은 이런 차이가 있습니다.
JPA가 정말 놀라운 것은, 어노테이션 설정 하나로 지연 로딩, 즉시 로딩을 전환할 수 있는 점 입니다.

그런데 언제 지연 로딩 또는 즉시 로딩을 사용해야 하는지 헷갈리는데, 영한님이 가이드라인 정해주셨습니다.

  1. 일단 개발할 때는 지연 로딩으로 쭉~ 개발 하기
  2. 테스트 혹은 서비스하며 슬로우 쿼리 확인하고, 최적화가 필요하면 즉시 로딩으로 바꾸는 것 검토

마무리 하며

JPA, Hibernate와 같은 ORM 기술들을 잘 사용하려면, 객체RDB에 대한 지식이 모두 필요하다고 하는데요.
그리고 한쪽에 치중된 설계나 개발을 지양하고, 둘 사이에서 적절한 균형을 잡는것이 중요하다고 합니다.

읽어주셔서 감사합니다.
오탈자, 혹은 잘못된 내용이 있을 경우 댓글로 알려주시면 감사하겠습니다!