[162] jpa와 모던 자바 데이터 저장 기술

Preview:

Citation preview

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

김영한

SK 플래닛 데이터 서비스 개발팀

김영한SI, J2EE 강사, DAUM, SK 플래닛

저서: 자바 ORM 표준 JPA 프로그래밍

contents

1. SQL 중심적인 개발의 문제점2. JPA 소개3. Spring Data JPA4. QueryDSL5. 실무 경험 공유

애플리케이션객체 지향 언어 - [Java,Scala, ...]

데이터베이스 세계의 헤게모니관계형 DB - [Oracle, MySQL, ...]

지금 시대는 객체를

관계형 DB에 관리

SQL! SQL!! SQL!!!

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

무한 반복, 지루한 코드

CRUD

INSERT INTO …

UPDATE …

SELECT …

DELETE …

자바 객체를 SQL로 ...

SQL을 자바 객체로 ...

객체 CURD

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;

...}

객체 CURD - 필드 추가

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 = ?

엔티티 신뢰 문제

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

계층형 아키텍처진정한 의미의 계층 분할이 어렵다.

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

패러다임의 불일치

객체 vs 관계형 데이터베이스

‘객체 지향 프로그래밍은 추상화, 캡슐화, 정보은닉, 상속, 다형성 등 시스템의 복잡성을 제어할 수 있는 다양한 장치들을 제공한다.’

­–어느 객체지향 개발자가

객체를 영구 보관하는 다양한 저장소

Object

RDB

NoSQL

File

OODB?

현실적인 대안은 관계형 데이터베이스

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

1. 상속

2. 연관관계

3. 데이터 타입

4. 데이터 식별 방법

객체를 관계형 데이터베이스에 저장

객체 RDB객체를

SQL로 변환 SQL

개발자 ≒ SQL 매퍼

상속

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

Album 저장

1. 객체 분해

2. INSERT INTO ITEM ...

3. INSERT INTO ALBUM …

Album 조회

1. 각각의 테이블에 따른 조인 SQL 작성...

2. 각각의 객체 생성...

3. 상상만 해도 복잡

4. 더 이상의 설명은 생략한다.

5. 그래서 DB에 저장할 객체에는 상속 관계 안쓴다.

자바 컬렉션에 저장하면?

list.add(album);

자바 컬렉션에서 조회하면?

Album album = list.get(albumId);

Item item = list.get(albumId);부모 타입으로 조회 후 다형성 활용

연관관계

- 객체는 참조를 사용: member.getTeam()

- 테이블은 외래 키를 사용: JOIN ON M.TEAM_ID = T.TEAM_ID

객체를 테이블에 맞추어 모델링

class Member { String id; //MEMBER_ID 컬럼 사용 Long teamId; //TEAM_ID FK 컬럼 사용 //** String username;//USERNAME 컬럼 사용}

class Team { Long id; //TEAM_ID PK 사용 String name; //NAME 컬럼 사용}

테이블에 맞춘 객체 저장

INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES …

class Member { String id; //MEMBER_ID 컬럼 사용 Long teamId; //TEAM_ID FK 컬럼 사용 //** String username;//USERNAME 컬럼 사용}

객체다운 모델링class Member { String id; //MEMBER_ID 컬럼 사용 Team team; //참조로 연관관계를 맺는다. //** String username;//USERNAME 컬럼 사용 Team getTeam() { return team; }}

class Team { Long id; //TEAM_ID PK 사용 String name; //NAME 컬럼 사용}

객체 모델링 저장

INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES …

class Member { String id; //MEMBER_ID 컬럼 사용 Team team; //참조로 연관관계를 맺는다. //** String username;//USERNAME 컬럼 사용}

member.getTeam().getId();

객체 모델링 조회

public Member find(String memberId) { //SQL 실행 ... Member member = new Member(); //데이터베이스에서 조회한 회원 관련 정보를 모두 입력 Team team = new Team(); //데이터베이스에서 조회한 팀 관련 정보를 모두 입력

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

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

객체 모델링, 자바 컬렉션에 관리

list.add(member);

Member member = list.get(memberId);Team team = member.getTeam();

객체 그래프 탐색

객체는 자유롭게 객체 그래프를 탐색할 수 있어야 한다.

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

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

member.getTeam(); //OK

member.getOrder(); //null

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

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

모든 객체를 미리 로딩할 수는 없다.

상황에 따라 동일한 회원 조회 메서드를 여러벌 생성

memberDAO.getMember(); //Member만 조회

memberDAO.getMemberWithTeam();//Member와 Team 조회

//Member,Order,DeliverymemberDAO.getMemberWithOrderWithDelivery();

비교하기String memberId = "100";Member member1 = memberDAO.getMember(memberId);Member member2 = memberDAO.getMember(memberId);

member1 == member2; //다르다.

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

비교하기 - 자바 컬렉션에서 조회

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

member1 == member2; //같다.

객체답게 모델링 할수록 매핑 작업만 늘어난다.

객체를 자바 컬렉션에 저장 하듯이DB에 저장할 수는 없을까?

JPA - Java Persistence API

JPA?

- Java Persistence API

- 자바 진영의 ORM 기술 표준

ORM?

- Object-relational mapping(객체 관계 매핑)

- 객체는 객체대로 설계

- 관계형 데이터베이스는 관계형 데이터베이스대로 설계

- ORM 프레임워크가 중간에서 매핑

- 대중적인 언어에는 대부분 ORM 기술이 존재

JPA는 애플리케이션과 JDBC 사이에서 동작

JPA 동작 - 저장

JPA 동작 - 조회

JPA 소개

EJB - 엔티티 빈(자바 표준)

하이버네이트 (오픈 소스)

JPA(자바 표준)

JPA는 표준 명세

- JPA는 인터페이스의 모음

- JPA 2.1 표준 명세를 구현한 3가지 구현체

- 하이버네이트, EclipseLink, DataNucleus

JPA 버전

- JPA 1.0(JSR 220) 2006년 : 초기 버전. 복합 키와 연관관계 기능이 부족

- JPA 2.0(JSR 317) 2009년 : 대부분의 ORM 기능을 포함, JPA Criteria 추가

- JPA 2.1(JSR 338) 2013년 : 스토어드 프로시저 접근, 컨버터(Converter), 엔티티 그래

프 기능이 추가

JPA를 왜 사용해야 하는가?

- SQL 중심적인 개발에서 객체 중심으로 개발

- 생산성

- 유지보수

- 패러다임의 불일치 해결

- 성능

- 데이터 접근 추상화와 벤더 독립성

- 표준

생산성 - JPA와 CRUD

• 저장: jpa.persist(member)

• 조회: Member member = jpa.find(memberId)

• 수정: member.setName(“변경할 이름”)

• 삭제: jpa.remove(member)

유지보수 - 기존: 필드 변경시 모든 SQL 수정

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 = ?

유지보수 - JPA: 필드만 추가하면 됨, SQL은 JPA가 처리

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 = ?

JPA와 패러다임의 불일치 해결

1. JPA와 상속

2. JPA와 연관관계

3. JPA와 객체 그래프 탐색

4. JPA와 비교하기

JPA와 상속

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

JPA와 상속 - 저장

jpa.persist(album);

INSERT INTO ITEM ...INSERT INTO ALBUM ...

개발자가 할일

나머진 JPA가 처리

JPA와 상속 - 조회

Album album = jpa.find(Album.class, albumId);

SELECT I.*, A.* FROM ITEM I JOIN ALBUM A ON I.ITEM_ID = A.ITEM_ID

개발자가 할일

나머진 JPA가 처리

JPA와 연관관계, 객체 그래프 탐색

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

연관관계 저장

객체 그래프 탐색Member member = jpa.find(Member.class, memberId);Team team = member.getTeam();

신뢰할 수 있는 엔티티, 계층

class MemberService { ... public void process() { Member member = memberDAO.find(memberId); member.getTeam(); //자유로운 객체 그래프 탐색 member.getOrder().getDelivery(); }}

JPA와 비교하기

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

member1 == member2; //같다.

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

JPA의 성능 최적화 기능

1. 1차 캐시와 동일성(identity) 보장

2. 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)

3. 지연 로딩(Lazy Loading)

1차 캐시와 동일성 보장

1.같은 트랜잭션 안에서는 같은 엔티티를 반환 - 약간의 조회 성능 향상2. DB Isolation Level이 Read Commit이어도 애플리케이션에서 Repeatable Read 보장

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

println(m1 == m2) //true

SQL 1번만 실행

트랜잭션을 지원하는 쓰기 지연 - INSERT

1.트랜잭션을 커밋할 때까지 INSERT SQL을 모음2. JDBC BATCH SQL 기능을 사용해서 한번에 SQL 전송

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

em.persist(memberA);em.persist(memberB);em.persist(memberC);//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.

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

트랜잭션을 지원하는 쓰기 지연 - UPDATE

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

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

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

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

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

Member member = memberDAO.find(memberId); Team team = member.getTeam(); String teamName = team.getName();

Member member = memberDAO.find(memberId); Team team = member.getTeam(); String teamName = team.getName();

SELECT M.*, T.* FROM MEMBERJOIN TEAM …

SELECT * FROM MEMBER

SELECT * FROM TEAM

지연 로딩

즉시 로딩

ORM은 객체와 RDB 두 기둥위에 있는 기술

JPA 기반 프로젝트• Spring Data JPA

• QueryDSL

2.Spring Data JPA

반복되는 CRUD

public class MemberRepository {

public void save(Member member) {...} public Member findOne(Long id) {...} public List<Member> findAll() {...}

public Member findByUsername(String username) {...}}

public class ItemRepository {

public void save(Item item) {...} public Member findOne(Long id) {...} public List<Member> findAll() {...}}

스프링 데이터 JPA 소개

• 지루하게 반복되는 CRUD 문제를 세련된 방법으로 해결

• 개발자는 인터페이스만 작성

• 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입

스프링 데이터 JPA 적용 전

public class MemberRepository {

public void save(Member member) {...} public Member findOne(Long id) {...} public List<Member> findAll() {...}

public Member findByUsername(String username) {...}}

public class ItemRepository {

public void save(Item item) {...} public Member findOne(Long id) {...} public List<Member> findAll() {...}}

스프링 데이터 JPA 적용 후public interface MemberRepository extends JpaRepository<Member, Long>{ Member findByUsername(String username);}

public interface ItemRepository extends JpaRepository<Item, Long> { //비어있음}

스프링 데이터 JPA 적용 후 클래스 다이어그램

스프링 데이터 JPA가 구현 클래스 생성

공통 인터페이스 기능

• JpaRepository 인터페이스: 공통 CRUD 제공

• 제네릭은 <엔티티, 식별자>로 설정

public interface MemberRepository extends JpaRepository<Member, Long> { //비어있음}

공통 인터페이스 기능

쿼리 메서드 기능

• 메서드 이름으로 쿼리 생성

• @Query 어노테이션으로 쿼리 직접 정의

메서드 이름으로 쿼리 생성

public interface MemberRepository extends JpaRepository<Member, Long> {

Member findByName(String username); }

• 메서드 이름만으로 JPQL 쿼리 생성

메서드 이름으로 쿼리 생성 - 사용 코드

List<Member> members = memberRepoitory.findByName(“hello”)

SELECT * FROM MEMBER M WHERE M.NAME = ‘hello’

실행된 SQL

이름으로 검색 + 정렬

SELECT * FROM MEMBER M WHERE M.NAME = ‘hello’ ORDER BY AGE DESC

실행된 SQL

public interface MemberRepository extends JpaRepository<Member, Long> { Member findByName(String username, Sort sort); }

이름으로 검색 + 정렬 + 페이징

SELECT * //데이터 조회 FROM ( SELECT ROW_.*, ROWNUM ROWNUM_ FROM ( SELECT M.* FROM MEMBER M WHERE M.NAME = ‘hello’ ORDER BY M.NAME ) ROW_ WHERE ROWNUM <= ? ) WHERE ROWNUM_ > ?

실행된 SQL 2가지

public interface MemberRepository extends JpaRepository<Member, Long> { Page<Member> findByName(String username, Pageable pageable); }

//전체 수 조회 SELECT COUNT(1) FROM MEMBER M WHERE M.NAME = ‘hello’

이름으로 검색 + 정렬 + 페이징, 사용 코드

Pagable page = new PageRequest(1, 20, new Sort…); Page<Member> result = memberRepoitory.findByName(“hello”, page);

int total = result.getTotalElements(); //전체 수 List<Member> members = result.getContent(); //데이터

전체 페이지수, 다음 페이지 및 페이징을 위한 API 다 구현되어 있음

@Query, JPQL 정의

• @Query를 사용해서 직접 JPQL 지정

public interface MemberRepository extends JpaRepository<Member, Long> {

@Query("select m from Member m where m.username = ?1") Member findByUsername(String username, Pageable pageable); }

반환 타입

List<Member> findByName(String name); //컬렉션

Member findByEmail(String email); //단건

Web 페이징과 정렬 기능

• 컨트롤러에서 페이징 처리 객체를 바로 받을 수 있음• page: 현재 페이지

• size: 한 페이지에 노출할 데이터 건수

• sort: 정렬 조건

@RequestMapping(value = "/members", method = RequestMethod.GET)String list(Pageable pageable, Model model) {}

/members?page=0&size=20&sort=name,desc

Web 도메인 클래스 컨버터 기능

• 컨트롤러에서 식별자로 도메인 클래스 찾음

/members/100

@RequestMapping("/members/{memberId}") Member member(@PathVariable("memberId") Member member) { return member; }

3.QueryDSL

QueryDSL 소개

• SQL, JPQL을 코드로 작성할 수 있도록 도와주는 빌더 API

• JPA 크리테리아에 비해서 편리하고 실용적임

• 오픈소스

SQL, JPQL의 문제점

• SQL, JPQL은 문자, Type-check 불가능

• 해당 로직 실행전까지 작동여부 확인 불가

SELECT * FROM MEMBERR WHERE MEMBER_ID = ‘100’

실행 시점에 오류 발견

QueryDSL 장점• 문자가 아닌 코드로 작성

• 컴파일 시점에 문법 오류 발견

• 코드 자동완성(IDE 도움)

• 단순하고 쉬움: 코드 모양이 JPQL과 거의 비슷

• 동적 쿼리

QueryDSL - 동작원리 쿼리타입 생성

Member.java @Entity

QMember .javaAPT

생성

QueryDSL 사용

JPAQuery query = new JPAQuery(em); QMember m = QMember.member; List<Member> list = query.from(m) .where(m.age.gt(18)) .orderBy(m.name.desc()) .list(m);

//JPQL select m from Member m where m.age > 18

QueryDSL - 조인

JPAQuery query = new JPAQuery(em); QMember m = QMember.member; QTeam t = QTeam.team; List<Member> list = query.from(m) .join(m.team, t) .where(t.name.eq("teamA")) .list(m);

QueryDSL - 페이징 API

JPAQuery query = new JPAQuery(em); QMember m = QMember.member; List<Member> list = query.from(m) .orderBy(m.age.desc()) .offset(10) .limit(20) .list(m);

QueryDSL - 동적 쿼리 String name = “member”; int age = 9; JPAQuery query = new JPAQuery(em); QMember m = QMember.member; BooleanBuilder builder = new BooleanBuilder(); if (name != null) { builder.and(m.name.contains(name)); } if (age != 0) { builder.and(m.age.gt(age); } List<Member> list = query.from(m) .where(builder) .list(m);

QueryDSL - 이것은 자바다!

return query.from(coupon) .where( coupon.type.eq(typeParam), coupon.status.eq(“LIVE”), marketing.viewCount.lt(markting.maxCount) ) .list(coupon);

서비스 필수 제약조건

QueryDSL - 이것은 자바다!

return query.from(coupon) .where( coupon.type.eq(typeParam), isServiceable() ) .list(coupon);

private BooleanExpression isServiceable() { return coupon.status.eq(“LIVE”) .and(marketing.viewCount.lt(markting.maxCount)); }

제약조건 조립가능

- 가독성, 재사용

서비스 필수 제약조건

5.실무 경험 공유

실무 경험• 테이블 중심에서 객체 중심으로 개발 패러다임이 변화

• 유연한 데이터베이스 변경의 장점과 테스트

• Junit 통합 테스트시에 H2 DB 메모리 모드

• 로컬 PC에는 H2 DB 서버 모드로 실행

• 개발 운영은 MySQL, Oracle

• 데이터베이스 변경 경험(개발 도중 MySQL -> Oracle 바뀐적도 있다.)

• 테스트, 통합 테스트시에 CRUD는 믿고 간다.

실무 경험• 빠른 오류 발견

• 컴파일 시점!

• 늦어도 애플리케이션 로딩 시점

• (최소한 쿼리 문법 실수나 오류는 거의 발생하지 않는다.)

• 대부분 비즈니스 로직 오류

실무 경험 - 성능• JPA 자체로 인한 성능 저하 이슈는 거의 없음.

• 성능 이슈 대부분은 JPA를 잘 이해하지 못해서 발생

• 즉시 로딩: 쿼리가 튐 -> 지연 로딩으로 변경

• N+1 문제 -> 대부분 페치 조인으로 해결

• 내부 파서 문제: 2000줄 짜리 동적 쿼리 생성 1초

• 정적 쿼리로 변경(하이버네이트는 파싱된 결과 재사용)

실무 경험 - 생산성• 단순 코딩 시간 줄어듬 -> 개발 생산성 향상 -> 잉여 시간 발생

• 비즈니스 로직 작성시 흐름이 끊기지 않음

• 남는 시간에 더 많은 테스트 작성

• 남는 시간에 기술 공부

• 남는 시간에 코드 금칠...

• 팀원 대부분 다시는 과거로 돌아가고 싶어하지 않음

많이 하는 질문

1. ORM 프레임워크를 사용하면 SQL과 데이터베이스는 잘 몰라도 되나요?

2. 성능이 느리진 않나요?

3. 통계 쿼리처럼 매우 복잡한 SQL은 어떻게 하나요?

4. MyBatis와 어떤 차이가 있나요?

5. 하이버네이트 프레임워크를 신뢰할 수 있나요?

6. 제 주위에는 MyBatis(iBatis, myBatis)만 사용하는데요?

7. 학습곡선이 높다고 하던데요?

팀 서버 기술 스택

• Java 8

• Spring Framework

• JPA, Hibernate

• Spring Data JPA

• QueryDSL

• JUnit, Spock(Test)

Q&A

Thank You

Recommended