개발/[Spring] 블로그 만들기

[코드로 배우는 스프링 웹 프로젝트] 17-1강. Ajax 댓글 처리 (게시판 댓글 페이징/CRUD 처리하기...)

ee2ee2 2022. 4. 28. 22:47
728x90
반응형

해당 프로젝트는 코드로 배우는 스프링 웹 프로젝트(개정판)을 기반으로 진행됩니다.


17.1 프로젝트의 구성

REST 처리를 위해서는 pom.xml에서 수정된 내용이 대부분이므로, PART3 에서 사용된 'src/main/java'폴더 아래 모든 Java코드를 그대로 복사해서 사용한다. 또한, 'src/main/resources'에서는 log4jdbc-log4j2를 이용하기 위해서 log4jdbc-log4j2.properties 파일을 추가해야한다.

 

프로젝트는 오라클을 이용하으로 프로젝트 설정을 통해 JDBC 드라이버가 프로젝트 경로에 포함되도록 설정이 필요하다.

* 프로젝트명 우클릭 > Properties > Java Build Path > Libraries > ojdbc추가

* 프로젝트명 우클릭 > Properties > Deployment Assembly > ojdbc추가

 

WEB 과 관련된 파일들 역시 PART3 예제의 내용을 복사해서 사용할 것이다. Resources 폴더와  views 폴더 아래 모든 내용을 복사하여 추가해 준다.


17.2 댓글 처리를 위한 영속 영역

댓글을 추가하시 위해서 댓글 구조에 맞는 테이블을 설계한다.

댓글 처리를 위한 테이블 생성과 처리

: bno라는 컬럼을 이용해서 해당 댓글이 어떤 게시물의 댓글인지를 명시하도록 한다.; 댓글 자체는 CRUD가 가능하므로, 별도의 PK를 부여하고, 외래키(FK) 설정을 통해 tbl_board 테이블을 참조하도록 한다.

CREATE TABLE tbl_reply (
    rno NUMBER(10,0),
    bno NUMBER(10,0) NOT NULL,
    reply VARCHAR2(1000) NOT NULL,
    replyer VARCHAR2(50) NOT NULL,
    replyDate DATE DEFAULT SYSDATE,
    updateDate DATE DEFAULT SYSDATE
);

CREATE SEQUENCE seq_reply;

ALTER TABLE tbl_reply ADD CONSTRAINT pk_reply PRIMARY KEY (rno);

ALTER TABLE tbl_reply ADD CONSTRAINT pk_reply_board FOREIGN KEY (bno) REFERENCES tbl_board (bno);

17.2.1 ReplyVO 클래스의 추가

tbl_reply 테이블을 참고해서 org.zerock.domain 패키지 아래 ReplyVO 클래스를 추가한다.

ReplyVO.java

package org.zerock.domain;

import java.util.Date;

import lombok.Data;

@Data
public class ReplyVO {
	private Long rno;
	private Long bno;
	
	private String reply;
	private String replyer;
	private Date replyDate;
	private Date updateDate;
}

17.2.2 ReplyMapper 클래스와 XML 처리

org.zerock.mapper 패키지에는 ReplyMapper 인터페이스를 처리하고, XML 파일도 생성해준다.

ReplyMapper 인터페이스

package org.zerock.mapper;

public interface ReplyMapper {

}

ReplyMapper.xml

: tbl_reply 테이블에 필요한 SQL을 작성한다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper 
PUBLIC "-//mtbatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="org.zerock.mapper.ReplyMapper">

</mapper>

17.2.3 CRUD 작업

ReplyMapper를 이용하여 댓글에 대해 CRUD 작업을 처리한다.

ReplyMapper 인터페이스

package org.zerock.mapper;

import org.zerock.domain.ReplyVO;

public interface ReplyMapper {

	//댓글 등록
	public int insert(ReplyVO vo);
	
	//댓글 조회
	public ReplyVO read(Long rno);
	
	//댓글 삭제
	public int delete (Long rno);
	
	//댓글 수정
	public int update(ReplyVO reply);

}

 

ReplyMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper 
PUBLIC "-//mtbatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="org.zerock.mapper.ReplyMapper">
	<!-- 댓글 작성(추가) -->
	<insert id="insert">
		insert into tbl_reply (rno, bno, reply, replyer)
		values (seq_reply.nextval, #{bno}, #{reply}, #{replyer})
	</insert>
	
	<!-- 댓글 조회 -->
	<select id ="read" resultType="org.zerock.domain.ReplyVO">
		select * from tbl_reply where rno = #{rno}
	</select>
	
	<!-- 댓글 삭제 -->
	<delete id ="delete">
		delete from tbl_reply where rno = #{rno}
	</delete>
	
	<!-- 댓글 수정 -->
	<update id="update">
		update tbl_reply set reply = #{reply}, updateDate = sysdate
		where rno = ${rno}
	</update>
</mapper>

 

ReplyMapperTests.java

: 테스트 코드 작성 후 테스트 진행해보기

package org.zerock.mapper;

import java.util.List;

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.SpringRunner;
import org.zerock.domain.ReplyVO;

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

@RunWith(SpringRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
@Log4j
public class ReplyMapperTests {
	
	//테스트를 위해 존재하는 게시물의 번호를 가진 배열 생성
	private Long[] bnoArr = {1202124L, 1202123L, 1202122L, 1202121L, 1202106L};
	
	@Setter(onMethod_ = @Autowired)
	private ReplyMapper mapper;
	
	@Test
	public void testMapper() {
		log.info("mapper....." + mapper);
	}
	
	/*
	@Test
	public void testCreate() {
		IntStream.rangeClosed(1, 10).forEach(i -> {
			ReplyVO vo = new ReplyVO();
			
			//게시물의 번호
			vo.setBno(bnoArr[i%5]);
			vo.setReply("댓글 테스트 " + i);
			vo.setReplyer("replyer"+i);
			
			mapper.insert(vo);
			
		});
	}
	*/
	
	@Test
	public void testRead() {
		Long targetRno = 5L;
		
		ReplyVO vo = mapper.read(targetRno);
		log.info("testRead : " + vo);
	}
	
	@Test
	public void testDelete() {
		Long targetRno = 10L;
		
		mapper.delete(targetRno);
	}
	
	@Test
	public void testUpdate() {
		Long targetRno = 7L;
		ReplyVO vo = mapper.read(targetRno);
			
		//게시물의 번호
		vo.setReply("댓글 수정 테스트 입니다.");
			
		int count = mapper.update(vo);
		
		log.info("UPDATE COUNT : " + count);
	}

}

17.2.4 @Param 어노테이션과 댓글 목록

댓글의 경우, 특정한 게시글의 댓글만을 대상으로 페이징 처리가 되어야 한다. 때문에 페이징 처리시 게시글의 번호가 필요하게 된다.

MyBatis는 두 개 이상의 데이터를 파라미터로 전달하기 위해서는 1) 별도의 객체로 구상하거나, 2) Map을 이용하는 방식, 3) @Param을 이용해서 이름을 사용하는 방식이 있다. 이 중, 가장 간단한 방법이 3) @Param을 이용하는 방식이다. @Param의 속성 값을 MyBatis/에서 SQL을 이용할 때 '#{}'이름으로 사용이 가능하다.

 

페이징 처리는 기존과 동일하게 Criteria를 이용한다. 여기에 추가적으로 해당 게시물의 번호는 파라미터를 전달하도록 ReplyMapper를 구상한다.

ReplyMapper 인터페이스 - getListWithPaging 추가

package org.zerock.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Param;
import org.zerock.domain.Criteria;
import org.zerock.domain.ReplyVO;

public interface ReplyMapper {
	
	//댓글 등록
	public int insert(ReplyVO vo);
	
	//댓글 조회
	public ReplyVO read(Long rno);
	
	//댓글 삭제
	public int delete (Long rno);
	
	//댓글 수정
	public int update(ReplyVO reply);
	
	//댓글의 목록과 페이징 처리
	public List<ReplyVO> getListWithPaging(@Param("cri") Criteria cri, @Param("bno") Long bno);

}

 

ReplyMapper.xml

: 페이징 처리는 이후에 진행할 것이다.

<!-- 특정 게시글의 댓글 조회 -->
<select id ="getListWithPaging" resultType="org.zerock.domain.ReplyVO">
		select rno, bno, reply, replyer, replyDate, updateDate
		from tbl_reply 
		where bno = #{bno}	@Param("bno")에 매핑되어 사용됨
		order by rno asc
</select>

 

ReplyMapperTests.java

///생략///

//테스트를 위해 존재하는 게시물의 번호를 가진 배열 생성
	private Long[] bnoArr = {1202124L, 1202123L, 1202122L, 1202121L, 1202106L};
	
	@Setter(onMethod_ = @Autowired)
	private ReplyMapper mapper;
	
	@Test
	public void testMapper() {
		log.info("mapper....." + mapper);
	}
	
///생략///
	
	@Test
	public void testList() {
		Criteria cri = new Criteria();
		
		List<ReplyVO> replies = mapper.getListWithPaging(cri, bnoArr[0]);
			
		replies.forEach(reply -> log.info("********************testList " + reply));
	}
    
///생략///

반응형

17.3 서비스 영역과 Controller 처리

서비스 영역과 Controller의 처리는 기존의 BoardService와 동일하게 ReplyService 인터페이스와 ResplyServiceImpl 클래스로 구성한다.

ReplyService 인터페이스

package org.zerock.service;

import java.util.List;

import org.zerock.domain.Criteria;
import org.zerock.domain.ReplyVO;

public interface ReplyService {

	public int register(ReplyVO vo);
	
	public ReplyVO get(Long rno);
	
	public int modify(ReplyVO vo);
	
	public int remove(Long rno);
	
	public List<ReplyVO> getList(Criteria cri, Long bno);

}

 

ReplyServiceImpl.java

package org.zerock.service;

import java.util.List;

import org.springframework.stereotype.Service;
import org.zerock.domain.Criteria;
import org.zerock.domain.ReplyVO;
import org.zerock.mapper.ReplyMapper;

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

@Service
@Log4j
@AllArgsConstructor
public class ReplyServiceImpl implements ReplyService{

	/* 
	 * ReplyServiceImpl은 ReplyMapper에 의존적인 관계이기 때문에 @Setter를 이용해서 처리하거나, 
	 * 스프링 4.3의 생성자와 자동주입을 이용해서 @AllArgsConstructor으로 대체 가능하다. 
	 */
	
	//@Setter(onMethod_ = @Autowired) -> @AllArgsConstructor으로 대체 가능
	private ReplyMapper mapper;
	
	@Override
	public int register(ReplyVO vo) {
		log.info("register....... " + vo);
		return mapper.insert(vo);
	}

	@Override
	public ReplyVO get(Long rno) {
		log.info("get....... " + rno);
		return mapper.read(rno);
	}

	@Override
	public int modify(ReplyVO vo) {
		log.info("modify....... " + vo);
		return mapper.update(vo);
	}

	@Override
	public int remove(Long rno) {
		log.info("remove....... " + rno);
		return mapper.delete(rno);
	}

	@Override
	public List<ReplyVO> getList(Criteria cri, Long bno) {
		log.info("get Reply List of a Board " + bno);
		return mapper.getListWithPaging(cri, bno);
	}

}

 


17.3.1 ReplyController 설계

ReplyController는 앞의 예제에서 SampleController와 유사하게 @RestController 어노테이션을 이용해서 설계하며 아래와 같은 URL로 동작할 수 있도록 한다.

REST 방식으로 동작하는 URL을 설계할 때는 PK를 기준으로 작성하는 것이 좋다. PK만으로 조회, 수정, 삭제가 가능하기 때문이다. 다만, 댓글의 목록은 PK를 사용할 수 없기 때문에 파라미터로 필요한 게시물의 번호(bno)와 페이지 번호(page) 정보들을 URL에서 표현하는 방식을 사용한다.

 

ReplyController.java

package org.zerock.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.zerock.service.ReplyService;

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

@RequestMapping("/replies/")
@RestController
@Log4j
@AllArgsConstructor
public class ReplyController {
	
	private ReplyService service;

}

17.3.2 등록 작업과 테스트

브라우저나 외부에서 서버를 호출할 때 데이터의 포맷과 서버에서 보내주는 데이터의 타입을 명확히 설계해야 한다. 예를 들어 댓글 등록의 경우 브라우저에서 JSON 타입의 댓글을 전송하고, 서버에서는 댓글의 처리 결과가 정상적으로 되었는지 리턴하도록 한다.

ReplyController 클래스 일부

public class ReplyController {
	
	private ReplyService service;
	
	//댓글 등록 함수
	@PostMapping(value="/new", 
			consumes = "application/json", 
			produces = {MediaType.TEXT_PLAIN_VALUE})
	public ResponseEntity<String> create(@RequestBody ReplyVO vo){
		log.info("ReplyVO 등록 : " + vo);
		
		int insertCount = service.register(vo);
		
		log.info("Reply INSERT COUNT : " + insertCount);
		
		return insertCount == 1 ? new ResponseEntity<>("success", HttpStatus.OK) : new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
	}
    
    ///생략

 

테스트 수행

<기존에 게시글 378번의 댓글을 없는 상태>

 

<POST 방식으로 댓글 생성 - JSON 으로 전송>

<댓글 등록 수행 로그>

<게시글 378번의 댓글 등록됨 확인>


17.3.3 특정 게시물의 댓글 목록 확인

ReplyController 클래스 일부

	//특정 게시물의 댓글 목록 확인 함수
	@GetMapping(value="/pages/{bno}/{page}", 
			produces = {MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_UTF8_VALUE})
	//public ResponseEntity<List<ReplyVO>> getList(@PathVariable("page") int page, @PathVariable("bno") Long bno) {
	public ResponseEntity<ReplyPageDTO> getList(@PathVariable("page") int page, @PathVariable("bno") Long bno) {
		log.info("Get LIST ...................");
		
		Criteria cri = new Criteria(page, 10);
		log.info("Criteria : " + cri);
		
		//return new ResponseEntity<>(service.getList(cri, bno), HttpStatus.OK);
		return new ResponseEntity<>(service.getListPage(cri, bno), HttpStatus.OK);
	}

 

테스트 수행

//특정 게시글의 댓글 조회

http://localhost:8080/replies/pages/378/1

<특정 게시물 댓글 조회 수행 로그>

 

<XML 타입으로 댓글 데이터 조회 성공>

http://localhost:8080/replies/pages/378/1

XML 타입 반환

 

<JSON 타입으로 댓글 데이터 조회 성공>

http://localhost:8080/replies/pages/378/1.json

JSON 타입 반환


17.3.4 댓글 삭제/조회

댓글의 수정/삭제/조회는 위와 유사한 방식으로 JSON이나 문자열을 반환하도록 설계한다.

ReplyController 클래스 일부

	//댓글 조회 함수
	@GetMapping(value="/{rno}", 
			produces = {MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_UTF8_VALUE})
	public ResponseEntity<ReplyVO> get(@PathVariable("rno") Long rno){
		log.info("get : " + rno);
		
		return new ResponseEntity<>(service.get(rno), HttpStatus.OK);
	}
	
	//댓글 삭제 함수
	@DeleteMapping(value="/{rno}", 
			produces = {MediaType.TEXT_PLAIN_VALUE})
	public ResponseEntity<String> remove(@PathVariable("rno") Long rno){
		log.info("remove : " + rno);
		
		return service.remove(rno) == 1 ? new ResponseEntity<>("success", HttpStatus.OK) : new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
	}

17.3.5 댓글 수정

댓글 수정은 JSON 형태로 전달되는 데이터와 파라미터로 전달되는 댓글 번호(rno)를 처리하기 때문에 아래와 같이 구현한다.

ReplyController 클래스 일부

	//댓글 수정 함수
	@RequestMapping(method = {RequestMethod.PUT, RequestMethod.PATCH}, 
			value="/{rno}", 
			consumes = "application/json", 
			produces = {MediaType.TEXT_PLAIN_VALUE})
	public ResponseEntity<String> modify(@RequestBody ReplyVO vo, @PathVariable("rno") Long rno){
		log.info("ReplyVO 수정 : " + vo);
		
		vo.setRno(rno);
		
		log.info("rno : " + rno);
		
		return service.modify(vo) == 1 ? new ResponseEntity<>("success", HttpStatus.OK) : new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
	}

 

테스트 수행 - 25번째 댓글 수정

<댓글 수정 수행 로그>

<댓글 수정 확인>

 


17강은 내용이 길어서 다음 게시글에 이어서 작성합니다!