9. 무한 반복, 지루한 코드
CRUD
INSERT INTO …
UPDATE …
SELECT …
DELETE …
자바 객체를 SQL로 ...
SQL을 자바 객체로 ...
10. 객체 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;
...
}
11. 객체 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 = ?
12. 엔티티 신뢰 문제
class MemberService {
...
public void process(String id) {
Member member = memberDAO.find(id);
member.getTeam(); //???
member.getOrder().getDelivery(); // ???
}
}
26. 자바 컬렉션에서 조회하면?
Album album = list.get(albumId);
Item item = list.get(albumId);
부모 타입으로 조회 후 다형성 활용
27. 연관관계
- 객체는 참조를 사용: member.getTeam()
- 테이블은 외래 키를 사용: JOIN ON M.TEAM_ID = T.TEAM_ID
28. 객체를 테이블에 맞추어 모델링
class Member {
String id; //MEMBER_ID 컬럼 사용
Long teamId; //TEAM_ID FK 컬럼 사용 //**
String username;//USERNAME 컬럼 사용
}
class Team {
Long id; //TEAM_ID PK 사용
String name; //NAME 컬럼 사용
}
29. 테이블에 맞춘 객체 저장
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES …
class Member {
String id; //MEMBER_ID 컬럼 사용
Long teamId; //TEAM_ID FK 컬럼 사용 //**
String username;//USERNAME 컬럼 사용
}
30. 객체다운 모델링
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 컬럼 사용
}
31. 객체 모델링 저장
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES …
class Member {
String id; //MEMBER_ID 컬럼 사용
Team team; //참조로 연관관계를 맺는다. //**
String username;//USERNAME 컬럼 사용
}
member.getTeam().getId();
32. 객체 모델링 조회
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
33. 객체 모델링, 자바 컬렉션에 관리
list.add(member);
Member member = list.get(memberId);
Team team = member.getTeam();
35. 처음 실행하는 SQL에 따라 탐색 범위 결정
SELECT M.*, T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
member.getTeam(); //OK
member.getOrder(); //null
36. 처음 실행하는 SQL에 따라 탐색 범위 결정
class MemberService {
...
public void process() {
Member member = memberDAO.find(memberId);
member.getTeam(); //???
member.getOrder().getDelivery(); // ???
}
}
37. 모든 객체를 미리 로딩할 수는 없다.
상황에 따라 동일한 회원 조회 메서드를 여러벌 생성
memberDAO.getMember(); //Member만 조회
memberDAO.getMemberWithTeam();//Member와 Team 조회
//Member,Order,Delivery
memberDAO.getMemberWithOrderWithDelivery();
38. 비교하기
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(...);
}
}
39. 비교하기 - 자바 컬렉션에서 조회
String memberId = "100";
Member member1 = list.get(memberId);
Member member2 = list.get(memberId);
member1 == member2; //같다.
49. JPA는 표준 명세
- JPA는 인터페이스의 모음
- JPA 2.1 표준 명세를 구현한 3가지 구현체
- 하이버네이트, EclipseLink, DataNucleus
50. JPA 버전
- JPA 1.0(JSR 220) 2006년 : 초기 버전. 복합 키와 연관관계 기능이 부족
- JPA 2.0(JSR 317) 2009년 : 대부분의 ORM 기능을 포함, JPA Criteria 추가
- JPA 2.1(JSR 338) 2013년 : 스토어드 프로시저 접근, 컨버터(Converter), 엔티티 그래
프 기능이 추가
51. JPA를 왜 사용해야 하는가?
- SQL 중심적인 개발에서 객체 중심으로 개발
- 생산성
- 유지보수
- 패러다임의 불일치 해결
- 성능
- 데이터 접근 추상화와 벤더 독립성
- 표준
53. 유지보수 - 기존: 필드 변경시 모든 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 = ?
54. 유지보수- 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 = ?
57. JPA와 상속 - 저장
jpa.persist(album);
INSERT INTO ITEM ...
INSERT INTO ALBUM ...
개발자가 할일
나머진 JPA가 처리
58. 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가 처리
59. JPA와 연관관계, 객체 그래프 탐색
member.setTeam(team);
jpa.persist(member);
연관관계 저장
객체 그래프 탐색
Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam();
60. 신뢰할 수 있는 엔티티, 계층
class MemberService {
...
public void process() {
Member member = memberDAO.find(memberId);
member.getTeam(); //자유로운 객체 그래프 탐색
member.getOrder().getDelivery();
}
}
61. JPA와 비교하기
String memberId = "100";
Member member1 = jpa.find(Member.class, memberId);
Member member2 = jpa.find(Member.class, memberId);
member1 == member2; //같다.
동일한 트랜잭션에서 조회한 엔티티는 같음을 보장
62. JPA의 성능 최적화 기능
1. 1차 캐시와 동일성(identity) 보장
2. 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)
3. 지연 로딩(Lazy Loading)
63. 1차 캐시와 동일성 보장
1. 같은 트랜잭션 안에서는 같은 엔티티를 반환 - 약간의 조회 성능 향상
2. DB Isolation Level이 Read Commit이어도 애플리케이션에서 Repeatable Read 보장
String memberId = "100";
Member m1 = jpa.find(Member.class, memberId); //SQL
Member m2 = jpa.find(Member.class, memberId); //캐시
println(m1 == m2) //true
SQL 1번만 실행
64. 트랜잭션을 지원하는 쓰기 지연 - 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(); // [트랜잭션] 커밋
65. 트랜잭션을 지원하는 쓰기 지연 - UPDATE
1. UPDATE, DELETE로 인한 로우(ROW)락 시간 최소화
2. 트랜잭션 커밋 시 UPDATE, DELETE SQL 실행하고, 바로 커밋
transaction.begin(); // [트랜잭션] 시작
changeMember(memberA);
deleteMember(memberB);
비즈니스_로직_수행(); //비즈니스 로직 수행 동안 DB 로우 락이 걸리지 않는다.
//커밋하는 순간 데이터베이스에 UPDATE, DELETE SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋
66. 지연 로딩과 즉시 로딩
• 지연 로딩: 객체가 실제 사용될 때 로딩
• 즉시 로딩: 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 MEMBER
JOIN TEAM …
SELECT * FROM MEMBER
SELECT * FROM TEAM
지연 로딩
즉시 로딩
70. 반복되는 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() {...}
}
71. 스프링 데이터 JPA 소개
• 지루하게 반복되는 CRUD 문제를 세련된 방법으로 해결
• 개발자는 인터페이스만 작성
• 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입
72. 스프링 데이터 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() {...}
}
73. 스프링 데이터 JPA 적용 후
public interface MemberRepository extends JpaRepository<Member, Long>{
Member findByUsername(String username);
}
public interface ItemRepository extends JpaRepository<Item, Long> {
//비어있음
}
76. 공통 인터페이스 기능
• JpaRepository 인터페이스: 공통 CRUD 제공
• 제네릭은 <엔티티, 식별자>로 설정
public interface MemberRepository extends JpaRepository<Member, Long> {
//비어있음
}
78. 쿼리 메서드 기능
• 메서드 이름으로 쿼리 생성
• @Query 어노테이션으로 쿼리 직접 정의
79. 메서드 이름으로 쿼리 생성
public interface MemberRepository extends JpaRepository<Member, Long> {
Member findByName(String username);
}
• 메서드 이름만으로 JPQL 쿼리 생성
80. 메서드 이름으로 쿼리 생성 - 사용 코드
List<Member> members = memberRepoitory.findByName(“hello”)
SELECT * FROM MEMBER M WHERE M.NAME = ‘hello’
실행된 SQL
81. 이름으로 검색 + 정렬
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);
}
82. 이름으로 검색 + 정렬 + 페이징
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’
83. 이름으로 검색 + 정렬 + 페이징, 사용 코드
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 다 구현되어 있음
84. @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);
}
86. Web 페이징과 정렬 기능
• 컨트롤러에서 페이징 처리 객체를 바로 받을 수 있음
• page: 현재 페이지
• size: 한 페이지에 노출할 데이터 건수
• sort: 정렬 조건
@RequestMapping(value = "/members", method = RequestMethod.GET)
String list(Pageable pageable, Model model) {}
/members?page=0&size=20&sort=name,desc
87. Web 도메인 클래스 컨버터 기능
• 컨트롤러에서 식별자로 도메인 클래스 찾음
/members/100
@RequestMapping("/members/{memberId}")
Member member(@PathVariable("memberId") Member member) {
return member;
}
89. QueryDSL 소개
• SQL, JPQL을 코드로 작성할 수 있도록 도와주는 빌더 API
• JPA 크리테리아에 비해서 편리하고 실용적임
• 오픈소스
90. SQL, JPQL의 문제점
• SQL, JPQL은 문자, Type-check 불가능
• 해당 로직 실행전까지 작동여부 확인 불가
SELECT * FROM MEMBERR WHERE MEMBER_ID = ‘100’
실행 시점에 오류 발견
91. QueryDSL 장점
• 문자가 아닌 코드로 작성
• 컴파일 시점에 문법 오류 발견
• 코드 자동완성(IDE 도움)
• 단순하고 쉬움: 코드 모양이 JPQL과 거의 비슷
• 동적 쿼리
92. QueryDSL - 동작원리 쿼리타입 생성
Member.java
@Entity
QMember
.java
APT
생성
93. 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
94. 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);
95. 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);
96. 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);
97. QueryDSL - 이것은 자바다!
return query.from(coupon)
.where(
coupon.type.eq(typeParam),
coupon.status.eq(“LIVE”),
marketing.viewCount.lt(markting.maxCount)
)
.list(coupon);
서비스 필수 제약조건
100. 실무 경험
• 테이블 중심에서 객체 중심으로 개발 패러다임이 변화
• 유연한 데이터베이스 변경의 장점과 테스트
• Junit 통합 테스트시에 H2 DB 메모리 모드
• 로컬 PC에는 H2 DB 서버 모드로 실행
• 개발 운영은 MySQL, Oracle
• 데이터베이스 변경 경험(개발 도중 MySQL -> Oracle 바뀐적도 있다.)
• 테스트, 통합 테스트시에 CRUD는 믿고 간다.
101. 실무 경험
• 빠른 오류 발견
• 컴파일 시점!
• 늦어도 애플리케이션 로딩 시점
• (최소한 쿼리 문법 실수나 오류는 거의 발생하지 않는다.)
• 대부분 비즈니스 로직 오류
102. 실무 경험 - 성능
• JPA 자체로 인한 성능 저하 이슈는 거의 없음.
• 성능 이슈 대부분은 JPA를 잘 이해하지 못해서 발생
• 즉시 로딩: 쿼리가 튐 -> 지연 로딩으로 변경
• N+1 문제 -> 대부분 페치 조인으로 해결
• 내부 파서 문제: 2000줄 짜리 동적 쿼리 생성 1초
• 정적 쿼리로 변경(하이버네이트는 파싱된 결과 재사용)
103. 실무 경험 - 생산성
• 단순 코딩 시간 줄어듬 -> 개발 생산성 향상 -> 잉여 시간 발생
• 비즈니스 로직 작성시 흐름이 끊기지 않음
• 남는 시간에 더 많은 테스트 작성
• 남는 시간에 기술 공부
• 남는 시간에 코드 금칠...
• 팀원 대부분 다시는 과거로 돌아가고 싶어하지 않음
104. 많이 하는 질문
1. ORM 프레임워크를 사용하면 SQL과 데이터베이스는 잘 몰라도 되나요?
2. 성능이 느리진 않나요?
3. 통계 쿼리처럼 매우 복잡한 SQL은 어떻게 하나요?
4. MyBatis와 어떤 차이가 있나요?
5. 하이버네이트 프레임워크를 신뢰할 수 있나요?
6. 제 주위에는 MyBatis(iBatis, myBatis)만 사용하는데요?
7. 학습곡선이 높다고 하던데요?
105. 팀 서버 기술 스택
• Java 8
• Spring Framework
• JPA, Hibernate
• Spring Data JPA
• QueryDSL
• JUnit, Spock(Test)