ORM

  • Object Relational Mapping
  • 객체와 관계형 데이터베이스의 테이블을 맵핑하여 데이터를 객체화 하는 것

  • 장점
    1. 객체 지향적인 코드로 인해 더 직관적이고 비즈니스로직에 집중할 수 있다
    2. 재사용성 및 유지보수가 용이하다 (반복적인 SQL을 작성할 필요가 없다)
    3. 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를 추가해야 한다
  • @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
}

기본키 매핑

  1. 직접 할당 - @Id 사용

  2. 자동 생성 - @Id@GeneratedValue 함께 사용

    1. IDENTITY : 기본 키 생성을 데이터베이스에 위임
      • DB가 AUTO_INCREMENT로 PK생성
      • 주로 MySQL, PostgreSQL, SQL Server에서 사용
    2. SEQUENCE : 시퀀스 사용 (@SequenceGenerator 필요)
      • 주로 오라클에서 사용
    3. TABLE : 시퀀스 생성용 테이블 이용 (@TableGenerator 필요)
    4. AUTO : 데이터베이스 Dialect에 따라 자동으로 선택
      • Oracle:SEQUENCE, MySQL: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)

  • 엔티티를 관리되는 환경
  • 엔티티 매니저에 의해 엔티티가 영속성 컨텍스트에 보관 및 관리된다.

image

EntityManager

  • 엔티티를 저장, 수정, 삭제, 조회 등 모든 일을 처리하는 관리자
  • EntityManagerEntityMangerFactory에 의해 여러개 생성이 가능하다
  • 엔티티 매니저는 쓰레드간에 공유되지 않고 요청마다 생성된다 (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에서 한다