• 카테고리

    질문 & 답변
  • 세부 분야

    백엔드

  • 해결 여부

    미해결

@SessionAttribute로 가져온 데이터가 LazyInitializationException을 발생시킬 수 있나요.

24.05.08 15:58 작성 24.05.08 16:00 수정 조회수 56

0

배운것을 가지고 개인적으로 게시판을 만들어보고있습니다. 게시물 추가(POST)를 하는 과정에서 세션에서 회원 정보를 가져오기 위해 컨트롤러의 매개변수로 @SessionAttribute를 사용했습니다. 컨트롤러 코드는 아래와 같습니다. 그런데 이 부분에서부터 LazyInitializationException이 발생하게 됩니다.

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.myproject.jpaboard.domain.Member.posts: could not initialize proxy - no Session

 

 

 /**
     * 게시물 추가
     */
    @PostMapping("/new")
    public String addPost(@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember, @Validated PostForm postForm, BindingResult bindingResult) {

        log.info("postForm={}", postForm);

        if (bindingResult.hasErrors()) {
            log.info("errors={}", bindingResult);
            return "newPost";
        }
        postService.addPost(postForm, loginMember);

        return "redirect:/board/list";
    }

이렇게 해도, 저렇게 해도 해결이 되지 않다가 Member 엔티티의 post를 EAGER로 세팅해주는 방식으로 하니 정상적으로 작동했습니다. 하지만 영한님이 강의에서 알려주셨듯, 이는 좋지 않은 방법입니다. 나름 배운대로 다 적용했다고 생각했는데 어디서 문제가 생긴건지 궁금합니다. 아래에 엔티티와 서비스 코드를 추가합니다.

 

@Entity
@Getter @Setter
public class Post {

    @Id
    @GeneratedValue
    @Column(name = "post_id")
    private Long id;

    private String title;
    private String writer;

    @Lob
    @Column(columnDefinition="LONGTEXT")
    private String content;

    @Column(updatable = false)
    private LocalDateTime createdTime;
    private LocalDateTime lastModifiedTime;
    private Long viewCount;

    @ManyToOne(fetch = FetchType.LAZY) // 다쪽이 연관관계의 주인
    @JoinColumn(name = "member_id")
    private Member member;

    @Enumerated(EnumType.STRING)
    private CategoryType category;

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Comment> comments = new ArrayList<>();


    // 연관관계 편의 메서드
    public void setMember(Member member) {
        this.member = member;
        member.getPosts().add(this);
    }

    @Override
    public String toString() {
        return "Post{" +
                "id=" + id +
                ", title='" + title + '\'' +
                ", writer='" + writer + '\'' +
                ", content='" + content + '\'' +
                ", createdTime=" + createdTime +
                ", lastModifiedTime=" + lastModifiedTime +
                ", viewCount=" + viewCount +
                ", category=" + category +
                '}';
    }
}
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PUBLIC)
public class Member {

    @Id
    @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    private String email;
    private String password;

    private String name;

    @Embedded
    private Address address;

    @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Post> posts = new ArrayList<>();


    @Override
    public String toString() {
        return "Member{" +
                "address=" + address +
                ", name='" + name + '\'' +
                ", password='" + password + '\'' +
                ", email='" + email + '\'' +
                ", id=" + id +
                '}';
    }
}
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PostService {

    private final BoardRepository boardRepository;
    private final PostRepository postRepository;

    /**
     * 게시물 추가
     */
    @Transactional
    public Post addPost(PostForm postForm, Member loginMember) {

        Post post = new Post();
        postForm.setCreatedTime(LocalDateTime.now());

        // setter로 세팅
        post.setTitle(postForm.getTitle());
        post.setWriter(postForm.getWriter());
        post.setContent(postForm.getContent());
        post.setCreatedTime(postForm.getCreatedTime());
        post.setCategory(postForm.getCategory());
        post.setViewCount(0L);
        post.setWriter(loginMember.getName());

        post.setMember(loginMember);
        postRepository.save(post);

        return post;
    }

 

답변 2

·

답변을 작성해보세요.

0

안녕하세요. Jaesang Yoon님

생각해보면 Session에 저장되는 것은 이미 트랜잭션의 범위를 넘어섭니다.

Session이라는 것은 하나의 트랜잭션이 끝나도 계속 존재할 수 있기 때문이지요.

따라서 Session에 보관하는 것은 트랜잭션이 끝나서 영속성 컨텍스트에 존재하지 않기 때문에 준영속 상태가 됩니다.

이 경우 해결 방안은 2가지 입니다.

  1. 트랜잭션 안에서 필요한 초기화를 모두 끝내야 합니다. fetch join, 강제 초기화 등을 사용해야 합니다. 그리고나서 세션에 보관해야 합니다.

  2. 이 방법을 추천하는데, DTO를 만들어서 세션에 보관하는 방법입니다. 세션은 많은 데이터 보다는 꼭 필요한 데이터만 보관하는 것을 권장하기 때문에, 꼭 필요한 데이터만으로 DTO를 구성해서 사용하는 것이 좋은 방법입니다.

감사합니다.

0

인프런 AI 인턴님의 프로필

인프런 AI 인턴

2024.05.08

안녕하세요, 인프런 AI 인턴입니다. Jaesang Yoon님의 문제에 대해 설명드리겠습니다.

LazyInitializationException은 주로 지연 로딩(Lazy Loading) 설정된 연관 관계의 엔티티를 실제 사용하는 시점에 영속성 컨텍스트(Persistence Context)가 이미 종료되어 있어 초기화를 할 수 없는 경우에 발생하는 예외입니다. 예외 메시지에서 “could not initialize proxy - no Session”은 세션이 이미 종료된 상태에서 프록시 초기화를 시도했음을 나타냅니다.

이 문제는 @Transactional 어노테이션을 활용하여 해당 작업을 수행하는 메소드 전체를 한 영속성 컨텍스트 안에서 실행될 수 있도록 함으로써 해결할 수 있습니다. Jaesang Yoon님의 코드에서는 PostServiceaddPost 메소드에 @Transactional이 이미 적용되어 있으나, Session(영속성 컨텍스트)이 종료되기 전에 지연 로딩 대상인 Memberposts 컬렉션에 접근하는 것으로 보입니다.

이는 @SessionAttribute를 통해 컨트롤러에서 가져온 Member 객체가 이미 영속성 컨텍스트 밖에서 준영속 상태가 되어 있기 때문에 발생하는 일인 것으로 추정됩니다. 서비스 계층 내에서 Member 객체를 다시 로딩하여 이러한 문제를 회피할 수 있습니다. 즉, Member 엔티티의 식별자(ID)만 컨트롤러로 전달받고, 서비스 계층에서 해당 ID를 이용하여 다시 Member 엔티티를 조회(영속 상태로 만듦)함으로써 해결할 수 있습니다.

계층간 데이터를 전달할 때는 엔티티 자체를 전달하기보다는 DTO(Data Transfer Object)를 사용하거나 식별자(ID)만 전달하는 것이 좋습니다. 이는 LazyInitializationException을 방지할 뿐만 아니라 계층 간의 결합도를 낮추고 코드의 유지 보수성을 높이는 데에도 도움이 됩니다.

유사한 문제를 다룬 기존 답변에서는 하이버네이트 세션 관리, @Transactional 의 적용 및 지연 로딩 관련 조언 등이 제공되었습니다.
관련 문제로 “failed to lazily initialize a collection of role 에러 질문”이 유사한 상황을 다루고 있으며, 지연 로딩과 관련하여 유용한 정보를 제공합니다.

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏
추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

지식공유자님의 답변을 기다립니다.