Servlet-JSP/Servlet-JSP 답변형 게시판 만들기

구조 및 세팅

voider 2020. 9. 11. 22:45

1. 구조

모델2 방식인 MVC구조로 만들 것이다.

컨트롤러와 DAO 사이에 왜 Service가 필요할까? 트랜잭션Transaction 때문이다.

조승우가 자주 가는 커뮤티니 사이트를 예로 들어보자.
조승우는 커뮤니티 사이트에서 '비밀의 숲2 기대된다 토요일 첫 방!!'이라는 게시물을 조회하려고 한다. 현재 조회수는 3944. 조승우가 게시물을 클릭해서 조회페이지로 이동하면 조회수가 3945가 되어야 한다. 그러니까 조회하는 쿼리와, 조회수를 증가시키는 쿼리를 동시에 처리해야 한다는 것이다.
만약 이 두 가지가 따로 동작한다고 해보자. 그러면 (아주 짧은 찰나겠지만) 아직 조회 전인데 조회수가 1이 증가하거나, 이미 조회했는데 조회수가 그대로일 것이다. 이런 일이 발생하지 않도록 두 개 이상의 작업을 하나로 묶어주는 것을 트랜잭션이라고 한다.
DAO는 메서드 단위로 호출하기 때문에 트랜잭션 처리를 하기가 애매하다. 그래서 중간에 service계층을 두고 트랜잭션 처리를 한다. 컨트롤러는 그저 원하는 기능을 가진 서비스 메서드만 호출하면 된다.

2.테이블 생성

MySQL

-- 회원 테이블
CREATE TABLE `t_member` (
  `id` varchar(30) NOT NULL,
  `pwd` varchar(50) NOT NULL,
  `name` varchar(50) DEFAULT NULL,
  `email` varchar(50) DEFAULT NULL,
  `regdate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

-- 답변형 게시판 테이블
CREATE TABLE `t_board` (
  `bno` int NOT NULL AUTO_INCREMENT,
  `p_bno` int DEFAULT '0',
  `title` varchar(100) NOT NULL,
  `content` text NOT NULL,
  `imgName` varchar(45) DEFAULT NULL,
  `regdate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `id` varchar(45) NOT NULL,
  PRIMARY KEY (`bno`),
  KEY `id_idx` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

-- 테스트 데이터
INSERT INTO `t_board` 
VALUES (1,0,'안녕하세요','인서트 테스트입니다.',NULL,'2020-08-14 13:05:35','coco')
,(2,0,'안녕하세요2','인서트 테스트입니다.',NULL,'2020-08-14 13:05:41','coco')
,(3,0,'안녕하세요3','인서트 테스트입니다.',NULL,'2020-08-14 13:05:42','coco')
,(4,0,'hong입니다.','인서트 테스트입니다.',NULL,'2020-08-14 13:06:18','hong')
,(5,0,'hong입니다2','인서트 테스트입니다.',NULL,'2020-08-14 13:06:20','hong')
,(6,0,'toto입니다.','스포츠 토토 아니에요.',NULL,'2020-08-14 13:06:51','toto')
,(7,0,'toto입니다.','시네마천국 토토요',NULL,'2020-08-14 13:07:02','toto')
,(8,7,'저도 그 영화 좋아하는데','엔니오 모리꼬네!',NULL,'2020-08-14 13:07:37','coco')
,(9,7,'조선 최고의 흥행작이죠 ','이따리오루',NULL,'2020-08-14 13:08:00','hong')
,(10,1,'코코님 하이!','친하게 지내요',NULL,'2020-08-14 13:08:25','hong')
,(11,4,'길동님 하이!','오늘 밤은 출동 안 하시낭?',NULL,'2020-08-14 13:10:24','coco')
;

-- 계층형 쿼리를 위한 함수
/*
구문 내에서 ';'이 자주 사용되므로
DELIMITER $$ 와 $$ DELIMITER로 시작과 끝을 명시해준다.
필수 요소는 아니다.
*/
DELIMITER $$

CREATE FUNCTION fnc_hierarchy() RETURNS INT
NOT DETERMINISTIC
READS SQL DATA

BEGIN
    DECLARE v_id INT;
    DECLARE v_parent INT;
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET @id = NULL;

    SET v_parent = @id;
    SET v_id = -1;

    IF @id IS NULL THEN
        RETURN NULL;
    END IF;

    LOOP

    SELECT MIN(bno)
    INTO @id
    FROM t_board
    WHERE p_bno = v_parent
    AND bno > v_id;

    IF (@id IS NOT NULL) OR (v_parent = @start_with) THEN
        SET @level = @level + 1;
    RETURN @id;
    END IF;

    SET @level := @level - 1;

    SELECT bno, p_bno
    INTO v_id, v_parent
    FROM t_board
    WHERE bno = v_parent;

    END LOOP;

END

$$ DELIMITER ;

```sql
CREATE TABLE `animal` (
  `id` int NOT NULL AUTO_INCREMENT,
  `p_id` int DEFAULT '0',
  `nm` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

insert into ANIMAL(p_id, nm) values ( 0, '동물');

insert into ANIMAL(p_id, nm) values ( 1, '말');

insert into ANIMAL(p_id, nm) values ( 1, '닭');

insert into ANIMAL(p_id, nm) values ( 2, '얼룩말');

insert into ANIMAL(p_id, nm) values ( 2, '조랑말');

insert into ANIMAL(p_id, nm) values ( 3, '흰닭');

insert into ANIMAL(p_id, nm) values ( 3, '검은닭');

insert into ANIMAL(p_id, nm) values ( 5, '망아지');

insert into ANIMAL(p_id, nm) values ( 6, '흰병아리');

insert into ANIMAL(p_id, nm) values ( 7, '검은병아리');

insert into ANIMAL(p_id, nm) values ( 9, '흰달걀');

insert into ANIMAL(p_id, nm) values ( 10, '검은달걀');

/*
구문 내에서 ';'이 자주 사용되므로
DELIMITER $$ 와 $$ DELIMITER로 시작과 끝을 명시해준다.
필수 요소는 아니다.
*/
DELIMITER $$

CREATE FUNCTION fnc_hierarchy() RETURNS INT
NOT DETERMINISTIC
READS SQL DATA
BEGIN
    DECLARE v_id INT;
    DECLARE v_parent INT;
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET @id = NULL;

    SET v_parent = @id;
    SET v_id = -1;

    IF @id IS NULL THEN
        RETURN NULL;
    END IF;

    LOOP

    SELECT MIN(id)
    INTO @id
    FROM animal
    WHERE p_id = v_parent
    AND id > v_id;

    IF (@id IS NOT NULL) OR (v_parent = @start_with) THEN
        SET @level = @level + 1;
    RETURN @id;
    END IF;

    SET @level := @level - 1;

    SELECT id, p_id
    INTO v_id, v_parent
    FROM animal
    WHERE id = v_parent;

    END LOOP;

END
$$
DELIMITER ;


--계층형 구조로 데이터를 출력

SELECT CASE WHEN LEVEL-1 > 0 
THEN CONCAT(CONCAT(REPEAT('     ', level - 1),'L'), ani.nm)
ELSE ani.nm
END AS nm
,ani.id
,ani.p_id
,fnc.level
FROM
    (SELECT fnc_hierarchy() AS id, @level AS level
    FROM (SELECT @start_with:=0, @id:=@start_with, @level:=0) vars
    JOIN animal
    WHERE @id IS NOT NULL) fnc
    JOIN animal ani ON fnc.id = ani.id
    ;

이런 구조로 출력된다면 성공.

p_bno는 답글을 달 게시물의 번호다.

3. 패키지 구조

  • 패키지는 com.coco와 테스트하는 패키지 com.test로 나눈다.

  • CRUD 테스트를 진행하고 문제가 없다면 본 프로젝트(com.coco)에 적용한다.

    • JUnit테스트 진행 시 톰캣 dbpc를 사용할 수 없음에 유의
  • Service는 인터페이스로 정의하고 ServiceImpl클래스가 Service인터페이스를 구현하는 구조로 만든다.

  • VO -> DAO -> Controller - view 순서로 개발한다.

  • WEB-INF폴더 안에 jsp파일을 만든 이유는, WEB-INF폴더 내에 있는 파일로 클라이언트가 직접 접근할 수 없기 때문이다.
    WEB-INF/board/list.jsp에 접근하려면 반드시 BoardController를 거쳐야 한다.

4. JDBC테스트

테스트를 통해서 DB 연동이 잘 되는 지 확인한다.

mysql connector 설정

처음에 톰캣 DBPC를 이용해서 DataSource 객체를 생성했다가

javax.naming.NoInitialContextException: Need to specify class name in environment or system property, or as an applet parameter, or in an application resource file: java.naming.factory.initial

이 에러 때문에 삽질을 좀 했다. 웹 환경에서 돌리는 게 아니므로 톰캣이 작동하지 않는다. JDBC설정이 Server - context.xml에 다 들어있는데 서버가 돌아가지 않으니 참조할 수 없는 것이다. 그래서 테스트를 진행할 때는 직접 DriverManager를 이용해서 커넥션을 생성했다.

package com.test.dao;
...
public class JDBCTest {
    private final static Logger log = Logger.getGlobal();
    private static final String DRIVER = "com.mysql.cj.jdbc.Driver";
    private static final String URL = "jdbc:mysql://127.0.0.1:3306/servletex?serverTimezone=Asia/Seoul";
    private static final String USER = "servlet";
    private static final String PW = "1234";

    @Test
    public void connectionTest() throws ClassNotFoundException {

        Class.forName(DRIVER);

        String sql = "select now() as time";
        try(
            Connection conn = DriverManager.getConnection(URL,USER,PW);
            PreparedStatement pstmt = conn.prepareStatement(sql);
            ResultSet rs = pstmt.executeQuery();
            ) {

            assertNotNull(conn);
            log.info("conn : " + conn);
            log.info("pstmt : " + pstmt);

            rs.next();
            log.info(rs.getString("time"));

        } catch(Exception e) {
            e.printStackTrace();
        }
    } // connection()
}

준비 끝.