개발에 관심이 있거나 개발을 해본 분들은 Github에 대해서 많이 들어봤을 것이다. 근데 Git은 무엇인가? 둘이 똑같은 거 아닌가?라는 생각을 할 수도 있다.
필자도 개발에 처음 입문했을 때 당시 정확한 의미를 모르고 사용했을 때, Github를 줄여서 편하게 Git이라고 부르는 줄 알고 있었다. 😂
둘은 다른 소프트웨어이고, 서로 맡은 역할이 다르다.
Git이란?
버전을 관리하기 위한 소프트웨어를 말한다. 여기서 버전이라는 것은 그럼 무엇일까?
버전은 변경사항이라는 단어로 대체해서 사용할 수도 있는데, 유의미한 결과가 결과물로서 나오는 것을 버전이라고 한다.
아직도 어려운 느낌이 있다. 그럼 유의미한 결과는 무엇일까?
수정하고, 삭제하고, 새롭게 생성하고, 버그를 수정하는 등의 행위들이 모두 유의미한 결과가 될 수 있다.
즉, 정리해 보면 Git이란유의미한 결과(변화)들이 최종적인 결과물로서 나오는 과정을 관리하기 위해서 사용되는 소프트웨어이다. 정리를 해봤지만 아직도 말이 조금 어려운 것 같다.
쉽게 말해서 내 컴퓨터에서 작업(수정, 삭제, 생성 등)을 했을 때, Git에서 작업 내용을 버전에 따라서 관리할 수 있도록 기능을 제공한다. 내가 원하는 변경 사항들만 스테이지에 올린 후 하나의 버전으로 만들 수 있고, 지금까지 작업했던 내용이 마음에 안 들면 원하는 버전까지 되돌아가는 등의 기능도 제공해 준다. 자세한 부분은 Git을 실습해 보면 크게 와닿을 수 있을 거라고 생각한다.
그럼 Github는 무엇인가?
Github는 원격 저장소 호스팅 서비스라고도 불린다. 말이 조금 어렵기 때문에 이렇게 바꾸어보자. Github는 인터넷상 어딘가에 있는 저장소에서 Git으로 관리한 프로젝트를 호스팅 해주는 서비스이다.
Git으로 관리한 프로젝트를 호스팅 해준다는 의미가 무엇일까? Git을 사용하면 내 컴퓨터(로컬)에서 편리하게 프로젝트를 관리할 수 있다. 하지만 이 프로젝트는 내 컴퓨터에 있기 때문에 나만 볼 수 있다. 근데 만약 다른 사람들에게 내가 Git으로 관리한 프로젝트를 공유하고 싶다면 어떻게 해야 할까라는 고민을 해보면 해답을 얻을 수 있다.
첫 번째 방법은 이 프로젝트 파일 자체를 다른 사람에게 USB나 외장하드를 통해서 전달해 주는 방법이다. 이것이 과연 효율적일까? 만약 내 프로젝트를 100명에게 줘야 한다면 USB를 100개를 사서 파일을 공유하던가 아니면 USB 1개를 사서 100번을 돌아다녀야 한다. 정말 비효율적인 방법이라고 할 수 있다.
두 번째 방법은 이 프로젝트를 인터넷상 어딘가에 올려서 다른 사람들도 볼 수 있도록 하는 것이다. 우리가 일상에서 사용하는 네이버 클라우드나 구글 클라우드를 생각해 보면 이해하기 쉬울 것이다.
Github는 두 번째 방법을 간편하게 이용할 수 있도록 호스팅 서비스를 제공해 준다. Github에 대해서 다시 정의해 보면 Git으로 관리한 내 컴퓨터에 있는 프로젝트를 인터넷상 어딘가에 저장할 수 있도록 자리를 빌려주는 서비스(호스팅)라고 할 수 있다.
그럼 그냥 공유 드라이브에 프로젝트 파일을 올리고 다운받아서 사용하면 되는 거 아닌가라는 의문이 들 수도 있다. Github에서는 단순하게 파일만 추가하고, 내려받는 것이 아니라 이 프로젝트를 진행하면서 생긴 변화 과정(버전)에 대한 정보(History)도 저장이 됩니다.
즉, 내가 원하는 버전으로 돌아갈 수도 있고 되돌릴 수도 있다는 것입니다. 그래서 개발자들이 협업을 할 때 Github를 많이 사용합니다. Git과 Github에 대해서 잘 학습해 두는 것이 나중에 팀원들과 협업할 때 유용하게 작용할 수 있을 거라고 생각합니다.
다음 게시물에서는 이 Git에서 기본적으로 사용하는 명령어와 Git을 터미널에서 어떻게 사용하는지에 대해서 기록해 볼 것이다. Git을 터미널에서 사용할 때는 CLI 명령어도 같이 사용되기 때문에 이와 관련된 내용을 같이 학습하고 넘어가는 것이 좋다.
Git을 터미널에서 원활하게 사용하기 위해서 필요한 CLI 명령어에 대해서 먼저 알아보는 시간을 가져보자.
CLI 명령어
CLI란 Command Line Interface의 약어로 우리가 사용하는 검은색 화면의 터미널을 말한다. CLI 명령어는 검은색 화면에서 내가 직접 칠 수 있는 글자(명령어)라는 의미이다.
Git을 다뤄보기 전에 CLI 명령어에 대해서 알아보는 시간을 가져보자.
# pwd
내가 현재 위치해 있는 경로를 확인할 수 있는 명령어이다.
우리가 폴더에서 폴더를 클릭하면서 옮겨다닐 때는 눈에 다 보여서 상관이 없지만, 터미널에서 글자만 가지고 이 폴더 저 폴더를 옮겨 다니다 보면 내가 지금 어디에 위치해 있는지 헷갈릴 때가 있다. 이럴 때 사용해 주면 유용하게 쓰일 수 있다.
# ls
현재 경로에 존재하는 파일, 디렉터리들의 목록을 출력할 수 있다.
# ls -a
현재 경로에 존재하는 파일, 디렉터리들의 목록을 출력하는데, 추가적으로 숨길 파일까지 모두 출력해준다.
위에 보면 .이 붙은 파일들이 있는데, 이 파일들이 모두 숨김 파일들이다. 우리가 클릭해서 폴더로 들어가면 볼 수 없게 숨겨져 있다.
# cd {이동하고자 하는 경로}
이동하고자 하는 경로 또는 디렉터리를 입력해 주면 된다. 위에 ls -a의 결과로 나온 화면 중에서 만약 IdealProjects로 이동하고 싶다면 아래와 같이 치면 된다.
pwd 명령어로 바로 현재 경로까지 확인해 봤다.
# cd ..
상위 디렉터리로 이동할 수 있는 명령어이다. 여기서 상위 디렉터리란 내가 지금 /Users/kimhunsope/IdealProjects에 있을 때, /Users/kimhunsope이 현재 나의 상위 디렉터리를 말한다.
cd ..을 사용 후에 pwd 명령어로 확인해봤을 때, 정상적으로 상위 디렉터리로 이동된 것을 볼 수 있다.
# cd .
현재 디렉터리로 이동할 수 있는 명령어이다. 이 명령어는 현재 디렉터리로 이동하기 때문에 쳐도 아무 변화도 일어나지 않습니다.
# cd ~
Home 경로로 이동하는 명령어이다. Home 경로는 뭔데? 각자 컴퓨터마다 Home 경로가 있다. 보통 터미널을 처음 켰을 때, 처음 위치한 곳이 Home 경로이다. 이 경로는 변경하지 않는 것이 좋다고 한다.
처음 pwd를 했을 때인 /Users/kimhunsope이 내 Home 경로일 것이다. 한번 다른 경로로 이동했다가 Home으로 돌아와 보자.
# touch {파일 이름}
{파일 이름}을 이름으로 가지는 비어있는 파일이 하나 생성된다. 예시 코드로 test.txt라는 비어있는 텍스트 파일을 하나 만들어보자.
test.txt라는 파일이 새로 생성된 것을 볼 수 있다.
# cat {파일 이름}
{파일 이름}을 이름으로 가지는 파일의 내용을 출력해 주는 명령어이다. 위에서 새롭게 만든 test.txt 파일의 내용을 한번 출력해 보자.
몇 번을 쳐봐도 아무것도 출력되지 않는다. 왜냐하면 우리가 touch로 만들어준 test.txt는 비어있는 텍스트 파일이기 때문이다. 그럼 이 test.txt 파일에 내용을 추가해 보자.
# vi {파일 이름}
{파일 이름}을 이름으로 가지는 파일의 내용을 수정해 줄 수 있는 vi 편집기 창이 열린다. vi 편집기는 터미널 상에서 내용을 수정할 수 있도록 해주는 편집창이다.
vi 편집기를 사용하기 위해서는 아래 2가지 정도의 내용은 알고 시작해야 한다.
1. 내용을 입력할 수 있는 모드
내용을 입력하기 위해서는 vi 편집기 창으로 가서 알파벳 i 또는 a를 누르면 아래에 INSERT라는 메시지가 뜨고 입력을 할 수 있는 모드로 변경된다.
이때부터 이제 vi 편집기에 내용을 입력할 수 있다.
2. 명령을 입력할 수 있는 모드
입력이 모두 완료됐으면 이제 저장을 하고, vi 편집기에서 나가야 한다. 이때 명령어를 사용해서 진행하는데, 이 명령어를 치기 위한 모드로 가기 위해서는 esc를 눌러주면 된다.
esc를 누르면 -- INSERT --라는 메시지가 사라지고 명령을 입력할 수 있는 모드가 된다. 이때 저장하는 명령어는 :w이고, 편집기에서 나가는 명령어는 :q이다. w는 write의 약자고, q는 quit의 약자이다. 기억하기 더 쉬울 것이다.
추가로 저장을 하고 바로 편집기에서 나가기 위한 명령어는 :wq이다.
이제 test.txt에 안녕하세요라는 내용을 입력하고 저장해 보자.
알파벳 i를 누르고 INSERT 모드에서 '안녕하세요.'를 입력했다. 그리고 항상 저장할 때는 엔터를 한 번 더 치고 나서 저장하는 것이 좋다. 이제 :wq 명령어로 저장을 하고 나가보자.
저장까지 모두 완료했다. cat 명령어로 우리가 저장한 내용이 잘 출력되는지 확인해 보자.
내용이 잘 출력되는 것을 확인할 수 있다.
# rm {파일 이름}
{파일 이름}의 이름을 가지는 파일을 삭제할 수 있다. test.txt 파일을 삭제해 보자.
두 번째 ls 명령어의 결과에서는 test.txt 파일이 사라진 것을 볼 수 있다.
# mkdir {디렉터리 이름}
{디렉터리 이름}을 이름으로 가지는 비어있는 디렉터리를 생성하는 명령어. 여기서 디렉터리란 윈도우에서는 폴더라는 말이 더 익숙할 수도 있다. mkdir이란 make directory의 약자이다. 외우기 쉽겠다. my_dir이라는 새로운 디렉터리를 만들어보자.
새로운 디렉터리가 생성된 것을 볼 수 있다. my_dir는 디렉터리이기 때문에 해당 디렉터리로 cd 명령어를 통해서도 들어갈 수 있다. 직접 해보시길 바란다.
# rmdir {디렉터리 이름}
생성도 있으면 삭제도 있어야 한다. {디렉터리 이름}을 이름으로 가지는 디렉터리를 삭제하는 명령어이다. 하지만 나중에 해보면 알 수 있지만 비어있는 디렉터리가 아니라면 rmdir을 이용해서 삭제할 수 없다. 이에 대한 내용은 다음에 알아보도록 하자. 앞서 만든 my_dir을 삭제해 보자.
두 번째 ls 명령어의 결과에서는 my_dir가 사라진 것을 볼 수 있다.
# rm -rf {디렉터리 이름}
위에서 의문이었던 점을 여기서 해결할 수 있다. 이 명령어는 비어있지 않는 디렉터리도 강제로 삭제해 주는 명령어이다. 그렇기 때문에 사용할 때는 반드시 주의가 필요하다. 해당 명령어로 삭제하는 디렉터리는 복구 못한다고 생각하는 게 편하다. 사용할 때는 주의하자!
my_dir이라는 디렉터리를 하나 만들고, my_dir 안에 second_dir라는 디렉터리와 test.txt라는 파일을 한 번 생성한 뒤에 my_dir을 강제로 삭제해 보자.
생성하고, 삭제하는 모든 과정을 캡처해 봤다.
빨간색 네모 부분이 my_dir을 생성하고, my_dir 안으로 들어가서 second_dir과 test.txt를 생성하는 과정이다.
파란색 네모 부분이 my_dir을 삭제하는 부분인데, 비어있지 않은 디렉터리는 rmdir 명령어로 삭제되지 않는 것을 보여주기 위해서 한번 삭제해 봤다. 해당 디렉터리가 비어있지 않다는 메시지와 함께 삭제되지 않는다.
그리고 바로 rm -rf 명령어로 my_dir을 정상적으로 삭제해봤다.
다음 게시글에서는 Git과 Github가 무엇이 다르며 각각 어떤 기능을 하는지에 대해서 알아볼 생각이다.
이번에 국민취업지원제도에 등록해서 국비지원 교육을 받게 되었습니다. 작년 초에 대학 동아리에서 처음 개발을 접하게 되었고 백엔드 개발자가 되기로 마음을 먹었습니다. 효율적인 성장을 하기 위해서는 뜻이 같은 사람들과 함께 성장하는 것이 중요하다고 생각합니다. 또한 앞으로 나아가는 법을 잘 모르는 상태이기 때문에 방향을 잡아줄 수 있는 교육 기관의 도움을 받는 것이 좋을 거 같다는 생각을 했고, 국비지원 교육을 받기로 결정했습니다.
패스트캠퍼스에 지원한 이유
그렇게 선택한 교육이 패스트캠퍼스 백엔드 개발 5기 부트캠프입니다. 많은 부트캠프가 존재하고, 선택의 요소들이 정말 많았습니다. 그중에서 패스트캠퍼스를 선택한 이유는 프로젝트가 많았기 때문입니다.
무조건 많은 프로젝트가 좋다는 의견은 아닙니다. 하지만 동아리에서도 그랬었고, 프로젝트를 통해서 협업 능력도 기를 수 있고 사람과 소통하는 법도 배울 수 있었다고 생각합니다. 그리고 프로젝트를 진행하면서 발생한 오류들을 해결하면서 많은 성장을 했고, 무엇보다 함께하는 즐거움을 느껴봤기 때문입니다.
패스트캠퍼스 백엔드 부트캠프 과정에서는 총 5번의 프로젝트를 합니다. 토이 프로젝트 3개, 미니 프로젝트 1개, 파이널 프로젝트 1개 이렇게 진행합니다. 이 부분이 저를 조금 끌리게 만들었던 요소였다고 생각합니다.
지원 과정 & 선발
처음 모집 요강을 봤을 때는 2023년 2월 14일이었습니다. 홈페이지에 들어가 지원을 하게 되면 지원을 할 때 입력했던 이메일로 메일이 도착하고 요구하는 대로 과정을 진행했습니다.
자기소개서 제출 -> 기초 소양 테스트 진행 -> 비대면 면접까지 하면 지원 완료입니다. 결과 발표는 3월 10일 날 최종 합격 발표를 받았습니다.
선발 메일 내용
패스트캠퍼스에서의 첫날
# 과정소개 및 Q&A
3월 17일 오늘 패스트캠퍼스에서 첫 교육이 시작됐습니다. 첫날은 OT를 진행했는데 대면으로는 진행되지 않고, zep이라는 프로그램을 이용해서 비대면으로 진행됐습니다.
zep 컨퍼런스홀
이렇게 캐릭터도 커스터마이징할 수 있고 막 돌아다닐 수도 있었습니다. 눈사람으로 꾸몄는데, 다른 귀여운 옷들도 많습니다. OT는 백엔드만 모여서 진행하는 것이 아닌 프런트엔드도 함께 진행합니다. 사람이 100명 정도로 많았기 때문에 가끔 렉도 걸리기도 합니다. 저는 맥북 M1을 사고 한 번도 발열을 느껴본 적이 없는데 오늘 처음 느껴봤습니다.
이렇게 컨퍼런스홀에 모여있으면 매니저분들이 과정에 대한 커리큘럼 설명과 Q&A 과정을 거쳤습니다. 생각보다 많은 사람들이 질문을 해주셨고, 매니저분들이 하나하나 꼼꼼하게 대답을 해주셨습니다.
# 아이스브레이킹 시간
이제 과정 설명 및 Q&A가 모두 끝나고 잠시 휴식시간을 가진 후에 백엔드 수강자들끼리 모여서 다양한 나이, 성별, 성격의 사람들이 처음 만났기 때문에 어색한 분위기를 없애기 위해서 아이스브레이킹 시간을 가졌습니다. 이때는 4인에서 5인으로 팀이 랜덤으로 정해졌습니다.
이렇게 팀원이 정해지면 컨퍼런스 홀과는 다른 공간으로 이동합니다. 이곳에서는 백엔드 수강자들만 모여있었고 팀별로 준비된 장소가 있었습니다.
백엔드 수강자들을 위한 장소
위에 보이는 하얀색 네모에 들어와 있는 사람들하고만 대화가 가능한 독립적인 공간입니다. 채팅을 해도 다른 공간에 있는 사람들은 안 보이는 것 같았습니다. 처음에는 조금 유치하다는 생각을 했는데 캐릭터가 보면 볼수록 매력 있습니다. 스페이스바를 누르면 점프도 가능하고, x를 누르면 앉기도 합니다. 다양한 감정표현도 존재합니다. 이쯤 되면 zep을 홍보하는 사람 같아서 그만하겠습니다.
이 공간으로 넘어와서는 팀끼리 팀장을 정했습니다. 저희 팀은 사다리 타기를 해서 팀장을 뽑았는데 저는 사다리에 정말 잘 걸립니다. 가위바위보는 맨날 지면서 이런 건 잘 걸립니다. 그렇게 팀장을 맡았고 팀원 맞추기 퀴즈, 영화 제목 맞추기, 잘린 사진 보고 뭔지 맞추기 등 다양한 활동들을 함께 하면서 아이스브레이킹 시간을 가졌습니다. 그렇게 17시 정도가 되었고 19시부터 있을 강민철 강사님의 특강이 준비되어 있었기 때문에 저녁 식사 시간을 가졌습니다.
# 개발자 마인드셋 특강
저녁 시간이 끝나고 19시부터 시작된 특강. 강민철 강사님의 특강 시간이었는데, 여기서 신기했던 점은 작년 대학교에서 멋쟁이 사자처럼 대학 10기에서 활동을 했었을 때 파이썬/장고로 개발하는 강의를 해주셨던 분이어서 정말 신기했습니다. 그때는 녹화강의였지만 오늘은...실시간 강의여서 그래서 더 집중해서 들었던 거 같기도 합니다.
2시간 정도 되는 특강의 주제는 다음과 같았습니다. 개발자로서 어떤 마인드와 자세로 성장해야 하는가, 어떻게 효율적인 성장을 할 수 있는가였습니다. 주로 얘기해 주셨던 내용은 무엇을 통해 성장을 할 수 있고, 어떻게 빠르게 성장할 수 있는지에 대한 내용이었습니다.
저는 이 특강을 듣고 "개발자 마인드셋"이라는 강의 이름을 정말 잘 지었다고 생각했습니다. 왜냐하면 제 마인드가 세팅된 거 같기 때문입니다. 사실 약간 정곡을 맞았던 부분이 많았습니다.
그중에서 가장 세게 맞은 부분은 블로그를 통한 성장에 대한 내용이었습니다. 저는 지금 성장을 위해 블로그를 하고 있습니다. 제가 작성한 게시글 중에서 토비의 스프링에 대해서 작성한 글들이 몇 개 있습니다. 사실 처음에는 토비의 스프링 책을 읽고 코드도 따라 쳐보면서 발생하는 오류에 대해서 정리를 좀 해볼까라는 생각으로 시작했는데 지금 보니 그냥 거의 책의 모든 내용을 요약해 놨다는 생각이 들었고, 이럴 거면 그냥 책을 보는 게 낫지 않을까라는 생각을 하면서 반성하게 되었습니다.
그래서 이제부터는 강의에서 얻은 대로 한번 블로그 글을 작성해 보는 연습을 해보자는 목표가 생겼습니다. 블로그는 제가 성장해 온 길을 증명할 수 있는 가장 강력한 무기가 될 것이기 때문입니다. 이렇게 마인드 세팅을 마무리하고, 이제 다음 주부터 10월 10일까지 긴 여정이 될 거 같습니다.
최대한 1주일에 한 번이나 별다른 내용이 많이 없다면 1달에 한번 정도는 회고글을 남겨볼 생각입니다. 나중에 제가 다시 볼 수도 있고, 누군가 이 글을 읽고 도움을 받을 수 있기 때문입니다. 끝나는 날까지 화이팅입니다.
계속 새로운 문제가 나오고 있지만 2023년 3월 16일 기준으로 레벨 1 문제를 모두 해결해 봤다.
언어는 모두 Java를 사용해서 풀었다.
원래 레벨 1을 조금 풀다가 이정도면 2 레벨로 들어가도 되겠다는 어리섞은 생각을 하게 되었다. 쉽게 풀렸던 몇몇 레벨 2문제를 만나서 그동안 몰랐던 거 같다.
그래서 적어도 레벨 1 문제를 빠르게 다 풀어보자는 생각으로 문제를 풀기 시작했다. 레벨 1문제를 풀며 느낀 점을 정리해보려고 한다.
🔎 무조건 List! 무조건 Map!
내가 처음에 레벨 2를 풀기 시작했을 때 했었던 생각이다. 배열 보다는 역시 리스트를 써야지 이런 생각을 했었고, 실제로도 그렇게 풀었었다. 물론 List, Map, Set 모두 훌륭한 자료구조이다. 하지만 레벨 1문제를 풀면서는 먼저 문제에 접근할 때 배열로 해결할 수 있지 않을까라는 생각으로 접근을 하는 연습을 많이 했다.
그 이유는 분명 List와 Map이 효율적인 문제에서는 속도도 빠르고, 오히려 배열보다 효율성도 좋을 때가 많다. 하지만 대부분의 레벨 1 문제는 배열을 사용해서 해결할 수 있는 문제가 대부분이었고, 속도도 배열이 대부분 빨랐다. 이건 무조건 배열을 쓰라는 소리가 아니다. 배열을 써서 해결할 수 있는 문제라면 굳이 List나 Map을 쓰지 않아도 된다는 말이다.
🔎 StringBuilder를 활용하자
문제를 풀다보면 String 타입의 결과를 도출해 내는 문제가 종종 있다. 풀었던 문제 중에서 예시를 들자면 어떤 String 타입의 배열이 주어졌을 때, "Kim"이라는 문자열이 존재하는 index를 "김서방은 {index}에 있다"라는 형태의 문자열로 반환하는 문제였다.
배열에서 해당 문자열의 위치를 찾는 건 반복문을 돌려서 찾거나 List 타입으로 변환해서 indexOf() 메소드를 사용해 주면 쉽게 얻을 수 있다. 이제 이렇게 얻은 index를 "김서방은 "과 "에 있다"라는 문자열 사이에 넣어야 한다. 선택은 여러 가지가 있다.
# 그냥 더해서 반환하기
return "김서방은 " + index + "에 있다";
String은 이렇게 직접 더하는 연산을 사용하면 속도가 많이 느려진다. 레벨 1짜리 문제여서 별 차이는 느끼지 못할 정도이지만 피하는 것이 좋다.
# 포매팅을 사용하기
return String.format("김서방은 %d에 있다", index);
정수형 변수인 index를 포매팅(%d)을 이용해서 반환해 줄 수 있다. 실제 속도 차이로 봤을 때는 위에서 String을 그대로 더한 결과보다 빨랐다.
# StringBuilder 사용하기
return new StringBuilder("김서방은 ").append(index).append("에 있다").toString();
오늘의 핵심이다. StringBuilder는 append()라는 메소드로 계속 이어서 문자를 이어 붙일 수 있다. 그리고 마지막에 toString()을 써주면 만들어진 문자열을 String 타입으로 반환해 준다. 속도도 위에서 사용했던 두 가지 방법보다 빨랐다.
🔎 char을 잘 활용하자
사실 이전까지 char 배열은 잘 사용하지 않았다. 어떻게 보면 char 자료형 자체를 많이 안 썼던 거 같다. 하지만 레벨 1 문제를 74문제나 푼 나의 생각은 바꼈다. char[]를 잘 사용하면...엄청난 결과를 얻을 것이니..
이 기능은 필자는 문제 풀 때 정말 많이 사용했던 기능이다. 쓰이는 곳이 정말 많고, 유용하다. char[]을 문자열 생성자에 넣어주면 그대로 문자열을 반환해 준다니... 흥미롭지 않은가...? 나만 흥미로울 수도 있다. 👀
# char는 숫자로도 쉽게 사용이 가능하다
char[] ch = new char[]{'0', '1', '2', '3', '4', '5'};
int zero = ch[0] - '0'; // 숫자 0
int one = ch[1] - '0'; // 숫자 1
int two = ch[2] - '0'; // 숫자 2
int three = ch[3] - '0'; // 숫자 3
아스키 코드를 사용한 방법인데, '0'의 아스키코드 번호는 48이고, '1'은 49 '2'는 50 이렇게 증가한다. 즉 문자인 '1'에서 숫자 1을 얻기 위해서는 '1' - '0'을 해주면 된다. 49 - 48 이기 때문에 가능한 것이다.
그냥 Integer.parseInt()나 Integer.valueOf()를 쓰면 되는 거 아닌가라고 생각하는 사람이 있으면 큰일이다. 한번 넣어보면 결과를 알 수 있을 것이다.
int zero = Integer.valueOf('0');
System.out.println(zero);
위에 있는 코드의 출력 결과가 뭐가 나올까? 0이 아닌 '0'의 아스키코드 번호인 48이 나온다. 그럼 parseInt()는? 이건 "java: incompatible types: char cannot be converted to java.lang.String" 이런 오류와 함께 컴파일 오류가 발생한다. parseInt()의 매개변수로 들어갈 수 있는 건 String 타입이기 때문이다. 그러니 아스키코드 활용을 잘 알아두면 좋을 것이다.
# 대문자와 소문자를 판별
char lowerValue = 'a';
char upperValue = 'A';
if (Character.isLowerCase(lowerValue)) {
System.out.println("소문자 입니다."); // 출력
}
if (Character.isUpperCase(upperValue)) {
System.out.println("대문자 입니다."); // 출력
}
Character에는 대문자와 소문자를 판별해주는 isLowerCase()와 isUpperCase() 메소드가 존재하기 때문에 이거도 잘 활용하면 코드를 대폭 줄일 수 있다. 숫자인지 판별하는 isDigit()도 있으니 잘 사용해 보기를 바랍니다.
🔎 코드를 줄이자!!! 스트림!!!
이라고 생각하면 큰일이다. Stream으로 해결한 몇몇 코드를 보면 정말 한 눈에 반할 정도로 멋지고, 나도 쓰고 싶어질 때가 있다. 심지어...한 줄로 해결해 버리는 문제도 많이 있었다.
하지만 스트림으로 푸는 문제들은 코드를 돌려보면 알겠지만 속도가 많이 느리다. 예를 들어 단순 반복문과 배열을 사용했을 때는 0.03ms에서 0.09ms가 나오는 반면에 스트림으로 해결한 문제를 돌려보면 1ms을 넘어가는 경우도 다반사이다. 아름다움에 현혹되서는 안 된다.
그렇다고 스트림이 좋지 않다라는 말은 아니지만 쓸 때와 쓰지 않을 때를 잘 구분해야 된다는 말이다. 알고리즘 문제를 풀 때는 이왕이면 스트림은 피하는 것이 좋다고 생각된다. 나같은 초짜는 언제 스트림을 쓰고 안 써야 하는지 구분을 못하기 때문에... 🙂
페이스북 개발자 센터의 앱 ID와 시크릿 코드는 개발자 센터에 들어가서 설정의 기본 설정으로 들어가면 확인하실 수 있습니다.
앱 ID와 앱 시크릿 코드
다음은 scope에 대해서 궁금하실 텐데 이 부분은 페이스북으로부터 사용자의 어떤 데이터를 받을지에 대한 범위를 지정할 수 있습니다.
개인정보는 민감한 부분이기 때문에 우리가 얻고 싶은 데이터를 얻는 것이 아니고 OAuth2.0를 제공하는 기업마다 다르기 때문에 문서를 참고해야 합니다. 우리는 email 정보와 프로필 정보를 가지고 있는 public_profile을 범위로 설정할 것입니다.
이렇게 yml 설정을 마치면 앞서 jsp에서 작성했던 경로인 "/oauth2/authorization/{registration에 등록한 기업명}"을 이제 사용할 수 있게 됩니다.
그리고 추가적으로 이번에는 다루지 않을 내용이지만 카카오, 네이버 로그인 기능 구현에는 요구되는 사항이라서 얘기하고 넘어가겠습니다. 페이스북, 구글은 Spring OAuth2.0 Client에서 제공되는 provider에 대한 yml 설정을 따로 해주지 않아도 됩니다. 우리나라에서는 빈번하게 사용되는 카카오, 네이버의 경우에는 우리가 직접 provider로 등록해서 사용해야 합니다. 다음에 네이버 소셜 로그인 기능 구현에 대한 게시글을 정리할 때 자세히 적어보겠습니다.
# SecurityConfig 클래스 수정하기
이름은 설정하기 나름이겠지만 SpringSecurity를 사용하여 로그인 기능을 구현할 때 사용했던 WebSecurityConfigurerAdapter를 상속받은 클래스를 의미합니다. 저는 이름을 SecurityConfig라고 지정했습니다.
configure() 메소드에 설정된 대로 SpringSecurity가 작동합니다. 위 코드는 OAuth2.0 보다는 SpringSecurity에 가깝기 때문에 자세히 설명하진 않겠습니다.
간단하게 말하자면 ["/", "/user/**", "/image/**", "/subscribe/**", "/comment/**", "/api/**"]에 해당하는 URL 요청이 들어온다면 인증이 필요합니다. 그 외의 요청이 들어오면 모두 허용합니다.
그리고 로그인 인증 요청이 들어오면 로그인 페이지(/auth/signin)로 이동하고, 로그인 처리는 SpringSecurity에 위임합니다. 로그인이 성공한다면 홈(/)으로 이동한다는 의미의 코드입니다. 여기에 이제 OAuth2 처리에 대한 부분도 추가해 줍니다.
@RequiredArgsConstructor // OAuth2DetailService를 DI 받기 위한 Lombok 어노테이션
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final OAuth2DetailService oAuth2DetailService; // DI를 받아올 Service
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/", "/user/**", "/image/**", "/subscribe/**", "/comment/**", "/api/**")
.authenticated()
.anyRequest()
.permitAll()
.and()
.formLogin()
.loginPage("/auth/signin")
.loginProcessingUrl("/auth/signin")
.defaultSuccessUrl("/")
.and()
.oauth2Login() // OAuth2 로그인 요청이 들어오면 처리해준다.
.userInfoEndpoint() // 요청이 끝나면 인증코드가 아닌 회원정보를 받겠다는 코드
.userService(oAuth2DetailService); // 해당 로직을 실행시키고, 데이터를 받을 서비스를 지정한다.
}
}
새롭게 추가된 부분에는 주석을 달아놨습니다. 먼저 의문이 드는 코드가 한 두 개가 아닙니다. 순서대로 확인해 봅시다.
OAuth2DetailService : 아직 구현하지 않은 클래스입니다. OAuth2 요청이 왔을 때, 그 응답 데이터를 받아서 로직을 처리하는 역할을 합니다.
.oauth2Login() : OAuth2 로그인 요청이 왔을 때 처리해 줍니다. 우리는 스프링에서 제공하는 OAuth2 Client 라이브러리를 사용하고 있기 때문에 이렇게 쉽게 처리가 가능하지만 원래는 인가 코드를 받아서 AccessToken을 요청한 후 돌려받은 AccessToken을 가지고 사용자 데이터를 요청해야 합니다.
.userInfoEndpoint() : 위 요청이 끝나면 인증코드가 아닌 회원정보를 받겠다는 코드입니다.
.userService(oAuth2DetailService) : oAuth2DetailService에서 OAuth2에 대한 요청에 대한 데이터를 받아서 로직을 처리하도록 합니다. 이때 userService에 들어갈 수 있는 타입은 DefaultOAuth2UserService입니다.
# OAuth2DetailService 생성하기
위에서 말했듯이 OAuth2 요청이 왔을 때 그 응답 데이터를 받아서 로직을 처리하는 역할을 하는 클래스입니다.
타입은 DefaultOAuth2UserService여야 하기 때문에 상속을 받아서 구현합니다.
@Service
public class OAuth2DetailService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
return null;
}
}
loadUser의 파라미터를 보면 OAuth2UserRequest 타입의 변수가 있는데, 여기에 OAuth2 로그인을 요청한 사용자의 정보가 담겨서 온다. System.out.println()으로 oAuth2User를 출력해 보면 OAuth2 요청 버튼을 눌렀을 때, 정상적으로 사용자 정보가 들어오는 것을 확인할 수 있다.
# OAuth2DetailService 로그인 로직 구현
이제 받은 페이스북 사용자 정보를 가지고 로그인 기능을 구현해 볼 것이다. 가장 먼저 우리가 받아야 하는 정보는 username, password, email, name이다.
@RequiredArgsConstructor // 생성자로 DI를 받아오기 위한 Lombok 어노테이션
@Service
public class OAuth2DetailService extends DefaultOAuth2UserService {
private final UserRepository userRepository; // 회원을 DB에 넣기위해 DI를 받아옴
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
Map<String, Object> userInfo = oAuth2User.getAttributes();
// facebook 사용자임을 구분하기 위해서 아이디 앞에 facebook_을 붙임
String username = "facebook_" + (String) userInfo.get("id");
// 암호는 임의의 UUID를 만들어 인코딩을 진행
String password = new BCryptPasswordEncoder().encode(UUID.randomUUID().toString());
// 받아온 데이터에서 email과 name에 해당되는 내용을 저장
String email = (String) userInfo.get("email");
String name = (String) userInfo.get("name");
User userEntity = userRepository.findByUsername(username);
// OAuth2 요청할 때마다 계속 INSERT되는 것을 막기위한 로직, 기존 회원이 존재하면 회원가입 로직 X
if (userEntity == null) {
// OAuth2 요청을 한 사용자가 기존 회원이 아닐 때, 새로운 User를 생성
User user = User.builder()
.username(username)
.password(password)
.email(email)
.name(name)
.role("ROLE_USER")
.build();
// UserDetails 타입으로 반환해야 Session에 저장하고 할 수 있다.
// 그래서 PrincipalUserDetail에서 OAuth2User를 상속받게하여 loadUser에서 사용할 수 있도록 해준다.
return new PrincipalUserDetails(userRepository.save(user), oAuth2User.getAttributes());
}
return new PrincipalUserDetails(userEntity, oAuth2User.getAttributes());
}
}
요청받은 사용자 데이터에는 attributes라는 필드가 있고, 그 안에 우리가 필요로 하는 데이터들이 들어있습니다. 그렇기 때문에 Map<> 타입으로 우리가 필요한 데이터를 저장해 줍니다. 이때 attribute를 가져오기 위해서 getAttributes() 메소드를 사용합니다.
Map<>의 value 타입은 Object이기 때문에 userInfo.get()을 통해서 가져온 데이터는 각각에 맞는 데이터 형태로 형 변환을 해줘야 합니다. 이때 (String)을 주로 사용했습니다.
username, password, email, name을 attribute에서 가져와서 값을 저장한 뒤 user 데이터베이스에 넣기 위해서 UserRepositroy의 save() 메소드를 사용합니다.
근데 그 아래 return 되는 PrincipalUserDetailssms 무엇인지 궁금할 것이다. PrincipalUserDetails는 SpringSecurity 로그인 기능을 구현하기 위해서 만들어놨던 클래스이다. SpringSecurity에서 세션 정보를 저장하기 위해서는 UserDetails 타입으로 반환되어야 하는데, 그래서 PrincipalUserDetails를 이용해서 반환을 해준 것이다.
하지만 한 가지 걸리는 점이 있다. 세션을 위해 UserDetails 타입으로 반환해 준 것은 좋다. 근데 loadUser() 메소드의 return 타입을 보면 OAuth2User 타입이다. 즉, UserDetails 타입이면서 OAuth2User 타입도 되어야 하기 때문에 PrincipalUserDetails 코드를 조금 수정해야 한다.
// OAuth2User 타입으로 만들기 위해서 OAuth2User를 상속받음
@Data
public class PrincipalUserDetails implements UserDetails, OAuth2User {
private User user;
// OAuth2 사용자 데이터의 attribute를 저장하기 위한 변수
private Map<String, Object> attributes;
public PrincipalUserDetails(User user) {
this.user = user;
}
// OAuth2 유저 구분을 위한 생성자 오버로딩
public PrincipalUserDetails(User user, Map<String, Object> attributes) {
this.user = user;
this.attributes = attributes;
}
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public String getName() {
return (String) attributes.get("name");
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collectors = new ArrayList<>();
collectors.add(() -> {
return user.getRole();
});
return collectors;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
기존 코드가 아닌 부분에는 주석을 달아놨다. 이렇게 OAuth2User 인터페이스를 상속받게 되면 PrincipalUserDetails 클래스는 UserDetails 타입이면서 OAuth2User 타입으로 사용할 수 있게 된다.
# 로그인 버튼 눌러서 확인해 보기
모든 기능 구현이 끝났기 때문에 만들어둔 로그인 버튼을 눌러서 로그인이 정상적으로 되는지 확인해 보자.
구현해둔 로그인 버튼
버튼을 눌러보면 아래와 같은 페이스북 로그인 페이지로 넘어가게 된다.
아이디와 비밀번호를 입력해서 로그인을 진행해 보면 로그인이 완료되는 것을 확인할 수 있다.
각자 진행하고 있는 프로젝트의 구조가 다르기 때문에 기능적으로 필요한 부분들만 얻어가셨으면 좋을 거 같다. 다음에는 카카오, 네이버, 구글 로그인에 대해서도 한번 다뤄볼 생각이다.