쌩로그

자바 ORM 표준 JPA 프로그래밍 Ch.13. 웹 애플리케이션과 영속성 관리 (2) 본문

Spring/JPA

자바 ORM 표준 JPA 프로그래밍 Ch.13. 웹 애플리케이션과 영속성 관리 (2)

.쌩수. 2024. 1. 30. 21:59
반응형

목록

  1. 포스팅 개요
  2. 본론
      2-1. OSIV
      2-2. 너무 엄격한 계층
  3. 요약

1. 포스팅 개요

자바 ORM 표준 JPA 프로그래밍13장 웹 애플리케이션과 영속성 관리를 학습하며 정리한 2번째 포스팅이다.

이번 포스팅에서 OSIV에 대해 알아본다.

참고로 이 포스팅애서 OSIV에 대해서 간단히 소개했었지만, 책에서 더 자세히 나온다.

참고로 이 글에서 이어쓰는 글이다.

2. 본론

2-1. OSIV

OSIV(Open Session In View)는 영속성 컨텍스트를 뷰까지 열어둔다는 뜻이다.
영속성 컨텍스트가 살아있으면 엔티티는 영속 상태로 유지된다.
따라서 뷰에서도 지연 로딩을 사용할 수 있다.

참고

OSIV는 하이버네이트에서 사용하는 용어다. JPA에서는 OEVI(Open EntityManager In View)라 한다. 하지만 관례상 모두 OSIV로 부른다.

과거 OSIV: 요청 당 트랜잭션

OSIV의 핵심은 뷰에서도 지연 로딩이 가능하도록 하는 것이다.
가장 단순한 구현 방법은 클라이언트의 요청이 들어오자마자 서블릿 필터나 스프링 인터셉터에서 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션도 끝내는 것이다.
이것을 요청 당 트랜잭션(Transaction per request) 방식의 OSIV라 한다.

그림처럼 요청이 들어오자마자 서블릿 필터나 스프링 인터셉터에서 영속석 컨텍스트를 만들면서 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션과 영속성 컨텍스트를 함꼐 종료한다. 이렇게 하면 영속성 컨텍스트가 처음부터 끝까지 살아있으므로 조회한 엔티티도 영속 상태를 유지한다.
뷰에서도 지연 로딩을 할 수 있으므로 엔티티를 미리 초기화할 필요가 없다.
뷰에서도 지연 로딩을 할 수 있게 되면서 FACADE 계층 없이도 뷰에 독립적인 서비스 계층을 유지할 수 있다.

요청 당 트랜잭션 방식의 OSIV 문제점

요청 당 트랜잭션 방식의 OSIV가 가지는 문제점컨트롤러나 뷰 같은 프리젠테이션 계층이 엔티티를 변경할 수 있다는 점이다.

예를 들어 고객 이름을 출력해야 하는데 보안상의 이유로 고객 이름을 XXX로 변경해서 출력해야 한다고 가정하면

class MemberController {

  public String viewMember(Long id) {
    Member member = memberService.getMember(id);
    member.setName("XXX"); // 보안상의 이유로 고객 이름을 XXX로 변경했다.
    model.addAttribute("member", member);
    ...
  }

}

컨트롤러에서 고객 일므을 XXX로 변경해서 렌더링할 뷰에 넘겨주었다.
개발자의 의도는 단순히 뷰에 노출할 때만 고객이름을 XXX로 변경하고 싶은 것이지 실제 데이터베이스에 있는 고객 이름까지 변경하고 싶은 것은 아니었다.
요청 당 트랜잭션 방식의 OSIV는 뷰를 렌더링ㅇ한 후에 트랜잭션을 커밋한다.
트랜잭션을 커밋하면 당연히 영속성 컨텍스트를 플러시한다.

이때 영속성 컨텍스트의 변경 감지 기능이 작동해서 변경된 엔티티를 데이터베이스에 반영해버린다.
이처럼 심각한 문제가 발생한다.

프레젠테이션 계층에서 데이터를 변경했다고 데이터베이스까지 변경 내용이 반영되면 애플리케이션을 유지보수하기 상상히 힘들어진다.

이런 문제를 해결하려면 프레젠테이션 계층에서 엔티티를 수정하지 못하게 막으면 된다.

프레젠테이션 계층에서 엔티티를 수정하지 못하게 막는 방법들은 다음과 같다.

  • 엔티티를 읽기 전용 인터페이스로 제공
  • 엔티티 레핑
  • DTO만 반환

엔티티를 읽기 전용 인터페이스로 제공

이 방법은 엔티티를 직접 노출하는 대신 다음의 코드처럼 읽기 전용 메소드만 제공하는 인터페이스를 프리젠테이션 계층에 제공하는 방법이다.

interface MemberView {
  public String getName();
}

@Entity
class Member implements MemberView {
  ...
}

class MemberService {

  public MemberView getMember(id) {
    return memberRepository.findById(id);
  }

}

위의 코드는 회원 엔티티가 있지만 프레젠테이션 계층에는 Member 엔티티 대신에 회원 엔티티의 읽기 전용 메소드만 있는 MemberView 인터페이스를 제공한다.

프레젠테이션 계층은 읽기 전용 메서드만 있는 인터페이스를 사용하므로 엔티티를 수정할 수 없다.

엔티티 레핑

다음 코드와 같이 엔티티의 읽기 전용 메소드만 가지고 잇는 엔티티를 감싼 객체를 만들고 이것을 프리젠테이션 계층에 반환하는 방법이다.

class MemberWrapper {

  private Member member;

  public MemberWrapper(member) {
    this.member = member;
  }

  // 읽기 전용 메소드만 제공
  public String getName() {
    memeber.getName();
  }

}

member 엔티티를 감싸고 있는 MemberWrapper 객체를 만들었다.
이 객체는 member 엔티티의 읽기 메소드만 제공한다.

DTO만 반환

가장 전통적인 방법이다.

다음과 같이 프리젠테이션 계층에 엔티티 대신에 데이터만 전달하는 객체인 DTO(Data Transfer Object)를 생성해서 반환하는 것이다.

하지만 이 방법은 OSIV를 사용하는 장점을 살릴 수 없고 엔티티를 거의 복사한 듯한 DTO 클래스도 하나 더 만들어야 한다.

class MemberDTO {

  private String name;

  // Getter, Setter

}
...
MemberDTO memberDTO = new MemberDTO();
memberDTO.setName(member.getNAme());
return memberDTO;

Member 엔티티와 거의 비슷한 MemberDTO를 만들고 엔티티의 값을 여기에 채워서 반환한다.


지금까지 설명한 방법 모두 코드량이 상당히 증가한다는 단점이 있다.
차라리 프리젠테이션 계층에서 엔티티를 수정하면 안 된다고 개발자들끼리 합의하는 것이 더 실용적일 수 있다.
혹은 적절한 도구를 사용해서 프리젠테이션 계층에서 엔티티의 수정자(Setter)를 호출하는 코드를 잡아내는 것도 하나의 방법이 될 수 있지만, 쉽지 않다.

지금까지의 OSIV는 요청 당 트랜잭션 방식의 OSIV다.
지금까지 설명했던 문제점들로 인해 최근에는 거의 사용하지 않는다.
최근에는 이런 문제점을 어느정도 보완해서 비즈니스 계층에서만 트랜잭션을 유지하는 방식의 OSIV를 사용한다.
스프링 프레임워크가 제공하는 OSIV가 바로 이 방식을 사용하는 OSIV 다.

스프링 OSIV: 비즈니스 계층 트랜잭션

스프링 프레임워크가 제공하는 OSIV를 알아보자.

스프링 프레임워크가 제공하는 OSIV 라이브러리

스프링 프레임워크의 spring-orm.jar는 다양항 OSIV 클래스를 제공한다.
OSIV를 서블릿 필터에서 적용할지 스프링 인터셉터에 적용할지에 따라 원하는 클래스를 선택해서 사용하면 된다.

  • 하이버네이트 OSIV 서블릿 필터
    • org.springframework.orm.hibernate5.support.OpenSessionInViewFilter
  • 하이버네이트 OSIV 스프링 인터셉터
    • org.springframework.orm.hibernate5.support.OpenSessionInViewInterceptor
  • JPA OEIV 서블릿 필터
    • org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter
  • JPA OEIV 스프링 인터셉터
    • org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor

예를 들어 JPA를 사용하면서 서블릿 필터에 OSIV를 적용하려면 OpenEntityManagerInViewFilter를 서블릿 필터에 등록하면 되고 스프링 인터셉터에 OSIV를 적용하려면 OpenEntityManagerInViewInterceptor를 스프링 인터셉터에 등록하면 된다.

스프링 OSIV 분석

이전에 설명했던 요청 당 트랜잭션 방식의 OSIV는 프리젠테이션 계층에서 데이터를 변경할 수 있따는 문제가 있다.
스프링 프레임워크가 제공하는 OSIV는 이런 문제를 어느 정도 해결했다.

스프림 프레임워크가 제공하는 OSIV는 "비즈니스 계층에서 트랜잭션을 사용하는 OSIV"다.
이름 그대로 OSIV를 사용하기는 하지만 트랜잭션은 비즈니스 계층에서만 사용한다는 뜻이다.

동작 원리는 다음과 같다.
클라이언트의 요청이 들어오면 영속성 컨텍스트를 생성한다.
이때 트랜잭션은 시작하지 않는다.

서비스 계층에서 트랜잭션을 시작하면 앞에서 생성해둔 영속성 컨텍스트에 트랜잭션을 시작한다.
비즈니스 로직을 실행하고 서비스 계층이 끝나면 트랜잭션을 커밋하면서 영속성 컨텍스트를 플러시한다.
이때 트랜잭션만 종료하고 영속성 컨텍스트는 살려둔다.
이후 클라이언트의 요청이 끝날 때 영속성 컨텍스트를 종료한다.

이를 좀 더 자세히 보자.

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

트랜잭션 없이 읽기

영속성 컨텍스트를 통한 모든 변경은 트랜잭션 안에서 이루어져야 한다.
만약 트랜잭션 없이 엔티티를 변경하고 영속성 컨텍스트를 플러시하면 javax.persistence.TransactionRequiredException 예외가 발생한다.(부트 3.xx라면 jakarta)

엔티티를 변경하지 않고 단순히 조회만 할 때는 트랜잭션이 없어도 되는데 이것을 트랜잭션 없이 읽기(Nontransactional reads)라 한다.
프록시를 초기화하는 지연 로딩도 조회 기능이므로 트랜잭션 없이 읽기가 가능하다.

정리하면 다음과 같다.

  • 영속성 컨텍스트는 트랜잭션 범위 안에서 엔티티를 조회하고 수정할 수 있다.
  • 영속성 컨텍스트는 트랜잭션 범위 밖에서 엔티티를 조회만 할 수 있다. 이것을 **트랜잭션 없이 읽기(Nontransactional reads)라 한다.

스프링이 제공하는 OSIV를 사용하면 프레젠테이션 계층에서는 트랜잭션이 없으므로 엔티티를 수정할 수 없다.

이는 프레젠테이션 계층에서 엔티티를 수정할 수 있는 기존 OSIV의 단점을 보완했다.

트랜잭션 없이 읽기를 사용해서 프리젠테이션 계층애서 지연 로딩 기능을 사용할 수 있다.

정리해보면 스프링이 제공하는 비즈니스 계층 트랜잭션 OSIV는 다음과 같은 특징이 있다.

  • 영속성 컨텍스트를 프리젠테이션 계층까지 유지한다.
  • 프리젠테이션 계층에는 트랜잭션이 없으므로 엔티티를 수정할 수 없다.
  • 프리젠테이션 계층에는 트랜잭션이 없지만 트랜잭션 없이 읽기를 사용해서 지연 로딩을 할 수 있다.

다음 코드는 요청 당 트랜잭션 방식의 OSIV를 설면하면서 문제가 있던 코드다.
스프링이 제공하는 OSIV를 적용하면 어떻게 될까?

class MemberController {

  public String viewMember(Long id) {

    Member member = memberService.getMember(id);
    member.setName("XXX"); // 보안상의 이유로 고객 이름을 XXX로 변경
    model.addAttribute("member", member);
  }
}

위의 예제는 컨트롤러에서 회원 엔티티를 member.setName("XXX")로 변경했다.
프리젠테이션 계층이지만 아직 영속성 컨텍스트가 살아있다.
만약 영속성 컨텍스트를 플러시하면 변경 감지가 동작해서 데이터베이스에 해당 회원의 이름을 변경할 것이다.

하지만!!!
다행히도 여기서는 2가지 이유로 플러시가 동작하지 않는다.

  • 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하려면 영속성 컨텍스트를 플러시해야 한다. 하지만 트랜잭션을 사용하는 서비스 계층이 끝날 때 트랜잭션이 커밋되면서 이미 플러시 해버렸다. 그리고 스프링이 제공하는 OSIV 서블릿 필터나 OSIV 스프링 인터셉터는 요청이 끝나면 플러시를 호출하지 않고 em.close()영속성 컨텍스트만 종료해 버리므로 플러시가 일어나지 않는다.
  • 프리젠테이션 계층에서 em.flush()를 호출해서 강제로 플러시해도 트랜잭션 범위 밖이므로 데이터를 수정할 수 없다는 예외를 만난다.
    • 발생 예외 : javax.persistence.TransactionRequiredException : no transaction is in progress(부트 3.XX라면 jakarta, 그냥 스프링은 모르겠음..)

따라서 예제는 프리젠테이션 계층에서 영속 상태의 엔티티를 수정했지만, 수정 내용이 데이터베이스에는 반영되지 않는다.

스프링 OSIV 주의사항

스프링 OSIV를 사용하면 프리젠테이션 계층에서 엔티티를 수정해도 수정 내용을 데이터베이스에 반영하지 않는다.

그런데 여기에는 한 가지 예외가 있다.
프리젠테이션 계층에서 엔티티를 수정한 직후에 트랜잭션을 시작하는 서비스 계층을 호출하면 문제가 발생한다.

다음 코드를 통해 주의사항을 알아보자.

class MemberController {

  public String viewMember(Long id) {

    Member member = memberService.getMember(id);
    member.setName("XXX"); // 보안상의 이유로 고객 이름을 XXX로 변경했다.

    memberService.biz(); // 비즈니스 로직
    return "view";
  }
}

class MemberService {

  @Transactional
  public void biz() {
    // ... 비즈니스 로직 실행

  }

}

예시 코드를 위의 그림으로 분석해보자.

  1. 컨트롤러에서 회원 엔티티를 조회하고 이름을 member.setName("XXX")로 수정했다.
  2. biz() 메서드를 실행해서 트랜잭션이 있는 비즈니스 로직을 실행했다.
  3. 트랜잭션 AOP가 동작하면서 영속성 컨텍스트에 트랜잭션 시작한다. 그리고 biz() 메서드를 실행한다.
  4. biz() 메소드가 끝나면 트랜잭션 AOP는 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시한다. 이 때 변경 감지가 동작하면서 회원 엔티티의 수정 사항을 데이터베이스에 반영한다.

컨트롤러에서 엔티티를 수정하고 즉시 뷰를 호출한 것이 아니라 트랜잭션이 동작하는 비즈니스 로직을 호출했으므로 이런 문제가 발생했다.
문제를 해결하는 단순한 방법은 트랜잭션이 있는 비즈니스 로직을 모두 호출하고 나서 엔티티를 변경하면 된다.
컨트롤러는 다음과 같이 비즈니스 로직을 먼저 호출하고, 그 결과를 조회하는 순서로 실행하므로 이런 문제는 거의 발생하지 않는다.

memberService.biz(); // 비즈니스 로직 먼저 실행

Member member = memberService.getMember(id);
member.setName("XXX"); // 마지막에 엔티티를 수정한다.

스프링 OSIV는 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있으므로 이런 문제가 발생한다.

OSIV를 사용하지 않는 트랜잭션 범위의 영속성 컨텍스트 전략트랜잭션의 생명주기와 영속성 컨텍스트의 생명주기가 같으므로 이런 문제가 발생하지 않는다.

OSIV 정리

스프링 OSIV의 특징

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

스프링 OSIV의 단점

  • OSIV를 적용하면 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있다는 점을 주의해야 한다. 특히 트랜잭션 롤백 주의해야 한다.(다다음? 혹은 그 다음? 포스팅에서 볼 수 있을 거 같다.)
  • 앞서 스프링 OSIV 주의사항에서도 언급했듯이 프리젠테이션 계층에서 엔티티를 수정하고나서 비즈니스 로직을 수행하면 엔티티가 수정될 수 있다.
  • 프리젠테이션 계층에서 지연 로딩에 의한 SQL이 실행된다. 따라서 성능 튜닝시에 확인해야 할 부분이 넓다.

OSIV vs FACADE vs DTO

OSIV를 사옹하지 않는 대안은 FACADE 계층이나 그것을 조금 변형해서 사용하는 다양한 방법이 있는데 어떤 방법을 사용하든 결국 준영속 상태가 되기 전에 프록시를 초기화해야 한다.
다른 방법은 엔티티를 직접 노출하지 않고 엔티티와 것의 비슷한 DTO를 만들어서 반환하는 것이다. 어떤 방법을 사용하든 OSIV를 사용하는 것과 비교해서 지루한 코드를 많이 작성해야 한다.

OSIV를 사용하는 방법이 만능은 아니다.

OSIV를 사용하면 화면을 출력할 때 엔티티를 유지하면서 객체 그래프를 마음껏 탐색할 수 있다.
하지만 복잡한 화면을 구성할 때는 이 방법이 효과적이지 않은 경우가 많다.
예를 들어 복잡한 통계 화면은 엔티티로 조회하기보다는 처음부터 통계 데이터를 구상하기 위한 JPQL을 작성해서 DTO로 조회하는 것이 효과적이다.
그리고 수많은 테이블을 조인해서 보여주어야 하는 복잡한 관리자 화면도 객체 그래프로 표현하기 어려운 경우가 많다.
이 때도 엔티티를 직접 조회하기보다는 JPQL로 필요한 데이터들만 조회해서 DTO로 반환하는 것이 더 나을 수도 있다.

OSIV는 같은 JVM을 벗어난 원격 상황에서는 사용할 수 없다.

OSIV는 같은 JVM을 벗어난 원격 상황에서는 사용할 수 없다.
예를 들어 JSON이나 XML을 생성할 때는 지연 로딩을 사용할 수 있지만 원격지인 클라이언트에서 연관된 엔티티를 지연 로딩하는 것은 불가능하다.
클라이언트가 필요한 데이터를 모두 JSON으로 생성해서 반환해야 한다.

보통 Jackson이나 Gson 같은 라이브러리를 사용해서 객체를 JSON으로 변환하는데, 변환 대상 객체로 엔티티를 직접 노출하거나 또는 DTO를 사용해서 노출한다.

JOSN으로 생성한 API는 한 번 정의하면 수정하기 어려운 외부 API와 언제든지 수정할 수 있는 내부 API로 나눌 수 있다.

  • 외부 API : 외부에 노출한다. 한 번 정의하면 변경이 어렵다. 서버와 클라이언트를 동시에 수정하기 어렵다.
  • 내부 API : 외부에 노출하지 않는다. 언제든지 변경할 수 있다. 서버와 클라이언트를 동시에 수정할 수 있다.

엔티티는 생각보다 자주 변경된다. 엔티티를 JSON 변환 대상 객체로 사용하면 엔티티를 변경할 때 노출하는 JSON API도 함께 변경된다.
따라서 외부 API는 엔티티를 직접 노출하기보다는 엔티티를 변경해도 완충 역할을 할 수 있는 DTO로 변환해서 노출하는 것이 안전하다. 내부 API는 엔티티를 변경해도 클라이언트와 서버를 동시에 수정할 수 있어서 실용적인 관점에서 엔티티를 직접 노출하는 방법도 괜찮다고 생각한다.

2-2. 너무 엄격한 계층

다음 코드는 상품을 구매한 후에 구매 결과 엔티티틀 조회하려고 컨트롤러에서 리포지토리를 직접 접근한다.

class OrderController {

  @Autowired OrderService orderService;
  @Autowired OrderRepository orderRepository;

  public String orderRequest(Order order, Model model) {

    long Id = orderService.order(order); // 상품 구매

    // 리포지토리 직접 접근
    Order orderResult = orderRepsitory.findOne(id);
    model.addAttribute("order", orderResult);
    ...

  }
}

@Transactional
class OrderService {

  @Autowired OrderRepository orderRepository;

  public Long order(order) {
    ... 비즈니스 로직
    return orderRepository.save(order);
  }
}

class OrderRepository {

  @PersistenceContext EntityManager em;

  public Order findOne(Long id) {
    return em.find(Order.class; id);
  }
}

OSIV를 사용하기 전에은 프리젠테이션 계층에서 사용할 지연 로딩된 엔티티를 미리 초기화해야 했다.
초기화는 아직 영속성 컨텍스트가 살아있는 서비스 계층이나 FACADE 계층이 담당했다.
하지만 OSIV를 사용하면 영속성 컨텍스트가 프리젠테이션 계층까지 살아있으므로 미리 초기화할 필요가 없다.
따라서 단순한 엔티티 조회는 컨트롤러에서 리포지토리를 직접 호출해도 아무 상관이 없다.

3. 요약

이전 포스팅까지
프리젠테이션 계층에서 엔티티가 준영속 상태가 되므로 지연 로딩을 할 수 없었고, 그 문제를 어떻게 해결할지를 살펴보다가 그 해결방법까지 문제가 되었기에 나온 개념이 OSIV였다.

OSIV를 사용하면 앞에서 봤던 내용들을 해결할 수 있다.
기존 OSIV는 프레젠테이션 계층에서도 엔티티를 수정할 수 있다는 단점이 있었다.
스프링 프레임워크가 제공하는 OSIV는 기존 OSIV의 단점들을 해결해서 프레젠테이션 계층까지 엔티티를 수정하지 않는다.

다음 포스팅은 책 기준 14장이다.

728x90
Comments