개발자입니다
[비트캠프] 47일차(10주차2일) - Java: (backend, frontend)-app-02 본문
[비트캠프] 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 : 여러 프로그래 언어를 구사하는 것.
과제
/
'네이버클라우드 AIaaS 개발자 양성과정 1기 > Java' 카테고리의 다른 글
[비트캠프] 49일차(10주차4일) - Java: (backend, frontend)-app-04~05, 상속, 생성자 (0) | 2023.01.12 |
---|---|
[비트캠프] 48일차(10주차3일) - Java: (backend, frontend)-app-03 (0) | 2023.01.11 |
[비트캠프] 46일차(10주차1일) - Java(캡슐화, getter/setter, 접근 범위), app-11~13, backend-app-01~02 (0) | 2023.01.09 |
[Java] 예제 소스 정리 - 생성자 활용, 인스턴스 메서드와 클래스 메서드 활용 (0) | 2023.01.06 |
[비트캠프] 45일차(9주차5일) - Java: myapp-08~10 (0) | 2023.01.06 |