TIL

[멋쟁이사자처럼 부트캠프 TIL 회고] 03.12 ~ 13 오목 2~3일차

Cadi 2025. 3. 14. 01:07

당분간은 자습이다. 

오늘 한 일 

1. 기획 구체화 + 공부

2. 서버 만들어보기 

 

01. 기획 구체화 + 공부당분간은 자습이다. 

 

오늘 한 일 

1. 기획 구체화 + 공부

 

 

 

01. 기획 구체화 + 공부

 

강사님께 지금까지의 회의 결과를 갖고 피드백을 요청드리러 갔다. 

조금 더 구체적인 ( 클래스와 기능들의 이름 정도는 포함한 ) 기획이 있었으면 좋을것 같다고 하셔서 

각자 상세한 기획을 갖고 다음 회의를 하기로 했다. 

 

 

질문 : 모든 라우터 함수를 app.js에 넣을 수도 있지만 , 따로 분리하는 이유는 효율성과 가독성을 위해서인가 ?

 

답변 : 맞다, 구조적으로 코드를 분리하면, 가독성과 유지보수성이 모두 좋아진다. 

 

예를 들어 

// app.js 안에 모든 라우터 로직 직접 작성
app.get('/userinfo', (req, res) => {...});
app.post('/move', (req, res) => {...});

 

모든 라우터 로직을 app.js 파일 안에 넣는다면, 코드가 굉장히 길어질 것이다 .

 

그렇기 때문에 필요한 로직들을 기능에 맞게 분리해 사용하면 좋다.

routes/
 ├── main.js      ← 유저 정보 관련 라우터
 └── game.js      ← 게임 관련 라우터

 

메인 정보를 가져오는 메인 관련 로직 

const express = require('express');
const router = express.Router();

router.get('/userinfo', (req, res) => {
  res.send('유저 정보');
});

module.exports = router;

 

멀티플레이 대전 중 수 놓기를 처리하는 게임 관련 로직  

const express = require('express');
const router = express.Router();

router.post('/move', (req, res) => {
  res.send('수 놓기 처리');
});

module.exports = router;

 

메인관련 로직과, 게임 관련 로직을 모두 받아서 사용한다고 선언하는 app.js 파일 

const mainRouter = require('./routes/main');
const gameRouter = require('./routes/game');

app.use('/main', mainRouter);
app.use('/game', gameRouter);

 

 

질문 : 굳이 하나의 파일을 더 만들어서 구조를 관리하는 이유는 무엇인가 ?

 

답변 : 흔히들 말하는 MVP 패턴을 구현하기 위함.

 

M(Model) : 데이터 구조 정의 및 DB와 소통 담당

V(View) : 사용자에게 보여주는 UI

C(Controller) : 로직 처리 및 기능 실행 담당 

 

 

 

 

02. 혼자 서버 만들어보기

 

대충 서버의 구조도를 그려봤다. 

 

이렇게 될 것 같다. 

 

그래서 일단 app.js 파일부터 손보자면 

// MongoDB와 연결 파트트
var mongoose = require('mongoose');
const app = express();

// MongoDB와 연결하는 함수 정의의
async function connectDB() {
  var databaseURL = "mongodb://localhost:27017/gomokuDB"
  try{
const database = await mongoose.connect(databaseURL, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});
console.log("DB 연결 완료: " + databaseURL);
app.set('mongoose', mongoose.connection);

  //연결 종료 처리리
  process.on("SIGINT", async()=>{
    await database.close();
    console.log("DB 연결 종료");
    process.exit(0);
  });
  } catch (err){
    console.error("DB 연결 실패 + " + err);
    process.exit(1);
  }
}

 

이번엔 조금 변형해서 mongoose로 만들어봤다. 두 모듈의 차이점은 다음과 같다. 

MongoDB 모듈 

  • 로우 레벨 API
  • 성능 높음
  • 기본적 기능 제공

Mongoose 모듈

  • 높은 수준의 추상화된 API
  • 데이터 유효성 검사, 스키마 정의, 모델링 등 추가 기능
  • 성능 약간 낮음

즉, Mongoose 모듈이 조금 더 유저 친화적으로 미리 정의된 기능들을 갖고 사용할 수 있게 되어 있다. 

MongoDB의 모든 기능을 세밀하게 제어하는 경우가 아니라면 Mongoose 모듈만으로 충분하다 .

 

그리고 monogdb 모듈에서는 클라이언트를 정의하고 그 클라이언트에서 db를 호출했었는데

mongoose 모듈에서는 그냥 mongoose.connection으로 쉽게 사용할 수 있다. 

(다른 곳에서도 그냥 mongoose 모델만 import해서 쓰면 연결이 자동으로 된다) 

 

 

이제 , users 파일이다. 

var express = require('express');
var router = express.Router();
const mainController = require('../controllers/mainController');

/* GET users listing. */
router.get('/', function(req, res, next) {
  res.send('respond with a resource');
});

router.get('/getUserInfo', mainController.getUserInfo);
router.post('/addUser', mainController.addUser);
 
module.exports = router;
 

 

간단하게 테스트를 위한 get('/' ~ ) 과 

기능들의 주소를 적어두었다. 

 

다음은 mainController 파일이다. 

 

const users = require('../models/User');

 

exports.addUser = async ( req , res) =>{

    try{
        var userName = req.body.userName;
        var userId = req.body.userId;
        var userPassword = req.body.userPassword;
   
        if (!userName || !userId || !userPassword){
            return res.status(400).send("모든 필드를 입력해주세요");
        }
       
       const existUserName = await users.findOne({userName: userName});
       if (existUserName){
        return res.status(409).send("이미 존재하는 이름입니다");
       }
       const existUserId = await users.findOne({userId: userId});
       if ( existUserId){
        return res.status(409).send("이미 존재하는 Id 입니다");
       }
   
        const newUser = new users(req.body);
        await newUser.save();
        res.send('사용자가 성공적으로 저장되었습니다.');
    }
    catch(err){
        console.log("사용자 추가 중 오류 발생 : " + err)
        res.status(500).send("사용자 추가 중 오류가 발생했습니다.");
    }
   
};

 

 

기본적인 정보를 확인하는 코드와, 사용자 정보를 추가하는 함수다. 

mongodb와 사용하는 방법이 달라서 애를 좀 먹었다. 

 

* 지금 봤는데 정리를 굉장히 잘 해주신 분이 있다. 

https://inpa.tistory.com/entry/ODM-%F0%9F%93%9A-%EB%AA%BD%EA%B5%AC%EC%8A%A4-%EC%82%AC%EC%9A%A9%EB%B2%95-%EC%A0%95%EB%A6%AC

 

[ORM] 📚 Mongoose 사용법 정리 (Node.js - MongoDB)

Mongoose 모듈 몽구스(mongoose)는 시퀄라이즈와는 달리 릴레이션이 아닌 도큐멘트를 사용하므로 ORM이 아니라 ODM (Object Document Mapping) 이라고 불린다. 몽구스는 노드 프로젝트에서 몽고디비를 다루기

inpa.tistory.com

 

 

 

 

유저 저장 완료

저 _id와 __v는 mongoDB상에서 식별하기 위한 id와 버전을 나타내주는 변수로 수정하지 않는 것이 좋다. 

 

 

추가적으로 addUser를 하는 코드에서 문제가 있다 .

 

현재 

exports.getUserInfo = async ( req , res) =>{
    try {
        const user = await users.findOne({ userName: req.query.userName});
        if ( !user){
            res.status(404).send('유저 정보를 찾을 수 없습니다.');
        }
        else{
            console.log("유저 정보 가져감감");
            res.json(user.userName);
        }
    }
    catch(err){
        console.log( "유저 정보를 가져올 수 없음 + " + err);
    }
};

 

이와 같은 코드를 get에서 호출하는데 

router.get('/getUserInfo', mainController.getUserInfo);
 

 

위 방식을 설명하자면, req에서의 body값 즉, userName으로 DB에서 특정 객체를 찾고, 그 객체를 되돌려줌으로써

그 객체의 이름(닉네임), 아이디, 비밀번호, 코인 수 , 승/패  데이터들을 가져가기 위한 방법이었다. 

 

다만 Post와 Get 방식에는 차이가 있어 위 방식으로는 올바르게 데이터를 가져갈 수 없다. 

 

질문 : Post와 Get 방식의 차이 

 

답변 :

 

Get 방식

  • 데이터 전달 위치 : URL 뒤 ?key=value 형식(쿼리스트링)
  • 보안성 : 낮음 ( 주소창에 노출)
  • 용도 : 데이터 조회 (읽기)
  • 서버 코드에서 받는 위치 : req.query
  • 멱등성(idempotent) : 동일한 연산을 여러번 수행하더라도 동일한 결과가 나타남

 

Post 방식

  • 데이터 전달 위치 : HTTP 요청의 body 안의 JSON 혹은 form 데이터
  • 보안성 : 높음 (body에 감춰짐)
  • 용도 : 데이터 생성/수정
  • 서버에서 코드 받는 위치 : req.body
  • 비-멱등성(Non-idempotent) : 동일한 연산을 여러번 수행하면, 다른 결과가 나올 수 있음

Post로도 Get의 역할을 대체할 수 있다. 

 

그렇다면 왜 Post 방식으로 통일하지 않고 , Get 방식을 혼용해서 사용하는가 ?

 

1. 빠르고 가볍다 

: 단순 요청이므로 브라우저나 프록시 서버가 캐싱을 지원해서 속도가 빠르다.

  반면, POST는 항상 서버까지 새로 가야해 무겁다. 

 

2. 공유/링크가 쉬움

: Get은 URL에 정보가 다 들어있기 때문에 링크 공유/북마크가 가능하다. ( 누가 클릭해도 같은 결과가 보인다 ) 

 

3. 의도하지 않은 데이터 변경 금지

: POST는 서버에 데이터를 저장하거나 수정할 수 있기 때문에 데이터 조작 위험이 존재한다. 

  GET은 조회만 하는 기능이므로 서버 상태를 바꾸지 않아 안정성이 올라간다. 

 

4. REST API 설계 원칙 : 메서드에 따라 역할을 명확히 나눔

  • 조회 : GET  (users?userName=홍길동)
  • 생성 : /users ( body에 데이터 포함)
  • 수정 : PUT ( users/: id)
  • 삭제 : DELETE ( users/: id)

이런 식으로 공통 규칙에 따라 만든다면, 협업에도 좋다.

 

 


이 두 가지 방식을 보고 우리가 사용했던 코드를 본다면 왜 내가 했던 GET 방식이 올바르게 작동하지 않았는지 알 수 있다

 

 

router.get('/score', async function(req,res,next){
  try{
    if (!req.session.isAuthenticated){
      return res.status(403).send("로그인이 필요합니다.");
    }
    var userId =req.session.userId;
    var database = req.app.get('database');
    var users = database.collection('users');


    const user = await users.findOne({ _id: new ObjectId(userId)});
    if(!user){
      return res.status(404).send("사용자를 찾을 수 없습니다.");
    }

    res.json({
      id: user._id.toString(),
      username: user.username,
      nickname: user.nickname,
      score: user.score || 0

    });
  } catch (err){
    console.error("점수 조회 중 오류 발생.", err);
    res.status(500).send("서버 오류가 발생했습니다.")
  }
});

 

여기서는 특정한 데이터 ( 원하는 유저이름 ) 을 받아서 실행한 것이 아니라 

세션으로 로그인이 되어 있는지 확인한 후 로그인이 되어있따면 , 그 userId를 바탕으로 데이터를 불러왔다. 

 

그렇기 때문에 내가 새로 작성한 코드에서는 userId를 Body에서 받아오려고 하였으나, 

GET 메서드의 특성상 body 데이터를 전달받는 (req) 방식이 아니기 때문에 null이 되었고, 데이터를 받아오지 못했다. 

 

그렇기 때문에 방식을 변경해서 GET으로 조회할 수 있게 하거나, POST 방식을 사용해야 한다. 

 

질문 : 그럼 비밀번호와 같은 공개되어서는 안되는 정보들은 DB에서 따로 관리하는가 ?

 

GET 방식으로 하려고 하다 보니 의문이 들었다. 

GET 방식은 히스토리에도 남고, 캐싱도 되고 , 보안성도 낮기 때문에 비밀번호와 같은 공개되어서는 안되는 정보들을 

DB 상에서 하나의 데이터로 묶는다면 예를 들어 랭킹정보나 승패정보를 가져올 때 비밀번호도 같이 전달되어

기능이 악용될 수 있다는 생각이 들었다. 그래서 이와같은 질문을 하게 되었다. 

 

답변 : 분리한다. GET으로 조회 가능한 정보와 GET으로 조회 불가능한정보를 다르게 설계한다. 

 

1. 단일 컬렉션 내에서 역할 구분

// User 스키마 예시
const userSchema = new mongoose.Schema({
  userName: String,          // 공개 가능
  win: Number,               // 공개 가능
  lose: Number,              // 공개 가능
  rank: Number,              // 공개 가능

  userId: String,            // 내부 식별자 (공개 불필요)
  userPassword: String,      // 민감 정보
  email: String,             // 민감 정보
});

위와 같은 스키마 구조가 있다면 아래와 같은 방식으로 특정 정보만 받는다. 

User.find({}, 'userName win lose rank') → 민감 정보 제외하고 전송.
// 유저 이름으로 유저 검색, 공개 가능한 필드만 보내기
const user = await User.findOne({ userName: '홍길동' }).select('userName win lose');

2. 컬렉션을 나누는 방식

// 예시
PublicUserInfo: {
  _id: ObjectId("abc123"),
  userName: "홍길동",
  win: 10,
  rank: 5,
}

PrivateUserInfo: {
  userId: ObjectId("abc123"),
  userPassword: "hashed_password",
  phone: "010-1234-5678"
}

 

ObjectId로 식별하고 ,필드를 다르게 둔다. 

 

두 구조 사이에 보안 차이는 거의 없다. ( 실수로 select를 누락하면 문제가 생길 수 있다.)

 

 


그럼 유니티에서 해야될 일은, GET으로 주소를 보낼 때 뒤에 query String을 포함시켜서 (원하는 사람의 userName)을 보내는 것이다. ( 혹은 userName이 중복된다면 objectId나 id 등 중복되지 않는 것으로 ) 

 

 

질문 : 현재 내 코드에서는 서버에서 데이터를 조회할 때 한 번에 조회하고 넘길 때 필요한 정보만 넘기는데, 
          이는 보안상으로 조회 시 필요한 데이터만 가져오는 것과 동일하게 안전한가 ?

 

답변 : 그냥 보내지 않는 것만으로는 100% 안전하다고 할 수 없다. 

애초에 DB에서 민감정보까지 다 읽고 있으므로 , 서버 내부에서라도 유출 위험 존재

혹은 실수로 다른 로직에 민감정보가 포함 될 수 있음.

 

그래서 당연하게도 select()를 하는 것이 중요하고, 더해서 민감정보들은 당연히 암호화 해야 한다. 

 

exports.getUserInfo = async ( req , res) =>{
    try {
        const user = await users.findOne({ userName: req.query.userName});
        if ( !user){
            res.status(404).send('유저 정보를 찾을 수 없습니다.');
        }
        else{
            console.log("유저 정보 가져감감");
            res.json({userName: user.userName, userId:user.userId});
        }
    }
    catch(err){
        console.log( "유저 정보를 가져올 수 없음 + " + err);
    }
};

 

현재 코드 = > 바꾼 코드 (가져올 때부터 민감하지 않은 정보만 가져옴 ) 

* 특이사항 select() 는 필드들을 공백으로 구분한다. 

        const user = await users.findOne({ userName: req.query.userName}).select('userName userId');
 

 

 


MongoDB에서 클러스터를 만들고 , 접속까지 성공했다. 그런데 ... 

질문 : 특정 IP 만 DB에 접속할 수 있다면, 게임의 모든 유저의 IP를 등록해 두어야 하나 ?

랭킹 시스템과 같이 다른 사람의 정보, 혹은 자기 자신의 정보에 접근하기 위해서는 DB를 열람해야 하는데 

DB에 접근하기 위해서는 권한이 필요했다. 혹시 이걸 다 줘야하나.. ? 라는 생각에서 나온 질문이었다.

조금은 멍청한 질문이었을지도.. ?ㅎ

 

답변 : 아니다, 서버의 IP 만 등록해두면, 이용자들은 애플리케이션을 통해 서버에 접속하고, 서버에서 DB에 접근할 수 있다. 

 

결국 그러면 서버라는 것은 하나의 (혹은 여러개의 ) 컴퓨터와 그 안에 소프트웨어 ( 요청을 처리하는 코드들 ) 로 이루어져 있고고, 각 애플리케이션들은 URL 주소를 통해 서버와 소통하며 데이터를 주고받는다. 

 

 


Koyeb과 MongoDB 무료 버전을 통해 아주 간단한 서버를 만들었다. 

내일은 서버를 통해 소통하는 코드를 만들 것이다.

그리고 동아리에 들어가게 되어서 대체과제인 체스 만들기를 할 것이다.