Docker란 ? 

  • Docker는 애플리케이션을 신속하게 구축, 테스트 및 배포할 수 있는 소프트웨어 플랫폼
  • Docker는 소프트웨어를 컨테이너라는 표준화된 유닛으로 패키징하며, 이 컨테이너에는 라이브러리, 시스템 도구, 코드, 런타임 등 소프트웨어를 실행하는 데 필요한 모든 것이 포함되어 있다.
  • Docker를 사용하면 환경에 구애받지 않고 애플리케이션을 신속하게 배포 및 확장할 수 있다.
  • Docker를 사용하면 컨테이너를 매우 가벼운 모듈식 가상 머신처럼 다룰 수 있다. 또한 컨테이너를 구축, 배포, 복사하고 한 환경에서 다른 환경으로 이동하는 등 유연하게 사용할 수 있어, 애플리케이션을 클라우드에 최적화하도록 지원한다.
  • 도커에서 가장 중요한 개념은 컨테이너와 함께 이미지라는 개념이다.

 

컨테이너(Container)

  • 컨테이너는 격리된 공간에서 프로세스가 동작하는 기술이다. 가상화 기술의 하나지만 기존방식과는 차이가 있다.
  • 기존의 가상화 방식은 주로 OS를 가상화하였다. 우리에게 익숙한 VMware나 VirtualBox같은 가상머신은 호스트 OS위에 게스트 OS 전체를 가상화하여 사용하는 방식이다. 이 방식은 여러가지 OS를 가상화(리눅스에서 윈도우를 돌린다던가) 할 수 있고 비교적 사용법이 간단하지만 무겁고 느려서 운영환경에선 사용할 수 없었다.
가상머신과 도커

 

  • 전가상화든 반가상화든 추가적인 OS를 설치하여 가상화하는 방법은 어쨋든 성능문제가 있었고 이를 개선하기 위해 프로세스를 격리 하는 방식이 등장했다.
  • 하나의 서버에 여러개의 컨테이너를 실행하면 서로 영향을 미치지 않고 독립적으로 실행되어 마치 가벼운 VMVirtual Machine을 사용하는 느낌을 준다. 실행중인 컨테이너에 접속하여 명령어를 입력할 수 있고 apt-get이나 yum으로 패키지를 설치할 수 있으며 사용자도 추가하고 여러개의 프로세스를 백그라운드로 실행할 수도 있다. CPU나 메모리 사용량을 제한할 수 있고 호스트의 특정 포트와 연결하거나 호스트의 특정 디렉토리를 내부 디렉토리인 것처럼 사용할 수도 있다.
  • 새로운 컨터이너를 만드는데 걸리는 시간은 겨우 1-2초로 가상머신과 비교도 할 수 없이 빠르다.

 

이미지(Image)

  • 이미지는 컨테이너 실행에 필요한 파일과 설정값등을 포함하고 있는 것으로 상태값을 가지지 않고 변하지 않는다(Immutable).
  • 컨테이너는 이미지를 실행한 상태라고 볼 수 있고 추가되거나 변하는 값은 컨테이너에 저장된다. 같은 이미지에서 여러개의 컨테이너를 생성할 수 있고 컨테이너의 상태가 바뀌거나 컨테이너가 삭제되더라도 이미지는 변하지 않고 그대로 남아있다.
  • 이미지는 컨테이너를 실행하기 위한 모든 정보를 가지고 있기 때문에 더 이상 의존성 파일을 컴파일하고 이것저것 설치할 필요가 없다.
  • 새로운 서버가 추가되면 미리 만들어 놓은 이미지를 다운받고 컨테이너를 생성만 하면 된다. 한 서버에 여러개의 컨테이너를 실행할 수 있고, 수십, 수백, 수천대의 서버도 문제없다.

 

Docker를 사용해야 하는 이유

  • Docker를 사용하면 코드를 더 빨리 전달하고,
  • 애플리케이션 운영을 표준화하고,
  • 코드를 원활하게 이동하고,
  • 리소스 사용률을 높여 비용을 절감할 수 있다.
  • Docker를 사용하면 어디서나 안정적으로 실행할 수 있는 단일 객체를 확보하게 된다.
  • Docker의 간단한 구문을 사용해 완벽하게 제어할 수 있다. 폭넓게 도입되었다는 것은 Docker를 사용할 수 있는 도구 및 상용 애플리케이션의 에코시스템이 강력하다는 의미이다.

 

Docker 설치 : https://docs.docker.com/desktop/install/mac-install/

 

Install on Mac

 

docs.docker.com

 

Docker image 받는 hub : https://hub.docker.com/

 

Docker Hub Container Image Library | App Containerization

Deliver your business through Docker Hub Package and publish apps and plugins as containers in Docker Hub for easy download and deployment by millions of Docker users worldwide.

hub.docker.com


[ 참고 자료 ]

 

 생활코딩 Docker 입구 수업

오랜만에 게더에 왔씁니다!

 

 

1. 어려웠던 부분 : 오늘은 프론트엔드분들이 채팅으로 공지사항을 내리는 부분에 받는 값이 없다는 이슈가 있다고 하셔서 그 부분을 수정하는 작업을 했다. 우린 분명히 값을 보내고 있는데 아예 받는 값이 없는 문제였다. 알고보니 백엔드 코드와 프론트엔드 코드에 몇가지 수정할 부분이 있었다. 

  1. 컨트롤러에서 "/pub" 떼기 (백엔드)
  2. gameroomId -> gameRoomId (프론트)
  3. start -> START (프론트)

 

1. 처음엔 아래와 같이 컨트롤러 부분에 "/pub" 으로 시작하는 URL 주소를 사용했다.

@MessageMapping 을 사용했기 때문에 보내는 입장인 백엔드에선 "/pub"을 제외하고 아래와 같이 수정해 주었다.

2. 아래와 같이 우리는 메세지 타입을 대문자 START로 보내고 있었는데, 프론트에선 소문자 start 로 받고 있었다. 이부분을 수정해주었다.

위와 같이 사소한 부분들을 수정해주니 제대로 동작하였다. 다행히 오류를 찾는에 오래 걸리진 않았다. 다시한번 느끼지만 점 하나, 슬래시 하나때문에 모든게 동작하지 않는 컴퓨터의 언어. 컴퓨터는 잘못 없어 내가 잘못 쳤어 ... ^^

 

2. 느낀 점 : 이번 프로젝트를 하면서 오랜만에 협업을 경험하니 새롭고 재미있다. 전 직장에서 일할때도 팀워크가 맞을때 참 짜릿하고 좋았는데, 이번에 팀을 잘 만나서 덕분에 나도 많이 배우고 재밌게 하고 있다. 열심히 하는 팀원분들을 보면서 동기부여도 되고 스스로 반성하게 되는 것 같다. 

 

3. 새로 알게 된 내용 : 오늘 전체조회를 두번이나 하면서 랜덤으로 카테고리를 뿌려주는 로직을 Enum을 활용해 훨씬 더 효율적인 코드로 수정했다. 사실 Enum이라는 개념이 너무 생소해서 지난주에 공부하긴 했지만 그래도 와닿지가 않았는데, 직접 코드에 적용해 보면서 아 ! 이게 이렇게 쓰이는 거구나 라는 걸 알 수 있었다. 역시 코딩은 글로 읽는게 아니라 직접 해봐야 더 이해가 되는 것 같다.

 

4. 셀프칭찬 (오늘 잘한 일) : 이번 프로젝트 때 내가 맡은 부분이 그렇게 크진 않아서 최대한 팀한테 어떻게 도움이 될까 많이 고민했다. 내가 할 수 있는 도움은 에러가 터졌을 때 같이 찾아보는 정도인데, 내가 로직은 잘 못짜고 더디더라도 오타 하나만큼은 잘 찾느 것 같다. 오타를 잘 찾았던 오늘의 나를 칭찬해줄래 

 

5. 내일 할 일 : 도커 공부하기 


[오늘 공부한 부분] 

  • 오류 수정
  • CI/CD 공부 

[12] 게임 로직 - 랜덤으로 카테고리 / 키워드 뿌려주기

 

[12] 게임 로직 - 랜덤으로 카테고리 / 키워드 뿌려주기

GameController 웹소켓을 사용하기 때문에 @PostMapping 이 아닌 @MessageMapping 을 사용한다. @PathVariable 대신 @DestinationVariable 을 사용한다. @Slf4j @RequiredArgsConstructor @RestController public class GameController { private f

leejincha.tistory.com

 

 

1. 어려웠던 부분 

  • 비교적 순탄했던 지난주와 다르게 매일매일을 새로운 에러메세지와 마주해야했던 고된 한 주 였다. 생각보다 WebRTC 시그널링 연결이 오래 걸렸는데, http인 로컬 서버로만 했을 때는 문제없던 기능들이 https 서버로 환경을 바꿔주었더니 틈만나면 아래와 같은 에러 혹은 403, 404, 405에러, 혹은 http 어쩌구리 parsing 에러를 뱉어냈다. 

  • 알고보니 https를 배포할 때 인증서 발급을 받는데, 거기에 프론트엔드 도메인 주소와 백엔드 주소를 모두 기입하지 않고 백엔드 주소만 입력해서 발새한 문제였다. 두 주소 모두 같은 인증서에 등록해주니 이 문제는 일차적으로 해결이 되었다.
  • 그러나 이어서 다시 기능을 좀 수정하고 Redis 서버로 배포하기위해 돌리는 과정에서 의존성주입에러가 발생하기 시작했다. 호~ 구글링해서 나온 모든 사이트를 들여다 봤다고 해도 과언이 아니다. 그런데도 불구하고 아직까지 해결하지 못했다. 아마 내일부터 다시 이 에러를 해결해야 할 것 같다. 거진 다 끝났다고 생각했는데 하루에 하나씩 새로운 에러가 터지니 새롭다 정말 ^_^

 

2. 느낀 점 :

  • 팀원들 모두 매일 늦게까지 고생한 한 주 였다. 프론트 백에드 할 것 없이 같이 에러를 수정해보고 계속 고치면서 테스트해보고, 이 과정이 모두 지칠법 한데도 서로 배려하는 분위기라 팀원들한테 많이 배운 2주차 였다. 에러때문에 지치긴 했지만 디자이너 님이 우리 제안한 사항을 잘 반영해서 너무 좋은 디자인을 만들어 주셔서 뭐가 힘을 낼 수 있는 주차였던 것 같다.

 

3. 새로 알게 된 내용 

  • Spring vs Spring boot
  • Git vs Github vs Gitflow vs Github flow
  • Refresh Token
  • Truble shooting
  • WebRTC , WebSocket 개념 재정리
  • https 배포

 

4. 셀프칭찬 : 내가 맡은 부분이 다른 팀원들에 비해 적은편이라 빨리 끝낼 수 있었다. 맡은 부분을 끝내놓고 다른 팀원들의 에러를 같이 해결하기 위해 노력한 한 주였다. 같이 구글링을 해보고 덕분에 내가 담당하지 않았지만 다른 팀원이 맡은 기능에 대한 공부도 해보고, 스스로 풀어지지 않고 열심히 하려고 한 나를 칭찬한다.

 

5. 다음주 할 일 : 다음주 주말에 중간 회고가 있다. 어떻게든 MVP 구현하기 ! 


[ 이번주 공부한 부분 링크 정리] 

 

< 트러블 슈팅 정리 >

[41] 트러블 슈팅 : nonuniqueresultexception: query did not return a unique result

[42] 트러블 슈팅 : Type definition error

[43] 트러블 슈팅 : @RequestMapping의 produces 속성

[46] 트러블 슈팅 : InvaliDataAccessApiUsageException: detached entity passed to persist

[49] 트러블 슈팅 : JWT signature does not match locally computed signature

[50] 트러블 슈팅 : CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource (allowedOrigins)

[51] 트러블 슈팅 : (WebRTC) Error parsing HTTP request header

트러블 슈팅 : Invalid character found in method name. HTTP method names must be tokens

 

< 기본 개념 정리 >

[44] Spring vs Spring boot 차이

[45] Git vs Github vs Git Flow

[03] (Spring Boot) WebSocket / WebRTC

[08] Spring Security + Refresh Token (1)

[10] Spring Security Refresh Token (2)

[47] Enum

 

 

< 그 외 >

[02] Redis 설치 및 명령어 정리

[04] 카카오 로그인 PostMan 테스트 방법

[05] Hashmap을 JSON으로 변환하는 법

[06] Spring Security 인증인가 - 예외 커스텀 핸들링

 

1. 어려웠던 부분 : 오늘 기술멘토링이 있는 날이었다. 우리가 궁금했던 부분을 여쭤봤는데, 사실 시원한 해답을 얻진 못했다. 그냥 더 열심히 구글링하고 깨지면서 해결을 해봐야 한다. 그래도 이제 https 문제느 해결이 됐는데, 다른 팀원분이 CrudRepository를 적용한 파일을 테스트용으로 빌드하고 배포하는 과정에서 아래와 같은 에러가 발생했다. 

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'signalHandler': Unsatisfied dependency expressed through field 'gameRoomSessionRepository'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'gameRoomSessionRepository' defined in com.example.namoldak.repository.GameRoomSessionRepository defined in @EnableRedisRepositories declared on RedisRepositoriesRegistrar.EnableRedisRepositoriesConfiguration: Invocation of init method failed; nested exception is java.lang.reflect.InaccessibleObjectException: Unable to make field private final transient 
java.net
.InetSocketAddress$InetSocketAddressHolder 
java.net
.InetSocketAddress.holder accessible: module java.base does not "opens 
java.net
" to unnamed module @e580929

 

거의 반나절을 구글링을 해봤다.

뭔가 @Autowired 어노테이션이 잘못된 것 같아서 구글링에서 나온대로 여기 저기 @Repository 어노테이션도 붙여보고 @Quilifer 어노테이션으로 이름도 붙여줘보고, @Primary 어노테이션으로 우선순위도 줘보려고 해지만 계속해서 같은 오류메시지가 떴다. 근데 결국 새벽 1시가 될때까지 해결하지 못했다. ㅜㅜ 산넘어 산이다 정말.

 

 

2. 느낀 점 : 어떻게 해야 효율적으로 정보를 찾을 수 있을까? 이번주 내내 에러의 늪에 빠져보며서 좋은 해답을 얻기 위해선 어떻게 구글링을하고 좋은 자료를 찾을 수 있을까에대한 고민이 깊어졌다. 기본기가 탄탄하다면 더 좋았을 텐데, 너무 많은 양을 단기간에 배웠다보니 이런 부분에서 부족함이 느껴지는 것 같다.

 

3. 새로 알게 된 내용 : 멘토님이 Refresh 토큰을 도입하고 저장하는 부분에대해 고려해봤냐고 질문을 주셨다. 사실 아무 생각없이 코드만 구혀해 놓았기 때문에 오늘 피드백이 끝나고 그 부분을 공부해봤다. 아직 프론트와 맞춰보지 않았기때문에 상황이 달라질 수 있지만, 프론트에 전달할 땐 로컬 스토리지에 저장하고 서버에 저장할 땐 Redis 스토리지에 저장하는 방법을 사용하게 될 것 같다. 어렵다 어려워. 공부가 더 필요하다 ! !

 

4. 셀프칭찬 (오늘 잘한 일) : 존버하는 하루 하루

 

5. 내일 할 일 : WIL 작성하기


[오늘 공부한 부분] 

 

[10] Spring Security Refresh Token (2)

 

[10] Spring Security Refresh Token (2)

Refresh Token 저장 위치 크게는 두 경우로 저장할 수 있는데 Backend DB, Redis 등의 Storage에 저장하거나 Client측에 저장할 수 있다. Backend에 저장할 경우 JWT의 원래 도입 배경인 서버를 Stateless하게 유지

leejincha.tistory.com

 

Refresh Token 저장 위치

  • 크게는 두 경우로 저장할 수 있는데 Backend DB, Redis 등의 Storage에 저장하거나 Client측에 저장할 수 있다.
  • Backend에 저장할 경우 JWT의 원래 도입 배경인 서버를 Stateless하게 유지하려는 노력과 상반 될 수 있다.
  • Client측에서는 Local Storage 혹은 Cookie에 저장할 수 있다.

 

Front

토큰 기반 인증에서는 로그인한 사용자의 상태 정보를 클라이언트 단의 저장소(쿠키, 로컬 스토리지, 세션 스토리지 중 하나)에 토큰의 형태로 저장한다. 그렇다면 토큰은 쿠키, 로컬 스토리지, 세션 스토리지 중 어디에 저장하는 게 좋을까? 정답이 있는 것은 아니지만, 세션 스토리지에 저장하는 경우는 많지 않고 보통은 쿠키나 로컬 스토리지에 저장한다. 그리고 이 둘은 각각 장단점이 존재한다.

 

1. Local Storage

  • local storage 는 구현이 쉽지만 자바스크립트로 접근이 너무 쉬워서 XSS 공격에 취약하고 보안상 문제 소지가 많다.
  • 로컬 스토리지와 같은 웹 스토리지의 데이터는 매 요청마다 서버에게 자동으로 전송되는 것이 아니기 때문에 CSRF 공격에 상대적으로 안전하다.
  • 다만, HttpOnly 옵션으로 XSS 공격을 방지할 수 있는 쿠키와 달리 웹 스토리지는 그러한 보안 옵션들을 설정할 수 없다. 
  • 따라서 이스케이프 처리 등으로 XSS 공격에 대한 대응을 면밀히 해줄 필요가 있다. XSS 취약점이 발생하면 동일한 도메인 내에서 CSRF 공격까지 발생할 수도 있기 때문에 더욱 주의해야 한다.
  • 다행히 대부분의 현대 웹 어플리케이션이나 라이브러리들은 자동 이스케이프 처리를 해주는 경우가 많다. 하지만 어찌 됐든 한 번 설정해주면 그만인 쿠키에 비해서는 신경 써줄 부분이 많다는 단점이 있다.

 

  • HTTPOnly 와 Secure 옵션을 사용하고 CSRF 공격에 대비를 하면 어느정도 보안을 할 수 있다.
  • 클라이언트 단에 저장되는 만큼 토큰은 탈취되기 쉽다. 따라서 보안을 더욱 강화하는 것이 중요한데, 다행히도 쿠키는 보안을 위한 몇몇 옵션들을 설정하는 것이 가능하다.
    • 먼저, XSS 공격에 의해 쿠키가 탈취당할 수도 있으므로 HttpOnly 옵션을 설정하여 JavaScript로는 해당 쿠키에 접근할 수 없도록 해야 한다.
    • 다음으로, CSRF 공격에 의해 의도치 않게 쿠키가 전송되는 것을 막기 위해 SameSite 옵션도 Lax 혹은 Strict로 설정해줘야 한다.
    • 또한, Secure 옵션도 설정하여 HTTPS 프로토콜을 사용할 때만 쿠키가 전송될 수 있도록 해야 한다.

 

Back(Server Side)

서버 issue를 백엔드에서 관리하기 용이하다. 반대로 access token을 재발급하기 위한 API 요청이 많아질 수 있다.

 

1. Session

  • 세션에 저장하고 세션 만료 주기를 늘리는 방식. 사용자가 많은 경우를 고려하면 사용하지 않는 것이 좋다. 또한 JWT 이용 목적에 적합하지 않다.

2. DB

  • refresh token 를 데이터베이스에 저장한 후 index 값을 쿠키나 로컬스토리지에 저장하는 방법. 클라이언트 단에서 refresh token 을 노출하지 않는 추세로 바뀌는 중. refresh token을 DB에 저장해야만 탈취 당했을 때 해당 토큰을 폐기할 수 있다.(RTR기법)

 

내가 고려하고 있는 도입 방식

Refresh Token을 백엔드 서버에 저장

refresh token을 백엔드 서버에 저장해두고 클라이언트는 refresh token에 대한 정보를 쿠키로 가지고 있는 방식이다. 새로고침했을 때 access token을 html 자원과 함께 받기 위해 반드시 백엔드 서버를 거쳐야한다.

 

장점

  • 서버 issue를 백엔드 서버에서 대부분 처리하기 때문에 관리에 용이하다.
  • 사용자가 많아짐에 따라 생기는 서버 issue를 백엔드에서 모두 처리하기 때문에 관리 포인트가 프론트 서버, 백엔드 서버 둘로 나뉘지 않고 백엔드 서버 한 곳에서 처리된다. 따라서 서버 issue 관리에 용이하다.

단점

  • 새로 고침하면 항상 프론트 서버를 거쳐 백엔드 서버까지 요청이 전달된다.
  • token이 프론트 서버에 따로 저장되지 않기 때문에 access token을 얻기 위해서는 항상 백엔드 서버까지 요청이 전달되어야 한다. 요청 횟수, 요청에 드는 간접비용 등을 고려해봤을 때 비효율적일 수 있다.

 

Redis에 Refresh Token을 저장하는 이유

레디스는 key-value 쌍으로 데이터를 관리할 수 있는 데이터 스토리지이다. 데이터베이스라고 표현하지 않은 이유는 기본적으로 레디스는 in-memory로 데이터를 관리하므로, 저장된 데이터가 영속적이지 않기 때문이다.

데이터가 HDD나 SDD가 아니라 RAM에 저장하므로 데이터를 영구적으로 저장할 수 없는 대신, 굉장히 빠른 액세스 속도를 보장받을 수 있다. 빠른 액세스 속도와 휘발성이라는 특징으로 보통 캐시의 용도로 레디스를 사용한다. Refresh Token의 저장소로 레디스를 선택한 이유도 위와 같다. 빠른 액세스 속도로 사용자 로그인시 (리프레시 토큰 발급시) 병목이 되지 않는다.

 

또한 리프레시 토큰은 발급된 후 일정 시간 이후 만료되어야 한다. 리프레시 토큰을 RDB등에 저장하면, 스케줄러등을 사용하여 주기적으로 만료된 토큰을 만료 처리하거나 제거해야한다. 하지만, 레디스는 기본적으로 데이터의 유효기간(time to live)을 지정할 수 있다. 이런 특징들은 리프레시 토큰을 저장하기에 적합하다.

 

그리고 리프레시 토큰은 실수로 제거되어도 다른 데이터에 비해 덜 치명적이다. 최악의 경우가 모든 회원들이 로그아웃 되는 정도이다.

물론 JWT와 같은 클레임 기반 토큰을 사용하면 리프레시 토큰을 서버에 저장할 필요가 없다. 하지만, 사용자 강제 로그아웃 기능, 유저 차단, 토큰 탈취시 대응을 해야한다는 가정으로 서버에서 리프레시 토큰을 저장하도록 구현하는게 좋다.

 


[ 참고 자료 ]

+ Recent posts