본문 바로가기

Spring

Tacademy JPA 강의 정리

JPA 프로그래밍 기본기 다지기 시리즈의 내용을 정리한 내용입니다.

왜 JPA 인가?

애플리케이션은 객체지향 언어로 짜고 관계형 DB가 지배하는 세상이다.
특수한 상황에서 NoSql을 쓰는 것이지 대부분은 RDB를 사용한다.

지금 시대는 객체를 RDB에 관리하고 SQL 중심적인 개발을 하게 된다. 문제점은 무엇인가?

  • 무한 반복, 지루한 CRUD 코드
  • 요구 사항 변경 시, 모든 쿼리 수정해야 한다. -> SQL을 유지보수하기 어렵다.
  • 엔티티 신뢰 문제, DAO에서 Member 객체를 가져오면 member.getTeam()과 같은 코드를 실행할 때 DAO에서 team을 넣어줬는지 신뢰할 수 없다. 까봐야 안다. 즉 보장이 되지 않는다. 또 NPE가 터질 우려가 있다.
  • 처음 실행하는 SQL에 객체의 탐색범위가 결정된다.
  • Controller, Service 등과 같이 계층형 아키텍처에서 진정한 의미의 계층 분할이 어렵다. 엔티티 신뢰 문제가 있기 때문에 Member를 직접 들여다봐야한다. 물리적인 계층은 분할이 되어있지만 논리적으로는 분할되어있지 않다.
  • SQL 의존적인 개발을 피하기 어렵다. SQL에 따라 비즈니스 로직이 결정된다.
  • 패러다임의 불일치, 객체와 RDB는 나온 사상이 다르다. RDB는 데이터를 잘 정규화해서 저장하는 것에 포커싱이 되어있다. OOP는 어떻게 추상화해서 데이터를 잘 관리 할지가 중점이다. 사상이 다른 두가지를 억지로 맵핑해서 일을 처리하다보니 힘든 것이다.
  • 객체를 영구히 저장할 대안은 현실적으로 RDB밖에 없다. 객체를 RDB에 저장하려면 개발자가 한땀한땀 SQL로 바꿔줘야 한다. 개발자가 SQL 매퍼 일을 너무 많이 한다.
  • 객체지향적으로 설계하면 RDB와 패러다임이 맞지 않기 때문에 SQL 양이 폭증한다. 그리고 그 안에서 버그가 폭증한다.
  • 객체답게 모델링 할수록 SQL 매핑 작업만 늘어난다.

객체 vs RDB

상속

  • 객체는 자유롭게 객체 그래프를 탐색할 수 있어야한다.
  • RDB에는 상속이 없다.
  • 상속되어 있는 객체를 RDB에 넣고 조회하기 위해서는 쿼리가 너무 복잡하다. 당연히 유지보수하기도 힘들다. 그래서 RDB에 저장할 때는 상속관계를 안쓴다.
  • 객체지향으로 설계하면 코드가 줄고 편하지만 RDB와의 불일치 때문에 객체지향적으로 설계할 수가 없다.

연관 관계

  1. 객체 지향에서는 Member 객체에서 Team 객체로 갈 수 있지만 Team 객체에서 Member 객체로는 갈 수 없다. 하지만 RDB는 JOIN하면 둘 다 왔다갔다 할 수 있는 양방향이다. 즉 객체 간의 관계에는 방향성이 있다. RDB는 방향성이 없다.
  2. 객체를 테이블에 맞추어 모델링하게 된다. Member 객체에서 Team 객체를 가지는 것이 아니라 teamId를 갖는다. Member에 Team을 가지는 것이 더 객체지향 적이다. 하지만 SQL을 편하게 짜기 위해서 테이블에 맞추어 객체를 모델링한다.
Member member1 = memberDAO.getMember(1);  
Member member2 = memberDAO.getMember(1);  
member1 == merber2 // 다르다.

Member member1 = members.get(1);  
Member member2 = members.get(1);  
member1 == member2 // 같다.

// RDB와 객체간의 식별자에도 패러다임 차이가 있다.

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

JPA(Java Persistence API)

  • 자바 진영의 ORM 기술 API 표준
  • ORM을 사용하기 위한 인터페이스를 모아둔 것
  • 내부적으로 JDBC API를 사용

ORM(Object Relational Mapping)

  • 객체는 객체대로 DB는 DB대로 설계해주는 기술
  • ORM 프레임워크가 중간에서 객체와 SQL을 맵핑 해준다.
  • JPA를 구현한 ORM 프레임워크의 대표는 Hibernate(오픈 소스)

단순히 DB에 넣어주는 정도가 아니라 INSERT 쿼리를 몇번이고 날리고, 조회할 때는 알아서 JOIN도 해준다. 즉 패러다임의 불일치 자체를 해결해준다.

JPA를 사용해야 하는 이유

SQL 중심적인 개발에서 객체 중심으로 개발할 수 있다.

생산성

  • 코드가 훨씬 줄어든다.

유지보수

  • 기존에는 필드 변경 시 쿼리문을 전부 수정해야 했지만, JPA는 필드만 변경해주면 된다.

패러다임 불일치 해결

  • 상속 -> 객체지향적으로 설계할 수 있다.
  • 연관관계 -> getTeam() 했을 때 Team이 null이라면 Team 만 조회하는 쿼리를 따로 날려서 가져와준다. 신뢰성이 있다.
  • 객체 그래프 탐색 -> 위와 이유로 신뢰된 객체 그래프 탐색이 가능하다.
  • 비교하기 -> Collection에서 꺼내듯이 == 비교도 같게 나온다. 1차 캐시에서 가져오기 때문
  • 성능 -> 1차 캐시와 동일성(identity)보장, 트랜잭션을 지원하는 쓰기 지연 -> 트랜잭션을 커밋할 때 까지 INSERT SQL을 모은다. JDBC BATCH SQL 기능을 사용해서 한번에 SQL 전송, 레이지 로딩 -> 객체를 실제 사용 될때 로딩된다. 성능에 이슈가 있다고 생각하면 설정으로 즉시 로딩으로 바꿀 수 있다. ==> JPA를 잘 사용하면 성능 최적화에 더 좋다.
  • 데이터 접근 추상화와 벤더 독립성
  • 표준

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

  • 개발자는 둘 다 잘다뤄야한다~

JPA 기초

  • @Entity : JPA가 DB랑 맵핑하는 클래스인지 인식하게 해준다.
  • persistence.xml: JPA 설정 파일, SpringBoot를 사용하면 알아서 다 해준다.

데이터베이스 방언(dialect)

  • JPA는 특정 DB에 종속적이지 않은 기술
  • 각각의 데이터베이스가 제공하는 SQL 문법과 함수가 조금씩 다르다.
  • 오라클은 SQL 표준을 따르지 않는다. 오라클만의 문법이 있음.
  • 즉 방언은 SQL 표준을 지키지 않거나 특정 데이터베이스만의 고유한 기능
  • hibernate.dialect 속성에 사용할 DB를 지정해주기만 하면 된다.

엔티티 매니저 팩토리 설정

Persistence

  1. 설정 정보 조회 -> META-INF/persistence.xml
  2. EntityManagerFactory 생성
// 설정 정보의 unit name이 hello이기 때문에 인자로 hello를 넘겨서 설정 정보를 가져온다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

EntityManagerFactory

  1. EntityManager 생성
  • EntityManagerFactory는 하나만 생성해서 애플리케이션 전체에서 공유해야 한다.
EntityManager em = emf.createEntityManager();

엔티티 매니저 설정

  • EntityManager는 쓰레드간의 공유하면 안된다.(사용하면 버려야 한다.) 데이터베이스 커넥션 당 한개씩 묶이기 때문에 버리지 않으면 공유될 수 있다. 사실 Spring에서는 다해준다.

트랜잭션

  • JPA는 모든 데이터 변경은 트랜잭션 안에서 실행해야 한다.
  • SpringBoot는 @Transcation
EntityTransaction tx = em.getTransaction();
tx.begin(); // 트랜잭션 시작, 커넥션

필드와 컬럼 매핑

데이터베이스 스키마 자동 생성

  • DDL을 애플리케이션 실행 시점에 자동 생성
  • 테이블 중심에서 객체 중심으로
  • 데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL 생성
  • 이렇게 생성된 DDL은 개발 장비에서만 사용, 운영에서 쓰면 안된다.
  • 생성된 DDL은 운영서버에서는 사용하지 않거나, 적절히 다듬은 후 사용

자동 생성 속성

  • create: 기존 테이블 삭제 후 다시 생성
  • create-drop: create와 같으나 종료시점에 drop
  • update: 변경 분만 반영 -> 운영에서는 바로 장애가 난다. 데이터가 몇 천만 건 있으면 웬만하면 lock 걸린다.
  • validate: 엔티티와 테이블이 정상 매핑되었는지만 확인, 개발에서 쓰면 괜찮다.
  • none: 사용하지 않음
  • 운영 장비에는 절대 validate를 제외하고 사용해선 안된다.
  • 개발 초기 단계에는 create 또는 update
  • 테스트 서버에는 update 또는 validate
  • 스테이징과 운영 서버에는 validate 또는 none

매핑 어노테이션

데이터베이스에 어떤 식으로 매핑이 될지를 결정, 자바 코드에 영향X

  • @Column: db 컬럼명과 필드명을 매핑
  • @Temporal: 시간 관련, 요즘에는 그냥 LocalDate 같은 거 쓰면 된다.
  • @Enumerated: Enum 관, 현업에서는 무조건 EnumType.STRING을 써야한다. 다른 타입은 index순으로 들어간다. default가 ORDIBAL이니 조심.

@Column

  • 가장 많이 사용됨
  • name: 필드와 매핑할 컬럼 이름
  • insertable, updatable -> 읽기 전용
  • nullable: null 허용여부 결정, DDL 생성시 사용
  • unique: 유니크 제약 조건, DDL 생성 시 사용
  • length: 길이 제한

@Lob

  • db 필드의 내용이 너무 길면 바이너리 파일로 DB가 바로 밀어넣어야 할 때 사용
  • CROB: Charactor 저장, String 타일에 @Lob하면 CROB이 된다.
  • BROB: Byty 저장, byte 파일에 @Lob하면 BROB이 된다.

@Transient

  • 이 필드는 DB와 매핑하지 않는다는 뜻
  • DB에는 매핑하고 싶지 않은데 객체에는 넣어두고 싶을 때 사용

식별자 매핑

  • @Id
  • @GeneratedValue

GeneratedValue의 전략

  • IDENTITY: 데이터베이스에 위임,MYSQL에서 사용
  • SEQUENCE: 데이터베이스 시퀀스 오브젝트 사용, ORACLE에서 사용, @SequenceGenerator 필요
  • TABLE: 키 생성용 테이블 사용, 모든 DB에서 사용, @TableGenerator 필요
  • AUTO: 방언에 따라 자동 지정, 기본값

권장하는 식별자 전략

  • 기본키 제약 조건: NULL X, 유일, 변하면 안됨
  • 미래까지 기본키 제약 조건을 만족하는 자연키는 찾기 힘들다. 주민 번호도 변할 수 있다. 대체키(인조키)를 사용하자.
  • 권장: Long + 대체키 + 키 생성전략 사용 -> ORM에서는 이게 좋다.

연관관계 매핑

객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다. -조영호(객체지향의 사실과 오해)

객체를 테이블에 맞추어 모델링(참조 대신 외래 키를 그대로 사용)

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    private String name;
    private Long teamId;
      ...
}

@Entity
public class Team {

    @Id @GeneratedValue
    private Long id;
    private String name;
      ...
}

Member 객체에서 Team 객체를 가지고 있는 것이 아니라 teamId를 가지고 있음.

이렇게 테이블에 맞추어 모델링 한다면 조회 시 전부 따로따로 가져와야 함

Member findMember = entityManage.find(Member.class, member.getId());
Long teamId = findMember.getTeamId();

Team findTeam = entityManager.find(Team.class, team.getId());

연관관계가 없기 때문에 하나하나 가져와야 한다. 이건 객체지향적인 스타일이 아니다. 데이터지향적인 방법이다. 협력관계를 만들 수 없다.

연관관계 매핑 이론

단방향 매핑

Member에서만 Team을 갈 수 있다.

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    private String name;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID)"
    private Team team;
      ...
}

member.setTeam(team); 으로 단방향 연관관계 설정, 참조 저장을 할 수 있다.

Member findMember = entityManage.find(Member.class, member.getId());
team findTeam = findMember.getTeam();

바로 연관된 걸 찾아서 쓸 수 있다.

처음에 Member 를 조회하면 한방 쿼리로 Team 까지 전부 조회해 온다. dafault 값이 Eager Loading 으로 되어있기 때문이다.
@ManyToOne(fetch = "FetchType.EAGER)

Lazy Loading 으로 Team 객체가 사용될 때 조회해오게 하고 싶으면
@ManyToOne(fetch = "FetchType.LAZY) 처럼 설정해주면 된다.

김영한님은 현업에서 전부 다 LAZY로 박는 걸 권장한다. 그리고 꼭 필요한 부분에서만 쿼리를 날리는 시점에 원하는걸 최적화해서 가져오는 방법이 있는데 그걸 사용하라고 한다. 자신이 속단해서 최적화 하지 말자.

양방향 매핑

Member와 Team 양쪽에서 왔다 갔다 할 수 있다.

하지만 테이블은 단방향 매핑과 바뀐게 없다. 왜냐하면 DB는 이거면 충분하기 때문이다. DB는 JOIN을 사용해서 항상 양방향이다. 패러다임의 차이

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    private String name;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID)"
    private Team team;
      ...
}

@Entity
public class Team {

    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
      ...
}

개발 잘하는 사람은 설계를 잘하는 사람이다.
설계가 이상하면 미래가 없다.

연관관계의 주인과 mappedBy

객체와 테이블간에 연관관계를 맺는 차이를 이해해야 한다.

객체의 연관관계

  • 팀 -> 회원 (연관관계 1개(단방향))
  • 회원 -> 팀 (연관관계 1개(단방향))

테이블의 연관관계

  • 회원 <-> 팀 (연관관계 1개(양방향))

객체의 양방향 연관관계는 사실 양방향 관계가 아니라 서로 다른 단방향 연관관계 2개를 양방향처럼 보이게 만든 것, 객체는 양뱡향이 없다.

테이블은 외래 키 하나로 두 테이블의 연관관계를 관리, Member.team.id 외래 키 하나로 양방향 연관관계를 가진다.

Member 객체에서 가지고 있는 Team을 바꾸면 테이블의 외래 키가 수정됐었다.
그렇다면 Team 객체에서 가지고 있는 members 에 member를 추가하면 어떻게 될까?

테이블에서 연관관계가 업데이트 되야만 한다.

Member에서도 Team을 수정해도 업데이트 되어야하고 Team에서 Member를 수정해도 업데이트 되어야한다. 테이블에서는 둘 중 뭘 믿어야 할까?

그래서 룰을 정했다. 두 객체는 다르기 때문에, 둘 중에 한 객체를 주인으로 만들고 한 객체는 조회만 할 수 있게 하자.

연관관계의 주인(Owner)

양방향 매핑 규칙

  • 객체의 두 관계 중 하나를 연관관계의 주인으로 지정
  • 연관관계의 주인만이 외래 키를 관리(등록, 수정)
  • 주인이 아닌 쪽은 읽기 즉 조회만 가능
  • 주인은 mappedBy 속성 사용 X
  • 주인이 아니면 mappedBy 속성으로 주인 지정

(mappedBy = "team")은 Member 객체 안의 team에서 매핑이 됐었을 뿐임을 뜻한다.

Team에서 members의 값을 변경해도 아무런 영향을 줄 수 없다.

어떤 객체를 주인으로 해야할까?

  • 외래 키가 있는 곳을 주인으로 정해라.
  • 여기서는 Member.team이 연관관계의 주인

왜 외래 키가 있는 테이블이 주인이여야 할까? 개발자들이 인지를 해야하기 때문이다.
외래키가 없는 테이블이 객체에서 주가 된다면 그 객체를 바꿨을 때 자신도 모르게 다른 테이블에 update문이 날라간다. 개발자들이 멘붕에 빠진다.

모델링은 DB 설계를 잘하고 연관관계의 주인은 외래키가 있는 쪽에다 하면 된다.

순환참조는 안좋은 영향을 많이 끼친다. 사실 양방향 연관관계는 좋은 것이 아니다.
단방향 연관관계가 심플하기 때문에 훨씬 좋다. 원래 모델링 할 때 처음에는 단방향에 주인에만 매핑하고 끝내버린다. 그리고 특수한 경우에만 양방향을 뚫는다.

단방향만으로도 ORM 매핑은 끝난 것이다. 양방향은 조회하는 걸 편하게하려고 부가기능을 추가하는 것 뿐이다.

양방향 매핑시 가장 많이 하는 실수가 연관관계의 주인에 값을 입력하지 않는 경우이다.
Team에다가 member를 추가하고 member에는 team을 추가하지 않는 경우 Member의 외래 키인 TEAM_ID는 null이다.

주인인 관계에서만 set을 해주면된다.
양쪽에서 다 set을 하면 주인이 아닌쪽은 무시된다.

객체에서 생각해보면 두군데에 값을 다 넣어줘야 맞는거다. 그래서 실제 코딩을 할 땐 양쪽 다 셋팅을 해주는 것을 권장한다.

양방향 매핑의 장점

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료
  • 양방향 매핑은 반대 방향으로 조회 기능이 추가된 것 뿐
  • JPQL에서 역방향으로 탐색할 일이 많다.
  • 단방향 매핑을 잘하고 양방향이 필요할 때 추가해도됨(테이블에 영향을 주지 않기 때문)

@ManyToMany 에노테이션은 현업에서는 제약조건이 많기 때문에 잘 사용하지 않는다. 다대다 관계까 생기면 1대다, 다대1로 풀어서 사용해야 한다.

상속 관계 매핑 어노테이션

  • @Inheritance
  • @DiscriminatorColumn
  • @DiscriminatorValue
  • @MappedSuperclass(매핑 속성만 상속)

책에 자세히 나와있다. 사용할 일 있으면 책을 보자.
객체의 상속관계를 모델링할 수 있다.
테이블을 여러개로 쪼갤지, 한 테이블로 퉁 칠지 전략을 선택할 수 있다

복합 키 어노테이션

  • @IdClass
  • @EmbeddedId
  • @Embeddable
  • @MapsId

키를 2개 이상으로 쓰고 싶을 때 사용


영속성 컨텍스트

JPA는 EntityManager Factory 에서 요청이 올 때마다, 쓰레드가 하나 씩 생성돼서 들어올 때 마다 EntityManager를 별도로 생성해야 한다. EntityManager에서는 내부적으로 데이터 커넥션 풀에서 DB를 사용한다.

영속성 컨텍스트란?

  • JPA를 이해하는 데 가장 중요한 용어
  • 엔티티를 영구 저장하는 환경 이라는 뜻
  • EntityManager.persist(entity)를 사용
  • 논리적인 개념이라 눈에 보이지 않는다.
  • EntityManager 를 통해서 영속성 컨텍스트에 접근

J2SE 환경에서는 EntityManager 가 곧 영속성 컨텍스트라고 이해하면 된다. 1:1 관계이다.

J2EE, 스프링 프레임워크 같은 컨테이너 환경에서는 EntityManager와 영속성 컨텍스트가 N:1 관계이다. 같은 트랜잭션이면 같은 영속성 컨텍스트에 접근한다.

EntityManager 를 생성하면 내부에 영속성 컨텍스트가 있구나 정도로 이해하면 된다.

엔티티의 생명주기

  • 비영속 (new/transient) - 영속성 컨텍스트와 전혀 관계가 없는 상태
  • 영속 (managed) -영속성 컨텍스트에 저장된 상태
  • 준영속 (detached) - 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제 (removed) - 삭제된 상태

영속성 컨텍스트의 이점

1차 캐시

  • 글로벌 캐시가 아니다.
  • 쓰레드 하나 시작하고 끝날 때 까지 잠깐 쓰는 것이다.
  • 서로 공유하지 않는다.
  • 트랜잭션이 시작하고 끝날 때 까지만 유지하는 굉장히 짧은 캐시이다.

동일성 보장

  • 1차 캐시 덕분

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

  • 트랜잭션 커밋하는 순간에 INSERT 쿼리 여러 개가 같이 날라간다.
  • 쓰기 지연 SQL 저장소에 INSERT 쿼리들을 모아놓았다가 커밋 시점에 DB에 INSERT 쿼리를 날린다. 즉 버퍼 기능으로 쿼리를 모으는 것.
  • 쓰기 지연 SQL 저장소에서 DB에 쿼리를 날리는 과정을 flush라고 한다.
  • DB와 싱크를 맞추는 것.
  • flush 후에 commit을 한다.
  • JPA는 모든 변경을 트랜잭션 안에서 해결하기 때문에 트랜잭션 commit 시점까지 쿼리를 빨리 보내야 성능상으로도 좋지 않고 의미가 없다.

변경 감지 (Dirty Checking)

  • 영속성 컨텍스트에 있는 객체를 수정하고 EntityManager에서 update를 하지 않아도 커밋하면 자동으로 업데이트 쿼리가 날라간다.
  • 1차 캐시를 생성되는 시점에 스냅샷을 떠둔다. 커밋이나 flush를 하는 시점에 바뀐 객체가 있으면 update 쿼리를 날린다.
  • flush() → 엔티티 스냅샷 비교 → UPDATE SQL 생성 → flush → commit
  • 컬렉션에 있는 객체를 get한 후 값을 수정하면 컬렉션에 있는 객체도 수정된다. 같은 컨셉이다.
  • 스냅샷으로 인해 메모리가 2배로 든다. 데이터를 너무 많이 유지하면 안된다. 최적화하는 방법은 다 있다.

플러시 발생

  • 변경 감지
  • 수정된 엔티티 쓰기 지연 SQL 저장소에 등록
  • 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송(등록, 수정, 삭제 쿼리)
  • 한 번에 네트워크를 타는 것은 아니다. 같은 테이블, jdbc 드라이버 지원, db지원, 하이버네이트 옵션 등의 것들이 맞아 떨어져야 한번에 탄다.

영속성 컨텍스트를 플러시 하는 방법

  • em.flush() → 직접 호출
  • 트랜잭션 커밋 → 플러시 자동 호출
  • JPQL 쿼리 실행 → 플러시 자동 호출

JPQL 쿼리 실행 시 플러시 자동 호출 이유

  • 영속성 컨텍스트에 쌓이고 반영이 안된 객체들과 JPQL 호출 시 DB에 찌른 결과의 불일치 때문
  • flush가 자동 호출되고 JPQL이 실행된다.
  • jpa만 사용할 땐 문제 X, mybatis와 같이 쓸 경우엔 직접 flush를 해줘야 한다.

플러시는?

  • 영속성 컨텍스트를 비우지 않음
  • 영속성 컨텍스트의 변경 내용을 데이터베이스와 동기화하는 목적
  • 트랜잭션이라는 작업 단위가 있기 때문에 플러시가 가능하다. → 버퍼 유지를 하다가 커밋 직전에만 동기화 하면 됨

준영속 상태란?

  • 영속 상태인 엔티티가 더 이상 영속 상태에서 관리가 안된다.
  • 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)
  • 영속성 상태가 제공하는 기능을 사용 못함. → 변경감지, 1차 캐시 등등
  • 더 이상 JPA가 관리하지 않는다. 값을 수정해도 update 쿼리가 안날라감. 삭제도 마찬가지

준영속 상태로 만드는 방법

  • em.detach(entity) → 특정 엔티티만 준영속 상태로 전환
  • em.clear() → 영속성 상태를 완전 초기화
  • em.close() → 영속성 컨텍스트를 종료, 쓰기 지연 저장소 포함 전부 날라간다.

지연 로딩(Lazy Loading)

  • Member를 조회할 때 Team을 조회해야 될까?
  • Team을 사용하지 않는다면 그럴 필요 없다. → Lazy Loading(지연 로딩)을 해야 한다.
  • 애플리케이션 로딩 시점에 Team은 Proxy 객체, 즉 가짜 객체가 들어온다. 가짜 클래스를 만들어낸다. Team을 실제 사용하는 시점에 값을 초기화
  • Member와 팀을 자주 사용한다면 EAGER 로딩을 걸면되는데, 현업에서는 조회할 때 fetchjoin을 한다. 한번에 가져올 수 있다. 시점에 따라 쿼리를 다르게 칠 수 있다.

프록시와 즉시 로딩 주의

  • 가급적 지연 로딩을 사용
  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생, Member 조회 → Team 조회 → Team 타입 조회 등등
  • 즉시 로딩은 JPQL에서 N + 1 문제를 일으킨다.
  • @ManyToOne, @OneToOne 은 기본이 즉시로딩 → LAZY로 설정해라.
  • @OneToMany, @ManyToMany는 기본이 지연 로딩
  • 그래서 Lazy Loading을 사용해야 하고 대부분 사용하게 되는데, 준영속 상태가 되면 사용할 수가 없다.

준영속 상태와 지연 로딩

@Test
    void persist() {
        Member findMember = em.find(Member.class, member.getId());

        em.close();

        Team findTeam = findMember.getTeam();
        findTeam.getName();
    }
  • member가 준영속 상태가 됐는데 team을 실제로 사용했기 때문에 지연로딩이 실행 되고 LazyInitalizationException이 터진다 → 현업에서 자주 볼 예외
  • 지연 로딩을 하려면 영속성 컨텍스트에서 꼭 관리해야 한다. 영속성 컨텍스트에서 DB 커넥션도 다 들고 있기 때문이다.
  • 현업에서는 스프링 프레임워크에서 한 메서드가 트랜잭션이 끝난시점에 컨트롤러에서 Lazy Loading 하려고 할 때 발생한다.
  • 필요하면 준영속 상태가 되기 전에 터치를 해두던가 미리 로딩을 해두는 식으로 해결하면 된다.

JPA와 객체지향 쿼리

JPA는 다양한 쿼리 방법을 지원

JPQL (Java Persistence Query Language) ★

  • JPA에서 지원하는 공식 쿼리, SQL 표준 문법과 똑같다.
  • 객체 그래프 탐색 지원
  • JPA를 사용하면 엔티티 객체를 중심으로 개발하는데 문제는 검색 쿼리, 검색 할 때도 테이블이 아닌 객체를 대상으로 검색,해야 한다. 자바에서 테이블을 알면 안된다.
  • 모든 DB 데이터를 객체를 변환해서 검색하는 것은 불가능하기 때문에 결국 검색 조건이 포함된 SQL이 필요하다. 그게 JPQL이다.
  • JPA는 SQL을 추상화한 JPQL 이라는 객체 지향 쿼리 언어 제공
  • 기존 SQL은 테이블 대상으로 쿼리를 날리기 때문에 JPQL과 차이가 있다.
// 검색
String jpql = "select m From Member m where m.name like '%hello'";

List<Member> result = em.createQuery(jpql, Member.class).getResultList();
  • 테이블이 아닌 객체를 대상으로 검색하는 객체 지향 쿼리
  • SQL을 추상화해서 특정 데이터베이스 SQL에 의존 X
  • JPQL을 한마디로 정의하면 객체 지향 SQL
  • JPQL은 객체 관련 대소문자를 구분
  • 엔티티 이름을 사용한다. 테이블 이름 X
  • 별칭은 필수(m)
  • orderBy를 무조건 해주자. 모든 DBMS로 방언이 다 동작하는데 오라클 같은 경우엔 orderBy를 명시 안해주면 장애가 날 수 있다.
  • JOIN에서 문법이 살짝 다르다. 쓸 일이 있으면 공부해서 쓰자.
  • 현업에서는 페치 조인을 많이 쓴다.LAZY Loadding으로 인해 성능 최적화를 위해 쿼리를 직접날릴 때 사용, EAGER를 걸은 것 처럼 사용 가능
  • 사용할 일이 있으면 특히 페치 조인을 공부해서 쓰자.
  • 실제 현업에서는 List 같은 거 여러개 뿌릴 때 페치 조인을 안하면 루프를 돌때마다 레이지 로딩을 하고 쿼리가 너무 많이 나가서 성능이 망한다. n+1 쿼리가 나간다. Memeber 조회하는 쿼리를 날리고 getTeam할 때 마다 Lazy로 인해 쿼리가 나간다.
  • @NamedQuery를 쓰면 컴파일 시점에 SQL 문법 오류를 잡을 수 있다. 쓰지 않으면 사용자가 직접 쓸 때 에러가 나게된다. JPA를 쓰면 @Query를 쓰면 똑같다. 문법에러를 잡아준다. 런타임 오류가 가장 나쁜 오류다.

JPA Criteria

QueryDSL ★

네이티브 SQL

JDBC API 직접 사용, MyBatis, Spring JDBC Template 함께 사용


JPA 기반 프로젝트

Spring Data JPA

  • 지루하게 반복되는 CRUD 문제를 세련된 방법으로 해결
  • 개발자는 인터페이스만 작성
  • 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입
  • Spring Data JPA 적용전에는 em.save 같은 코드를 직접 구현해줬어야 했다.
  • @Query 로 JPQL 정의 가능
  • 메서드 이름으로 쿼리 생성 가능
// interface에 변수 이름으로 선언만 해줘도 쿼리를 생성해서 날려준다.
public interface MemberRepository extends JpaRepository<Member, Long> {
    Member findByName(String name);
}
  • 메서드 이름과 일치하는 인스턴스 변수가 없으면 애플리케이션 로딩 시점에 다 잡아낸다. 이름으로 검색+정렬+페이징 까지 가능하다. 페이징을 다해줘서 굳이 안만들어줘도 된다.
  • Web 도메인 클래스 컨버터 가능, 컨트롤러에서 식별자로 도메인 찾음
@GetMapping("/members/{memberId}")
public Member getMember(@PathVariable("memberId") Member member) {
    return member;
}

JPA에서 Spring Data JPA는 옵션이아닌 필수이다.

QueryDSL

  • SQL, JPQL을 코드로 작성할 수 있도록 도와주는 빌더 API
  • JPA Criteria에 비해서 편리하고 실용적임
  • 오픈소스이다.
  • SQL, JPQL의 문제: 문자이다. Type-Check가 불가능, 컴파일 시점에 알 수없는 문자열, 자바의 한계 해당 로직 실행 전까지 작동 여부 확인 불가
  • 문자가 아닌 코드로 작성
  • 컴파일 시점에 작동 여부를 확인하는 것이 핵심, 컴파일 시점에 문법 오류 발견 가능
  • 코드 자동 완성(IDE 도움)
  • 단순 하고 쉬움, 코드 모양이 JPQL과 거의 비슷
  • 동적 쿼리이다.
@PersistenceContext
EntityManager em;

public void helloQueryDSL() {
    JPAQueryFactory query = new JPAQueryFactory(em);
        Qmember m = Qmember.member;

        query.selectFrom(m)
                 .where(m.age.gt(18).and(m.name.contains("hello"))))
                 .orderBy(m.age.desc())
         .fetch();
}
  • 진짜 장점을 다시 말하면 IDE의 도움을 받는 것, 쿼리 때문에 실수할 일 자체가 없다. 컴파일 타임에 잡아주니까
  • 페이지네이션도 다 가능하다.
  • 다른 것보다 QueryDSL을 쓰는 진짜 이유는 동적 쿼리 때문이다. JPQL은 정적 쿼리만 지원한다. 문자를 더하기 해야 한다.
  • DTO로 원하는 걸 찍어서 translate하는 게 다 잘 되어있다. 통계 쿼리 같은 복잡한 것은 전부 QueryDSL로 한다. 다 지원을 해준다.
  • QueryDSL은 자바다. 메서드로 뽑아서 where문에 넣을 수 있다. 가독성이 좋아진다. 이쁘게 만들 수 있다. 정말 중요하다.

실무 경험 공유

  • 테이블 중심에서 객체 중심으로 개발 패러다임이 변화
  • 유연한 데이터베이스 변경의 장점과 테스트
    • Junit 통합 테스트시에 H2 DB 메모리 모드
    • 로컬 PC에서는 H2 DB 서버 모드로 실행
    • 개발 운영은 MySql, Oracle
  • 데이터 베이스 변경 경험(MySql 도중 → Oracle로 바뀐 적 있다.)
  • 테스트, 통합 테스트시에 CRUD는 믿고 간다. → 중요하다. 테스트 안해봐도 된다.
  • 빠른 오류 발견 → 컴파일 시점, 늦어도 애플리케이션 로딩 시점
  • 최소한 쿼리 문법 실수나 오류는 거의 발생하지 않는다.
  • 대부분이 비즈니스 로직 오류이다,

성능

  • JPA 자체로 인한 성능 저하 이슈는 거의 없음, 대부분이 JPA를 잘 이해하지 못해서 발생
    • 즉시 로딩: 쿼리가 튐 → 지연 로딩으로 변경
    • N+1 문제: 대부분 패치 조인으로 해결 가능

생산성

  • 단순 코딩 시간 줄어듦 → 개발 생산성 향상 → 잉여 시간 발생
  • 비즈니스 로직 작성 시 흐름이 끊기질 않음
  • 남는 시간에 테스트 더 많이 작성
  • 남는 시간에 기술 공부, 코드 리팩토링
  • 팀원들 대부분은 과거로 돌아가고 싶어하지 않음
  • 과거의 코드량에서 개발 코드량이 3/1로 줄어버렸다.

자주하는 질문

  1. ORM 프레임워크를 사용하면 SQL과 데이터베이스는 잘 몰라도 되나요?
    • 더 잘알아야 한다. 오히려 SQL, DB를 모르고 쓴다는 것이 말이 안되는 것, ORM이라는 것은 객체지향, DB 둘 다 잘아는 사람이 편하게 쓰려고 중간에서 맵핑을 해주는 것.
  2. 성능이 느리진 않나요?
    • 잘 쓰면 최적화 할 수 있는 포인트가 더 많다.
  3. 통계 쿼리처럼 매우 복잡한 SQL은 어떻게 하나요?
    • 거의 다 QueryDSL로 DTO로 바로 뽑고, 진짜 안될때는 네이티브 쿼리를 쓴다.
    • 토비의 스프링에 그런 내용이 있다.
  4. MyBatis와 어떤 차이가 있나요?
    • SQL을 직접 안짜도 된다.
  5. 하이버네이트 프레임워크를 신뢰할 수 있나요?
    • 쿠팡, 배민에서 기본으로 깔고간다. 조 단위의 거래를 책임지고 트래픽도 엄청난다. 다 JPA로 가능하다.
  6. 제 주위에는 MyBatis만 사용하는데요?
    • JPA를 할 줄 아는 사람이 없어서 그렇다.
  7. 학습 곡선이 높다고 하던데요?
    • 높다. 제대로 일주일 공부하고 평생 시간을 아껴라.
    • 어려운 몇가지 포인트를 극복하면 생산성이 폭발한다.

표준 스택

  • Java 8
  • Spring Framework, 요새는 SpringBoot
  • JPA, Hibernate
  • Spring Data JPA
  • QueryDSL
  • JUnit, Spook(Test)

'Spring' 카테고리의 다른 글

@Mock vs @MockBean  (0) 2020.08.14
MockMvc VS RestAssured  (0) 2020.08.14
용어 정리  (0) 2020.04.21
Web server failed to start. Port 8080 was already in use 에러  (9) 2019.11.07
Mybatis, Invalid bound statement (not found) 에러  (0) 2019.11.05