미니 프로젝트/게시판

[Java] 게시판 ⑦ Controller

웹개발자(진) 2024. 5. 28. 14:57
반응형
잡담

Controller는 MVC (Model-View-Controller) 패턴에서 클라이언트로부터 받은 요청에 대한 응답을 생성하는 역할을 합니다. 이전에도 언급했듯이 클라이언트로부터 HTTP 요청을 받고 해당요청을 적절한 메서드로 라우팅 합니다. 해당 메서드에서 데이터 검색, 조작, 생성등의 작업을 수행하면서 모델링 된 데이터를 적절한 view를 선택해서 응답합니다.


목차
BoardController
 1.list(GetMapping)
 2. register(Get/PostMapping)
 3. read, modify(GetMapping)
 4. modify(PostMapping)
 5. delete(PostMapping)

BoardController

스프링 기반의 웹 애플리케이션에서 게시판(Board) 관련 기능을 처리하는 컨트롤러 클래스입니다. 화면 구성을 어떻게 가져가야될지를 먼저 생각해야 하는데요

1. 메인 화면을 list 메서드를 통한 게시글을 출력하는 페이지가 필요합니다. (GetMapping)

2. register 메서드를 활용하여 게시글을 추가하는 페이지가 필요합니다. 게시글 추가 페이지를 통해 받아온 데이터를 DB에 저장합니다.(Get/PostMapping)

3. 게시글 중 선택한 하나의 게시글만 불러옵니다. (GetMapping)

4. 하나의 게시글을 modify / remove 메서드를 사용해서 수정/삭제합니다. (Get/PostMapping)

package org.zerock.b01.Controller;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.zerock.b01.dto.BoardDTO;
import org.zerock.b01.dto.PageRequestDTO;
import org.zerock.b01.dto.PageResponseDTO;
import org.zerock.b01.service.BoardService;

@Controller
@RequestMapping("/board")
@Log4j2
@RequiredArgsConstructor
public class BoardController {

    private final BoardService boardService;

    @GetMapping("/list")
    public void list(PageRequestDTO pageRequestDTO, Model model){
        PageResponseDTO<BoardDTO> responseDTO = boardService.list(pageRequestDTO);
        model.addAttribute("responseDTO", responseDTO);
    }

    @GetMapping("/register")
    public void registerGET(){

    }

    @PostMapping("/register")
    public String registerPOST(@Valid BoardDTO boardDTO, BindingResult bindingResult,
                               RedirectAttributes redirectAttributes){
        if(bindingResult.hasErrors()){
            redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
            return "redirect:/board/register";
        }

        Long bno = boardService.register(boardDTO);
        redirectAttributes.addFlashAttribute("result", bno);
        return "redirect:/board/list";
    }

    @GetMapping({"/read", "/modify"})
    public void read(Long bno, PageRequestDTO pageRequestDTO, Model model){
        BoardDTO boardDTO = boardService.readOne(bno);
        model.addAttribute("dto", boardDTO);
    }

    @PostMapping("/modify")
    public String modify(PageRequestDTO pageRequestDTO,
                         @Valid BoardDTO boardDTO,
                         BindingResult bindingResult,
                         RedirectAttributes redirectAttributes){

        if(bindingResult.hasErrors()){
            String link = pageRequestDTO.getLink();
            redirectAttributes.addFlashAttribute("errors",
                    bindingResult.getAllErrors());
            redirectAttributes.addAttribute("bno", boardDTO.getBno());
            return "redirect:/board/modify?"+link;
        }
        boardService.modify(boardDTO);
        redirectAttributes.addFlashAttribute("result", "modified");
        redirectAttributes.addAttribute("bno", boardDTO.getBno());
        return "redirect:/board/read";
    }

    @PostMapping("/delete")
    public String delete(Long bno, RedirectAttributes redirectAttributes){
        boardService.remove(bno);
        redirectAttributes.addFlashAttribute("result", "deleted");
        return "redirect:/board/list";
    }
}

 

코드 자체가 길어 보이지만 나눠서 보면 그리 어렵지 않습니다.

@RequestMapping을 통해 여러 개의 컨트롤러를 사용하여 애플리케이션을 모듈화 하고, 각 컨트롤러의 경로를 일관된 방식으로 그룹화할 수 있습니다. 이렇게 하면 코드의 유지보수와 가독성이 향상되고, 경로 충돌을 방지할 수 있습니다.

여기서는 "/board" 아래에 다른 여러 가지 경로 매핑됩니다.

@Controller
@RequestMapping("/board")
@Log4j2
@RequiredArgsConstructor
public class BoardController {

    private final BoardService boardService;
    
}

@RequiredArgsConstructor를 통해 생성자를 자동으로 생성해 줍니다. 사실 Lombok 라이브러리에서 제공되는 해당 어노테이션은 사용하지 않고도 코드를 작성할 수 있습니다.

아래의 코드는 @RequiredArgsConstructor를 사용하지 않고 생성자를 만드는 코드입니다.


@Controller
@RequestMapping("/board")
@Log4j2
public class BoardController {

    private final BoardService boardService;
    
    public BoardController(BoardService boardService){
    	this.boardService = boardService;
    }
}

final 변수를 가진 생성자를 직접 정의하는 것이 번거롭기도 하고 실수할 수 있기 때문에 해당 어노테이션을 사용합니다.

 


 

1. list(GetMapping)

아래의 호출은 게시판의 메인화면에 게시글에 대한 데이터들을 출력하는 메서드입니다.

@GetMapping("/list")
    public void list(PageRequestDTO pageRequestDTO, Model model){
        PageResponseDTO<BoardDTO> responseDTO = boardService.list(pageRequestDTO);
        model.addAttribute("responseDTO", responseDTO);
    }

기본적으로 어떤 화면을 출력하는 경우 GET호출을 사용합니다. 해당호출을 통해 DB에 저장되어 있는 데이터들을 가져와 "/list" URL에 출력하게 됩니다. @GetMapping("/list") 어노테이션이 붙은 메서드는 HTTP GET 요청이 "/list" 경로로 들어올 때 해당 메서드를 실행하며, 그 결과로 "list.html"을 출력한다는 뜻입니다.

위에서  @RequestMapping을 통해 그룹화했기 때문에 경로는 "/board/list"가 될 것입니다.

list는 매개변수로 PageRequestModel을 받습니다. 

PageRequestDTO는 페이지 요청과 관련된 정보를 담고 있습니다. 페이지 번호, 페이지 크기, 검색 타입(type), 검색 키워드(keyword) 등 페이지 요청에 필요한 모든 정보를 포함합니다.

앞서 만들어놨던 service인 list메서드를 활용해서 해당 페이지의 조건이 바뀌어도 그 페이지에 대한 출력을 보여줍니다.

HTTP 요청에서 Model은 서버에서 클라이언트로 데이터를 전달하는 데 사용되는 개념입니다. Spring MVC에서 Model은 컨트롤러에서 뷰로 데이터를 전달할 때 사용되며, 클라이언트가 화면에 표시할 데이터를 담고 있습니다. 

model.addAttribute("responseDTO", responseDTO);

일반적으로 Spring MVC에서 컨트롤러는 비즈니스 로직을 수행한 결과를 얻고, 이 결과를 Model에 담아서 뷰로 전달합니다. 뷰는 Model에 담긴 데이터를 사용하여 클라이언트에게 보여줍니다.

모델에 attribute를 통해 responseDTO라는 이름으로 responseDTO의 값들을 Model에 담아 뷰로 뿌려줍니다.

 


 

2. register(Get/PostMapping)

아래의 호출은 게시글 추가 페이지와 입력받은 게시글에 대한 데이터를 DB에 저장하는 메서드입니다.

 @GetMapping("/register")
    public void registerGET(){

    }

    @PostMapping("/register")
    public String registerPOST(@Valid BoardDTO boardDTO, BindingResult bindingResult,
                               RedirectAttributes redirectAttributes){
        if(bindingResult.hasErrors()){
            redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
            return "redirect:/board/register";
        }

        Long bno = boardService.register(boardDTO);
        redirectAttributes.addFlashAttribute("result", bno);
        return "redirect:/board/list";
    }

 

GetMapping

메인화면에서 '추가'버튼을 눌렀을 경우 정보를 입력할 수 있는 페이지를 호출해 주어야 합니다. GetMapping을 통해 해당 url호출을 해주는데 페이지만 열고 따로 부여해야 될 값이 없기 때문에 매개변수로 받아올 것이 없으며 redirect 해줘야 할 페이지도 없습니다.

PostMapping

PostMapping에서는 POST 요청을 처리합니다. /register 경로로 POST 요청이 들어오면 이 메서드가 호출됩니다. 해당 요청에서는 View단에서 입력받은 데이터를 DB에 Mapping을 통해 값을 전달하고 지정경로로 redirect 해주게 되는데 다른 코드와 조금 다른 점이 있습니다.

매개변수로 BoardDTO를 받을 때 앞에 어노테이션 @Valid가 붙었습니다.

@valid

스프링 프레임워크에서 객체의 유효성을 검사하는 데 사용됩니다. 이 애너테이션을 사용하면, 스프링은 객체에 정의된 유효성 검사 규칙에 따라 데이터를 검증하고, 결과를 BindingResult 객체에 저장합니다. 객체에 정의된 유효성 검사 규칙은 앞서 BoardDTO 클래스 코드를 작성했을 때 

@NotEmpty
@Size(min = 3, max = 10)
private String title;
@NotEmpty
private String content;
@NotEmpty
private String writer;

'@NotEmpty 데이터가 비어있으면 안 되고, @Size(min = 3, max = 10) 글자값이 3보다 작거나 10보다 커서는 안된다.'는 규칙을 정해놓았습니다. 해당규칙을 이행하지 않고 데이터가 넘어온 경우 @Valid 어노테이션을 통해 걸러지게 되고 결과를 BindingResult 객체에 저장합니다. BindingResult 객체를 통해 오류의 유무와 어떤 오류가 생성되었는지도 확인할 수 있습니다.

BindingResult의 주요 메서드

  • hasErrors(): 유효성 검사나 바인딩 과정에서 오류가 발생했는지 여부를 반환합니다.
  • hasFieldErrors(String field): 특정 필드에 오류가 있는지 여부를 반환합니다.
  • getFieldError(String field): 특정 필드의 첫 번째 오류를 반환합니다.
  • getAllErrors(): 발생한 모든 오류를 리스트로 반환합니다.
  • reject(String errorCode): 글로벌 오류를 추가합니다.
  • rejectValue(String field, String errorCode): 특정 필드에 대한 오류를 추가합니다.

 

if(bindingResult.hasErrors()){
            redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
            return "redirect:/board/register";
        }

만약 bindingResult에 @Valid 조건에 맞지 않아 에러가 있다면,  "errors"에 해당 에러들을 전부 담아서 view단으로 전송합니다. 이때 addFlashAttribute를 통해 해당페이지를 redirect 하게 되는데

addFlashAttribute는 일회성의 특징으로 URL에 붙지 않고 리프레시할 경우 데이터가 소멸됩니다. 에러가 계속해서 발생할 수 있는데 앞서 발생했던 에러가 있어도 한번 리프레시시키고 새로 전달한다고 이해하면 될 거 같습니다.

 

Long bno = boardService.register(boardDTO);
redirectAttributes.addFlashAttribute("result", bno);
return "redirect:/board/list";

에러가 발생하지 않았다면 입력받은 데이터를 DB에 저장하고 "result"에 bno값을 담아서 list(메인화면)으로 전송하여 alerm을 통해 완료됨을 표시하게 합니다.

 


 

3. read, modify(GetMapping)

@GetMapping({"/read", "/modify"})
    public void read(Long bno, PageRequestDTO pageRequestDTO, Model model){
        BoardDTO boardDTO = boardService.readOne(bno);
        model.addAttribute("dto", boardDTO);
    }

특정 게시글을 클릭하여 읽어 들이거나 수정할 때 호출하는 창은 모든 데이터가 입력되어 있는 화면으로 동일하게 출력될 것입니다. 그래서 두 html 같은 경우 같은 호출을 사용하여 화면을 출력할 수 있습니다. 여기서 PageRequestDTO는 왜 매개변수로 받아왔을까요?

내가 원하는 데이터를 출력하기 위해선 해당 값을 찾을 수 있는 Primarykey인 Long타입의 bno값만 있으면 됩니다. bno값은@GeneratedValue(strategy = GenerationType.IDENTITY)을 통해서 sequence로 동작하여 값이 들어올 때마다 1씩 증가하는 값을 가집니다. query문의 where절에 bno를 사용하면 해당 데이터들을 불러올 수 있는데요.

그렇다면 왜 pageRequestDTO를 매개변수로 받아왔을까요? 

그 이유는 PageRequestDTO에 만들어놓은 getLink() 메서드 때문입니다. read나 modify에서 수정 버튼을 누르거나 이전에 list 버튼을 통해 메인화면으로 돌아올 때 이전까지 내가 보던 그 창을 열어야 하지 계속 main으로 돌아가는 건 효율적으로 좋지 않고 보기에도 좋지 않습니다. 예를 들어서 내가 100page에 있는 내용을 보고 있다가 해당 게시글이 궁금해서 들어갔는데 다시 뒤로 갔더니 1번 page에 와있으면 불편할 것입니다.

따라서 getLink() 메서드를 사용하기 위해 PageRequestDTO를 model에 담아서 같이 넘깁니다.

 


 

4. modify(PostMapping)

이번엔 수정할 데이터 값을 View단에서 form을 통해 받아 처리하는 코드입니다.

    @PostMapping("/modify")
    public String modify(PageRequestDTO pageRequestDTO,
                         @Valid BoardDTO boardDTO,
                         BindingResult bindingResult,
                         RedirectAttributes redirectAttributes){

        if(bindingResult.hasErrors()){
            String link = pageRequestDTO.getLink();
            redirectAttributes.addFlashAttribute("errors",
                    bindingResult.getAllErrors());
            redirectAttributes.addAttribute("bno", boardDTO.getBno());
            return "redirect:/board/modify?"+link;
        }
        boardService.modify(boardDTO);
        redirectAttributes.addFlashAttribute("result", "modified");
        redirectAttributes.addAttribute("bno", boardDTO.getBno());
        return "redirect:/board/read";
    }

앞서 register에서도 설명했듯이 @Valid 어노테이션을 사용하면 유효성검사가 가능합니다. 에러가 있을 경우 이번에는 pageRequestDTO에 있는 getLink 메서드를 String 변수에 담았는데요. 이는 수정하는 곳에서 오류가 발생했을 경우 해당 게시글의 수정하는 곳으로 return 해주어야 하기 때문입니다. 따라서 link에 페이지 요청정보를 사용해서 해당링크로 redirect 합니다.

오류가 없이 잘 진행되었다면 DB에 수정한 값을 저장하고,  read.html을 호출하는데 addFlashAttribute와 addAttribute를 이용해서 나중에 알람이나 모달을 출력할 수 있도록 함께 전달합니다.


 

5. delete(PostMapping)

    @PostMapping("/delete")
    public String delete(Long bno, RedirectAttributes redirectAttributes){
        boardService.remove(bno);
        redirectAttributes.addFlashAttribute("result", "deleted");
        return "redirect:/board/list";
    }

이미 비슷한 코드들을 활용했기 때문에 따로 코드설명이 필요 없을 것 같습니다. Primary-key인 bno을 통해 해당 게시글의 데이터들을 삭제하면 됩니다.

 


 

글을 마치며

이번에는 게시글을 호출하는 Controller에 대해서 알아보았습니다. Controller 코딩을 잘해놓아야 웹 애플리케이션의 유지보수성과 확장성을 높이는 데 매우 중요합니다. Spring Boot와 같은 프레임워크를 사용할 때, Controller는 사용자 요청을 처리하고 응답을 생성하는 중추적인 역할을 합니다. 다음에는 마지막으로 호출을 통해 출력되는 html을 코딩해 보도록 하겠습니다. servlet에서는 JSP를 활용했는데 Springboot에서는 thymeleaf를 통해 구현해 보도록 하겠습니다. 감사합니다.

반응형