RDBMS는 스키마를 생성하고 데이터를 저장하는 형식을 취하지만 NoSQL은 스키마를 생성하지 않고 데이터를 저장하는 것이 가능
NoSQL은 스키마의 구조가 변경이되도 구조를 변경할 필요없이 데이터 저장이 가능
RDBMS는 데이터의 값으로 다른 테이블의 데이터나 배열을 삽입할 수 없기 때문에 테이블을 분할하고 Foreign Key와 Join의 개념을 이용해서 여러 종류의 데이터를 저장하지만 NoSQL들은 데이터의 값으로 객체나 배열이 가능하기 때문에 하나의 Collection에 여러 종류의 데이터를 저장할 수 있어서 Join을 하지 않아도 되기 때문에 처리 속도가 빠를 수 있음 Join 대신 Embedding이나 Linking의 개념을 사용
RDBMS는 일반적으로 엄격한 트랜잭션을 적용하지만 NoSQL들은 느슨한 트랜잭션을 적용
NoSQL은 복잡한 거래가 없는 경우나 비정형 데이터만을 저장하기 위한 용도로 많이 사용
2) ODM
Relation이라는 개념 대신에 Document를 하나의 객체에 매핑하는 방식
하나의 Document에 대한 모양을 만들고 사용해야 하기 때문에 NoSQL의 Collection도 하나의 정형화된 모양을 가져야 함
MongoDB에서 ODM을 사용할 수 있도록 해주는 대표적인 라이브러리가 mongoose
※ 프로그래밍 언어가 데이터베이스와 연동하는 방식 > 드라이버의 기능만을 이용해서 사용하는 방식 > SQL Mapper : 관계형 데이터베이스에만 존재하는 방식으로 SQL과 프로그래밍 언어 코드의 분리를 이용하는 방식 > ORM이나 ODM 같은 객체 지향 문법을 이용해서 사용할 수 있도록 해주는 방식
관계형 데이터베이스 = 3Line 모두 고려, NoSQL = 1,3Line 고려
Authentication
1. Authentication(인증) 과 Authorizarion(인가)
인증 : 계정 관련, 로그인 관련
인가 : 권한 관련
2. Authentication(인증)을 구현하는 방법
로컬 로그인 : 회원 정보를 저장하고 있다가 인증
회원 정보를 저장할 때는 비밀번호는 복호화가 불가능한 방식을 사용하고 개인을 식별할 수 있는 정보는 마스킹 처리를 하거나 복호화가 가능한 방식의 암호화를 활용해야 함
OAuth(공통된 인증 방식) 로그인 : 다른 서버에 저장된 인증 정보를 활용해서 인증을 하는 방식
3. 인증을 위한 프로젝트 기본 설정
로그인을 할 수 있도록 회원 가입을 하고 로그인 처리를 수행하고 간단한 글을 업로드 할 수 있는 프로젝트
#Project 생성 📁 authentication
#필요한 패키지 설치 > npm install express morgan dotenv compression morgan file-stream-rotator multer cookie-parser express-session express-mysql-session mysql2 sequelize sequelize-cli nunjucks > npm install --save -dev nodemon
#package.json 파일을 수정 📁 authentication -> 📄 package.json
"scripts": {
"start":"nodemon app",
...
#sequelize(node 의 ORM) 초기화 > npx sequelize init
#디렉토리 생성 📁 views : 화면에 출력할 파일이 저장되는 디렉토리 📁 routes : 사용자의 요청이 왔을 때 처리하는(Controller) 라우팅 파일이 저장되는 디렉토리 📁 public : 정적인 파일(resource)들이 저장되는 디렉토리
#Project에 .env 파일을 생성하고 작성 소스 코드에 노출되서 안되는 내용이나 개발 환경에서 운영 환경으로 이행(Migration)할 때 변경될 내용을 작성하는데 이 내용은 실행 중에는 변경되지 않는 내용이어야 함. [Migration : DevOps, CI/CD에서 중요]
대표적인 내용이 데이터베이스 접속 정보 나 암호화를 하기 위한 키 또는 서버 포트 번호와 같은 것들임. 이러한 내용은 대부분 실행 중에는 변경되지 않지만 개발 환경에서 운영 환경으로 이행 할 때 변경될 가능성이 높은 내용임
"PORT 번호 80번을 사용하면 localhost:9000 -> localhost로만 접속 가능"
#App.js 파일을 만들고 기본 설정을 추가
📁 authentication -> 📄 App.js
const express = require('express');
//.env 파일을 읽어서 process.env 에 대입해주는 설정
const dotenv = require('dotenv');
dotenv.config();
//서버 설정
const app = express();
app.set('port', process.env.PORT);
//로그 출력 설정을 위해서 파일을 읽기 위한 모듈 과 경로 설정 모듈
const fs = require('fs');
const path = require('path');
//static 파일의 경로 설정
app.use(express.static(path.join(__dirname, 'public')));
//view template(template engine)
//서버의 데이터를 html 과 합쳐서 다시 html로 변환해주는 라이브러리
const nunjucks = require('nunjucks');
app.set('view engine', 'html');
nunjucks.configure('views', {
express:app,
watch: true,
});
//로그 설정
const morgan = require('morgan');
const FileStreamRotator = require('file-stream-rotator');
const logDirectory = path.join(__dirname, 'log');
// 로그 디렉토리 생성
fs.existsSync(logDirectory) || fs.mkdirSync(logDirectory);
// 로그 파일 옵션 설정
const accessLogStream = FileStreamRotator.getStream({
date_format: 'YYYYMMDD',
filename: path.join(logDirectory, 'access-%DATE%.log'),
frequency: 'daily',
verbose: false
});
// 로그 설정
app.use(morgan('combined', {stream: accessLogStream}));
//출력하는 파일 압축해서 전송
const compression = require('compression');
app.use(compression());
//post 방식에서 form 이 아닌 형태로 데이터를 전송하는 경우 파라미터 읽기
let bodyParser = require('body-parser');
app.use( bodyParser.json()); // to support JSON-encoded bodies
app.use(bodyParser.urlencoded({ // to support URL-encoded bodies
extended: true
}));
//쿠키 설정
const cookieParser = require('cookie-parser');
app.use(cookieParser(process.env.COOKIE_SECRET));
//세션 설정
const session = require("express-session");
let options = {
host :process.env.HOST,
port : process.env.MYSQLPORT,
user : process.env.USERID,
password : process.env.PASSWORD,
database : process.env.DATABASE
};
const MySQLStore = require('express-mysql-session')(session);
app.use(
session({
secret: process.env.COOKIE_SECRET,
resave: false,
saveUninitialized: true,
store : new MySQLStore(options)
})
);
//404 에러가 발생한 경우 처리
app.use((req, res, next) => {
const err = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
err.status = 404;
next(err);
});
//404 이외의 에러가 발생한 경우 처리
app.use((err, req, res, next) => {
res.locals.message = err.message;
res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
res.status(err.status || 500);
res.render('error');
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기 중');
});
디렉토리 안에 index.js 파일을 가져올 때는 디렉토리 이름만 require 하면 됨. App.js : 모든 클라이언트의 요청이 들어오는 곳 - Front Controller routers 디렉토리 안의 js 파일 : 특정 요청에 대한 처리를 수행 - PageController
#기본적인 처리 routes 디렉토리에 page.js 파일을 만들고 메인화면, 프로필 화면 그리고 회원가입 화면을 위한 라우팅 코드를 작성 📁 routes -> 📄 page.js
const express = require('express');
const router = express.Router();
// 공통된 처리 - 무조건 수행
router.use((req, res, next) => {
// 로그인한 유저 정보
res.locals.user = null;
// 게시글을 follow하고 되고 있는 개수
res.locals.followCount = 0;
res.locals.followingCount = 0;
// 게시글을 follow 하고 이는 유저들의 목록
res.locals.followIdList = [];
next();
})
//메인화면
router.get('/',(req, res, next) => {
const twits = [];
// 템플릿 엔진을 이용한 출력
// views 디렉토리의 main.html로 출력
res.render('main', {title:"Node Authentication", twits});
});
//회원가입
router.get('/join', (req, res, next) => {
res.render('join', {title: "회원가입 - Node Authencation"});
});
//프로필 화면 처리
router.get('/profile', (req, res, next) => {
res.render('profile', {title: "나의 정보 - Node Authencation"});
});
module.exports = router;
# App.js 파일에 라우팅 파일을 포함시켜주는 코드를 작성
//라우터 설정
const pageRouter =require('./routes/page');
//여기 설정한 URL과 page.js에 설정된 URL의 조합으로 URL을 설정
app.use ('/',pageRouter);
Routing#공통된 레이아웃을 위한 내용을 views 디렉토리에 layout.html 파일을 만들고 작성 📁 views -> 📄 layout.html
#서버를 실행하고 localhost 로 접속한 후 화면 출력을 확인하고 회원가입 클릭해보기
4. 데이터베이스 작업
1) 테이블 구조
# 회원 테이블 이메일 닉네임 패스워드 로그인 방법 : 직접 로그인 했는지 아니면 카카오로 로그인 했는지 여부를 저장 카카오 아이디 생성 시간 수정 시간 삭제 시간 - 삭제할 때 실제 지워지지 않고 삭제한 시간을 기록
# POST 테이블 게시글 내용 이미지 파일의 경로
# HashTag 테이블 테이블 이름
# 관계 User와 Post는 1:N 관계 HashTag 와 Post는 N:N 관계 User와 User는 N:N 관계
2) models 디렉토리에 user 테이블의 모델을 위한 user.js 파일을 생성하고 작성
📁 models -> 📄 user.js
# 기본 구조
const Sequelize = require('sequelize');
module.exports = class 모델이름 extends Sequelize.Model{
// 테이블에 대한 설정
static init(sequelize){
return super.init({
// 컬럼에 대한 설정
}, {
// 테이블에 대한 설정
});
}
// 관계에 대한 설정
static associate(db){
}
}
const Sequelize = require('sequelize');
module.exports = class User extends Sequelize.Model{
// 테이블에 대한 설정
static init(sequelize){
return super.init({
// 컬럼에 대한 설정
email:{
type:Sequelize.STRING(40),
allowNull:true,
unique:true
},
// 다른 경로의 로그인 방식 때문에 allowNull의 값은 false가 된다.
// "카카오 로그인 시 닉네임은 불러와짐"
nick:{
type:Sequelize.STRING(40),
allowNull:false
},
// 64의 배수
password:{
type:Sequelize.STRING(128),
allowNull:true
},
provider:{
type:Sequelize.STRING(10),
allowNull:false,
defaultValue:'local'
},
snsId:{
type:Sequelize.STRING(50),
allowNull:true
}
}, {
// 테이블에 대한 설정
sequelize,
timestamps:true,
underscored:false,
modelName:'User',
tableName:'snsuser',
paranoid:true,
charset:'utf8',
collate:'utf8_general_ci'
});
}
// 관계에 대한 설정
static associate(db){
db.User.hasMany(db.Post);
db.User.belongsToMany(db.User, {
foreignKey:'followingId',
as:'Followers',
through:'Follow'
});
db.User.belongsToMany(db.User, {
foreignKey:'followerId',
as:'Followings',
through:'Follow'
});
}
}
3) models 디렉토리에 post 테이블의 모델을 위한 post.js 파일을 생성하고 작성
📁 models -> 📄 post.js
const Sequelize = require('sequelize');
module.exports = class Post extends Sequelize.Model{
// 테이블에 대한 설정
static init(sequelize){
return super.init({
// 컬럼에 대한 설정
content:{
type:Sequelize.STRING(200),
allowNull:false
},
img:{
type:Sequelize.STRING(200),
allowNull:true
}
}, {
// 테이블에 대한 설정
sequelize,
timestamps:true,
underscored:false,
modelName:'Post',
tableName:'posts',
paranoid:true,
charset:'utf8mb4',
collate:'utf8mb4_general_ci'
});
}
// 관계에 대한 설정
static associate(db){
//User 와의 관계는 1:N
db.Post.belongsTo(db.User);
//Hashtag 와는 N:M
//다대다 관계는 테이블이 생성되는데 through가 테이블 이름
db.Post.belongsToMany(db.Hashtag, {
through:'PostHashtag'
})
}
}
4) models 디렉토리에 hashtag.js 파일을 생성하고 HashTag에 대한 정보를 설정
📁 models -> 📄 hashtag.js
데이터 베이스 설계 : 저장할 항목 그리고 테이블 간의 관계(1:1인지 1:N인지)
const Sequelize = require('sequelize');
module.exports = class Hashtag extends Sequelize.Model{
// 테이블에 대한 설정
static init(sequelize){
return super.init({
// 컬럼에 대한 설정
title:{
type:Sequelize.STRING(15),
allowNull:false,
unique:true
}
}, {
// 테이블에 대한 설정
sequelize,
timestamps:true,
underscored:false,
modelName:'Hashtag',
tableName:'hashtags',
paranoid:false,
charset:'utf8mb4',
collate:'utf8mb4_general_ci'
});
}
// 관계에 대한 설정
static associate(db){
//다대다
db.Hashtag.belongsToMany(db.Post,{
through:'PostHashtag'
})
}
}
인증 작업 로그인에 성공하면 세션을 생성해서 세션에 아이디나 기타 정보를 저장하고 다음부터 로그인을 확인할 때는 세션의 정보가 있는지를 확인해서 로그인 여부를 판단하고 로그아웃을 하면 세션의 정보를 삭제함
❖ 쿠키와 세션 쿠키 : 클라이언트 (조작이 가능한 정보를 쿠키에 저장해서는 안됨) 세션 : 서버
6. 로컬 로그인 구현
1) 필요한 모듈 설치
> npm install passport passport-local bcrypt (암호화를 해서 비교는 가능하지만 복호화는 안되는 암호화 모듈)
2) App.js 파일에 Passport 모듈을 사용할 수 있는 설정을 추가
📁 authentication -> 📄 App.js
const passport = require('passport');
const passportConfig = require('./passport');
passportConfig();
app.use(passport.initialize());
// 세션 기능은 passport 모듈이 알아서 사용
app.use(passport.session());
3) Passport 모듈 사용 설정
passport 디렉토리 생성
📁 passport -> 📄 index.js
const passport = require('passport');
const local = require('./localStrategy');
const User = require('../models/user');
module.exports = () => {
//login 성공시 정보를 deserializeUser 함수에게 넘기는 함수
passport.serializeUser((user, done) => {
done(null, user.id);
});
// 넘어온 Id에 해당하는 데이터가 있으면 데이터베이스에서 찾아서 세션에 저장
passport.deserializeUser((id, done) => {
User.findOne({where:{id}})
.then(user => done(null, user))
.catch(err => done(err));
});
local();
}
> 이렇게 하면 로그인 여부를 request 객체의 isAuthenticated() 함수로 할 수 있게 됨
4) 로그인 여부를 판단할 수 있는 함수를 routes 디렉토리의 middlewares.js 파일을 추가하고 작성
📁 routes -> 📄 middlewares.js
// 로그인 여부 판단
exports.isLoggedIn = (req, res, next) => {
if(req.isAuthenticated()){
next();
}else{
res.status(403).send('로그인 필요');
}
}
// 로그인하지 않은 경우를 판단
exports.isNotLoggedIn = (req, res, next) => {
if(!req.isAuthenticated()){
next();
}else{
//메시지를 생성하는 query string(parameter)로 사용할 것이라서 encoding을 해주어야 함
const message = encodeURIComponent('로그인 한 상태입니다.');
// 이전 request 객체의 내용을 모두 삭제하고
// 새로운 요청 흐름을 만든것으로 새로고침을하면 결과 화면만 새로고침 됨
res.redirect(`/?error=${message}`);
}
}
❖ 로그인 여부 판단 웹 애플리케이션을 구현하게 되면 로그인 여부를 판단해서 작업을 해야 하는 경우가 발생하는데 이는 비지니스 로직과는 상관이 없고 이런 부분은 별도로 처리해야 함. > Node는 이런 로직을 middleware로 처리 > Java WEB에서는 Filter로 처리 > Spring에서는 AOP나 interceptor를 이용해서 처리
5) routes 디렉토리의 page.js 파일을 수정
📁 routes -> 📄 page.js
const express = require('express');
const {isLoggedIn, isNotLoggedIn} = require("./middlewares");
const router = express.Router();
// 공통된 처리 - 무조건 수행
router.use((req, res, next) => {
// 로그인한 유저 정보
// 유저 정보를 res.locals.user에 저장
res.locals.user = req.user;
// 게시글을 follow하고 되고 있는 개수
res.locals.followCount = 0;
res.locals.followingCount = 0;
// 게시글을 follow 하고 이는 유저들의 목록
res.locals.followIdList = [];
next();
})
//메인화면
router.get('/',(req, res, next) => {
const twits = [];
// 템플릿 엔진을 이용한 출력
// views 디렉토리의 main.html로 출력
res.render('main', {title:"Node Authentication", twits});
});
//회원가입 - 로그인이 되어있지 않은 경우만 수행
router.get('/join', isNotLoggedIn, (req, res, next) => {
res.render('join', {title: "회원가입 - Node Authencation"});
});
//프로필 화면 처리 - 로그인이 되어 있는 경우만
router.get('/profile', isLoggedIn, (req, res, next) => {
res.render('profile', {title: "나의 정보 - Node Authencation"});
});
module.exports = router;
6) 회원 가입, 로그인, 로그아웃 처리를 위한 내용을 routes 디렉토리에 auth.js 파일을 만들고 작성
이 내용은 page.js 에 작성해도 됨
page.js 는 화면을 보여주는 역할을 하고 auth.js는 처리하는 역할을 하도록 분리한 것임
📁 routes -> 📄 auth.js
const express = require("express");
//로그인 및 로그아웃 처리를 위해서 가져오기
const passport = require("passport");
//회원 가입을 위해서 가져오기
const bcrypt = require("bcrypt");
const {isLoggedIn, isNotLoggedIn} = require("./middlewares");
const User = require("../models/user");
const router = express.Router();
//회원 가입 처리 - /auth/join 인데 라우팅 할 때 /auth 추가
router.post('/join', isNotLoggedIn, async(req, res, next)=>{
//데이터 찾아오기
//req.body에서 email, nick, password를 찾아서 대입
const {email, nick, password} = req.body;
try{
//email 존재 여부 확인
const exUser = await User.findOne({where:{email}});
//이미 존재하는 이메일
if(exUser){
//회원 가입 페이지로 리다이렉트하는데
//error 키에 메시지를 가지고 이동
return res.redirect('/join?error=exist');
}else{
//비밀번호를 해싱
const hash = await bcrypt.hash(password, 12);
//저장
await User.create({
email, nick, password:hash
})
return res.redirect('/');
}
}catch(error){
console.error(error);
return next(error);
}
});
//로그인 처리
router.post('/login', isNotLoggedIn, (req, res, next) => {
//passport 모듈을 이용해서 로그인
passport.authenticate('local', (authError, user, info) => {
if(authError){
console.error(authError);
return next(authError);
}
//일치하는 User 가 없을 때
if(!user){
return res.redirect(`/?loginError=${info.message}`)
}
return req.login(user, (loginError) => {
if(loginError){
console.error(loginError);
return next(loginError);
}
//로그인 성공하면 메인 페이지로 이동
return res.redirect('/')
})
})(req, res, next);
})
//로그아웃 처리
router.get('/logout', isLoggedIn, (req, res, next) => {
req.logout((error) => {
if(error){return next(error);}
//세션을 초기화
req.session.destroy();
res.redirect('/');
})
})
module.exports = router;
7) passport 디렉토리에 로컬 로그인을 위한 localStrategy.js 파일을 생성하고 작성