Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

개발자입니다

[비트캠프] 47일차(10주차2일) - Java: (backend, frontend)-app-02 본문

네이버클라우드 AIaaS 개발자 양성과정 1기/Java

[비트캠프] 47일차(10주차2일) - Java: (backend, frontend)-app-02

끈기JK 2023. 1. 10. 10:29

 

myapp-13 → 02.backend-app

 

왼쪽 그림에서 BoardHhandler, MemberHandler는 CLI UI (boundary) 역할이다.

오른쪽 그림에서 BoardController, MemberController는 REST API (boundary) 역할이다.

                           패키지 vo, dao는 myapp에서 만든 클래스를 재사용한다.

 

 

 

Use-case 주도 객체지향 설계 패턴 (by Ivar Jacobson)

 

설계 패턴 : System

(ICONIX Process에서 robustness diagram)

user와 만나는 UI를 담당하는 클래스를 boundary라 한다.

중간에서 boundary와 Entity를 제어하는 클래스를 control이라 한다.

데이터 담당하는 클래스를 Entity라 한다.

 

myapp은 boundary, control 두 역할을 BoardHandler가 하고, Entity 역할을 BoardDao, Board가 한다.

backend-app은 boundary, control 두 역할을 BoardController가 하고, Entity 역할을 BoardDao, Board가 한다.

 

 

 

robustness diagram

 

① CLI 에서 《boundary, control》 역할의 BoardHandler 에서 《entity》 역할의 BoardDao로 요청을 한다.

② MVC Web 에서 《control》 역할의 BoardServlet (Controller) 에서 《entity》 역할의 BoardDao (Model)로 요청을 한다. 또는 《boundary》 역할의 board_list.jsp (View)로 데이터를 준다.

③ backend-app 에서 《boundary, control》 역할의 BoardController 에서 《entity》 역할의 BoardDao로 요청을 한다.

④ frontend-app 에서 《boundary》 역할의 .html 에서 《control》 역할의 .js로 요청하고 JSON 에 요청한다.

backend와 frontend를 하나로 묶어서 보면 frontend 가 boundary, control 역할을 하고 backend가 entity 역할을 한다.

 

 

 

SpringBoot 패키지 구조 참고 사이트 : https://velog.io/@sunil1369/Spring-boot-%ED%8C%A8%ED%82%A4%EC%A7%80-%EA%B5%AC%EC%A1%B0

 

 

GRASP 패턴의 중요 3가지

1. 변수를 다루는 메서드를 변수 쪽으로 두라 - Information Expert

2. 1 클래스 1 역할을 하도록 하라 - High Cohesion

3. 클래스간 상호의존도가 낮게 책임을 부여 - Low Coupling

 

 

 

REST API 와 HTTP method

 

'mdn fetch' 참고 : https://developer.mozilla.org/ko/docs/Web/API/Fetch_API/Using_Fetch

  HTTP
Method
의미 URL 예
① add post → 요청할 때마다 데이터가 추가된다. http://localhost:8080/boards
② retrieve(read) get → 요청 처리 후 데이터 변경은 없다. http://localhost:8080/boards 는 목록 조회이다. s로 복수형 쓴다.
http://localhost:8080/boards/112 에서 112는 데이터(ID)를 특정하고 상세조회한다.
③ update put → 요청 처리 후 추가되는 데이터는 없다. http://localhost:8080/boards/112
④ delete delete → 요청 처리 후 데이터 삭제. http://localhost:8080/boards/112

 

HTML form은 get, post 두 가지 요청만 할 수 있다.

웹 프로그래밍(서버 사이드 렌더링) 방식에서는 boards /add, /update, /delete 이런 식으로 행위를 주소에 적는다.

REST API 방식에서는 method에 행위를 적는다.

REST API 방식을 RESTful 방식이라 한다.

 

 

C:\Users\bitcamp\git\bitcamp-ncp\frontend-app\app\board\form.html

등록 누르면 status: 'success' 받도록 되어있다.

 

 

 

요청과 응답 과정

 

Web Browser에서 제목, 내용, 암호 작성해서 전송한다.

 

POST /boards  HTTP/1.1

Host: localhost:8080

Content-Type: application/x-www-form-urlencoded

...

title=O&content=O&password=O

위 형식으로 getBoards에 요청한다.

정상 수신시 HTTP/1.1 200 ok 보낸다.

 

Web Browser가 보낸 값인 title, content, password 를 서버에 저장한다.

응답 정보를 contentMap(Java 객체)에 담아 return한다.

이를 SpringBoot가 serialize해서 JSON 형식의 문자열로 변환한다. 이를 다시 deserialize해서 JavaScript 객체에 전달한다.

 

 

 

웹 브라우저가 데이터 정상적으로 수신했는지 확인하는 법

 

/boards 에 정보 제대로 전달됐는지 확인하려면 개발자 도구 > Network에서 Preserve log, Disable cache 체크하고 Name: boards 클릭한다.

Headers > Request Headers > Content-type 이 application/x-www-form-urlencoded 인지 확인한다.

Payload > Form Data > view source 클릭해서 형식 정상인지 확인한다.

Response에서 응답 데이터 확인한다.

 

 

웹브라우저는 javascript를 실행하지 못한다. HTML을 실행하며 CSS, js가 있으면 실행한다.

 

 

 

AJAX 와 CORS

 

클라이언트가 ① HTML 요청을 서버 1로 하고 ② 응답 받아서 출력한다.

클라이언트가 ③ AJAX 요청을 서버 2로 하고 ④ 응답 받아서 출력한다. 이때 CORS 제약에 걸리는데 @CrossOrigin("http://localhost:5500") 을 서버페이지에 입력한다. ← 이 서버에서 다운로드 받을 HTML 페이지에서 AJAX 요청을 한다면 허락한다.

 

 

현재 HTML의 출신을 [개발자 도구] > Network > Name: boards > Headers > Request Headers > Origin 에서 알 수 있다.

 

 

 

 

Board : frontend, backend 구현

 

 

frontend-app 소스

 

git\bitcamp-ncp\frontend-app\app\board\

 

 

list.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <h1>게시글</h1>
  <a href="form.html">새 글</a>
  <table border="1">
    <thead>
      <tr>
        <th>번호</th>
        <th>제목</th>
        <th>작성일</th>
        <th>조회수</th>
      </tr>
    </thead>
    <tbody>
    </tbody>
  </table>

  <script>
    var tbody = document.querySelector('tbody');

    fetch('http://localhost:8080/boards')
      .then((response) => { return response.json(); })
      .then((obj) => {
        var html = '';
        for(var b of obj.data) {
          html += `<tr>
            <td>${b.no}</td>
            <td><a href="view.html?no=${b.no}">${b.title} </a></td>
            <td>${b.createdDate}</td>
            <td>${b.viewCount}</td>
            </tr>\n`;
        }
        tbody.innerHTML = html;

      })
      .catch(() => {
        alert("서버 요청 오류!")
        console.log(err);
      });
  </script>
</body>
</html>

 

 

form.html 코드
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>게시글</title>
</head>
<body>
  <h1>새 게시글</h1>
  <form>
  <table border="1">
    <tbody>
      <tr>
        <th>제목</th>
        <td><input type="text" name="title" id="f-title"></td>
      </tr>
      <tr>
        <th>내용</th>
        <td><textarea name="content" rows="10" cols="50" id="f-content"></textarea></td>
      </tr>
      <tr>
        <th>암호</th>
        <td><input type="password" name="password" id="f-password"></td>
      </tr>
    </tbody>
  </table>
  <button id="add-btn" type="button">등록</button>
  <button id="cancel-btn" type="button">취소</button>
  </form>

  <script>
    document.querySelector('#add-btn').onclick = (e) => {
      var title = encodeURIComponent(document.querySelector('#f-title').value);
      var content = encodeURIComponent(document.querySelector('#f-content').value);
      var password = document.querySelector('#f-password').value;
      // 한글 입력하므로 encodeURIComponent 함수로 처리한다.
      // console.log(`title=${title}&content=${content}&password=${password}`);  → 출력 해본다.
      
      fetch('http://localhost:8080/boards', {
        method: 'POST',
        headers: {
          'Content-type': 'application/x-www-form-urlencoded'
        },
        body: `title=${title}&content=${content}&password=${password}`
      })
      .then((response) => {return response.json();})
      .then((obj) => {
          location.href = "list.html";
        })
        .catch(() => {
          alert("서버 요청 오류!")
          console.log(err);
      });
    };

    document.querySelector('#cancel-btn').onclick = (e) => {
      location.href = "list.html";
    }

  </script>
</body>
</html>

 

 

 view.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <h1>게시글</h1>
  <form>
  <table border="1">
    <tbody>
      <tr>
        <th>번호</th>
        <td><input type="text" name="no" id="f-no" readonly></td>
      </tr>
      <tr>
        <th>제목</th>
        <td><input type="text" name="title" id="f-title"></td>
      </tr>
      <tr>
        <th>내용</th>
        <td><textarea name="content" rows="10" cols="50" id="f-content"></textarea></td>
      </tr>
      <tr>
        <th>암호</th>
        <td><input type="password" name="password" id="f-password"></td>
      </tr>
      <tr>
        <th>작성일</th>
        <td><span id="f-createddate"></span></td>
      </tr>
      <tr>
        <th>조회수</th>
        <td><span id="f-viewcount"></span></td>
      </tr>
  
    </tbody>
  </table>
  <button id="update-btn" type="button">변경</button>
  <button id="delete-btn" type="button">삭제</button>
  <button id="list-btn" type="button">목록</button>
  </form>

  <script>

    // location.href --> http://localhost:5500/frontend-app/app/board/view.html?no=1
    // values[0] --> http://localhost:5500/frontend-app/app/board/view.html
    // values[1] --> no=1
    var values = location.href.split("?");
    if (values.length != 2) {
      alert("올바른 페이지 주소가 아닙니다.")
      throw "no 파라미터 값이 누락되었습니다.";
    }
    
    // values[1] --> no=20
    // values2[0] --> no
    // values2[1] --> 20
    var values2 = values[1].split("=");
    if (values2.length != 2 || values2[0] != "no") {
      alert("올바른 페이지 주소가 아닙니다.")
      throw "no 파라미터 값이 누락되었습니다.";
    }
    
    var no = parseInt(values2[1]);
    if (isNaN(no)) {
      alert("페이지 번호가 옳지 않습니다.")
      throw "no 파라미터 값이 숫자가 아닙니다.";
    }

    fetch(`http://localhost:8080/boards/${no}`)
      .then((response) => response.json())
      .then((obj) => {
        if (obj.status == "failure") {
          alert("서버 요청 오류!");
          console.log(obj.data);
          return;
        }
        document.querySelector('#f-no').value = obj.data.no;
        document.querySelector('#f-title').value = obj.data.title;
        document.querySelector('#f-content').value = obj.data.content;
        document.querySelector('#f-createddate').innerHTML = obj.data.createdDate;
        document.querySelector('#f-viewcount').innerHTML = obj.data.viewCount;
      })
      .catch((err) => {
        alert("서버 요청 오류!");
        console.log(err)
      });

    document.querySelector('#update-btn').onclick = (e) => {
      var title = encodeURIComponent(document.querySelector('#f-title').value);
      var content = encodeURIComponent(document.querySelector('#f-content').value);
      var password = document.querySelector('#f-password').value;

      fetch(`http://localhost:8080/boards/${no}`, {
        method: 'PUT',
        headers: {
          'Content-type': 'application/x-www-form-urlencoded'
        },
        body: `title=${title}&content=${content}&password=${password}`
      })
        .then((response) => response.json())
        .then((obj) => {
          if (obj.status == "failure"){
            alert("게시글 변경 오류!\n" + obj.data);
            console.log(obj.data);
            return;
          }
          location.href = "list.html";
        })
        .catch(() => {
          alert("서버 요청 오류!")
          console.log(err);
        });
    };

    document.querySelector('#delete-btn').onclick = (e) => {
      var password = prompt('암호를 입력하세요');
      
      fetch(`http://localhost:8080/boards/${no}`, {
        method: 'DELETE',
        headers: {
          'Content-type': 'application/x-www-form-urlencoded'
        },
        body: `password=${password}`
      })
        .then((response) => response.json())
        .then((obj) => {
          if (obj.status == "failure") {
            alert("게시글 삭제 오류!\n" + obj.data);
            console.log(obj.data);
            return;
          }
          location.href = "list.html";
        })
        .catch(() => {
          alert("서버 요청 오류!")
          console.log(err);
        });
    }
    
    document.querySelector('#list-btn').onclick = (e) => {
      location.href = "list.html";
    }
  </script>
</body>
</html>

 

 

 

backend-app 소스

 

backend-app의 구조는 아래와 같다.

패키지 만들고 myapp에서 복제한다.

dao 패키지 > BoardDao

vo 패키지 > Board 

 

 

app.java
package bitcamp.bootapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RestController;

@CrossOrigin("*")
@SpringBootApplication
@RestController
public class App {

  public static void main(String[] args) {
    SpringApplication.run(App.class, args);
  }

}

 

 

BoardController.java
package bitcamp.bootapp.controller;

import java.sql.Date;
import java.util.HashMap;
import java.util.Map;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import bitcamp.bootapp.dao.BoardDao;
import bitcamp.bootapp.vo.Board;

//@CrossOrigin(origins = "http://127.0.0.1:5500")  // origins = {}, 값 한개일 때 중괄호 생략 가능
@CrossOrigin(origins = {"http://127.0.0.1:5500", "http://localhost:5500"})
@RestController
public class BoardController {

  BoardDao boardDao = new BoardDao();

  @PostMapping("/boards")  // localhost:8080 생략됨
  public Object addBoard(
      @RequestParam(required = false) String title,
      @RequestParam(required = false) String content,
      @RequestParam(required = false) String password) {
    // @RequestParam("title") String title 이렇게 작성해야 하지만 괄호 지정하지 않으면 해당 이름으로 파라미터 찾는다.
    // @String title 처럼 RequestParam 생략 가능. @에서 생략하면 RequestParam 이다.
    // @RequestParam(required = false) String title 처럼 필수가 아니라고 지정할 수 있다. 값이 없어도 에러 띄우지 말도록 한다.

    Board b = new Board();
    b.setTitle(title);
    b.setContent(content);
    b.setPassword(password);
    b.setCreatedDate(new Date(System.currentTimeMillis()).toString());

    this.boardDao.insert(b);

    // 응답 결과를 담을 맵 객체 준비
    Map<String, Object> contentMap = new HashMap<>();
    contentMap.put("status",  "success");

    return contentMap;  // 객체를 던지면 SpringBoot가 json 형식으로 바꾼다.
  }


  @GetMapping("/boards")
  public Object getBoards() {

    Board[] boards = this.boardDao.findAll();

    Map<String, Object> contentMap = new HashMap<>();
    contentMap.put("status",  "success");
    contentMap.put("data",  boards);

    return contentMap;
  }

  @GetMapping("/boards/{boardNo}")
  public Object getBoard(@PathVariable int boardNo) {
    // 주소에 있는 변수를 저장할때는 @PathVariable라 하면 SpringBoot가 저장해준다.

    Board b = this.boardDao.findByNo(boardNo);

    // 응답 결과를 담을 맵 객체 준비
    Map<String, Object> contentMap = new HashMap<>();

    if (b == null) {
      contentMap.put("status", "failure");
      contentMap.put("message", "해당 번호의 게시글이 없습니다.");
    } else {
      contentMap.put("status",  "success");
      contentMap.put("data", b);
    }

    return contentMap;
  }

  @PutMapping("/boards/{boardNo}")
  public Object updateBoard(
      @PathVariable int boardNo,
      @RequestParam(required = false) String title,
      @RequestParam(required = false) String content,
      @RequestParam(required = false) String password) {

    Map<String, Object> contentMap = new HashMap<>();

    Board old = this.boardDao.findByNo(boardNo);
    if (old == null || !old.getPassword().equals(password)) {
      contentMap.put("status",  "failure");
      contentMap.put("data",  "게시글이 없거나 암호가 맞지 않습니다.");
      return contentMap;
    }

    Board b = new Board();
    b.setNo(boardNo);
    b.setTitle(title);
    b.setContent(content);
    b.setPassword(password);
    b.setCreatedDate(old.getCreatedDate());
    b.setViewCount(old.getViewCount());

    this.boardDao.update(b);

    contentMap.put("status", "success");

    return contentMap;
  }

  @DeleteMapping("/boards/{boardNo}")
  public Object deleteBoard(
      @PathVariable int boardNo,
      @RequestParam String password) {
    // 주소에 있는 변수를 저장할때는 @PathVariable라 하면 SpringBoot가 저장해준다.

    Board b = this.boardDao.findByNo(boardNo);

    // 응답 결과를 담을 맵 객체 준비
    Map<String, Object> contentMap = new HashMap<>();

    if (b == null || !b.getPassword().equals(password)) {
      contentMap.put("status", "failure");
      contentMap.put("message", "게시글이 없거나 암호가 맞지 않습니다.");
    } else {
      this.boardDao.delete(b);
      contentMap.put("status",  "success");
    }

    return contentMap;
  }

}

 

 

 


 

 

조언

 

*polyglot : 여러 프로그래 언어를 구사하는 것.

 

 

 


 

과제

 

/