[SpringBoot] 자바 ORM 표준 JPA 프로그래밍 [1장]

한동근·2024년 2월 27일
1

SpringBoot

목록 보기
8/12

1장. JPA 소개

1.1. SQL을 직접 다룰 때 발생하는 문제점

1.1.1. 반복, 반복, 그리고 반복

자바와 관계형 데이터베이스를 사용해서 회원 관리 기능(CRUD)을 개발해보자.

# 회원 객체
public class Memer {

	private String memberId;
    private String name;
	...
    }
# 회원 DAO(데이터 접근 객체)
public class MemberDAO{
	public Member find(String memberId){...}
    }

MemberDAO의 find()메소드를 완성하기 위해서는 다음과 같은 순서로 개발을 진행하게 된다.

  1. 회원 조회용 SQL 작성.

  2. JDBC API 사용해서 SQL 실행.

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

회원 등록, 수정, 삭제하는 기능을 추가하기 위해서 이런 일을 반복해야 한다.

데이터베이스는 객체 구조와는 다른 데이터 중심을 가지므로 개발자가 SQL과 JDBC API를 사용해 변환 작업을 직접 해주어야 한다.

여기서 문제점은 객체를 데이터베이스에 CRUD하려면 매우 많은 SQL과 JDBC API를 코딩해야 한다. 한마디로 반복, 반복, 또 반복이다.

1.1.2. SQL에 의존적인 개발

회원 객체를 관리하는 MemberDAO를 완성하였는데, 회원의 연락처도 함께 저장해달라는 요구사항이 추가되었다. 어떻게 해야할까?

  1. 등록 코드 변경
    회원 객체에 연락처 필드를 추가하고, INSERT SQL 수정한다. 그리고 회원 객체의 연락처 값을 꺼내 등록 SQL에 전달해준다.

  2. 조회 코드 변경
    회원 조회용 SQL에 연락처 컬럼을 추가해주고, 조회 결과를 회원 객체에 추가로 매핑한다.

  3. 수정 코드 변경
    연락처가 수정되지 않는 버그가 발생했다. MemberDAO를 열어 UPDATE SQL을 확인해야 한다.

  4. 연관된 객체(요구사항 추가)
    회원은 어떤 한 팀에 필수로 소속되어야 한다는 요구사항이 추가되었다. 이 때도 역시 MemberDAO를 열어 확인해보고 SQL을 수정해야 한다.

여기서 알 수 있는 SQL의 문제점은 데이터 접근 계층(DAO)을 사용해 SQL을 숨겨도 DAO를 열어서 어떤 SQL이 실행되는지 확인해야 한다는 것이다.

또 SQL에 의존하는 상황에서는 개발자들이 엔티티를 신뢰할 수 없다.

그리고 물리적으로는 SQL과 JDBC API를 데이터 접근 계층에 숨겼을지라도 논리적으로는 엔티티와 아주 강한 의존관계를 가지고 있다. 항상 DAO를 열어서 어떤 SQL이 실행되는지 확인해야 한다. 이는 진정한 의미의 계층분할이 아니다.

1.1.3. JPA와 문제 해결

JPA를 사용하면 객체를 DB에 저장하고 관리할 때, JPA가 제공하는 API를 사용하면 된다. JPA는 개발자 대신 SQL을 생성해 DB에 전달한다. JPA의 CRUD API를 간단히 알아보자.

  1. 저장 기능
    persist() 메소드를 호출하면 JPA가 객체와 매핑정보를 보고 INSERT SQL을 생성해 DB에 전달한다.

  2. 조회 기능
    find()메소드를 호출하면 객체와 매핑정보를 보고 SELECT SQL을 생성해서 DB에 전달하고 결과로 객체를 생성해서 반환한다.

  3. 수정 기능
    JPA는 별도의 수정 메소드를 제공하지 않지만 대신 객체를 조회해서 값을 변경하기만 하면 트랜잭션 커밋 시 DB에 UPDATE SQL이 전달된다.

  4. 연관된 객체 조회
    JPA는 연관된 객체를 사용하는 시점에 SELECT SQL을 실행한다.

1.2. 패러다임의 불일치

객체와 관계형 데이터베이스는 목적이 서로 다르므로 둘의 기능과 표현 방법도 다르다. 이것을 패러다임 불일치 문제라 한다. 그래서 객체 구조를 테이블 구조에 저장하는 데는 한계가 있다. 이러한 문제를 개발자가 중간에서 해결하는데는 많은 시간이 소비된다.

패러다임의 불일치로 인해 발생하는 문제를 살펴보고 JPA를 통한 해결방법도 알아보자.

1.2.1. 상속

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

그나마 슈퍼타입 서브타입 관계를 사용하면 객체 상속과 유사하게 설계할 수 있다. DTYPE의 값이 MOVIE이면 영화 테이블과 관계가 있다.

# 객체 모델 코드
abstract class Item {
	Long id;
	String name; 
    int price;
    }

class Album extends Item {
	String artist;
}
class Movie extends Item {
	String director;
	String actor;
}
class Book extends Item {
	String author;
	String isbn;
}

Album, Movie 객체를 저장하려면 객체를 분해해서 SQL을 작성해야 한다.

JDBC API를 사용해서 코드를 완성하려면 부모 객체에서 부모 데이터만 꺼내 ITEM용 INSERT SQL을 작성해야 하고 자식 객체에서 자식 데이터만 꺼내서 ALBUM용 INSERT SQL을 작성해야 하는데 작성해야 할 코드량이 매우 많다.

이런 과정이 패러다임의 불일치를 해결하기 위해 소모되어야 한다.

JPA는 상속과 관련된 패러다임의 불일치 문제를 해결해준다. JPA를 사용해서 Item을 상속한 Album 객체를 저장해보자.

  1. persist() 메소드 실행

  2. JPA는 다음 SQL을 실행해 객체를 두 테이블에 나누어 저장.
    INSERT INTO ITEM...
    INSERT INTO ALBUM...

1.2.2. 연관관계

객체는 참조를 사용해 다른 객체와 연관관계를 가지고 참조에 접근해 연관된 객체를 조회하지만 테이블은 외래키를 사용해 다른 테이블과 연관관계를 가지고 조인을 사용해서 연관된 테이블을 조회한다.

Member 객체는 Member.team 필드에 Team 객체의 참조를 보관해 Team객체와 관계를 맺는다.

MEMBER 테이블은 MEMBER.TEAM_ID 외래키 컬럼을 사용해서 TEAM 테이블과 관계를 맺는다.

이제 객체와 테이블의 차이를 알아보기 위해 객체를 테이블에 맞추어 모델링해보자.

class Member{

	String id;
    Long teamId;
    String username;
}

class Team{

	Long id;
    String name;
}

여기서 teamID 필드에는 문제가 있다. 관계형 데이터베이스는 조인 기능이 있어 외래 키 값을 그대로 보관해도 되지만, 객체는 연관된 객체의 참조를 보관해야 참조를 통해 연관된 객체를 찾을 수 있다.

ex) Team team = member.getTeam();

이렇게 되면 Member 객체와 연관된 Team 객체를 참조를 통해서 조회할 수 없다.

객체는 참조를 통해 관계를 맺는다. 따라서 이와 같이 참조를 사용하도록 모델링해야 한다.

class Member{

	String id;
    Team team;
    String username;
    
    Team getTeam(){
    	return team;
    }
}

class Team{

	Long id;
    String name;
}

이제 회원과 연관된 팀을 조회할 수 있다. 하지만 객체지향 모델링을 사용하면 객체를 테이블에 저장하거나 조회하기 어렵다. 객체 모델은 참조만 있으면 되지만 테이블은 참조가 필요없고 외래키만 있으면 되기 때문에 개발자가 중간에서 변환 역할을 해야 한다.

  1. 저장
    team 필드를 TEAM_ID 외래 키 값으로 변환해야 한다.
member.getId(); //MEMBER_ID PK에 저장
member.getTeam().getId(); //TEAM_ID FK에 저장
member.getUsername(); // USERNAME 컬럼에 저장

이렇게 값을 찾아서 INSERT SQL을 만들어야 한다.

  1. 조회
    TEAM_ID 외래 키 값을 Member 객체의 team 참조로 변환해서 객체에 보관해야 한다.

JPA는 연관관계와 관련된 패러다임의 불일치 문제를 해결해준다.

member.setTeam(team); //회원과 팀 연관관계 설정
jpa.persist(member); //회원과 연관관계 함께 저장

개발자는 회원과 팀의 관계를 설정하고 회원 객체를 저장하면 JPA는 team의 참조를 외래키로 변환해 INSERT SQL을 데이터베이스에 전달한다. 객체를 조회할 때 외래키를 참조로 변환하는 일도 역시 해준다.

이제 연관관계와 관련해서 극복하기 어려운 패러다임의 불일치 문제를 알아보자.

1.1.3. 객체 그래프 탐색

객체에서 회원이 소속된 팀을 조회할 때는 참조를 사용해서 연관된 팀을 찾게 되는데, 이를 객체 그래프 탐색이라 한다.


객체 연관관계가 다음과 같이 설계되어 있다 가정해보자.

SQL을 직접 다루면 처음 실행하는 SQL에 따라 객체 그래프를 어디까지 탐색할 수 있는지 정해진다. 이는 비즈니스 로직에 따라 사용하는 그래프가 다른데 언제 끊어질지 모를 객체 그래프를 함부로 탐색할 수는 없어 큰 제약이다.

class MemberService{
	...
    public void process(){
    	Member member = memberDAO.find(memberId);
        member.getTeam(); //member->team 객체 그래프 탐색 가능?
        member.getOrder().getDelivery(); // ???
    }
}

이 코드에서 MemberService는 memberDAO를 통해 member 객체를 조회했지만 이 객체와 연관된 Team, Order, Delivery 방향으로 객체 그래프를 탐색할 수 있을지는 이 코드만 보고 예측할 수 없다.
결과적으로 어디까지 객체 그래프 탐색이 가능한지 알려면 데이터 접근 계층인 DAO를 열어서 SQL을 직접 확인해야 한다.

JPA는 이 문제를 어떻게 해결할까?

JPA는 객체 그래프를 마음껏 탐색할 수 있다.

JPA는 연관된 객체를 사용하는 시점에 SELECT SQL을 실행한다. 실제 객체를 사용하는 시점까지 데이터베이스 조회를 미룬다고 해서 지연 로딩이라고 한다.

이 기능을 사용하기 위해서는 JPA와 관련된 어떤 코드도 구현하지 않아도 된다. JPA는 지연 로딩을 투명하게 처리한다.

1.2.4. 비교

데이터베이스는 기본키의 값으로 각 열(row)를 구분한다. 반면 객체는 동일성 비교와 동등성 비교라는 두 가지 비교 방법이 있다.

  1. 동일성 비교는 == 비교. 객체 인스턴스의 주소 값을 비교한다.
  2. 동등성 비교는 equals() 메소드를 사용해 객체 내부의 값을 비교한다.
String memberId = "100";
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);

member1 == member2;

이 둘은 기본키 값이 같다. 하지만 동일성 비교(==)를 하면 false가 반환된다. 둘은 다른 인스턴스기 때문이다.

JPA는 같은 트랙잭션일 때 같은 객체가 조회되는 것을 보장한다. 그래서 다음 코드에서 member1과 member2는 동일성 비교에 성공한다.

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

member1 == member2;

1.3. JPA란 무엇인가?

JPA(Java Persistence API)는 자바 진영의 ORM기술 표준이다. JPA는 애플리케이션과 JDBC 사이에서 동작한다.

ORM(Obeject-Relational Mapping)은 객체와 관계형 데이터베이스를 매핑한다는 뜻이다. ORM 프레임워크는 객체와 테이블을 매핑해서 패러다임의 불일치 문제를 해결해준다. 말 그대로 객체를 데이터베이스에 저장할 때 INSER SQL을 직접 작성하지 않고 자바 컬렉션에 저장하듯이 ORM 프레임워크에 저장하면 된다.


JPA를 사용해서 객체를 저장할 때 이와 같은 구조를 통해 DB에 저장된다.


조회할 때에는 이와 같은 구조로 객체를 조회한다.

1.3.1. JPA 소개

JPA는 자바 ORM 기술에 대한 API 표준 명세다. 쉽게 이야기해 인터페이스를 모아둔 것이다. 따라서 JPA를 사용하려면 JPA를 구현한 ORM 프레임워크를 선택해야 한다.

1.3.2. 왜 JPA를 사용해야 하는가?

  1. 생산성
    JPA를 사용하면 지루하고 반복적인 코드와 CRUD용 SQL을 개발자가 직접 작성하지 않아도 된다. 또 CREATE TABEL 같은 DDL 문을 자동으로 생성해주는 기능도 있다.

  2. 유지보수
    SQL을 직접 다루면 엔티티에 필드를 하나만 추가해도 관련된 등록, 수정, 조회 SQL과 결과 매핑을 위한 JDBC API 코드를 모두 변경해야 한다. 하지만 JPA는 이런 과정을 대신 해주므로 유지보수해야 하는 코드 수가 줄어든다.

  3. 패러다임의 불일치 해결
    JPA는 상속, 연관관계, 객체 그래프 탐색, 비교하기와 같은 패러다임의 불일치 문제를 해결해준다.

  4. 성능
    JPA는 애플리케이션과 데이터베이스 사이에서 다양한 성능 최적화 기회를 제공한다.

String memberId = "helloId";
Member member1 = jpa.find(memberId);
Member member2 = jpa.find(memberId);

같은 트랙잭션 안에서 같은 회원을 두 번 조회하는 코드의 일부분이다. JDBC API를 사용해서 코드를 작성했다면 회원을 조회할 때마다 SELECT SQL을 사용해서 DB와 두 번 통신해야 한다. JPA를 사용하면 SELECT SQL을 한 번만 DB에 전달하고 두 번째는 조회한 회원 객체를 재사용한다.

  1. 데이터 접근 추상화와 벤더 독립성
    JPA는 애플리케이션과 데이터베이스 사이에 추상화된 데이터 접근 계층을 제공해서 애플리케이션이 특정 데이터베이스 기술에 종속되지 않도록 한다. 데이터베이스를 변경하면 JPA에게 다른 데이터베이스를 사용한다고 알려주기만 하면 된다.

  2. 표준
    JPA는 자바 진영의 ORM 기술 표준으로 다른 구현 기술로 손쉽게 변경할 수 있다.

1.4. 정리

지금까지 SQL을 직접 다룰 때 발생하는 다양한 문제와 객체지향 언어와 관계형 데이터베이스 사이의 패러다임 불일치 문제를 설명했다. 다음 장에서 테이블 하나를 CRUD하는 간단한 JPA 애플리케이션을 만들어보자.

학습 교재 : 자바 ORM 표준 JPA 프로그래밍(에이콘출판사, 김영한)

profile
와플대조교의 개발 블로그

0개의 댓글