[JPA] ORM과 Hibernate 구현체
ORM
- Object Relational Mapping
-
객체와 관계형 데이터베이스의 테이블을 맵핑하여 데이터를 객체화 하는 것
- 장점
- 객체 지향적인 코드로 인해 더 직관적이고 비즈니스로직에 집중할 수 있다
- 재사용성 및 유지보수가 용이하다 (반복적인 SQL을 작성할 필요가 없다)
- DBMS의 종속성이 줄어든다
Hibernate
JPA는 자바 ORM 대한 표준 명세 인터페이스이다. 그리고 JPA를 구현한 ORM 프레임워크에는 여러가지가 있다.
JPA 구현체중에 hibernate
가 가장 일반적이다.
Dialect(방언)
- ORM은 객체맵핑을 통해 자동으로 쿼리를 작성해준다. 하지만 수많은 DBMS 종류가 있고 DBMS마다 문법과 용어가 약간씩 다르다
- 하이버네이트는 40여가지의 Dialect를 지원해 특정 DBMS에 종속되지 않게 사용할 수 있다
- 이들은
org.hibernate.dialect
의 Dialect 추상클래스를 구현하고 있다
ex) H2Dialect, MariaDB103Dialect, MySQL8Dialect, Oracle12cDialect
- 이들은
Entity
- 객체와 테이블 매핑
어노테이션
-
@Entity
: JPA가 관리하는 클래스- 자동으로 기본생성자를 생성한다 (like lombok의
@NoArgsConstructor
) - 다만, 이미 어느 생성자라고 있는 경우 생성하지 않는다
- 즉, @AllArgsConstructor 추가하는 순간 @NoArgsConstructor를 추가해야 한다
- 자동으로 기본생성자를 생성한다 (like lombok의
-
@Table
: 엔터티와 매핑할 테이블 지정 -
@NoArgsConstructor
- @Entity에 포함되어 있어 생략해도 된다
- 다만, 접근제어자를 제한하고 싶으면
protected
로 사용하자. JPA가 프록시 객체를 만들 때 기본생성자를 사용하기 때문에private
은 사용할 수 없다
-
@ToString
- exclude 속성 : 양방향 연관관계를 갖는 엔티티에서 순환참조를 막기위해 사용한다
-
@Id
: 속성 중 기본키 -
@Column
: 일반속성- @Column을 사용하지 않으면 변수명과 같은 컬럼명을 사용함
-
@NotNull
: null 값이 들어오면 NPE 발생-
@Column(nullable = false)
와 같은 역할을 한다 - Bean Validation의 어노테이션이지만 다른 어노테이션(
@NotEmpty
,@NotBlank
)과는 다르게 컬럼에not null
제약조건을 추가한다 - @NotNull 사용 추천
-
-
@Enumerated
: 자바의 enum 타입을 맵핑- EnumType.ORDINAL: enum 순서로 저장 (0, 1)
- EnumType.STRING (권장): enum 이름으로 저장 (ADMIN, USER)
-
@Temporal
: 날짜 타입을 맵핑
◼︎ 연관관계 (링크)
- @JoinColumn : 외래키 맵핑
- @ManyToOne : 다대일관계
- fetch : 즉시로딩(eager) or 지연로딩(lazy) 선택
- cascade : 영속성전이 선택
- @OneToMany : 일대다관계
- mappedBy : 양방향 맵핑일 경우 반대쪽 맵핑의 필드 이름을 값으로 사용 (mappedBy=”team”)
- @OneToOne : 일대일관계
-
@ManyToMany : 다대다관계 (실무에서 사용 X)
- @ManyToOne, @OneToOne : 기본이 eager
- @OneToMany, @ManyToMany : 기본이 lazy
@Getter
@ToString(exclude = "team") // 중요! 지연로딩 사용시 제외
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@Entity
@Table(name = "member")
public class MemberEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(nullable = false)
private String name;
private String address;
@Column(name = "zip_code", length = 10)
private String zipCode;
@CreatedDate
private LocalDatetime createDate; // insert 시 시간 자동 저장
@LastModifiedDate
private LocalDatetime updateDate; // update 시 시간 자동 저장
@Enumerated(EnumType.STRING)
private RoleType roleType;
@ManyToOne(fetch = FetchType.LAZY) // 중요! 쿼리효율을 위해 항상 지연로딩 사용
@JoinColumn(name = "TEAM_ID")
private TeamEntity team;
public void setTeam(Team team) {
this.team = team;
}
}
@Entity
public class TeamEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(nullable = false)
private String name;
}
enum RoleType {
ADMIN, USER
}
기본키 매핑
-
직접 할당 -
@Id
사용 -
자동 생성 -
@Id
와@GeneratedValue
함께 사용- IDENTITY : 기본 키 생성을 데이터베이스에 위임
- DB가
AUTO_INCREMENT
로 PK생성 - 주로 MySQL, PostgreSQL, SQL Server에서 사용
- DB가
- SEQUENCE : 시퀀스 사용 (
@SequenceGenerator
필요)- 주로 오라클에서 사용
- TABLE : 시퀀스 생성용 테이블 이용 (
@TableGenerator
필요) - AUTO : 데이터베이스 Dialect에 따라 자동으로 선택
- Oracle:
SEQUENCE
, MySQL:IDENTITY
- Oracle:
- IDENTITY : 기본 키 생성을 데이터베이스에 위임
기본 사용법
기능 | 메서드 | 설명 |
---|---|---|
목록조회 | createQuery(JPQL, Entity.class).getResultList() | 엔티티 전체 목록 출력 |
상세조회 | find(Entity.class, id) | 엔티티 1개 출력 |
생성(Create) | persist(entity) | 해당 엔티티 저장 |
수정(Update) | entity.setColumn(value) | 해당 엔티티 수정 |
삭제(delete) | remove(entity) | 해당 엔티티 삭제 |
@Repository
public static void MemberRepository() {
@PersistenceContext
EntityManager em;
// 1. 생성
public void save(MemberEntity member) {
em.persist(member);
}
// 2. 1개 조회
public MemberEntity findOne(Long id) {
return em.find(MemberEntity.class, id);
}
// 3. 목록 조회
public List<MemberEntity> findAll() {
return em.createQuery("SELECT m from MemberEntity m", MemberEntity.class).getResultList();
// 4. 수정
public void update(MemberEntity member) {
MemberEntity findMember = em.find(MemberEntity.class, member,getId());
findMember.setName("name2");
}
// 5. 삭제
em.remove(member);
}
상속관계 맵핑
- 관계형 데이터베이스에는 상속관계는 존재하지 않지만 비슷한 개념으로
슈퍼타입-서브타입
관계가 있다
1. 조인 전략 (추천)
- 클래스마다 테이블을 만들고 슈퍼타입에 어느 서브타입인지 구분할 수 있는 필드를 추가한다
- 장점 : 테이블 정규화, 외래키 참조무결성 제약조건 사용가능, 저장공간 효율
- 단점 : 조인을 많이 사용해 성능저하, 데이터 INSERT시 2번실행
2. 단일 테이블 전략
- 여러 클래스의 모든 필드을 다모아 하나의 테이블만 생성
- 장점 : 조인이 필요없어 조회성능이 빠름, 모든 쿼리가 1번만 실행
- 단점 : 테이블이 커질수 있다, 모두 null 허용
3. 구현 클래스마다 테이블 전략 (추천 X)
- 슈퍼타입의 필드를 모든 서브타입 테이블에 추가
-
@Inheritance(strategy=InheritanceType.XXX)
-
JOINED
: 조인 전략 -
SINGLE_TABLE
: 단일테이블 전략 -
TABLE_PER_CLASS
: 구현 클래스마다 테이블 전략
-
-
@DiscriminatorColumn(name=“DTYPE”)
- 상위 클래스에 선언
- 슈퍼타입에서 서브타입을 구분하는 용도의 컬럼을 추가
- default 컬럼명 : DTYPE
-
@DiscriminatorValue(“XXX”)
- 하위 클래스에 선언
- 슈퍼타입 테이블에서 서브타입을 구분하는 컬럼(DTYPE)에서 저장되는 value를 변경할 때 사용
- default value명 : 테이블명
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn // default=DTYPE
public class Item {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int price;
}
@Entity
@DiscriminatorValue("A")
public class Album extends Item {
private String artist;
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
private String director;
private String actor;
}
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
private String author;
private String isbn;
}
영속성 컨텍스트(Persistence Context)
- 엔티티를 관리되는 환경
- 엔티티 매니저에 의해 엔티티가 영속성 컨텍스트에 보관 및 관리된다.
EntityManager
- 엔티티를 저장, 수정, 삭제, 조회 등 모든 일을 처리하는 관리자
-
EntityManager
는EntityMangerFactory
에 의해 여러개 생성이 가능하다 -
엔티티 매니저는 쓰레드간에 공유되지 않고 요청마다 생성된다 (DB커넥션처럼 사용되고 버려진다)
- request마다 다른 쓰레드를 사용하기 때문에 다른 엔티티 매니저를 사용한다
public interface EntityManager {
public void persist(Object entity);
public <T> T merge(T entity);
public void remove(Object entity);
public <T> T find(Class<T> entityClass, Object primaryKey);
public void flush();
public <T> TypedQuery<T> createQuery(CriteriaQuery<T> criteriaQuery);
public EntityTransaction getTransaction();
...
}
- 스프링 컨테이너에서 기본적으로 영속성 컨텍스트의 범위는 트랜잭션의 범위와 같다
- 서비스계층에서 트랜잭션이 시작하면 영속성 컨텍스트가 만들어지고 끝나면 영속성 컨텍스트가 제거된다
- 다른 엔티티 매니저를 사용해도 같은 트랜잭션에서는 같은 영속성 컨텍스트를 사용한다
EntityManagerFactory
-
EntityManager
를 생성하는 인터페이스 - EntityManager를 생성하는 방식에는 Container-Managed, Application-Managed이 있다
public interface EntityManagerFactory {
public EntityManager createEntityManager();
...
}
1. Container-Managed
- 스프링 컨테이너에 등록된
EntityManagerFactory
에서EntityManager
를 생성해 주입받는 방식 -
@PersistenceContext
: EntityManager 빈을 주입받는 어노테이션- EntityManagerFactory에서 새로운 EntityManager를 생성해서 주입
- 또는, Transaction에 의해 기존에 생성된 EntityManager를 주입
- 스프링은 싱글톤 패턴을 사용해 모든 빈을 쓰레드가 공유하지만,
@PersistenceContext
을 사용하면 주입받은 EntityManager 빈을 Proxy에 감싸 Lock을 걸어 Thread-Safe를 보장한다
@PersistenceContext
EntityManager entityManager;
2. Application-Managed
- 어플리케이션에서 직접 EntityManager를 생성해서 사용하는 방법
@Repository
public class JpaRepository {
@Autowired
private EntitymanagerFactory emf;
public Long save(Member member) {
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
...
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
}
}
특징
1. 1차 캐시
- 영속성 컨텍스트 내부에 캐시가 있는데 이를
1차 캐시
라고 한다. 1차 캐시는 Map의 형태로 key:@Id, value:Entity 이다. - 엔티티 조회를 하면 우선 1차 캐시에서 ID값으로 엔티티를 찾는 데 없을 시 DB에 접근하여 조회 후 엔티티를 생성하여 1차 캐시에 저장 후 영속된 엔티티를 반환한다.
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
em.persist(member);
2. 동일성 보장
- 동일한 트랜잭션에서 같은 ID값으로 여러 번을 호출해도 같은 엔티티를 반환한다.
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.prineln(a == b); // true
→ 똑같은 ID값이 ‘member1’인 엔티티를 호출했으니 1차 캐시에 있는 같은 엔티티가 반환된다
3. 트랜잭션을 지원하는 쓰기 지연
- 엔티티 매니저는 트랜잭션이 커밋되지 전까지
내부 쿼리 저장소
에 SQL을 모아뒀다가 커밋시 한번에 DB로 보낸다(Flush).
4. 변경 감지(Dirty Checking)
- 수정하여
commit
하면 수정 후의 스냅샷과 엔티티가 영속성 컨텍스트에 처음 저장될 때의 스냅샷을 비교하여 변경된 엔티티를 찾는다. - 영속성 컨텍스트에 존재하는 영속된 엔티티만 적용된다.
5. 지연 로딩
- 엔티티 조회 시점이 아닌 엔티티 내 연관관계를 참조하는 시점에 필요한 데이터를 조회하는 것
public void printUserAndTeam(String memberId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
team.getName(); // 실제 연관관계를 갖은 team 엔티티를 참조하는 시점
}
public void printUser(String memberId) {
Member member = em.find(Member.class, memberId);
}
→ printUserAndTeam()
: Member와 연관관계를 갖는 Team 엔티티
를 참조한다. 이 때, 지연로딩을 사용한다면 참조하는 시점에 조회한다.
→ printUser()
: Member 엔티티
만 사용한다. 즉, Team 엔티티를 조회하는 것은 효율적이지 않다. 이 때, 조회하지 않은 Team 엔티티는 프록시로 대체한다.
◼︎ 프록시 : 실제 클래스를 상속 받아 겉모양이 같은 가짜 클래스
- 지연 로딩시 연관관계를 갖지만 조회되지 않은 엔티티는 프록시를 넣어둔다.
- 실제 사용시 프록시객체를 통해 실제 엔티티에 접근한다
- 이때, 실제 엔티티가 영속성 컨텍스트에 존재하지 않으면 DB에서 가져와 저장한 후 반환한다
public void printUserAndTeam(String memberId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam(); // JPA는 Team의 프록시 객체를 넣어둠
System.out.println(team.getClass()); // class hello.jpa.Team$HibernateProxy$z4JtUeLD
team.getName(); // 연관관계를 참조하는 시점으로 DB조회가 실제로 일어남
}
엔티티 생애주기
1. 비영속(New)
- 영속성 컨텍스트와 전혀 관계가 없는 상태
- 엔티티 객체를 생성했지만 영속성 컨텍스트에 저장되지 않은 상태
Member member = new Member();
2. 영속(Managed)
- 영속성 컨텍스트에 저장된 상태
- 엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 저장된 상태이며 영속성 컨텍스트에 의해 관리됨
- 영속성 컨텍스트에 저장된 엔티티는
식별자값(ID)
를 갖게 된다
em.persist(member);
3. 준영속(Detached)
- 영속성 컨텍스트에 저장되었다가 분리된 상태
- 영속성 컨텍스트가 더 이상 엔티티를 관리하지 않는 상태
- 다만, 이미 한번 영속이 되었던 적이 있기에 ID값을 갖고 있다.
// 1. 특정 엔티티만 준영속상태로 전환
em.detach(member);
// 2. 영속성 컨텍스트 초기화
em.clear();
// 3. 영속성 컨텍스트 종료
em.close();
4. 삭제(Removed)
- 삭제된 상태
- 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제한다.
em.remove(member);
플러시(flush)
- 영속성 컨텍스트의 변경 내용을 DB에 반영 하는 것
-
내부 쿼리 저장소
에 저장되어 있던 쿼리들을 DB에 전송한다 (내부 쿼리 저장소가 비워진다) -
영속성 컨텍스트
가 비워지는 것이 아님1. em.flush() 직접 호출 (거의 사용하지 않음)
2. 트랜잭션 커밋시 자동 호출
3. JPQL 실행시 자동 호출
영속성 전이 (Cascade)
- 영속성 전이는 부모 엔티티에게 수행된 작업이 자식 엔티티에도 전파된다
-
연관관계를 맵핑하는 것과 아무런 관련이 없다. 엔티티를 영속화할 때 연관된 엔티티도 같이 영속화하는 편리함을 제공할 뿐이다
- CascadeType
- ALL, PERSIST, REMOVE, MERGE, REFRESH, DETECH
@Entity
public class Parent{
...
@OneToMany(mappedBy = "parent", cascade=CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
public void addChild(Child child){
childList.add(child);
child.setParent(this);
}
...
}
@Entity
public class Child{
...
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
...
}
고아객체
- 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
- 부모로부터 연관관계가 끊어지고 다른 곳에서도 참조하지 않을 경우 고아로 판단한다
application.yml
spring:
devtools:
livereload:
enabled: true
restart:
enabled: true
thymeleaf:
cache: false
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/nims?serverTimezone=Asia/Seoul
username: root
password: 1234
jpa:
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
open-in-view: false
hibernate:
ddl-auto: update
show-sql: true # 콘솔에 SQL 출력
properties:
hibernate:
format_sql: true # 출력시 SQL 가독성있게 포맷팅
logging.level:
org.hibernate.SQL: debug # 로그로 SQL 출력
org.hibernate.type: trace # 바인딩 정보 출력
- database-platform : DBMS Dialect
- hibernate.ddl-auto : 데이터베이스 초기화전략
- none : 아무것도 하지 않음
-
create: 기존 테이블 삭제 후 생성 (DROP + CREATE) -
create-drop: 기존 테이블 삭제 후 생성, 연결 종료시 테이블 삭제 (DROP + CREATE + DROP) -
update: 수정된 스키마만 적용 -
validate : 엔티티와 테이블 정상 맵핑 확인
- 운영환경에서 절대로
create
,create-drop
,update
를 사용해서는 안된다 - 개발환경에서는
update
사용 - 운영환경에서는
none
또는validate
사용 / DB변경은 직접 DBMS에서 한다