▸Spring MVC/기본 문법

Mybatis, MySQL 계층형 게시판 쿼리, 페이징, Date 처리

코데방 2020. 4. 11.
728x90
- Develop OS : Windows10 Ent, 64bit
- WEB/WAS Server : Tomcat v9.0
- DBMS : MySQL 5.7.29 for Linux (Docker)
- Language : JAVA 1.8 (JDK 1.8)
- Framwork : Spring 3.2.9 Release
- Build Tool : Maven 3.6.3
- ORM : Mybatis 3.2.8

 

계층형 게시판의 로직은 JSP/서블릿으로 작성할 때와 다른점이 없습니다. 다만 이번에는 오라클DB에서 MySQL로 바꾸면서 쿼리 방식이 좀 바뀌고, MyBatis로 DB 작업을 하면서 또 조금 바뀐 부분이 있어 정리합니다. 계층형 게시판에 대한 설명은 아래 링크를 참조하시면 됩니다.

 

[JSP & Servlet/- 기본 문법] - MVC 패턴의 게시판 만들기_계층형 게시판 로직 설계 [1/5]

 

 

 


 

 

먼저 기본적인 테이블 구성입니다. 게시물 고유번호(bId)를 위해 Sequence를 따로 만들어줬던 오라클 DB와는 달리 값이 자동 증가되도록 설정할 수 있습니다. 대신 해당 컬럼은 무조건 Primary Key로 지정해줘야합니다. 값을 insert할 때는 0을 넣어주면 자동으로 증가되고, 마지막 값보다 큰 숫자로 아무 숫자나 넣어주면 해당 숫자부터 다시 증가됩니다.

 

오라클의 Sequence보다 불편한 점이 있긴 한데 게시판마다 Sequence를 하나씩 만들어주지 않아도 되니 오히려 더 깔끔한 것 같습니다. (timestamp 타입에 문제가 생겨서 내용을 추가했습니다. 아래 내용을 추가로 확인하시기 바랍니다.)

CREATE TABLE board_info (
  bId int NOT NULL AUTO_INCREMENT,
  bGroup int NOT NULL,
  bOrder int NOT NULL,
  bIndent int NOT NULL,
  bTitle varchar(50) NOT NULL,
  bContent varchar(20000) NOT NULL,
  bUserId varchar(20) DEFAULT NULL,
  bUsername varchar(20) NOT NULL,
  bDate timestamp NOT NULL,
  bHit int NOT NULL,
  bGood int NOT NULL,
  bHate int NOT NULL,
  
  PRIMARY KEY (`bId`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8

 

 

테스트 하다가 auto_increment를 초기화해서 다시 1부터 시작하고 싶다면 아래 쿼리를 실행시켜주면 됩니다.

alter table board_info auto_increment=0;

 

 

그런데 테스트하다보니 문제를 하나 발견했습니다. 작성일을 넣어줄 timestamp 타입에 자동으로 default 값을 잡아주고 있습니다. 데이터 삽입이나 변경 시에 값을 넣어주지 않으면 자동으로 DB서버의 현재시간을 넣어주는 기능입니다.

 

 

 

문제는 일부 컬럼 값을 업데이트할 때도 시간이 자동으로 바뀐다는 것입니다. 게시물에 답글을 달면 하위 게시물들의 Order(순서) 정보를 +1씩 업데이트해줘야 하는데 이 때 작성 시간이 같이 변경돼서 문제가 됐습니다. timestamp 타입의 특성 상 현재 시간이 무조건 디폴트로 들어가는 것 같습니다. 어떻게 해봐도 자동으로 설정되네요.

 

그래서 datetime으로 타입을 바꿔주기로 했습니다. 이미 테이블을 만든 후라면 아래와 같이 변경해주면 됩니다.

alter table board_info change bDate bDate datetime not null;

 

새로 만들 때 쿼리입니다.

CREATE TABLE board_info (
  bId int NOT NULL AUTO_INCREMENT,
  bGroup int NOT NULL,
  bOrder int NOT NULL,
  bIndent int NOT NULL,
  bTitle varchar(50) NOT NULL,
  bContent varchar(20000) NOT NULL,
  bUserId varchar(20) DEFAULT NULL,
  bUsername varchar(20) NOT NULL,
  bDate datetime NOT NULL,
  bHit int NOT NULL,
  bGood int NOT NULL,
  bHate int NOT NULL,
  
  PRIMARY KEY (`bId`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8

 

 


 

 

테이블 생성 후 가장 헤맸던 부분은 bGourp 컬럼의 값을 업데이트하는 일이었습니다. 고유 값으로 자동 증가만 하면 되는 bId와는 달리, bGroup은 새 글의 경우 bId와 동일하게 입력되어야 하고 답글의 경우 부모 게시물의 bGroup과 동일해야 합니다.

 

오라클 DB에서는 이 문제는 Sequence를 사용해서 간단히 해결했지만 MySQL에서 Sequence를 구현하려면 많이 복잡해보이기 때문에 그냥 select를 통해 해결하기로 했습니다. 즉 새글의 경우 기존 bId의 max값을 select해서 1을 추가해주는 방식으로 구성하는거죠. DB를 좀 더 깊게 공부하지 않는 이상 이 방법이 최선인 것 같습니다.

 

먼저 새글쓰기에 대한 insert를 하기 전에 아래 쿼리로 현재 테이블의 bId의 max값을 가져옵니다. 저는 여러 게시판을 한번에 다루기 위해 테이블명도 변수로 받도록 했습니다. #{ }는 작은 따옴표가 붙는 문자열 형식으로 변환되기 때문에 숫자나 테이블명, 컬럼명 등은 ${ } 안에 넣어줘야 합니다.

	<!-- 게시물 Bid MAX값 select -->
	<select id="selectBoardBidMax" parameterType="String" resultType="Integer">
		select max(bId) as max from
		${value}
	</select>

 

 

현재 max값을 구한 뒤 1을 더해서 bGourp 값을 넣어줍니다. 이 때 한 가지 주의해야할 점은 게시물이 하나도 없을 경우 null 값이 리턴된다는 점입니다. 따라서 그냥 int형으로 resultType을 설정하면 안되고 Integer로 설정해줘야 합니다. null 값을 받기 위해 래퍼 클래스로 객체화하는 것이죠.

 

그리고 게시물이 하나도 없어 null이 리턴됐을 경우 새 글의 bId는 1이 들어갈 것이니 bGroup도 1을 넣어주고, 그 외의 경우에는 max + 1 값으로 넣어주도록 했습니다. 이정도 간단한 조건문은 삼항연산자가 제격입니다.

		// 새글의 bGroup 설정 (새글은 Bid와 동일하게 셋팅)
		Integer maxBid = mapper.selectBoardBidMax(boardName);

		// 글이 하나도 없을 경우 bGroup = 1, 있을 경우 bGroup = maxBid + 1
		int bGroup = maxBid != null ? maxBid + 1 : 1;
		boardVO.setbGroup(bGroup);

 

 


 

 

게시물을 작성하는 부분(insert)은 위의 두 가지만 알면 크게 어려울 것은 없을 것 같습니다. 이번엔 게시물 List를 뽑아오는 방법과 주의 사항입니다. 게시물 리스트를 뽑아오기 위해서는 요청된 페이지에 맞는 페이징 범위 산출과 해당 페이지에 맞는 게시물 리스트를 가져오는 작업이 필요합니다.

 

먼저 페이징 범위 산출입니다. 방법은 아래 링크글을 참조하시면 되고 쿼리 부분의 주의점만 남기겠습니다.

 

[JSP & Servlet/- 기본 문법] - MVC 패턴의 게시판 만들기_페이징 처리(오라클DB) [2/5]

 

 

MySQL에서 테이블의 데이터 갯수를 세는 count 쿼리는 unsigned int 형으로 반환됩니다. 갯수에는 음수가 없으니까요. unsigned int형은 자바에서 long 타입으로 반환되기 때문에 int 또는 Integer 타입으로 받아주면 예외가 발생하게 됩니다. 따라서 아래와 같이 long 또는 Long 타입으로 resultType을 설정해줍니다. 

 

DB에서 count만 뽑아오면 페이징 범위 산출은 쉽습니다. 위 링크를 참조하시면 됩니다. 저는 페이징이 int의 범위를 넘어갈 일이 없다고 생각하기 때문에 Long 타입으로 받아와 래퍼 클래스의 intValue() 메소드로 int형으로 변환한 뒤 사용했습니다. 뒤에서 다른 변수들과 연산을 해야하는데 long 타입으로 설정하면 다른 변수들도 모두 long 타입으로 맞춰줘야 하기 때문에 괜히 메모리 낭비의 느낌이 나서요. ㅎㅎ

	<!-- 게시물 갯수 Count(bId) select -->
	<select id="selectBoardCount" parameterType="String" resultType="Long">
		select count(bId) as count
		from ${value}
	</select>
	/**
	 * BASIC 게시판 페이징 범위 산출
	 * 
	 * @param boardName : 게시판 이름
	 * @param page      : 요청된 페이지
	 * @return : 게시물 0개일 경우 null, 그 외 페이징 범위
	 */
	public int[] basicBoardPaging(String boardName, int page) {

		// 총 게시물 갯수
		int totalCount = mapper.selectBoardCount(boardName).intValue();

		// 게시물 없으면 null
		if (totalCount == 0) {
			return null;
		}

		// 총 페이지 계산
		int totalPage = totalCount / CONTENTS_PER_PAGE;

		// 나머지가 있다면 1페이지 추가
		if (totalCount % CONTENTS_PER_PAGE > 0) {
			totalPage++;
		}

		// 요청된 페이지가 전체 페이지 범위에 있지 않다면(없는 페이지 번호라면)
		if(page > totalPage) {
			return null;
		}
		
		int startPage = ((page - 1) / PAGING_PER_PAGE) * PAGING_PER_PAGE + 1;
		int endPage = startPage + PAGING_PER_PAGE - 1;

		// 끝 페이지가 총 페이지보다 커지면 총 페이지가 끝 페이지가 됨
		if (endPage > totalPage) {
			endPage = totalPage;
		}

		return new int[] { startPage, endPage };
	}

 

 

 

두 번째는 요청된 페이지에 들어갈 게시물들을 select 하는 작업입니다. 이 부분은 3중 서브쿼리를 사용했던 오라클 DB와는 다르게 매우 간단하게 해결할 수 있습니다.

 

먼저 order by를 통해 정렬시킨 뒤, limit으로 몇 번째 게시물부터 몇 개를 가져올건지 설정해주면 됩니다. ROWNUM을 쓸 필요도 없습니다. 게시물의 순서는 0부터 시작합니다. 만약 한 페이지에 10개를 출력하는 리스트의 2페이지라면 11번째 게시물부터 가져오면 되는데, 11번째 게시물의 번호는 10이라는 거죠. 따라서 간단히 게시물의 시작 번호는 "(2(page) - 1) * 10"으로 설정해주면 됩니다. 

 

아래 쿼리는 "bGroup으로 내림차 정렬한 뒤 bOrder로 오름차순 정렬을 한번 더 하고, 0번째 데이터부터 10개를 가져온다" 라는 의미입니다.

 

- SELECT * FROM board_usual ORDER BY bGroup DESC, bOrder LIMIT 0, 10

- LIMIT의 게시물 시작점 : (요청된 페이지 - 1) * 한 페이지에서 보여줄 게시물 갯수

- LIMIT의 게시물 갯수 : 한 페이지에서 보여줄 게시물 갯수

 

실제 사용한 Mapper의 쿼리 설정입니다.

	<!-- 게시물 리스트 select -->
	<select id="selectBoardList" parameterType="HashMap" resultType="BasicBoardVO">
		select bId, bIndent, bTitle, bUserId,
		bUsername, bDate, bHit, bGood, bHate from ${boardName}
		order by bGroup desc, bOrder limit
		${startContentIndex},${CONTENTS_PER_PAGE}
	</select>

 

 

리스트를 가져오는 서비스 클래스의 코드 부분입니다. 쿼리에 대한 이해만 있으면 코드는 입맛대로 짜면 됩니다. 저 같은 경우는 제네릭이 지정되는 List<> 타입 등의 경우 타입 캐스팅을 하면 Warning이 발생하기 때문에 뷰에 전달할 때는 항상 일반 배열 형태로 바꿉니다.

	/* BASIC 게시판 페이징 상수 */
	private static final int CONTENTS_PER_PAGE = 10;
	private static final int PAGING_PER_PAGE = 10;


	/**
	 * BASIC 게시물 리스트 출력
	 * 
	 * @param boardName : 게시판 이름
	 * @param page      : 요청된 페이지
	 * @return : 게시물 없으면 null, 그 외 요청 페이지에 따른 VO 객체 배열
	 */
	public BasicBoardVO[] selectBoardList(String boardName, int page) {

		// 쿼리문을 위한 HashMap
		HashMap<String, Object> boardListSelectInfo = new HashMap<String, Object>();

		boardListSelectInfo.put("boardName", boardName);
		boardListSelectInfo.put("startContentIndex", (page - 1) * CONTENTS_PER_PAGE); // 첫 게시물 시작지점 (0부터 시작)
		boardListSelectInfo.put("CONTENTS_PER_PAGE", CONTENTS_PER_PAGE); // 한번에 가져올 갯수

		// 리스트 select
		List<BasicBoardVO> boardList = mapper.selectBoardList(boardListSelectInfo);

		if (boardList == null) {
			return null;
		}

		// 일반 배열로 변경해 리턴(제네릭의 타입 캐스팅 Warning 피하기 위함)
		return boardList.toArray(new BasicBoardVO[boardList.size()]);
	}

 

 

 


 

 

 

마지막으로 시간에 대한 처리입니다. DB에서 TimeStamp 또는 DateTime 타입은 자바 util의 Date 클래스와 매칭됩니다. 입력할 때는 간단히 new Date()를 해주면 생성되는 시간으로 입력이 되는데, 꺼내올 때는 뒤에 정보들이 더 붙어서 내용이 길어집니다. 형식도 게시판에 출력하기는 좀 별로입니다.

 

 

이 때 일반적으로 사용하는 방법은 SimpleDateFormat 클래스를 이용해 출력 포맷을 변경해주는 것입니다. 간단히 아래와 같이 작성해주면 원하는 형식으로 포맷이 바뀌는 것을 알 수 있습니다. 여러 가지 형태로 포맷을 변경할 수 있으니 더 필요하면 해당 클래스를 검색해보시면 됩니다.

 

		Date date = new Date();
		SimpleDateFormat sdf = new SimpleDateFormat("yy/MM/dd HH:mm:ss");
		System.out.println(sdf.format(date));

 

 

이 포맷 변경을 어떻게 처리할지는 여러 방법이 있겠습니다만, 저는 VO 객체에서 처리해줬습니다. getter 메소드를 조금 손봐서 Date 타입의 필드를 get할 때 원하는 포맷으로 변환된 String 문자열을 리턴받는 것이죠.

	private Date bDate;
    
	public String getbDate() {

		SimpleDateFormat sdf = new SimpleDateFormat("yy/MM/dd HH:mm:ss");
		return sdf.format(bDate);
	}

 

 


 

 

기본 JDBC, Oracle DB를 사용하다가 Mybtis, MySQL DB로 변경하면서 헷갈리는 부분에 대해 정리해봤습니다. 위에서 다룬 부분 외에는 그냥 단순 SELECT 및 CRUD 작업으로 해결이 가능할 것 같습니다. 

728x90

댓글

💲 추천 글