프로젝트

[교내 프로젝트] 오픈소스를 활용하여 테트리스 게임 개발하기 (2)

jeongpil 2021. 7. 7. 15:37

1편에 이어 2편에서는 코드를 수정하고 개발한 과정과 디버깅한 과정, 마지막으로 github에 최종 업로드한 과정을 포스팅하도록 하겠습니다.

 

 

3. 코드 수정 및 개발

 

3-1. 협업을 위한 git 협업규칙 정하기

 

코드를 협업해서 개발하기 위해서는 git이 필수적입니다. 저희 팀이 정한 git 협업규칙은 다음과 같습니다.

 

  1. Fork한 개인 Repository를 Local로 가져와서 자신이 구현할 기능 이름으로 branch를 생성하고 해당 branch로 이동한 뒤 작업을 한다. 주의사항: 반드시 main branch에서 branch를 생성해야 한다.
  2. 작업을 한 뒤 개인 Repository에 add, commit, push를 한 후 Main Repository로 pull request한다.
  3. 팀원들은 pull request를 확인한 후 Main Repository로 merge할지 결정한다. 팀원 모두 Approve할 경우 merge한다.
  4. Main Repository 변경되었으므로 다른 팀원들도 자신의 Local Repository 동기화해야 한다. 주의사항: main branch 이동한  변경된 최신내용을 받아와야 한다. (git pull upstream main)

git 덕분에 코드를 주고 받을 필요 없이 같은 파일을 동시에 수정하는 것이 가능했고

 

코드를 점진적으로 개발해나가면서 프로젝트를 효과적으로 관리할 수 있었습니다.

 

 

3-2. 팀원들이 진행한 부분

 

코드 수정 및 개발을 시작할 때 각자 역할을 나누어 진행했습니다.

 

각자 역할을 나누긴 했지만 막히는 부분이 있거나 오류가 발견되면 서로 도와서 작업을 진행했습니다.

 

본 포스팅에는 팀원들이 주로 맡아서 진행한 부분은 간략하게 적고 제가 주로 맡아서 진행한 부분에 대해서만 자세하게 적도록 하겠습니다.

 

  • 캐릭터 선택 기능 추가 및 캐릭터 진화 기능 구현
  • 테마 및 bgm 선택 기능
  • 기존 프로젝트의 오류 해결
  • 다양한 게임 모드 추가

 

저를 제외한 팀원 2명이 위 4가지를 나눠서 맡아 진행했습니다. 

 

캐릭터는 총 3가지로 선택할 수 있게 하였고 고전 게임인 테트리스의 느낌을 살려 캐릭터를 픽셀로 직접 만들었습니다.

 

테마와 bgm은 각각 두가지의 선택지를 제공하였습니다.

 

 

캐릭터 선택 화면

 

저희 캐릭터 귀엽지 않나요? ㅎㅎ

 

이렇게 게임 안의 캐릭터, 테마, bgm에 선택지를 제공하여 각자의 취향에 맞게 게임을 즐길 수 있도록 하였습니다!

 

 

 

3-3. 내가 진행한 부분

 

 

3-3-1. AWS의 RDS와 MySQL을 이용한 회원가입/로그인 기능

 

 

로그인 서비스를 구현하기 위해 계정 정보를 저장하기 위한 데이터베이스가 필요했습니다.

 

데이터베이스는 AWS의 RDS 프리티어를 사용하였고 데이터베이스 엔진은 MySQL을 사용하였습니다.

 

데이터베이스

 

 

기존 프로젝트에서도 AWS의 RDS를 데이터베이스로 이용하여 전체 랭킹 시스템을 구현해놨습니다.

 

저는 이 코드를 발전시켜 자신의 계정을 데이터베이스에 저장할 수 있도록 하였습니다.

 

회원가입할 때 id와 password를 데이터베이스에 저장하고 로그인할 때 입력한 id와 password를 데이터베이스의 정보와 비교하는 코드를 구현했습니다.

 

import pymysql
import bcrypt

class Database:
    def __init__(self):
        self.score_db = pymysql.connect( #데이터베이스
            user='----',
            password='----',
            host='-----',
            db='----',
            charset='utf8'
        )

    def id_not_exists(self,input_id): # 아이디가 데이터베이스에 존재하는지 확인
        curs = self.score_db.cursor(pymysql.cursors.DictCursor)
        sql = "SELECT * FROM users WHERE user_id=%s"
        curs.execute(sql, input_id)
        data = curs.fetchone()
        curs.close()
        if data:
            return False
        else:
            return True
            
    
    def add_id_data(self,user_id): # 아이디 추가
        #추가하기
        curs = self.score_db.cursor()
        sql = "INSERT INTO users (user_id) VALUES (%s)"
        curs.execute(sql, user_id)
        self.score_db.commit()  #서버로 추가 사항 보내기
        curs.close()

    def add_password_data(self,user_password,user_id): # 비밀번호 추가
        #회원가입시 초기 경험치값은 0으로 설정
        #추가하기
        initial_exp=0
        #해싱과 솔팅
        new_salt=bcrypt.gensalt()
        new_password=user_password.encode('utf-8')
        hashed_password=bcrypt.hashpw(new_password,new_salt)
        decode_hash_pw=hashed_password.decode('utf-8')
        curs = self.score_db.cursor()
        sql = "UPDATE users SET user_password= %s WHERE user_id=%s"
        curs.execute(sql,(decode_hash_pw,user_id))
        self.score_db.commit()  #서버로 추가 사항 보내기
        curs = self.score_db.cursor()
        sql = "UPDATE users SET user_exp= %s WHERE user_id=%s"
        curs.execute(sql, (initial_exp, user_id))
        self.score_db.commit()
        curs.close()

    def compare_data(self, id_text, pw_text): # 로그인 시 데이터베이스의 아이디와 비밀번호 비교
        input_password=pw_text.encode('utf-8')
        curs = self.score_db.cursor(pymysql.cursors.DictCursor)
        sql = "SELECT * FROM users WHERE user_id=%s"
        curs.execute(sql,id_text)
        data = curs.fetchone()
        curs.close()
        check_password=bcrypt.checkpw(input_password,data['user_password'].encode('utf-8'))
        return check_password

 

 

 

3-3-2. 계정의 보안을 위한 비밀번호 Hashing & Salting

 

 

로그인 서비스를 구현하기 위해서 보안성 문제로 인해 비밀번호를 암호화해서 데이터베이스에 저장해야 했습니다.

 

이를 위해서  Hashing  Salting이 필요했습니다.

 

회원가입 할 때 비밀번호를 Hashing 과 Salting을 통해 암호화해서 데이터베이스에 저장하고

 

로그인을 할 때 입력한 비밀번호의 Hash값이 데이터베이스에 저장된 Hash값과 동일한지 비교하고

 

같으면 로그인에 성공하는 방식으로 회원가입 및 로그인 서비스를 구현했습니다.

 

하지만 해커가 무한히 임의의 값을 입력하여 비밀번호를 알아낼 수도 있기 때문에

 

이를 보완하기 위해 비밀번호에 소금을 치는 Salting이란 방법을 같이 이용했습니다.

 

Salting이란 랜덤 텍스트를 비밀번호에 섞어서  해쉬 값을 무작위로 만들어 비밀번호를 찾기 어렵게 만드는 것이라고 볼 수 있습니다.

 

 

저는 파이썬의 password hashing 라이브러리인 bcrypt를 사용했습니다.

 

bcrypt란 데이터베이스에 비밀번호와 같이 암호화가 필요한 데이터를 쉽게 다룰 수 있도록 해주는 라이브러리입니다.

 

pip install bcrypt를 통해 bcrypt 라이브러리를 설치하고 import bcrypt를 하고 진행하면 됩니다.

 

 

1. bcrypt.hashpw()

 

이 함수는 String값의 비밀번호를 암호화 해주는 함수이고 String값과 Salt 두 가지 인자를 받습니다.

 

Salt는 bcrypt.gensalt()를 통해 자동으로 랜덤 값을 받을 수 있습니다.

 

password='1234'
hashed_password = bcrypt.hashpw(password,bcrypt.gensalt())

 

이렇게 하면 TypeError: Unicode-Objects must be encoded before hashing 이란 에러가 출력 될 겁니다.

 

입력받은 비밀번호가 utf-8 유니코드로 되어있는데 bcrypt 인자는 bytes로 입력을 받기 때문에

 

비밀번호를 bytes형으로 바꿔줘야 합니다.

 

hashed_password = bcrypt.hashpw(password.encode('utf-8'),bcrypt.gensalt())

 

이렇게 하면 Hashing된 비밀번호를 생성할 수 있습니다.

 

데이터베이스에 Hashing된 비밀번호를 저장하기 위해서는 다시 bytes형에서 String으로 바꿔줘야 합니다.

 

decode_hashed_password = hashed_password.decode('utf-8')

 

이제 이 decode_hashed_password를 데이터베이스에 저장하면 됩니다.

 

데이터베이스에 저장된 모습은 다음과 같습니다.

 

 

암호화된 비밀번호가 데이터베이스에 저장된 모습

 

 

※ 저는 MySQL에서 user_password  Column을 VARCHAR(45)로 지정하였는데 암호화된 비밀번호가

 

45자를 넘어서 계속 오류가 발생했습니다...이 원인을 찾기까지 3시간이 걸렸습니다...단순한 건데...

 

VARCHAR(100)으로 변경하니 해결되었고 여러분들은 이런 실수를 안하시기를 바랍니다 : )

 

 

2. bcrypt.checkpw()

 

이제 비밀번호를 암호화해서 회원가입을 했으니

 

로그인을 할 때 내가 입력한 비밀번호와 Hashing과 Salting을 통해 암호화된 비밀번호를 비교해서 로그인 할 수 있는 과정이 필요하겠죠?

 

bcrypt 라이브러리에서는 이 과정을 checkpw() 라는 함수를 통해 가능하게 해 줍니다.

 

이 함수는 로그인할 때 입력받는 비밀번호와 데이터베이스에 저장된 암호화된 비밀번호 두 개를 인자로 받습니다.

 

input_password='1234'
bcrypt.checkpw(input_password,decode_hashed_password)

 

이렇게 하면 또!  TypeError: Unicode-Objects must be encoded before checking이라는 에러가 발생합니다.

 

hashing할 때와 똑같은 이유입니다. 유니코드가 아닌 bytes형 값을 입력해야 합니다. 

 

bcrypt.checkpw(input_password.encode('utf-8'),decode_hashed_password.encode('utf-8'))

 

이렇게 둘 다 bytes형 값을 인자로 넣어줘야 합니다. 위의 값은 boolean값으로 True or False로 반환되고

 

이 값이 True면 로그인이 성공하는 겁니다.

 

 

 

3-3-3. 모드별 랭킹 구현 및 랭킹 실시간 업데이트

 

 

모드별 랭킹을 구현하기 위해 MySQL에 각 모드별 테이블을 생성했습니다.

 

 

 

 

모드별 테이블을 생성해주고 게임오버 될 시에 점수를 데이터베이스에 저장되도록 하였습니다.

 

점수를 데이터베이스에 저장하기 위한 코드는 다음과 같습니다.

 

  • 게임오버 함수
    if self.board.game_over():
        Var.base_bgm.stop()
        Var.base_bgm2.stop()
        Var.ai_bgm.stop()
        Var.game_over.play()
        self.Score = self.board.score
        self.database.add_score_data(self.mode, Var.user_id, self.Score) #게임오버 시 데이터베이스에 점수 저장
        self.board.show_my_score()
        break

 

  • 데이터베이스에 점수 추가하기
    def add_score_data(self,game_mode,  ID, score):
        #데이터베이스에 모드별 점수 추가하기
        curs = self.score_db.cursor()
        if game_mode == 'basic':
            sql = "INSERT INTO original_score (ID, score) VALUES (%s, %s)"
        elif game_mode == 'hard':
            sql = "INSERT INTO hard_score (ID, score) VALUES (%s, %s)"
        elif game_mode == 'two':
            sql = "INSERT INTO twohands_score (ID, score) VALUES (%s, %s)"
        elif game_mode == 'mini':
            sql = "INSERT INTO mini_score (ID, score) VALUES (%s, %s)"
        elif game_mode == 'big':
            sql = "INSERT INTO big_score (ID, score) VALUES (%s, %s)"
        elif game_mode == 'ai':
            sql = "INSERT INTO ai_score (ID, score) VALUES (%s, %s)"
        curs.execute(sql, (ID, score))
        self.score_db.commit()  #서버로 추가 사항 보내기
        curs.close()

 

점수를 불러와서 상위 5개를 랭킹에 나타내는 코드는 다음과 같습니다.

 

  • 데이터베이스에서 점수 불러오기
    def load_data(self, game_mode): #점수 데이터 불러오기
        self.database.__init__()
        curs = self.database.score_db.cursor(pymysql.cursors.DictCursor)
        if game_mode == 'easy':
            sql = "SELECT * FROM original_score ORDER BY score DESC "
        elif game_mode == 'hard':
            sql = "SELECT * FROM hard_score ORDER BY score DESC "
        elif game_mode == 'two':
            sql = "SELECT * FROM twohands_score ORDER BY score DESC"
        elif game_mode == 'mini':
            sql = "SELECT * FROM mini_score ORDER BY score DESC"
        elif game_mode == 'big':
            sql = "SELECT * FROM big_score ORDER BY score DESC"
        curs.execute(sql)
        data = curs.fetchall() #리스트 안에 딕셔너리가 있는 형태
        curs.close()
        return data

 

  • 상위 5개 점수 보여주는 랭크 화면
    def Single_the_rank(self): #기본 이지 모드 랭크 보는 화면
        self.surface = pygame.display.set_mode((self.w, self.h), RESIZABLE)
        self.menu = pygame_menu.Menu(self.h, self.w, '', theme=self.mytheme)
        self.page='page7'
        Var.click.play()
        self.menu.clear()
        self.mytheme.widget_margin=self.widget_margin_rank
        self.menu.add_vertical_margin(self.margin_main)
        self.menu.add_label("Single Easy Rank", selectable=False, font_size=self.font_main)
        self.menu.add_vertical_margin(self.margin_rank)
        self.menu.add_button("       ID       Score", self.pass_, font_size=self.font_sub)
        original_data = self.load_data("easy") # 오리지날 모드 데이터 받아오기
        for i in range(Var.rank_max) : #최대 몇명까지 보여줄건지 설정
            original_name=original_data[i]['ID']
            original_score = '{0:>05s}'.format(str(original_data[i]['score'])) #상위 5개 점수
            r= "#{} : ".format(i+1) + original_name+"    "+ original_score
            self.menu.add_button(r, self.pass_,font_size=self.font_sub)
        self.menu.add_button('back', self.show_rank,font_size=self.font_sub)

 

이렇게 게임오버시 점수를 데이터베이스에 저장하고 저장한 정보를 불러와 랭킹에 나타내는 함수까지 구현을 완료했습니다!

 

이와 같이 팀원들은 전체적인 UI 부분과 게임의 다양성(캐릭터, 테마, bgm) 부분을 맡고 저는 회원가입/로그인 기능과 랭킹 시스템을 주로 맡아서 작업했습니다!

 

역할을 나눠 작업을 했지만 서로 막히는 부분이 있거나 오류가 발생하는 부분이 있으면 도와가며 작업했습니다.

 

혼자였으면 해결하지 못했을 부분도 팀원들과 같이 고민하다보니 해결이 되더라고요!

 

집단지성의 힘인거 같습니다 ㅎㅎ

 

 

 

4. 디버깅

 

코드 수정 및 개발을 완료한 후 게임을 점검하는 과정에서 역시나 오류들이 발견되었습니다.

 

하지만 큰 오류들은 아니였고 마지막 주에 디버깅을 진행하여 프로젝트를 더욱 완성도 있게 만들었습니다.

 

 

 

5. github에 최종 업로드

 

이제 프로젝트를 완성했으니 github에 최종 파일들을 올려줍니다!

 

다음에 이 오픈소스프로젝트 수업을 듣는 학생들이 저희의 프로젝트를

 

Base 코드로 사용해주면 뿌듯할 것 같습니다 :)

 

https://github.com/CSID-DGU/2021-1-OSSPC-Tongsan1-2

 

CSID-DGU/2021-1-OSSPC-Tongsan1-2

Contribute to CSID-DGU/2021-1-OSSPC-Tongsan1-2 development by creating an account on GitHub.

github.com

 

 

 

느낀점

 

저는 이 수업을 통해 처음으로 오픈소스를 활용한 프로젝트를 진행하면서 기존 소스 코드를 디버깅하고 새로운 기능을 추가하는 코드를 짜는 경험을 할 수 있었습니다. 

 

git을 활용하여 팀원들과 함께 코드를 개발해나가는 과정과 AWS의 RDS와 MySQL을 통해 데이터베이스를 생성하고 데이터를 저장 및 불러오는 방법등에 대해서도 공부할 수 있었습니다. 

 

무엇보다 팀원들과 같이 고민하면서 혼자라면 해결하지 못했을 문제들을 해결한 경험이 값진 경험이라 생각합니다.

 

또한 결과적으로 저희 프로젝트의 목표를 모두 달성하며 마무리하여 뿌듯합니다 :)

 

( 학점도 A+을 받아서 더욱 뿌듯하네요 ㅎㅎ)