-
[3-ch8 스프링] 영속/비지니스 계층의 CRUD 구현, Mapper 인터페이스와 xmlBack-End/Spring Legacy 2022. 8. 19. 10:41
VO 클래스 작성
VO 클래스를 생성하는 작업은 테이블 설계를 준으로 작성하면 된다. tbl_board 테이블의 구성대로 작성한다.
프로젝트에 org.zerock.domain 패키지를 생성하고 BoardVO 클래스를 정의한다.
package org.zerock.domain; import java.util.Date; import lombok.Data; @Data public class BoardVO { private Long bno; private String title; private String content; private String writer; private Date regDate; private Date updateDate; }
Mapper 인터페이스와 Mapper XML
MyBatis는 SQL을 처리하는 어노테이션이나 XML을 이용할 수 있다. 간단한 SQL이라면 어노테이션을 이용해서 처리하는 것이 무난하겠지만, SQL이 점점 복잡해지고 검색과 같이 상황에 따라 다른 SQL문이 처리되는 경우 어노테이션은 그다지 유용하지 못하다는 단점이 있다. XML의 경우 단순 텍스트를 수정하는 과정만으로 처리가 끝났지만 어노테이션의 경우 코드를 수정하고 다시 빌드 하는 등의 유지 보수성이 떨어지는 이유로 기피하는 경우도 종종 있다.
그렇다면, Mapper 인터페이스를 작성하는 이유는?
일단) Mapper 인터페이스는 Mapping 파일에 기재된 SQL을 호출하기 위한 인터페이스이다. 따라서 Mapping 파일에 있는 SQL을 자바 인터페이스를 통해 호출할 수 있도록 해준다. Mapper 인터페이스를 사용하면 mapper.xml에서( src/main/resources, db관련 xml 파일에서 <mapper namespace 속성 값을 Mapping 파일이 있는 경로를 넣어주면 된다.
그 후 DAO에서 사용할 때 BoardMapper 객체를 생성해 주고 거기에 mapper.xml에서 지정해둔 select 태그 속성갑 중 id 값을 입력하면 된다.
@Autowired
private BoardMapper boardmapper;
@Overiide
public boid insert(BoardVO board){
boardmapper.insert(board);
}만약 Mapper 인터페이스를 사용하지 않았다면?
mapper.xml에서 mapper namespace 값을 임의의 문자열로 지정해준 다음
SqlSession 클래스 객체를 생성해준 뒤 거기에다가 필요한 sqlSession 클래스에서 제공하는 메서드의 인자값으로 mapper.xml의 namespace 값.원하는 쿼리문 id값을 입력해주면 된다. 이렇게 하는 방법은 문자열의 오타때문에 에러가 발생할 수 있기 때문에 Mapper 인터페이스를 사용했다.
Mapper 인터페이스
root-context.xml 아래 부분 mybatis-spring:scan 태그에 org.zerock.mappr 파일을 값으로 넣어서 스프링 프레임워크에서 관리해야 하는 객체를 설정해준다.
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mybatis-spring="http://mybatis.org/schema/mybatis-spring" xsi:schemaLocation="http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring-1.2.xsd http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- Root Context: defines shared resources visible to all other web components --> <bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig"> <property name="driverClassName" value="net.sf.log4jdbc.sql.jdbcapi.DriverSpy"></property> <property name="jdbcUrl" value ="jdbc:log4jdbc:oracle:thin:@localhost:1521:XE"></property> <property name="username" value="book_ex"></property> <property name="password" value="book_ex"></property> </bean> <!-- HikariCP configuration --> <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close"> <constructor-arg ref="hikariConfig" /> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"></property></bean> <mybatis-spring:scan base-package="org.zerock.mapper" /> </beans>
Mapper 인터페이스를 작성할 때는 리스트(select)와 등록(insert) 작업을 우선해서 작성한다. org.zerock.mapper 패키지를 작성하고, BoardMapper 인터페이스를 추가한다.
package org.zerock.mapper; import java.util.List; import org.zerock.domain.BoardVO; public interface BoardMapper { @Select("select * from tbl_board where bno>0") public List<BoardVO> getList(); }
BoardMapper 인터페이스를 작성할 떄는 이미 만들어진 BoardVO 클래스를 적극 활용해 필요한 SQL 어노테이션의 속성값으로 처리할 수 있다. SQL 뒤에 'where bno > 0'과 같은 조건은 테이블을 검색하는데 bno라는 칼럼 조건을 주어서 Primary key를 이용하도록 유도하는 조건이다.
작성된 BoardMapper 인터페이스를 테스트 하기 위해 테스트 환경인 'src/test/java'에 'org.zerock.mapper' 패키지를 작성하고 BoardMapperTests 클래스를 추가한다.
package org.zerock.mapper; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import lombok.Setter; import lombok.extern.log4j.Log4j2; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml") @Log4j2 public class BoardMapperTests { @Setter(onMethod_ = @Autowired) private BoardMapper mapper; @Test public void testGetList() { mapper.getList().forEach(board -> log.info(board)); } }
BoardMapperTests 클래스는 스프링 이용해 BoardMapper 인터페이스의 구현체를 주입받아서 동작하게 한다. testGetList()의 결과는 sql developer에서 실행된 것과 동일해야만 정상적으로 동작한 것이다.
Mapper XML 파일
BoardMapperTests를 이용해 테스트가 완료되었다면 src/main/resources내에 패키지와 동일한 org/zerock/mapper 단계의 폴더를 생성하고 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="org.zerock.mapper.BoardMapper"> <select id="getList" resultType="org.zerock.domain.BoardVO"> <![CDATA[ select * from tbl_board where bno>0 ]]> </select> </mapper>
XML을 작성할 때 반드시 <mapper>의 namespace 속성값을 Mapper 인터페이스와 동일한 이름을 주는 것에 주의하고 <select> 태그의 id 속성값은 메서드의 이름과 일치하게 작성한다.
xml에 sql 문이 처리 되었으므로 BoardMapper 인터페이스의 @select 어노테이션은 제거한다. 이렇게 mapper.xml을 작성하고 위의 인터페이스에서 어노테이션을 이용한 값과 동일한 값이 나오는지 테스트 코드를 통해 실행해본다.
영속 영역의 CRUD 구현
영속 영역은 기본적으로 CRUD 작업을 하기 때문에 테이블과 VO 등 약간의 준비만으로도 비즈니스 로직과 무관하게 CRUD 작업을 작성할 수 있다. MyBatis는 내부적으로 JDBC의 PreparedStatement를 활용하고 필요한 파라미터를 처리하는 '?'에 대한 치환은 '#{속성}'을 이용해서 처리한다. 예를들어 values(seq_board.nextval,?,?,?) 이렇게 처리하고 그 값에 getter 메서드가 실행되면서 title, content, writer 등등 값을 읽어오는 것. 아래 설명할 내용을 보면 이해할 수 있다.
create(insert) 처리
tbl_board 테이블은 PK 칼럼으로 bno를 이용하고, 시퀀스를 이용해서 자동으로 데이터가 추가될 때 번호가 만들어지는 방식을 사용한다. 이렇게 PK 값이 정해지는 경우에는 2가지 방식으로 처리할 수 있다.
- Insert만 처리되고 생성된 PK값을 알 필요가 없는 경우
- Insert문이 실행되고 생성된 PK 값을 알아야 하는 경우BoardMapper 인터페이스에는 위의 상황들을 고려해서 다음과 같이 메서드를 추가 선언한다.
public void insert(BoardVO board); public void insertSelectKey(BoardVO board);
BoardMapper.xml은 다음과 같은 내용 추가한다.
<!-- 게시글 추가 --> <insert id="insert"> <![CDATA[ insert into tbl_board (bno, title, content, writer) values (seq_board.nextval, #{title}, #{content}, #{writer}) ]]> </insert> <!-- pk값을 미리 sql을 통해 처리해 두고 특정 이름으로 지정 bno 같이 --> <insert id="insertSelectKey"> <selectKey keyProperty="bno" order="BEFORE" resultType="long"> select seq_board.nextval from dual </selectKey> insert into tbl_board (bno, title, content, writer) values (#{bno}, #{title}, #{content}, #{writer}) </insert>
BoardMapper의 insert()는 단순히 시퀀스의 다음 값을 구해서 insert 할 때 사용한다. insert문은 몇 건의 데이터가 변경되었는지만을 알려주기 때문에 추가된 PK 값을 알 수 없지만, 1번의 처리만으로 작업이 완료되는 장점이 있다.
insertSelectKey()는 @SelectKey라는 MyBatis의 어노테이션을 이용한다. 주로 pk값을 미리(before) SQL을 통해 처리해 두고 특정한 이름으로 결과를 보관하는 방식이다. @Insert할 때 SQL문을 보면 #{bno}와 같이 이미 처리된 결과를 이용하는 것을 볼 수 있다.
<BoardMappersTests>
@Test public void testInsert() { BoardVO board = new BoardVO(); board.setTitle("새로 작성하는 글"); board.setContent("새로 작성 내용"); board.setWriter("newbie"); mapper.insert(board); log.info(board); }
테스트 결과의 마지막을 살펴보면 BoardVO 클래스의 toString() 결과가 출력되는 것을 볼 수 있는데, bno 값이 null로 비어있는 걸 확인할 수 있다.
@SelectKey를 이용하는 경우 다음과 같다.
<BoardMappersTests>
@Test public void testInsertSelectKey() { BoardVO board = new BoardVO(); board.setTitle("새로 작성하는 글 select key"); board.setContent("새로 작성 내용 select key"); board.setWriter("newbie"); mapper.insertSelectKey(board); log.info(board); }
실행되는 결과를 살펴보면 'select seq_board.nextval from dual'과 같은 쿼리가 먼저 실행되고 여기서 생성된 결과를 이용해 bno 값으로 처리되는 것을 볼 수 있다. BoardMapper의 insertSelectKey()의 @Insert 문의 SQL을 보면
insert into tbl_board (bno, title, content, writer)
values (#{bno}, #{title}, #{content}, #{writer})와 같이 파라미터로 전달되는 BoardVO의 bno 값을 사용하게 되어 있다. 테스트 코드의 마지막 부분을 보면 BoardVO 객체의 bno 값이 이전과 달리 지정된 것을 볼 수 있다. @SelectKey를 이용하는 방식은 SQL을 한 번 더 실행하는 부담이 있기는 하지만 자동으로 추가되는 PK 값을 확인해야 하는 상황에서 유용하게 사용될 수 있다.
read(select) 처리
insert가 된 데이터를 조회하는 작업은 PK를 이용해서 처리하므로 BoardMapper 파라미터 역시 BoardVO 클래스의 타입 정보를 이용해서 처리한다.
<BoardMapper 인터페이스>
public BoardVO read(Long bno);
<BoardMapper.xml>
<!-- read(select)처리 원하는 게시글 조회--> <select id="read" resultType="org.zerock.domain.BoardVO"> <![CDATA[ select * from tbl_board where bno =#{bno} ]]> </select>
MyBatis는 Mapper 인터페이스의 리턴 타입에 맞게 select의 결과를 처리하기 때문에 tbl_board의 모든 칼럼은 BoardVO의 'bno, title, content, writer, regdate, updateDate' 속성값으로 처리된다. MyBatis는 bno라는 칼럼이 존재하면 인스턴스의 'setBno()'를 호출하게 된다. MyBatis의 모든 파라미터와 리턴 타입의 처리는 get 파라미터명(), set 칼럼명()의 규칙으로 호출된다. 다만 위와 같이 #{속성}이 1개만 존재하는 경우 별도의 get() 파라미터명을 사용하지 않고 처리된다.
<BoardMapperTests>
@Test public void testRead() { //존재하는 게시글 번호로 테스트 BoardVO board = mapper.read(6L); log.info(board); }
mapper.read()를 호출할 경우 현재 테이블에 있는 데이터의 bno 값이 존재하는지 여부를 반드시 확인해야 한다.
delete 처리
특정한 데이터를 삭제하는 작업 역시 PK 값을 이용해서 처리하므로 조회하는 작업과 유사하게 처리된다. 등록, 삭제, 수정과 같은 DML 작업은 '몇 견의 데이터가 삭제 OR 수정 되었는지 반환할 수 있다.
<BoardMapper 인터페이스>
public int delete(Long bno);
<BoardMapper.xml>
<delete id="delete"> delete from tbl_board where bno=#{bno} </delete>
delete()의 메서드 리턴 타입은 int로 지정해서 만일 정상적으로 데이터가 삭제되면 1 이상의 값을 가지도록 작성한다. 테스트 코드는 현재 테이블에 존재하는 번호의 데이터를 삭제해 보고 '1'이라는 값이 출력되는지 확인해야 한다.
<BoardMapperTests>
@Test public void testDelete() { log.info("delete count : " + mapper.delete(3L)); }
testDelete()에서 3번 데이터가 존재했다면 다음과 같은 로그가 기록된다.
만약 이미 3번 게시글을 삭제한 상태에서 testDelete()를 한 번 더 실행시킨다면 delete count 값은 당연하게 0이 나오는 것도 확인 가능하다.
Update 처리
게시물의 업데이트는 제목, 내용을 수정한다고 가정해본다. 업데이트할 때 최종 수정시간을 데이터베이스 내 현재 시간으로 수정한다. Update는 delete와 마찬가지로 '몇 개의 데이터가 수정되었는가'를 처리할 수 있게 int 타입으로 메서드를 설계할 수 있다.
<BoardMapper 인터페이스>
public int update(BoardVO board);
<BoardMapper.xml>
<!-- 게시글 수정 --> <update id="update"> update tbl_board set title =#{title}, content = #{content}, updateDate = sysdate where bno =#{bno} </update>
SQL에서 주의 깊게 봐야 하는 부분은 update 칼럼이 최중 수정시간을 의미하는 칼럼이기 때문에 현재 시간으로 변경해주고, redirect 칼럼은 최초 생성 시간이므로 건드리지 않는다는 것이다. #{title}과 같은 부분은 파라미터로 전달된 BoardVO객체의 getTitle()과 같은 메서드들을 호출해서 파라미터들이 처리된다.
테스트 코드는 read()를 이용해서 가져온 BoardVO 객체의 일부를 수정하는 방식이나 직접 BoardVO 객체를 생성해서 처리할 수 있다. 주의할 점은 setBno의 값도 꼭 입력해줘야 한다.!!
<BoardMapperTests 클래스>
@Test public void testUpdate() { BoardVO board = new BoardVO(); board.setBno(5L); board.setTitle("수정"); board.setContent("수정된 내용"); int count = mapper.update(board); log.info("update count : " +count); }
만약 데이터베이스에 5번 글이 존재했다면 다음과 같은 로그들이 출력된다. updatedate 변경사항도 확인해보자.
반응형'Back-End > Spring Legacy' 카테고리의 다른 글
[Spring] GET/POST 두 방식의 특징과 차이점 알아보기 (0) 2022.08.23 [3-ch9 비즈니스 계층] Service의 생성과 설정 (0) 2022.08.19 [3-ch7 스프링 MVC] 프로젝트 기본 구성, DB 더미 테이블 생성 (0) 2022.08.17 [2-ch6 스프링] MVC의 Controller 어노테이션 (@RequestMapping, @requestParam, Model, 반환타입) (0) 2022.08.16 [2-ch5 스프링 MVC 패턴] 기본구조와 로딩구조 (DispatcherServlet, listener, ViewResolver) (0) 2022.08.15