여러분은 모두 초기 세팅을 완료했을 것이다.
이번 편에서는 데이터베이스에 접근해 정보를 가져올 수 있는 Mapper 설정을 해보겠다.
Mybatis 설정
bulletin-board(프로젝트 이름)>src>main>java>wedatalab(사용자가 설정한 group 이름)>bulletinboard 에 BulletinBoardApplication 이라는 클래스를 살펴보자.
원래 이런 코드가 작성되어 있는데,
여기에 몇 줄의 코드를 추가하여 프로젝트에서 Mapper를 스캔할 수 있도록 해보자.
방법은 다음과 같다.
@SpringBootApplication 어노테이션 다음 줄에
@MapperScan 어노테이션을 추가하고, (basePackages="본인의 패키지 경로") 를 작성한다.
나의 경우에는 아래의 코드를 추가해줬다.
@MapperScan(basePackages="wedatalab.bulletinboard")
BulletinBoardApplication에서 @MapperScan 어노테이션을 할 때 Cannot resolve symbol 'MapperScan' 이라는 오류가 뜰 수 있다.
이때 @MapperScan 위에 마우스를 가만히 올려놓고 있으면 아래 회색 박스가 뜨는데, 지시대로 import class 를 해주면 된다. 단축키는 alt + shift + enter.
import org.mybatis.spring.annotation.MapperScan;
BulletinBoardApplication 클래스 코드 맨 윗줄에 위와 같은 코드가 생성된다.
Board.class 생성
wedatalab>bulletinboard(본인 프로젝트 이름) 폴더에 domain이라는 폴더를 만든다. ((마우스 우클릭 - new - package)
domain 에 Board 라는 클래스를 만든다. (마우스 우클릭 - new - java class)
Board 의 코드는 다음과 같다.
@Getter 아래부터 붙여 넣으면 되는데 Getter, Setter, NoArgsConstructor, AllArgsConstructor, LocalDateTime 다섯개는 import 해준다.
package wedatalab.bulletinboard.domain;
import lombok.AllArgsConstructor; // 아래 AllArgsConstructor 어노테이션을 하면 자동 생성됨
import lombok.Getter; // 아래 Getter 어노테이션을 하면 자동 생성됨
import lombok.NoArgsConstructor; // 아래 NoArgsConstructor 어노테이션을 하면 자동 생성됨
import lombok.Setter; // 아래 Setter 어노테이션을 하면 자동 생성됨
import java.time.LocalDateTime; // 아래 변수 선언하면 자동 생성됨
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Board {
private Long boardId;
private String title;
private String content;
private String name;
private LocalDateTime createDate;
private int read;
private Long memberId;
}
MapperInterface 생성
MapperInterface는 위에 Mybatis 설정 챕터에서 BulletinBoardApplication 에 추가해줬던 @MapperScan 의 대상임을 명시하는 것이다.
MapperScan 의 대상이라는 것은 @Repository 어노테이션을 통해 해당 인터페이스가 저장소라는 것을 명시할 수 있다.
MapperInterface를 생성하는 방법은 다음과 같다.
wedatalab>bulletinboard(본인 프로젝트 이름) 폴더에 mapper 라는 폴더를 생성한다. (new - package)
그 폴더에 BoardMapper라는 (Mapper) Interface 를 생성한다. (new - java class - interface)
<인터페이스를 생성하는 방법>
mapper 폴더 위에 마우스 우클릭 - new - java class
팝업창이 뜨면 빈칸에 BoardMapper 입력하고 class 가 아닌 interface를 선택해 enter
아래의 코드를 입력하자.
@Repository
public interface BoardMapper {
int boardCount(); // 곧 생성할 BoardMapper.xml 첫번째 sql 문의 id 와 같음.
List<Board> getList();
}
참고로 뒤에 ( ) 가 오면 메소드라고 하는데, 따라서 위 코드에서 int boardCount(); 또한 메소드라고 할 수 있다.
그런데 앞에 자료형은 int 라서, 처음에 정수형이 어떻게 메소드일 수 있지? 라고 헷갈렸다.
만약 int boardCount; 라고만 했으면 정수형 자료 선언일 텐데 뒤에 괄호가 붙었으니까 그냥 정수형 변수 선언과는 다르다고 볼 수 있다.
결론적으로, int boardCount(); 는 괄호 안에 추가적으로 선언을 해주었을 때 return 값이 정수형이 나오도록 한다는 의미이다.
이 메소드는 주석에서도 언급했다시피 곧 생성할 BoardMapper.xml 의 메소드 id 와 같도록 한다.
이렇게 Mapper Interface 생성을 완료했다.
이 인터페이스로 곧 생성할 Mapper.xml 를 통해 sql 명령어로 데이터베이스에 접근할 수 있게 된다.
그럼 Mapper.xml 를 생성해보자.
BoardMapper.xml 생성
전 단계에서 MapperInterface 를 생성한 것과 동일한 경로로 폴더를 생성한다.
그리고 그 폴더에 BoardMapper.xml 을 생성한다.
나의 경우 다음과 같은 과정을 거쳤다.
resources>wedatalab(본인이 설정한 group 이름)>bulletinboard(본인 프로젝트 이름) 폴더에 mapper 폴더 생성 (new - package)
그 폴더에 BoardMapper.xml 생성 (new - file)
BoardMapper.xml 생성 후 Valid XML document must have a root tag 라는 오류가 뜬다. 이는 xml 형식의 코드를 작성하면 사라진다.
그러니 바로 아래의 코드를 주석을 삭제한 뒤 입력해주자.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="wedatalab.bulletinboard.mapper.BoardMapper">
<select id="boardCount" resultType="int"> // id 와 BoardMapper.interface 의 메소드가 같음
SELECT count(boardId) AS cnt FROM tbl_board // 게시글 수 반환
</select>
<select id="getList" resultType="wedatalab.bulletinboard.domain.Board">
SELECT
*
FROM tbl_board // 게시글 리스트 반환
</select>
</mapper>
sql 문에 대해 설명을 덧붙이자면,
SELECT count(boardId) AS cnt FROM tbl_board;
는 tbl_board 테이블의 행 개수를 정수 타입으로 반환함을 의미한다.
앞서 BoardMapper 라는 인터페이스에서 다음과 같은 메소드를 생성했던 것이 기억나는가?
int boardCount();
이 메소드 (boardCount) 와 첫번째 sql 문 id (boardCount) 가 같다.
여기서 우리는 중요한 사실 하나를 알 수 있다.
인터페이스의 메소드 명과 xml 의 id 명이 같도록 작성해야 한다는 것이다.
참고)
BoardMapper.xml을 작성하다가 아래와 같은 오류가 뜰 수 있다.
Typo: In word ' ~ '
IntelliJ는 영문 스펠링을 인식해 아래에 맞춤법을 교정하는 기능을 기본으로 제공하는데, 내가 설정한 group과 프로젝트 이름에 오타가 있다는 뜻이다. 사용자 지정이기 때문에 오타가 아니니까 이 오류를 없애고 싶었다.
찾아보니 없애는 방법도 있긴 한데 나는 설정을 못 찾겠어서 그냥 신경쓰지 않기로 했다.
서비스
BoardService.class 생성
이제 서비스 단을 만들어 볼 차례다.
java>wedatalab(본인이 설정한 group 이름)>bulletinboard(본인 프로젝트 이름) 폴더에 service 폴더 생성 (new - package)
그 폴더에 BoardService 클래스 생성 (new - java class)
@Service // 서비스 역할을 하는 것임을 명시
@RequiredArgsConstructor // Mapper 생성자를 사용할 수 있게 함
@Transactional(readOnly = true)
public class BoardService {
private final BoardMapper boardMapper;
public int boardCount() {
return boardMapper.boardCount(); // 게시글 수 반환
}
public List<Board> boardList() {
return boardMapper.getList(); // 게시글 리스트 반환
}
}
자, 이제 mapper 와 service 설정까지 완료했으니 제대로 쿼리가 동작하는지 확인해보자!
BoardController.class 수정
1편에서 BoardController 클래스를 만들었는데, 아래 주석에 '추가'라고 쓰여 있는 코드를 추가한다.
package wedatalab.bulletinboard.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import wedatalab.bulletinboard.service.BoardService;
@Controller
@RequestMapping("/board/**")
@RequiredArgsConstructor // 추가1
public class BoardController {
private final BoardService service; //추가2
@GetMapping("/hello")
public String Hello() {
return "/boards/hello";
}
// 추가3
@GetMapping("/test")
public String test(Model model) {
model.addAttribute("cnt", service.boardCount());
model.addAttribute("test", service.boardList());
return "/boards/hello";
}
}
추가된 코드를 살펴보면 Model 객체를 사용했다.
스프링이 "cnt" 라는 Model 객체에 service.boardCount() 를 통해 받은 데이터를 넣어 뷰 (hello.html) 쪽으로 넘겨준다는 뜻이다.
그럼 뷰 (hello.html) 에서는 ${ } 를 통해 값을 가져와 출력하게 된다.
Model 객체에 대한 자세한 내용은 아래 글을 참고하자.
https://blog.naver.com/jjb0010/222615503560
테스트
위에서 BoardController 클래스에 테스트를 위한 GetMapping 을 해줬다.
이에 맞게 hello.html 을 다음과 같이 수정하자.
html 파일이므로 주석은 빼고 입력하면 된다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>테스트 페이지</title>
</head>
<body>
<h1>테스트페이지!</h1>
<h1>[[${cnt}]]</h1> // cnt : 게시글 수
<h1>[[${test[0].title}]]</h1> // test : 게시글 리스트, test[0].title : 첫번째 게시글 제목
</body>
</html>
두번째 줄에 게시글 수, 세번째 줄에 첫번째 게시글 제목이 잘 나온다.
1편에서 h2 database 에 insert 한 값인데, 위 테스트 페이지에 뜬 값과 동일한 것을 확인할 수 있다.
참고로 IntelliJ을 닫거나 서버를 끄면 (빨간색 정지 버튼을 누르면) h2 console 에 있는 값들이 모두 사라진다. 그래서 http://localhost:8080/board/test 를 사이트에 치면 Table "TBL_BOARD" not found; 오류가 뜬다.
이럴 때는 서버를 다시 켜고 h2 console 에 다시 create table 을 해주면 된다.
아니면 resources>templates 에 schema.sql 파일을 만들어 그 안에 다음의 코드를 복사해 저장하자.
그러면 서버를 재부팅할 때마다 테이블을 만들어주어야 하는 번거로움을 해결할 수 있다.
CREATE TABLE tbl_board(
boardId Long auto_increment,
title varchar (30) not null,
content varchar (30) not null,
name varchar (30) not null,
read varchar (30) not null default 0,
primary key(boardId)
);
INSERT INTO tbl_board( title, content, name, read) VALUES ('제목1', '내용1', '이름1');
INSERT INTO tbl_board( title, content, name, read) VALUES ('제목2', '내용2', '이름2');
INSERT INTO tbl_board( title, content, name, read) VALUES ('제목3', '내용3', '이름3');
INSERT INTO tbl_board( title, content, name, read) VALUES ('제목4', '내용4', '이름4');
INSERT INTO tbl_board( title, content, name, read) VALUES ('제목5', '내용5', '이름5');
INSERT INTO tbl_board( title, content, name, read) VALUES ('제목6', '내용6', '이름6');
INSERT INTO tbl_board( title, content, name, read) VALUES ('제목7', '내용7', '이름7');
INSERT INTO tbl_board( title, content, name, read) VALUES ('제목8', '내용8', '이름8');
INSERT INTO tbl_board( title, content, name, read) VALUES ('제목9', '내용9', '이름9');
INSERT INTO tbl_board( title, content, name, read) VALUES ('제목10', '내용10', '이름10');
아래 게시물을 참고하여 저 sql 문이 정상 작동되도록 해보자.
https://blog.naver.com/jjb0010/222620771919
2편 끝!