24.10.05(토)
이번 주차는 트위너를 활용해 SignInScene에 스플래시 이미지를 삽입하고,
서버 프로세스의 전반적인 부분과 세션, 그리고 웹 필터에 관해 학습했다.
[ 🖥️ Client Part. ]
1. 유니티 Tweener
Unity에서 트위너(Tweener)는 일반적으로 DOTween과 같은 트위닝(tweening) 라이브러리에서 사용되는 애니메이션 객체를 의미한다. 트위닝은 어떤 값(위치, 크기, 회전, 색상 등)을 시간에 따라 부드럽게 변화시키는 기법으로, DOTween은 Unity에서 가장 많이 사용되는 트위닝 라이브러리 중 하나이다.
우리는 이 트위너를 활용해 로그인 씬에서 스플래시 이미지를 생성할 것이다.
Step 1. SignInScene으로 진입
Step 2. Unity > Window > Asset Store에 진입
Step 3. 에셋 스토어에서 트위너 asset을 찾기 위해 tween을 검색
Step 4. 검색 결과에서 free로 필터를 적용하고, DOTween 페이지의 Download를 클릭하여 다운로드 -> 유니티에서 열기
Step 5. 유니티 에디터의 패키지매니저 창에서 DOTween 페이지의 Download를 클릭하여 다운로드
Step 6. 다운로드가 완료되면 Import 버튼을 눌러 나오는 창에서 그대로 import를 눌러 진행
Step 7. 유틸리티 패널이 나오면 가운데의 setup 버튼을 누르고 나오는 창에서 apply를 눌러 진행
Step 8. 패널의 Get Started를 눌러 나오는 웹 페이지의 도큐먼트를 참고
(https://dotween.demigiant.com/getstarted.php)
Step 9. 스플래시용 이미지를 하나 다운받아 리소스 폴더에 삽입
Step 10. 캔버스 하위에 image 오브젝트를 생성하고 이미지 소스 다운받은 이미지를 넣어주고 적당한 크기로 조절
Step 11. 하이어라키에 Tweener 오브젝트와 Tweener 스크립트 만들기
using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;
public class tweener : MonoBehaviour
{
public Image img;
void Start()
{
img.DOFade(0f, 3f).SetEase(Ease.Linear);
Invoke("FadeIn", 4f);
}
void FadeIn()
{
img.DOFade(1f, 3f).SetEase(Ease.Linear);
}
}
- using DG.Tweening 네임스페이스 상단에 추가
- Tweener class에 public image 변수를 하나 선언
- fade in/out 작업을 위해 도큐먼트에서 fade 메소드를 검색
- DOFade를 사용할 것
- 선언했던 이미지에 DOFade 메소드를 연결
Step 12. Tweener 오브젝트에 스크립트 달아주고, img 파라미터에 이미지 오브젝트 추가
이렇게 하면 스플래시 이미지에 fade 효과가 적용이 완료되었다.
2. CommonDefine
CommonDefine 스크립트는 게임에서 공통적으로 사용하는 값을 통합적으로 관리하기 위한 스크립트이다.
스크립트에 public static class CommonDefine 클래스를 생성하고 내부에 public const int test1 = 5; 구문을 작성한다고 하면,
다른 스크립트에서도 CommonDefine.test1 형식으로 접근할 수 있다.
우리가 사용할 NetworkManager 스크립트의 접근 경로와 같은 경우도 이러한 방식이 필요하다.
NetworkManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.SceneManagement;
public class NetworkManager : MonoBehaviour
{
public IEnumerator ServerCall(string api, List<CommonDefine.serverPacket> packetList)
{
string serviceName = CommonDefine.serverURL + api + "?";
for (int i = 0; i < packetList.Count; i++)
{
var packet = packetList[i];
if (i > 0) serviceName += "&";
serviceName += packet.packetType + "=" + packet.packetValue;
}
UnityWebRequest www = UnityWebRequest.Get(serviceName);
yield return www.SendWebRequest();
if (www.result == UnityWebRequest.Result.Success)
{
string response = www.downloadHandler.text;
if (response == "success")
{
SceneManager.LoadScene("SignInScene");
}
else
{
Debug.LogWarning("로그인 실패: " + response);
}
}
else
{
Debug.LogError("네트워크 오류: " + www.error);
}
}
}
LoginManager.cs
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
public class LoginManager : MonoBehaviour
{
public InputField idField;
public InputField passwordField;
public NetworkManager networkManager;
public void OnClickLoginBtn()
{
string userId = idField.text;
string userPwd = passwordField.text;
List<CommonDefine.serverPacket> packetList = new List<CommonDefine.serverPacket>();
CommonDefine.serverPacket packet1;
packet1.packetType = "userid";
packet1.packetValue = userId;
packetList.Add(packet1);
CommonDefine.serverPacket packet2;
packet2.packetType = "userpwd";
packet2.packetValue = userPwd;
packetList.Add(packet2);
StartCoroutine(networkManager.ServerCall("login", packetList));
}
}
이렇게 스크립트를 작성하고 오브젝트를 만들어준 뒤 연결까지 마무리했다
LoginManager 오브젝트에 Input field, password field, networkmanager 오브젝트 연결해주고
로그인 버튼에 LoginManager의 OnClickLoginBtn 함수까지 연결했다.
그리고 서버가 완성되지 않은 상태에서 id, pwd 필드를 입력하고 로그인 버튼을 누르면 정상적으로 네트워크 오류 메시지가 콘솔에 등장한다.
[ 💾 Server Part. ]
1. 서버 프로세스
이번에는 서버 프로세스가 클라이언트의 요청을 어떻게 처리하는지 즉, 요청이 서버에 도달해서 응답이 나올 때까지의 프로세스에 대해 학습했다.
사용자가 서비스에서 로그인 버튼을 눌러 'http://example.com/login' 요청을 보낸다고 가정해보자.
1. Client 에서 요청을 보냄
: Client ➡️ Web Server로 요청 보냄
2. 보안 게이트
: WAF(Web Application Firewall)에서 악성 요청을 차단하고 Reverse Proxy / Load Balancer에서 트래픽 분산, SSL 처리, 인증 헤더 검사 과정을 거치고 나서,
Web Server는 정적 리소스를 처리 혹은 WAS 로 요청을 포워딩한다. 즉, 이 단계에서 WAS 까지 전달될 자격이 있는지 검사함
3. Web Filter
: WAS 내부에서는 본격적인 로직을 처리하기 전에 Web Filter 또는 Interceptor 같은 중간 필터 계층이 동작한다.
- 공통 로깅
- 인증 토큰 확인
- CORS 설정
- 디코딩 처리
4. 유효성 검사 (Validation)
: WAS에서 Spring, Node.js, Django 등에서 해당 API 로직으로 넘어가기 전에 유효성 검사를 진행한다
- 필수 파라미터가 비어있지 않은지
- 타입이 올바른지
- 길이 제한을 넘지 않았는지
여기서 잘못된 요청은 400 Bad Request 로 바로 응답한다.
5. DB 연결
요청이 유효하면 이제 비즈니스 로직을 시작하기 위해 DB에서 데이터를 조회하거나 저장하는 단계로 넘어간다.
이때 DB에 직접 연결하는 게 아니라 커넥션 풀에서 미리 열어둔 연결(Connection)을 가져온다.
- 미리 만들어둔 DB 연결을 재사용 → 성능 향상
- 동시 요청 처리 가능
커넥션 풀이 없으면 요청마다 연결/해제 작업이 발생해 성능이 심각하게 저하된다.
6. 트랜잭션 처리, 동시성 이슈 해결
여러 개의 DB 작업이 하나의 흐름으로 수행돼야 할 경우 트랜잭션이 필요하다.
정해진 순서의 작업 과정을 보장하고, 만약 문제가 발생한다면 모든 작업을 되돌리는 것(롤백)을 보장한다.
- 주문 생성 → 결제 처리 → 재고 차감 → 모두 성공해야 커밋
- 중간 실패 → 전체 작업 롤백(Rollback)
데이터 무결성 보장을 위한 핵심 절차로 무시하면 동시성 버그, 데이터 꼬임 등이 발생한다.
7. 비즈니스 로직 처리 마무리
8. 에러 처리 및 응답 포장 (Exception Handler / Web Filter)
에러 처리 단계는 요청 처리 중 발생한 예외나 실패 상황에 대해, 어떤 형식으로 응답할지 정의하는 것이다.
즉, 서버에서 문제가 발생했을 때 사용자에게 어떤 메시지를 보낼지? 어떤 HTTP 상태코드를 줄지? (400, 401, 403, 500 등), 응답 본문(body)에 어떤 구조(JSON 등)를 사용할지? 를 명확히 설계하고 처리하는 단계이다.
반면에 Web Filter에서는 정상적인 응답 또는 에러 응답이 클라이언트로 나가는 과정이 이루어진다.
cf) WAF - 캐시서버 - [WEB서버 - WAS - DB] [ ]구성을 3티어(계층) 구성이라고 한다.
ex) aws : WAF - CloudFront - ALB - EC2 – RDS
2. 세션
일반적인 로그인 과정에는 ID와 Password가 필요하다. 서버에서는 로그인 호출이 들어오면 해당 유저가 누구인지를 알아야 하고 로그인 이후에 유저가 자신의 랭킹 정보 등을 요청하려면 유저를 구분할 수 있어야 한다. 유니티의 경우 싱글톤 패턴으로 userInfo를 들고 있어서 ID를 보내 유저를 구분할 수 있으나, 서버는 매번 아이디를 가져오는 방식으로 처리하지 않는다. ID 값은 DB에 저장되어 있고 서버는 일반적으로 로그인을 세션으로 관리한다. 서버는 로그인이 성공하면 세션 아이디라는 것을 전송해 준다. 아이디만 보냈을 때 서버에서 내 정보를 보내주는 방식을 보완해 보고자 나온 것이 세션 아이디이다.
우리는 세션 아이디는 서버에서 Redis에 저장할 것이고 레디스의 기능을 통해서 세션 아이디의 만료 시점을 정할 수도 있다. 만료가 되면 사용자에게 재로그인을 요구하게 되고 이 과정이 세션을 관리하는 방식이다.
사용자가 한 번 로그인을 하면 쿠키에다가 세션 아이디 값을 저장하게 해주고 쿠키의 세션 아이디도 만료 시점이 있다 (서버와 같은 시간 혹은 더 짧게)
보안적 문제를 위해 우리의 프로젝트도 이 세션 체크 방식을 도입할 것이다.
3. 실습
- 백엔드: Node.js
- 데이터베이스: PostgreSQL (사용자 정보 저장)
- 세션 저장소: Redis (세션 ID와 사용자 정보 매핑, 만료 시간 설정)
- 유틸리티: UUID (고유한 세션 ID 생성)
1) 데이터베이스 구조
- PostgreSQL 테이블 구조
1. 사용자 테이블 (users)
CREATE TABLE users (
userid VARCHAR(50) PRIMARY KEY,
level INTEGER NOT NULL,
nickname VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
phone VARCHAR(20)
);
2. 점수 테이블 (scores)
CREATE TABLE scores (
userid VARCHAR(50) PRIMARY KEY,
score INTEGER NOT NULL
);
샘플 데이터
-- 사용자 데이터 삽입
INSERT INTO users (userid, level, nickname, email, phone)
VALUES
('testid1', 1, 'JohnDoe', 'johndoe@example.com', '123-456-7890'),
('testid2', 2, 'JaneSmith', 'janesmith@example.com', '098-765-4321');
-- 점수 데이터 삽입
INSERT INTO scores (userid, score)
VALUES
('testid1', 100),
('testid2', 222);
필요한 패키지 설치
npm install express pg redis uuid dotenv
.env
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=your_password_here
POSTGRES_DB=postgres
REDIS_HOST=localhost
REDIS_PORT=6379
서버 구현
const express = require('express');
const { Pool } = require('pg');
const redis = require('redis');
const { v4: uuidv4 } = require('uuid');
const { promisify } = require('util');
require('dotenv').config();
const app = express();
const port = 3000;
// PostgreSQL 연결 설정
const pgPool = new Pool({
host: process.env.POSTGRES_HOST,
port: process.env.POSTGRES_PORT,
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
database: process.env.POSTGRES_DB
});
// Redis 클라이언트 설정
const redisClient = redis.createClient({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT
});
// Redis 명령어를 Promise로 변환
const redisSetEx = promisify(redisClient.setex).bind(redisClient);
const redisGet = promisify(redisClient.get).bind(redisClient);
// 로그인 엔드포인트
app.get('/login', async (req, res) => {
try {
const { userid, userpwd } = req.query;
// 입력 검증
if (!userid || !userpwd) {
return res.status(400).json({ error: '사용자 ID와 비밀번호가 필요합니다.' });
}
// 실제 구현에서는 비밀번호 검증 로직 필요
// 여기서는 단순화를 위해 사용자 ID만 확인
const userResult = await pgPool.query(
'SELECT userid, level, nickname, email, phone FROM users WHERE userid = $1',
[userid]
);
if (userResult.rows.length === 0) {
return res.status(401).json({ error: '사용자 정보가 없습니다.' });
}
const user = userResult.rows[0];
// 세션 ID 생성 (UUID)
const sessionId = uuidv4();
// Redis에 세션 정보 저장 (30분 만료)
await redisSetEx(
sessionId,
1800, // 30분 (초 단위)
JSON.stringify(user)
);
res.json({
success: true,
message: '로그인 성공',
sessionId,
user: {
userid: user.userid,
nickname: user.nickname,
level: user.level
}
});
} catch (error) {
console.error('로그인 오류:', error);
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
}
});
// 점수 조회 엔드포인트 (세션 인증 필요)
app.get('/score', async (req, res) => {
try {
const { sessionid } = req.query;
if (!sessionid) {
return res.status(401).json({ error: '인증이 필요합니다.' });
}
// Redis에서 세션 정보 조회
const userDataStr = await redisGet(sessionid);
if (!userDataStr) {
return res.status(401).json({ error: '세션이 만료되었거나 유효하지 않습니다.' });
}
const userData = JSON.parse(userDataStr);
// 사용자 점수 조회
const scoreResult = await pgPool.query(
'SELECT score FROM scores WHERE userid = $1',
[userData.userid]
);
if (scoreResult.rows.length === 0) {
return res.status(404).json({ error: '점수 정보가 없습니다.' });
}
res.json({
success: true,
user: {
userid: userData.userid,
nickname: userData.nickname,
level: userData.level
},
score: scoreResult.rows[0].score
});
} catch (error) {
console.error('점수 조회 오류:', error);
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
}
});
// 웹 필터 구현 (미들웨어)
const sessionFilter = async (req, res, next) => {
// /login 경로는 필터 적용 제외
if (req.path === '/login') {
return next();
}
const { sessionid } = req.query;
if (!sessionid) {
return res.status(401).json({ error: '인증이 필요합니다.' });
}
try {
// Redis에서 세션 정보 조회
const userDataStr = await redisGet(sessionid);
if (!userDataStr) {
return res.status(401).json({ error: '세션이 만료되었거나 유효하지 않습니다.' });
}
// 세션 정보를 요청 객체에 추가
req.user = JSON.parse(userDataStr);
next();
} catch (error) {
console.error('세션 필터 오류:', error);
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
}
};
// 모든 요청에 세션 필터 적용 (로그인 제외)
app.use((req, res, next) => {
if (req.path !== '/login') {
return sessionFilter(req, res, next);
}
next();
});
app.listen(port, () => {
console.log(`서버가 http://localhost:${port} 에서 실행 중입니다.`);
});
테스트 방법
http://127.0.0.1:3000/login?userid=testid2&userpwd=123 접속 후
반환받은 세션아이디를 통해 아래처럼 점수 조회 해보기
http://127.0.0.1:3000/score?sessionid={위에서_받은_sessionid}
'Study' 카테고리의 다른 글
[ SJCE 스터디 / 6주차 ] 암호화기법 (1) | 2025.05.07 |
---|---|
[ SJCE 스터디 / 4주차 ] Docker, Redis, Nodejs(Async/Await), Coroutines (0) | 2025.04.29 |
[ SJCE 스터디 / 3주차 ] Unity 씬 전환, DontDestroy, 물리엔진, 싱글톤 (0) | 2025.04.25 |
[ SJCE 스터디 / 2주차 ] 유니티 기초 (라이프 사이클, State Machine , 배경 설정, 델타 타임, Enum) 와 DB (0) | 2025.04.24 |
[ SJCE 스터디 / 1주차 ] 3-Tier Architecture 기반 클라이언트, 서버 개발 시작하기 (2) | 2025.04.24 |