7. 게시판 프로젝트 - BaseTimeEntity, MemberDto
Member와 Board 엔티티가 공통적으로 갖고있는 생성일자와 수정일자가 중복되니 JPA Auditing 기능을 사용하여 관리하는 것이 편합니다.
공통적으로 도메인들이 가지고 있는 생성일자, 수정일자, 식별자 같은 필드 및 컬럼이 있습니다.
도메인마다 공통으로 존재한다는 의미는 결국 코드가 중복된다는 말과 일맥상통합니다.
데이터베이스에서 누가, 언제하였는지 기록을 잘 남겨놓아야 합니다. 그렇기 때문에 생성일, 수정일 컬럼은 대단히 중요한 데이터 입니다. 그래서 JPA에서는 Audit이라는 기능을 제공하고 있습니다. Audit은 감시하다, 감사하다라는 뜻으로 Spring Data JPA에서 시간에 대해서 자동으로 값을 넣어주는 기능입니다. 도메인을 영속성 컨텍스트에 저장하거나 조회를 수행한 후에 update를 하는 경우 매번 시간 데이터를 입력하여 주어야 하는데, audit을 이용하면 자동으로 시간을 매핑하여 데이터베이스의 테이블에 넣어주게 됩니다.
BaseTimeEntity
@MappedSuperclass
@Getter
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
@CreatedDate
private LocalDateTime createdDate; //생성일자
@LastModifiedDate
private LocalDateTime modifiedDate; //수정일자
}
- @MappedSuperclass : JPA Entity 클래스들이 해당 추상 클래스(BaseEntity)를 상속할 경우 createDate, modifiedDate를 컬럼으로 인식
- 매핑정보만 상속해주는 Superclass라는 의미이다.
- @MappedSuperclass가 선언되어 있는 클래스는 엔티티가 아니다. 당연히 테이블과 매핑도 안된다.
- 직접 생성해서 사용할 일이 없으므로 추상 클래스로 만드는 것을 권장한다.
- @EntityListeners(AuditingEntityListener.class) : 해당 클래스에 Auditing 기능을 포함
- @CreatedDate : Entity가 생성되어 저장될 때 시간이 자동 저장
- @LastModifiedDate : 조회한 Entity의 값을 변경할 때 시간이 자동 저장
BoardApplication
@SpringBootApplication
@EnableJpaAuditing
public class BoardApplication {
public static void main(String[] args) {
SpringApplication.run(BoardApplication.class, args);
}
}
- @EnableJpaAuditing : JPA Auditing을 활성화
Board
@Entity
@Getter @Setter
public class Board extends BaseTimeEntity {
...
Member
@Entity
@Getter @Setter
public class Member extends BaseTimeEntity {
...
이전에 로그인 된 회원을 세션에 저장해놓았다. 미쳐 생각하지 못한 점이 있었다.
엔티티를 세션에 보관하면 안 된다. 이를 어기고 엔티티를 그냥 세션에 보관하고 꺼내 사용하여
"failed to lazily initialize a collection of role: khm.board.domain.Member.boards,
could not initialize proxy - no Session" 오류가 났었다.
이를 해결하기 위해 MemberDto를 만들고 세션에 MemberDto를 저장하면 해결된다.
MemberDto
@Data
@NoArgsConstructor
public class MemberDto {
private Long id;
@NotBlank
private String loginId;
@NotBlank
private String password;
private String email;
@Builder
public MemberDto(String loginId, String password, String email) {
this.loginId = loginId;
this.password = password;
this.email = email;
}
public MemberDto(Member member) {
id = member.getId();
loginId = member.getLoginId();
password = member.getPassword();
email = member.getEmail();
}
public Member toEntity() {
return Member.builder()
.loginId(loginId)
.password(password)
.email(email).build();
}
}
- @NoArgsConstructor: 파라미터가 없는 기본 생성자를 생성한다.
- DTO는 기본 생성자가 무조건 있어야 하므로 선언해주어야 한다.
MemberController
@Controller
@RequestMapping("/member")
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
private final LoginService loginService;
@GetMapping("/login")
public String loginForm (@ModelAttribute MemberDto memberDto) {
return "member/loginMemberForm";
}
@PostMapping("/login")
public String login (@Validated @ModelAttribute MemberDto memberDto, BindingResult bindingResult,
HttpServletRequest request) {
if(bindingResult.hasErrors()) { //예외 발생
return "member/loginMemberForm";
}
//로그인 처리
Member loginMember = loginService.login(memberDto.getLoginId(), memberDto.getPassword());
if(loginMember==null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "member/loginMemberForm";
}
HttpSession session = request.getSession();
memberDto = new MemberDto(loginMember);
session.setAttribute(SessionConst.LOGIN_MEMBER, memberDto);
return "redirect:/"; //일단 홈으로 이동하게 설정
}
@GetMapping("/create")
public String createForm (@ModelAttribute MemberDto memberDto) {
return "member/createMemberForm";
}
@PostMapping("/create")
public String create (@Validated @ModelAttribute MemberDto memberDto, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
if(bindingResult.hasErrors()) {
return "member/createMemberForm";
}
Member newMember = memberService.createMember(memberDto.toEntity());
if(newMember==null) {//중복된 ID일 경우
bindingResult.reject("duplicateID","해당 아이디가 이미 존재합니다.");
return "member/createMemberForm";
}
redirectAttributes.addAttribute("memberId",newMember.getId());
return "redirect:/member/saved/{memberId}";
}
@GetMapping("/saved/{memberId}")
public String saved (Model model, @PathVariable Long memberId) {
Member findMember = memberService.findOne(memberId);
model.addAttribute("memberDto",new MemberDto(findMember));
return "member/savedMemberForm";
}
}
- 모델에 Member를 보내지않고 MemberDto를 보내도록 관련 코드 수정
createMemberForm, loginMemberForm, savedMemberForm
- 모델에서 값을 꺼낼 때 member가 아닌 memberDto에서 꺼내도록 수정