• 크게 한걸음 - 로그인과 글쓰기 (Service, Controller)

    2022. 6. 12.

    by. 내이름은 킹햄찌

    현재까지 구현한 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:/";
        }
    
    }
    

    댓글