ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JPA] OSIV(Open Session In View) 정리
    legacy/JPA 2024. 4. 7. 21:42

    OSIV(Open Session In View)란?

    JPA 하면 떠오르는 것이 영속성 컨텍스트입니다. 영속성 컨텍스트란 DB로부터 조회한 엔티티를 관리하는 저장소라고 생각하면 됩니다. 트랜잭션이 시작되어 조회된 엔티티는 영속성 컨텍스트(1차 캐시)에 저장하여 트랜잭션이 종료될 때까지 사용됩니다.

     

    스프링을 사용한다면 기본적으로 OSIV가 켜진 상태로 사용됩니다. OSIV는 영속성 컨텍스트와 관련이 있는데 이에 대해서 알아보겠습니다.

     

    OSIV = true

    application.yml, OSIV = true

    먼저 사용자의 요청이 들어오면 이것을 처리하기 위해서 트랜잭션이 시작됩니다. 각 트랜잭션마다 영속성 컨텍스트가 생성되고, 트랜잭션이 살아있는 동안 영속성 컨텍스트가 살아있다는 의미가 됩니다. 영속성 컨텍스트가 살아있다면 엔티티의 조회 및 수정을 진행할 수 있습니다.

     

    아래는 OSIV가 켜져 있는 상태의 다이어그램입니다. 스프링 인터셉터 또는 서블릿 필터에서 영속성 컨텍스트가 시작되어, 비즈니스 로직을 처리하고 Controller까지 유지됩니다. 영속성 컨텍스트가 유지된다는 의미는 위에서 말했듯이 엔티티의 조회 및 수정을 진행할 수 있다는 것입니다.

    사진1. OSIV = true

     

    code
    Member.class
    @Entity
    @Getter
    @Setter
    @NoArgsConstructor
    public class Member {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "member_id")
        private Long id;
    
        private String username;
    
        public Member(String username) {
            this.username = username;
        }
    }
    Team.class
    @Entity
    @Getter
    @Setter
    @NoArgsConstructor
    public class Team {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "team_id")
        private Long id;
    
        private String teamName;
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "member_id")
        private Member member;
    
        public Team(String teamName) {
            this.teamName = teamName;
        }
    }
    MemberController.class
    @RestController
    @Slf4j
    public class MemberController {
    
        @Autowired
        MemberService memberService;
    
        @PostMapping("/members/save")
        public Member saveMember(@RequestBody SaveMemberRequest request) {
            return memberService.saveMember(request);
        }
    }
    TeamController
    @RestController
    public class TeamController {
    
        @Autowired
        TeamService teamService;
        @Autowired
        MemberService memberService;
    
        @GetMapping("/teams/{teamId}")
        public ResponseTeam findTeam(@PathVariable Long teamId) {
            Team findTeam = teamService.findTeam(teamId);
            return new ResponseTeam(findTeam.getMember().getUsername(), findTeam.getTeamName());
        }
    
        @PostMapping("/teams")
        public Team saveTeam(@RequestBody SaveTeamRequest request) {
            return teamService.saveTeam(request);
        }
    }
    MemberService
    @Service
    public class MemberService {
    
        @Autowired
        MemberRepository memberRepository;
    
        @Transactional(readOnly = true)
        public Member findMember(Long id)  {
            return memberRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 사용자가 없습니다."));
        }
    
        @Transactional
        public Member saveMember(SaveMemberRequest request) {
            Member member = new Member(request.getUsername());
            return memberRepository.save(member);
        }
    
    }
    TeamService
    @Service
    public class TeamService {
    
        @Autowired
        TeamRepository teamRepository;
        @Autowired
        MemberRepository memberRepository;
    
        @Transactional
        public Team saveTeam(SaveTeamRequest request) {
            Team team = new Team(request.getTeamName());
            Member findMember = memberRepository.findById(request.getMemberId()).orElse(null);
            team.setMember(findMember);
            teamRepository.save(team);
            return team;
        }
    
        @Transactional(readOnly = true)
        public Team findTeam(Long teamId) {
            return teamRepository.findById(teamId).orElse(null);
        }
    }

     

    OSIV가 켜져있기 때문에 Service 레이어에서 트랜잭션이 종료되더라도 영속성 컨텍스트가 종료되지 않고 Controller에서도 유지됩니다. TeamController의 findTeam API를 보면 Service 레이어에서 조회된 엔티티를 dto로 변환하는 로직이 있습니다. 영속성 컨텍스트가 살아있기 때문에 Team과 지연로딩 관계에 있는 Member를 사용하여 dto로 변환할 수 있는 것입니다.

     

    OSIV = false

    application.yml, OSIV = false

     

    OSIV가 꺼져있을 때는 영속성 컨텍스트가 어떻게 관리될까요? 켜져 있을 때처럼 Controller까지 영속성 컨텍스트가 유지될까요? 결론부터 말하자면 트랜잭션이 시작과 종료되는 Service 레이어까지만 영속성 컨텍스트가 유지됩니다.

     

    사진2. OSIV = true

    사진 2를 보면 사진 1과 달리 영속성 컨텍스트 생존 범위가 줄어들었습니다. 이것이 어떠한 영향을 미치는지 알아보겠습니다.

     

    OSIV가 켜져있을 때와 코드는 동일하고, Team을 조회 후 dto로 변환하는 findItem API를 호출해보겠습니다. 에러 메시지를 보면 could not initialize proxy 오류가 발생했습니다. 이는 Controller까지 영속성 컨텍스트가 유지되지 않기 때문에 발생합니다.

     

    Team은 Member와 N:1 연관관계에 있습니다. 또한 Member를 지연로딩으로 조회하도록 설정하였습니다. 트랜잭션이 시작되고 Team을 조회하게 되면서 Member 엔티티가 Proxy 객체로 저장됩니다. 왜냐하면 지연로딩을 사용하기 때문에 Member에는 실제 객체가 아닌 Proxy 객체(가짜 객체)가 저장되게 됩니다. 가짜 객체인 Member를 포함하는 Team 엔티티를 Controller로 전달하게 됩니다. 따라서 Controller 입장에서는 영속성 컨텍스트가 더 이상 살아있지 않기 때문에 dto로 변환하는 과정에서 Member 엔티티에 접근할 수 없음에 따라 오류가 발생하게 되는 것입니다.

     

    결국 트랜잭션이 종료됨에 따라 영속성 컨텍스트가 같이 사라지게 됩니다. 이 문제를 해결하기 위해서는 트랜잭션이 종료되기 전에 지연로딩을 강제로 호출해주어야 합니다. 또는 지연로딩이 걸린 엔티티를 Controller가 아닌 트랜잭션 범위 내에서 dto를 변환하는 방법도 있습니다.

    지연로딩 객체 강제 호출

    결론

    OSIV = true

    • 트랜잭션이 종료되더라도 Controller까지 영속성 컨텍스트가 유지된다.
    • Controller에서 지연로딩이 걸린 엔티티를 접근할 수 있다.

    OSIV = false

    • 트랜잭션이 종료되면 영속성 컨텍스트가 사라진다.
    • Controller에서 지연로딩이 걸린 Proxy 객체에 접근할 수 없다.
      • 트랜잭션 범위 내에서 지연로딩이 걸린 Proxy 객체를 강제로 호출한다.
Designed by Tistory.