Spring

JPA를 왜 쓰는가?

potatoo 2024. 9. 21. 15:35
728x90

기존의 SQL, JDBC API를 사용하면서 애플리케이션의 비즈니스 로직보다 SQL과 JDBC API를 작성하는 데 더 많은 시간을 소비했었다.

이게 조금 나아지면서 JdbcTemplate같은 SQL매퍼를 사용해서 코드 양을 줄였다.

 

그렇지만 여전히 CRUD는 작성해야 한다.

이건 너무 비생산적인 반복이라고 한다.

 

영한님이 생각한 것은 객체지향의 장점을 살린 객체모델링을 적용하는 것이었다.

하지만 객체 모델링은 세밀해질 수록 저장과 조회가 어려웠다.

 

결국 데이터 중심 모델로 변해간다는 결과가 나왔다고 한다.

 

그래서 이런 객체와 관계형 데이터베이스(RDB) 간의 차이를 중간에서 해결하기 위해 ORM(object relational mapping)이 찾았다고 한다.

 

나도 이런 문제해결적 공부를 할 수 있을까 시작부터 막막한 감이 없지않아 있다...

 

JPA는 자바 진영의 ORM 기술 표준어이다.

 

JPA는 CRUD를 알아서 처리해주고, 객체와 RDB의 간의 차이도 해결했다.

개발자가 직접 SQL을 직접 작성하지 않아도 된다는게 얼마나 좋은지!! 매번 프로젝트를 할때마다 느끼는 것 같다ㅎㅎ

또한 조회된 결과를 객체로 매핑하는 작업도 대부분 자동으로 처리해서 데이터를 저장하는 작업의 코드도 줄었다고 한다.

 

객체중심으로 하기 때문에 유지보수도 좋고~,생산성도 좋다고 한다.

확실히 Jdbc를 쓸때보단 좋다는걸 나도 느꼈다.

그리고 DB가 바뀌어도 쿼리를 거의 작성하지 않기 때문에 크게 코드 수정을 할 필요가 없어서 확실히 좋다.

 

"개발자는 SQL Mapper가 아니다."  이 말이 얼마나 와닿는지ㄷㄷ

 

1. SQL을 직접 다룰 때의 문제

Jdbc를 사용해서 회원을 CRUD하는 기능을 개발한다고 해보자.

순서는 이렇다.

R : 1. 회원 조회용 SQL을 작성한다.

      2. JDBC API를 이용해서 SQL을 실행한다.

      3. 조회 결과를 Member 객체로 매핑한다.

C : 1. 회원 등록용 SQL을 작성한다.

      2. 회원 객체의 값을 꺼내서 등록 SQL에 전달한다.

      3. JDBC API를 사용해서 SQL을 실행한다.

이것만 봐도 벌써 이것저것 번거롭게 코드가 길어질게 예상이간다...

 

객체를 DB에 저장하려고 한다면 직접 저장할 수 없기 때문에 개발자가 SQL과 JDBC API를 사용해서 변환 작업을 직접 해야한다.

그런데 이런 CRUD 작업을 각 테이블마다 해주어야 할텐데 이런 테이블이 100개가 넘어간다면?

코드의 길이와 그걸 작성하는 개발자의 피로도는 어떨까..?

상상도 안된다.. 너무 길자나...

DAO(data access object) 계층을 개발하는 일은 반복 작업이다.

 

만약 기능 개발이 끝난 상태에서 기능 추가를 요청한다면 어떻게 될까?

연락처를 추가해서 등록 코드를 변경하는 순서는 아래와 같다.

C : 1. 회원 객체에 tel 필드를 추가

      2. INSERT SQL을 수정

      3. 회원 객체의 연락처 값을 꺼내서 등록 SQL에 전달

      4. SQL과 JDBC API를 수정

      5. 테스트

다음은 조회 기능이다.

R : 1. SQL 수정

      2. Member 객체에 추가로 매핑

수정도 이곳저곳 많이 고쳐야 한다는걸 느낄 수 있다.

 

연관 객체를 추가하는 경우는 어떨까?

회원이 어떤 한 팀에 필수로 소속되어야 하는 요구사항이 추가됐을 때,

Member에 team 필드가 추가되어 있다.

회원을 조회하면 팀 이름도 출력하는 기능을 만든다면, member.getTeam().getTeamName()을 사용한다.

이때 매번 member.getTeam()의 값이 null인 문제가 생겼고, MemberDAO에 추가된 findWithTeam()이라는 메서드를 확인하고,

DAO에서 SQL을 확인하고 나서야 문제를 파악하고 해결한다.

이 과정에서의 문제는 Member객체가 Team객체를 사용할 수 있을지 없을지는 전적으로 사용하는 SQL에 의존한다는 것

DAO로 SQL을 숨겨도 결국은 SQL을 확인해야 한다는 점이다.

 

엔티티란 비즈니스 요구사항을 모델링한 객체를 말한다.

 

지금 같은 DAO의 상황에서는 물리적으로는 SQL과 JDBC API를 DAO 계층에 숨기는데 성공했을지는 몰라도 논리적으로는 엔티티와 아주 강한 의존 관계를 가진다고 한다.

 

즉, SQL을 직접 다룰때의 문제는

1. 반복적인 작업이 많다.

2. SQL 의존적 개발이 이루어진다.

    - 진정한 의미의 계층 분할이 어렵다.

    - 엔티티의 신뢰성이 없다.

    - SQL에 의존적인 개발 피하기 어렵다.

 

JPA는 이런 문제를 어떻게 해결하는가?

1. 저장

   - persist() : 객체를 DB에 저장. 객체와 매핑정보를 보고 적절한 INSERT SQL을 생성해서 DB에 전달한다.

2. 조회

   - find() : 객체 하나를 DB에서 조회. 객체와 매핑정보를 보고 적절한 SELECT SQL을 생성해서 DB에 전달한다. 그 후 객체를 생성해서                    반환

3. 수정

   - 별도로 수정 메소드를 제공하지 않는다. 대신에 객체를 조회해서 값을 변경만 하면 트랜잭션을 커밋할 때 DB에 적절한 UPDATE SQL        이 전달된다.

4. 연관 객체 조회

 

2. 패러다임의 불일치

객체와 RDB의 패러다임 불일치 문제란?

지향하는 목적이 서로 다른 둘의 기능과 표현 방법. 이것을 패러다임 불일치 문제라고 한다.

RDB는 데이터 중심으로 구조화되어 있고, 집합적인 사고를 요구한다. 그리고 추상화, 상속, 다형성 같은 개념이 없다.

그래서 객체 구조를 테이블 구조에 저장하는 데는 한계가 있다.

 

1) 상속 문제

객체는 상속이라는 기능을 가지고 있다. 하지만 테이블은 상속이라는 기능이 없다.

슈퍼 타입과 서브 타입을 사용하면 그나마 상속과 유사한 형태로 테이블을 설계할 수 있다.

DTYPE 컬럼을 사용해서 어떤 자식 테이블과 관계가 있는지 정의하는 방식이다.

 

이런 방식을 사용하는 것은 부모 테이블과 자식 테이블을 JOIN하여서 조회를 한 다음 객체를 판단하여 만들어야한다는 비용 소모가 발생한다.

 

이런 문제를 JPA가 해결해준다.

 

Item을 상속한 Album 객체를 저장할 때, persist() 메소드를 사용해서 저장하면 된다.

JPA는 SQL을 실행해서 객체를 ITEM, ALBUM 두 테이블에 나누어 저장한다.

 

Album 객체를 조회한다면, find() 메소드를 사용해서 객체를 조회하면 된다.

JPA는 ITEM과 ALBUM 두 테이블을 조인해서 필요한 데이터를 조회하고 그 결과를 반환한다.

 

2) 연과 관계

객체는 참조를 통해 다른 객체와 연관관계를 가지고 참조에 접근해서 연관된 객체를 조회한다.

테이블은 외래 키를 사용해서 다른 테이블과 연관관계를 가지고 조인을 사용해서 연관된 테이블을 조회한다.

 

이 두 방식의 차이는 패러다임 불일치 문제로 객체지향 모델링을 거의 포기하게 만들 정도로 극복하기 어렵다고 한다.

 

추가로 객체는 참조가 있는 방향으로만 조회할 수 있다. member.getTeam()은 가능해도, team.getMember()는 참조가 없어서 불가능한 경우가 있다.

하지만 테이블은 외래키로 JOIN을 하기 때문에 양방향으로 가능하다.

 

객체 모델은 외래키는 필요없고, 참조만 있으면 된다. 반면에 테이블은 외래키만 있으면 되고, 참조는 필요 없다.

결국 개발자가 변환 해줘야한다.

이런 번거로운 작업을 JPA가 해결해준다.

 

member.setTeam(team);
jpa.persist(member);

 

개발자는 회원과 팀의 관계를 설정하고 회원 객체를 저장하면 된다.

그러면 JPA가 team의 참조를 외래키로 변환해서 적절한 INSERT SQL을 DB에 전달한다.

조회하는 것도 마찬가지로 해준다.

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

 

위의 부분은 어느정도 SQL을 직접 다뤄도 코드 작성을 하면 극복할 수 있는 문제라고 한다.

 

하지만 진짜 문제는 객체 그래프 탐색이다.

 

3) 객체 그래프 탐색

객체에서 회원이 속한 팀을 조회할 때 참조로 연관팀을 찾는데, 이것을 의미한다.

SQL을 직접 다룬다면 처음 실행하는 SQL에 따라 객체 그래프를 어디까지 탐색 가능한지 정해진다.

이것이 가장 큰 문제이다.

member가 team, order와 연관되어있다면 join을 통해서는 둘 중 하나는 탐색할 수 없다.

왜냐면 비즈니스 로직에 따라 사용하는 객체 그래프가 다른데 언제 끊어질지 모를 객체 그래프를 함부로 탐색할 수는 없기 때문이다.

 

class MemberService {
	...
    public void process() {
    
    
    	Member member = memberDAO.find(memberId);
        member.getTeam();
        member.getOrder().getDelivery();
    }
}

위 코드만을 보고서는 객체 그래프 탐색이 어디까지 가능한지 파악할 수 없다. 결국 DAO 부분의 코드를 까서 SQL을 직접 확인 해야한다.

이것 또한 엔티티가 SQL에 의존적인 논리적 종속으로 발생하는 문제이다.

결국, 회원을 조회하는 메소드를 상황에 따라 여러개로 만들어서 사용해야 한다.

memberDAO.getMember();
memberDAO.getMemberWithTeam();
memberDAO.getMemberWithOrderWithDelivery();

 

JPA는 이런 문제를 지연로딩으로 해결했다.

지연로딩이란? 실제 객체를 사용하는 시점까지 DB 조회를 미루는 방식이다.

연관된 객체를 사용하는 시점에 적절한 SELECT SQL을 실행하는 방식이다.

이를 통해 연관된 객체를 신뢰하고 마음껏 조회할 수 있다.

JPA는 지연로딩을 투명(transparent)하게 처리한다. 메소드 구현 부분에서 JPA와 관련된 어떤 코드도 직접 사용하지 않는다.

 

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

Order order = member.getOrder();
order.getOrderDate();; // Order 사용 시점에 SELECT ORDER SQL

Member 사용할 때마다 Order를 함께 사용하면 한 테이블씩 조회하는 것보다는 Member를 조회하는 시점에 SQL 조인으로 함께 조회하는 것이 효과적이라는데,

JPA는 연관된 객체를 즉시 함께 조회할지 아니면 실제 사용되는 시점에 지연해서 조회할지를 간단한 설정으로 정의한다.

 

4) 비교

테이블에서의 비교는 기본키를 통한 로우를 구분한다.

반면에 객체는 동등성, 동일성 비교 두 가지가 존재한다.

- 동일성 비교==을 통한 인스턴스의 주소값 비교이다.

- 동등성 비교equals()를 통한 객체 내부 값을 비교한다.

 

같은 기본키 값을 가진 member를 조회하여 비교했을 때, 동일성 비교에서는 false가 나온다. 객체로 봤을 때는 둘은 다른 인스턴스이기 때문이다.

이런 패러다임 불일치 문제는 같은 로우를 조회할 때마다 같은 인스턴스를 반환하도록 구현하는 것은 어렵고, 트랙잭션이 동시에 실행되는 상황까지 고민하면 문제는 더 커진다고 한다.

 

JPA는 이 문제도 해결했다.ㄷㄷ

find()를 사용해서 같은 트랜잭션일 때 같은 객체가 조회되는 것을 보장한다.

 

결론, 객체 모델링은 할수록 패러다임 불일치 문제는 더욱 커지고, 개발자가 소모해야하는 비용도 점점 커진다. 그래서 결국은 데이터 중심 모델로 변해가는 문제가 발생하는데 이런 문제를 해결한 것이 JPA다. JPA를 통해서 패러다임 불일치 문제를 해결하고, 정교한 객체 모델링을 유지하게 되었다. 

728x90