
Game Matching Site
프로젝트 개요
사람들과의 파티를 형성하기 위한 채팅방 생성 및 실시간 채팅 기능, 카테고리 필터 기능, 추천 기능, 게시판 밀 설문조사 기능을 제공하는 플랫폼
수행 업무
게임파티 목록
- 조회 및 필터링, 상세보기, 게임파티 생성, 채팅, 게임파티 추천
- 사용자가 게임파티를 생성하거나 입퇴장 시 입퇴장 로그를 db에 기록
- 게임 파티 목록을 필터링하며 페이징 처리
- 장르별 필터링 시 UI 비동기 전환
채팅룸
- 웹소켓을 활용한 채팅룸 구현
시스템 아키텍처

주요 기능 구현 설명
게임파티방 목록 조회 및 필터링
파티방을 생성하거나 파티방에 입장 시 로그인 체크를 실시했습니다.

인원이 가득찬 파티방이나 인원이 없는 방은 참석할 수 없도록 했습니다.


페이지 하단의 페이지네이션과 함께 상단엔 필터링을 구현했습니다. 옵션 변경시 자동으로 검색결과가 리셋되도록 했습니다.

function checkGenre(roomNum){
var genre = $('#g_genre2').val();
if(genre=='rpg'){
rpgFilter(roomNum);
}else if(genre=='fps'){
fpsFilter(roomNum);
}else if(genre='aos'){
aosFilter(roomNum);
}else if(genre='sport'){
sportFilter(roomNum);
}
return false;
}
function rpgFilter(roomNum) {
var loc = "roomlist?";
if(roomNum !=null){
loc = loc + "page="+roomNum +"&";
}
$('#g_genre').val($('#g_genre2').val());
$('#chk1').val($('#rpg_server').val());
$('#chk2').val($('#rpg_boss').val());
$('#chk3').val($('#rpg_map').val());
$('#chk4').val($('#rpg_stat').val());
console.log($('#g_genre').val());
if ($('#g_genre').val() != "") {
loc = loc + 'g_genre=g_genre:' + $('#g_genre').val() + '&';
}
if ($('#chk1').val() != "") {
loc = loc + 'chk1=rpg_server:'
+ encodeURI($('#chk1').val(), "UTF-8") + '&';
}
if ($('#chk2').val() != "") {
loc = loc + 'chk2=rpg_boss:'
+ encodeURI($('#chk2').val(), "UTF-8") + '&';
}
if ($('#chk3').val() != "") {
loc = loc + 'chk3=rpg_map:'
+ encodeURI($('#chk3').val(), "UTF-8") + '&';
}
if ($('#chk4').val() != "") {
loc = loc + 'chk4=rpg_stat:'
+ encodeURI($('#chk4').val(), "UTF-8") + '&';
}
loc = loc.substr(0, loc.length - 1);
location = loc;
}또한 선택된 게임에 따라서 표시되어야할 필터가 비동기로 세팅되도록 했습니다.
<%@ page language="java" contentType="text/html; charset=EUC-KR"
pageEncoding="EUC-KR"%>
<div class="input-group mb-3" style="float: left;">
<div class="input-group-prepend">
<label class="input-group-text" for="inputGroupSelect01">서버</label>
</div>
<select class="custom-select" name="rpg_server" id="rpg_server" onchange="rpgFilter()">
<option value="" id="none1"></option>
<option value="헤라" id="헤라">헤라</option>
<option value="루나" id="루나">루나</option>
<option value="스카니아" id="스카니아">스카니아</option>
<option value="레드" id="레드">레드</option>
</select>
</div>
<div class="input-group mb-3" style="float: left;">
<div class="input-group-prepend">
<label class="input-group-text" for="inputGroupSelect01">보스</label>
</div>
<select class="custom-select" name="rpg_boss" id="rpg_boss" onchange="rpgFilter()">
<option value="" id="none2"></option>
<option value="슬라임" id="슬라임">슬라임</option>
<option value="루시드" id="루시드">루시드</option>
<option value="시그너스" id="시그너스">시그너스</option>
<option value="파퓰라투스" id="파퓰라투스">파퓰라투스</option>
</select>
</div>
<div class="input-group mb-3" style="float: left;">
<div class="input-group-prepend">
<label class="input-group-text" for="inputGroupSelect01">사냥터</label>
</div>
<select class="custom-select" name="rpg_map" id="rpg_map" onchange="rpgFilter()">
<option value="" id="none3"></option>
<option value="초보자사냥터" id="초보자사냥터">초보자사냥터</option>
<option value="슬리피우드" id="슬리피우드">슬리피우드 깊은곳</option>
<option value="시련의 숲" id="시련의 숲">시련의 숲</option>
<option value="얌얌 아일랜드" id="얌얌 아일랜드">얌얌 아일랜드</option>
</select>
</div>
<div class="input-group mb-3" >
<div class="input-group-prepend">
<label class="input-group-text" for="inputGroupSelect01">요구 스텟 : </label>
</div>
<select class="custom-select" name="rpg_stat" id="rpg_stat" onchange="rpgFilter()">
<option value="" id="none4"></option>
<option value="1500" id="1500">1500</option>
<option value="2500" id="2500">2500</option>
<option value="3500" id="3500">3500</option>
<option value="4500" id="4500">4500</option>
</select>
</div>장르에 따른 파티방 목록을 조회할 수 있게끔 장르별 쿼리를 작성했습니다.
<select id="rpgListPage" resultType="party" parameterType="map">
select * from (
select rownum r_num, a.* from (
select * from party_board where pb_num in (select rpg_num from rpg_party_board where (rpg_server like('%${rpg_server}%') or rpg_server is null )
and (rpg_boss like('%${rpg_boss}%') or rpg_boss is null) and (rpg_map like('%${rpg_map}%') or rpg_map is null ) and (rpg_stat like('%${rpg_stat}%') or rpg_stat is null ))
order by 1 desc) a
) where r_num BETWEEN #{begin} and #{end}
</select>
<select id="fpsListPage" resultType="party" parameterType="map">
select * from (
select rownum r_num, a.* from (
select * from party_board where pb_num in (select fps_num from fps_party_board where (fps_tier like('%${fps_tier}%') or fps_tier is null )
and (fps_match like('%${fps_match}%') or fps_match is null ) and (fps_map like('%${fps_map}%') or fps_map is null ))
order by 1 desc) a
) where r_num BETWEEN #{begin} and #{end}
</select>
<select id="aosListPage" resultType="party" parameterType="map">
select * from (
select rownum r_num, a.* from (
select * from party_board where pb_num in (select aos_num from aos_party_board where (aos_tier like('%${aos_tier}%') or aos_tier is null )
and (aos_match like('%${aos_match}%') or aos_match is null ))
order by 1 desc) a
) where r_num BETWEEN #{begin} and #{end}
</select>
<select id="sportListPage" resultType="party" parameterType="map">
select * from (
select rownum r_num, a.* from (
select * from party_board where pb_num in (select sport_num from sport_party_board where
(sport_match like('%${sport_match}%') or sport_match is null ))
order by 1 desc) a
) where r_num BETWEEN #{begin} and #{end}
</select>게임 장르별 파티방 생성
파티방 생성시 게임 장르를 선택후 게임종류를 선택하고 해당 게임의 추가 옵션을 선택할 수 있도록 했습니다.

장르와 게임을 변경할 때마다 UI가 비동기로 변경되도록 했습니다.
<script>
function onChange() {
var genre = $('#g_genre').val();
if (genre == 'rpg') {
$('#pb_gname').html("<option>메이플스토리</option>");
} else if (genre == 'aos') {
$('#pb_gname').html("<option>롤</option>");
} else if (genre == 'fps') {
$('#pb_gname').html("<option>배틀그라운드</option>");
} else if (genre == 'sport') {
$('#pb_gname').html("<option>피파온라인</option>");
}
var game = $('#pb_gname').val();
$.ajax({
url : 'addRoomGenre?genre='+genre+'&game='+encodeURI(game,"UTF-8"),
success : function(data) {
$('#gameForm').html(data);
}
});
}
</script>
<article>
<form action="" method="post" id="form" onsubmit="return checkGenre();">
<fieldset>
<legend>입력 폼 </legend>
<div class="form-group">
<label>제목 </label><input type="text" id="pb_title" name="pb_title" class="form-control" placeholder="제목을 입력해주세요">
</div>
<div class="form-group">
<label>모집 인원 : </label><input type="number" max="10"
min="2" id="pb_limit" name="pb_limit" class="form-control" placeholder="인원을 입력해주세요">
</div>
<div class="form-group">
<label>게시글 마감시간 : </label><input type="datetime-local" id="pb_valid" name="pb_valid" class="form-control">
</div>
<div class="form-group">
<label>게임 시작시간 : </label><input type="datetime-local" id="pb_begin" name="pb_begin" onchange="maxDate()" class="form-control">
</div>
<div class="form-group">
<label>게임 종료시간 : </label><input type="datetime-local" id="pb_end" name="pb_end" min="" class="form-control">
</div>
<div class="form-group">
<label>상세 요건 : </label>
<textarea class="form-control col-sm-5" rows="10" id="pb_cont" name="pb_cont" placeholder="요구 조건혹은 파티 상세 요건을 적어주세요"></textarea>
</div>
</fieldset>
<label>게임 장르 : </label> <select name="g_genre" id="g_genre"
onchange="onChange()">
<option value="rpg">rpg</option>
<option value="aos">aos</option>
<option value="fps">fps</option>
<option value="sport">sport</option>
</select>
<hr>
<label>게임 종류 : </label> <select name="pb_gname" id="pb_gname" onchange="onChange()">
<option>메이플스토리</option>
</select>
<div id="gameForm">
</div>
<input type="submit" value="방 만들기">
<input type="button" value="방 목록" onclick="location.href='roomlist'">
</form>
</article><%@ page language="java" contentType="text/html; charset=EUC-KR"
pageEncoding="EUC-KR"%>
<div class="input-group mb-3" style="float: left;">
<div class="input-group-prepend">
<label class="input-group-text" for="inputGroupSelect01">서버</label>
</div>
<select class="custom-select" name="rpg_server" id="rpg_server">
<option value=""></option>
<option value="헤라">헤라</option>
<option value="루나">루나</option>
<option value="스카니아">스카니아</option>
<option value="레드">레드</option>
</select>
</div>
<div class="input-group mb-3" style="float: left;">
<div class="input-group-prepend">
<label class="input-group-text" for="inputGroupSelect01">보스</label>
</div>
<select class="custom-select" name="rpg_boss" id="rpg_boss">
<option value=""></option>
<option value="슬라임">슬라임</option>
<option value="루시드">루시드</option>
<option value="시그니스">시그니스</option>
<option value="파퓰라투스">파퓰라투스</option>
</select>
</div>
<div class="input-group mb-3" style="float: left;">
<div class="input-group-prepend">
<label class="input-group-text" for="inputGroupSelect01">사냥터</label>
</div>
<select class="custom-select" name="rpg_map" id="rpg_map">
<option value=""></option>
<option value="초보자사냥터">초보자사냥터</option>
<option value="슬리피우드">슬리피우드 깊은곳</option>
<option value="시련의 숲">시련의 숲</option>
<option value="얌얌 아일랜드">얌얌 아일랜드</option>
</select>
</div>
<div class="input-group mb-3" >
<div class="input-group-prepend">
<label class="input-group-text" for="inputGroupSelect01">요구 스텟 : </label>
</div>
<select class="custom-select" name="rpg_stat" id="rpg_stat">
<option value=""></option>
<option value="1500">1500</option>
<option value="2500">2500</option>
<option value="3500">3500</option>
<option value="4500">4500</option>
</select>
</div>파티방 생성 시 이를 처리하는 컨트롤러입니다.
@RequestMapping(value = "/rpg", method = RequestMethod.POST)
public String rpgAddInf(PartyBoardVO pvo, RpgPartyBoardVO rvo, HttpSession session, HttpServletRequest request,
Model mav) {
session.setAttribute("roomBoss", "boss");
pvo.setPb_valid(pvo.getPb_valid().replaceAll("T", " "));
pvo.setPb_end(pvo.getPb_end().replaceAll("T", " "));
pvo.setPb_begin(pvo.getPb_begin().replaceAll("T", " "));
pvo.setPb_id((String) session.getAttribute("sessionID"));// 세션에 저장된 정보를 입력
// PartyLogVO lvo = new PartyLogVO();
// lvo.setPl_id((String) session.getAttribute("sessionID"));// 세션에 저장된 정보를 입력
partyService.addParty(pvo, rvo);
int roomNum = partyService.getNum();
session.setAttribute("inRoom", roomNum); // 현재 내가 들어간 방
// System.out.println(map.get(roomNum).get(0).getAttribute("id"));
String genre = partyService.getGenre(roomNum);
System.out.println("장르: "+genre);
mav.addAttribute("genre", genre);
mav.addAttribute("vo", partyService.getDetail(genre, roomNum));
mav.addAttribute("roomNum", roomNum);
end = request.getRequestURL().lastIndexOf("/");
url = request.getRequestURL().substring(7, end);
System.out.println("url: " + url);
mav.addAttribute("url", url);
return "simwha/room";
}<insert id="add" parameterType="party">
insert into party_board
values(pb_seq.nextVal,#{pb_id},#{pb_gname},#{pb_title},#{pb_limit},'모집 중',
to_date(#{pb_valid},'yyyy-mm-dd
hh24:mi'),to_date(#{pb_begin},'yyyy-mm-dd
hh24:mi'),to_date(#{pb_end},'yyyy-mm-dd hh24:mi'),
#{pb_cont},sysdate)
</insert>
<insert id="addRPG" parameterType="rpg">
<selectKey keyProperty="rpg_num" resultType="int"
order="BEFORE">
select pb_seq.currVal from dual
</selectKey>
insert into rpg_party_board
values(#{rpg_num},#{rpg_server},#{rpg_boss},#{rpg_map},#{rpg_stat})
</insert>파티 입장/퇴장 및 채팅
파티에 입장하게 되면 해당 방에 입장한 유저들끼리 실시간 채팅이 가능합니다.
- 새로운 유저가 입장하면 화면에 해당 유저의 프로필 사진이 표시됩니다.
- 채팅을 입력하면 자신의 화면과 상대의 화면에 입력한 글이 출력됩니다.

WebSocket Handler를 등록하여 room.jsp와 Spring 서버 간에 웹소켓 통신이 가능하도록 했습니다.
이후 유저의 프로필 이미지와 아이디 혹은 유저가 보낸 채팅 메세지를 WebSocketSession에 보내어 참가한 유저의 정보가 실시간으로 파티방에 표시되도록 했습니다.


파티방을 새롭게 만들거나 참가했을 때 "join"이라는 메세지를 서버에 전달하면서 웹소켓 통신이 시작되는데 유저의 액션에 따라서 파티방에 일어날 이벤트가 다르므로 이에 대한 분기처리를 설정해두었습니다.
파티방 입장
1. 파티방 생성
파티방의 고유번호가 파티방 목록 보관 Map에 없다면 새롭게 파티방을 만든 것으로 판단했습니다.
파티방을 만든 현재 유저의 세션을 고유번호에 매핑하여 ArrayList로 보관했습니다.

2. 파티방 참가
어떤 유저가 방에 입장했을 때 새로 입장한 유저와 기존에 있던 유저들에게는 다른 정보를 전달해야 합니다.
- 새로 입장한 유저에게는 파티방의 고유번호에 해당 되는 session 목록을 조회하여 모든 유저의 정보를 전달합니다.
- 기존 유저들에게는 새로 입장한 유저의 정보만 전달합니다.

3. 유저가 파티룸으로 돌아왔을 때
유저가 파티룸을 퇴장하지 않고 목록을 둘러보다 자신의 파티룸으로 다시 들어왔을 경우, 새로 들어온 유저가 기존에 참가되어있던 유저인지 검사 후 새로운 웹소켓 세션을 부여합니다.

파티 방에 입장 후 다른 파티방 목록 조회 시 유저가 현재 소속 되어있는 파티방에는 별도 표시를 해두었으며, 참여 불가능한 파티방에 입장 시도 시 앨럿을 띄우도록 했습니다.

파티방 퇴장
퇴장 시 방장과 일반 참여자 모두 연결되어있던 WebSocketSession을 삭제합니다.
하지만 방장과 일반 참여자가 퇴장 했을 때 발생하는 이벤트는 다릅니다.
1. 방장
만약 방장이 현재 파티방에서 나갔다면 접속중인 다른 유저에서 알림창이 뜨게 하여 모두 목록으로 이동하도록 했습니다.


2. 일반 참여자
일반 참여자가 퇴장 했을 경우(방장이 퇴장하여 일반 참여자도 나가게 되는 경우 포함) 자신이 퇴장할 경우 별도 처리가 없지만, 다른 참여자가 퇴장할 경우 그 유저의 퇴장 정보를 전달 받게 됩니다.
퇴장한 유저의 프로필이 삭제되며, 인원이 만석인 파티방이었다면 모집상태를 변경하게 됩니다.

파티방 입/퇴장 로깅
aop를 활용하여 입장과 퇴장에 대한 로그를 남기도록 했습니다.
@Component
@Aspect
public class PartyLogAdvice {
@Autowired
private PartyService partyService;
//인터셉터를 사용해서 로그를 남길 것이다.
@Around("execution(* kr..PartyController.*Inf(..))")
public String partyLogger(ProceedingJoinPoint pjp) {
String rpath = null;
String methodName = pjp.getSignature().getName();
PartyLogVO lvo = new PartyLogVO();
HttpSession session = null;
Model m = null;
Object[] fd = pjp.getArgs();
for(Object e: fd) {
if(e instanceof Model) {
m = (Model) e;
continue;
}if(e instanceof HttpSession) {
session = (HttpSession) e;
continue;
}
}
int alRoom;
if(session.getAttribute("inRoom") == null) { //내가 기존에 어떤 방에 있었는가 분별 -> null: 방에 안 소속
alRoom = 0;
}else {
alRoom = (int) session.getAttribute("inRoom");
}
try {
rpath = (String) pjp.proceed();
String id = (String) session.getAttribute("sessionID");
lvo.setPl_id(id);
if (alRoom == 0) { //만약 내가 아무 방에도 안 소속되어있다면
if(methodName.contains("AddInf")) {
Map<String, Object> mMap = m.asMap();
int roomNum = (Integer) mMap.get("roomNum");
lvo.setPl_phnum(roomNum);
}else if (methodName.contains("partyInInf")) {
lvo.setPl_phnum((int) fd[0]);
}
lvo.setPl_status("IN");
partyService.partyInoutLog(lvo);
}else if(methodName.equals("partyOutInf")) {
lvo.setPl_phnum((int) fd[0]);
lvo.setPl_status("OUT");
partyService.partyInoutLog(lvo);
}
} catch (Throwable e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("Test");
return rpath;
}
}<insert id="inoutLog" parameterType="plog">
insert into party_log values(pl_seq.nextVal,#{pl_phnum}, #{pl_id },
to_date(to_char(sysdate,'yyyy-mm-dd hh24:mi'), 'yyyy-mm-dd hh24:mi'),#{pl_status})
</insert>레슨런
실시간 통신과 웹소켓
실시간으로 데이터가 끊김 없이 전달되는 과정이 늘 궁금했기에, 이번 프로젝트에서는 실시간 채팅 기능을 직접 구현해보았습니다. 기존의 HTTP 프로토콜에서는 한계로 느꼈던 단방향 구조를 넘어, WebSocket의 양방향 통신 방식을 경험할 수 있었습니다. 이 경험을 통해 실시간 소통의 원리를 체감했고, 앞으로 더욱 즉각적이고 생동감 있는 서비스를 만들고 싶다는 목표가 생겼습니다. 사용자가 직관적으로 느낄 수 있는 실시간의 가치를 전달하는 개발자가 되고자 합니다.
비동기 렌더링과 사용자 경험(UX)
전체 화면이 새로고침될 때마다 사용자가 느끼는 부담을 줄이고자, 비동기 통신 기반의 부분 렌더링을 도입했습니다. 작은 기술적 선택이 사용자의 경험을 얼마나 크게 바꿀 수 있는지 직접 확인할 수 있었고, 그만큼 사용자 중심의 개발이 중요하다는 걸 배웠습니다. 앞으로도 성능을 높이면서 부드러운 인터랙션까지 고려해, 늘 사용자를 우선에 두는 뷰 구현에 앞장서겠습니다.
아키텍처의 중요성과 객체지향 원칙
프로젝트가 점점 커지면서, 유지보수에 대한 고민이 커졌습니다. 이에 MVC 패턴과 레이어드 아키텍처를 현업에서 적용해보니, 코드의 캡슐화와 단일 책임 원칙이 얼마나 큰 신뢰와 안정성을 주는지 몸으로 느꼈습니다. 확장성 있는 구조를 고민하고, 동료 개발자도 쉽게 이해하고 관리할 수 있는 튼튼한 코드 베이스를 만들어가는 것이 저만의 개발 원칙이 되었습니다.
서버 동시성 제어와 WAS 스레드 풀
수많은 클라이언트가 몰려올 때 서버는 어떻게 효율적으로 요청을 처리할까 하는 궁금증에서부터 공부를 시작했습니다. 단순히 요청마다 스레드를 생성하는 것이 아닌, WAS가 스레드 풀을 활용해 자원 소모를 최소화하고 효율적으로 관리하는 구조임을 명확하게 이해할 수 있었습니다. 이 과정은 기능 구현에만 머무르지 않고, 시스템 전체 성능과 안정성까지 고려하는 개발자로 성장하는 데 밑거름이 되었습니다.
예외 상황과 데이터 정합성 관리
복잡한 로직 작업 중 예외가 발생하면 데이터에 치명적인 오류가 남을 수 있다는 점이 늘 걱정이었습니다. 이를 해결하고자 트랜잭션을 도입하며 롤백뿐만 아니라 전파 속성, 타임아웃, 격리 수준 등 세부적인 데이터 무결성 보장 방법도 깊이 배우게 되었습니다. 보이지 않는 곳의 예외까지 제어하는 노력을 통해, 언제 어떤 상황에서도 안정적인 시스템을 만드는 개발자로 성장해가고 있습니다.
함께 문제를 해결하는 협업의 가치
프로젝트에서 채팅 도메인이라는 중요한 역할을 맡게 됐을 때, 처음에는 막막함도 있었지만 팀원들과 끊임없이 소통하며 아이디어를 나누고, 시행착오를 공유하는 과정에서 혼자서는 얻지 못할 협업의 시너지를 경험했습니다. 덕분에 복잡한 문제도 함께 해결할 수 있었고, 앞으로도 동료들과 적극적으로 의견을 교환하며, 어떤 도전 앞에서도 함께 길을 찾는 신뢰받는 팀원으로 성장해 나가겠습니다.