영속성 컨텍스트에 대한 잘못된 이해로부터 발생한 오류!

이진우·2024년 6월 8일
0

스프링 학습

목록 보기
34/41

기존 코드 문제점

기존에 @AuthenticatinPrincipal 이나 SecurityContextHolder 를 계속 사용하는 것이 마음에 안들어서

아래와 같은 커스텀 어노테이션을 만들었다.

@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : #this.getMember()")
public @interface CurrentMember {

}

#this 는 인증 객체인 CustomUserDetails 를 의미하는데 #this.getMember() 을 통해 현재 로그인된 Member를 가져오려고 시도하였었다.

그에 따라 당연히

public Member getMember(){
      
        return member;
    }

    public CustomUserDetails(String username,String password,
            Collection<? extends GrantedAuthority> authorities,Member member){
        this(username, password, true, true, true, true, authorities,member);
    }
    

이런 메서드가 내가 만든 CustomUserDetails 안에 포함되어 있어야 했다.

아무튼

이를 통해

@GetMapping("/test/login")
 @Operation(summary = "로그인 테스트 확인", description = "로그인 되어 있는지 판단합니다")
 public ResponseEntity<String> checkLogin(@CurrentMember Member member) {
     return ResponseEntity.ok("로그인 성공을 축하드립니다 당신의 이름은 "+member.getName());
 }

이렇게 로그인된 멤버를 쉽게 꺼낼 수 있었다.

하지만 이에 대해 치명적인 문제점이 있었는데

Member 엔티티와 1대1로 연관관계가 설정되어 있는 Profile 에 접근하려 하면 could not initialize proxy 와 같은 에러가 발생하였다.

원인 파악

이러한 원인은 대부분 영속성 컨텍스트 밖에서 LAZY 로딩된 객체에 접근하려 할때 발생하게 된다.

이를 OSIV 와 함께 간단한 실험을 통해 더 정확히 알아보자!

OSIV 란 Open Session In View 의 약어로 이의 값이 true 로 설정된다면 JPA 의 영속성 컨텍스트가 View 영역까지 확장되는 것을 의미한다. 따라서 VIEW 영역인 Controller 단에서도 영속성 컨텍스트가 살아 있을 수 있다. 영속성 컨텍스트가 살아있으면 ?? LAZY 로딩된 객체에 접근이 가능하다.

Controller 단에서 조회

실험 코드 - AtController

     @GetMapping
      public void showProfile(){
      Member member = getMember();
       System.out.println(member.getName());
      System.out.println(member.getProfile().getYoutubeLink());
    //  profileService.showProfile(member);
  }
  
   private Member getMember(){
      Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
      return memberRepository.findMemberByLoginId(authentication.getName()).orElseThrow(()->new NotFoundException(ErrorCode.DUPLICATE_LOGIN_ID));
  }

OSIV 를 켰을 때

위 코드에 대한 결과로

이진우
Hibernate: 
 select
     p1_0.profile_id,
     p1_0.personal_link,
     p1_0.personal_statement,
     p1_0.twitter_link,
     p1_0.youtube_link 
 from
     profile p1_0 
 where
     p1_0.profile_id=?
null

우리가 예상한 결과를 받을 수 있다.

OSIV 를 껐을 때

이진우
2024-06-09T00:25:23.490+09:00 ERROR 284 --- [nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : 
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing fa
iled: org.hibernate.LazyInitializationException:
could not initialize proxy [com.gaduationproject.cre8.member.entity.Profile#1] - no Session] with root cause

위와 같은 오류가 발생한다.

OSIV 를 끄고 컨트롤러 단에서 조회를 시도하며 그에 대한 LAZY 로딩된 객체에 접근할 경우에는 오류가 발생한다

그 원인은 아직 @Transactional 이 시작되지 않은 상태이며 OSIV 를 끈 상태이므로 영속성 컨텍스트는 아직 시작되지 않음을 유추해 볼 수 있다.

영속성 컨텍스트가 시작되지 않았으므로 프록시 객체를 실 객체로 바꿔주는 영속성 컨텍스트를 사용할 수 없다.

@Transactional 안에서 조회?

실험 코드

 @GetMapping
  public void showProfile(){
      Member member = getMember();
      profileService.showProfile(member);
  }

  private Member getMember(){
      Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
      return memberRepository.findMemberByLoginId(authentication.getName()).orElseThrow(()->new NotFoundException(ErrorCode.DUPLICATE_LOGIN_ID));
  }
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ProfileService {
    public void showProfile(final Member member){

        System.out.println(member.getName());
        System.out.println(member.getProfile().getYoutubeLink());
        //member.changeMyName("jinu");
       // member.getProfile().changeProfile("www.youtube.com","www.personal",null,null);

    }

OSIV 를 킨 경우

당연히 조회가 성공할테니 이 경우는 패쓰~

OSIV 를 껐을 때

이렇게 바꿔보자.

이에 대한 실험 결과로

이진우
2024-06-09T00:36:12.065+09:00 ERROR 13712 --- [nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : 
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
[Request processing failed: org.hibernate.LazyInitializationException: 
could not initialize proxy [com.gaduationproject.cre8.member.entity.Profile#1] - no Session] with root cause

여전히 같은 오류가 발생한다. 이는 @Transactinal 을 메서드 위에 따로 붙여도 같은 오류가 발생한다.

이는 비단 조회 뿐만이 아니라 수정에도 영향을 끼친다. 조금 더 실험해 보자!

수정 - Member 의 칼럼 수정

실험코드

   @Transactional
    public void showProfile(final Member member){
        member.changeMyName("jinu");
    }

OSIV를 켰을 때

아무것도 수정되지 않는다.

OSIV 를 껐을 때

위 수정 코드에 대한 결과로 아무것도 수정되지 않는다.
당연히 @Transactional 이 끝날 때 변경감지로 수정을 생각하지만 말이다.

수정 - Member 의 연관관계 객체 수정

실험 코드

 @Transactional
    public void showProfile(final Member member){
    member.getProfile().changeProfile("www.youtube.com","www.personal",null,null);

    }

OSIV를 켰을 때

@Transactional 밖에 나가면 변경감지를 통해 수정되는 것을 볼 수 있다.

update
        profile 
    set
        personal_link=?,
        personal_statement=?,
        twitter_link=?,
        youtube_link=? 
    where
        profile_id=?

OSIV 를 껐을 때

org.hibernate.LazyInitializationException: could not initialize proxy [com.gaduationproject.cre8.member.entity.Profile#1] - no Session
	at 

기존에 우리가 봤던 오류를 다시 한번 볼 수 있다.

실험에 대한 결과 정리

결과를 다시 한번 정리해보자.

OSIV 를 true 로 한 경우 즉 영속성 컨텍스트의 범위를 VIew 영역까지 확장한 경우 컨트롤러 단에서 조회한 연관관계 객체에 대한 조회, 서비스 단의 @Transactional 안에서 조회 , 서비스단의 @Transactional 안에서 연관관계 객체에 대한 수정 이 모두 성공하지만, 컨트롤러에서 이미 조회한 객체(Member) 를 서비스단의 @Transactioanl 안에서 수정한다고 해도 그 값이 수정되지 않는다.

OSIV 를 false 로 한 경우 컨트롤러 단에서 조회한 연관관계 객체에 대한 조회 , 서비스 단의 @Transactional 안에서 조회, 서비스단의 @Transactional 안에서 연관관계 객체에 대한 수정이 모두 LazyInitializationException 이 발생한다. 컨틀롤러에서 이미 조회한 객체(Member) 를 서비스단의 @Transactioanl 안에서 수정한다고 해도 오류가 발생하지 않을 뿐 그 값이 수정되지는 않는다.

결과 분석

이 모든 것을 관통하는 것은 딱 한개 밖에 없다. 영속성 컨텍스트 밖에서 가져온 객체에 대해서 아무리 @Transactional 안 이라도 영속성 컨텍스트의 관리를 받지 않아 그와 LAZY 연관된 객체에 대해 조회, 수정 등이 불가하다.

분석 바탕으로 수정

목표는 로그인된 객체를 꺼내오는 작업을 Service 단에서 수행하는 것이다.

@CurrentMember 로 로그인된 Member를 바로 가져오는 것을 지금은 포기한다.

지금으로써는 방법이 없어보인다. ㅠ 꼭 생각해보자

대신 @CurrentMemberLoginId 로 수정하고 Service 단에서 조회하는 로직을 새로 짜도록 했다.

LikeThis

At Controller

 @GetMapping
    public ResponseEntity<BaseResponse<ProfileResponseDto>> showProfile(@CurrentMemberLoginId String loginId){

        return ResponseEntity.ok(BaseResponse.createSuccess(profileService.showMyProfile(loginId)));
    }

At Service

  public ProfileResponseDto showMyProfile(final String loginId){

        Member member = getLoginMember(loginId);

        Profile profile = member.getProfile();

        return ProfileResponseDto.builder().profile(profile).build();

    }
profile
기록을 통해 실력을 쌓아가자

0개의 댓글