포스트

자바 ORM 표준 JPA5

자바 ORM 표준 JPA 프로그래밍5

김영한 지음

JPA 에서 교과서로 유명한 책을 읽고 일부 부분을 메모해봤습니다.

13장 웹 애플리케이션과 영속성 관리

스프링 컨테이너 기본 전략

트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다. 트랜잭션이 시작할 때 영석송 컨텍스트를 생성하고 트랜잭션이 끝날 때 영속성 컨텍스트를 종료한다. 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근한다.
트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다. 멀티쓰레드 상황에서도, 트랜잭션을 다르게 사용하면 다른 영속성 컨텍스트를 사용한다.

준영속 상태와 지연 로딩

컨테이너 환경의 기본 전략을 사용하면, 트랜잭션이 없는 계층(service layer 이상의 계층)에서 엔티티는 준영속 상태다. 변경 감지 기능이나, 지연 로딩과 같은 기능이 작동되지 않는 것이다.
준영속 상태의 지연 로딩 문제를 해결하기 위해서는, 뷰가 필요한 엔티티를 미리 로딩해두거나, OSIV 를 사용하는 방법이 있다.
즉시 로딩으로 하는 경우, N+1 문제가 발생할 수 있기에 이런 경우에 JPQL 페치 조인 방법을 고려해보자.

FACADE 계층 추가

뷰를 위한 프록시 초기화를 담당하는 계층이다. 서비스 계층과 프리젠테이션 계층 사이에 논리적인 의존성을 분리할 수 있다.

OSIV

영속성 컨텍스트를 뷰까지 열어두는 것이다. 즉, 뷰에서도 지연 로딩이 가능하도록 한다. 그러나 뷰 같은 프레젠테이션 계층이 엔티티를 변경할 수 있다는 위험성이 있다.

1
2
3
4
Member member=memberService.getMember(id);
  member.setName("익명사용자1"); // 보안상의 이유로 익명 처리했다. 그러나 실제 db 의 member 이름이 변경된다.

이를 해결하기 위해서 다음과 같은 방법이 있다.

  1. 엔티티를 읽기 전용 인터페이스로 제공
  2. 엔티티 래핑
  3. DTO 만 반환

스프링 OSIV

스프링 프레임워크가 제공하는 OSIV는 비즈니스 계층에서 트랜잭션을 사용하는 OSIV 다.

  1. 클라이언트 요청이 들어옿면 서블릿 필터나, 스프링 인터셉터에서 영속성 컨텍스트를 생성한다.
  2. 서비스 계층에서 @Transactional 로 트랜잭션을 시작할 때 미리 생성해둔 영속성 컨텍스트를 찾아와서 트랜잭션을 시작한다.
  3. 서비스 계층이 끝나면 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시한다. 트랜잭션은 끝내지만 영속성 컨텍스트는 종료하지 않는다.
  4. 컨트롤러와 뷰까지 영속성 컨텍스트가 유지되므로 조회한 엔티티는 영속 상태를 유지한다.
  5. 서블릿 필터나, 스프링 인터셉터로 요청이 돌아오면 영속성 컨텍스트를 종료한다.

OSIV 정리

  • OSIV 는 클라이언트의 요청이 들어올 때 영속성 컨텍스트를 생성해서 요청이 끝날 때까지 같은 영속성 컨텍스트를 유지한다.
  • 엔티티 수정은 트랜잭션이 있는 계층에서만 동작한다. 트랜잭션이 없는 프리젠테이션 계층은 지연 로딩을 포함해서 조회만 할 수 있다.

OSIV 의 단점

  • OSIV 를 적용하면 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있다.
  • 프리젠테이션 계층에서 지연 로딩에 의해 SQL 이 실행될 수 있다.

14장 컬렉션과 부가 기능

하이버네이트는 엔티티를 영속 상태로 만들 때 컬렉션 필드를 하이버네이트에서 준비한 컬렉션으로 감싸서 사용한다. 하이버네이트는 컬렉션을 효율적으로 관리하기 위해 엔티티를 영속 상태로 만들 때 원본 컬렉션을 감싸고 있는 내장 컬렉션을 생성해서 내장 컬렉션을 사용하도록 참조를 변경한다.
이런 특징 때문에 컬렉션을 사용할 때, 즉시 초기화해서 사용하는 것을 권장한다.

1
2
3
Collection<Member> members = new ArrayList<Member>();

Set

set 은 엔티티를 추가할 때 중복된 엔티티가 있는지 비교한다. 따라서 엔티티를 추가할 때 지연 로딩된 컬렉션을 초기화한다.

List + @OrderColumn

List 인터페이스에 @OrderColumn 을 추가하면 순서가 있는 특수한 컬렉션으로 인식한다. 순서가 있다는 의미는 DB 에 순서 값을 저장해서 조회할 때 사용한다는 의미다.

1
2
3
4
5
@OneToMany(mappedBy= "board")
@OrderColumn(name = "POSITION")
private List<Comment> comments = new ArrayList<Comment>();

db 에 순서 값도 함께 관리한다. Comment 테이블에는 Position 컬럼이 추가된다. 그러나 실무에서는 잘 사용되지 않는다.

  • OrderColumn 은 Board 엔티티에서 매핑하므로, Comment 는 position 의 값을 알 수 없다. Comment 를 insert 할 때, position 이 저장되지 않는다. 따라서 추가적인 update SQL 이 발생한다.
  • List 를 변경하면 추가적인 SQL 이 발생한다. 순서를 바꿀 때마다 다른 댓글들의 Postion 값이 업데이트된다.
  • 중간에 Position 값이 없으면 조회한 List 에 null 이 보관되기에, List 를 순회하다가 NPE 가 발생할 수 있다.

OrderBy

Order By 는 데이터베이스의 Order By 절을 사용해서 컬렉션을 정렬한다.

1
2
3
4
5
@OneToMany(mappedBy= "board")
@OrderBy("username desc, id asc")
private List<Comment> comments = new ArrayList<Comment>();

Team.members 를 초기화 할 때, SQL 을 보면 order by 가 사용된 것을 확인할 수 있다.

1
2
3
4
select M.* from Member m where M.TEAM_ID =?
order by M.MEMBER_NAME DESC, M. ID asc

Converter

컨버터를 사용하면 엔티티의 데이터를 변환해서 DB 에 저장할 수 있다.

1
2
3
4
@Convert(converter=BooleanToYNConverter.class)
private boolean vip;

BooleanToYNConverter 를 사용해서, boolean 필드가 DB 에 Y 또는 N 으로 저장되도록 했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Converter
public class BooleanToYNConverter implements AttributeConverter<Boolean, String> {

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

    @Override
    public Boolean convertToEntityAttribute(String dbdate) {
        return "Y".equals(dbData);
    }
}

글로버 설정으로 모든 Boolean 타입에 컨버터를 적용하는 방법도 있다. @Convter(autoApply = true) 를 적용하면 된다.

리스너

모든 엔티티를 대상으로 언제 어떤 사용자가 삭제를 요청했는지 로그로 남기는 경우, 리스너를 활용할 수 있다.

  • 엔티티에 직접 적용 : @PrePersist 등의 어노테이션 활용
  • 별도의 리스너 등록 : @EntityListeners(DuckListener.class) 처럼 사용
  • 기본 리스너 사용 : xml 이나 config 에서 기본 리스너를 등록

엔티티 그래프

연관된 엔티티를 함께 조회하기 위해 사용할 수 있다. 엔티티를 조회하는 시점에 함께 조회할 연관된 엔티티를 선택해보자.

1
2
3
4
5
@NamedEntityGraph(name = "Order.withMember", attributeNodes = { @NamedAttributeNode("member") })
@Entity
public class Order ...

Order 를 조회할 때 엔티티 그래프를 사용하면 연관된 member 도 함께 조회할 수 있다.

1
2
3
4
5
6
7
EntityGraph graph = em.getentityGraph("Order.withMember");
Map hints = new HashMap();
hints.put("javax.persitence.fetchgraph", graph);

Order order = em.find(Order.class, orderId, hints);

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.