티스토리 뷰

Spring Framework

스프링 MVC 프로젝트

VIRGIL ABLOH 2021. 7. 6. 12:02
반응형

영속/비즈니스 계층의 CRUD 구현

코드를 이용해서 데이터에 대한 CRUD 작업을 진행한다.

다음과 같은 순서로 진행한다.

  • 테이블의 칼럼 구조를 반영하는 VO(Value Object) 클래스의 생성
  • MyBatis의 Mapper 인터페이스의 작성/XML 처리
  • 작성한 Mapper 인터페이스의 테스트

위의 과정 전에 먼저 JDBC 연결을 테스트하는 과정을 거치는 것이 좋지만

SQL Developer의 연결 자체가 이미 JDBC 연결되어 있다는 것이므로 이대로 진행한다.

 

영속 예측의 구현 준비

거의 모든 웹 애플리케이션의 최종 목적은 데이터베이스에 데이터를 기록하거나,

원하는 데이터를 가져오는 것이 목적이기 때문에

개발할 때 어느 정도의 설계가 진행되면 데이터 베이스 관련 작업을 하게 된다.

 

VO 클래스의 작성

VO 클래스를 생성하는 작업은 테이블 설계를 기준으로 작성하면 된다.

현재 tbl_board 테이블의 구성은 아래와 같다.

프로젝트에 com.osk2090.domain 패키지를 생성하고,BoardVO 클래스를 정의한다.

BoardVO.class

@Data
public class BoardVO {
    private Long bno;
    private String title;
    private String content;
    private String writer;
    private Date regdate;
    private Date updatedate;
}

BoardVO 클래스는 Lombok을 이용해서 생성자와 getter/setter,toString() 등을 만들어 내는 방식을 사용한다.

이를 위해서 @Data 어노테이션을 적용한다.

 

Mapper 인터페이스와 Mapper XML

MyBatis는 SQL을 처리하는데 어노테이션이나 XML을 이욯할 수 있다.

간단한 SQL이라면 어노테이션을 이용해서 처리하는 것이 무난하지만,

SQL이 점점 복잡해지고 검색과 같이 상황에 따라 다른 SQL문이 처리되는 경우에는

어노테이션은 그다지 유용하지 못하다는 단점이 있다.

XML의 경우 단순 텍스트를 수정하는 과정만으로 처리가 끝나지만,

어노테이션의 경우 코드를 수정하고 다시 빌드하는 등의 유지 보수성이 떨어지는 이유로 기피하는 경우도 종종 있다.

 

Mapper 인터페이스

RootConfig,class에서

@MapperScan(basePackages = {"com.osk2090.mapper"})

com.osk2090.mapper 패키지를 스캔하도록 이미 설정 하였다.

 

Mapper 인터페이스를 작성할 때는 리스트(select)와 등록(insert) 작업을 우선해서 작성한다.

com.osk2090.mapper 패키지를 작성하고,BoardMapper 인터페이스를 추가한다.

 

BoardMapper.interface

public interface BoardMapper {
    
    @Select("select * from tbl_board where bno >0")
    public List<BoardVO> getList();
}

BoardMapper 인터페이스를 작성할 때는 이미 작성된 BoardVO 클래스를 적극적으로 활용해서 필요한 SQL을 어노테이션의 속성값으로 처리할 수 있다.(SQL을 작성할 때는 반드시 ';'이 없도록 작성해야 한다.)

SQL뒤에 'where bno>0'과 같은 조건은 테이블을 검색하는데 bno라는 칼럼 조건을 주어서 Primary key(PK)를 이용하도록 유도하는 조건이다.이 조건문을 SQL Developer에서 확인해서 맞게 출력이 되는지 확인해본다.

SQL Developer에서 먼저 확인하는 이유는

  1. SQL이 문제 없이 실행 가능한지를 확인하기 위한 용도
  2. 데이터베이스의 commit을 하지 않았다면 나중에 테스트 결과가 달라지기 때문에 이를 비교할 수 있도록 하기 위함

작성된 BoardMapper 인터페이스를 테스트 할 수 있게 테스트 환경인 'src/test/java'에 'com.osk2090.mapper'패키지를 작성하고 BoardMapperTests 클래스를 추가한다.

 

필자는 jdbc에서 에러가 뜨는데 아래와 같이 수정하였더니 성공 하였다.

  • 1.log4jdbc.log4j2.properties 생성 후 추가
  • log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
  • 2.pom.xml 의존성 추가하기
    <dependency>
        <groupId>org.bgee.log4jdbc-log4j2</groupId>
        <artifactId>log4jdbc-log4j2-jdbc4</artifactId>
        <version>1.16</version>
    </dependency>
    
    <dependency>
        <groupId>com.oracle.database.jdbc</groupId>
        <artifactId>ojdbc6</artifactId>
        <version>11.2.0.4</version>
    </dependency>

에러 확인후 다시 테스트를 해보면 아래와 같이 콘솔창에 출력된다.

Mapper XML 파일

BoardMapperTests를 이용해서 테스트가 완료되었다면 src/main/resources 내에 패키지와 동일한 com/osk2090/mapper 단계의 폴더를 생성하고 XML 파일을 작성한다.

(폴더를 한 번에 생성하지 말고 하나씩 생성해야 하는 점을 주의해야 한다.)

 

BoardMapper.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="com.osk2090.mapper.BoardMapper">
    <select id="getList" resultType="com.osk2090.domain.BoardVO">
        <![CDATA[
        select *
        from tbl_board
        where bno > 0
]]>
    </select>
    
</mapper>

XML을 작성할 때는 반드시 <mapper>의 namespace 속성값을 Mapper 인터페이스와 동일한 이름을 주는 것에 주의하고,<select> 태그의 id 속성값은 메서드의 이름과 일치하게 작성한다.

resultType 속성의 값은 select 쿼리의 결과를 특정 클래스의 객체로 만들기 위해서 설정한다.

XML에 사용한 CDATA 부분은 XML에서 부등호를 사용하기 위해서 사용한다.

 

XML에 SQL문이 처리되었으니 BoardMapper 인터페이스에 SQL은 제거한다.

    public List<BoardVO> getList();

인터페이스 수정 후에는 반드시 기존의 테스트 코드를 통해서 기존과 동일하게 동작하는지 확인해야 한다.

 

영속 영역의 CRUD 구현

웹 프로젝트 구조에서 마지막 영역이 영속 영역이지만

실제로 구현을 가장 먼저 할 수 있는 영역도 영속 영역이다.

영속 영역은 기본적으로 CRUD 작업을 하기 때문에

테이블과 VO(DTO) 등 약간의 준비만으로도 비즈니스 로직과 무관하게 CRUD 작업을 작성할 수 있다.

MyBatis는 내부적으로 JDBC의 PreparedStatement를 활용하고

필요한 파라미터를 처리하는 '?'에 대한 치환은 '#{속성}'을 이용해서 처리한다.

 

create(insert) 처리

tbl_board 테이블은 PK 칼럼으로 bno를 이용하고,

시퀸스를 이용해서 자동으로 데이터가 추가될때 번호가 만들어지는 방식을 사용한다.

이처럼 자동으로 PK 값이 정해지는 경우에는 다음과 같은 2가지 방식으로 처리할 수 있다.

  • insert만 처리되고 생성된 PK 값을 알 필요가 없는 경우
  • insert문이 실행되고 생성된 PK 값을 알아야 하는 경우

BoardMapper 인터페이스에는 위의 상황들을 고려해서 다음과 같이 메서드를 추가 선언한다.

 

BoardMapper.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="com.osk2090.mapper.BoardMapper">
    <select id="getList" resultType="com.osk2090.domain.BoardVO">
        <![CDATA[
        select *
        from tbl_board
        where bno > 0
]]>
    </select>

    <insert id="insert">
        insert into tbl_board (bno, title, content, writer)
        values (seq_board.nextval, #{title}, #{content}, #{writer})
    </insert>

    <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>
    
</mapper>

BoardMapper의 insert()는 단순히 시퀸스의 가음 값을 구해서 insert 할 때 사용한다.

insert문은 몇 건의 데이터가 변경되었는지만을 알려주기 때문에 추가된 데이터의 PK 값을 알 수는 없지만,

1번의 SQL 처리만으로 작어비 완료되는 장점이 있다.

 

insertSelectKey()는 @SelectKey라는 MyBatis의 어노테이션을 이용한다.

@SelectKey는 주로 PK 값을 미리(before) SQL을 통해서 처리해 두고 특정한 이름으로 결과를 보관하는 방식이다.

@Insert 할 때 SQL문을 보면 #{bno}와 같이 이미 처리된 결과를 이용하는 것을 볼 수 있다.

 

우선 insert()에 대한 테스트 코드를 src/test/java 내에 BoardMapperTests 클래스에 새로운 메서드로 작성해보면

다음과 같이 작성할 수 있다.

 

BoardMapperTests.class

@Test
    public void testInsert() {
        BoardVO board = new BoardVO();
        board.setTitle("새로 작성하는 글");
        board.setContent("새로 작성하는 내용");
        board.setWriter("newbie");

        mapper.insert(board);

        log.info(board);
    }

테스트 코드의 마지막에 log.into(board)를 작성한 이유는

Lombok이 만들어주는 toString()을 이용해서 bno 멤버 변수(인스턴스 변수)의 값을 알아보기 위함이다.

...
INFO : jdbc.audit - 1. PreparedStatement.setString(1, "새로 작성하는 글") returned 
INFO : jdbc.audit - 1. PreparedStatement.setString(2, "새로 작성하는 내용") returned 
INFO : jdbc.audit - 1. PreparedStatement.setString(3, "newbie") returned 
INFO : jdbc.sqlonly - insert into tbl_board (bno, title, content, writer) values (seq_board.nextval, '새로 작성하는 글', 
'새로 작성하는 내용', 'newbie') 

INFO : jdbc.sqltiming - insert into tbl_board (bno, title, content, writer) values (seq_board.nextval, '새로 작성하는 글', 
'새로 작성하는 내용', 'newbie') 
 {executed in 5 msec}
...

제대로 들어갔는지 테스트 해본다.

|----|----------|-----------|-------|----------------------|----------------------|
|bno |title     |content    |writer |regdate               |updatedate            |
|----|----------|-----------|-------|----------------------|----------------------|
|1   |테스트 제목    |테스트 내용     |user00 |2021-07-04 16:24:09.0 |2021-07-04 16:24:09.0 |
|21  |새로 작성하는 글 |새로 작성하는 내용 |newbie |2021-07-05 16:33:16.0 |2021-07-05 16:33:16.0 |
|22  |새로 작성하는 글 |새로 작성하는 내용 |newbie |2021-07-05 16:36:17.0 |2021-07-05 16:36:17.0 |
|----|----------|-----------|-------|----------------------|----------------------|

@SelectKey를 이용하는 경우 테스트 코드는 다음과 같다.

@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);
    }

 

testInsertSelectKey()의 테스트 결과의 일부는 다음과 같다.

|--------|
|nextval |
|--------|
|23      |
|--------|

INFO : jdbc.resultset - 1. ResultSet.next() returned false
INFO : jdbc.resultset - 1. ResultSet.close() returned void
INFO : jdbc.audit - 1. Connection.getMetaData() returned oracle.jdbc.driver.OracleDatabaseMetaData@28c88600
INFO : jdbc.audit - 1. PreparedStatement.isClosed() returned false
INFO : jdbc.audit - 1. PreparedStatement.close() returned 
INFO : jdbc.audit - 1. PreparedStatement.new PreparedStatement returned 
INFO : jdbc.audit - 1. Connection.prepareStatement(insert into tbl_board (bno, title, content, writer)
        values (?, ?, ?, ?)) returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@607b2792
INFO : jdbc.audit - 1. PreparedStatement.setLong(1, 23) returned 
INFO : jdbc.audit - 1. PreparedStatement.setString(2, "새로 작성하는 글 select key") returned 
INFO : jdbc.audit - 1. PreparedStatement.setString(3, "새로 작성하는 내용 select key") returned 
INFO : jdbc.audit - 1. PreparedStatement.setString(4, "newbie") returned 
INFO : jdbc.sqlonly - insert into tbl_board (bno, title, content, writer) values (23, '새로 작성하는 글 select key', '새로 
작성하는 내용 select key', 'newbie') 

INFO : jdbc.sqltiming - insert into tbl_board (bno, title, content, writer) values (23, '새로 작성하는 글 select key', '새로 
작성하는 내용 select key', 'newbie') 
 {executed in 2 msec}

실행되는 결과를 살펴보면 'selecr 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 클래스의 bno 타입 정보를 이용해서 처리한다.

 

BoardMapper.interface

public BoardVO read(Long bno);

BoardMapper.xml

</select>
    <select id="read" resultType="com.osk2090.domain.BoardVO">
    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 파라미터명()을 사용하지 않고 처리된다.

 

현재 테이블에 존재하는 데이터의 bno 칼럼의 값을 이용해서 테스트 코드를 통해 확인할 수 있다.

@Test
    public void testRead() {
        BoardVO board = mapper.read(30L);
        log.info(board);
    }

mapper,read()를 호출할 경우에는 현재 테이블에 있는 데이터의 bno 값이 존재하는지 여부를 반드시 확인해야 한다.

테스트 코드의 결과

INFO : com.osk2090.mapper.BoardMapperTest - BoardVO(bno=30, title=새로 작성하는 글 select key, content=새로 작성하는 내용 select key, writer=newbie, regdate=Tue Jul 06 11:17:28 KST 2021, updatedate=Tue Jul 06 11:17:28 KST 2021)
INFO : jdbc.connection - 9. Connection opened
INFO : jdbc.audit - 9. Connection.new Connection returned 
INFO : jdbc.audit - 9. Connection.setReadOnly(false) returned 
INFO : jdbc.audit - 9. Connection.setAutoCommit(true) returned 
INFO : org.springframework.context.support.GenericApplicationContext - Closing org.springframework.context.support.GenericApplicationContext@79b06cab: startup date [Tue Jul 06 11:17:45 KST 2021]; root of context hierarchy
INFO : com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...

 

delete 처리

특정한 데이터를 삭제하는 작업 역시 PK 값을 이용해서 처리하므로 조회하는 작업과 유사하게 처리한다.

등록,삭제,수정과 같은 DML 작업은 '몇 건의 데이터가 삭제(혹은 수정)되었는지'를 반환할 수 있다.

 

BoardMapper.interface

public int delete(Long bno);

BoardMapper.xml

<delete id="delete">
        delete
        from tbl_board
        where bno = #{bno}
</delete>

 

delete()의 메서드 리턴 타입은 int로 지정해서 만일 정상적으로 데이터가 삭제되면 1 이상의 값을 가지도록 작성한다.

테스트 코드는 현재 테이블에 존재하는 번호의 데이터를 삭제해 보고 '1'이라는 값이 출력되는지 확인한다.

만일 해당 번호의 게시물이 없다면 '0'이 출력된다.

BoardMapperTest.class

@Test
public void testDelete() {
	log.info("DELETE COUNT: " + mapper.delete(28L));
}

testDelete()의 경우 28번 데이터가 존재했으므로 1이 출력된다.그러므로 해당 숫자의 데이터는 삭제되었다.

INFO : jdbc.sqltiming - delete from tbl_board where bno = 28 
 {executed in 5 msec}
INFO : jdbc.audit - 1. PreparedStatement.execute() returned false
INFO : jdbc.audit - 1. PreparedStatement.getUpdateCount() returned 1
INFO : jdbc.audit - 1. PreparedStatement.isClosed() returned false
INFO : jdbc.audit - 1. PreparedStatement.close() returned 
INFO : jdbc.connection - 8. Connection opened
INFO : jdbc.audit - 8. Connection.new Connection returned 
INFO : jdbc.audit - 8. Connection.setReadOnly(false) returned 
INFO : jdbc.audit - 8. Connection.setAutoCommit(true) returned 
INFO : jdbc.audit - 1. Connection.clearWarnings() returned 
INFO : com.osk2090.mapper.BoardMapperTest - DELETE COUNT: 1

 

update 처리

마지막으로 update를 처리한다.게시물의 업데이트는 제목,내용,작성자를 수정한다고 가정한다.

업데이트할 때는 최종 수정시간을 데이터베이스 내 현재 시간으로 수정한다.

Update는 delete와 마찬가지로 '몇 개의 데이터가 수정되었는가'를 처리할 수 있게 int 타입으로 메서드를 설계할 수 있다.

 

BoardMapper.interface

public int update(BoardVO boardVO);

 

BoardMapper.xml

<update id="update">
        update tbl_board
        set title=#{title},
            content=#{content},
            writer=#{writer},
            updateDate=sysdate
        where bno = #{bno};
</update>

 

SQL에서 주의 깊게 봐야하는 부분으 update 칼럼이 최종 수정시간을 의미하는 칼럼이기 때문에 현재 시간으로 변경해 주고 있다는 점과,regdate 칼럼으니 최초 생성 시간이므로 건드리지 않는다는 점이다.

#{title}과 같은 부분은 파라미터로 전달된 BoardVO 객체의 getTtlte()과 같은 메서드들을 호출해서 파라미터들이 처리된다.

테스트 코드는 read()를 이용해서 가져온 BoardVO 객체의 일부를 수정하는 방식이나 직접 BoardVO 객체를 생성해서 처리할 수 있다.

 

BoardMapperTests.class

@Test
public void testUpdate() {
    BoardVO boardVO = new BoardVO();
    boardVO.setBno(23L);
    boardVO.setTitle("수정된 제목");
    boardVO.setContent("수정된 내용");
    boardVO.setWriter("user00");

    int count = mapper.update(boardVO);
    log.info("UPDATE COUNT: " + count);
}
INFO : jdbc.sqlonly - update tbl_board set title='수정된 제목', content='수정된 내용', writer='user00', updateDate=sysdate 
where bno = 23 

INFO : jdbc.connection - 8. Connection opened
INFO : jdbc.audit - 8. Connection.new Connection returned 
INFO : jdbc.audit - 8. Connection.setReadOnly(false) returned 
INFO : jdbc.audit - 8. Connection.setAutoCommit(true) returned 
INFO : jdbc.sqltiming - update tbl_board set title='수정된 제목', content='수정된 내용', writer='user00', updateDate=sysdate 
where bno = 23 
 {executed in 6 msec}
INFO : jdbc.audit - 1. PreparedStatement.execute() returned false
INFO : jdbc.audit - 1. PreparedStatement.getUpdateCount() returned 1
INFO : jdbc.audit - 1. PreparedStatement.isClosed() returned false
INFO : jdbc.audit - 1. PreparedStatement.close() returned

 

반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2026/06   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함