본문 바로가기
Java & Kotlin/Spring

[Spring] 게시판 프로젝트 (1)

by heekng 2021. 5. 17.
반응형

[Spring] 게시판 프로젝트 (1)_errorpage, vo, mapper, service

스프링 프로젝트로 게시판을 만드는 과정.

assets, images는 기존의 템플릿에서 가져왔으며, list, get, modify, register.jsp파일 작성은 따로 다루지 않는다.


errorPage 설정

<servlet>
	<servlet-name>appServlet</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<init-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
	</init-param>
	<init-param>
		<param-name>throwExceptionIfNoHandlerFound</param-name>
		<param-value>true</param-value>
	</init-param>
	<load-on-startup>1</load-on-startup>
</servlet>

web.xml의 servlet태그 속에 <init-param>태그를 추가하여 noHandelrFoundException이 발생한 것을 잡아줄 수 있도록 한다.

500에러의 경우 Internal Server Error이므로 @ExceptionHandler를 이용해서 처리가 가능하다.

404에러는 잘못된 URL을 호출할 때 나타나므로 500에러와는 다르게 처리해야한다.

package com.heekng.exception;

import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.NoHandlerFoundException;

import lombok.extern.log4j.Log4j;

@ControllerAdvice //해당 객체가 스프링의 컨트롤러에서 발생시키는 예외를 처리하는 존재임을 명시
@Log4j
public class CommonExceptionAdvice {
	/*
	 * 500메세지는 Internal Server Error 이므로 @ExceptionHandler를 이용해서 처리가 가능하지만
	 * 400메세지는 잘못된 URL을 호출할 때 보이므로 다르게 처리해주어야 한다.
	 */
	
	//500 error
	@ExceptionHandler(Exception.class)
	public String except(Exception e, Model model) {
		/*
		 * 예외가 발생하게 되면 해당 예외 필드가 메모리에 할당된다.
		 * 할당된 예외필드의 주소값을 받을 객체가 필요하므로 매개변수에 Exception타입의 e객체를 선언해놓는다.
		 */
		log.error("Exception..:.." + e.getMessage());
		model.addAttribute("exception", e);
		log.error(model);
		return "errorPage";
	}
	
	//404 error
	@ExceptionHandler(NoHandlerFoundException.class)
	@ResponseStatus(HttpStatus.NOT_FOUND)
	public String handle404() {
		return "errorPage";
	}
}

 

CommonExceptionAdvice는 @ControllerAdvice어노테이션이 붙어있기 때문에 일종의 컨트롤러로 작동한다.

404, 500 error이 발생하면 CommonExceptionAdvice로 들어오게 되고, 각각의 해당하는 메소드로 연결된다.

컨트롤러에서 return 값은 이동할 페이지를 나타내며, 경로와 .jsp확장자를 함께 작성하지 않는 이유는 servlet-context.xml에 작성되어있는

<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
	<beans:property name="prefix" value="/WEB-INF/views/" />
	<beans:property name="suffix" value=".jsp" />
</beans:bean>

위 태그 때문이다.

따라서 webapp폴더 속의 /WEB-INF/views/errorPage.jsp로 페이지 이동이 된다.

<context:component-scan base-package="com.heekng.exception"/>

root-context.xml에 위 context:component-scan태그로 com.heekng.exception패키지를 bean으로 등록한다.


게시판 글 목록 (list)

게시판 목록을 출력하려면 1. DB의 게시판 테이블, 2. 게시판 컨트롤러, 3. 게시판 객체 등이 필요하다.

게시판 테이블 생성

CREATE SEQUENCE SEQ_BLOG_BOARD;

CREATE TABLE BLOG_BOARD(
	BNO NUMBER(10),
	TITLE VARCHAR2(200) NOT NULL,
	CONTENT VARCHAR2(2000) NOT NULL,
	WRITER VARCHAR2(200) NOT NULL,
	REGDATE DATE DEFAULT SYSDATE,
	UPDATEDATE DATE DEFAULT SYSDATE,
	CONSTRAINT PK_BLOG_BOARD PRIMARY KEY(BNO)
);

위와 같이 DB에 게시판 테이블과 시퀀스 생성

VO 클래스 생성

package com.heekng.domain;

import lombok.Data;

@Data //getter, setter, toString, 생성자 자동생성
public class BoardVO {
	private Long bno;
	private String title;
	private String content;
	private String writer;
	private String regDate;
	private String updateDate;
}

**VO 테이블 컬럼을 가지고 작성하는 클래스이고 DTO는 사용자가 임의로 생성한 클래스를 뜻한다.

Persistence Tier 작성

스프링 프로젝트에서 이용하는 데이터 통신을 위해서 mapper, mapper인터페이스 등을 작성하고, 테스트한다.

1차적으로 삽입, 조회, 삭제, 수정 등의 단순한 하나의 역할을 하는 쿼리문을 작성하고, service에서 트랜젝션을 구성한다.

위와 같이 mapper인터페이스와 xml파일 생성

BoardMapper.java 인터페이스에서 사용할 쿼리문을 boardMapper.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.heekng.mapper.BoardMapper">
	<select id="getList" resultType="com.heekng.domain.BoardVO">
		<![CDATA[
			SELECT * FROM BLOG_BOARD WHERE BNO > 0
		]]>
	</select>
	
	<insert id="insert">
		<selectKey keyProperty="bno" order="BEFORE" resultType="long">
			SELECT SEQ_BLOG_BOARD.NEXTVAL FROM DUAL
		</selectKey>
		INSERT INTO BLOG_BOARD (BNO, TITLE, CONTENT, WRITER) 
		VALUES(#{bno}, #{title}, #{content}, #{writer})
	</insert>
	
	<select id="getTotal" resultType="_int">
		SELECT COUNT(BNO) FROM BLOG_BOARD
	</select>
	
	<select id="read" resultType="com.heekng.domain.BoardVO">
		SELECT * FROM BLOG_BOARD WHERE BNO = #{bno}
	</select>
	
	<delete id="delete">
		DELETE FROM BLOG_BOARD WHERE BNO = #{bno}
	</delete>
	
	<update id="update">
		UPDATE BLOG_BOARD 
		SET TITLE = #{title}, CONTENT = #{content}, WRITER = #{writer}, UPDATEDATE = SYSDATE
		WHERE BNO = #{bno}
	</update>
</mapper>
  • mapper태그의 namespace는 해당 쿼리문을 이용할 인터페이스의 경로를 작성해준다.
  • resultType이 VO또는 DTO객체라면 경로를 포함하여 작성한다.
  • <![CDATA[]]>는 쿼리문 속에 < or >이 작성되었을 때 태그를 열고닫는 역할이 아니라는 것을 나타낸다.
  • <selectKey>태그는 하나의 쿼리문 속에서 또 select 쿼리문을 작성하고 싶을 때 사용하며 keyProperty는 리턴되는 값을 밖 쿼리문의 필드에 바로 넣어줄 필드명을 뜻하고, order은 밖 쿼리문을 실행하기 전 또는 후를 선택한다.
    위의 insert문에서 사용되는 역할은 게시물을 insert해야 해당 게시물 번호가 생성되어 게시물 번호를 찾는 쿼리를 다시 실행해서 두번의 DB조회를 하는 것을 selectKey를 이용해 bno을 먼저 조회하여 insert문의 BoardVO객체에 bno를 set하고, 이후에 insert를 한다.
    재미있는 점은 selectKey를 통해 bno를 set 해주면 실제 파라미터로 가져온 VO객체의 bno에도 값이 들어가있다는 점이다.

BoardMapper.java 인터페이스

package com.heekng.mapper;

import java.util.List;

import com.heekng.domain.BoardVO;

public interface BoardMapper {
	//전체 게시물 리스트
	public List<BoardVO> getList();
	
	//등록
	public void insert(BoardVO board);
	
    //전체 게시물 개수 가져오기
    public int getTotal();
    
	//상세보기
	public BoardVO read(Long bno);
	
	//삭제
	public int delete(Long bno);
	
	//수정
	public int update(BoardVO board);
}

mapper.xml의 id와 일치하게 메소드를 선언해준다.

<mybatis-spring:scan base-package="com.heekng.mapper"/>

mybatis-spring:scan 태그를 통해 mapper패키지를 등록한다.

mapper 테스트

package com.heekng.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.Log4j;

@RunWith(SpringJUnit4ClassRunner.class) //테스트 코드가 스프링을 실행한다.
//지정된 클래스나 문자열을 이용해서 필요한 객체들을 스프링 내에 객체로 등록한다.
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
@Log4j
public class BoardMapperTests {
	@Setter(onMethod_ = @Autowired)
	private BoardMapper mapper;
	
	@Test
	public void testGetList() {
		mapper.getList().forEach(board -> log.info(board));
	}
}
  • @RunWith(SpringJUnit4ClassRunner.class) : 테스트 서버이기 때문에 임의의 스프링을 실행시킨다.
  • @ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml") : 스프링에서 참고할 root-context.xml의 경로를 지정한다.
  • @Test : 테스트할 메소드

테스트 실행 시 Junit으로 실행하며 콘솔을 통해 쿼리문 실행 결과가 출력된다.

테스트서버는 실제 웹에서 테스트하면서 생기는 오류를 줄이고, mapper 또는 service의 코드를 테스트해보며 문제가 있지는 않은지 확인할 수 있기 때문에 매우 중요한 역할을 한다.

service 작성

mapper작성 이후 service를 작성한다.

과정은 mapper과 유사하나 역할은 조금 다른데, mapper가 한가지 역할만 한다면 service는 mapper에서 만든 메소드를 단위별로 묶어서 실행한다는 차이점이 있다.

boardService 인터페이스 생성

package com.heekng.service;

import java.util.List;

import com.heekng.domain.BoardVO;

public interface BoardService {
	//전체 게시물 가져오기
	public List<BoardVO> getList();
	
	//게시물 등록
	public void register(BoardVO board);
	
	//특정 게시물 가져오기
	public BoardVO get(Long bno);
	
	//게시물 삭제
	public boolean remove(Long bno);
	
	//게시물 수정
	public boolean modify(BoardVO board);
}

BoardServiceImpl 클래스 생성

package com.heekng.service;

import java.util.List;

import org.springframework.stereotype.Service;

import com.heekng.domain.BoardVO;
import com.heekng.mapper.BoardMapper;

import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j;

@Service
@Log4j
@AllArgsConstructor
public class BoardServiceImpl implements BoardService{
	private BoardMapper mapper;
	
	@Override
	public List<BoardVO> getList() {
		log.info("getList......");
		return mapper.getList();
	}

	@Override
	public void register(BoardVO board) {
		log.info("register..:.."+board);
		mapper.insert(board);
	}

	@Override
	public BoardVO get(Long bno) {
		log.info("get..:.."+bno);
		return mapper.read(bno);
	}

	@Override
	public boolean remove(Long bno) {
		log.info("remove..:.."+bno);
		return mapper.delete(bno) == 1;
	}

	@Override
	public boolean modify(BoardVO board) {
		log.info("modify..:.."+board);
		return mapper.update(board) == 1;
	}
}

인터페이스 생성 후 implements받는 클래스를 생성하고, 메소드를 채워넣는다.

위의 경우 특별한 트렌젝션이 구성되지 않는 코드이기때문에 mapper과 특별한 차이점이 보이지 않는다.

service 테스트

package com.heekng.service;

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 com.heekng.domain.BoardVO;

import lombok.Setter;
import lombok.extern.log4j.Log4j;

@RunWith(SpringJUnit4ClassRunner.class) //테스트 코드가 스프링을 실행
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
//지정된 클래스나 문자열을 이용해서 필요한 객체들을 스프링 내에 객체로 등록
@Log4j
public class BoardServiceTests {
	@Setter(onMethod_ = @Autowired)
	private BoardService service;
	
//	@Test
//	public void getListTest() {
//		service.getList();
//	}
	
//	@Test
//	public void registerTest() {
//		BoardVO board = new BoardVO();
//		board.setContent("서비스 테스트 글 내용");
//		board.setTitle("서비스 테스트 글 제목");
//		board.setWriter("heekng");
//		
//		service.register(board);
//	}
	
//	@Test
//	public void getTest() {
//		service.get(21L);
//	}
	
//	@Test
//	public void modifyTest() {
//		BoardVO board = new BoardVO();
//		board.setBno(21L);
//		board.setContent("수정된 서비스 테스트 글 내용");
//		board.setTitle("수정된 서비스 테스트 글 제목");
//		board.setWriter("heekng");
//		
//		service.modify(board);
//	}
	
	@Test
	public void removeTest() {
		service.remove(21L);
	}
}

위와 같이 작성하여 mapper와 같이 service를 테스트한다.

 

반응형