반응형

JPA와 모던 자바 데이터 저장기술

1. 현재 개발 트렌드

애플리케이션 : 객체지향 언어 (ex: [Java, Scala, Kotlin...])

데이터베이스 : 관계형 데이터 (ex: [Oracle, Mysql, PostgreSQL...])

 객체를 관계형 DB에 관리하는 것에 시간을 많이씀

객체 데이터, 데이터 객체 : SQL 중심적인 개발이 됨

 

 

2. SQL 중심적인 개발의 문제점

2.1 기능추가, 테이블 생성 될때마다 CRUD SQL을 다 만들어야함

(JdbcTemplate, MyBatis가 Mapping에 도움을 주는 것은 있지만 그래도 한계가 있음)

 

회원 객체의 CRUD가 구현되있는 상황

 - 기존 회원 객체 테이블 기능 쿼리 구현

/*회원 객체*/
public class Member {
	private String memberId;
	private String name;
}

/*쿼리*/
INSERT INTO MEMBER(MEMBER_ID, NAME) VALUES ...
SELECT MEMBER_ID, NAME FROM MEMBER M
UPDATE MEMBER SET .

- 전화 번호 필드를 추가해야하는 상황

/*회원 객체*/
public class Member {
	private String memberId;
	private String name;
	private String tel;
}
/*쿼리*/
INSERT INTO MEMBER(MEMBER_ID, NAME, TEL) VALUES ...
SELECT MEMBER_ID, NAME, TEL FROM MEMBER M
UPDATE MEMBER SET ...TEL = ?

※ 모든 CRUD에 TEL을 하나하나 추가해야함

※ SQL에 의존적인 개발

 

 

2.2 패러다임의 불일치 (객체 VS RDBMS)

관계형 데이터베이스와 객체지향은 사상이 다름
객체지향개발 : 추상화, 캡슐화, 정보은닉, 상속, 다형성등 시스템의 복잡성을 제어하는 다양한 장치들 제공
관계형데이터베이스 : 데이터를 잘 정규화해서 저장하는 것이 목표.


 객체를 관계형 데이터베이스에 저장하는 도식

※ 객체를 SQL로 변환해서 RDB에 저장하는 변환과정을 개발자가 해야함

 

 

3. 객체와 관계형 데이터 베이스의 차이

3.1 상속

객체 상속관계 VS Table 슈퍼타입 서브타입 관계

 객체의 상속관계와 유사한 관계형 데이터베이스의 개념으로 Table 슈퍼타입, 서브타입 관계가 있음
상속 받은 객체(Album, Movie, Book)을 데이터베이스에 저장하려면 복잡함
 - 객체 분해 : Album객체를 Item과 분리

- Item Table에 하나, Album테이블에 하나 두개의 쿼리를 작성해서 저장

 Album 객체를 DB에서 조회하는것도 복잡
 - SQL로 ITEM과 ALBUM을 조인해서 데이터를 가져옴

 - 조회한 필드를 각각 맞는 객체(ITEM, ALBUM)에 매핑시켜서 가져와야함
결론: DB에 저장할 객체는 상속관계를 쓰지 않음

 

 

3.2 연관 관계

객체 연관 관계 VS 테이블 연관 관계

 객체는 참조를 사용 : member.getTeam();
 테이블은 외래 키를 사용 : JOIN ON M.TEAM_ID = T.TEAM_ID


 Member와 Team간에 참조  
- 객체:  Member → Team 은 member.getTeam()을 통해 가능, Team → Member 는 참조할 객체가 없기 때문에 불가능
 - 테이블: 서로가 서로를 참조할 키(FK)가 있기 때문에 양측이 참조가 가능하다. Member ↔ Team


객체를 테이블에 맞춰 모델링, 테이블에 맞춘 객체 저장

/* 회원 객체 */
class Member{
	String id;        //MEMBER_ID 컬럼 사용
	Long teamId;      //참조로 연관관계를 맺는다.
	String username;  // USERNAME 컬럼 사용
}

/* 팀객체 */
class Team{
	Long id;      //TEAM_ID 컬럼 사용
	String name;  //NAME 컬럼 사용
}

/* 쿼리 */
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES ...
INSERT INTO TEAM(TEAM_ID, NAME) VALUES...

- 객체 지향적이지 못함

 

 객체다운 모델링, 객체 모델링 저장

/* 회원 객체 */
class Member{
	String id;        // MEMBER_ID 컬럼 사용
	Team team;        // Team 객체 참조로 연관관계를 맺음
	String username;  // USERNAME 컬럼 사용
    
    Team getTeam(){
        return team;
    }
}

/* 팀객체 */
class Team{
	Long id;      // TEAM_ID 컬럼 사용
	String name;  // NAME 컬럼 사용
}


/* 쿼리에 teamId를 저장하기 위해 꺼냄*/
member.getTeam().getId();


/*쿼리*/
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES ...
INSERT INTO TEAM(TEAM_ID, NAME) VALUES...

 

객체 모델링 조회

SELECT M.*, T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID

public Member find(String memberId){
	//SQL 실행
	Member member = new Member();

	//데이터터베이스에서 조회한 회원 관련 정보를 모두 입력
	Team team = new Team();

	//회원과 팀 관계 설정
	member.setTeam(team);
	return member;
}

 

 객체 그래프 탐색

 - 객체는 자유롭게 객체 그래프를 탐색할 수 있어야 함

 - Member 객체에서 엔티티 그래프를 통해 Category 까지도 접근이 가능해야 함

ex) member.getOrder().getOrderItem().getItem().getCategory;

 

처음 실행하는 SQL에 따라 탐색 범위가 결정

SELECT M.*, T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID

member.getTeam(); //OK
member.getOrder(); //null


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

※ 엔티티 신뢰 문제가 발생

 - member안에 모든 참조를 자유롭게 참조 할 수 없음

 - 새로운 필드를 추가했는데 조회 로직에서 해당 부분 매핑을 빼놓을 가능성도 있음
 - 모든 코드와 쿼리를 분석해보기전까지는 엔티티 그래프 검색이 어디까지 되는지 확신할 수 없음 

 - 모든 객체를 미리 로딩할 수도 없음

memberDAO.getMember(); //Member만 조회
memberDAO.getMemberWithTeam(); //Member와 Team조회
memberDAO.getMemberWithOrderWithDelivery(); // Member, ORder, Delivery 조회
...

 

※ 기존의 방식으로는 계층형 아키택처, 진정한 의미의 계층분할을 구현 할 수 없음

 

 

4. 객체 비교

동일한 식별자(memberId)로 조회한 두 객체 비교

String memberId = "100";
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);

member1 == member2; // false 다르다

class MemberDAO{
	public Member getMember(String memberId){
		String sql = "SELECT * FROM MEMBER WHERE MEMBER_ID = ?";
		...
		//JDBC API, SQL 실행
		return new Member(...);
	}
}

 - member를 조회할 때마다 new Member()를 통해 새로운 객체를 만들어서 조회 하기 때문에 두 인스턴스 비교는 내용물이 같더라도 참조값이 다름

 

• 자바 컬렉션에서 조회한 객체 비교

String memberId = "100";
Member member1 = list.get(memberId);
Member member2 = list.get(memberId);

member1 == member2; // true 같다.

 

※ 동일성과 동등성(Identical & Equality)
 - 자바에서 두 개의 오브젝트 혹은 값이 같은가
: 동일한 오브젝트라는 말(identical)은 같은 레퍼런스를 바라보고 있는 객체라는 말로 실제론 하나의 오브젝트라는 의미

: 동등한 오브젝트라는 말(Equivalent)은 같은 내용을 담고 있는 객체라는 의미 

 

 

 

 

※ 결론

객체지향적으로 모델링을 할 수록 매핑작업만 늘어나고 불일치가 늘어나서 사이드이펙트가 커지기만함. 
객체를 자바 컬렉션에 저장하듯이 DB에 저장하기위해 나온 것이 JPA

 

 

JPA (Java Persistence API)

1. 용어

 JPA : 자바진영의 ORM 기술 표준

 ORM

- Object-relational mapping(객체 관계 매핑)
- 객체는 객체대로 설계 하고, RDBMS는 RDBMS대로 설계해서 ORM 프레임워크가 중간에서 매핑 해줌
- 대중적인 언어에는 대부분 ORM 기술이 존재

 

2. JPA 동작 원리

2.1 애플리케이션과 JDBC 사이에서 동작함

 

2.2 저장

 

2.3 조회

 

3. JPA를 사용하는 이유

3.1 생산성

• 저장: jpa.persist(member)
• 조회: Member member = jpa.find(memberId)
• 수정: member.setName(“변경할 이름”)
• 삭제: jpa.remove(member)

※ CRUD가 간편함

 

3.2 유지보수

/*회원 객체*/
public class Member {
	private String memberId;
	private String name;
	private String tel;
}
/*쿼리*/
INSERT INTO MEMBER(MEMBER_ID, NAME, TEL) VALUES ...
SELECT MEMBER_ID, NAME, TEL FROM MEMBER M
UPDATE MEMBER SET ...TEL = ?

※ 기존에는 필드 변경시 모든 SQL을 수정했어야했으나, JPA는 필드만 추가하면 되고 SQL은 JPA가 수행함

 

 

3.3 패러다임 불일치 해결

• 상속

 - 상속되어 있는 객체의 저장

 : 개발자의 할일  jpa.persist(album);

 : JPA가 나머지 처리

 

 - 상속되어 있는 객체의 조회

 : 개발자가 할일 Album album = jpa.find(Album.class, albumId);

 : JPA가 나머지 처리

 

 

• 연관관계

 - 연관관계 저장

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

 

• 객체 그래프 탐색

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

※ JPA로 계층 엔티티를 신뢰할수 있음

 

• 비교

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

※ 동일한 트랜잭션에서 조회한 엔티티는 같음을 보장

 

 

3.4 성능 최적화 기능

 1차 캐시와 동일성 보장

String memberId = "100";
Member m1 = jpa.find(Member.class, memberId); //SQL
Member m2 = jpa.find(Member.class, memberId); //캐시
println(m1 == m2) //true

- 같은 트렌젝션 안에는 같은 엔티티를 반환(조회성능 향상)

- DB Isolation Level이 Read Committed이어도 애플리케이션에서 Repeatable Read 보장

- SQL을 한번만 수행함

 

※ DB Isolation Level(DB 격리 수준)

<아래로 내려갈수록 트랜잭션간 고립 정도가 높아지고 성능이 떨어짐>

 - READ UNCOMMITTED : 어떤 트랜잭션의 변경내용이 COMMIT이나 ROLLBACK과 상관없이 다른 트랜잭션에서 보여짐

 - READ COMMITTED : 어떤 트랜잭션의 변경 내용이 COMMIT 되어야만 다른 트랜잭션에서 조회할 수 있음

 - REPEATABLE READ : 트랜잭션이 시작되기 전에 커밋된 내용에 대해서만 조회할 수 있는 격리수준

 - SERIALIZABLE : 읽기 작업에도 공유 잠금 설정, 동시에 다른 트랜잭션에서 이 레코드를 변경하지 못하게 됨

 

 

쓰기 지연

- insert

transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.

//커밋하는 순간 데이터베이스에 INSERT SQL을 모아서 보낸다.
transaction.commit(); // [트랜잭션] 커밋

※ 트랜잭션을 커밋할 때까지 INSERT SQL을 모으고 JDBC BATCH SQL 기능을 사용해서 한번에 SQL 전송

 

- update

transaction.begin(); // [트랜잭션] 시작

changeMember(memberA);
deleteMember(memberB);
비즈니스_로직_수행(); //비즈니스 로직 수행 동안 DB 로우 락이 걸리지 않는다.

//커밋하는 순간 데이터베이스에 UPDATE, DELETE SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋

※ UPDATE, DELETE로 인한 로우(ROW)락 시간 최소화
※ 트랜잭션 커밋 시 UPDATE, DELETE SQL 실행하고, 바로 커밋

 

 

지연 로딩

- 지연 로딩: 객체가 실제 사용될 때 로딩
- 즉시 로딩: JOIN SQL로 한번에 연관된 객체까지 미리 조회

반응형

+ Recent posts