Spring Example: Community #2 Controller 개발 (Spring Security)

함형주·2023년 1월 4일
0

질문, 피드백 등 모든 댓글 환영합니다.

지난 프로젝트에선 Service -> Repository -> Controller 순으로 개발했었습니다.

하지만 프로젝트를 진행하며 웹 계층(Controller, View)의 구성에 따라 Service 계층에서 수정이 발생하는 경우가 많아 Controller 먼저 개발하고자 합니다.

또한 DTO의 위치는 Service 계층에 포함시켜 Controller는 단순히 HTTP 요청을 받고 핵심 기능은 Service에 위임하도록 개발했습니다.

개발 순서는 HomeController -> PostController -> CommentController -> MemberController -> ExceptionController 입니다.

실제 개발할 때에는 PostController -> PostService -> PostRepository 순으로 개발했지만 블로그는 패키지 계층으로 나누어 작성하겠습니다.

이 프로젝트에서 HTTP 요청 데이터를 검증(Validation)하는 것이 핵심이 아니고 이전 프로젝트에서 이미 구현을 했었기 때문에 해당 부분은 제외하고 개발하겠습니다.

Controller 개발에 앞서 Spring Security 설정을 해주겠습니다.

Spring Security

Spring Security는 인증, 인가를 담당하는 프레임워크입니다. 컨트롤러를 개발하며 UserDetails를 사용하기에 먼저 해당 부분을 개발하겠습니다.

SecurityFilterChain

Configurer

@Configuration
public class Configurer implements WebMvcConfigurer {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/", "/join", "/login", "/member/**", "/error", "/css/**", "/js/**").permitAll()
                .antMatchers("/post/**").authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/post")
                .and()
                .logout()
                .logoutSuccessUrl("/");

        return http.build();
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

SecurityFilterChain를 스프링 빈으로 등록하여 사용자 인증이 필요한 경로와 필요없는 경로 등을 설정할 수 있습니다.

AuthenticationProvider, UserDetailsService, UserDetails 의 경우 이전 프로젝트와 동일하기에 해당 블로그 참고 부탁드립니다.
또한 csrf() 옵션에 관해선 이전에 작성한 블로그 참고 부탁드립니다.

HomeController

HomeController

@Controller
public class HomeController {

    @GetMapping("/")
    public String home(@AuthenticationPrincipal UserDetailsImpl userDetails, Model model) {
        if (userDetails != null) model.addAttribute("member_id", userDetails.getMember().getId());

        return "home/home";
    }
}

회원 탈퇴 form 렌더링을 위해 로그인 사용자의 id를 Model에 담아주었습니다.

PostController

PostController

@Controller
@RequiredArgsConstructor
public class PostController {

    private final PostService postService;
    private final HeartService heartService;
    ...
    
}

GetMapping

public class PostController {

    /**
     * 게시글리스트 read
     */
    @GetMapping("/post")
    public String postList(Model model) {
        List<PostListDto> postList = postService.findList();
        model.addAttribute("postListDto", postList);

        return "post/list";
    }

    /**
     * 게시글 read
     */
    @GetMapping("/post/{post_id}")
    public String post(@PathVariable Long post_id, Model model,
                       @ModelAttribute("commentDto") CommentDto commentDto,
                       @AuthenticationPrincipal UserDetailsImpl userDetails) {
        PostDto postDto = postService.findPostAndComment(post_id);
        model.addAttribute("postDto", postDto);
        model.addAttribute("guest_id", userDetails.getMember().getId());

        return "post/post";
    }
}

post()에서 게시글을 작성한 사용자에게 수정, 삭제 버튼을 활성화할 수 있도록 로그인 사용자의 id를 model에 저장했습니다.
또한 댓글 form 렌더링을 위해 CommentDto를 model에 저장했습니다.

public class PostController {

    /**
     * 게시글 등록 폼
     */
    @GetMapping("/post/add")
    public String addPostForm(@ModelAttribute("postDto") WritePostDto writePostDto) {
        return "post/addform";
    }

    /**
     * 게시글 수정 폼
     */
    @GetMapping("/post/edit/{post_id}")
    public String editPostForm(@PathVariable Long post_id, Model model) {
        WritePostDto writePostDto = postService.findWritePostDto(post_id);
        model.addAttribute("postDto", writePostDto);

        return "post/editform";
    }
}

Post, Put DeleteMapping

public class PostController {

	/**
     * 게시글 등록
     */
    @PostMapping("/post")
    public String createPost(@ModelAttribute WritePostDto writePostDto, RedirectAttributes redirectAttributes,
                             @AuthenticationPrincipal UserDetailsImpl userDetails) {

        Long post_id = postService.createPost(writePostDto, userDetails.getMember().getId());
        redirectAttributes.addAttribute("post_id", post_id);

        return "redirect:/post/{post_id}";
    }

    /**
     * 게시글 수정
     */
    @PutMapping("/post/{post_id}")
    public String updatePost(@PathVariable Long post_id,
                             @ModelAttribute WritePostDto writePostDto,
                             RedirectAttributes redirectAttributes) {

        if (post_id != null) postService.updatePost(post_id, writePostDto);

        redirectAttributes.addAttribute("post_id", post_id);

        return "redirect:/post/{post_id}";
    }

    /**
     * 게시글 삭제
     */
    @DeleteMapping("/post/{post_id}")
    public String deletePost(@PathVariable Long post_id) {
        if (post_id != null) postService.delete(post_id);

        return "redirect:/post";
    }
}

핵심 비지니스 로직은 Service 계층에서 처리합니다.

이후 적절한 경로로 redirect 시킵니다.

좋아요 기능

좋아요를 등록하고 취소하는 기능은 게시글 안에서 이루어지고 하나의 메서드로 해결할 수 있기에 PostController에 포함했습니다.

public class PostController {

    /**
     * 좋아요 기능
     */
    @PostMapping("/post/{post_id}/heart")
    public String changeHeartStatus(@PathVariable Long post_id,
                       @AuthenticationPrincipal UserDetailsImpl userDetails,
                       RedirectAttributes redirectAttributes) {

        heartService.changeHeartStatus(post_id, userDetails.getMember().getId());

        redirectAttributes.addAttribute("post_id", post_id);
        return "redirect:/post/{post_id}";
    }
}

마찬가지로 Service 계층에서 비지니스 로직을 처리하고 redirect 시켰습니다.

CommentController

@Controller
@RequiredArgsConstructor
public class CommentController {

    private final CommentService commentService;
    
    /**
     * 댓글 수정 폼
     */
    @GetMapping("/post/{post_id}/comment/edit/{comment_id}")
    public String editForm(@PathVariable Long post_id, @PathVariable Long comment_id, Model model) {
        CommentDto commentDto = commentService.findCommentDto(comment_id);
        model.addAttribute("commentDto", commentDto);

        return "comment/editform";
    }
    
    /**
     * 댓글 등록
     */
    @PostMapping("/post/{post_id}/comment")
    public String addComment(@PathVariable Long post_id, RedirectAttributes redirectAttributes,
                             @ModelAttribute CommentDto commentDto,
                             @AuthenticationPrincipal UserDetailsImpl userDetails) {

        commentService.save(commentDto, post_id, userDetails.getMember().getId());
        redirectAttributes.addAttribute("post_id", post_id);

        return "redirect:/post/{post_id}";
    }

    /**
     * 댓글 수정
     */
    @PutMapping("/post/{post_id}/comment/{comment_id}")
    public String updateComment(@PathVariable Long post_id, @PathVariable Long comment_id,
                                @ModelAttribute CommentDto commentDto, RedirectAttributes redirectAttributes) {

        commentService.update(comment_id, commentDto);
        redirectAttributes.addAttribute("post_id", post_id);

        return "redirect:/post/{post_id}";
    }

    /**
     * 댓글 삭제
     */
    @DeleteMapping("/post/{post_id}/comment/{comment_id}")
    public String deleteComment(@PathVariable Long post_id, @PathVariable Long comment_id,
                                RedirectAttributes redirectAttributes) {
        commentService.delete(comment_id, post_id);
        redirectAttributes.addAttribute("post_id", post_id);

        return "redirect:/post/{post_id}";
    }
}

MemberController

@Controller
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

    /**
     * 회원가입 폼
     */
    @GetMapping("/join")
    public String joinForm(@ModelAttribute("memberDto") MemberJoinDto memberJoinDto) {
        return "member/join";
    }

    /**
     * 로그인 폼
     */
    @GetMapping("/login")
    public String loginForm(@ModelAttribute("loginDto") LoginDto loginDto,
                            @AuthenticationPrincipal UserDetails userDetails) {
        return (userDetails == null) ? "member/login" : "redirect:/";
    }

    /**
     * 회원가입
     */
    @PostMapping("/member")
    public String join(@ModelAttribute MemberJoinDto memberJoinDto) {
        memberService.save(memberJoinDto);
        return "redirect:/";
    }

    /**
     * 회원탈퇴
     */
    @DeleteMapping("/member/{member_id}")
    public String deleteMember(@PathVariable Long member_id) {
        memberService.delete(member_id);
        return "redirect:/";
    }

}

ExceptionController

ExceptionController

@ControllerAdvice(annotations = Controller.class)
public class ExceptionController {

    @ExceptionHandler
    public String ex(IllegalArgumentException ex, RedirectAttributes redirectAttributes) {
        redirectAttributes.addAttribute("error", "bad-request");
        return "redirect:/";
    }
}

서비스 계층에서 잘못된 접근의 경우 IllegalArgumentException을 발생시키는데 그 예외를 처리하는 컨트롤러입니다. 단순하게 쿼리 파라미터에 error를 표시하고 "/"로 리다이렉트 시켜주었습니다.

다음으로

Service 와 Repository를 개발합니다. DTO의 패키지가 Service 패키지에 위치하므로 다음 블로그에서 DTO에 관한 내용을 포함할 예정입니다.

github , 배포 URL (첫 접속 시 로딩이 걸릴 수 있습니다.)

profile
평범한 대학생의 공부 일기?

0개의 댓글