스프링 Spring

Part3. 기본적인 웹 게시물 관리 21. 04. 05.

Levinni 2021. 4. 6. 00:42

07. 스프링 MVC 프로젝트의 기본 구성

7.1 각 영역의 Naming Convention(명명 규칙)

화면계층, 비즈니스 계층, 영속 계층 각 영역은 설게 당시부터 영역을 구분하고, 해당 연결 부위는 인터페이스를 이용해 설계하는 것이 일반적.

  • 네이밍 규칙
    • xxController
    • xxService, xxServiceImpl
    • xxDAO, xxRepository
    • VO, DTO: VO는 Read Only 목적이 강하고, 데이터 자체도 불변하게 설계하는 것이 정석.
      DTO는 데이터 수집의 용도가 더 강함.
      ex) 웹 화면에서 로그인하는 정보는 DTO 사용.

7.2 프로젝트를 위한 요구사항

프로젝트를 진행하기 전 고객의 요구사항을 인식하고, 이를 설계하는과정이 필요!! (요구사항 분석 설계)
고객이 무엇을 원하는지, 어느 정도까지 구현할 것인지.
능숙하지 않다면, 최대한 _단순하고 눈에 보이는 결과_를 만드는 것부터 진행!
요구사항은 "온전한 문장"으로 정리해야 함.

요구사항 명세서를 통해 인터페이스를 정의. 정의된 인터페이스를 바탕으로 구현.

예시 (문장 하나 = 서비스!)
고객은 새로운 게시물을 등록할 수 있어야 한다.

  • 요구사항에 따른 화면 설계

위 예시 이어서
어떤 내용들을 입력하게 될 것인가 > 테이블이나 멤버 변수들을 설계하게 됨.

스토리보드 만들기!(주로 ppt)

화면 페이지에서 어떤 버튼을 눌렀을 때 해야 할 일(ex.팝업), 그 다음에 올 흐름들.
한 번에 다 완성하는 것이 아니라 프로젝트 진행하면서 점점 완성하기.
나중에 이 스토리보드가 후에 산출물이 되는데, 옆 체크 메모만 바뀌는 것. -> 결국 메뉴얼이 됨
페이지별 1. 조 이름 2. 변경이력 표(누구에 의해 언제 무엇이 변경 되었는지)

📌프로젝트에서 스토리보드와 메뉴얼은 반드시 필요!

각 화면 설계에서는 사용자가 입력해야 하는 값과 함께 전체 페이지의 흐름이 설계 됨. 이 화면의 흐름은 _URL_로 구성하게 되는데 이 경우 get/post 방식에 대해 같이 언급해두는 것이 좋음!


7.3 예제 프로젝트 구성

ex01 -> ex02
pom.xml

ex00 -> ex02
log4j(main, test 둘 다) & .properties, root-context.xml, TimeMapper.xml, root-context.xml

  • 테이블 생성과 더미 데이터 생성
CREATE SEQUENCE SEQ_BOARD;
CREATE TABLE TBL_BOARD (
    BNO NUMBER(10,0) CONSTRAINT PK_BOARD PRIMARY KEY,
    TITLE VARCHAR2(200) NOT NULL,
    CONTENT VARCHAR2(2000) NOT NULL,
    WRITER VARCHAR2(50) NOT NULL,
    REGDATE DATE DEFAULT SYSDATE,
    UPDATEDATE DATE DEFAULT SYSDATE  
);
INSERT INTO TBL_BOARD (BNO, TITLE, CONTENT, WRITER)
VALUES (SEQ_BOARD.NEXTVAL, '테스트 제목', '테스트 내용', 'user00');

7.4 데이터베이스 관련 설정 및 테스트

root-context.xml 일부

    <mybatis-spring:scan base-package="site.levinni.mapper"/>        
    <bean class="com.zaxxer.hikari.HikariConfig" id="hikariConfig">
        <property name="driverClassName">
            <value>net.sf.log4jdbc.sql.jdbcapi.DriverSpy</value>
        </property>
        <property name="jdbcUrl">
            <value>jdbc:log4jdbc:oracle:thin:@58.74.90.2:1521/xe</value>
        </property>
        <property name="username">
            <value>BOOK_EX05</value>
        </property>
        <property name="password">
            <value>BOOK_EX05</value>
        </property>
    </bean>
    <bean class="com.zaxxer.hikari.HikariDataSource" id="dataSource" destroy-method="close">
        <constructor-arg ref="hikariConfig"></constructor-arg>
    </bean>
    <bean class="org.mybatis.spring.SqlSessionFactoryBean" id="sqlSessionFactory">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

DataSourceTests.java

package site.levinni.persistence;

import static org.junit.Assert.fail;
import java.sql.Connection;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
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.extern.log4j.Log4j;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
@Log4j
public class DatasourceTests {
    @Autowired
    private DataSource dataSource;
    @Autowired
    private SqlSessionFactory sqlSessionFactory; 

    @Test
    public void testConnection() {
        try(Connection conn = dataSource.getConnection()) {
            log.info(conn);
        } catch (Exception e) {
            fail(e.getMessage());
        }
    }
    @Test
    public void testMyBatis() {
        try(SqlSession sqlSession = sqlSessionFactory.openSession();
                Connection conn = dataSource.getConnection()) {
            log.info(conn);
            log.info(sqlSession);
        } catch (Exception e) {
            fail(e.getMessage());
        }    
    }
}

이전에 만들었던 JDBCTests.java도 잘 연동이 됐는지 확인해보자!


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

정석인 영속 계층의 작업 순서

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

8.1 영속 계층의 구현 준비

  • VO 클래스의 작성

BoardVO.java

package site.levinni.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

BoardMapper.java

package site.levinni.mapper;

import java.util.List;
import org.apache.ibatis.annotations.Select;
import site.levinni.domain.BoardVO;

public interface BoardMapper {
    @Select("SELECT * FROM TBL_BOARD WHERE BNO > 0")
    public List<BoardVO> getList();
}

WHERE BNO > 0을 한 이유는 ? INDEX 태우려고! 글번호가 PK이기 때문. PK는 자동으로 NOT NULL, UNIQUE, INDEX가 생긴다. (상기하기! 😃)

 

 

BoardMapperTests.java

public class BoardMapperTests {
    @Autowired
    private BoardMapper mapper;

    @Test
    public void testGetList() {
        mapper.getList().forEach(log::info);
    }
}

인터페이스와 테스트 클래스는 항상 페키지명이 같아야 하는 거 잊지 말 것!

 

 

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="site.levinni.mapper.BoardMapper">
    <select id="getList" resultType="site.levinni.domain.BoardVO">
        <![CDATA[
        SELECT * FROM TBL_BOARD WHERE BNO > 0
        ]]>

    </select>
</mapper>

resultType 속성의 값은 select 쿼리의 결과를 특정 클래스의 객체로 만들기 위해 설정.
<![CDATA]]> :  XML에서 부등호를 태그로 인식 하기 때문에 XML에서 부등호를 사용하기 위해서 사용함.
XML에서 SQL문 처리를 했으니 BoardMapper.java에 있던 어노테이션 sql은 제거하고 테스트 수행!


 

8.2 영속 영역의 CRUD 구현

MyBatis는 내부적으로 JDBC의 PreparedStatement를 활용하고 필요한 파라미터를 처리하는 '?'는 #{속성}을 이용한다. (SpEL 스프링 표현 언어

  • create(insert)처리

 

BoardMapper.java

package site.levinni.mapper;

import java.util.List;
import org.apache.ibatis.annotations.Select;
import site.levinni.domain.BoardVO;

public interface BoardMapper {
    public List<BoardVO> getList();

    public void insert(BoardVO boardVO);
    public void insertSelectKey(BoardVO boardVO);
}

 

 

BoardMapperTests.java


package site.levinni.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Select;
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.extern.log4j.Log4j;
import site.levinni.domain.BoardVO;
import site.levinni.persistence.DatasourceTests;

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

    @Test
    public void testGetList() {
        mapper.getList().forEach(log::info);
    }

    @Test
    public void testInsert() {
        BoardVO boardVO = new BoardVO();
        boardVO.setTitle("단위 테스트 작성 제목234");
        boardVO.setContent("단위 테스트 작성 내용23");
        boardVO.setWriter("newbie");

        mapper.insert(boardVO);
        log.info(boardVO);
    }

    @Test
    public void testUpdate() {
        BoardVO boardVO = new BoardVO();
        boardVO.setTitle("수정 단위 테스트 작성 제목234");
        boardVO.setContent("수정 단위 테스트 작성 내용23");
        boardVO.setWriter("newbie");
        boardVO.setBno(5L);

        log.info(mapper.update(boardVO));
    }

    @Test
    public void testInsertSelectKey() {
        BoardVO boardVO = new BoardVO();
        boardVO.setTitle("단위 테스트 작성 제목");
        boardVO.setContent("단위 테스트 작성 내용");
        boardVO.setWriter("newbie");

        mapper.insertSelectKey(boardVO);
        log.info(boardVO);
    }

    @Test
    public void testRead() {
        log.info(mapper.read(1L));
    }

    @Test
    public void testDelete() {
        log.info(mapper.delete(3L));
    }
}

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="site.levinni.mapper.BoardMapper">
    <select id="getList" resultType="site.levinni.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>

@SelectKey: 주로 PK 값을 미리(before) SQL을 통해 처리해 두고 특정 이름으로 결과를 보관하는 방식의 MyBatis 어노테이션

)

testInsert() 출력 결과에선 bno=null 인데, @SelectKey를 이용한 테스트의 출력 결과를 보면 SELECT SEQ_BOARD.NEXTVAL FROM DUAL가 먼저 실행되고 이 결과가 bno 값으로 처리되는 것을 알 수 있다.

  • read(select) 처리

BoardMapper.java에 public BoardVO read(Long bno);
BoardMapper.xml에

    <select id="read" resultType="site.levinni.domain.BoardVO">
        SELECT * FROM TBL_BOARD WHERE BNO = #{bno}
    </select>

MyBatis는 Mapper 인터페이스의 리턴 타입에 맞게 select의 결과를 처리하기 때문에, bno라는 칼럼이 존재하면 인스턴스의 'setBno()'를 자동으로 호출한다.

  • delete 처리

INSERT, UPDATE, DELETE는 몇 건의 데이터가 처리되었는지를 반환할 수 있다.

BoardMapper.java

public int delete(Long bno);

BoardMapper.xml

    <delete id="delete">
        DELETE TBL_BOARD WHERE BNO = #{bno}
    </delete>

태그 이름이 delete! id는 메서드 명일 뿐

  • update 처리

BoardMapper.java

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>

spEL 부분은 파라미터로 전달된 BoardVO 객체의 getTitle()같은 메서드들을 호출해서 파라미터들이 처리된다.