JPA 입문(1)

김민지·2022년 10월 22일
1

JPA

목록 보기
9/27

JDBC와 MyBatis 차이

JDBC(Java Database Connectivity)는 즉 자바에서 DB에 연결하기 위해 제공되는 API로서 SQL(Structured Query Language)에 접근한다.
JDBC 한 파일에서는 SQL작성, DB연결, Java언어가 모두 존재하기때문에 재사용성이 좋지 않다.
반면에 MyBatis는 SQL문이 어플리케이션 소스 코드로부터 분리된다. 또한 JDBC를 통해 수동으로 세팅한 파라미터와 결과 매핑을 대신해주어 JDBC로 처리하는 작업 보다 더 간편하게 작업할 수 있으며, 코드량이 줄어 생산성을 높여준다. 

JDBC

테이블을 객체로 매핑하는게 가능하다면 즉, 자바의 객체라는 개념이 데이터베이스에도 존재한다면 save(member)만 하면 되는걸 매번 필드값을 넣어주어야한다.
이것이 패러다임의 불일치로 나오는 불편함이다.

직렬화와 역직렬화

  • 전송 가능한 형태로 만드는 것을 의미한다. 객체들의 데이터를 연속적인 데이터로 변형하여 Stream을 통해 데이터를 읽도록 해준다.
  • 직렬화된 파일 등을 역으로 직렬화하여 다시 객체의 형태로 만드는 것을 의미한다.

패러다임의 불일치

데이터베이스와 자바는 다음과 같은 패러다임의 불일치가 있다.
1. 상속구현방법

  • 자바는 이전에 배웠던방법과 같이 상속을 구현하면 된다. 자바만 개발했던 사람이 한 클래스를 저장하는것을
    상상해본다면 save(team);과 같은 한줄로 저장을 하면된다고 생각하겠지만
    데이터베이스는 자바에서의 상속의 개념이 없고, 이와 비슷한 다른 개념을 이용해서 상속을 구현해야한다.
    예를들어 고양이테이블이 동물테이블을 상속받는다고할때
    고양이 엔티티를 저장할때는
    insert 고양이(고양이만의 필드)
    insert 동물(동물의 공통적인 필드)
    두개의 쿼리를 작성해야한다. 또한 하위타입이 어느타입이냐에 따라 dtype로 설정해주어야한다
    조회하는것도 두테이블을 조인해와서 조회를해야하는것이다
    자바처럼 간단한 문제가 아니다.
    jdbc api를 사용하면 이렇게 불편하다. 하지만 jpa는 자바의 장점을 활용할 수 있다.
    save(고양이)만 저장하면 고양이와 연관된 동물까지 알아서 저장해준다.
    나는 save(고양이) 코드를 작성하면 jpa가 알아서 그에대한 쿼리를 만들어준다는 얘기다
  1. 참조
  • 객체는 참조를 사용하여 다른 객체와 연관관계를 가지고 참조에 접근해서 연관된 객체를 조회한다
    반면에 테이블을 외래키를 사용하여 다른 테이블과 연관관계를 가지고, 조인을 사용해서 연관된 테이블을 조회한다
    디비의 패러다임에 맞춰서 id값을 필드로 저장하면 조회나 저장할땐 편리하지만 만약에 참조를 이용해야할땐?
    매번 조회하는 쿼리를 날려서 얻어와야한다. 자바의 객체참조기능이 있어도 사용하지 못하게되며
    객체지향적인 코드를 짤 수 없어진다.
    그렇다고 Team이라는 entity자체를 필드에 넣기에는 디비에 넣을때마다 매번 .getid를 해서 넘겨줘야한다
    그리고 결과저장할때도 매번 변환해주는 작업이 필요하겠지..?
    뭔가 중간에서 자바와 데이터베이스의 패러다임의 불일치를 해결해주는 무언가가 필요하다. 그게 jpa다
  1. 객체그래프탐색
  • 객체는 마음껏 객체그래프를탐색할수있어야하는데 쿼리문에 따라서 탐색범위가 달라져요
    이 말의 의미는..
    쿼리문에서 해당 연관관계맺어진 테이블을 조인하는지여부를 매번 확인해봐야한다는거예요
    엔티티가 어떤 sql문이 실행되는지에 따라 저장되는 필드가 달라져서 생기는 문제죠
  1. 비교
  • 데이터베이스는 기본키의 값으로 각 row를 구분, 객체는 동일성, 동등성 비교를 합니다
    jdbc에서는 동일한 id로 member를 조회하면 매번 다른 주솟값을 가지는 인스턴스가 반환됩니다.

jpa란 무엇인가?

  • 애플리케이션과 JDBC사이에서 동작

ORM이란?

  • 객체와 관계형데이터베이스를 매핑해주는 것
  • 객체와 테이블을 매핑해서 패러다임의 불일치 문제를 개발자 대신 해결해준다

변경감지

  1. flush()메서드 호출
  2. 엔티티와 스냅샷비교
  3. 업데이트 쿼리 생성 후 쓰기지연 sql저장소로 보냄
  4. 디비에 flush
  5. 디비에 commit

flush, flush(), commit의 차이

  1. 엔티티와 스냅샷 비교 후 변경된 것에 대한 SQL 생성
  2. 생성된 SQL을 쓰기 지연 SQL 저장소에 등록
  3. 쓰기 지연 SQL 저장소에 등록된 쿼리를 DB로 전송
    flush() : 1+2+3
    flush : 3
    그리고 commit까지 진행해야 디비에 반영됨.

em.persist후 jpa가 처리해주는 일

  • entity분석
  • sql문 작성
  • JDBC API사용
  • 페러다임불일치해결

왜 JPA?

  • 생산성
    지루하고 반복적인 코드는 JPA가 대신 처리해준다
  • 유지보수
    수정할 사항이 생겼을때 개발자가 직접 수정해야하는 사항이 적다
    SQL에 의존적인 개발이면
    쿼리를 일일이 DAO도 일일이 다 수정해줘야한다
  • 패러다임불일치해결
    JPA는 애플리케이션과 디비사이에있는 계층인데 이런계층이 있으면 최적화에 도움이 될 수 있다. 1차캐시와 같은 예시가 있다
  • DBMS에 종속적이지않음

준영속

  • repository.save를 호출하면 jpa는 경우에따라 merge or persist를 호출한다
    id값은 영속성컨텍스트에있는데(관리대상) value값은 다른경우 merge를 호출하고
    둘다 같은 경우 persist를 호출한다
    merge를 호출하면 내가 newEntity = repository.save(entity)에서 entity!=newEntity이다
    영속화된 객체니까 다른 해시코드가 부여된다

merge의 동작방식

  1. 1차 캐시에서 엔티티를 찾는다
  2. 없으면 디비를 조회한다
  3. 찾아온 것에 값을 채운다 없으면 새로운 객체를 생성하고 거기에 값을 채운다
  4. 그리고 값을 반환한다

jpa에서 단순히 식별자로 조회하는 find는 flush가 발생하지 않는다

em.find: 영속성 컨텍스트에서 먼저 찾고 없으면 DB를 찌른다

  1. 영속성 컨텍스트에서 찾는다
    -> 어차피 해당 엔티티에 대한 변경 사항들은 영속성 컨텍스트 내에 저장된 엔티티 객체에 다 반영되어 있어서 flush 할 필요가 없음
  2. 영속성 컨텍스트에 없어서 DB에서 찾아온다
    -> 영속성 컨텍스트에 없었으니 당연히 변경 사항도 없음

예제

sequece 시 save와 update의 처리 과정

@Test @Transactional
public void test() {

    Member first = repository.save(new Member("first")); // 1. save 쿼리
    log.info("first 를 저장합니다.");                       // 2. save 로깅

    first.changeName("second");         // 3. second update 쿼리
    log.info("second 로 변경합니다.");      // 4. second 로깅

    first.changeName("third");          // 5. third update 쿼리
    log.info("third 로 변경합니다.");       // 6. third 로깅
}
  • sequence옵션이라고 했을때 위의 코드는 다음과 같이 동작한다
    1line : persist(first); -> 영속성 컨텍스트에 넣어놓고 쓰기지연저장소에도 넣어놓음
    2linm : 출력됨
    3line : 변경이 됨
    4line : 출력됨
    5line : 변경됨
    커밋 : 커밋직전에 flush가 일어나면서 쓰기지연저장소에있던 1line에 대한 쿼리가 디비로 전송된다
    member의 스냅샷과 나중상태를 비교한다. 뭔가 달라졌다. 그래서 update쿼리도 생성한다
    디비에 flush하고 커밋한다.

IDENTITY

  • Identity는 생성전략 자체를 db에 위임하는데 영속성 컨텍스트에서는 식별자를 통해서 관리를 하니까 save할때 동시에 insert가 나간다
  • SAVE이외에는 쿼리가 나가지 않는다

sequence 최적화

  • 식별자를 구하기 위해 시퀸스를 조회한다음에 조회한 시퀸스를 할당해야한다. 이렇게 두번의 작업을 해야된다. 그래서 jpa는 이 횟수를 줄이기 위해 이 방법을 사용한다. 어떤방법이냐면 일단 1~50정도까지를(설정한범위를) 메모리에 할당해두고 할당해둔것으로 id값을 준다

데이터 중심의 개발

  • 예를들어 member가 team_id를 가지고 있는 개발방식은 객체지향적이지 못하다.
    왜냐하면 객체지향적인설계라는것은 참조를 이용해 객체그래프를 탐색할 수 있어야하는데 id값을 가지고 있는것은 테이블설계에 맞춘 개발방식이기때문이다. 참조값이 아닌 id를 저장하게 되면 참조를 이용할 수 없게되고 매번 디비에서 찾아내야한다.

식별과 비식별

  • 부모의pk를 fk로 사용하는 것 : 비식별
  • 부모의 pk를 자신의 pk로도 사용하는것 : 식별

@idclass?

  • 둘이상의 컬럼으로 구성된 복합기본키는 단순하게 id어노테이션을 두개이상쓰면안된다.

  • PA 어노테이션 @IdClass 는 복합키 사용을 위해 쓴다. 
    칼럼을 여러개 묶어서 사용하는 복합키는 사용하기 전에 클래스를 별도로 하나 만들고
    사용하는 테이블 상단에 해당 어노테이션을 적어놓고 사용.

  • 복합키는 별도의 식별자 클래스로 만들어야한다

  • Serializeable 을 구현해야한다

  • equels와 hashcode구현해야함

  • 기본생성자 필요

  • 식별자 클래스는 public이어야함

  • idclass말고 embedded를 사용할수도있음

idclass vs embeddedId

  • idClass
    1) 복합키를 사용할 클래스에 @Idclass정의
    2) serializeable구현한 class필요
    3) 만약 이 클래스와 비식별관계인 child class가 있다면 그 클래스에 joincolumns정의 필요
    4) 만약 이 클래스와 식별관계인 child class가 있다면 그 childclass에 @ManyToOne정의
  • embeddedId
    1) 사용할 클래스의 필드로 embeddedId타입의 class를 어노테이션과 함께 선언
    2) 해당클래스는 @Embeddable과 함께 Serializable상속
    3) 동일
    4) 만약 이 클래스와 식별관계인 child class가 있다면 child에 @Id대신 @MapsId(parent_id)를 선언

예제

https://velog.io/@flre_fly/JPA-%EC%A7%88%EB%AC%B8-%EB%AA%A8%EC%9D%8C

비식별 복합키

  • 복합키를 사용하는 class에 @IdClass(복합키클래스명.class)선언
  • 복합키 클래스는 serializeable을 구현해서 만들고 위의 방식대로 구현한다
Parent parent = new Parent();
parent.setId1("id1");
parent.setId2("id2");
em.persist(parent);
  • 영속성컨텍스트에 엔티티를 등록하기 직전에 내부에서 식별자 클래스인 parentId를 생성하고 영속성컨텍스트의 키로 활용한다

식별·비식별의 장단점

  • 식별관계는 부모의 키를 자식이 갖고있게 되니 자식의 기본키가 늘어나서 조인할때의 sql이 복잡해지고 기본키 인덱스가 불필요하게 커질 수 있다
  • 식별관계는 보통 복합키를 만들어야하는데 굉장히 번거롭다
    번거롭다는 말은 테이블구조가 유연하지 못하다는 말이다. 매번 위의 과정을 거쳐줘야하니 많은 노력이 필요하다
  • 식별관계라면 특정상황에 조인없이 하위테이블만으로 검색을 완료할 수 있다
    부모id가 a인 모든 자식을 조회한다던가.. 하는일
    -> 될수있으면 비식별관계를 사용해라

상속관계매핑

1) 부모, 자식들을 각각의 entity로 매핑하고 매번 조인을 사용하는 전략

  • 객체는 type으로 서로를 구분할 수 있지만 테이블은 그럴 수 없으니 dtype이라는 서로를 구분하는 컬럼이 따로 필요하다
  • 부모에 @Inheritance(strategy = InheritanceType.JOINED),
    @DiscriminatorColumn(name = "DTYPE") 선언 필요
    자식에@DiscriminatorValue(name = "A")선언 필요

장단점

  • 테이블이 정규화된다(=중복되는 컬럼이 없어짐)
  • 저장공간을 효율적으로 사용할 수 있다
  • 조회할때 조인이 많이 돼서 성능저하 일어날수있음
  • 조회쿼리가 복잡하다
  • 데이터를 등록할 insert sql을 두번실행

2) 하나의 테이블의 부모,자식컬럼을 모두때려박는 전략

장단점

  • 조인이 필요없기때문에 조회성능이 빠르다
  • 조회쿼리 단순
  • 자식엔티티가 매핑한 컬럼은 모두 null을 허용해야한다
  • 테이블이 커질 수 있다. 그래서 상황에 따라서는 조회성능이 느려질 수도 있다

3) 자식테이블만 구현하는 대신 자식들에게 공통적인 컬럼들도 매번 중복해서 적는 전략

  • 추천x
  • 여러 자식 테이블을 함께 조회할때 성능이 느리다
  • 자식들을 통합해서 쿼리하기가 어렵다
  • 자식을 구분해서 처리할때 효과적

mappedsuperclass

  • 예를들어 엔티티마다 수정일자, 생성일자를 만들어서 넣어줘야한다. 근데 엔티티마다 이걸 넣어주기 귀찮다. 이때 mappedsuperclass를 활용하면된다. 공통필드를담는 추상클래스를 만들고 mappedsuperclass라는 어노테이션을 붙여준다. 그리고 이를 상속받으면 엔티티마다 공통필드를 클래스 하나로 정의할 수 있다

조인테이블

https://velog.io/@flre_fly/%EC%A1%B0%EC%9D%B8%ED%85%8C%EC%9D%B4%EB%B8%94

  • 쓰는걸 별로 못봤어서 별로 안중요해 보인다. 그래서 따로 빼놨다.

엔티티 하나에 여러 테이블 매핑

  • 예를들어 Board와 BoardDetail이라는 테이블이 있다고 가정하자 근데 나는
    BoardDetail만 주르륵 보고싶고 Board만따로 주르륵 보고싶어서 그런건지 한 엔티티에 대해 두 테이블을 매핑하고 싶다.
    이런경우에 쓰는걸까?
  • 해당 엔티티에 @SecondaryTable(name, pkjoincolumns);을써주고
    두번쨰 테이블에 대한 변수들선언 위에는 @Column(table = "두번째테이블")
@Entity
@NoArgsConstructor
@AllArgsConstructor
@SecondaryTable(name = "board_detail", pkJoinColumns = @PrimaryKeyJoinColumn(name= "board_detail_id"))
public class Board {
    @Id
    @Column(name = "board_id")
    private Long id;
    private String title;
    @Column(table = "board_detail")
    private String content;
}
@Test
	@Transactional
	@Commit
	public void 엔티티하나에여러테이블매핑(){
		Board board = new Board(1l, "asd", "Asd");
		em.persist(board);
	}



출처
https://hyoni-k.tistory.com/70
https://flowarc.tistory.com/entry/Java-객체-직렬화Serialization-와-역직렬화Deserialization
https://junghyungil.tistory.com/138
https://dev-elop.tistory.com/entry/JPA-IdClass-란

profile
안녕하세요!

1개의 댓글

comment-user-thumbnail
2022년 11월 27일

정말 멋있어용❤

답글 달기