마이바티스란?
마이바티스는 개발자가 지정한 SQL, 저장프로시저 그리고 몇가지 고급 매핑을 지원하는 퍼시스턴스 프레임워크이다. 마이바티스는 JDBC로 처리하는 상당부분의 코드와 파라미터 설정및 결과 매핑을 대신해준다.
마이바티스의 장단점
장점
단점 => 환경설정이 쉽지 않다.(오타때문에 찾기가 어렵다)
마이바티스 환경설정하기
글목록보기도 같이 진행
마이바티스를 사용하면 엄청 편해지지만 환경설정이 복잡하다
우선 환경설정을 하면서 이전에 했던 미니게시판프로젝트를 마이바티스를 적용하고자한다.
SpringBoard에 Mybatis를 적용
이전에 정리했던 미니게시판만들기 프로젝트인 SpringBoard에 Mybatis를 적용하고자 한다
따라서 SpringBoard를 복사해서 SpringMybatis라고 이름을 바꾸고 이어서 시작한다.
index.jsp를 미리 바꿔준다
<%
response.sendRedirect("http://localhost:8090/SpringMybatis/list.do");
%>
WEB-INF/lib 안에 라이브러리들 다 지우고 새로 넣는다
=>(3.x버전을 사용하기에 바꿔줘야함)
WEB-INF에 jdbc.properties생성
=>db연결
jdbc.driverClassName=oracle.jdbc.driver.OracleDriver
jdbc.url=jdbc:oracle:thin:@localhost:1521:orcl
jdbc.username=scott
jdbc.password=tiger
WEB-INF에 dataAccessContext-local.xml 생성
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN"
"http://www.springframework.org/dtd/spring-beans-2.0.dtd" >
<beans>
<!-- 1.jdbc.properties파일 읽어오기(클래스)경로지정해서 메모리에 로딩 -->
<bean id="propertyConfigurer"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>WEB-INF/jdbc.properties</value>
</list>
</property>
</bean>
<!-- 2.jdbc.properties(각각의 키명에 해당되는 값을 불러오는 구문
destroy-method="프로그램 종료시 자동으로 호출할 메서드명을 지정"
close메서드(메모리해제) ${키명}-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
<!-- 3.Mybatis빈을 등록(SqlSessionFactoryBean)
1)configLocation->전체 테이블에 대한 xml파일을 불러올때 사용(접속하자마자 가져올 테이블 정보)
2)dataSource->DB연결정보를 가진 멤버변수
-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="configLocation" value="WEB-INF/SqlMapConfig.xml" />
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 4.SqlSessionTemplate(sqlSession객체를 더 쉽게 얻어오게 설정) -->
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
</beans>
web.xml내용추가 => 기존에 있는 내용들 위에 새로운 환경설정 추가
~생략~
<!-- 외부의 DB에 관련된 환경설정파일을 불러오는 경우(관련클래스,매개변수) -->
<!-- 매개변수 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/dataAccessContext-local.xml</param-value>
</context-param>
<!--위의 경로를 메모리에 올려주는 클래스 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
~생략~
SqlMapConfig.xml 생성 내용추가
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"HTTP://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<typeAlias type="lee.BoardCommand" alias="board" />
</typeAliases>
<mappers>
<mapper resource="lee/Board.xml" />
<!--
<mapper resource="lee/Sangpum.xml" /> 상품테이블이라면
<mapper resource="lee/Member.xml" /> 멤버테이블이라면
<mapper resource="lee/Notice.xml" /> 공지테이블이라면
,,,
-->
</mappers>
</configuration>
Mybatis에서 테이블에 대한 SQL구문을 작성하는 방법
<?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">
<!-- 1.글목록보기 --> <!-- 별칭 -->
<select id="list" resultType="board">
select * from springboard order by num desc
</select>
</mapper>
BoardCommand.java에 내용추가
~생략~
int num;
String author,title,content;//num,date,readcnt=>입력X
//스프링=><jsp:setProperty name="~" property="*" /> 자바클래스가 존재 O
String writeday;
int readcnt;
String searchName,searchValue;//이부분때문에 따로 클래스 작성X
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public String getWriteday() {
return writeday;
}
public void setWriteday(String writeday) {
this.writeday = writeday;
}
public int getReadcnt() {
return readcnt;
}
public void setReadcnt(int readcnt) {
this.readcnt = readcnt;
}
public String getSearchName() {
return searchName;
}
public void setSearchName(String searchName) {
this.searchName = searchName;
}
public String getSearchValue() {
return searchValue;
}
public void setSearchValue(String searchValue) {
this.searchValue = searchValue;
}
~생략~
BoardDAO를 인터페이스로 변환
package lee;
import java.util.List;
import org.springframework.dao.DataAccessException;//스프링 전용 예외처리클래스
public interface BoardDAO{
//1.글목록보기
public List list() throws DataAccessException;//try~catch(X)
}
엄청 긴 DAO가 이렇게 간단하게 바뀐다.
BoardDAO(인터페이스)를 상속받는 자식클래스 생성
=> SqlMapBoardDao클래스를 작성
package lee;
import java.util.List;
import org.mybatis.spring.support.SqlSessionDaoSupport;
import org.springframework.dao.DataAccessException;
//업무에 다른 메서드를 구현
//SqlSessionDaoSupport 상속
//SqlSession클래스 객체->DB연동할때 필요로하는 객체
public class SqlMapBoardDao extends SqlSessionDaoSupport implements BoardDAO {
//SqlSession sqlSession;=>getSqlSession()을 제공해준다.
//setSqlSession(~){this.sqlSession=sqlSession;}
@Override
public List list() throws DataAccessException {
// TODO Auto-generated method stub
/*
* select->레코드 한개이상(List)->selectList("실행시킬 sql구문의 id부여")
* select->레코드 한개(DTO,String,Integer,,,)->selectOne("실행시킬 sql구문의 id부여")
*/
return getSqlSession().selectList("list");//이렇게 바로 반환받을수있다.
}
}
Board-servlet.xml 수정
~생략~
<bean id="boardDAO" class="lee.SqlMapBoardDao">
<property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
~생략~
여기까지 하면 환경설정은 끝이다
환경설정하면서 글목록보기도 같이 설정해주고있는데 잘 설정됐는지 확인하기위해 글목록보기를 마저 설정 후 실행해보고 확인하고자 한다.
ListActionController (컨트롤러) 수정
package lee;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
//public class ListAction implements CommandAction{ 페이지이동,클래스는 틀리지만 처리메서드 동일
public class ListActionController implements Controller {
BoardDAO dao;//BoardDAO dao=new BoardDAO(); 스프링컨테이너가 해준다.
public void setDao(BoardDAO dao) {//<property name="dao"><ref bean~
this.dao = dao;
System.out.println("setDao() 호출됨(dao)=>"+dao);
}
@Override
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// TODO Auto-generated method stub
System.out.println("ListActionController의 handleRequest()호출됨");
//ArrayList list=dao.list();
List list=dao.list();
//화면에 출력할 list.jsp에 전달할 페이지명과 전달할값을 설정하기위해
ModelAndView mav=new ModelAndView();//viewName 멤버변수
mav.setViewName("list");//이동할 페이지명만 확인->경로? 확장자?
mav.addObject("list",list);//request.setAttribute("list",list);
//mav.addObject("키명",전달할값),,,,
//${list(키명)} request.getAttribute("list");
return mav;
}
}
list.jsp 수정
package lee;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
//public class ListAction implements CommandAction{ 페이지이동,클래스는 틀리지만 처리메서드 동일
public class ListActionController implements Controller {
BoardDAO dao;//BoardDAO dao=new BoardDAO(); 스프링컨테이너가 해준다.
public void setDao(BoardDAO dao) {//<property name="dao"><ref bean~
this.dao = dao;
System.out.println("setDao() 호출됨(dao)=>"+dao);
}
@Override
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// TODO Auto-generated method stub
System.out.println("ListActionController의 handleRequest()호출됨");
//ArrayList list=dao.list();
List list=dao.list();
//화면에 출력할 list.jsp에 전달할 페이지명과 전달할값을 설정하기위해
ModelAndView mav=new ModelAndView();//viewName 멤버변수
mav.setViewName("list");//이동할 페이지명만 확인->경로? 확장자?
mav.addObject("list",list);//request.setAttribute("list",list);
//mav.addObject("키명",전달할값),,,,
//${list(키명)} request.getAttribute("list");
return mav;
}
}
여기까지 하면 환경설정 및 글목록보기 완료
글 목록보기가 잘 나오면 환경설정이 잘 완료된것이다.
글쓰기
BoardDAO (인터페이스) 내용추가
~생략~
//2.최대값구하기
public int getNewNum() throws DataAccessException;
//3.글쓰기
public void write(BoardCommand data) throws DataAccessException;
SqlMapBoardDAO 내용추가
~생략~
@Override
public int getNewNum() throws DataAccessException {
// TODO Auto-generated method stub
//형식) selectOne->레코드 한개만 또는 각 필드(문자열,숫자,,)모든객체
//Object -> Integer로 형변환 ->int
return (Integer)getSqlSession().selectOne("getNewNum");
}
@Override
public void write(BoardCommand data) throws DataAccessException {
// TODO Auto-generated method stub
//insert태그-> 형식)sqlSession객체명.insert("실행시킬 id",매개변수명);
getSqlSession().insert("write",data);
}
Board.xml 내용추가
~생략~
<!-- 2.글쓰기(게시물번호) -->
<select id="getNewNum" resultType="int">
select max(num) from springboard
</select>
<!--3.글쓰기 #{매개변수명} 매개변수명은 DTO(or VO)의 멤버변수(getter를 호출해서 불러와넣을때) #{num}은 getNum-->
<insert id="write" parameterType="board">
insert into springboard(num,author,title,content,writeday) values(#{num},#{author},#{title},#{content},sysdate)
</insert>
WriteActionController (컨트롤러)소스 편집
package lee;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.validation.BindException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractCommandController;
//입력을 받아서 처리(글쓰기,글수정,로그인),입력X 처리해주는경우(페이지 이동(글쓰기폼),글상세보기,글삭제
public class WriteActionController extends AbstractCommandController {
//<property name="commandClass" value="lee.BoardCommand" />
//setCommandClass(BoardCommand command)호출
BoardDAO dao;
public void setDao(BoardDAO dao) {
this.dao = dao;
System.out.println("setDao()호출=>"+dao);
}
//1.request(요청객체),2.response(응답객체) 3.입력받은값을 저장한 객체(Object->다 입력가능하게)
//4.BindException->사용자로부터 값을 입력시 에러발생->예외처리해주는 클래스
@Override
protected ModelAndView handle(HttpServletRequest request,
HttpServletResponse response,
Object command, BindException error)
throws Exception {
// TODO Auto-generated method stub
System.out.println("WriteActionController 실행됨!");
request.setCharacterEncoding("utf-8");//한글처리
BoardCommand data=(BoardCommand)command;
//확인
/* before
String author=data.getAuthor();
String title=data.getTitle();
String content=data.getContent();
*/
/*
* String author=request.getParameter("author");
* BoardCommand command=new BoardCommand();
* command.setAuthor(author);
* ,,,
*/
//최대값 구하기(계산해서 넣을것)
int newNum=dao.getNewNum()+1;
data.setNum(newNum);//5->6->7
dao.write(data);//->getNum()호출해서 #{num}에 전달된다.
return new ModelAndView("redirect:/list.do");
}
}
글상세보기
BoardDAO (인터페이스) 내용추가 => 조회수증가와 글상세보기 둘다 입력해야한다
~생략~
//4-1.조회수 증가
public void updateReadcnt(String num) throws DataAccessException;
//4-2.글상세보기
public BoardCommand retrieve(String num) throws DataAccessException;
Board.xml 내용추가
~생략~
<!-- 4.글상세보기(조회수증가) java.lang.String, (String) ,string(X) -->
<update id="updateReadcnt" parameterType="String">
update springboard set readcnt=readcnt+1 where num=#{num}
</update>
<!-- 5.게시물번호에 따른 레코드 한개 상세보기 -->
<select id="retrieve" parameterType="String" resultType="board">
select * from springboard where num=#{num}
</select>
SqlMapBoardDao 오버라이드 후 내용추가
~생략~
@Override
public void updateReadcnt(String num) throws DataAccessException {
// TODO Auto-generated method stub
//update태그-> 형식)sqlSession객체명.update("실행시킬 id",매개변수명);
getSqlSession().update("updateReadcnt",num);// #{num}
}
@Override
public BoardCommand retrieve(String num) throws DataAccessException {
// TODO Auto-generated method stub
return (BoardCommand)getSqlSession().selectOne("retrieve",num);
}
RetrieveActionController (컨트롤러) 내용 수정
package lee;
import java.util.ArrayList;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
//public class ListAction implements CommandAction{ 페이지이동,클래스는 틀리지만 처리메서드 동일
public class RetrieveActionController implements Controller {
BoardDAO dao;//BoardDAO dao=new BoardDAO(); 스프링컨테이너가 해준다.
public void setDao(BoardDAO dao) {//<property name="dao"><ref bean~
this.dao = dao;
System.out.println("setDao() 호출됨(dao)=>"+dao);
}
@Override
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// TODO Auto-generated method stub
System.out.println("RetrieveActionController의 handleRequest()호출됨");
// /retrieve.do?num=3
String num=request.getParameter("num");
//ArrayList list=dao.list();
//Board data=dao.retrieve(num); before
//after
dao.updateReadcnt(num);//조회수증가
BoardCommand data=dao.retrieve(num);
///////////////////////////////////////////////////////////
ModelAndView mav=new ModelAndView("retrieve");//생성자(이동할페이지명)
mav.addObject("data",data);//request.setAttribute("list",list);
return mav;
}
}
retrieve.jsp 수정
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ page import="lee.*" %>
<%
//Board data=(Board)request.getAttribute("data");//${data}
BoardCommand data=(BoardCommand)request.getAttribute("data");//${data}
//웹상에서 Getter Method
int num=data.getNum();
String title=data.getTitle();
String author=data.getAuthor();
String content=data.getContent();
%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>내용보기 및 수정</title>
</head>
<body>
<form action="update.do">
<!-- readonly="readonly"(읽기전용) -->
번 호 : <input type="text" name="num" value="<%= num %>" readonly="readonly" /><br>
제 목 : <input type="text" name="title" value="<%= title %>"><br>
작성자:<input type="text" name="author" value="<%= author %>"><br>
내 용 : <textarea name="content" rows="5" cols="30"><%= content %></textarea><p/>
<input type="submit" value="수정완료" />
<a href="delete.do?num=<%= num %>">삭제</a>
<a href="list.do">목록보기</a>
</form>
</body>
</html>
글삭제하기, 글수정하기, 검색하기를 한번에 하고자한다.
BoardDAO(인터페이스) 내용추가
~생략~
//5.글삭제하기
public void delete(String num) throws DataAccessException;
//6.글수정하기
public void update(BoardCommand data) throws DataAccessException;
//7.글검색하기(String searchName,String searchValue)
//------------> 1.HashMap이용 2.클래스 작성(BoardCommand)
public List search(BoardCommand data) throws DataAccessException;
SqlMapBoardDao 오버라이드 후 내용추가
~생략~
@Override
public void delete(String num) throws DataAccessException {
// TODO Auto-generated method stub
//형식) sqlSession객체명.delete("실행구문의 id",전달할 매개변수명)
getSqlSession().delete("delete",num); //#{num}
}
@Override
public void update(BoardCommand data) throws DataAccessException {
// TODO Auto-generated method stub
getSqlSession().update("update",data);
}
@Override
public List search(BoardCommand data) throws DataAccessException {
// TODO Auto-generated method stub
return getSqlSession().selectList("search",data);
//data->searchName,searchValue(분리되서 각 테이블의 필드)
}
Board.xml 내용추가
글삭제하기 =>parameterType 사용
글 수정하기 =>parameterType 사용
글검색하기 =>=>parameterType 사용, resultType사용,
=>#을 쓰기않고 $를 사용한다
매개변수가 순수한 값을 입력받는값이 아닌 필드명으로 값을 입력받거나 특수기호가 포함되는 값을 입력받은 경우엔 #{매개변수} 사용불가
=> 매개변수 받는 위치 =>필드명 ,특수기호가 포함되는 경우
#{매개변수} =>${매개변수}을 써야 된다.
~생략~
<!-- 6.글삭제하기 -->
<delete id="delete" parameterType="String">
delete from springboard where num=#{num}
</delete>
<!-- 7.글수정하기 -->
<update id="update" parameterType="board">
update springboard set title=#{title},content=#{content},author=#{author} where num=#{num}
</update>
<!-- 8.글검색하기(검색분야,검색어) -->
<select id="search" parameterType="board" resultType="board">
select * from springboard where ${searchName} like '%${searchValue}%' order by num desc
</select>
DeleteActionController (컨트롤러)
=>수정할게 없음 그대로 사용
package lee;
import java.util.ArrayList;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
//public class ListAction implements CommandAction{ 페이지이동,클래스는 틀리지만 처리메서드 동일
public class DeleteActionController implements Controller {
BoardDAO dao;//BoardDAO dao=new BoardDAO(); 스프링컨테이너가 해준다.
public void setDao(BoardDAO dao) {//<property name="dao"><ref bean~
this.dao = dao;
System.out.println("setDao() 호출됨(dao)=>"+dao);
}
@Override
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// TODO Auto-generated method stub
System.out.println("DeleteActionController의 handleRequest()호출됨");
// /delete.do?num=3
String num=request.getParameter("num");
dao.delete(num);
//화면에 출력할 list.jsp에 전달할 페이지명과 전달할값을 설정하기위해
ModelAndView mav=new ModelAndView();//viewName 멤버변수
mav.setViewName("redirect:/list.do");//삭제->ListActionController->/list.jsp
return mav;
}
}
UpdateActionController (컨트롤러)수정
package lee;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.validation.BindException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractCommandController;
//입력을 받아서 처리(글쓰기,글수정,로그인),입력X 처리해주는경우(페이지 이동(글쓰기폼),글상세보기,글삭제
public class UpdateActionController extends AbstractCommandController {
//<property name="commandClass" value="lee.BoardCommand" />
//setCommandClass(BoardCommand command)호출
BoardDAO dao;
public void setDao(BoardDAO dao) {
this.dao = dao;
System.out.println("setDao()호출=>"+dao);
}
//1.request(요청객체),2.response(응답객체) 3.입력받은값을 저장한 객체(Object->다 입력가능하게)
//4.BindException->사용자로부터 값을 입력시 에러발생->예외처리해주는 클래스
@Override
protected ModelAndView handle(HttpServletRequest request,
HttpServletResponse response,
Object command, BindException error)
throws Exception {
// TODO Auto-generated method stub
System.out.println("UpdateActionController 실행됨!");
request.setCharacterEncoding("utf-8");//한글처리
BoardCommand data=(BoardCommand)command;
////추가(게시물번호가 전달->hidden객체 or 읽기전용 전달 input box이용)
/* before
String num=request.getParameter("num");
//---------------------------------------------
String author=data.getAuthor();
String title=data.getTitle(); String
content=data.getContent();
*/
//after
dao.update(data);
return new ModelAndView("redirect:/list.do");
}
}
SearchActionController (컨트롤러)수정
package lee;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
//public class ListAction implements CommandAction{ 페이지이동,클래스는 틀리지만 처리메서드 동일
public class SearchActionController implements Controller {
BoardDAO dao;//BoardDAO dao=new BoardDAO(); 스프링컨테이너가 해준다.
public void setDao(BoardDAO dao) {//<property name="dao"><ref bean~
this.dao = dao;
System.out.println("setDao() 호출됨(dao)=>"+dao);
}
@Override
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// TODO Auto-generated method stub
System.out.println("SearchActionController의 handleRequest()호출됨");
//추가(검색분야,검색어에 해당하는 레코드만 보여줄 수 있도록)
String searchName=request.getParameter("searchName");//검색분야
String searchValue=request.getParameter("searchValue");//검색어
//----------------------------
//ArrayList list=dao.search(searchName,searchValue); before
//---------------------------------------------------------
BoardCommand data=new BoardCommand();
data.setSearchName(searchName);//검색분야
data.setSearchValue(searchValue);//검색어
List list=dao.search(data);
//----------------------------------------------------------
//화면에 출력할 list.jsp에 전달할 페이지명과 전달할값을 설정하기위해
ModelAndView mav=new ModelAndView();//viewName 멤버변수
mav.setViewName("list");//이동할 페이지명만 확인->경로? 확장자?
mav.addObject("list",list);//request.setAttribute("list",list);
//mav.addObject("키명",전달할값),,,,
//${list(키명)} request.getAttribute("list");
return mav;
}
}
실행은 index.jsp에서 한다
<%
response.sendRedirect("http://localhost:8090/SpringMybatis/list.do");
%>
여기까지 저번에 했던 미니게시판에 Mybatis를 적용하는 과정이다
2022-08-30