들어가기 전에
하기 포스팅은 "스프링부트 시작하기(김인우 저)" 책을 공부하며 적은 포스팅입니다.
게시판 구현하기 2 까지 진행하면서, 기본 설정은 완료하였습니다. 이번 시간에는 스프링 MVC 구조 및 역할을 생각하며 게시판 목록 만들기를 진행해보도록 하겠습니다.
DTO 만들기
DTO(Data Transfer Object)는 어플리케이션 내의 각 계층 간 데이터를 주고 받는 데 사용되는 객체입니다. 각 계층이란 뷰, 컨트롤러, 서비스, DAO 그리고 데이터베이스 등을 의미합니다.
1. 최상위 패키지인 board 패키지 밑에 board.dto 폴더를 생성하고 BoardDto 클래스를 생성합니다.
2. BoardDto 클래스에 하기와 같이 작성합니다.
package board.board.dto;
import java.time.LocalDateTime;
import lombok.Data;
@Data
public class BoardDto {
private int boardIdx;
private String title;
private String contents;
private int hitCnt;
private String creatorId;
private LocalDateTime createdDatetime;
private String updaterId;
private LocalDateTime updatedDatetime;
}
import lombok.Data;
항목 및 롬복 어노테이션에 의해 BoardDto 클래스의 모든 필드에 대해 getter와 setter를 생성하고 toString, hashCode, equals 메소드를 생성합니다.- BoardDto의 각 필드는 데이터베이스의 게시판 테이블 컬럼과 매칭됩니다. 일반적으로자바는 카멜 표기법을 사용하지만, 데이터베이스는 _(underscore)를 사용하는 스네이크(snake_case) 표기법을 사용합니다. 따라서 표기법과 관련된 설정도 필요합니다.
- 카멜 표기법: 카멜케이스(Camelcase)라고도 불리며, 각 단어의 첫 글자만 대문자로 표기해 낙타 등처럼 보여 이러한 명칭이 붙었습니다. 자바는 클래스 이름은 대문자로 시작하고, 변수나 메소드 이름은 클래스의 이름과 구분하기 위해 소문자로 시작합니다.
참고로 롬복 사용 결과는, 따로 콘솔에 표기되진 않지만 Outline을 보면 get과 set 메소드가 어떻게 생기는지 알 수 있습니다.
마이바티스(myBatis) 설정하기
앞서 lombok을 BoardDto에 설정 시, 자바는 카멜 표기법을 사용하지만 데이터베이스는 스네이크 표기법을 사용하기 때문에 이를 위한 설정이 필요하다고 적었습니다.
데이터베이스는 데이터 조회시, board_idx라는 이름으로 조회되지만, DTO의 변수는 boardIdx라는 이름을 가지고 있어 양측의 데이터를 매핑해 줄 필요가 있습니다. 물론, 데이터베이스에서 SELECT board_idx AS boardIdx와 같이 조회할 수도 있지만 수 많은 쿼리를 그렇게 작성할 수는 없습니다.
이와 관련해 Dto 변수와 데이터베이스 변수 간 매핑을 지원하는 myBatis 기능을 사용할 수 있습니다.
application.properties에 설정 추가하기
myBatis가 제공하는 설정 중 mapUnderscoreToCamel은 전통적인 데이터베이스의 컬럼과 자바 변수를 매핑해주는 기능을 합니다. 기본적으로 false로 설정되어 있으며, 이를 true로 변경하면 됩니다.
// application.properties에 하기 항목 추가
mybatis.configuration.map-underscore-to-camel-case=true
빈(bean) 등록하기
앞서 설정한 mapUnderscoreToCamelCase 설정값을 적용해야 합니다. 먼저, DatabaseConfiguration 클래스 내에 다음 코드를 추가합니다.
...
@Bean
@ConfigurationProperties(prefix="mybatis.configuration")
public org.apache.ibatis.session.Configuration mybatisConfig() {
return new org.apache.ibatis.session.Configuration();
}
...
- application.properties 설정 추가: mybatis 관련 설정을 가져옵니다.
- 가져온 mybatis 설정을 자바 클래스로 만들어 반환합니다.
그 후, DatabaseConfiguration 클래스 메소드 중 sqlSessionFactory 메소드 내에 하기와 같이 추가합니다.
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath:/mapper/**/sql-*.xml"));
/* 추가할 부분 start */
sqlSessionFactoryBean.setConfiguration(mybatisConfig());
/* 추가할 부분 end */
return sqlSessionFactoryBean.getObject();
}
컨트롤러 영역
컨트롤러는 클라이언트의 요청을 받아 해당 요청을 수행하는데 필요한 비즈니스 로직을 호출하고 그 결과를 포함해 응답을 주는 디스패처(Dispatcher) 역할을 합니다. 컨트롤러는 아래 과정을 통해 구현합니다.
- 컨트롤러 클래스에
@Controller
어노테이션을 적용합니다. @RequestMapping
어노테이션을 이용해 요청에 대한 주소를 지정합니다.- 요청에 필요한 비즈니스 로직을 호출합니다(비즈니스 로직이 필요한 경우).
- 실행된 비즈니스 로직의 결과를 뷰로 리턴합니다.
먼저 board 패키지 밑에 board.controller 패키지를 생성한 후, controller 패키지 안에 BoardController 클래스를 생성합니다.
package board.board.controller;
import java.util.List;
import board.board.dto.BoardDto;
import board.board.service.BoardService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class BoardController {
@Autowired
private BoardService boardService;
@RequestMapping("/board/openBoardList.do")
public ModelAndView openBoardList() throws Exception {
ModelAndView mv = new ModelAndView("/board/boardList");
List<BoardDto> list = boardService.selectBoardList();
mv.addObject("list", list);
return mv;
}
}
- 아직 BoardService 클래스가 생성되지 않아, 관련 부분에 에러가 발생합니다.
@Controller
: controller 어노테이션을 붙여줌으로써 BoardController 클래스를 컨트롤러로 동작하게 합니다. 또한, 해당 클래스가 스프링 MVC의 컨트롤러임을 나타냅니다.private BoardService boardService
: 비즈니스 로직을 처리하는 서비스 빈을 연결합니다.@RequestMapping
어노테이션의 값으로 주소를 지정합니다. @RequestMapping 값으로 /board/openBoardList.do가 지정되어 있는데, 웹브라우저에서 /board/openBoardList.do 주소를 호출하면 스프링 디스패처는 호출된 주소와 @RequestMapping 어노테이션 값이 동일한 메소드를 찾아 실행합니다. 즉, 클라이언트에서 호출한 주소와 그것을 수행할 메소드를 연결합니다.MoedelAndView mv = new ModelAndView("/board/boardList")
: 호출된 요청의 결과를 보여줄 뷰를 지정합니다. 여기서는 /board/boardList로 지정했는데, 이는 templates 폴더 아래의 board/boardList.html 파일을 의미합니다. 해당 파일은 추후 작성 예정입니다. Thymeleaf와 같은 템플릿을 사용할 경우, 스프링부트의 자동 설정 기능으로 .html과 같은 접미사를 생략할 수 있습니다.List<BoardDto> list = boardService.selectBoardList()
: 게시글 목록을 조회합니다. 게시글 목록을 조회한다는 비즈니스 로직을 수행하기 위해 BoardService 클래스의 selectBoardList 메소드를 호출합니다. 게시글 목록을 저장하기 위해 List를 사용했습니다.mv.addObject("list", list)
: 실행된 비즈니스 로직의 결과 값을 뷰에 list라는 이름으로 저장합니다. 뷰에서 사용할 경우 list라는 이름으로 조회 결과를 사용할 수 있습니다.
서비스 영역
서비스 영역은 일반적으로 2개의 파일로 구성됩니다. Service 인터페이스와 이를 구현한 ServiceImpl 클래스입니다. 인터페이스와 인터페이스의 구현 클래스로 분리할 경우 여러 장점이 있습니다.
- 느슨한 결합(loose coupling)을 유지하여 각 기능 간 의존관계 최소화
- 의존관계의 최소화로 인해 기능의 변화에도 최소한의 수정으로 개발할 수 있는 유연함 가짐
- 모듈화를 통해 어디서든 사용할 수 있도록 하여 재사용성 높임
- 스프링의 IoC/DI(Inversion of Control / Dependency Injection) 기능을 이용한 빈 관리 기능 사용 가능
위와 같은 장점이 있더라도, 개발하는 환경에 따라 서비스 영역을 굳이 나눌 필요가 없을 수도 있습니다. 또한 서비스 영역 뿐 아니라, repository 영역도 나눌 수 있습니다. 따라서 인터페이스와 구현체의 필요성에 따라 적절히 사용하면 됩니다.
board 패키지 밑에 service 패키지를 생성합니다. 그 후, service 패키지 안에 BoardService 인터페이스와 BoardServiceImpl 클래스를 생성합니다.
// BoardService.java
package board.board.service;
import board.board.dto.BoardDto;
import java.util.List;
public interface BoardService {
List<BoardDto> selectBoardList() throws Exception;
}
// BoardServiceImpl.java
package board.board.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import board.board.dto.BoardDto;
import board.board.mapper.BoardMapper;
@Service
public class BoardServiceImpl implements BoardService {
@Autowired
private BoardMapper boardMapper;
@Override
public List<BoardDto> selectBoardList() throws Exception {
return boardMapper.selectBoardList();
}
}
BoardService 인터페이스에는 비즈니스 로직을 수행하기 위한 메소드를 정의합니다. 그리고 BoardServiceImpl 클래스는 BoardService 인터페이스를 사용하여 실제 기능을 구현합니다.
@Service
: 비즈니스 로직을 처리하는 서비스 클래스를 나타내는 어노테이션입니다. 해당 어노테이션을 사용하여 스프링 MVC의 서비스임을 나타냅니다.private BoardMapper boardMapper
: 데이터베이스에 접근하는 DAO 빈을 선언합니다. 아직 BoardMapper를 만들지 않아 에러가 발생합니다.return boardMapper.selectBoardList()
: 사용자 요청을 처리하기 위한 비즈니스 로직을 구현합니다. 여기서는 데이터를 조회하도록 BoardMapper 클래스의 selectBoardList 메소드를 호출합니다.
매퍼 영역
마이바티스는 데이터 접근 객체인 DAO(Data Access Object)를 만드는 것보다 SqlSessionDaoSupport
나 SqlSessionTemplate
을 사용하기를 권장합니다. 이렇게 함으로써 마이바티스 스프링 연동 모듈은 다른 빈에 직접 주입할 수 있는 mapper를 생성할 수 있습니다. 또한, mapper를 사용함으로써 일일이 DAO를 만들지 않고 인터페이스만을 이용해 좀 더 편하게 개발할 수 있습니다.
board 패키지 밑에 mapper 패키지를 생성합니다. 그 후 mapper 패키지 내에 BoardMapper 인터페이스를 생성합니다.
package board.board.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import board.board.dto.BoardDto;
@Mapper
public interface BoardMapper {
List<BoardDto> selectBoardList() throws Exception;
}
@Mapper
: 마이바티스 mapper 인터페이스임을 선언합니다.List<BoardDto> selectBoardList() throws Exception
: 인터페이스이기 때문에 메소드의 이름과 반환 형식만 지정합니다. 메소드 이름은 SQL 이름과 동일해야 합니다.
SQL 작성하기
마이바티스는 쿼리를 XML에 작성하고 id를 이용해 매핑합니다. XML 파일의 경우 src/main/java 폴더가 아닌, src/main/resources에 놓이게 됩니다. src/main/resources 폴더 밑에 mapper 폴더를 생성합니다. 그리고, mapper 폴더 안에 sql-board.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="board.mapper.BoardMapper">
<select id="selectBoardList" resultType="board.board.dto.BoardDto">
<![CDATA[
SELECT
board_idx,
title,
hit_cnt,
created_datetime
FROM
t_board
WHERE
deleted_yn = 'N'
ORDER BY board_idx DESC
]]>
</select>
</mapper>
<mapper namespace="board.mapper.BoardMapper">
: mapper의 네임스페이스를 지정합니다. mapper와 xml 쿼리를 매칭해서 사용하려면 매퍼 인터페이스의 경로와 메소드의 이름과 쿼리의 이름이 같아야 합니다.- 즉, BoardMapper 인터페이스에 있는 selectBoardList에서 selectBoardList라는 이름과 쿼리와 매핑하기 위해서는 그 전체 경로인 board.mapper.BoardMapper.selectBoardList가 필요합니다.
- 쿼리의 id를 지정할 때 각각의 쿼리에 전체 경로를 일일이 명시하는 것은 중복된 작업이기 때문에, mapper의 namespace에서 공통적으로 사용되는 경로를 지정해서 namespace와 쿼리 id가 합쳐져서 호출되도록 합니다.
<select id="selectBoardList" resultType="boar.dto.BoardDto">
:- 위 xml 파일에선 사용하지 않았지만, parameterType이라는 속성도 존재합니다. 이 속성은 입력되는 파라미터의 형식을 지정해줍니다.
- parameterType과 resultType을 명시할 때는 해당 클래스의 패키지를 포함해서 지정해야 합니다.
- 만약, 개발자가 생성한 클래스가 아닌, 자바의 기본 클래스를 사용하여 Type을 지정할 경우, 전체 패키지 경로를 지정해야 합니다(예: HashMap → java.util.HashMap, 하지만 마이바티스 타입 별칭 기능으로 인해 hashmap이라고 단순히 작성해도 인식이 가능합니다).
대표적인 자바 클래스 타입 별칭
별칭 | 매핑된 타입 |
---|---|
int | Integer |
float | Float |
double | Double |
string | String |
boolean | Boolean |
map | Map |
hashmap | HashMap |
list | List |
arrayList | ArrayList |
뷰 작성하기
마지막으로 사용자에게 보여 줄 화면을 작성할 차례입니다. src/main/reousrces/templates 폴더 안에 board 폴더를 생성합니다. 그 후, board 폴더 안에 boardList.html 파일을 생성합니다.
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>board</title>
<link rel="stylesheet" th:href="@{/css/style.css}"/>
</head>
<body>
<div class="container">
<h2>게시글 목록</h2>
<table class="board_list">
<colgroup>
<col width="15%"/>
<col width="*"/>
<col width="15%"/>
<col width="20%"/>
</colgroup>
<thead>
<tr>
<th scope="col">글번호</th>
<th scope="col">제목</th>
<th scope="col">조회수</th>
<th scope="col">작성일</th>
</tr>
</thead>
<tbody>
<tr th:if="${#lists.size(list)} > 0" th:each="list : ${list}">
<td th:text="${list.boardIdx}"></td>
<td class="title"><a href="/board/openBoardDetail.do?boardIdx=" th:attrappend="href=${list.boardIdx}" th:text="${list.title}"></a></td>
<td th:text="${list.hitCnt}"></td>
<td th:text="${#temporals.format(list.createdDatetime, 'yyyy-MM-dd HH:mm:ss')}"></td>
</tr>
<tr th:unless="${#lists.size(list)} > 0">
<td colspan="4">조회된 결과가 없습니다.</td>
</tr>
</tbody>
</table>
<a href="/board/openBoardWrite.do" class="btn">글 쓰기</a>
</div>
</body>
</html>
xmlns:th="http://www.thymeleaf.org">
: Thymeleaf의 th 속성을 사용하기 위한 네임스페이스를 선언합니다.<tr th:if="${#lists.size(list)} > 0" th:each="list : ${list}">
: 조회된 데이터(여기서는 게시글 개수에 따라 늘어남)가 여러 개 일 경우 목록으로 보여줍니다.<td class="title" th:text="${list.title}"></td>
: list 변수를 통해 서버에서 전달된 데이터에 접근해서 화면에 표시합니다. 쿼리에서 게시글에 필요한 board_idx, title, hit_cnt, created_datetime이라는 컬럼을 조회하고, 이는 BoardDto 클래스의 boardIdx, title, hitCnt, createdDatetime이라는 변수에 저장되어 있습니다. 뷰에서는 서버로부터 전달된 게시글 목록을 list 변수를 통해 접근할 수 있습니다.<td th:text="${temporals.format(list.createdDateTime, 'yyyy-MM-dd HH:mm:ss')}"></td>
: Thymeleaf에서는 날짜를 처리하기 위한 dates 객체를 제공합니다. 하지만, dates 객체는 LocalDateTime과 같은 자바 8에서 추가된 날짜 관련 클래스를 처리할 수 없습니다. 따라서, Thymeleaf는 자바 8의 날짜 API를 지원하기 위해 temporals 객체를 추가했습니다. temporals는 날짜와 관련 여러 기능을 제공합니다.<tr th:unless="${$lists.size(list)} > 0">
: 조회된 데이터가 없을 경우 "조회된 결과가 없습니다" 보여주기 위해 사용합니다.
실행 결과 확인하기
게시판 목록 호출 주소인 localhost:8080/board/openBoardList.do를 입력하면 아래와 같은 결과를 볼 수 있습니다.
현재 데이터베이스에는 테이블만 생성되어 있어 게시글이 존재하지 않습니다. 따라서 조회돈 목록이 없을 경우 보여주는 "조회된 결과가 없습니다"라는 메시지가 나타나게 됩니다.
이번에는, 게시글을 하나 등록하고 정상적으로 목록을 조회하는지 확인해보도록 하겠습니다. 아래 쿼리를 mySQL GUI 툴을 이용하여 실행한 후 다시 게시글 목록을 조회해보도록 하겠습니다.
INSERT INTO t_board
(
title,
contents,
creator_id,
created_datetime
)
VALUE
(
'first title',
'first contents',
'admin',
NOW()
)
DTO 만들기
DTO(Data Transfer Object)는 어플리케이션 내의 각 계층 간 데이터를 주고 받는 데 사용되는 객체입니다. 각 계층이란 뷰, 컨트롤러, 서비스, DAO 그리고 데이터베이스 등을 의미합니다.
1. 최상위 패키지인 board 패키지 밑에 board.dto 폴더를 생성하고 BoardDto 클래스를 생성합니다.
2. BoardDto 클래스에 하기와 같이 작성합니다.
package board.board.dto;
import java.time.LocalDateTime;
import lombok.Data;
@Data
public class BoardDto {
private int boardIdx;
private String title;
private String contents;
private int hitCnt;
private String creatorId;
private LocalDateTime createdDatetime;
private String updaterId;
private LocalDateTime updatedDatetime;
}
- import lombok.Data; 항목 및 롬복 어노테이션에 의해 BoardDto 클래스의 모든 필드에 대해 getter와 setter를 생성하고 toString, hashCode, equals 메소드를 생성합니다.
- BoardDto의 각 필드는 데이터베이스의 게시판 테이블 컬럼과 매칭됩니다. 일반적으로자바는 카멜 표기법을 사용하지만, 데이터베이스는 _(underscore)를 사용하는 스네이크(snake_case) 표기법을 사용합니다. 따라서 표기법과 관련된 설정도 필요합니다.
- 카멜 표기법: 카멜케이스(Camelcase)라고도 불리며, 각 단어의 첫 글자만 대문자로 표기해 낙타 등처럼 보여 이러한 명칭이 붙었습니다. 자바는 클래스 이름은 대문자로 시작하고, 변수나 메소드 이름은 클래스의 이름과 구분하기 위해 소문자로 시작합니다.
참고로 롬복 사용 결과는, 따로 콘솔에 표기되진 않지만 Outline을 보면 get과 set 메소드가 어떻게 생기는지 알 수 있습니다.
마이바티스(myBatis) 설정하기
앞서 lombok을 BoardDto에 설정 시, 자바는 카멜 표기법을 사용하지만 데이터베이스는 스네이크 표기법을 사용하기 때문에 이를 위한 설정이 필요하다고 적었습니다.
데이터베이스는 데이터 조회시, board_idx라는 이름으로 조회되지만, DTO의 변수는 boardIdx라는 이름을 가지고 있어 양측의 데이터를 매핑해 줄 필요가 있습니다. 물론, 데이터베이스에서 SELECT board_idx AS boardIdx와 같이 조회할 수도 있지만 수 많은 쿼리를 그렇게 작성할 수는 없습니다.
이와 관련해 Dto 변수와 데이터베이스 변수 간 매핑을 지원하는 myBatis 기능을 사용할 수 있습니다.
application.properties에 설정 추가하기
myBatis가 제공하는 설정 중 mapUnderscoreToCamel은 전통적인 데이터베이스의 컬럼과 자바 변수를 매핑해주는 기능을 합니다. 기본적으로 false로 설정되어 있으며, 이를 true로 변경하면 됩니다.
// application.properties에 하기 항목 추가
mybatis.configuration.map-underscore-to-camel-case=true
빈(bean) 등록하기
앞서 설정한 mapUnderscoreToCamelCase
설정값을 적용해야 합니다. 먼저, DatabaseConfiguration
클래스 내에 다음 코드를 추가합니다.
...
@Bean
@ConfigurationProperties(prefix="mybatis.configuration")
public org.apache.ibatis.session.Configuration mybatisConfig() {
return new org.apache.ibatis.session.Configuration();
}
...
- application.properties 설정 추가: mybatis 관련 설정을 가져옵니다.
- 가져온 mybatis 설정을 자바 클래스로 만들어 반환합니다.
그 후, DatabaseConfiguration 클래스 메소드 중 sqlSessionFactory 메소드 내에 하기와 같이 추가합니다.
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath:/mapper/**/sql-*.xml"));
/* 추가할 부분 start */
sqlSessionFactoryBean.setConfiguration(mybatisConfig());
/* 추가할 부분 end */
return sqlSessionFactoryBean.getObject();
}
컨트롤러 영역
컨트롤러는 클라이언트의 요청을 받아 해당 요청을 수행하는데 필요한 비즈니스 로직을 호출하고 그 결과를 포함해 응답을 주는 디스패처(Dispatcher) 역할을 합니다. 컨트롤러는 아래 과정을 통해 구현합니다.
- 컨트롤러 클래스에
@Controller
어노테이션을 적용합니다. @RequestMapping
어노테이션을 이용해 요청에 대한 주소를 지정합니다.- 요청에 필요한 비즈니스 로직을 호출합니다(비즈니스 로직이 필요한 경우).
- 실행된 비즈니스 로직의 결과를 뷰로 리턴합니다.
먼저 board 패키지 밑에 board.controller 패키지를 생성한 후, controller 패키지 안에 BoardController 클래스를 생성합니다.
package board.board.controller;
import java.util.List;
import board.board.dto.BoardDto;
import board.board.service.BoardService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class BoardController {
@Autowired
private BoardService boardService;
@RequestMapping("/board/openBoardList.do")
public ModelAndView openBoardList() throws Exception {
ModelAndView mv = new ModelAndView("/board/boardList");
List<BoardDto> list = boardService.selectBoardList();
mv.addObject("list", list);
return mv;
}
}
- 아직 BoardService 클래스가 생성되지 않아, 관련 부분에 에러가 발생합니다.
@Controller
: controller 어노테이션을 붙여줌으로써 BoardController 클래스를 컨트롤러로 동작하게 합니다. 또한, 해당 클래스가 스프링 MVC의 컨트롤러임을 나타냅니다.private BoardService boardService
: 비즈니스 로직을 처리하는 서비스 빈을 연결합니다.@RequestMapping
어노테이션의 값으로 주소를 지정합니다. @RequestMapping 값으로 /board/openBoardList.do가 지정되어 있는데, 웹브라우저에서 /board/openBoardList.do 주소를 호출하면 스프링 디스패처는 호출된 주소와 @RequestMapping 어노테이션 값이 동일한 메소드를 찾아 실행합니다. 즉, 클라이언트에서 호출한 주소와 그것을 수행할 메소드를 연결합니다.MoedelAndView mv = new ModelAndView("/board/boardList")
: 호출된 요청의 결과를 보여줄 뷰를 지정합니다. 여기서는 /board/boardList로 지정했는데, 이는 templates 폴더 아래의 board/boardList.html 파일을 의미합니다. 해당 파일은 추후 작성 예정입니다. Thymeleaf와 같은 템플릿을 사용할 경우, 스프링부트의 자동 설정 기능으로 .html과 같은 접미사를 생략할 수 있습니다.List<BoardDto> list = boardService.selectBoardList()
: 게시글 목록을 조회합니다. 게시글 목록을 조회한다는 비즈니스 로직을 수행하기 위해 BoardService 클래스의 selectBoardList 메소드를 호출합니다. 게시글 목록을 저장하기 위해 List를 사용했습니다.mv.addObject("list", list)
: 실행된 비즈니스 로직의 결과 값을 뷰에 list라는 이름으로 저장합니다. 뷰에서 사용할 경우 list라는 이름으로 조회 결과를 사용할 수 있습니다.
서비스 영역
서비스 영역은 일반적으로 2개의 파일로 구성됩니다. Service 인터페이스와 이를 구현한 ServiceImpl 클래스입니다. 인터페이스와 인터페이스의 구현 클래스로 분리할 경우 여러 장점이 있습니다.
- 느슨한 결합(loose coupling)을 유지하여 각 기능 간 의존관계 최소화
- 의존관계의 최소화로 인해 기능의 변화에도 최소한의 수정으로 개발할 수 있는 유연함 가짐
- 모듈화를 통해 어디서든 사용할 수 있도록 하여 재사용성 높임
- 스프링의 IoC/DI(Inversion of Control / Dependency Injection) 기능을 이용한 빈 관리 기능 사용 가능
위와 같은 장점이 있더라도, 개발하는 환경에 따라 서비스 영역을 굳이 나눌 필요가 없을 수도 있습니다. 또한 서비스 영역 뿐 아니라, repository 영역도 나눌 수 있습니다. 따라서 인터페이스와 구현체의 필요성에 따라 적절히 사용하면 됩니다.
board 패키지 밑에 service 패키지를 생성합니다. 그 후, service 패키지 안에 BoardService 인터페이스와 BoardServiceImpl 클래스를 생성합니다.
// BoardService.java
package board.board.service;
import board.board.dto.BoardDto;
import java.util.List;
public interface BoardService {
List<BoardDto> selectBoardList() throws Exception;
}
// BoardServiceImpl.java
package board.board.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import board.board.dto.BoardDto;
import board.board.mapper.BoardMapper;
@Service
public class BoardServiceImpl implements BoardService {
@Autowired
private BoardMapper boardMapper;
@Override
public List<BoardDto> selectBoardList() throws Exception {
return boardMapper.selectBoardList();
}
}
BoardService 인터페이스에는 비즈니스 로직을 수행하기 위한 메소드를 정의합니다. 그리고 BoardServiceImpl 클래스는 BoardService 인터페이스를 사용하여 실제 기능을 구현합니다.
- @Service: 비즈니스 로직을 처리하는 서비스 클래스를 나타내는 어노테이션입니다. 해당 어노테이션을 사용하여 스프링 MVC의 서비스임을 나타냅니다.
- private BoardMapper boardMapper: 데이터베이스에 접근하는 DAO 빈을 선언합니다. 아직 BoardMapper를 만들지 않아 에러가 발생합니다.
- return boardMapper.selectBoardList(): 사용자 요청을 처리하기 위한 비즈니스 로직을 구현합니다. 여기서는 데이터를 조회하도록 BoardMapper 클래스의 selectBoardList 메소드를 호출합니다.
매퍼 영역
마이바티스는 데이터 접근 객체인 DAO(Data Access Object)를 만드는 것보다 SqlSessionDaoSupport나 SqlSessionTemplate을 사용하기를 권장합니다. 이렇게 함으로써 마이바티스 스프링 연동 모듈은 다른 빈에 직접 주입할 수 있는 mapper를 생성할 수 있습니다. 또한, mapper를 사용함으로써 일일이 DAO를 만들지 않고 인터페이스만을 이용해 좀 더 편하게 개발할 수 있습니다.
board 패키지 밑에 mapper 패키지를 생성합니다. 그 후 mapper 패키지 내에 BoardMapper 인터페이스를 생성합니다.
package board.board.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import board.board.dto.BoardDto;
@Mapper
public interface BoardMapper {
List<BoardDto> selectBoardList() throws Exception;
}
@Mapper
: 마이바티스 mapper 인터페이스임을 선언합니다.List<BoardDto> selectBoardList() throws Exception
: 인터페이스이기 때문에 메소드의 이름과 반환 형식만 지정합니다. 메소드 이름은 SQL 이름과 동일해야 합니다.
SQL 작성하기
마이바티스는 쿼리를 XML에 작성하고 id를 이용해 매핑합니다. XML 파일의 경우 src/main/java 폴더가 아닌, src/main/resources에 놓이게 됩니다. src/main/resources 폴더 밑에 mapper 폴더를 생성합니다. 그리고, mapper 폴더 안에 sql-board.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="board.mapper.BoardMapper">
<select id="selectBoardList" resultType="board.board.dto.BoardDto">
<![CDATA[
SELECT
board_idx,
title,
hit_cnt,
created_datetime
FROM
t_board
WHERE
deleted_yn = 'N'
ORDER BY board_idx DESC
]]>
</select>
</mapper>
<mapper namespace="board.mapper.BoardMapper">
: mapper의 네임스페이스를 지정합니다. mapper와 xml 쿼리를 매칭해서 사용하려면 매퍼 인터페이스의 경로와 메소드의 이름과 쿼리의 이름이 같아야 합니다.- 즉, BoardMapper 인터페이스에 있는 selectBoardList에서 selectBoardList라는 이름과 쿼리와 매핑하기 위해서는 그 전체 경로인 board.mapper.BoardMapper.selectBoardList가 필요합니다.
- 쿼리의 id를 지정할 때 각각의 쿼리에 전체 경로를 일일이 명시하는 것은 중복된 작업이기 때문에, mapper의 namespace에서 공통적으로 사용되는 경로를 지정해서 namespace와 쿼리 id가 합쳐져서 호출되도록 합니다.
<select id="selectBoardList" resultType="boar.dto.BoardDto">
:- 위 xml 파일에선 사용하지 않았지만, parameterType이라는 속성도 존재합니다. 이 속성은 입력되는 파라미터의 형식을 지정해줍니다.
- parameterType과 resultType을 명시할 때는 해당 클래스의 패키지를 포함해서 지정해야 합니다.
- 만약, 개발자가 생성한 클래스가 아닌, 자바의 기본 클래스를 사용하여 Type을 지정할 경우, 전체 패키지 경로를 지정해야 합니다(예: HashMap → java.util.HashMap, 하지만 마이바티스 타입 별칭 기능으로 인해 hashmap이라고 단순히 작성해도 인식이 가능합니다).
대표적인 자바 클래스 타입 별칭
별칭 | 매핑된 타입 |
---|---|
int | Integer |
float | Float |
double | Double |
string | String |
boolean | Boolean |
map | Map |
hashmap | HashMap |
list | List |
arrayList | ArrayList |
뷰 작성하기
마지막으로 사용자에게 보여 줄 화면을 작성할 차례입니다. src/main/reousrces/templates 폴더 안에 board 폴더를 생성합니다. 그 후, board 폴더 안에 boardList.html 파일을 생성합니다.
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>board</title>
<link rel="stylesheet" th:href="@{/css/style.css}"/>
</head>
<body>
<div class="container">
<h2>게시글 목록</h2>
<table class="board_list">
<colgroup>
<col width="15%"/>
<col width="*"/>
<col width="15%"/>
<col width="20%"/>
</colgroup>
<thead>
<tr>
<th scope="col">글번호</th>
<th scope="col">제목</th>
<th scope="col">조회수</th>
<th scope="col">작성일</th>
</tr>
</thead>
<tbody>
<tr th:if="${#lists.size(list)} > 0" th:each="list : ${list}">
<td th:text="${list.boardIdx}"></td>
<td class="title" th:text="${list.title}"></td>
<td th:text="${#temporals.format(list.createdDatetime,
'yyyy-MM-dd HH:mm::ss')}"></td>
</tr>
<tr th:unless="${#lists.size(list)} > 0">
<td colspan="4">조회된 결과가 없습니다.</td>
</tr>
</tbody>
</table>
<a href="/board/openBoardWrite.do" class="btn">글 쓰기</a>
</div>
</body>
</html>
xmlns:th="http://www.thymeleaf.org">
: Thymeleaf의 th 속성을 사용하기 위한 네임스페이스를 선언합니다.<tr th:if="${#lists.size(list)} > 0" th:each="list : ${list}">
: 조회된 데이터(여기서는 게시글 개수에 따라 늘어남)가 여러 개 일 경우 목록으로 보여줍니다.<td class="title" th:text="${list.title}"></td>
: list 변수를 통해 서버에서 전달된 데이터에 접근해서 화면에 표시합니다. 쿼리에서 게시글에 필요한 board_idx, title, hit_cnt, created_datetime이라는 컬럼을 조회하고, 이는 BoardDto 클래스의 boardIdx, title, hitCnt, createdDatetime이라는 변수에 저장되어 있습니다. 뷰에서는 서버로부터 전달된 게시글 목록을 list 변수를 통해 접근할 수 있습니다.<td th:text="${temporals.format(list.createdDateTime, 'yyyy-MM-dd HH:mm:ss')}"></td>
: Thymeleaf에서는 날짜를 처리하기 위한 dates 객체를 제공합니다. 하지만, dates 객체는 LocalDateTime과 같은 자바 8에서 추가된 날짜 관련 클래스를 처리할 수 없습니다. 따라서, Thymeleaf는 자바 8의 날짜 API를 지원하기 위해 temporals 객체를 추가했습니다. temporals는 날짜와 관련 여러 기능을 제공합니다.<tr th:unless="${$lists.size(list)} > 0">
: 조회된 데이터가 없을 경우 "조회된 결과가 없습니다" 보여주기 위해 사용합니다.
실행 결과 확인하기
게시판 목록 호출 주소인 localhost:8080/board/openBoardList.do를 입력하면 아래와 같은 결과를 볼 수 있습니다.
현재 데이터베이스에는 테이블만 생성되어 있어 게시글이 존재하지 않습니다. 따라서 조회돈 목록이 없을 경우 보여주는 "조회된 결과가 없습니다"라는 메시지가 나타나게 됩니다.
이번에는, 게시글을 하나 등록하고 정상적으로 목록을 조회하는지 확인해보도록 하겠습니다. 아래 쿼리를 mySQL GUI 툴을 이용하여 실행한 후 다시 게시글 목록을 조회해보도록 하겠습니다.
INSERT INTO t_board
(
title,
contents,
creator_id,
created_datetime
)
VALUE
(
'first title',
'first contents',
'admin',
NOW()
)
'FRAMEWORK > Spring' 카테고리의 다른 글
[SpringBoot] 게시판 구현하기 5 (게시글 상세 화면 만들기) (0) | 2020.12.21 |
---|---|
[SpringBoot] 게시판 구현하기 4 (게시글 등록 기능 생성하기) (0) | 2020.12.21 |
[SpringBoot] 게시판 구현하기 2 (게시판을 만들기 위한 기본 설정) (1) | 2020.12.13 |
[SpringBoot] 게시판 구현하기 1 (SpringBoot 프로젝트 생성 및 Database 연동) (0) | 2020.12.13 |
[Spring] 스프링 프레임워크 이해하기 (0) | 2020.12.12 |