Mybatis를 이용한 댓글형 게시판 구현 (1)

송범·2025년 1월 22일

프로젝트에 필요한 리소스 관리

  • resources 배포 : sql, xml문 , 오라클 서버관련 설정 파일
    -templates : html문서 배포 -> 타임리프 배포(React, Vue 등 사용)

  • 로그설정 파일 : 공통된 관심사 - Spring AOP(Aspect Oriented Programming) -> 관점지향프로그래밍

  • application.yml : 반복되는 코드 줄여줌, hibernate, HikariCP설정, 파일 업로드 설정, 이메일 설정,
    ---> 자바는 컴파일을 해야 하므로 설정하기에서는 제외 되었다. : Properties - 버전관리 - Git - 형상관리팀

만약 Oracle 데이터베이스의 정보들을 알고 싶거나 바꾸고 싶으면?

-----> D:\app\사용자계정\product\11.1.0\db_1\NETWORK\ADMIN

ORCL11 =
  (DESCRIPTION =
    (ADDRESS = (PROTOCOL = TCP)(HOST = 서버IP주소)(PORT = 포트번호))
    (CONNECT_DATA =
      (SERVER = DEDICATED)
      (SERVICE_NAME = orcl11)
    )
  )

<?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.mybatis.mapper.BoardMapper">
    <select id="boardList" parameterType="java.util.Map" resultType="java.util.Map">
        SELECT rb.b_no, rb.b_title, rb.b_content, rb.email, rb.b_hit, rb.b_date, rm.nickname
        FROM react_board rb inner join react_member rm on rb.email = rm.email
        WHERE 1=1
      /*
        Map<String, Object> pmap = new HashMap<>();
        pmap.put("cb_search", "b_title or b_content or nickname");
        pmap.put("keyword", keyword);
      */
        <if test="cb_serach != null and cb_search.equals('b_title')">
            AND rb.b_title LIKE '%' || #{keyword} || '%'
        </if>
        <if test="cb_serach != null and cb_search.equals('b_content')">
            AND rb.b_content LIKE '%' || #{keyword} || '%'
        </if>
        <if test="cb_serach != null and cb_search.equals('b_title')">
            AND rm.nickname '%' || #{keyword} || '%'
        </if>
    </select>
    <select id="current" parameterType="java.util.HashMap" resultType="java.lang.String">
        SELECT to_char(sysdate, 'YYYY-MM-DD HH:MI:SS') FROM dual
    </select>
</mapper>
<%@ page language="java" contentType="application/json; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="java.util.List,java.util.Map" %><%@ page import="java.util.ArrayList,java.util.HashMap"%>
<%@ page import ="com.google.gson.Gson" %>
<%
    List<Map<String,Object>> list = new ArrayList<>();
    Map<String,Object> rmap = new HashMap<>();
    rmap.put("b_no",1);
    rmap.put("b_title","제목1");
    rmap.put("b_writer","이순신");
    list.add(rmap);
    rmap = new HashMap<>();
    rmap.put("b_no",2);
    rmap.put("b_title","제목2");
    rmap.put("b_writer","김유신");
    list.add(rmap);
    rmap = new HashMap<>();
    rmap.put("b_no",3);
    rmap.put("b_title","제목3");
    rmap.put("b_writer","강감찬");
    list.add(rmap);
    Gson g = new Gson();
    String json = g.toJson(list);
    out.print(json);
%>

Board CRUD 작업 경로 및 설명

기능URL메서드설명
게시글 목록 조회 (페이지 호출)/pojo/board/boardList.doboardList응답 페이지를 직접 호출하여 목록을 조회
게시글 목록 조회 (JSON)/pojo/board/jsonBoardList.dojsonBoardList결과를 JSON 포맷으로 반환하여 목록을 조회
게시글 작성/pojo/board/boardInsert.doboardInsert새로운 게시글을 작성
게시글 수정/pojo/board/boardUpdate.doboardUpdate기존 게시글의 내용을 수정
게시글 삭제/pojo/board/boardDelete.doboardDelete특정 게시글을 삭제
게시글 상세조회/pojo/board/boardDetail.doboardDetail특정 게시글을 삭제

  1. 목록 조회 (boardList vs jsonBoardList)

    • boardList: 브라우저에서 직접 페이지를 호출하며, HTML로 렌더링된 결과를 제공.
    • jsonBoardList: 클라이언트에서 AJAX 요청 등을 통해 JSON 데이터를 받아오는 방식.
  2. CRUD 경로
    각 작업에 대해 직관적인 URL 구조를 사용하여 어떤 작업인지 명확히 알 수 있습니다.
    boardInsert, boardUpdate, boardDelete는 각각 삽입, 수정, 삭제 작업을 의미합니다.

package com.example.demo.pojo;


import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.log4j.Log4j2;

import java.io.IOException;

//요청 URL패턴에 /를 사용하게 되면 이 서버에 들어오는 모든 요청이 이 클래스로 유입됨.
//의미없는 확장자이지만 식별자로 사용하기 위해서 do를 붙임.
//개발자 입장에서는 doGet, doPost이든 모두 처리를 해야 합니다. - 창구 일원화 - 관리
//표준 서블릿에서는 메소드마다 URL패턴을 가질 수 없다.
//Rest API가 아닌 메소드는 구현할 수 있는 있겠지만 메소드 파라미터에 request와 response
//객체를 주입받지 못함
//웹 서비스 구현시 자바는 안되고 서블릿만 되는 이유 request, response 제공된다.


@Log4j2 //lombok을 이용해서 log4j2를 사용할 수 있게 해줌.
@WebServlet("*.do")
public class ActionServlet extends HttpServlet {

    public void doService(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // -> http://localhost:8000/
        // -> pojo/board/boardList.do -> 20
        // -> pojo/board/boardInsert.do -> 22
        // -> pojo/board/boardUpdate.do -> 22
        String uri = req.getRequestURI();
        log.info(uri); //home
        String context = req.getContextPath(); // server.xml -> jsp-step1플젝 배포 -> 루트
        log.info(context);
        String command = uri.substring(context.length()+1); // /home
        int end = command.lastIndexOf("."); // .do의 이전위치 정보를 계산함.
        //pojo/board/boardList
        //pojo/board/boardInsert
        //pojo/board/boardUpdate
        //pojo/board/boardDelete 만 들고온다.
        command = command.substring(0, end); //문자열 뒤에 substring함수를 붙임
        //요청 URL맨 앞에 pojo는 react서버에서 다른 출처인 경우 CORS 정책에 의해 차단될 수 있음.
        //3000번 포트에서 8000번 포트로 요청이 바뀌면 프록시 활용하여 요청을 전달함. - CORS이슈 묵인
        //우리는 배열에 값이 두개면 된다.
        //upmu[0] = 업무이름저장 - board
        //upmu[1] = 페이지 이름 - boardList
        //배열은 복사가 안되므로 새로운 배열을 생성해야 함.
        String temp[] = null;
        temp = command.split("/"); //temp[0] = pojo, temp[1] = board, temp[2] = boardList or boardInsert or boardUpdate or boardDelete
        String upmu[] = new String[temp.length-1]; //2개
        //업무에배열을 생성시에 temp에 담긴 pojo는 제외시킨다.
        System.arraycopy(temp, 1, upmu, 0, temp.length-1);
        //결과는 temp[0]에 있는 pojo는 제외되었다.

        log.info(upmu.length); // 2
        req.setAttribute("upmu", upmu); //업무이름과 페이지이름을 저장 결과적으로 얕은복사
        //웹서비스에 필요한 request와 response객체는 ActionServlet으로부터 주입 받는다.
        BoardController boardController = new BoardController();

    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        log.info("doGet() 호출");
        doService(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        log.info("doPost() 호출");
        doService(req, resp);
    }

    @Override
    protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        log.info("doPut() 호출");
        doService(req, resp);
    }

    @Override
    protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        log.info("doDelete() 호출");
        doService(req, resp);
    }



}

MyBatis Insert문 추가해보기

select '2025-01-10', sysdate, to_date(sysdate+1) from dual;

select to_char(sysdate,'yyyy-mm-dd'), to_char(sysdate,'yyyy-mm-dd HH:MI:SS AM') from dual; //문자 타입으로 형 전환을 해준다.

select to_char(1000000,'999,999,999') from dual; // 3개씩 끊어서 해준다.

    <insert id="boardInsert" parameterType="map">
        INSERT INTO react_board (b_no, b_title, email, b_content, b_hit, b_date, b_file)
        VALUES (SEQ_REACTBOARD_NO.NEXTVAL, #{b_title}, #{email} , #{content}, 0, to_char(sysdate,'yyyy-mm-dd'), #{b_file})
    </insert>

	<delete id = "boardDelete" parameterType="java.util.Map">
        DELETE FROM react_board WHERE b_no = #{value}             
    <delete>
      
    <update id ="boardUpdate" parameterType="map">
         UPDATE react_board 
      	 SET b_title = #{b_title}
      	 	,email = #{email}
      		,b_content = #{b_content}
       		,b_file = #{b_file}
     	 WHERE b_no = #{b_no}
    <update>
                                                 
//BoardDao.java
public int boardInsert(Map<Objcet,String> pmap){
	int result = -1;//초기값
    
    return result // 1이면 입력성공, 0이면 입력실패
}

컴파일을 하는 언어 -> 실행 전에 소스 코드를 기계어로 번역
컴파일을 하지 않는 언어(html, js 타입을 안쓴다.) -> 실행 중에 소스 코드를 해석

컴파일 하지 않는 언어 var -> 호이스팅 이슈 & const, let

XML은 파싱과정을 통해서 유효한지 정확한지

XML 문서를 작성할 때는 명세서 있는 약속된 태그이름만 사용할 것.

XML은 html보다 철저히 지켜야 한다. ---> 엄격하다. ---> 안전,공평하다.

  • 클래스 설계하기(BoardController, BoardLogic, BoardDao)

  • 메소드 설계하기
    먼저 쿼리문을 작성 후 리턴타입과 파라미터를 알아야 한다.

  • BoardController의 경우 URL 매핑하기가 불가하므로 if문 처리

  • BoardLogic의 경우 가능 : Model 계층이다
    • 게시글 목록 : List<Map> boardList(Map pmap)
    • 게시글 등록 : int boardInsert(Map pmap)
    • 게시글 수정 : int boardUpdate(Map pmap)
    • 게시글 삭제 : int boardDelete(int b_no)

인증/인가(스프링 시큐리티 코딩)

  • 쿠키와 세션 처리(Cookie,Session) -> 상태관리
  • redux와 react-redux -> 상태관리 - 프로젝트 생성하기
  • 이메일과 비번 로그인(firebase)
  • 구글 로그인(firebase) -> 구글로그인을 하면 당신은 회원가입 대상입니다. 회원가입을 유도하는 로직

댓글형게시판 구현하기

  • 작성자 정보는 세션에 저장되어 있는 (로그인시 확인한)이름으로 처리한다.

요청 순서

http://loaclhost:3000/board 엔터 (요청하기)

BoardDBList.jsx가 실행됨 - 함수 호출 -> 함수가 태그처럼 사용된다.

<Router>
	<Route path = "/board">
    
    <Route>
<Router>
 


 useEffect(()=>{
 	const asyncDB = async () => {
    	const response = await BoardDBList(); //json 포맷
      	//board.js 파일에서 선언한 함수 이름이다.
      	//-> Back-end 요청
      	//-> http://localhost:8000/pojo/reboard/boardList.do
      	//바로 이지점에서 CORS이슈 발생
     	//setProxy.js에서 백엔드 요청에 접두어로 pojo를 지정해두었다.
    }
    asyncDB
 },[])
  1. 회원제로 운영되는 시스템

  2. 인증/인가 - 권한, 뱃지, 등급

  3. 로그인 - 이름 - 유지

    문제제기

    -> 웹서비스는 http 프로토콜 stateless - 비상태 프로토콜이다.

    -> 상태가 유지되지 않는다. 또 로그인을 해야 한다??

    -> 동시 접속자 수가 많다 즉, 많은 사람들을 대상으로 안정적인 서비스를 제공해야 한다.

    문제해결

    시간동안 유지되도록 처리해보자
    찜 상품 - 3일간 유지

    로그인 -> 상품 조회 -> 장바구니 담기 -> 결제하기 -> 배송처리 -> 반품처리, 파손처리

    결제하기 구현하려면
    집합이 회원집합, 주문집합, 주문집합상세, 재고, 단가

    쿠키로 처리해보자
    쿠키로 로그인처리 가능함
    쿠키로 찜상품 처리 가능함
    쿠키로 장바구니 처리 가능함

    세션으로 처리해보자
    세션으로 로그인처리 가능함
    세션으로 찜상품 처리 가능함
    세션으로 장바구니 처리 가능함

    공통점 - 시간을 조절 가능, 비상태 프로토콜 단점 부분에 보안

    차이점

    쿠키
    -> 로컬PC에 기록이 남는다.(보안 취약) , 문자열 -> Cookie

    -> 로그인(인증처리), 광고팝업(오늘 하루 보지 않기), 장바구니 최근에 본 상품,
    온라인 시험구현시 한 화면에 한 문제씩 출제가 된다고 했을 때 1번 문제부터 마지막 문제까지의 답안지를 기억해야 할 때

    ex) 쿠키 생성하기

    	Cookie c = new Cookie("이름", "값");
       c.setPath("/") or c.setPath("/board") or setPath("/pay")
       response.addCookie(c) //생성된 쿠기를 클라이언트측으로 내릴 때도 response객체가 필요하다.
       POJO 프로젝트는 서블릿에 의존적인다. <-> 스프링은 request와 response에 의존적이지 않다.
       사용하지 않아도 돼 -> 코드 양이 줄어든다 ->-> 유지보수에 유리하다.

    ex) 쿠키 읽어오기

	//주의 브라우저를 여는 순간 JESSIONID발급 -> 쿠키에 저장 -> 디폴트가 1개에서 시작된다.
    //쿠키는 로컬에 저장되어 있고 사용자가 로그인 후에 로컬에 저장되어 있는 쿠키정보를 요청하기
    String cookieValue = null;
    Cookie[] cs = request.getCookies(); // 리턴타입은 Cookie배열 타입이다.
    for(int i=0;i < cs.length;i++){
    	//쿠키안에 이름이 email인 값이 존재한다는 것은 로그인을 하고 입장한 사람이고
        //이 값이 null이라는 건 로그인을 하지 않고 url을 쳐서 입장한 사람이다.
        //웹 페이지에 대한 접근 권한이 필요하다. (Spring Boot Security 전략)
        //인증과 인가는 개발단계에서는 일단 배제하고 개발을 진행한다.
        //include로 분리해서 페이지를 따로 구현해 둔다. - 마지막 단계에서
    	if(cs[i].getName().equals("email")){
        	cookieValue= cs[i].getValue(); //쿠키값이 채워짐
        }
    }
    

ex) 쿠키 삭제하기

	//쿠키 생성하기 - 값 부분에 빈 문자열이나 0 디폴트 값으로 생성함.
   //시간 값을 0으로 설정하기
   //응답 객체를 통해서 클라이언트 측으로 내린다.
   Cookie c = new Cookie("email","");
   c.setMaxAge(0)
   c.setPath("/")
   response.addCookie(c)
   

세션 -> 서버PC cache메모리 저장, 객체모두 다 가능함, 보안안정 -> Session

게시판 구현시 글쓰기에서 작성자 이름은 세션에서 가져와서 자동으로 입력 처리한다.

쿠키 생성

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
   <title>웹서버에서 쿠키 생성하기</title>
</head>
<body>
   <h1>쿠키 생성</h1>
   <%
       Cookie c1 = new Cookie("notebook","삼성갤럭시N9");
       //만일 시간을 부여하지 않으면 브라우저가 종료될 때 자동으로 삭제됨.
       c1.setMaxAge(60); // 60초 동안 유지
       Cookie c2 = new Cookie("hp","아이폰16");
       //만일 시간을 부여하지 않으면 브라우저가 종료될 때 자동으로 삭제됨.
       c2.setMaxAge(60*2); 
       Cookie c3 = new Cookie("mouse","로지텍마우스");
       //만일 시간을 부여하지 않으면 브라우저가 종료될 때 자동으로 삭제됨.
       c3.setMaxAge(60*60);  
       //c3.setPath("/"); //쿠키를 모든 경로에서 사용가능하다.
       //쿠키는 로컬 PC에 저장된다.
       //쿠키는 문자열만 저장된다.
       //쿠키는 클라이언트측으로 데이터를 내려야 한다.
       //새로운 쿠키를 생성하면 브라우저에 전송함.
       response.addCookie(c1);
       response.addCookie(c2);
       response.addCookie(c3);
   %>
</body>
</html>
### 쿠키 정보 읽기

![](https://velog.velcdn.com/images/songbeom00/post/bfd3a4fb-40d3-4ae7-95a9-f3d616363162/image.png)

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>

쿠키정보 읽어오기 <% Cookie[] cs = request.getCookies(); if(cs != null && cs.length > 0) { for (Cookie c : cs) { if("notebook".equals(c.getName())){ String c_name = c.getValue(); out.println(c_name); } else if("hp".equals(c.getName())){ String c_name = c.getValue(); out.println(c_name); } else if("mouse".equals(c.getName())){ String c_name = c.getValue(); out.println(c_name);
    }
}

}
else {
out.print("쿠키가 없습니다.");
}

%>

``` 같은 경로 mycookie

다른 경로 mycookie2

다른 경로 mycookie2 -> setPath 지정

실습 결과

  1. 쿠키에 사용한 한글을 깨지지 않고 잘 출력 되었다. - 추가 한글 처리 필요 X

  2. 쿠키는 같은 path에서만 유지된다.
    : 쿠키생성은 mycookie폴더에서 생성했는데 mycookie2에서 쿠키에 저장된 값을 읽기는 불가하다.

  3. 그러면 모든 path에서 접근하도록 c3.setPath("/"); -> c3 쿠키는 모든 경로에서 사용가능하다.

  4. 쿠키는 path 설정에 따라 값을 유지하기도 하고 못하기도 한다. 현재 내가 바라보는 프로젝트의 모든 path에서 접근하고 싶다면 반드시 루트패스로 설정할 것. -> 주의!

쿠키를 삭제할 때는 쿠키의 maxAge를 0으로 설정하면 된다. -> 다른 API 처럼 remove() delete() 같은 메소드가 없다.

쿠키를 활용한 로그인 구현하기

1) loginForm.jsp - 로그인 화면을 제공한다.

2) 로그인할 때 입력할 값은 무엇인가? -> 아이디와 비밀번호

3) 아이디와 비번을 입력하고 돌려받는 값은 무엇인가? -> 사용자의 이름과 PK(Primary Key)

4) 아이디와 비번을 어디로 전송해야할까? ->

아래는 get방식으로 전송하기의 예시이다.

 
 
 <form method="get" action"/mycookie/login.do>
                           
                           
                           
</form>

                           
@WebServlet(urlPattern="/mycookie/login.do") //요청을 서블릿이 받아준다.
LoginServlet.java         
                                          
                                                                                     

get방식과 post방식의 차이는? -> body 유무

get방식은 queryString을 보낸다 -> 노출이 된다.
http://localhost:8000/mycookie/login.do?mem_id=kiwi&mem_pw=123

post방식은 body에 담아서 보낸다 -> 노출이 되지 않는다. (보안)
http://localhost:8000/mycookie/login.do

웹서비스에서는 어떤 메소드가 호출될 것인가를 URL을 통해서 알 수 있다. -> 기억한다.

메소드마다 해당 요청을 식별 할 수 있는 URL이름이 필요하다
스프링에서는 이런 역할을 하는 어노테이션이 있다.

@RequestMapping("/mycookie/login")    
@GetMapping("/mycookie/login")                                             
@PostMapping("/mycookie/login")  
                             
                                                                     

                              																																																																																																												
                              
                              
                              
                              
                              
                              
profile
BackEnd&Data Scientist가 되고 싶은 개발 기록 노트

0개의 댓글