일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 백준#boj#12755
- 백준#BOJ#2615#오목
- 백준#boj#16932#모양만들기
- 백준#BOJ#8012#한동이는영업사원
- 백준#BOJ#1939#중량제한
- 백준#BOJ#14501#퇴사#브루트포스
- 백준#BOJ#12865#평범한배낭
- Today
- Total
순간을 성실히, 화려함보단 꾸준함을
[JPA] could not initialize proxy - no Session 발생 본문
안녕하세요.
오늘도 역시 토이 프로젝트를 진행하다가 발생한 문제점을 가져와서 해결했던 점을 포스팅 해보도록 하겠습니다.
먼저 구조부터 설명하자면, 현재 로그인 기능을 구현하기 위해서 HttpSession 에 회원의 정보들을 담을려고 합니다.
그리고 이 session에 담긴 '권한'으로 이 회원이 어디까지 '인가'될 수 있는지 판단해 주는 로직을 구현하려고 하는 중입니다.
맴버 Entity
package boomrabbit.logintest.mvc.domain;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.util.HashMap;
import java.util.Map;
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
@Column(name="ID")
private Long id;
/**
* 아이디
*/
private String memberId;
/**
* 비밀번호
*/
private String password;
/**
* Member 권한 (회원가입시 처음은 무조건 LEVEL1)
*/
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(name = "MEMBER_STATUS" ,
joinColumns = @JoinColumn(name = "ID"))
private Map<Long,MemberStatus> memberStatus = new HashMap<>();
/**
* 전화번호(휴대폰 번호)
*/
private String phoneNumber;
}
눈여겨 볼 부분은 Map<Long,MemberStatus> memberStatus = new HashMap<>();
입니다.
key, value = { 팀 id, 권한 } 으로 정의가 됩니다.
왜 이렇게 복잡하게 권한을 설정했냐면, 맴버 한명이 A라는 팀에는 일반 팀원일 수도 있고, B라는 팀에는 팀장역할을 수행할 수도 있기 때문입니다. 즉, 자신이 속한 팀에 따라 권한이 다를 수 있기 때문에 Map 자료구조를 사용해서 정의하였습니다.
이를 구현하기 위해서 값 타입 컬렉션 을 사용하였습니다.
MemberStatus
package boomrabbit.logintest.mvc.domain;
/**
* 사용자(level 1) - 로그인한 사람
* 팀원(level 2) - 사용자 중에 특정 팀에 가입한 사람
* 임원(level 3) - 특정 팀의 팀원으로서 팀원 강퇴, 초대권한이 있는 사람
* 팀장(level 4) - 특정 팀의 팀원으로서 임원의 권한 및 팀삭제 권한을 가진사람
*/
public enum MemberStatus {
USER, TEAM_MEMBER, MANAGER, LEADER
}
이렇게 4개의 권한이 존재합니다.
loginController
@PostMapping("/login")
public String loginV3(@Valid LoginForm loginForm, BindingResult result,
HttpServletRequest request, @RequestParam(defaultValue = "/") String redirectURL){
if(result.hasErrors()){
return "login/loginForm";
}
Member loginMember = loginService.login(loginForm.getLoginId(), loginForm.getPassword());
if(loginMember==null){
result.reject("loginFail","아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인처리
HttpSession session = request.getSession();
session.setAttribute(SessionConst.LOGIN_MEMBER,loginMember);
return "redirect:" + redirectURL;
}
회원이 로그인을 하였을때 session에 Member Entity를 담아주고 있습니다.
LoginInterceptor
public class LogInterceptor implements HandlerInterceptor {
public static final String LOG_ID = "logId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(!(handler instanceof HandlerMethod)){
return true;
}
HandlerMethod hm = (HandlerMethod) handler;
Auth auth = hm.getMethodAnnotation(Auth.class);
//1. @Auth 가 없는 경우는 인증이 별도로 필요없음(비회원도 접근 가능)
if(auth==null){
return true;
}
//2. @Auth 가 있는 경우에는 세션이 있는지 확인
String requestURI = request.getRequestURI();
HttpSession session = request.getSession();
log.info("인증 체크 인터셉터 실행 {}",requestURI);
if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null){
log.info("미인증 사용자 요청");
response.sendRedirect("/login?redirectURL=" + requestURI);
return false;
}
//3. 각 권한 분기처리(팀장,임원,팀원)
Member member = (Member)session.getAttribute(SessionConst.LOGIN_MEMBER);
MemberStatus status = member.getMemberStatus().get(1L); //임시로 회원가입을 하였을때 팀id 는 1로 고정하였습니다.
if(auth.role().equals(LEADER)){
log.info("@Auth : LEADER");
if(!LEADER.equals(status)){
log.info("Your don't access here");
response.sendRedirect("/notAccess");
return false;
}
}
else if(auth.role().equals(MANAGER)){
log.info("@Auth : MANAGER");
if(!LEADER.equals(status) && !MANAGER.equals(status)){
log.info("Your don't access here");
response.sendRedirect("/notAccess");
return false;
}
}
else if(auth.role().equals(TEAM_MEMBER)){
log.info("@Auth : TEAM_MEMBER");
if(!LEADER.equals(status) && !MANAGER.equals(status)&&!TEAM_MEMBER.equals(status)){
log.info("Your don't access here");
response.sendRedirect("/notAccess");
return false;
}
}
//log.info("REQUEST [{}][{}][{}]",uuid,requestURI,handler);
return true;
}
}
권한에 따라 접근할 수 있는 페이지를 Interceptor를 사용하여 구현해 주었습니다.
자 그럼 대략적인 구조파악은 끝났으니 본문으로 들어가 보겠습니다.
프로젝트를 실행시키고 난 후 동작한 순서는 이렇습니다.
1.회원가입
2.로그인
3.'권한' 페이지에 접근
그러나 3번에서 에러가 떠버립니다....바로 could not initialize proxy - no Session 라는 에러메세지입니다.
왜 이런 에러가 발생하는 것일까요????
바로 프록시 객체로 채워진 Member Entity를 타입캐스팅 하려고 시도했기 때문입니다.Member member = (Member)session.getAttribute(SessionConst.LOGIN_MEMBER);
이부분이죠.
현재 HttpSession 에는 트랜잭션 범위를 벗어난 상태이고 영속성 컨텍스트도 제거된 상태입니다. 이런 상태에서 Member Entity를 직접적으로 접근하여 사용하려고 하니 이런 에러가 발생하게 된 것 입니다.
그러면 이런 문제점을 어떻게 해야될까요???
처음에는 맘편하게 memberStatus 를 @ElementCollection(fetch = FetchType.EAGER) 로 설정해주었습니다. 그러면 지연로딩이 아닌 실제 객체들로 채워지게 될테니 말이죠. 그러나 이는 결코 좋은 방법이 아닙니다. 당장은 해결한 것 처럼 느껴질 수 있으나 후에 프로젝트가 더 커지고 복잡해 지면 주체할 수 없는 에러들을 유발할 수 있기 때문입니다.
그래서 다른 방법을 고안했습니다.바로 Entity를 직접 사용하는 것이 아니라 Data Transfer Object(DTO)를 사용하는 것 입니다.
@Data
public class SessionMemberDto {
private String memberId;
private Map<Long, MemberStatus> memberStatus = new HashMap<>();
}
이렇게 DTO를 선언해 줘서 이놈을 사용하는 것이죠. 즉, Member에서 값들을 빼와서 새로 만들어준 DTO 에 넣어주어서 이놈을 사용하면 됩니다.
LoginController
@PostMapping("/login")
public String loginV3(@Valid LoginForm loginForm, BindingResult result,
HttpServletRequest request, @RequestParam(defaultValue = "/") String redirectURL){
if(result.hasErrors()){
return "login/loginForm";
}
Member loginMember = loginService.login(loginForm.getLoginId(), loginForm.getPassword());
if(loginMember==null){
result.reject("loginFail","아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인처리
HttpSession session = request.getSession();
SessionMemberDto memberDto = transferLoginMember(loginMember);
session.setAttribute(SessionConst.LOGIN_MEMBER,memberDto);
return "redirect:" + redirectURL;
}
private SessionMemberDto transferLoginMember(Member loginMember) {
SessionMemberDto memberDto = new SessionMemberDto();
memberDto.setMemberId(loginMember.getMemberId());
Map<Long,MemberStatus> statusMap = new HashMap<>();
for(Map.Entry<Long,MemberStatus> u : loginMember.getMemberStatus().entrySet()){
Long key = u.getKey();
MemberStatus value = u.getValue();
statusMap.put(key,value);
}
memberDto.setMemberStatus(statusMap);
return memberDto;
}
그럼 자연스럽게 Interceptor 도 SessionMemberDto로 가져와서 사용할 수 있게 되는 것이죠.
LoginInterceptor
SessionMemberDto memberDto = (SessionMemberDto)session.getAttribute(SessionConst.LOGIN_MEMBER);
MemberStatus status = memberDto.getMemberStatus().get(1L);
오늘의 결론 : 절대로 데이터를 주고 받을때 엔티티(Entity)를 직접적으로 사용하지 말아라!!!! 후에 Entity 속성이 변경이 되었을때 구현해놓은 api들이 있다면 api 스택들이 바뀌어 버리는 아주 큰 문제점이 발생할 수 있다. 그러니 무조건 DTO를 만들어서 이놈을 사용하자!!!
끝!
'나의 개발 메모장' 카테고리의 다른 글
[Oracle] DBeaver 에서는 프로시저를 어떻게 실행시킬까? (0) | 2022.11.29 |
---|---|
[JPA] @JoinColumn 확실히 알고가기!!! (8) | 2022.01.12 |
[static inner class]non-static inner classes like this can only by instantiated using default (0) | 2022.01.04 |
@RequiredArgsConstructor 를 사용할때 주의점 (1) | 2021.12.24 |
[JPA]Should have [public, protected] no-arg constructor?? (0) | 2021.12.22 |