크게 한걸음 - 로그인과 글쓰기 (Service, Controller)
현재까지 구현한 Member와 Post Entity에 대한 Service, Controller, Dto내용을 정리했다.
testCode가 눈에 띄게 부실해서, 관련 모듈들을 더 공부한 후에 보충해서 따로 정리를 할 예정이다.
Member
MemberRepository
package spring.postproject.Member.Repository;
import org.springframework.data.jpa.repository.JpaRepository;
import spring.postproject.Member.Entity.Member;
import java.util.Optional;
public interface MemberRepository extends JpaRepository<Member,Long> {
Optional<Member> findByUserIdAndPassword(String userId, String Password);
}
JpaRepository를 상속받아 구현하였고 로그인에 사용할 findByUserIdAndPassword 만 선언해주었습니다.
MemberService
package spring.postproject.Member.Service;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import spring.postproject.Excetion.ExceptionBoard;
import spring.postproject.Member.Entity.Member;
import spring.postproject.Member.Repository.MemberRepository;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MemberService {
private final MemberRepository memberRepository;
// 회원가입
@Transactional
public Member signUp(Member member){
return memberRepository.save(member);
}
//계정 탈퇴
@Transactional
public void withdraw (Member member, String password){
if (!member.getPassword().equals(password)){
throw ExceptionBoard.INVALID_PASSWORD.getException();
}
memberRepository.delete(member);
}
public Member findOne(Long id){
return memberRepository.findById(id).orElseThrow(ExceptionBoard.NOT_FOUND_MEMBER::getException);
}
}
MemberService에는 회원가입과, member 하나만 찾는 로직을 추가 했다
LoginService
public interface LoginService {
Member login(String id, String password);
}
LoginService 인터페이스를 이용하여 추후에 Login 방법이 바뀌게 될 경우 구현체만 바꿀 수있도록 하기 위해 처리했다
SessionLoginServiceImpl
package spring.postproject.Member.Service;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import spring.postproject.Excetion.ExceptionBoard;
import spring.postproject.Member.Entity.Member;
import spring.postproject.Member.Repository.MemberRepository;
@Service
@RequiredArgsConstructor
public class SessionLoginServiceImpl implements LoginService{
private final MemberRepository memberRepository;
@Override
public Member login(String id, String password) {
return memberRepository.findByUserIdAndPassword(id, password).orElseThrow(ExceptionBoard.NOT_FOUND_MEMBER::getException);
}
}
Session을 이용할 Service인데 막상 Session을 컨트롤 하지는 않는것 같아 수정이 필요하다.
MemberCreateDto
package spring.postproject.Member.dto;
import lombok.Data;
@Data
public class MemberCreateDto {
private String nickname;
private String userId;
private String password;
}
회원가입시 전달받을 Dto입니다. setter등 여러가지를 지원하고 있는 @Data를 이용했다.
MemberLoginDto
package spring.postproject.Member.dto;
import lombok.*;
@Data
public class MemberLoginDto {
private String userId;
private String password;
}
로그인시 전달받을 Dto입니다. setter등 여러가지를 지원하고 있는 @Data를 이용했다.
LoginController
package spring.postproject.Member.Controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import spring.postproject.Member.Entity.Member;
import spring.postproject.Member.Entity.MemberRoll;
import spring.postproject.Member.Service.MemberService;
import spring.postproject.Member.Service.SessionLoginServiceImpl;
import spring.postproject.Member.dto.MemberCreateDto;
import spring.postproject.Member.dto.MemberLoginDto;
import spring.postproject.Post.Entity.Post;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.List;
@Controller
@RequiredArgsConstructor
@Slf4j
public class LoginController {
private final SessionLoginServiceImpl loginService;
private final String login = "login";
private final MemberService memberService;
@GetMapping("/")
public String loginForm(@SessionAttribute(name = login,required = false)Member member, Model model){
if(member != null){
model.addAttribute("member",member);
List<Post> posts = member.getPostList();
model.addAttribute("post",posts);
return "/post/home";
}
model.addAttribute("memberLoginDto",new MemberLoginDto());
return "/member/loginForm";
}
@PostMapping("/")
public String login(MemberLoginDto memberLoginDto, HttpServletRequest request,
Model model){
if (result.hasErrors()) {
return "member/loginForm";
}
Member loginMember = loginService.login(memberLoginDto.getUserId(), memberLoginDto.getPassword());
if (loginMember == null) {
result.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "member/loginForm";
}
//로그인 성공 처리
//세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
HttpSession session = request.getSession();
//세션에 로그인 회원 정보 보관
session.setAttribute(login, loginMember.getId());
List<Post> posts = loginMember.getPostList();
model.addAttribute("post",posts);
return "/post/home";
}
@GetMapping("/member/new")
public String createMember(Model model){
model.addAttribute("memberCreateDto",new MemberCreateDto());
return "member/createMember";
}
@PostMapping("/member/new")
public String create(MemberCreateDto memberCreateDto){
Member member = Member.builder()
.userId(memberCreateDto.getUserId())
.password(memberCreateDto.getPassword())
.nickName(memberCreateDto.getNickname())
.build();
member.updateRole(MemberRoll.NORMAL);
memberService.signUp(member);
return "redirect:/";
}
@GetMapping("/member/logout")
public String logout(HttpServletRequest request) {
// getSession(false) 를 사용해야 함 (세션이 없더라도 새로 생성하면 안되기 때문)
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
return "redirect:/";
}
}
- 우선 로그인을 먼저 하도록 만들었기 때문에 “/” 리소스 Get요청에는 member/loginForm 리소스에 MemberLoginDto를 담아서 전달하도록 했고, Post요청시에는 MemberLoginDto에서 userId와 password만 db에서 확인하여 세션을 부여했다.
- @SessionAttribute 어노테이션을 이용하여 세션이 있을 경우에는 해당 SESSIONID의 Member로 home에 접근 하도록 했다. home의 경로가 현재는 post/home으로 되어 있는데 추후에 알맞은 네이밍과 경로로 수정할 예정이다.
- /member/new리소스의 Get 요청에는 로그인 페이지인 /member/createMember 리소스에 MemberCreateDto 담아서 전달고, Post 요청에는 id와 password로 Member 앤티티를 만들어 저장하고 로그인을 할 수 있게 로그인 페이지로 "redirect:/" 해주었다.
- logout에서는 클라이언트에서 보낸 세션을 삭제시킨 후 "redirect:/" 했다.
- 여기서 @Valid를 하나도 사용하지 않았는데 이유는 현재 앤티티에서 모두 처리할 수 있도록 만들었기 때문에 추가를 하지 않았는데 controller 단에서 처낼 수 있는 exception이 앤티티까지 밀고 들어온다는게 옳지 않은 방법인것 같은데 추후 리팩토링시 좋은 방향으로 반영할 예정이다.
- 세션 관련글은 여기를 참고
Post
PostRepository
package spring.postproject.Post.Repository;
import org.springframework.data.jpa.repository.JpaRepository;
import spring.postproject.Post.Entity.Post;
public interface PostRepository extends JpaRepository<Post,Long> {
}
PostRepostiroy도 마찬가지로 JpaRepository를 상속받아서 생성하였는데 따로 더 필요한 custom 쿼리는 없어서 추가하지 않았다.
PostService
package spring.postproject.Post.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import spring.postproject.Excetion.ExceptionBoard;
import spring.postproject.Member.Entity.Member;
import spring.postproject.Post.Entity.Post;
import spring.postproject.Post.PostDto.PostDto;
import spring.postproject.Post.Repository.PostRepository;
import java.util.List;
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class PostService {
private final PostRepository postRepository;
public Post create(Post post, Member member){
post.setMember(member);
return postRepository.save(post);
}
//더티체킹으로 업데이트 하도록 함
public Post update(Long id,PostDto postDto){
Post post = postRepository.findById(id).orElseThrow(ExceptionBoard.NOT_FOUND_POST::getException);
post.update(postDto);
return post;
}
public void delete(Long id){
Post post = postRepository.findById(id).orElseThrow(ExceptionBoard.NOT_FOUND_POST::getException);
postRepository.delete(post);
}
public Post findOne(Long id){ return postRepository.findById(id).orElseThrow(ExceptionBoard.NOT_FOUND_POST::getException).counting(); }
@Transactional(readOnly = true)
public List<Post> findAll(){
return postRepository.findAll();
}
}
create에서 post를 영속화하기전 member과 연관관계 매핑을 하도록 했다.
update에서는 주석을 달아놓은것처럼 더티체킹으로 update되도록 하기위해 post를 영속화 시킨 후 내용을 Entity단에서 update만 해주었다.
나머지는 어렵지 않은데 findOne에서 @Transactonal 옵션을 readOnly로 하지 않은 이유는 조회수 로직때문이다. readOnly를 할 경우에는 JPA에서 스냅샷을 찍지 않아 수정내용이 반영되지 않기때문에 count()로직이 실행되어도 수정이 반영되지 않는다. 현재 조회수 카운팅방법에는 문제가 있고 세션, 토큰등 다양한 방법이 있다는 것을 인지하고 있기 때문에 이 부분은 추후 업데이트 대상이 될 것 같다.
PostDto
package spring.postproject.Post.PostDto;
import lombok.Data;
import spring.postproject.Post.Entity.Post;
@Data
public class PostDto {
private String title;
private String content;
public void toDto(Post post) {
this.title = post.getTitle();
this.content = post.getContent();
}
}
post 수정시 전달받을 Dto입니다. 마찬가지로 편의를 위해 @Data 어노테이션 사용했다.
PostController
package spring.postproject.Post.Controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import spring.postproject.Member.Entity.Member;
import spring.postproject.Post.Entity.Post;
import spring.postproject.Post.PostDto.PostDto;
import spring.postproject.Post.Service.PostService;
@RequiredArgsConstructor
@Controller
@Slf4j
public class PostController {
private final PostService postService;
@GetMapping("/post/new")
public String createPost(Model model){
model.addAttribute("postDto",new PostDto());
return "/post/postCreate";
}
@PostMapping("/post/new")
public String create(PostDto postDto, @SessionAttribute(name = "login",required = false) Member member){
if(member == null){
log.info("member session is not valid");
return "redirect:/";
}
Post post = Post.builder()
.title(postDto.getTitle())
.content(postDto.getContent())
.build();
postService.create(post, member);
return "redirect:/";
}
@GetMapping("/post/{postId}/detail")
public String detail(@PathVariable("postId") Long postId, Model model){
model.addAttribute("post",postService.findOne(postId));
return "post/postDetail";
}
@GetMapping("/post/{postId}/update")
public String updateForm(@PathVariable("postId") Long postId, Model model){
Post post = postService.findOne(postId);
PostDto postDto = new PostDto();
postDto.toDto(post);
model.addAttribute("postDto",postDto);
model.addAttribute("postId",postId);
return "post/postUpdate";
}
@PostMapping("/post/{postId}/update")
public String update(@PathVariable("postId") Long postId,PostDto postDto, Model model){
Post post = postService.update(postId, postDto);
return "redirect:/";
}
@GetMapping("/post/{postId}/delete")
public String delete(@PathVariable("postId")Long postId){
postService.delete(postId);
return "redirect:/";
}
}