[JPA] 핵심 정리

BBinss·2020년 8월 11일
0
post-thumbnail

JPA란

Java Persistence API. Java ORM(Object Relational Mapping) 기술 표준 인터페이스.

SQL 중심 개발 이슈

데이터를 영구적으로 저장하기 위해서는 저장소를 사용해야 하며, 가장 보편적인 관계형 데이터 베이스 관리 시스템(RDBMS)을 사용하기 위해서는 DB SQL을 사용하게 된다.

1- CRUD를 위한 코드를 많이 생산해야 한다. 스키마가 변경되는 경우 SQL 수정이 필요하다.

class MemberEntity {
    long id;
    String name;
    // String email;
}
INSERT INTO member (name) VALUES (?);
SELECT id, name FROM member WHERE id = ?;
UPDATE member SET name = ? WHERE id = ?

2- JAVA는 상속 및 참조 관계를 가지지만, DB TABLE은 FK를 통한 연관관계를 가진다.
: 패러다임 불일치.

DB 중심 모델링

class MemberHistoryEntity {
    long id;
    long memberId;
}

class MemberEntity {
    long id;
}

객체 중심 모델링

class MemberHistoryEntity {
    long id;
    MemberEntity memberEntity;
}

class MemberEntity {
    long id;
}

3- 필요에 따라 여러가지 DB 조회 메소드와 SQL이 생성 필요하다.

Boilerplate code를 많이 생산해야 한다는 의미.

memberDao.getMember(long id);
memberDao.getMemberAndHistory(long id);
memberDao.getMemberAndAnother(long id);

memberDao.updateMemberName();
memberDao.updateMemberEmail();

4- DB에 조회되는 데이터들은 다른 인스턴스를 가진다.
: 이자체로 문제나 JPA만의 이점을 가진다는 의미는 아니며,
JPA를 통해 Java 객체도 동일한 동일성을 가지게 보장 한다는 의미이다.

MemberEntity member1 = memberDao.getMember(1);
MemberEntity member2 = memberDao.getMember(1);

assertThat(member1 == member2).isTrue();
// false
List<MemberEntity> memberList = memberDao.getMemberAll();
// 첫 번째 index의 entity가 member-id=1 데이터라고 가정한다면
MemberEntity member1 = memberList.get(0);
MemberEntity member2 = memberList.get(0);

assertThat(member1 == member2).isTrue();
// true

기본 개념 및 동작 방식


JPA는 표준 인터페이스고 구현체중 가장 보편적으로 쓰는게 Hibernate이다.

JPA는 매 요청시마다 EntityManagerFactory를 통해 EntityManager를 생성하며,
내부적인 커넥션 풀을 통해 DB에 접근한다.

영속성 컨텍스트
https://hyeooona825.tistory.com/87
논리적인 개념으로 엔티티를 영구 저장하는 환경. 영속성의 장점은 아래 "특징"에서 설명.


Entity 생명 주기
1- 비영속 (new) : 영속성 컨텍스트와 전혀 관계가 없는 상태
2- 영속 (managed) : 영속성 컨텍스트에 저장된 후 관리되는 상태.
: em.persist(memberEntity);

3- 준영속 (detached) : 영속성 컨텍스트에서 관리되었다가 관리에서 제외한 상태.
: em.detach(memberEntity);

4- 삭제 (removed) : 삭제된 상태. flush()가 일어나는 시점에 영속성 컨텍스트와 DB 삭제.
: em.remove(memberEntity);

트랜잭션 범위의 영속성 컨텍스트
https://derekpark.tistory.com/93

flush

  • 실제 DB로 데이터가 동기화 되는 시점. (DB에 쿼리가 실행되는 시점)

flush 발생 시점
1. em.flush()를 호출.
2. JPQL 실행시.
3. 트랜잭션 커밋시.

@Transactional(readOnly = true)

  • 트랜잭션 읽기 전용 모드의 경우 flush()가 발생하지 않음.
    변경 감지 (dirth checking)시 사용되는 스냅샷 비교와 같은 비용을 줄이게 된다.

기본 함수

Spring 에서 편리함을 위한 @Transactional 및 CrudRepository와 같은 mapping interface를 제공.

// 엔티티 매니저 팩토리 애플리케이션 전체에서 공유.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("unit-member");
// 엔티티 매니저 쓰레드간 공유 금지.
EntityManager em = emf.createEntityManager();

// 저장
// public void persist(Object entity);
em.persist(memberEntity);

// 조회
// public <T> T find(Class<T> entityClass, Object primaryKey);
em.find(MemberEntity.class, 1);

// 삭제
// public void remove(Object entity);
em.remove(memberEntity);

객체 지향 모델링과 조인 전략

테이블 지향 모델링과 조회 비교
테이블 지향 모델링

// MemberEntity와 MemberHistoryEntity를 각각의 식별자로 조회.
MemberEntity memberEntity = em.find(MemberEntity.class, memberDto.getId());
MemberHistoryEntity memberHistoryEntity = em.find(MemberHistoryEntity.class, historyDto.getId());

객체 지향 모델링

// 객체는 참조를 사용해서 연관된 객체를 찾는다.
MemberEntity memberEntity = em.find(MemberEntity.class, memberDto.getId());
MemberHistoryEntity memberHistoryEntity = memberEntity.getMemberHistoryEntity();

단방향 매핑
https://dublin-java.tistory.com/51
@NoArgsConstructor(access = AccessLevel.PROTECTED) https://cobbybb.tistory.com/14

  • Entity는 Builder를 통해서만 생성, Setter를 제거하여 불변 객체 유지.
  • private final을 통해 입력 받지 않을 멤버 변수 지정.

MemberEntity와 MemberHistoryEntity의 단방향 매핑 후 CRUD 실행 쿼리 확인.

@Entity
@Table(name = "member")
@Builder
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
class MemberEntity {
    @Id
    @GeneratedValue
    private final long id = 0;
    
    // 단방향 매핑.
    /* cascade 설정 하지 않고 자식 entity까지 연관 관계를 통해 insert시 
       "object references an unsaved transient instance" 와 같은 에러 발생.
    */
    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
    @JoinColumn(name = "member_id") // 자식 Entity가 가지는 외래키 이름.
    Set<MemberHistoryEntity> memberHistoryEntitySet = new HashSet<>();
    
    String email;
}

@Entity
@Table(name = "member_history")
@Builder
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
class MemberHistoryEntity {
    @Id
    @GeneratedValue
    private final long id;
    
    @Column(name = "member_id")
    long memberId;
    
    String history;
}
// memberHistoryEntitySet 생성.
MemberHistoryEntity memberHistoryEntity = MemberHistoryEntity.builder()
		.history("변경사항1")
		.build();
Set<MemberHistoryEntity> memberHistoryEntitySet = new HashSet<>();
memberHistoryEntitySet.add(memberHistoryEntity);

// memberEntity 생성.
MemberEntity memberEntity = MemberEntity.builder()
		.memberHistoryEntitySet(memberHistoryEntitySet)
		.email("a@gmail.com")
		.build();
                    
// memberEntity 저장.
memberRepository.save(memberEntity);
/*
    insert into member (email) values (?)
    insert into member_history (member_id, history) values (?, ?)
    update member_history set member_id=? where id=?
*/

// memberEntity 조회.
Optional<MemberEntity> optionalMemberSelectEntity = memberRepository.findById(1L);
/*
    select member0_.id as id_1_4_0
    from member member0_
    left outer join member_history member1_ on member0_.id=member1_.member_id
    where member0_.id=?
*/
// memberEntity 삭제.
memberRepository.deleteById(1L);
memberRepository.delete(memberEntity);
/*
    # deleteById() 인경우 select 발생.
    select member0_.id as id_1_4_0
    from member member0_
    left outer join member_history member1_ on member0_.id=member1_.member_id
    where member0_.id=?
    
    update member_history set member_id=null where member_id=?
    delete from member where id=?
*/

양방향 매핑

  1. 각각의 단방향 매핑 2개의 개념.
  2. 외래키를 갖는 쪽이 주인의 개념을 갖는다. (@JoinColumn)
  3. 주인 Entity에서만 insert. 주인이 아닌쪽에서는 select만 가능.
    : 다만 양쪽 모두에 조인 연관 관계를 추가하도록 권장.
    : 입력, 수정을 하겠다하면 insertable, upd
  4. mappedBy 속성이 "다른 Entity에게 매핑되었어. 나는 주인이 아니다" 라는 의미.
  5. 중요한 부분은 주인쪽에 외래키 컬럼을 별도로 설정하지 않는다.
    아래의 예제에서 MemberHistoryEntity.memberId 컬럼 필드를 중복으로 선언하지 않음. 자동적으로 외래키 정보가 입력되며, 중복 선언으로 인한 insert=false, update=false 오류도 발생하지 않는다.
@Entity
@Table(name = "member")
@Builder
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
class MemberEntity {
    @Id
    @GeneratedValue
    long id;
    
    @OneToMany(mappedBy = "memberEntity", fetch = FetchType.EAGER)
    private final Set<MemberHistoryEntity> memberHistoryEntitySet = new HashSet<>();
    
    String email;
}

@Entity
@Table(name = "member_history")
@Builder
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
class MemberHistoryEntity {
    @Id
    @GeneratedValue
    long id;
    
    String history;
    
    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
    @JoinColumn(name = "member_id")
    MemberEntity memberEntity;
}
// 외래키를 가지지 않고 mappedBy를 사용한 memberEntity 생성. 주인이 아님.
MemberEntity memberEntity = MemberEntity.builder()
		.email("a@gmail.com")
		// 여기서 .memberHistoryEntitySet() 할 수 없도록 entity에서 private final 로 선언. 
		// 이후 add() 사용시 NPE 방지를 위해 초기화.
		.build();

// memberHistoryEntity 생성.
MemberHistoryEntity memberHistoryEntity = MemberHistoryEntity.builder()
		.history("변경사항1")
		.memberEntity(memberEntity)
		.build();
// 순수한 객체 관점에서 주인이 아닌 memberEntity쪽에도 연관 관계의 memberHistoryEntity 데이터 추가.
memberEntity.getMemberHistoryEntitySet().add(memberHistoryEntity);

// memberHistoryEntity 저장.
memberHistoryRepogitory.save(memberHistoryEntity);
/*
    insert into member (email) values (?)
    insert into member_history (member_id, history) values (?, ?)
*/

// 조회 양방향 가능.
// 삭제 cascade 속성을 통해 역시 양방향 가능.

양방향 매핑의 장점

사실 양방향이란 건 서로 다른 단방향 매핑으로 이루어져 있으며,
주인을 기준으로 입력을 하고 주인이 아닌쪽에서도 조회가 필요한 경우가 있으므로 (반대 방향 조회) 설정하는 매핑의 개념.
물론 FK 테이블에 입력시 update가 일어나지 않는 쿼리상 이점도 있다.

cascade, orphanRemoval

  • cascade (전이) : 영속 전이에 대한 속성. Entity의 변화가 있을때 연관되어 있는 Entity까지 영향을 줄건지에 대한 속성.
  • orphanRemoval (고아) : 연관관계의 Entity의 참조를 제거하는 것만으로도 연관 관계 Entity를 DB 삭제.
@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true)
Set<MemberHistoryEntity> memberHistoryEntitySet;

// casecade 속성으로 영향이 있는 명시적 삭제. entityManager.remove();
memberRepogitory.delete(memberEntity);
/*
    delete from member where id=?
    delete from member_history where member_id=?
    delete from member_history where member_id=?
    ... N+1 발생.
*/

// orphanRemoval 속성으로 영향이 있는 Entity의 참조 제거.
// 물론 entity manager를 통해 영속성 컨텍스트에서 가져온 Entity일 경우 발생.
memberEntity.getMemberHistorySet().clear();
/*
    delete from member_history where id=?
    delete from member_history where id=?
    ... N 발생.
*/

다만 두가지 옵션 모두 delete시 N+1 현상 발생하므로 별도의 FK를 통한 삭제 JPQL 메소드를 사용하는게 성능상 이점. (삭제를 위한 select도 일어나지 않게 됨)

@Modifying(clearAutomatically = true)
@Query("DELETE FROM MemberHistory h WHERE h.memberEntity.id = :memberId")
void deleteByMemberId(@Param("memberId") long memberId);

장점과 특징

장점

  • 객체 중심의 개발, APP과 DB의 패러다임 불일치 해결
  • 생산성과 유지보수
  • 선택적 성능 향상

1- 1차 캐시와 동일성 보장

  • 동일한 트랜잭션내에서는 같은 인스턴스(엔티티)를 보장.
  • APP에서의 DB 트랜잭션 Reapeatable Read 레벨을 보장.
    https://jupiny.com/2018/11/30/mysql-transaction-isolation-levels
    : Mysql의 기본 트랜잭션 레벨로 트랜잭션별 snapshot을 통해 아래 내용 보장.
    -- 1. 아직 COMMIT 되지 않은 데이터 읽지 않음.(dirty read)
    -- 2. 동일 트랜잭션에서 동일한 SELECT 결과 보장. (non-repeatable read)
    -- 3. 예전 SELECT 결과에 없던 row가 생기지 않음 (phantom read)
@Transactional(value = "memberTransanctionManager", readOnly = true)
public MemberEntity getMember() {
    MemberEntity memberEntity1 = em.find(MemberEntity.class, 1);
    MemberEntity memberEntity2 = em.find(MemberEntity.class, 1);
    
    assertThat(memberEntity1 == memberEntity2).isTrue();
    // true
}

2- INSERT, UPDATE 쓰기 지연
https://cheese10yun.github.io/jpa-flush/

transaction.begin();

em.persist(memberEntity);
em.persist(memberEntity2);
em.persist(memberEntity3);

// 1. 영속성 컨텍스트 flush.
// 2. 쓰기 지연 SQL 저장소의 쿼리를 DB에 호출.
// 3. DB commit.
transaction.commit();


3- 변경 감지 (dirty checking)

transaction.begin();

MemberEntity memberEntity = em.find(MemberEntity.class, 1);
memberEntity.changeEmail("a@gmail.com");

// 조회한 엔티티를 수정할 때 별도의 save(), update() 호출 없이 자동 변경 감지.
transaction.commit();

JPA는 엔티티의 최초 상태를 영속성 컨텍스트에 스냅샷으로 저장해 둔다.
1. 트랜잭션 커밋시 flush() 호출.
2. 영속성 컨텍스트의 entity를 수정 했으므로, 스냅샷과 비교해서 변경된 엔티티 확인.
3. 변경된 엔티티가 있으면 update 쿼리를 생성해서 쓰기 지연 SQL 저장소 보관.
4. DB에 쓰기 지연 저장소의 SQL 실행.
5. DB 트랜잭션 커밋.

builder pattern
builder pattern을 사용하기 때문에 setter를 통한 변경 감지를 사용하지 않는다.
대신 명확한 변경을 위한 method를 구현하여 해당 method로만 update를 하도록 권장.

변경된 필드만 수정
@DynamicUpdate 하지만 모든 필드를 변경하도록 하면, 모든 수정 쿼리가 동일하다.

4- 지연 로딩, 즉시 로딩
지연 로딩
: 해당 객체가 사용될 때 조회 및 로딩. (FetchType.LAZY)
: Lazy 로딩이 설정되어 있는 객체는 proxy 객체로 조회가 된다.

MemberEntity memberEntity = em.find(MemberEntity.class, memberDto.getId());

log.info("memberHistoryEntity : {}", memberEntity.getMemberHistoryEntity());

MemberHistoryEntity memberHistoryEntity = memberEntity.getMemberHistoryEntity().getId();

// SELECT * FROM member WHERE id = ?;
// memberHistoryEntity : class app.jpa.Member$HibernateProxy$c3eaiZR
// SELECT * FROM member_history WHERE id = ?;

즉시 로딩
: JOIN 구문으로 연관된 객체까지 미리 조회 및 로딩. (FetchType.EAGER)

MemberEntity memberEntity = em.find(MemberEntity.class, memberDto.getId());
MemberHistoryEntity memberHistoryEntity = memberEntity.getMemberHistoryEntity();

/* 
  SELECT * FROM member A
  INNER JOIN member_history B ON A.history_id = B.id;
*/

4- JPQL && Query methods
: Java Persistence Query Language
: 특정 SQL에 종속되지 않은 Java 객체 지향 쿼리 언어.
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.at-query
: JPQL은 영속성 컨텍스트를 먼저 조회하지 않고 먼저 DB를 조회 하며,
식별자 값 기준으로 동일한 entity가 있는 경우 영속성 컨텍스트 1차 캐시에 저장하지 않고 버린다.

Query methods
: method 선언만으로 이름에 맞게 JPQL 쿼리를 자동 생성해주는 기능.
: 예) CrudRepository.findAll();

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation

5- join, fetch join
https://m.blog.naver.com/PostView.nhn?blogId=writer0713&logNo=221550531372&proxyReferer=https:%2F%2Fwww.google.com%2F

주의 사항

N+1 현상
https://kapentaz.github.io/jpa/hibernate/@ManyToOne%EC%9D%98-N+1-%EB%AC%B8%EC%A0%9C-%EC%9B%90%EC%9D%B8-%EB%B0%8F-%ED%95%B4%EA%B2%B0/

예를 들어 member에 3건, member_history테이블에 2건(중요하지 않음) 있는 경우,
member기준의 조인을 통한 조회시 1번의 조인 쿼리로 모든 조인된 데이터를 조회해 오지 않고,
member_history(3) + member(1) 건의 쿼리를 통해 조회하는 현상.
(3건의 member 기준의 데이터를 member_history에 각각 조회)

1- JPQL은 global fetch 전략을 관여하지 않고 순수하게 구문만을 해석해서 SQL 실행하는 특징이 있음.

2- findAll(), findAllById()과 같이 전체 혹은 다수의 데이터 조회시 발생.

// FetchType.EAGER 인 경우 즉시 member_history N+1 조회.
List<MemberEntity> memberEntity = memberEntity.findAll();
// select * from member;
// select * from member_history where member_id = 1
// select * from member_history where member_id = 2
// select * from member_history where member_id = 3


// FetchType.LAZY 인 경우 해당 Entity 조회시 DB 조회.
// select * from member;
memberEntity.get(0).getMemberHistoryEntity();
// select * from member_history where member_id = 1
// select * from member_history where member_id = 2
// select * from member_history where member_id = 3

JPQL 입장에서는 memberEntity가 @OneToMany join 관계를 가지고 있더라도 구문만을 해석해서 1차적으로 member 테이블을 조회하는 SQL만을 생성한다.

그리고 JPA에서 memberEntity를 살펴봤더니 fetch 전략에 따라 즉시 로딩인 경우 (지연 로딩도 결국은 동일) memberHistoryEntity를 같이 조회해줘야 하기 때문에 별도로 N번 만큼 memberHistory 테이블을 조회하게 된다.

해결방안
0- id를 통한 조회. findById() 와 같이 id를 통한 조회시 (outer/inner) join을 통해 DB 조회.
1- @BatchSize
2- Fetch Join
3- Lazy Loading

MultipleBagFetchException
: 2개 이상의 OneToMany 자식 테이블에 Fetch Join을 선언할 때 발생.

1- Set 사용
2- Lazy Loading
3- @LazyCollection(LazyCollectionOption.FALSE)

fetch join 카테시안 곱
: 2개 이상의 테이블간에 유효한 Join 조건이 없는 경우 모든 행을 곱한 수만큼 조회 되는 경우.
: @OneToMany를 2개 이상 갖는 경우 발생.

예를 들어,

member

id
1
2

member_history

idmember_id
11
21
31
42

와 같이 member에 2건, member_history에 4건이 있을 때 memberEntity를 join fetch (inner join)으로 조인된 4건을 전체 조회해 왔다.

JPA의 MemberEntity 입장으로는 2개의 id 결과 값에 각각 MemberHistoryEntity가 3개, 1개씩 들어있는 형태로 조회를 해오고 싶다.
하지만 DB에서 조회된 형태의 4개의 결과값에 중복된 MemberEntity id 결과값을 리턴한다.

생각해보면, JPA 입장에선 DB에서 조회된 결과를 Entity에만 잘 매핑한 것이지 모든 결과를 확인하며 중복을 제거해주진 않는다.

  • Join 설정된 Entity를 Set Collection 으로 선언.
  • 조회 JPQL에 distinct 선언.

두가지 방법 중 하나로 원하는 결과를 얻을 수 있다.

@JsonIgnore 설정

  • 양방향 조인 설정 후 json 형태로 객체 리턴시 무한 참조가 일어날 수 있음.
    조회의 기준이 되는 Entity 반대쪽에는 @JsonIgnore 설정.
@Entity
@Table(name = "member")
class MemberEntity {
    @OneToMany(fetch = FetchType.EAGER, mappedBy = "memberEntity")
    // NPE 방지.
    Set<MemberHistoryEntity> memberHistoryEntitySet = new ArrayList<>();
}

@Entity
@Table(name = "member_history")
class MemberHistoryEntity {
    @ManyToOne(mappedBy = "memberHistoryEntitySet", insertable=false, updatable=false)
    @JsonIgnore
    MemberEntity memberEntity;
}

JPA 사용의 방향성

  • 작은 단위의 필요한 데이터만 담고 있는 정규화 잘 된 DB 모델링.
    이를 통해 entity와 테이블간 간결한 1:1 연결 구조.
  • 가능한 native query보다 query method interface만 선언하는 형태로 repository를 구성.
    (query method를 쓴다는건 함수 이름만으로도 유추 가능한 직관적인 DB 조회를 한다는 의미므로)

    query method > JPQL > native query

@Repository
public interface MemberRepository extends CrudRepository<MemberEntity, Long> {
  int countByEmail(String email);
  
  List<MemberEntity> findAllByEmail(String email);
}
  • 복잡한 native query가 아닌 개별 entity의 id를 통해 조회 후에 데이터 조합 혹은 간단한 조인 수준으로 가져올 순 없을지 한번 더 고민.
  • 일대다 양방향 매핑 권장. 일대다 단방향 매핑은 외래키를 자신이 가지게 된다.
  • 조인 데이터 조회시 식별키를 이용한 조회를 하며,
    다수 데이터 조회시 N+1 피하기 위해 Fetch join 선언된 interface 사용.
    @Query("SELECT DISTINCT m FROM MemberEntity m JOIN FETCH m.memberHistoryEntityList")
    Set<MemberEntity> findAllJoinMemberHistory();
  • 적절한 조인을 통해 기준이 되는 entity를 통해서만 save() 하는 것도 필요.
    : 예로 member_history는 member 테이블에 액션이 일어날 때만 저장하기로 하였다면,
    1- memberEntity와 memberHistoryEntity를 조인 설정.
    2- memberHistoryEntity에서는 memberEntity insertable=false를 지정.
    (memberEntity 통해서만 입력하겠다 의미)
    3- memberEntity를 통해서만 save하며 해당 액션이 일어날때 memberHistoryEntity를 같이 save 해준다.

  • service의 method단위로 @Transactional을 설정하여 트랜잭션 및 1차 캐시 동일성을 보장 받음.

  • delete 시에는 JPQL와 FK를 통한 별도 삭제 메소드를 사용하는 것이 성능상 이점. (N+1 미발생)

  • 신규 Entity 생성시에는 반드시 쿼리 로그로 성능상 이슈 확인.

TIP

도메인 클래스 컨버터
https://arahansa.github.io/docs_spring/jpa.html#core.web.basic.domain-class-converter

Entity 식별키를 기준으로 별도의 Service나 Repository 호출 없이 Controller에서도 해당 Entity를 조회 하도록 지원.

불필요한 코드 없이 Entity를 조회하기 위한 목적에 맞는 생산성 높은 기능을 제공!
@EnableSpringDataWebSupport
class SpringApplication {}
@RestController
@RequestMapping(value = "/member", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public class MemberController {
    @GetMapping(value = "/{memberId}")
    public MemberEntity member(
           @PathVariable(value = "memberId") MemberEntity memberEntity
    ) {
        return memberEntity;
    }	
}

OSVI (Open Setting View)
영속성 컨텍스트의 영속 상태를 View 레이어까지 열어둘 수 있다.
View단에서까지 지연 로딩이나 트랜잭션을 가지지 않는다면 false로 지정.

Spring JPA 디버깅 쿼리 로그 설정

// spring-boot:2.1.3 기준.
logging:
  level:
    org:
      hibernate:
        type:
          descriptor:
            sql: trace   # 쿼리 바인딩 파라미터까지 노출.
        SQL: debug   # 디버깅 쿼리 노출.

참고 자료

profile
궁빈

0개의 댓글