JPA 프로그래밍 읽으면서 이것저것

이완희·2023년 6월 19일
0

깔끔하게 정리한게 아니라 책을 읽으면서 중요하거나 처음보는 개념을 메모해뒀습니다. 누군가 보고 공부하기에는 좋은 문서는 아닙니다.

JPA, ORM, Hibernates는 각각 무엇인가?

  • ORM은 객체와 관계형 데이터베이스 간의 데이터를 매핑해주는 기술이다.
  • JPA는 자바 진영에서 관계형 데이터베이스를 다루기 위한 표준 인터페이스이다.
  • Hibernate는 JPA의 구현체 중 하나로, JPA를 실제로 사용할 수 있게 해주는 프레임워크

Application에서 SQL을 직접 호출하면 여러 문제가 있다. 진정한 의미의 계층 분할이 어렵다. 엔티티를 신뢰할 수 없다. SQL에 의존적인 개발을 피하기 어렵다.

동일성(identity)비교는 ==비교다. 객체 인스턴스의 주소 값을 비교한다.

동등성(equality)비교는 equals() 메소드를 사용하여 객체 내부의 값을 비교한다.

DB에서 PK가 같은 회원을 조회한다. Member member1 = dao.getMember(), Member member2 = dao.getMember(); 으로 선언했다. PK가 같으니 DB측면에선 동일하지만 다른 인스턴스이기 때문에 동일성 비교는 false이다. 하지만 JPA를 이용하면 같은 트랜잭션일 때 같은 객체가 조회되는 것을 보장한다.

//엔티티 매니저 팩토리 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
EntityManager em = emf.createEntityManager(); //엔티티 매니저 생성

EntityTransaction tx = em.getTransaction(); //트랜잭션 기능 획득

굳이 엔티티 매니저 팩토리와 엔티티 매니저를 생성 해야 하나? 나는 JpaRepository를 상속받아서 DB와 통신했다.

영속성 컨텍스트‘엔티티를 영구 저장하는 환경'을 뜻한다.

영속성 컨텍스트의 특징

  • 영속석 컨텍스트는 엔티티를 식별자 값으로 구분한다. 따라서 영속 상태는 식별자 값이 반드시 있어야 한다.
  • 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 DB에 반영하는 flush를 수행
  • 1차 캐시, 동일성 보장, 트랜잭션을 지원하는 쓰기 지연, 변경 감지, 지연로딩의 장점이 있다.

영속성 컨텍스트는 내부에 1차 캐시를 갖고 있다. persist()를 하면 DB에 저장되진 않더라도 1차 캐시엔 엔티티를 저장한다. find()를 하면 우선 1차 캐시에서 조회한다. 없다면 DB에서 조회한다. 그리고 1차 캐시에 저장한 후 영속 상태의 엔티티를 반환한다.

변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 스냅샷을 활용하여 적용된다. 비영속, 준영속은 엔티티 값을 변경해도 DB에 반영되지 않는다.

복합키를 지정하는 다른 방법이 있다.

나는 LineDistance에 departureTmlCd, arrivalTmlCd를 복합키로 지정하기 위해 @IdClass(LineDistancePK.class)

선언하고 LindDestancePK class를 생성했지만 보다 간편하게 지정하는 방법이 있다.

LineDistance에 아래 Annotation을 선언한다.

@Table(name="LineDistance", uniqueConstraints = @UniqueConstraint(
        name = "LineDistance_UNIQUE",
        columnNames = {"DepartureTmlCd", "ArrivalTmlCd"} ))

별도의 클래스를 만들필요 없이 Annotation을 선언만 해주면 되어서 이 방법이 훨씬 간편하다.

간단하기는 아래가 훨씬 좋지만 로직등을 구현할 순 없다. 그럴땐 @IdClass를 사용하자.

@ManyToOne과 @OneToMany은 다대일과 일대다를 지정해준다.

양방향 매핑을 한다면 두 연관관계 중 하나를 연관관계의 주인으로 정해야 하는데 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다. 연관관계의 주인만이 외래 키의 값을 변경할 수 있다.

이 부분은 한번만 봐서는 이해하기가 쉽지 않다. 복습이 필요하다.

RDBMS에서 객체지향의 상속을 다루기 위해서는 슈퓨타입 서브타입 관계 모델링 기법을 사용하면 된다.

  1. 각각의 테이블로 변환하고 모두 조회할 때 조인하는 조인 전략
    1. 장점
      1. 테이블이 정규화 됨
      2. 외래 키 참조 무결성 제약조건을 활용할 수 있음
      3. 저장공간을 효율적으로 사용
    2. 단점
      1. 조인이 많이 사용되므로 성능저하
      2. 조회 쿼리가 복잡
      3. 데이터 등록시 Insert도 2번
    3. 의견: 성능저하 되는데 굳이 사용해야 하나? 알아두기만 하고 실제로 쓰지는 않을거 같다.
  2. 테이블을 하나만 사용해서 통합하는 단일 테이블 전략
    1. 장점
      1. 조인이 없으므로 성능이 빠르다
      2. 조회 쿼리가 단순하다
    2. 단점
      1. 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다
      2. 단일 테이블에 모든 것을 저장하므로 테이블이 과도하게 커질 수 있다
    3. 의견: 결국 조회 성능이 젤 빠르니 얘가 짱 아닌가? null값이 이상하게 들어오지는 않을지 조심해야 겠다.
  3. 서브 타입마다 하나의 테이블을 만드는 구현 클래스마다 테이블 전략
    1. 장점
      1. 서브 타입을 구분하기에 효과적
      2. not null 제약조건 사용가능
    2. 단점
      1. 여러 자식 테이블을 함께 조회할 때 성능이 느리다
      2. 자식 테이블을 통합해서 쿼리하기 어렵다.
    3. 의견: 얘는 추천하지 않는다고 하니 알아만 두자
@Data
@Entity
@Table(name = "cost_rate")
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@EqualsAndHashCode(callSuper = false)
public class CostRate extends AuditEntity {
...
}

얘는 객체로만 상속받고 있지 데이터 상에서는 상속받고 있지 않음에 유의하자.

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)등이 있어야 한다.

@MappedSuperclass는 실제 Entity와 매핑할 필요는 없지만 매핑 정보를 상속할 때에 선언하면 된다. Entity가 없는 AuditEntity에 선언되어 있다. 그리고 추상클래스로 만들어두자.

엔티티를 조회할 때 연관된 엔티티들이 항상 사용되는 것은 아니다.

멤버와 팀이 1:N의 관계에 있다고 생각해보자. 멤버를 조회할 때 굳이 팀도 조회를 해야할까? 분명 비효율적이다. 따라서 JPA는 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 지연 로딩(Lazy Loading)이라 한다. 지연 로딩 기능을 사용하려면 실제 엔티티 객체 대신에 DB조회를 지연할 수 있는 가짜 객체가 필요한데 이를 프록시 객체라고 한다. @ManyToOne(fetch = FetchType.LAZY)를 사용하여 구현가능하다. 잘못 사용하면 지연 로딩으로 인해 추가적인 성능 저하가 발생할 수 있으므로 즉시 로딩(eager loading)을 사용할지는 개발자에게 달렸다.

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 영속 상태로 만들고 싶다면 영속성 전이(transitive persistence)기능을 사용하면 된다. 이를 사용하면 부모 엔티티가 저장될 때 자식 엔티티도 저장 된다. JPA에서 엔티티를 저장할 떄 연관된 모든 엔티티는 영속 상태여야 한다. @OneToOne(cascade = CascadeType.*ALL)*

와 같이 사용하면 된다. 반대로 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하려면 고애 겍체 제거 기능을 사용하면 된다.

영속상태(Persistent State)와 준영속성상태(Detached state)에 대해 정리하고 넘어가도록 하자.

  • 영속상태는 JPA가 관리하는 상태로, 엔티티가 영구 저장소에 저장되어 있는 상태이다. 그러므로 DB와의 동기화도 보장되어 있다. 영속상태라면 JPA가 데이터의 변경을 감지하고 데이터베이스에 반영한다. 영속상태로 만들기 위해서는 JPA의 EntityManager를 통해 영속성 컨텍스트에 저장한다. 영속성 컨텍스트는 엔티티를 관리하는 1차 캐시를 제공한다.
  • 준영속성상태는 영속성 컨텍스트에서 분리된 상태이다. 상태가 변경되어도 JPA가 추적하지 않으므로 DB와의 동기화를 보정하지 않는다. 엔티티에서 영속성 컨텍스트를 분리하면 된다.

즉, 영속상태는 JPA가 관리하는 상태로 DB와 동기화를 보장하고 준영속상태는 영속성 컨텍스트에서 분리된 상태로 변경추적과 DB 동기화가 보장되지 않는 상태이다.

@Transactional을 사용하는 서비스레이어에선 엔티티는 영속상태이다. 트랜잭션을 커밋하면 JPA는 먼저 영속성 컨텍스트를 플러시하여 변경 내용을 DB에 반영하고 DB 트랜잭션에 커밋한다. 트랜잭션 범위 = 영속성 컨텍스트의 범위라 생각하면 이해가 쉽다. 둘은 운명공동체야. 반대로 Presentation Layer에선 준영속상태이다. @Controller에서 @Transactional을 선언하는 만행을 저지르지는 않을테니깐.

Lazy Loading은 쉽게 말해 필요하기 전까지 호출을 늦추는 방식이다. A와 B테이블이 1:N관계를 갖고 있다고 치자. A엔티티를 조회하면 당연히 B엔티티도 조회하게 될텐데 이를 조회하지 않고 B엔티티도 조회할때 이를 로딩한다. 이는 프록시(Proxy)객체를 사용하여 구현한다. 실제 객체와 동일한 인테페이스를 갖고는 있지만, 실제 데이터는 필요한 시점에 로딩한다. 필요한 객체만 조회하므로 시간, 메모리 성능을 향상시킬 수 있지만 객체에 접근할 때마다 DB에 추가적인 접근을 해야하므로 남발하다 보면 오히려 성능이 떨어질 수 있다. 이를 적절히 조절하는게 실력있는 개발자겠지.

JPA의 유용한 기능 중 하나를 소개한다. String이 “Y’면 true를, “N”이면 false를 저장해야 한다면 어떻게 할까? 비지니스 로직에서도 처리할 수 있지만 @Converter를 이용하면 보다 편리하게 처리 가능하다. 아래 코드를 참조하자.

@Converter
public class YNConverter implements AttributeConverter<Boolean, String> {

    public String convertToDatabaseColumn(Boolean attr) {
        return (attr != null && attr) ? "Y" : "N";
    }

    public Boolean convertToEntityAttribute(String s) {
        return "Y".equals(s);
    }
}

원하는 엔티티에 @Converter를 추가하자. 클래스 레벨에서도 선언 가능하다. 그때는 attributeName에서 컬럼을 넣어주면 된다.

Entity의 생명주기는 알고 넘어가자.

Managed는 영속성 컨텍스트에 저장된 상태이다. 엔티티를 아직 DB에 저장은 안했지만 트랜잭션의 커밋 시점에서 DB에 저장하게 된다. 준영속(Detached)는 영속석 컨텍스트(Managed)에 없으므로 1차캐시, 지연로딩등에서 조회가 되지 않는다.

Dirty Checking은 인티티의 수정이 일어날때 개발자가 .save()를 하지 않아도 영속성 컨텍스트가 알아서 변경사항을 체크하고 DB에 저장한다. 트랜잭션이 시작되고 엔티티와 스냅샷을 비교하여 변경된 엔티티를 찾는다. 변경된 엔티티가 있다면 update 쿼리를 수행하여 DB에 변경된 값을 저장한다. 참고로 모든 컬럼을 Update하기 원치 않는다면 @DynamicUpdate를 사용하자. 그렇다면 .save()는 영속화되지 않은 엔티티에 대해서만 실행하면 되려나? 근데 준영속 상태를 DB에 저장할일은 없을거같은데…?

1차 캐시는 영속성 컨텍스트 내부에 위치한 메모리 공간이다. 처음으로 DB에 조회하고 나오면 해당 엔티티를 1차 캐시에 저장한다. 이후 트랜잭션 범위 내에서 같은 엔티티를 다시 조회하면 DB까지 조회하지 않고 1차 캐시에서 엔티티를 반환한다. 2차 캐시는 여러 개의 영속성 컨텍스트 간에 공유되는 메모리 공간이다. 여러 클라이언트가 같은 엔티티를 조회할 때, 2차 캐시에서 엔티티를 반환하여 DB 접근을 피할 수 있다. 1차와는 다르게 다른 트랜잭션에서도 유효하다. @Cacheable어노테이션을 사용하면 2차 캐시라고 보면된다. 물론 1차 캐시도 포함이다. 다만 일관성과 동시성에 대한 것도 고려하고 있어야 한다.

간선 프로젝트에서는 EntityManager를 안쓰고 Repository Interface를 사용해서 데이터를 조작했는데 괜찮을까? 지금은 상관없지만 앞으로는 필요하다고 생각한다. EntityManager의 세부 동작과 생명 주기를 알지 못해도 간단한 데이터의 CRUD작업은 전혀 문제되지 않는다. 개발자에게 편의성을 제공하는게 Repository Interface의 역할이니깐. 하지만 실력을 더 키우기 위해선 보다 복잡한 데이터 액세스 작업등을 해야할 것이고 보다 높은 performance를 발휘해야 할 상황도 있을것이다. 그럴 때마다 repository.save()만 사용할수는 없지 않을까? 둘 다 잘 알아서 상황에 따라 잘 사용할 수 있는 진짜 개발자가 되도록 하자.

profile
인생을 재밌게, 자유롭게

0개의 댓글