프로젝트/게시판 만들기로 배우는 Spring Data JPA

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

내이름은 킹햄찌 2022. 6. 12. 20:54

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

}