[JPA]1. 기본개념

인프런에서 백기선님의 스프링 jpa 강의를 보고 나름대로 정리한 것입니다
디테일한 정보를 얻기 위해선 강의를 수강하는 것이 더 좋을 것입니다

JPA 학습 이유

  • 도메인 주도 개발 가능
    • 어플리케이션 코드가 SQL DB관련 코드에 잠싱 당하는 것을 방지
    • 도메인 기반의 프로그래밍으로 비지니스 로직을 구현하는데 집중할 수 있음
    • 개발 생산성이 좋으며 DB에 독립적인 프로그래밍이 가능
    • 타입 세이프한 쿼리 작성 가능
    • Persistent Context가 제공하는 캐시 기능으로 성능 최적화까지 가능
    • 단점: 높은 학습 비용

관계형 DB와 자바

  • 데이터를 영속화(Persistent) -> 가장 일반적인 방법 -> 관계형 db
  • JDBC
  • 자바의 DB용 라이브러리

  • 인터페이스로 되어있으며 드라이버만 바꿀 수 있음.

  • DriverManager를 사용하여 db소스에 연결

    • 소스코드에 JDBC 드라이버 클래스 이름및 URL 직접 입력해야 해서 이식성 떨어짐.
  • DataSource

    • 기본연결, 커넥션풀링, 트랜잭션 처리까지 담당, 벤더별로 구현되어 있음
    • 안에서 외부나 혹은 프로퍼티를 File클래스로 읽거나 해서 처리 가능
  • preparedstatement

    • 쿼리와 파라미터 정의
    • execution 실행
  • SQL

    • DDL : Definition -> 스키마, 테이블 생성, 인덱스 생성및 조작
    • DML : manipulation -> 데이타의 crud
jdbc를 이용하는 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package me.rkaehdaos;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class Application {
public static void main(String[] args) throws SQLException {
String url = "jdbc:postgresql://192.168.0.79:5432/springdata";
String user = "ahn";
String pass = "pass";

try(Connection connection = DriverManager.getConnection(url,user,pass)) {
System.out.println("connection is created?: "+ connection);

//String sql ="CREATE TABLE ACCOUNT(id int, username varchar(255), password varchar(255));";
String sql ="INSERT INTO ACCOUNT VALUES(1, 'GeunChang', 'pass');";
try(PreparedStatement statement = connection.prepareStatement(sql) ){
statement.execute();
}
}
}
}
  • 무엇이 문제인가?
    • 테이블 생성이 번거롭다.
    • 테이블에서 꺼낸 데이터를 도메인에 매핑하는 과정도 번거롭다.
    • SQL을 실행하는 비용이 비싸다. (커넥션등 )
    • SQL이 데이터베이스 마다 조금씩 다르다. 잘못하면 다시 짜야할 수도 있다.
    • 스키마가 바뀌면 코드가 많이 바뀐다.
    • 반복적인 코드가 너무 많다. try-catch일 때는 헬이다
    • 당장 필요가 없는데 언제 쓸 줄 모르니까 미리 다 읽어오게 된다.
      • 필요할 때 (출력하거나 이용하는 런타임때) 가져오는게 효과적임
      • DB SQL 특성상 lazy loading 전략 쓰기가 어렵다.

ORM : Object Relation Mapping

JDBC 사용
1
2
3
4
5
6
7
try(Connection connection = DriverManager.getConnection(url, username, password)) {
System.out.println("Connection created: " + connection);
String sql = "INSERT INTO ACCOUNT VALUES(1, 'ahn', 'pass');";
try(PreparedStatement statement = connection.prepareStatement(sql)) {
statement.execute();
}
}
도메인 모델 사용예제. ORM툴로 궁극적으로 원하고자 하는 방법
1
2
Account account = new Account(“ahn”, “pass”);
accountRepository.save(account);
  • JDBC 대신 도메인 모델을 사용하려 하는 이유

    • OOP의 장점을 활용하기 좋다.
    • 각종 디자인 패턴
    • 코드 재활용성
    • 비지니스 로직 구현 및 테스트가 편함
  • ORM의 정의

    • 어플리케이션의 클래스와 SQL DB의 테이블 사이의 매핑 정보를 기술한 메타데이터
      를 사용하여, 자바 어플리케이션의 객체를 DB테이블에 자동으로(또한 깨끗하게) 영속화 해주는 기술.
    • 깨끗하게 = 비침투적(trasparent) –> 논란의 여지가 있음. 완전 비침투적은 아님
    • 자동으로 = SQL 자동 생성
  • 장점

    • 생산성: 매핑정의만 하면 정말 쉽고 빠르게 데이타를 넣고 뺄 수 있음
    • 유지보수성: 코드가 아주 간결. 정말 로직만 보이게 됨
    • 성능: SQL 단건만 보면 느릴수 있지만,성능 최적화 방법이 여럿 있으며, 캐쉬가 있어서
      불필요한 쿼리를 아예 날리지 않는다.
    • 벤더 독립성: dialect만 바꾸면 되고 코드가 아주 바뀌지 않는다.
  • 단점

    • 무시무시한 학습비용
      • SQL도 잘 알아야 함.
      • ORM이 어떻게 쿼리가 발생되는지 알아야 커스터마이징하고 성능튜닝이 가능하다.
      • 쉬운 프레임워크가 아니며 공부하기 어렵고 시간이 오래걸리는 프레임워크
      • 하이버네이트에서 성능 못뽑으면 학습이 부족한거임

ORM 패러다임 불일치

  • 객체를 릴레이션에 매핑하는 과정에서 일어나는 문제들
  • 해결하기 쉽지 않은 문제들
  • ORM이 학습하기 어려운 이유
  1. 밀도의 문제
  • 객체
    • 다양한 크기의 객체 생성이 가능
    • 커스텀한 타입 정의가 쉽다.
  • 릴레이션
    • 테이블 또는 릴레이션타입 또는 릴레이션 컬럼의 컬럼타입
    • UDT(user defined type)은 db간 호환 보장없음, 비추
  1. 서브타입 문제
  • 객체
    • 상속 구조 만들기가 쉽다.
    • 이렇게 만든 객체를 다형성으로 참조도 쉬움
  • 릴레이션
    • 테이블 상속개념 없음
    • 상속기능 구현했어도 표준 기술이 아님
    • 다형적 관계를 표현할 방법이 없음
  1. 식별성(Identity) 문제
  • 객체
    • 레퍼런스동일성(==)
    • 인스턴스 동일성(equals()메소드)
  • 릴레이션
    • 주키(Primary Key) 개념
    • 두레코드의 PK가 같아야 같은 레코드로 판단
    • 이런 PK의 개념이 객체쪽엔 또 없음.
  1. 관계(Association) 문제
  • 객체
    • 객체 레퍼런스로 관계표현
    • 근본적으로 방향존재
    • n:m[다대다] 관계를 가질 수 있음
  • 릴레이션
    • 왜래키(Foreign key)로 관계 표현
    • “방향”이라는 의미가 없음, 그냥 Join으로 아무거나 묶을 수 있음.
    • 태생적으로 다대다 관계를 만들 수가 없다.
    • 조인 또는 링크 테이블을 사용해서 2개의 1:n 관계로 풀어야 한다.
  1. 데이터 네비게이션(Navigation)의 문제
    -> 가장 복잡하고 어렵고 성능에도 영향을 주며 해결책도 각각 달라 쉽지 않은 문제
  • 객체
    • 레퍼런스를 이용해서 다른 객체로 이동 가능
    • 콜렉션 순회도 가능
  • 릴레이션
    • 릴레이션에서 그런 방식은 데이터 조회에 있어서 매우 비효율적
    • DB에 요청을 적게 할 수록 성능이 좋기 때문에 Join을 쓴다.
    • Join을 해도 달라지는 경우의 수가 너무 많다.
    • DB 커넥션 자체가 매우 비싼 비용
      -> 한 트랜잭션 내의 쿼리를 줄이는게 성능 유리
    • 그렇다고 너무 많은 join은 메모리 비효율적
    • 쓰지도 않는 데이터 로딩하는 것은 db에도 어플리케이션 메모리에도 부하가 걸린다.
    • lazy loading: A만 가져오고 B를 조인하지 않고 필요시 그때마다 select?
    • n+1 select problem : 하이버네이트에서 가장 흔한 문제중 하나
      • 무려 n+1개의 쿼리가 실행 됨
      • 해결책이 여러가지가 있음

JPA 기초 샘플

  • 일반적으로 스프링 data jpa를 사용하고 직접 하이버네이트, jpa를 사용할일 없음
  • 공부는 필요.
  • starter-data-jpa
    • 수많은 일들이 이루어짐.. 수많은 의존성들
    • 예를들어 dbcp등도 자동설정에 의해서 DataSource가 만들어짐
    • 자동설정에 의해 참조될 정보들은 필요함
      application.properties
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      spring.datasource.url=jdbc:postgresql://192.168.0.79:5432/springdata
      spring.datasource.username=ahn
      spring.datasource.password=pass

      #create: 어플리케이션 구동 될때마다 새로 스키마 생성, 개발할때 유용
      #validate: 스키마는 만들어져 있다는 가정하에, 잘 매핑이 되어있는지 검증
      # 매핑오류시 에러. 운용시에 아주 유용.
      # 스키마 생성 하는 부분을 마이그레이션 툴등으로 직접 관리하는
      # 경우에는 Validate로 개발하는 것이 좋다.
      #update: 기존 데이타 유지하면서 계속 추가 할때 유용.
      # 예를들어 기존 데이타가 존재하는데 새로 Entity에 필드를 추가하는 경우
      # 기존 데이타에도 해당 필드가 컬럼으로 추가가 된다. 그러나 주의 할 것이
      # 추가한 필드를 삭제 한다고 해서 테이블의 컬럼이 사라지진 않는다는 것이다.
      # 이처럼 스키마가 지저분해질 수 있으니 설마 개발에 필요해서 update를
      # 사용한다 하더라도 중간중간에 create를 사용해서 스키마를 비워주자.
      # 또한 이미 만들어진 컬럼은 타입을 바꿔도 바뀌지 않는다.
      # 또한 rename은 해당 컬럼이 바뀐는게 아니라 새로 컬럼이 추가됨을 유의하자
      # 이런 경우 DB 마이그레이션 툴을 사용하자.

      spring.jpa.hibernate.ddl-auto=create
      Account.java DB 테이블과 매핑되는 엔티티 클래스
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40

      import javax.persistence.Entity;
      import javax.persistence.GeneratedValue;
      import javax.persistence.Id;

      @Entity //이 클래스는 Account 테이블과 매핑된다는 뜻
      public class Account {
      @Id //DB PK에 매핑이 된다는 뜻
      @GeneratedValue //자동으로 생성되는 값을 사용하겠다는 뜻
      private Long id;

      //@Entity 가 붙은 클래스의 멤버 변수들은 테이블에 자동으로 매핑이 된다.
      //사실상 @Column이 생략된것으로 볼 수 있다.
      private String username;
      private String password;

      public Long getId() {
      return id;
      }

      public void setId(Long id) {
      this.id = id;
      }

      public String getUsername() {
      return username;
      }

      public void setUsername(String username) {
      this.username = username;
      }

      public String getPassword() {
      return password;
      }

      public void setPassword(String password) {
      this.password = password;
      }
      }
JPA 오퍼레이션을 하는 러너 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
@Transactional
// EntityManager에 관련된 모든 오퍼레이션은 한 트랜잭션 안에서
public class JpaRunner implements ApplicationRunner {

@PersistenceContext //JPA 어노테이션
EntityManager entityManager; //JPA 가장 핵심 클래스

@Override
public void run(ApplicationArguments args) throws Exception {
Account account = new Account();
account.setUsername("rkaehdaos");
account.setPassword("jpa");
entityManager.persist(account); //영속화: db에 저장
}
}

이러면 데이터는 들어가는데 워닝?에러?가 발생한다.
Caused by: java.sql.SQLFeatureNotSupportedException: Method org.postgresql.jdbc.PgConnection.createClob() is not yet implemented.
이는 현재 포함한 postgresql Driver가 해당 메소드를 구현하지 않아서다.
이 에러를 없애려면 application.properties에 다음과 같이 추가한다.

위의 에러를 막아주는 방법
1
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true

이 Runner의 내용이 가장 핵심적인 JPA API이며 JPA 밑단에는 하이버네이트가 돌아간다.
그래서 사실상 하이버네이 API도 사용할 수 있다.

하이버네이트 사용 예제
1
2
3
4
// entityManager.persist(account); //위의 JPA대신 아래와 같이 하이버네이트 가능
Session session = entityManager.unwrap(Session.class);
session.save(account);
// Session은 org.hibernate.Session이다.

JPA 기초 : 엔티티 매핑

JPA 의 가장 기초

  • @Entity

    • ‘엔티티’는 객체 세상에서 부르는 이름
    • 보통 클래스와 같은 이름을 사용하기 때문에 값을 변경하지 않음.
      필요한 경우 name에 따로 줄 수 있음. 언제?
      • User라는 엔티티가 있는데 특정 DB의 경우(postgresql)
        User라는 테이블을 만들 수가 없다.
    • 엔티티의 이름은 JPQL에서 쓰임
    • @Entity 이기에 Table과 매핑된다. 사실상 @Table 생략되어있다
  • @Table

    • ‘릴레이션’세상에서 부르는 이름
    • @Entity 이름이 기본. @Entity의 name에 따로 이름을 주었으면 그걸 따라감
    • 테이블의 이름은 SQL에서 쓰임
  • @Id

    • 엔티티의 주 키를 매핑할 때 사용
    • 자바의 모든 primitive 타입과 그 래퍼 타입을 사용할 수 있음
      • Date, BigDecimal, BigInteger도 사용 가능
      • 가능한 wrapper를 쓰자.
        • 예를들어 id가 0인 데이타와 새로 new로 만든 data 구분?
        • 래핑을 쓰면 id가 null이므로 구분이 쉽다.
    • 복합 키를 만드는 매핑 방법도 있음.. 나중에..
  • @GeneratedValue

    • 주키 생성 방법을 매핑
    • 생성 전략과 생성기를 설정 가능
      • default : AUTO -> 사용하는 DB에 따라 적절한 전략 선택
      • TABLE, SEQUENCE, IDENTITY중 하나
      • AUTO 말고 사용할 경우 strategy 에 원하는 것 설정
      • 대부분 기본 설정을 이용
  • @Column

    • 엔티티의 모든 멤버에는 이것이 붙어있는 거나 다름없음
    • 밑의 설정등을 기본값에서 바꿀때는 명시적으로 어노테이션 사용후 값 설정
    • unique
    • nullable
    • length
    • columnDefinition : 반드시 반드시 필요한 경우 직접적으로 명시 가능
    • Getter/Setter가 없어도 컬럼으로 매핑 가능
  • @Temporal

    • java.util.Date, java.util.Calendar 지원.
    • 현재 2.1까지
    • 만약 자바 8의 LocalDate를 쓰려면?
      • 매핑은 가능. 공부가 필요
      • 커스텀 매핑, 커스텀한 컨버터를 등록해서 어떤식으로 컨버팅 할지 정의
      • 나중에 고급수준으로 쓸 수 있는 방법이므로 나중에 공부
    • 추가
      • 스프링 부트 2.1.1 에서 하이버네이트 5.7 사용하고 jpa2.2 구현
      • 어노테이션 없이 자바 8의 LocalDate도 자동 매핑 확인 했음.
  • @Transient

    • 해당 필드를 컬럼으로 매핑을 안해줌
  • SQL 보기

    • spring.jpa.show-sql=true
      sql을 콘솔로 찍어준다.
    • spring.jpa.properties.hibernate.format_sql=true
      포맷팅해서 들여쓰기등으로 긴 쿼리를 보기 쉽게 해준다.
    • 개발시에는 거의 필수
    • 운영시에도 나쁘지 않음- ? 매핑되는 값이 나오지 않으므로
    • ? 값은? 로거쪽 설정으로 보게 할 수 있다.
      1
      logging.level.org.hibernate.type.descriptor=trace

JPA 기초 : Value 타입 매핑

  • 엔티티 타입과 Value 타입 구분

    • 엔티티타입 : Account 엔티티는 id라는 고유한 식별자를 가진다.
      다른 엔티티에서도 Account 엔티티를 독립적으로 참조할 수 있다.
    • Value타입 : Account.username, Account.password는 Account 엔티티를 통해서만
      접근 가능.
  • Value 타입 종류

    • 기본 타입 : String, Date, Boolean ….
    • Composite Value Type : 기본단위보다 좀 더 큰
      • 예를 들어 주소, 엔티티로 하기엔 애매, 엔티티로 만들 수도 있지만..
      • Account에 속한 하나의 Data로 Address로 처리 하려 할 때
      • 어떤 엔티티클래스에 에 종속적인 클래스타입
      • 매핑
        • @Embadable
        • @Embadded
        • @AttributeOverrides
        • @AttributeOverride
    • Collection Value 타입 (따로 공부 필요)
      • 기본 타입의 콜렉션
      • 콤포팃 타입의 콜렉션
Composite Value 타입 선언
1
2
3
4
5
6
7
8
9
10
11
import javax.persistence.Embeddable;

@Embeddable // 이를 붙이면 Composite Value 타입으로 임베디드 될 수 있다.
public class Address {
//@Column 생략 되어 있음
// 원하면 어노테이션을 써서 더 커스터마이징 가능
private String street;
private String city;
private String state;
private String zipCode;
}
Composite Value를 엔티티에서 추가하는 방법
1
2
@Embedded
private Address address;

이렇게 하고 실행하면 자동으로 Adress하위의 컬럼을 만들고 매핑이 된다.
만약 Adress 타입을 여러개 쓰고 싶으면 어떻게 해야할까?

Composite Value를 엔티티에서 각각 다른 변수로 추가하는 방법
1
2
3
4
5
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "street", column = @Column(name = "home_street"))
})
private Address homeAdress;

JPA 기초 1:n 매핑

  • 관계에는 항상 두 엔티티가 존재

    • 하나는 그 관계의 주인(owning)이고
    • 다른 하나는 종속된(non-owning)쪽이다.
    • 해당 관계의 반대쪾 레퍼런스를 가지고 있는 쪽이 주인
  • 단방향에서의 관계의 주인 : 관계를 정의한 쪽이 그 관계의 주인, 명확

  • 1:N

    • 매우 흔한 방식
    • 실제 DB 설계시 자주 사용
    • ex) 부모-자식 관계, 게시글과 댓글과의 관계
    • 계층 구조로도 이해 가능
    • 게시글과 댓글이 있을때 댓글의 정보에는 어떠한 게시물에 대한 댓글인지 정보가 필요
    • 댓글 테이블에 게시글의 pk를 FK로 가져오게 된다
  • 단방향 @ManyToOne 예제

    • 기본값은 FK 생성
    • Study에 작성자를 Account로 가지고 있다고 하자.
    • 이 경우 Study에서 Accout의 pk를 FK로 가져와야한다
작성자 한명이 여러 Study를 작성 할 수 있으므로 ManyToOne
1
2
3
4
5
6
7
@Entity
public class Study {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne //단방향 관계 정의
private Account owner;
이처럼 owner_id가 FK로 constraint가 걸리는 부분이 확인된다.
1
2
3
4
alter table if exists study
add constraint FK210g5r7wftvloq2ics531e6e4
foreign key (owner_id)
references account
  • 단방향 @OneToMany 예제
    • 위의 정보를 이제 Account에서 처리한다고 생각해보자
    • 햇갈리면 애노테이션의 앞과 끝을 보면서 확인
    • 끝이 many로 끝났으므로 해당 애노테이션은 Collection에 사용 가능하다
account에서 study set을 관리
1
2
@OneToMany //account하나는 여러 study를 작성 가능하므로
private Set<Study> studies= new HashSet<>();
JpaRunner의 run()메소드 부분
1
2
3
4
5
6
7
8
9
10
11
12
Account account = new Account();
account.setUsername("rkaehdaos");
account.setPassword("jpa");

Study study = new Study();
study.setName("Spring Data JPA");

//study.setOwner(account); // 주인이 바뀌었다.
account.getStudies().add(study); //주인쪽에서 관계설정.

entityManager.persist(account); //영속화: db에 저장
entityManager.persist(study);
결과: @OneToMany는 Join 테이블까지 3개를 만든다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
create table account (
id int8 not null,
date timestamp,
date2 date,
city varchar(255),
state varchar(255),
home_street varchar(255),
zip_code varchar(255),
password varchar(255),
username varchar(255) not null,
yes varchar(255),
primary key (id)
)

create table account_studies (
account_id int8 not null,
studies_id int8 not null,
primary key (account_id, studies_id)
)

create table study (
id int8 not null,
name varchar(255),
primary key (id)
)

/*이건 entity의 컬럼에 username에 대한 unique 설정을 했기 때문 */
alter table if exists account
add constraint UK_gex1lmaqpg0ir5g1f5eftyaa1 unique (username)

alter table if exists account_studies
add constraint UK_tevcop76y9etp9vx5vce7gns6 unique (studies_id)

alter table if exists account_studies
add constraint FKem9ae62rreqwn7sv2efcphluk
foreign key (studies_id)
references study

alter table if exists account_studies
add constraint FK4h3r1x3qcsugrps8vc6dgnn25
foreign key (account_id)
references account
  • 조인테이블까지 3개가 생성
  • unique id마다 constraint
  • 양쪽 서로로 FK 설정이 들어감
  • 관계정보는 account_studies라는 조인테이블에 들어감

주의

  • 만약 위의 2가지 단방향이 동시에 쓰이면 어떨까?
    account에서 Set studies를 @OneToMany로 가지고 있으면서 동시에,
    Study에서도 Account owner를 @ManyToOne으로 가지면 이게 양방향일까?

  • 답은 NO!

  • 이것은 2개의 단방향일 이다

  • 그러면 양방향은?

  • 양방향

    • FK를 가지고 있는 쪽이 오너, 따라서 기본값은 @ManyToOne을 가진 쪽이 주인이다
    • 양방향을 설정하기 위해서는 주인이 아닌 다른 쪽에서(여기서는 @OneToMany있는 쪽)
      mappedBy를 사용해서 관계를 맺고 있는 필드를 설정해야 한다.
    • 그래야 불필요한 제약등이 생성됨을 막을 수 있다
Account.java 현재 반대쪽 Account 타입인 owner와 매핑, 이로써 양방향 설정
1
2
@OneToMany(mappedBy = "owner")
private Set<Study> studies= new HashSet<>();
  • mappedBy를 사용하지 않았을때 서로 주인이며 서로 단방향이었다면 이 mappedBy를
    사용함으로써 주인을 명시해주는 것이다. 이러면 studies는 주인이 아니고 종속된다
  • 이렇게 해야 필요없는 스키마와 필요없는 중복 데이터가 발생하지 않는다.
  • 즉 현재 양방향 관계에서 Study가 관계에서 주인이다.
  • 조인테이블을 이용하면 이와 다른 관계가 되는 방법이 있는데 나중에 공부하자
  • 또한 양방향 예제 실행전에 account_studies 로 만들어진 조인 테이블을 삭제한다
    예제를 다시 실행하면 현재 사용하는 테이블만 드랍 후 다시 만들기 때문에 저 조인
    테이블이 삭제가 안된다. 나머지 두 테이블은 알아서 드랍되므로 놔둔다.

아직 문제가 남아 있다. 위의 단방향 @OneToMany예제를 실행하는 부분을 살펴보면

jpaRunner의 Run()메소드에서 관계 설정하는 부분
1
2
//study.setOwner(account); // 1. 현재 주석 처리 하였다.
account.getStudies().add(study); // 2. account쪽에 관계를 설정하고 있다.

이처럼 account에 관계를 정의하기 때문에 실행하면 에러는 없지만 실제 db를 열어보면
study 테이블의 FK에 정보가 들어가 있지 않는 것을 알 수 있다.원래 1번으로 고쳐주면
정상적으로 FK에 정보가 들어감을 알 수 있다.
그럼 이제 맞는것일까? 그럼 또 관계가 양방향이 아님을 알 수 있다.
정답은 1,2 그대로 남겨두는 것이다. 1번은 반드시 필요하다. 2번은 Optional이나
객체 지향적으로 생각하면 양방향의 관계에서 서로의 레퍼런스를 가지고 있는 일은 반드시 해줘야할 일이다. 사실 한 묶음으로 만들어서 관계 한쪽에 놓고 사용하게 해야 한다.
한번에 되게 리팩토링을 해보자.

jpaRunner의 Run()메소드에서 관계 설정하는 부분
1
2
3
//study.setOwner(account);
//account.getStudies().add(study);
account.addStudy(study);
Account.java
1
2
3
4
5
6
7
8
9
//새로 메소드를 추가해서  위 1,2를 한번에 같이
public void addStudy(Study study) {
this.getStudies().add(study);
study.setOwner(this);
}
//비워주는 것도 마찬가지로 만들어 준다.
public void removeStudy(Study study) {
this.getStudies().remove(study);
study.setOwner(null);
  • 이러한 관계를 관리하는 메소드를 convenient method라고 한다
  • add뿐 아니라 비워주는 remove메소드도 위와같이 만들 수 있음을 확인하자

##JPA기초 : Cascade
-> 엔티티의 상태변화를 전파시키는 옵션
-> 위에서 봤던 @OneToMany, @ManyToOne등에서 사용 가능하다
}
-> 사용하면 현재 엔티티의 상태가 연관된 타겟에 똑같이 전이 된다

엔티티의 상태 기본

  • JPA와 하이버네이트에서 가장 중요한 개념
  • 기본값은 없음. 즉 상태변화의 전이가 아무것도 발생하지 않는다
  • Transient(뜻:일시적인) : JPA가 모르는 상태
  • Persistent(뜻:지속성있는) : JPA가 관리중인 상태
    (1차캐시,Dirty Checking, Write Behind 등등)
  • Detached(뜻:분리된) : JPA가 더이상 관리하지 않는 상태
  • Removed : JPA가 관리하나 삭제하기로 한 상태

엔티티 상태 부연

  • Transient

    • 위의 예제에서 저장전에는 JPA가 전혀 이 객체의 존재를 모른다.
      다시 JpaRunner의 run()메소드 부분
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      //여기부터
      Account account = new Account();
      account.setUsername("rkaehdaos");
      account.setPassword("jpa");

      Study study = new Study();
      study.setName("Spring Data JPA");

      study.setOwner(account);
      account.getStudies().add(study);
      //여기까지의 객체 상태는 만약 Persistent등이 실행되지 않는다면
      //JPA에서 전혀 알수 없으며 그냥 GC에 의해 사라질 수도 있다.
      //이 상태를 Transient라고 한다
  • Persistent

    • 위 상태에서 JPA의 persist나 하이버네이트의 Session.save가 실행되면
      이제 Persistent 상태가 된다.
    • 이 상태는 아직 DB에 직접적으로 데이타가 들어간 상태는 아니다. 하이버네이트가
      봐서 싱크가 필요하다고 생각하는 시점에 데이터가 들어간다.
    • 이 상태에서는 캐시등에 보관되어 관리되어지므로 Session.save 이후에 다시
      Session.Load등으로 꺼내 쓸때는 DB에서 읽는게 아니라 이렇게 JPA가 관리하는
      1차캐시등에서 꺼내서 쓰게 된다.
      Runner의 run메소드
      1
      2
      3
      4
      5
      6
      7
      8
      Session session = entityManager.unwrap(Session.class);
      session.save(account);
      session.save(study);

      Account ahn = session.load(Account.class, account.getId());
      ahn.setUsername("rkaehdaos2");
      System.out.println("============??===========");
      System.out.println(ahn.getUsername());
    • 실제 실행해보면 저 출력이 일어날 때까지도 save 되어 있지 않는다.
    • runner가 다 실행 되고 트랜잭션이 다 끝난후 커밋이 필요할 때
      그때서야 insert 문이 실행되서 실제 DB로 데이터가 들어가는 것을 볼 수 있다
    • 이렇게 1차캐시등이 있기 때문에 무거운 DB실행을 insert후
      다시 select하는 부분의 코스트를 줄일 수 있다
    • 실행 결과를 보면 insert후 update한 것을 볼 수 있다.
      save이후 다시 save를 하지 않았음에도 불구하고 트랜잭션이 끝났을때
      save했었을때부터 Persistent 상태로써 JPA가 관리를 하고 잇었기 때문에
      현재 변함을 DB에 반영하고자 자동으로 update가 실행된다.
    • Dirty Checking은 객체 변경 사항을 계속 모니터링 하는 것이고,
      Write Behind는 객체 상태 변화를 DB에 최대한 늦게 필요한 시점에 적용.
  • Detached

    • 트랜잭션이 끝나서 Session 밖으로 나갔을 때 Detached 상태가 된다.
    • 예를 들어서 Service에서 Repository를 호출해서 그 트랜잭션 안에서 위처럼
      사용되었고 Repository에서 객체를 Service에 리턴하고 트랜잭션이 종료되었을 때,
      이제 Service에서 사용될 때는 이 인스턴스는 한번 DB에 들어갔던 객체 이기 때문에,
      JPA가 관리 했던 객체이고 DB에 매핑이 되는 레코드가 있기는 하지만
      현재 상태에서는 Detached 상태이므로 JPA가 관리하는 객체가 아니다.
      따라서 1차 캐시나 lazy loading등이 일어나지 않는다.
    • 다시 이 기능들을 사용하고 싶을 때는 ReAttached 해서
      다시 persist상태로 만들어야 한다. 이때 사용되는 메소드들이
      Session.update(), Session.merge(), Session.saveOrUpdate()등이다.
  • Removed

    • JPA가 관리 하지만 삭제하기로 한 상태
    • 트랜잭션이 끝나고 실제 커밋이 일어날 때 삭제가 이루어진다.
    • persist상태에서 Session.delete()등을 사용했을때 Removed 상태가 된다.

Casecade는 위에서 본 이러한 상태변화를 전이 시키는 것이다

  • A가 지워질때 B도 지워져야할 때, A가 저장되면 B도 저장되어야할 때 사용 할 수 있다
  • 위의 Account와 Study처럼 독립적인 관계가 아니라 parent 와 child관계에 유용하다.
  • 예를들어서 post와 comment관계에 유용하다
    Post.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Entity
    public class Post {
    @Id @GeneratedValue
    private Long id;
    private String title;
    @OneToMany(mappedBy = "post")
    private Set<Comment> comments = new HashSet<>();

    //convinient Method
    public void addComment(Comment comment){
    this.getComments().add(comment);
    comment.setPost(this);
    }
    Comment.java
    1
    2
    3
    4
    5
    6
    7
    @Entity
    public class Comment {
    @Id @GeneratedValue
    private Long id;
    private String comment;
    @ManyToOne
    private Post post;
    Runner에서의 예쩨
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Post post = new Post();
    post.setTitle("Spring Data JPA 언제보나 ...");

    Comment comment = new Comment();
    comment.setComment("빨리 보고 싶어요");
    post.addComment(comment);

    Comment comment1 = new Comment();
    comment.setComment("곧 보게 됩니다");
    post.addComment(comment1);

    session.save(post);
  • 이처럼 했을때 session.save를 comment를 안하고 post만 했기때문에 comment는 안들어가고 post만 들어가게 된다. 실제 쿼리도 post에 대해서만 insert 쿼리가 이루어진다
  • 개발자가 원하는것은 comment는 post에 속함으로써 post가 저장될때 comment도 자동으로 저장이 되고, 삭제 될때도 같이 삭제되는 상태 전파가 되기를 원한다. 이럴때 쓰는 것이 cascade다
    Post.java cascade옵션에서 저장해서 persist 상태시 상태전파 하도록
    1
    2
    @OneToMany(mappedBy = "post", cascade = CascadeType.PERSIST)
    private Set<Comment> comments = new HashSet<>();
  • 이제 이렇게 하면 자동으로 comment도 insert되서 잘 저장이 되는 것을 확인할 수 있다
  • cascade = {CascadeType.PERSIST, CascadeType.REMOVE} 식으로 여럿 가능하다
  • 이러고 Session.delete(post)등을 하면(DB데이타있을시) comment 까지 delete된다
  • CascadeType.ALL로 어떤 상태이든 무조건 상태전파를 하도록 할 수 있다

##JPA기초 : Fetch

  • 연관관계에 있는 엔티티를 어떻게 가져올 것인가에 대한 설정
  • 종류: Eager-지금, Lazy-나중에
  • @OneToMany의 기본값은 lazy
    • 위의 예제에서 Post를 불러올때 해당 어노테이션된 Set comments는 나중에
    • 합리적이다. comments 갯수도 모르는데 너무 객체 값이 커질 수도 있기 때문
    • @OneToMany 소스를 까보면 실제로 기본값이 Lazy로 설정되어 있음을 확인할 수 있다
    • 만약 Eagar로 Fetch모드를 바꾼후 Post를 불러오면 쿼리에서 comment까지 같이 select 하는 것을 확인할 수 있다
  • @ManyToOne의 기본값은 Eagar
    • DB에 데이터가 저장된 상태에서 comment를 불러오는 경우 쿼리를 살펴보면 자동으로
      Post를 불러오게 된다.
    • Session.load()는 실패하면 예외를 던지지만 Session.get()은 null을 던진다.
    • load()는 프록시에서 가져올 수도 있고 get()은 무조건 DB에서 가져온다
  • 성능에 많은 영향을 미침
  • n+1 재현 실패, 버전업시 스마트해지는 것으로 보임
  • 그러면 n+1은실패하는 대신 한번에 다 읽어와서 메모리 차지하는 것만 유의하면 될 듯

##JPA기초 : Query

  • 위에서 지금까지는 하이버네이트 API를 사용 했었다.

    1
    Session session = entityManager.unwrap(Session.class);
  • EntityManager 가 하이버네이트 구현체를 사용하기 때문에 unwrap이란 메소드를 사용해서 JPA가 감싸고 있는 하이버네이트 API를 접속해서 사용했었다.

  • EntityManager를 사용해서도 지금까지 했던 똑같은 기능을 할 수 있다.persist()를 사용해서 저장할 수 있고, find()를 사용해서 오브젝트를 가져올 수 있다.

  • JPQL(HQL)

    • Java Persistence Query Language / Hibernate Query Lanuage
    • DB 테이블이 아닌, 엔티티 객체 모델 기반으로 쿼리 작성
    • JPA or Hibernate가 해당 쿼리를 SQL로 변환해서 실행함
Runner의 run(), 현재 저장된 DB에서 post를 읽어온다.
1
2
3
4
5
6
@Override
public void run(ApplicationArguments args) throws Exception {
TypedQuery<Post> query = entityManager.createQuery("SELECT p FROM Post as p", Post.class);
List<Post> posts = query.getResultList();
posts.forEach(System.out::println);
}
  • Post의 ToString()구현에 따라 출력된다.

  • 단점: 타입세이프 x, 문자열이기 때문에 얼마든지 오타가 발생 할 수 있다.

  • Criteria

    • 타입 세이프한 쿼리 가능. 문자열 자체가 없다
    • 지저분함
      JPQL대신 Criteria를 사용한 모습, 문자열이 아예 없다. 지저분하다.
      1
      2
      3
      4
      5
      6
      CriteriaBuilder builder = entityManager.getCriteriaBuilder();
      CriteriaQuery<Post> criteria = builder.createQuery(Post.class);
      Root<Post> root = criteria.from(Post.class);
      criteria.select(root);
      List<Post> posts = entityManager.createQuery(criteria).getResultList();
      posts.forEach(System.out::println);
  • Native Query

    • SQL 쿼리를 직접 실행
Native Query 예제
1
2
3
4
5
//메소드 자체가 타입을 인자로 받았는데도 리턴값을 타입을 지원하지 않고 Query 반환
//Query nativeQuery = entityManager.createNativeQuery("select * from Post", Post.class);
//하지만 타입을 가지고 받을 수는 있다. 이상한 API
List<Post> posts = entityManager.createNativeQuery("select * from Post", Post.class).getResultList();
posts.forEach(System.out::println);

##JPA기초 : Spring Data JPA

  • 지금까지 배운 JPA 기술로 DAO역할을 하는 Repository를 만들 수 있다
    JPA를 사용한 Repository 예제
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    package me.rkaehdaos.jpademo1;

    import org.springframework.stereotype.Repository;
    import org.springframework.transaction.annotation.Transactional;

    import javax.persistence.EntityManager;
    import javax.persistence.PersistenceContext;
    import java.util.List;

    @Repository
    @Transactional
    public class PostRepository {

    //빈을 받는다
    //@Autowired 보단
    @PersistenceContext //로 받는게 낫다. JPA 어노테이션을 사용하는 것이고
    //스프링의 코드를 최대한 감출 수 있기 때문. 스프링 코드를 최대한 감추는 것도
    //스프링의 철학이다
    EntityManager entityManager;

    public Post add(Post post) {
    entityManager.persist(post);
    return post;
    }

    public void delete(Post post){
    entityManager.remove(post);
    }

    public List<Post> findAll() {
    return entityManager.createQuery("SELECT p FROM Post As p", Post.class).getResultList();
    }
    }

  • 이런 식으로 구현하고 Service등의 레이어에서 사용하면 된다. 필요하면 더 구현하고
  • 이에 대한 테스트 코드도 다 만들어야 한다
  • 이것도 번거로운 일
  • 6~7년전만 하더라도 이런 뻔한 코드들은 제네릭한 프레임워크를 만들어서 구현하였다
    현재는 사용하지 않는 아주 예전에 사용하던 해결방법
    1
    2
    3
    @Repository
    public class PostRepository extends GenericRepository<Post, Long> {
    }
  • 또한 스프링 Roo가 AspectJ를 사용해서 이러한 프레임워크를 구현하였다
  • 하지만 이후에 나타난 Spring Data JPA가 획기적인 Repository 개발법을 보인다
    • 레포지토리 클래스가 아니라 레포지토리 인터페이스를 만든다.
    • 또다른 인터페이스 하나만 상속 받으면 된다
제네릭한 Repo를 만드는 가장 진보적인 형태의 방법, @Repository 조차 없음
1
2
3
import org.springframework.data.jpa.repository.JpaRepository;
public interface PostRepository extends JpaRepository<Post, Long> {
}
  • POJO는 아니다
    • 침투적이긴 하다. Spring Data의 인터페이스를 상속받아야하기 때문
    • Spring Data JPA의 구동원리에 의해 PostRepository가 빈등록 됨
    • 원래 등록 방법: @Configuration이 붙은 클래스에 @EnableJpaRepositories를 붙여야 사용이 가능하다
    • 스프링 부트를 사용하면 저 설정을 자동으로 해주기 때문에 생략해도 된다
    • @Repository를 붙일 필요가 없음. 없어도 빈으로 등록이 된다
Runner부분, JPA와 하이버네이트 사용과 비교할 수 없을 정도로 간결하다
1
2
3
4
5
6
7
@Autowired
PostRepository2 postRepository;

@Override
public void run(ApplicationArguments args) throws Exception {
postRepository.findAll().forEach(System.out::println);
}
  • 이렇게 한 줄로 끝나게 된다

  • 그것도 Spring Data JPA가 검증된 코드

  • 따로 테스트를 작성할 필요가 없다

    • Repository에 작성한 코드가 없기 때문
  • 코드 자체가 적고, 개발생산성, 유지보수성 좋다

  • 원리와 이해

    • @SpringBootApplication가 @EnableJpaRepositories를 해준다
    • @EnableJpaRepositories의 정의를 보면 @Import(JpaRepositoriesRegistrar.class)가 있음을 알 수 있다
    • JpaRepositoriesRegistrar가 JPA 리포지토리를 빈으로 등록해주는 역할을 한다
    • JpaRepositoriesRegistrar는 ImportBeanDefinitionRegistrar을 상속한다
    • ImportBeanDefinitionRegistrar는 빈의 정의 등록해주는 인터페이스로 Spring Data가 아닌 springframework에 속해 있는 인터페이스로 사용자도 이를 이용해서 빈등록이 가능하다
      Post 를 자동으로 빈 등록하는 ImportBeanDefinitionRegistrar 구현
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      import org.springframework.beans.factory.support.BeanDefinitionRegistry;
      import org.springframework.beans.factory.support.GenericBeanDefinition;
      import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
      import org.springframework.core.type.AnnotationMetadata;

      public class PostRegistar implements ImportBeanDefinitionRegistrar {
      @Override
      public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
      GenericBeanDefinition bf = new GenericBeanDefinition();
      bf.setBeanClass(Post.class);
      bf.getPropertyValues().add("title","타이틀");
      registry.registerBeanDefinition("beanname", bf);
      }
      }
    • 이렇게 만든 Registarar를 @Import(PostRegistar.class)로 임포트 하면 된다
  • 쿼리 값 확인하기

    • 쿼리에서 ? 된 값을 확인하는 방법
      1
      2
      3
      4
      5
      6
      7
      8
      #쿼리 보이기
      spring.jpa.show-sql=true
      spring.jpa.properties.hibernate.format_sql=true

      # ? 값 확인하기
      #logging.level.org.hibernate.SQL=debug # spring.jpa.show-sql=true 했으니 주석
      #위 2개는 같은 의미다
      logging.level.org.hibernate.type.descriptor=trace

Related POST

공유하기