SpringBoot

SpringBoot 게시판 기능구현 1

신입 개발자 박상우의 개발자 도전기 2022. 11. 20. 20:51
일반적으로 게시판은 Login이 된 User를 기준으로 기능이 구현된다. 즉 Session에 저장된(로그인이 완료가 된) User의 값을 토대로 게시판 기능구현이 수행되어야 한다.

1. 게시판 DB 구조

  • 이번 프로젝트에서 사용할 게시판 기능은 총 3가지이다. 기본적 게시판 구조, 좋아요 기능 구현, 대댓글 등이 있다.
  1. Board Table
CREATE TABLE `study_bbs`.`boards`
(
    `id`   VARCHAR(10)  NOT NULL,
    `text` VARCHAR(100) NOT NULL,
    CONSTRAINT PRIMARY KEY (`id`)
);
  • id에는 free, notice, qna 3가지의 값이 들어가고(이때 id값들은 각각의 도메인 끝의 값의 역할을 한다.)
  • text에는 각각의 id값에 맞는 text값을 작성 해준다.

  2.  Article Table

CREATE TABLE `study_bbs`.`articles`
(
    `index`       INT UNSIGNED   NOT NULL AUTO_INCREMENT,
    `user_email`  VARCHAR(50)    NOT NULL,
    `board_id`    VARCHAR(10)    NOT NULL,
    `title`       VARCHAR(100)   NOT NULL,
    `content`     VARCHAR(10000) NOT NULL,
    `view`        INT UNSIGNED   NOT NULL DEFAULT 0,
    `written_on`  DATETIME       NOT NULL DEFAULT NOW(),
    `modified_on` DATETIME       NOT NULL DEFAULT NOW(),
    CONSTRAINT PRIMARY KEY (`index`),
    CONSTRAINT FOREIGN KEY (`user_email`) REFERENCES `study_member`.`users` (`email`)
        ON DELETE CASCADE
        ON UPDATE CASCADE,
    CONSTRAINT FOREIGN KEY (`board_id`) REFERENCES `study_bbs`.`boards` (`id`)
        ON DELETE CASCADE
        ON UPDATE CASCADE
);
  • article테이블은 게시글 테이블로 게시판 테이블에 의존적이다.
  • user_email, board_id는 중복이 되면 안되기 때문에 외래키로 지정을 해두고 index를 기본키로 지정해 둠으로써 다른 테이블과의 연결고리로 활용한다.(이때 board_id는 게시판 테이블에서 free, notice, qna를 가리킨다.)

  3. ArticleLike Table

CREATE TABLE `study_bbs`.`article_likes`
(
    `user_email`    VARCHAR(50)  NOT NULL,
    `article_index` INT UNSIGNED NOT NULL,
    `created_on`    DATETIME     NOT NULL DEFAULT NOW(),
    CONSTRAINT PRIMARY KEY (`user_email`, `article_index`),
    CONSTRAINT FOREIGN KEY (`user_email`) REFERENCES `study_member`.`users` (`email`)
        ON DELETE CASCADE
        ON UPDATE CASCADE,
    CONSTRAINT FOREIGN KEY (`article_index`) REFERENCES `study_bbs`.`articles` (`index`)
        ON DELETE CASCADE
        ON UPDATE CASCADE
);
  • 게시글 좋아요 데이터를 담당할 테이블이다.
  • 이때 중요시 해야할 부분은 기본키 설정을 할때 user_email과 article_index를 둘다 기본키로 같이 엮어 주어야 한다. 게시글의 index 번호와 user의 email값이 일치할 경우 좋아요를 했다는 기록을 남길 수있게 구조를 짰다. 

  4. Comment Table

CREATE TABLE `study_bbs`.`comments`
(
    `index`         INT UNSIGNED  NOT NULL AUTO_INCREMENT,
    `comment_index` INT UNSIGNED  NULL,
    `user_email`    VARCHAR(50)   NOT NULL,
    `article_index` INT UNSIGNED  NOT NULL,
    `content`       VARCHAR(1000) NOT NULL,
    `written_on`    DATETIME      NOT NULL DEFAULT NOW(),
    CONSTRAINT PRIMARY KEY (`index`),
    CONSTRAINT FOREIGN KEY (`comment_index`) REFERENCES `study_bbs`.`comments` (`index`)
        ON DELETE CASCADE
        ON UPDATE CASCADE,
    CONSTRAINT FOREIGN KEY (`user_email`) REFERENCES `study_member`.`users` (`email`)
        ON DELETE CASCADE
        ON UPDATE CASCADE,
    CONSTRAINT FOREIGN KEY (`article_index`) REFERENCES `study_bbs`.`articles` (`index`)
        ON DELETE CASCADE
        ON UPDATE CASCADE
);
  • 댓글 기능의 데이터를 저장할 Table이다.
  • 이와 동시에 대댓글도 구현할 수 있다. 대댓글의 경우 comment_index의 값을 다시 한번 참조하기 때문에 comment 테이블에서 index의 값을 기준으로 그 안에서 대댓글의 index가 생기게 될 수 있다. 이때 comment_index는 comment Table안에서 돌게 되는 구조가 된다.
  • 이때 주의해야 할 부분은 comment_index의 값이 null이 될 수있다는 점이다.(대댓글은 게시글, 댓글에 의존적이고 동시에 값이 null 즉, 대댓글을 작성하지 않을 수 있기 때문에 null로 표시해준다.)
  • user_email, article_index의 값은 게시판, 게시글, 댓글 3개가 서로 의존적임으로 외래키로 걸어둔다.

  5. CommentLike Table

CREATE TABLE `study_bbs`.`comment_likes`
(
    `user_email`    VARCHAR(50)  NOT NULL,
    `comment_index` INT UNSIGNED NOT NULL,
    `created_on`    DATETIME     NOT NULL DEFAULT NOW(),
    CONSTRAINT PRIMARY KEY (`user_email`, `comment_index`),
    CONSTRAINT FOREIGN KEY (`user_email`) REFERENCES `study_member`.`users` (`email`)
        ON DELETE CASCADE
        ON UPDATE CASCADE,
    CONSTRAINT FOREIGN KEY (`comment_index`) REFERENCES `study_bbs`.`comments` (`index`)
        ON DELETE CASCADE
        ON UPDATE CASCADE
)
  • 댓글 좋아요의 데이터를 저장할 테이블이다.
  • 여기서도 주의해야할 점은 user_email과 위 Comment Table에서 지정한 comment의 index값을 참조하는 comment_index 레코드를 작성해준다.(댓글의 index를 기준으로 데이터가 저장이된다.(대댓글 index와는 상관이 없다.))

2. 게시판 접속시 도메인에 대한 값 가져오기 기능구현

@RequestMapping(value = "write", method = RequestMethod.GET, produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView getWrite(@SessionAttribute(value = "user", required = false)UserEntity user,
                             @RequestParam(value = "bid", required = false)String bid) {
    
    ModelAndView modelAndView;
    if(user == null) {
        modelAndView = new ModelAndView("redirect:/member/login");
    } else {
        modelAndView = new ModelAndView("bbs/write");
        if(bid == null || this.bbsService.getBoard(bid)==null) {
            modelAndView.addObject("result", CommonResult.FAILURE.name());
        }else {
            modelAndView.addObject("text", this.bbsService.getBoard(bid));
            //위에서 받아온 값은 그냥 String이다. 보내줘야 하는것 boardEntity타입의 변수이다.
            modelAndView.addObject("result", CommonResult.SUCCESS.name());
        }
    }
    return modelAndView;
}

정말 헷갈리던 부분이 많은 Controller이다. 기존의 값을 어디서 어떻게 가져오는지 이 Controller를 작성함으로써 다시한번 이해하게 되는 순간이였다. 하나하나씩 위에서 부터 코드를 뜯어봐야겠다.

 

  • @SessionAttribut의 역할은 컨트롤러 밖(인터셉터 또는 필터 등)에서 만들어 준 세션 데이터에 접근할 때 사용한다. 즉 MemberController에서 만들었던 로그인이 인증된 UserEntity의 user값을 가져오기 위해 사용한것이다. 어찌 보면 당연하다. 로그인이 된 클라이언트만 게시판의 글을 작성할 수 있게 기능이 구현되어야 하기 때문이다.(@SessionAttributes는 작성이 된 컨트롤러 페이지 안에서만 작동하므로 지금 우리가 로그인 컨트롤러를 다른Controller페이지에 작성하였기 때문에 사용이 되어지지는 않는다.)
  • 다음은 문제의 변수로 가져온 "bid"값이다.
  • @RequestParam을 통해 변수로 가져온 bid는 현재 String타입의 값이 아무것도 들어있지 않은 빈 값이다.(흔히 우리가 문자열의 어떠한 값도 기입하지 않은 의미없는 문자열타입의 변수로 지정해준것과 같은 역할이다.)(MemberController에서는 보통 Entity타입으로 값을 가져왔기 때문에 많이 헷갈렸다.)
  • user가 null이다 라는 뜻은 userEntity의 값이 null일경우 즉, 로그인을 하지 않은 유저라는 뜻이므로 로그인 페이지로 돌려보낸다.
  • 로그인이 된 user에 대해서는 bbs/write 페이지로 이동이 되는데 이때 if조건문을 살펴보자.
  • bid == null 이라는 뜻은 말그대로 위에서 변수로 지정한 순수 bid를 뜻한다. 즉 bid가 null값이란 말은 아무 값도 없는 것을 뜻하므로 Failure를 반환해준다. 두번째 비교에서 this.bbsService.getBoard(bid)값이 null이라는 뜻은 뒤에서 다루겠지만 getBoard 메서드 쿼리문에서 whrer절에 id값을 비교해 주고있다. 즉 id값에 일치하지 않는 값이라는 뜻으로 위에서 만들었던 Board Table에서 id값에 어긋나는(free, notice, qna중) 값을 가지고 있다는 뜻이다(ex) bid = aaa, bid= abc등)
  • 그게 아니라 bid값도 null이아니고 getBoard()메서드를 통해 DB와 값을 비교했을시 id와 같은 값이 있다면 SUCCESS를 반환한다.
  • 이때 id값에 맞는 text값도 가지고 올 수있다. 어떻게 가져오게 된 것일까?(아래를 참고하자)
<select id="selectBoardById"
        resultType="dev.babsang.studymemberbbs.entities.bbs.BoardEntity">
    SELECT `id`   AS `id`,
           `text` AS `text`
    FROM `study_bbs`.`boards`
    WHERE BINARY `id` = #{id}
</select>
  • 위 글은 Board의 Id값을 가져오기 위해 짰던 쿼리문이다. 이때 SELECT부분을 유심히 보면 결론은 BoardEntity에서 id값과 text를 둘다 Select하는 것을 알 수가 있다. 즉 id값만 뽑는건 조건절일 뿐이지 selectBoardById를 통해 text의 값을 가져 올 수 있다는 뜻이 된다.
@Service(value = "dev.babsang.studymemberbbs.services.BbsService")
public class BbsService {
    private final IBbsMapper bbsMapper;

    @Autowired
    public BbsService(IBbsMapper bbsMapper) {
        this.bbsMapper = bbsMapper;
    }

    public BoardEntity getBoard(String id) {
        return this.bbsMapper.selectBoardById(id);
    }
    //컨트롤러 - 매퍼 - 연결고리
}
  • 위 로직은 Service를 구현한 것이다. 지금의 서비스 getBoard()메서드는 순수하게 Mapper와 Controller를 이어주는 역할을 한다.
  • 위의 로직을 통해 현재 getBoard메서드는 DB에 접속후 SELECT하여 id값이 일치하는지를 비교후 그 값을 컨트롤러에 전달해주는 역할을 한다.
  • 최종적으로 Controller에서 addObject(Html에서 th:text=${}의 값을 던져주기 위해서)를 통해 키값을 "text"로 지정해주고 위에서 지정한 의미없는 변수 bid를(클라이언트가 치고 들어오는 도메인의 아이디가 일치하냐)비교해주어야 하기 때문에 this.bbsService.getBoard(bid)를 통해 아이디가 일치하냐에서 SUCCESS를 반환해준다. 즉 일단 id는 정상적인 DB의 값이다 라는 뜻이된다.

th:text를 통한 DB값 넣기

  • h1 태그에 th:text를 통해 Controller에서 BoardEntity의 id값이 일치하냐에서 success가 발생한 키값을 가지고 온후 그 값의 getText()메서드를 통해 BoardEntity에서 text값을 가지고 오는 것이다. 이것이 위에 xml에서 작성한 SELECT에 id와 text를 두가지 다 사용했기에 가능한 일이다. 만약 SELECT 쿼리에서 id만 가지고 왔다면 Text의 관한 xml을 새로 짜주어야 하지만 가독성이 떨어지기도 하고 굳이 그럴 필요가 없게 된다. (목적에 맞게 최대한 공용성이 좋도록 쿼리를 짜는것이 좋은 습관인것 같다.)