Spring book

유요한·2022년 12월 29일
0

Spring

목록 보기
15/15
post-thumbnail

영속 계층의 구현 준비

VO 클래스의 작성

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

package com.example.domain;

import lombok.Data;

import java.util.Date;

@Data
public class BoardVO {
    private Long boardNum;
    private String boardTitle;
    private String boardContents;
    private String userId;
    private Date regDate;
    private  Date updateDate;
}

1. DAO(Data Access Object)

DAO는 실제로 DB의 data에 접근하기 위한 객체입니다.

  • 실제로 DB에 접근하여 data를 삽입, 삭제, 조회, 수정 등 CRUD 기능을 수행합니다.
  • Service와 DB를 연결하는 고리 역할을 합니다.
  • Repository package가 바로 DAO입니다.

2. DTO(Data Transfer Object)

DTO는 계층 간 데이터 교환을 하기 위해 사용하는 객체로, DTO는 로직을 가지지 않는 순수한 데이터 객체(Java Beans)입니다.

  • DTO는 즉, getter/setter 메서드만 가진 클래스를 의미합니다.
  • DB에서 데이터를 얻어서 Service나 Controller 등으로 보낼 때 사용합니다.
  • 즉 엔티티를 DTO 형태로 변환한 후 사용합니다.

3. VO(value Object)

VO는 DTO와 달리 Read-Only속성을 지닌 값 오브젝트입니다. DTO는 setter를 가지고 있어서 값이 변할 수 있지만 VO의 경우에는 getter만 가지고 있어서 수정이 불가능합니다.

DTO와 VO의 차이점은 DTO는 인터턴스 개념이고, VO는 리터럴 값 개념입니다. VO는 값들에 대해 Read-Only를 보장해줘야 존재의 신뢰성이 확보되지만 DTO의 경우에는 단지 데이터를 담는 그릇의 역할일 뿐 값은 그저 전달되어야 할 대상일 뿐입니다. 따라서 값 자체에 의미가 있는 VO와 전달될 데이터를 보존해야 하는 DTO의 특성상 개념이 다릅니다.

따라서 VO의 핵심은 두 객체의 모든 필드 값들이 동일하면 두 객체는 같다입니다. 따라서 완전히 값 자체 표현 용도로만 사용하는 게 목적이라면, 두 객체의 모든 필드 값들이 모두 같으면 같은 객체이도록 만드는 것(equals() 와 hashCode()의 오버라이딩)이 중요하지, 메소드는 어떤 메소드가 있든 말든 상관 없습니다.


MyBatis

xxxmapper.xml을 만들때 패키지를 생성해야하는데 한번에 폴더로 만들게 하면 하나의 폴더로 만들어지므로 하나씩 만들어야한다. XML을 작성할 때는 반드시 <mapper>의 namespace 속성값을 Mapper 인터페이스와 동일한 이름을 주는 것에 주의하고, <select> 태그의 id 속성값은 메서드의 이름과 일치하게 작성합니다. resultType 속성의 값은 select 쿼리의 결과를 특정 클래스의 객체로 만들기 위해서 설정합니다. XML에 사용한 CDATA 부분은 XML에서 부등호를 사용하기 위해 사용합니다.

package com.example.mapper;

import com.example.domain.BoardVO;
import org.apache.ibatis.annotations.Select;

import java.util.List;

public interface BoardMapper {

    public List<BoardVO> getList();
}
<?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.example.mapper.BoardMapper">
    <select id="getList" resultType="com.example.domain.BoardVO">
        <![CDATA[
            select * from board where boardNum > 0
    ]]>
    </select>
</mapper>
package com.example.persistance;

import com.example.mapper.BoardMapper;
import lombok.Setter;
import lombok.extern.log4j.Log4j;
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;

@RunWith(SpringJUnit4ClassRunner.class)
@Log4j
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
public class BoardMapperTests {
    @Setter(onMethod_ = @Autowired)
    private BoardMapper mapper;

    @Test
    public void GetListTest() {
//        log.info(mapper.getList());
        mapper.getList().forEach(board -> log.info(board));
    }
}

영속 영역의 CRUD 구현

웹 프로젝트 구조에서 마지막 영역이 영속 영역이지만, 실제로 구현을 가장 먼저 할 수 있는 영역도 영속 영역입니다. 영속 영역은 기본적으로 CRUD 작업을 하기 때문에 테이블과 VO(DTO) 등 약간의 준비만으로도 비즈니스 로직과 무관하게 CRUD 작업을 작성할 수 있습니다. MyBatis는 내부적으로 JDBC의 PrepareStatement를 활용하고 필요한 파라미터를 처리하는 ?에 대한 치환은 #{속성}을 이용해서 처리합니다.

create(insert) 처리

board 테이블을 보면 boardNum은 데이터가 추가될 때 자동으로 번호가 만들어지는 방식을 사용합니다. 이처럼 자동으로 PK 값이 정해지는 경우에는 다음과 같은 2가지 방식으로 처리할 수 있습니다.

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

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

    public void insert(BoardVO board);

    public void insertSelectKey(BoardVO board);
<insert id="insert">
       insert into board (boardTitle, boardContents, userId) VALUES (#{boardTitle},#{boardContents}, #{userId})
   </insert>
   <insert id="insertSelectKey">
       <selectKey keyProperty="boardNum" order="BEFORE" resultType="long">
           select boardNum from board
       </selectKey>

       insert into board (boardTitle, boardContents, userId) VALUES (#{boardTitle},#{boardContents}, #{userId})
   </insert>

select key 엘리먼트 속성

  • keyProperty: selectKey구문의 결과가 셋팅될 대상 프로퍼티.

  • keyColumn: 리턴되는 결과셋의 칼럼명은 프로퍼티에 일치한다. 여러개의 칼럼을 사용한다면 칼럼명의 목록은 콤마를 사용해서 구분한다.

  • resultType: 결과의 타입. 마이바티스는 이 기능을 제거할 수 있지만 추가하는게 문제가 되지는 않을것이다. 마이바티스는 String을 포함하여 키로 사용될 수 있는 간단한 타입을 허용한다.

  • order: BEFORE 또는 AFTER를 셋팅할 수 있다. BEFORE로 설정하면 키를 먼저 조회하고 그 값을 keyProperty 에 셋팅한 뒤 insert 구문을 실행한다. AFTER로 설정하면 insert 구문을 실행한 뒤 selectKey 구문을 실행한다. 오라클과 같은 데이터베이스에서는 insert구문 내부에서 일관된 호출형태로 처리한다.

  • statementType: STATEMENT, PREPARED 또는 CALLABLE중 하나를 선택할 수 있다. 마이바티스에게 Statement, PreparedStatement 또는 CallableStatement를 사용하게 한다. 디폴트는 PREPARED 이다.

사용이유

SQL 수행작업 중 insert된 이후에 알 수 있는 값 또는, 생성된 값을 바로 가져와서 select 쿼리를 보내야 하는 경우가 있다. 주로 생성하고 난 후의 인덱스(번호)를 가져와 작업해야 하는 상황에서 많이 사용한다. 이런경우 java에서 insert 쿼리를 실행하고 값을 받은 후 값을 가지고 다시 쿼리를 DB에 전송하는 방법이 있다. 하지만, 불필요한 여러번의 DB 입출력은 시간이 느려진다는 단점이 있다. 그때, selectKey를 사용하여 바로 적용할 수 있다.

read(select) 처리

insert가 된 데이터를 조회하는 작업은 PK를 이용해서 처리하므로 BoardMapper의 파리미터 역시 BoardVO 클래스의 boardNum 타입 정보를 이용해서 처리합니다.

public BoardVO read(long boardNum);
   <select id="read" resultType="com.example.domain.BoardVO">
        select * from board where boardNum = #{boardNum}
    </select>

Mybatis는 Mapper 인터페이스의 리턴 타입에 맞게 select의 결과를 처리하기 때문에 board의 모든 칼럼은 BoardVO의 속성값으로 처리됩니다. 좀 더 엄밀히 말하면 Mybatis는 boardNum라는 칼럼이 존재하면 인스턴스의 setBoardNum()를 호출하게 됩니다. Mybatis는 모든 파라미터와 리턴 타입의 처리는 get 파라미터명(), set컬럼명()의 규칙으로 호출됩니다. 다만 위와 같이 #{속성}이 1개만 존재하는 경우에는 별도의 get파라미터명()을 사용하지 않고 처리됩니다.

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

delete 처리

특정한 데이터를 삭제하는 작업 역시 PK 값을 이용해서 처리하므로 조회하는 작업과 유사하게 처리합니다. 등록, 삭제, 수정과 같은 DML 작업은 몇 건의 데이터가 삭제(혹은 수정)되었는지를 반환할 수 있습니다.

public  int delete(long boardNum);
    <delete id="delete">
        delete from board where boardNum = #{boardNum}
    </delete>

delete()의 메서드 리턴 타입은 int로 지정해서 만일 정상적으로 데이터가 삭제되면 1 이상의 값을 가지도록 작성합니다. 테스트 코드는 현재 테이블에 존재하는 번호의 데이터를 삭제해 보고 '1'이라는 값이 출력되는지 확인합니다. 만일 해당 번호의 게시물이 없다면 '0'이 출력됩니다.

   @Test
    public void  DeleteTest() {
        log.info("delete count : " + mapper.delete(3L));
    }

update 처리

마지막으로 update를 처리합니다. 게시물의 업데이트는 제목, 내용, 작성자를 수정한다고 가정합니다. 업데이터할 때는 최종 수정시간을 데이터베이스 내 현재 시간으로 수정합니다. Update는 delete와 마찬가지로 몇 개의 데이터가 수정되었는가를 처리할 수 있게 int 타입으로 메서드를 설계할 수 있습니다.

    public int update(BoardVO board);
    <update id="update">
        update board set
                         boardTitle = #{boardTitle},
                         boardContents = #{boardContents},
                         userId = #{userId},
                         updateDate = now() where boardNum = #{boardNum}
    </update>

SQL에서 주의 깊게 봐야 하는 부분은 update 칼럼이 최종 수정시간을 의미하는 칼럼이기 때문에 현재 시간으로 변경해 주고 있다는 점과, regdate 칼럼은 최초 생성 시간이므로 건드리지 않는다는 점입니다. #{boardTitle}과 같은 부분은 파라미터로 전달된 BoardVO 객체의 getBoardTitle()과 같은 메서들을 호출해서 파라미터들이 처리됩니다.


    @Test
    public void updateTest() {
        BoardVO board = new BoardVO();

        // 실행전 존재하는 번호인지 확인할 것
        board.setBoardNum(5L);
        board.setBoardTitle("수정된 제목");
        board.setBoardContents("수정된 내용");
        board.setUserId("user01");

        int count = mapper.update(board);
        log.info("update count : " + count);
    }

비즈니스 계층

비즈니스 계층은 고객의 요구사항을 반영하는 계층으로 프레젠테이션 게층과 영속 게층의 중간 다리 역할을 하게 됩니다. 영속계층은 데이터 베이스를 기준으로 해서 설계를 나눠 구현하지만, 비즈니스 계층은 로직을 기준으로 해서 처리하게 됩니다. 쇼핑몰에서 상품을 구매한다고 가정해보면 해당 쇼핑몰의 로직이 물건을 구매한 회원에게는 포인트를 올려준다고 하면 영속 계층의 설계는 상품과 회원으로 나누어서 설게하게 됩니다. 반면에 비즈니스 계층은 상품 영역과 회원 영역을 동시에 사용해서 하나의 로직을 처리하게 되므로 다음과 같은 구조를 만들게 됩니다.

			Business		Persistence tier    
            구매서비스	↔	상품 처리 객체
            			↔ 	회원 처리 객체

일반적으로 비즈니스 영역에 있는 객체들은 서비스(Service)라는 용어를 많이 사용합니다.

package com.example.service;

import com.example.domain.BoardVO;

import java.util.List;

public interface BoardService {
    public void register(BoardVO board);

    public BoardVO get(long boardNum);
    public boolean modify(BoardVO board);
    public boolean remove(long boardNum);
    public List<BoardVO> getList();
}
package com.example.service;

import com.example.domain.BoardVO;
import com.example.mapper.BoardMapper;
import lombok.AllArgsConstructor;
import lombok.Setter;
import lombok.extern.log4j.Log4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@Log4j
@AllArgsConstructor
public class BoardServiceImpl implements BoardService {
    @Setter(onMethod_ = @Autowired)
    private BoardMapper mapper;

    @Override
    public void register(BoardVO board) {

    }

    @Override
    public BoardVO get(long boardNum) {
        return null;
    }

    @Override
    public boolean modify(BoardVO board) {
        return false;
    }

    @Override
    public boolean remove(long boardNum) {
        return false;
    }

    @Override
    public List<BoardVO> getList() {
        return null;
    }
}

BoardServiceImpl 클래스에 가장 중요한 부분은 @Service라는 어노테이션 입니다. @Service는 계층 구조상 주로 비즈니스 영역을 담당하는 객체임을 표시하기 위해 사용합니다. 작성된 어노테이션은 패키지를 읽어 들이는 동안 처리됩니다. BoardServiceImpl가 정상적으로 동작하기 위해서는 BoardMapper 객체가 필요합니다. 이는 @Autowired와 같이 직접 설정해줄 수 있고, setter를 이용해서 처리할 수도 있습니다.

@AllArgesContstructor는 모든 파라미터를 이용하는 생성자를 만들기 때문에 BoardMapper를 주입받는 생성자가 만들어지게 됩니다.

목록(리스트) 작업의 구현과 테스트

    public List<BoardVO> getList();
    @Override
    public List<BoardVO> getList() {
        return mapper.getList();
    }

BoardServiceTests

    @Test
    public void  testGetList() {
        service.getList().forEach(board -> log.info(board));
    }

BoardServiceImpl

 @Override
    public BoardVO get(long boardNum) {
        return mapper.read(boardNum);
    }

BoardServiceTests

 @Test
    public void testGet() {
        log.info(service.get(1L));
    }

profile
발전하기 위한 공부

0개의 댓글