나는 개발 일을 하는 것이 좋다. 그래서 퇴근 후에 사이드 프로젝트도 자주 한다. 사이드 프로젝트에서는 내가 직접 방향을 정하고, 요구사항도 결정하며 마음껏 상상할 수 있어서 좋다. 이 과정에서 창의적인 문제 해결의 재미를 느낄 수 있다. 그러나 때때로 요구사항이 불분명하거나 방향성을 잡기 힘든 업무를 맡게 되면, 그 재미는 급격히 줄어들고, 마치 주어진 임무를 기계적으로 수행하는 듯한 느낌을 받을 때가 있다. 개발자가 아니더라도 기획자나 디자이너도 이와 같은 상황에서 비슷한 기분을 느낄 것이라고 확신한다. 그럼에도 우리는 요구사항을 더 명확히 하고, 공학적으로 접근하여 더 나은 답을 도출하기 위해 노력한다. 왜냐하면 우리는 결국 사용자들의 니즈를 해결해야 하는 엔지니어들이기 때문이다.

 

 

작품 속 용기는 이런 점에서 나와 닮아있었다. 아니, 어쩌면 이따금씩 이러한 상황에 직면하는 우리들보다 더 힘든 상황에 처해 있을지도 모른다. 용기는 매일 보안업체에서 출동 업무를 성실히 수행한다. 그러나 그의 진정한 열정은 다른 곳에 있다. 그는 원래 럭비 선수가 되고 싶었지만, 부상으로 인해 그 꿈을 포기해야만 했다. 용기가 지금 하는 일과 럭비 선수의 직업이 유사한 점이라면, 둘 다 피지컬을 사용한다는 점뿐이다. 그가 보안업체에서 출동할 때면 대부분의 경우가 취객의 실수이거나 쥐나 고양이 같은 동물로 인한 헤프닝에 불과했다. 이처럼 유의미한 출동은 거의 없었지만, 용기는 현실적인 성격 덕분에 주어진 임무를 묵묵히 수행한다. 그러면서 앞으로 주어진 먹먹한 현실에 우울해하고 있다. 마냥 해맑기만하고 긍정적인 7살 어린 여자친구의 처지도 그다지 달라보이지않는데... 어찌 그렇게 명랑하기만 한 것인지... 그뿐만일까? 용기는 마음속 깊은 곳에서는 여전히 못이룬 럭비 선수에 대한 아픈 미련이 영 떠나지 않는다. 꿈속에서 조차 럭비 경기를 뛰어야 했으니까 말이다... 

 

 

어찌되었든 이제 괜찮다고, 용기는 지친 자신을 다독였다. 여기가 내 자리야. 꿈꿨던 직업은 아니지만 변두리의 밤을 지키는 출동 요원이 되었다. 팀 사람들도 다 맘에 들고, 격의 없는 동네 누나와 가끔 놀고, 귀엽고 꼬인 데 없는 여자친구와 데굴거리고. 더 바랄게 없다. 돌아가고 싶은 때도 장소도 없다.
  인생이 테트리스라면, 더이상 긴 일자 막대는 내려오지 않는다. 갑자기 모든 게 좋아질 리가 없다. 이렇게 쌓여서, 해소되지 않는 모든 것들을 안고 버티는 거다.

 

 

용기가 새로 생긴 여자친구가 있었음에도 그의 X, 재화는 여전히 그에 대한 미련이 엄청 났었다 보다. 그녀는 세컨드 직업으로 작가를 준비하고 있었다. 그녀의 소설속에서는 매번 용기가 등장하고, 그는 매번 죽었다. 누군가는 용기에 대한 악감정이 얼마나 심하면 이렇겠냐고 생각 할 수도 있겠다만, 나는 이것이 그를 잊지 못한 미련이라고 생각한다.

 

 

원전폐기물 보관함처럼, 위태롭지만 조용하게. 엉망인 내부를 숨기면서 사는 건 모두가 마찬가지 아닐까? 뭔가 중요한 부분이 고장나버렸다면 더욱 들켜서는 안 된다. 안쪽에 나쁜 냄새가 나는 죽은 것들이 가득하다는 걸 상대가 알아버리면 바로 도망치고 말 테다. 용기가 그랬던 것처럼.
  돌아누울 때마다 머릿속에서 부품들이 굴러다니는 소리가 들리지만, 아직은 버틸 수 있다. 괜찮다.

 

 

승주와 헤어져 밖으로 나서니, 초가을 거리에는 매미들이 죽어 떨어져 있었다. 여름 내내 강렬했던 구애의 끝이 가루로 부서지는 몸이라니 슬펐다. 그래서, 너희는 바라던 사랑을 얻고 죽었니? 재화는 죽은 매미들을 깨워 묻고 싶었다.

 

 

정세랑 작가의 책은 독자의 마음 깊숙이 스며들어 잊고 있던 기억을 살며시 불러낸다. 그녀의 이야기는 독특한 전개와 때로는 황당하게 느껴질 법한 상상 속에서도 한 편의 시처럼 마음을 적시며 독자를 이야기의 품으로 부드럽게 이끌어간다. 마치 오래된 꿈 속을 헤매는 듯, 그녀의 글은 천천히, 그러나 확실히 내 안에 잔잔한 울림을 남긴다. 회사에서 점심을 먹고나면 30분 정도가 남는다. 누군가는 동료들과 커피한잔을 하러가거나, 동기와 수다를 떨러가거나, 운동을 하러가거나, 엎드려 잠을 자곤한다. 나는 이 책을 조금 이라도 더 읽고 싶은 마음에, 회사에서 남은 점심시간을 수시로 모니터의 오른쪽 아래 시계를 봐가며 책의 내용에 초집중을 했다. 덧니가 보고 싶어는 정세랑 작가의 책 중 유독 마음에 드는, 내 마음을 아프게도하고, 웃게도 만드는 글들이 많이 있었다. 나의 삶을 관철하고 있는 것 같은 기분이 들었다.

 

 

노래들을 듣고 나서, 여왕이 싱어송라이터에게 말했다.
"얼음에 손도 대지 말아요. 얼음을 어쩌겠다는 생각도 하지 말아요."
싱어송라이터는 동의의 뜻을 밝혔다. 다음날도, 그 다음날도 소리 나지 않는 기타를 들고 앉아 있다 갔을 뿐이었다.
이례적인 관계의 두 사람을 얼음을 사이에 두고 그대로 있는게 그저 좋았다. 영원히 그런 날들이 지속될 줄 알았다.
  두 사람이 동굴 안에서 미처 알아채지 못했던 것은, 지구온난화였다. 지구 온난화가 아주 심해졌고, 어어, 하는 사이에 어느 날 얼음 관이 모두 녹았다. 싱어송라이터는 무척이나 당황해서는 뒤로 멀찍이 물러나 기다렸다. 여왕은 미지근하게 젖은 머리카락에서 물기를 짜고는, 드디어 낯선 악기의 소리를 제대로 들었다.

 

 

나는 이 이야기를 좋아한다. 현실적이지 않아서 좋다. 해피엔딩으로 끝났으니까.

 

 

 

 

영화 부탁 하나만 들어줘에서 숀은 에밀리를 만나기 위해 모든 걸 포기 했다고 스테파니에게 말한다. (사실 에밀리는 미친 여자였지만 말이다. 미친 여자인걸 낌새를 알았지만서도 당시 그는 그녀에게 푹 빠져있었다.) 나는 재화가 이 숀을 닮았다고 생각했다. 여전히 용기에게 푹 빠져있는 그녀의 내면을 보면서.

 

 

선이의 특제 카레는 아니었지만, 짜장도 꽤 맛있었다. 때때로 인생이 그렇다는 생각이 들었다. 간절히 원하는 것은 가질 수 없고, 엉뚱한 것이 주어지는데 심지어 후자가 더 매력적일 때도 있다. 그렇게 난감한 행운의 패턴이 삶을 장식하는 것이다. 물론 매력적인 후자를 가지게 되었음에도 최초의 마음, 그 간절한 마음은 쉽게 지워지지 않아 사람을 괴롭히기도 하고.

 

 

정세랑 작가는 어쩌면 인생 2회차인 것이 아닐까...? 그렇지 않고서야 이러한 경험을, 누구나가 경험 할 수 있는, 그러나 무의식적으로만 느낄 뿐 그 누구도 누군가에게 이야기하지 않고 지나쳐버릴 삶의 한 현상을 글로 이렇게 표현 할 수 있다니... 아무튼 이 부분에서 나는 재화의 용기를 향한 여전한 마음을 느낄 수 있었다. 내 생각엔 어쩌면 용기도 7살 연하 여자친구를 두고 그러한 생각을 가지고 있었으리라. 그리고 나는 이따금 잘 만든 가정집 카레가 너무 먹고싶다는 생각을 하곤 했는데, 이 대목을 읽으며 그러한 생각이 더 강렬해졌고, 이 글을 쓰며 다시 되새김질 되어 정말 너무 카레가 먹고싶어졌다. 농담을 하는 것이 아니다. 어쨌거나, 위 대목의 내용에 대해 나는 매우 진지하게 생각했다.

 

 

옛날 사람들처럼 편심, 촌심, 단심 같은 단어들을 쓸 때마다 지잉, 하고 뭔가 명치께에서 진동하고 만다. 수천 년 동안 쓰여온, 어쩌면 이미 바래버린 말들일지도 모르는데, 마음을 '조각' 혹은 '마디'로 표현하고 나면 어쩐지 초콜릿 바를 꺾어주듯이 마음도 뚝 꺾어줄 수 있을 듯해서. 그렇게 일생일대의 마음을 건네면서도 무심한 듯 건넬 수 있을 듯해서.
  언젠가 용기에게 사랑한다고 말했던 날이 있었다. 용기는 그 말을 초콜릿 바를 받은 가벼이 받았었다. 재화의 마음, 꺾인 부분에서는 잔 가루들이 날렸는데.
  너는 모르지.

 

 

역시 재화는 용기를 애타게 기다렸다보다, 떠나간 이를 정리하지 못하고 그때의 그이에 대한 아쉬움이 드러나는 대목이다. '너는 모르지...' 우리는 이런 말을 마음속으로만 하곤한다. 상대방에게 들리지도 않을 이야기를. 그리고 어쩌면 우리도 누군가에게 이런 말들을 수도 없이 들었을지도 모른다. 아마도 정세랑 작가의 표현들을 읽다보면, 그런 생각이 난다. 결국 이러한 글귀들이 곧 정세랑 작가의 실제 경험과 그것에 따른 영감이 나온게 아닐 까하는.. 예를 들면, 지하철 플랫폼에서 어떤 여자가 강아지를 와앙하고 입을 크게 벌리고 먹어 깜짝놀랐으나 알고 보았더니 그것이 왕만두였다는 내용이 그러하다. 분명 정세랑 작가는 그 글을 쓰기 며칠 전 멀리서 왕만두를 보고 말티즈로 착각했으리라...(아님말고요ㅎ)

 

 

부모님 입장도 이해는 갔다. 미운 오리 새끼 같은 딸을 열심히 키우면 백조가 될줄 알았는데, 시조개나 가루다처럼 알 수 없는 괴생물로 자라버렸으니. 그러나 부모의 승인을 받는 트랙에서는 벗어난지 오래였고 돌아가고 싶지도 않았다.

 

 

  나는 오늘도 네 좌표를 알지 못해. 우리의 좌표가 어디서부터 어긋났는지 알지 못해. 그렇게 말할 필요가 있었다.
  재화는 정말로 우주선에 있을 법한 작고 딱딱한 침대에서 잠이 들었다. 발끝이 시렸다. 잠결에, 엔진처럼 무언가 허밍하는 소리를 들었던 것도 같았다.

 

 

  "너한테 설명할 말이 없어. 하지만 이건 내가 한 일이 아니고 재화가 한 일도 아닐 거야."
  용기는 자기가 얼마나 재수없는 얼굴을 하고 있을까 상상하며 말했다.
  "닥쳐."
  충분히 재수없는 모양이었다.

 

 

"초등학교 때 모둠 비빔밥을 해먹으면 꼭 돈가스를 해오는 애들이 있었어. 아마 엄마한테 알림장을 보여주지 않았던 거겠지. 그런데 비빕밥에 들어간 그 엉뚱한 돈가스가 의외로 또 맛있었다? 다 부서지고 눅눅해지고 그랬는데도 맛이 있었어. 그 돈가스처럼 오빠가 좋았어."

 

 

이 소설속에서 제일 딱한 존재는 용기의 7살 어린 여자친구다..... 용기는 그녀에게 어떠한 적절한 해명도 해줄 수 없었다. 그녀에게 용기는 아마 제일 재수없는 X에 미쳐버린 나사빠진 전남친 정도로 기억될 것이다. 그녀의 친구들도 함께 술자리에서 쌍욕을 해주며 잊으라고 할 것이다. 아마 용기는 이런 부분들도 담담히 받아들이고 오히려 다행이야... 라고 생각 할 것 같다. 똥차가고 벤츠가 온다 했던가, 분명 그녀라면 이제 나이 차이가 덜나는 근사한 남자친구가 생겼을 것이다.

 

 

그랬던 용기가 열여섯 시간씩 자게 되었다. 자고 있을 때에만 크고 작은 상처들이 아무는 걸 느꼈다. 안쪽이, 오래전에 잃었던 균형 잡인 상태로 되돌아가는 것 같았다. 그런 경험을 하고 나서야 비로소 동물들에게 미안해졌다. 코알라들의 대화를 이해할 수 없으니 잘은 모르겠지만, 어쩌면 코알라들도 여자친구에게 세게 차였는지도. 그저 아주 멋진 꿈을 꾸는 종일 수도 있지만.......
....
미안해, 코알라들.
미안해, 여자친구.
미안해, 재화.

 

 

피곤할때면 잠을 정말 열시간, 열 두시간 씩 자곤한다. 정말 피로에 지쳐 그런 걸까? 아니면 이 세상에서 잠시 벗어나 꿈속에서 나만의 안식처를 찾고 싶은 걸까... 아니면 둘다인걸까? 어찌돼었든, 코알라가 잠을 많이 자는 이유는 그들의 식단과 에너지 소비 방식과 관련이 있다. 코알라는 주로 유칼립투스 잎을 먹고 사는데, 이 잎들은 영양가가 낮고 소화하기 어렵다. 또 유칼립투스 잎에는 독소가 포함되어 있어서 이를 해독하는 데도 에너지가 필요하다. 코알라는 이와 같은 저영양 식단을 보충하기 위해 하루에 약 18~22시간을 잠으로 보낸다.

 

 

  "아니야, 언닌 정답이야. 무슨 일이 있어도 계속 정답으로 지켜나가는 사람이니까. 난 누군가의 유사답 정도는 되어본 적 있는 것 같은데, 한 번도 정답은 못 되어봤네."
  선이는 빨대 껍질을 잘게 찢으며 재화의 말을 곰곰 따져보는 듯했다.
  "그런거 될 필요 없는 것 같아. 누구의 무엇도."

 

 

작품 속 선이는 현명한 사람이다. 누군가의 언니, 누군가의 누나로서 좋은 메시지를 계속 전한다. 카레를 짜장으로 바꾸어 버리는 엉뚱하고 황당한 헤프닝을 만들기도 하지만 말이다. 그리고 무엇보다도, 재화와 용기를 엮어주는 중요한 매개체이자, 그들의 잠재된 서로에 대한 마음을 행동으로 바꾸어주는 촉매의 역할을 단단히 해준다. 작품속의 그 누구보다도 단단한 마음과, 그 마음속에는 여유마저 갖추고 있는듯 보인다. 결국, 선이의 어시스트로 두 사람은 만나게 된다. 그것도 무시무시한 스릴러 내용 속에서 극적으로 말이다. 나는 스릴러를 꽤나 좋아하는 편인데 아주 자극적으로 표현한 정세랑 작가의 솜씨에 감탄하게 되었다. 이를 6개나 뽑아버린 상황을 가정하는데, 꽤나 끔찍하게 묘사되어 인상적이었다. 실제로 영화라도 보는 듯한 생생함을 느꼈다.

 

 

이러한 극적인 상태에서 결국 두 사람은 만나는 것으로 끝이 난다. 마지막을 읽고 나서야 깨닫게 되었다. 용기는 재화의 소설속에서만 컨테이너(container)였었다. 감금된 채 이가 뽑힌채로 죽을 마당에 생겼던 그녀를 구출하고자 했던 현실에서는 커리지(Courage)였던 것이다. 재화는 자신을 회피했고 떠나간 용기를 컨테이너라고 생각했었다. 그를 여전히 사랑하지만 도무지 자신을 구해러와주지 않는 용기를 자신의 이야기속에서 죽였었고 겁쟁이 취급을 했다. 어쩌면 재화는 앞으로 자신의 소설속에서 더이상 죽지 않는 용기, 커리지한 용기를 그려나갈 것이다. 나는 이 책이 좋다. 마음을 요동치게 하는 글들이 너무나 많고, 일일히 그것을 담아두고자 카메라앱을 열어서 캡쳐를 해놓는다. 언제든 그 글들을 읽어 볼 수 있도록. 이번 주말은 푹 쉬었다. 하고싶었던 개발 공부도하고, 아파서 저번에 쉬었던 수영도 열심히 했다. 여름이라 그런지 사람들이 수영장에 많아졌고 수영장의 기본 예의를 모르는 사람들도 많아졌다. 그래도 개의치 않고 내가 가고자했던 1km는 넘게 헤엄을 쳤다. 그리고 삼겹살을 주문해서 맥주와 함께, 영화 한편과 저녁을 보냈다. 그리고 일요일은 내리 잠만 잤다. 코알라처럼.

나는 이 이야기들을 좋아한다. 현실적이지 않아서 좋다. 해피엔딩으로 끝났으니까.

(그리고 사실 표지 속 그려진 용기가 나를 묘하게 닮은 구석이 있는 것 같아 책이 별나다는 생각과 이상한 기분을 느꼈다. 또 하필 장면이... 에로에로젤리가 떠오른다.)

 

 

끝.

 

 

 

 

 

 

 

 

 

 

 

 

'독서' 카테고리의 다른 글

지구에서 한아뿐  (6) 2024.09.04
밝은 밤  (0) 2024.08.22
보건교사 안은영 (팬픽션)  (0) 2024.08.02
비행운  (0) 2024.07.27
아주 희미한 빛으로도  (0) 2024.07.14

안녕하세요? 2차 인증(Two-Factor Authentication)을 위해 OTP에 대해 공부하던 중 알게된 사실들에 대해 정리해보려고 합니다.

OTP(One-Time Password, 일회용 비밀번호)의 동작 원리는 사용자가 인증을 시도할 때마다 한 번만 사용할 수 있는 비밀번호를 생성하고 검증하는 것을 의미 합니다. OTP는 보안성을 높이기 위해 고안된 방식으로, 주요하게 두 가지 방식이 있습니다: 시간 기반 OTP(Time-based OTP, TOTP)와 카운터 기반 OTP(HMAC-based OTP, HOTP). 여기서 TOTP가 더 일반적으로 사용됩니다. 오늘 알아볼 내용도 TOTP에 대해 알아봅니다 :)

제가 알아본 방법은 commons-codec 방식과 warrenstrange/googleauth 라이브러리 방식이 있는데요. 이 둘중에 무엇을 활용할지는 프로젝트 요구사항과 선호하는 개발 스타일에 따라 달라질 수 있습니다. 두 가지 접근 방식의 장단점은 아래와 같습니다.

 

 

Commons-Codec 방식

장점:

  1. 경량성: commons-codec는 일반적인 인코딩/디코딩 작업을 위한 라이브러리로, OTP 생성에 필요한 HMAC-SHA 알고리즘을 구현하는 데 사용할 수 있습니다. 이 라이브러리는 상대적으로 가볍습니다.
  2. 유연성: 라이브러리를 사용하여 직접 OTP 알고리즘을 구현하면, 특정 요구사항에 맞게 커스터마이징할 수 있습니다. HMAC-SHA1, SHA256, SHA512 등의 다양한 해시 알고리즘을 사용할 수 있습니다.
  3. 통제: OTP 생성 로직을 직접 구현하기 때문에 세부 사항을 커스텀 할 수 있습니다.

단점:

  1. 개발 복잡성: OTP 알고리즘을 직접 구현하려면 추가적인 코딩 작업이 필요하며, 표준 준수 및 보안성을 확보하기 위해 신중한 구현이 필요합니다.
  2. 버그 발생 가능성: 직접 구현할 경우 실수로 인한 버그가 발생할 가능성이 있습니다.

 

Warrenstrange/Googleauth

장점:

  1. 간편함: warrenstrange/googleauth는 구글 OTP 알고리즘(TOTP) 구현을 위한 라이브러리로, 설정과 사용이 간편합니다. 복잡한 구현 없이 쉽게 OTP 기능을 추가할 수 있습니다.
  2. 신뢰성: 이미 검증된 라이브러리를 사용함으로써 보안성과 신뢰성을 확보할 수 있습니다. 널리 사용되는 라이브러리이기 때문에 커뮤니티와 문서를 쉽게 찾을 수 있다는 장점이 있어요.
  3. 통합: 구글 OTP 표준을 준수하므로, 구글 인증 앱 등과 쉽게 통합할 수 있습니다.

단점:

  1. 제한된 유연성: 라이브러리의 기능과 설정에 제약이 따릅니다. 즉 특정 요구사항에 맞게 커스터마이징하기 어려울 수 있습니다.
  2. 외부 의존성: 외부 라이브러리에 의존하게 되므로, 라이브러리 업데이트 및 유지보수에 대한 추가적인 고려가 필요합니다.

 

결론

  • 단순하고 빠른 구현이 필요한 경우: warrenstrange/googleauth 라이브러리
  • 커스터마이징과 세부 제어가 필요한 경우: commons-codec

 

TOTP (Time-based OTP)의 동작원리

TOTP는 시간에 기반하여 OTP를 생성합니다. 사용자는 일정한 시간 간격마다 변경되는 비밀번호를 사용하여 인증을 받아야 하는 방식이죠. 일반적으로 우리는 사용자 측면에서만 TOTP를 이용해왔습니다. 우선 1차 로그인을 하면, 구글 OTP와 같은 앱을 켜서 정해진 시간마다 변동돼는 6자리 숫자를 모바일로 확인해서 웹화면에 입력하죠. 그렇다면 이러한 일련의 과정은 전체적으로 어떤 과정이 일어나기에 가능한걸까요? 이번에는 TOTP의 그 동작 원리에 대해 확인해 봅시다.

  1. 공유 비밀 키(Secret Key): 서버와 클라이언트(사용자)는 미리 공유된 비밀 키를 가지고 있어야 합니다.
  2. 시간 동기화: 서버와 클라이언트는 시간을 동기화해야 합니다. 일반적으로 Unix 타임스탬프(1970년 1월 1일 00:00:00 UTC부터의 초)를 사용합니다.
  3. 시간 슬롯 계산: 현재 시간을 일정한 간격(예: 30초)으로 나누어 슬롯을 계산합니다. 이 간격을 시간 틱(Time Tick)이라고 합니다.
  4. HMAC 계산: 현재 시간 슬롯과 비밀 키를 사용하여 HMAC-SHA1 알고리즘을 적용합니다. 여기서 HMAC은 키를 사용한 해시 기반 메시지 인증 코드입니다.
  5. OTP 생성: HMAC의 결과로부터 특정 길이(예: 6자리)의 숫자를 추출하여 OTP를 생성합니다. 일반적으로 OTP는 HMAC 결과의 일부를 사용하여 6자리 또는 8자리 숫자로 변환됩니다.
  6. 검증: 사용자가 입력한 OTP를 서버에서 동일한 방식으로 생성된 OTP와 비교하여 검증합니다. 시간 슬롯이 짧기 때문에 OTP는 매우 짧은 시간 동안만 유효합니다.

 

Warrenstrange/Googleauth를 이용한 키 제너레이트 및 검증

 

pom.xml

<dependency>
    <groupId>com.warrenstrange</groupId>
    <artifactId>googleauth</artifactId>
    <version>1.5.0</version>
</dependency>

 

import com.warrenstrange.googleauth.GoogleAuthenticator;
import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator;

public class GoogleAuthExample {

    public static void main(String[] args) {
        // Create an instance of GoogleAuthenticator
        GoogleAuthenticator gAuth = new GoogleAuthenticator();

        // Generate a new key
        GoogleAuthenticatorKey key = gAuth.createCredentials();
        String secret = key.getKey();
        System.out.println("Secret key: " + secret);

        // Generate a QR code URL to be scanned by Google Authenticator app
        String userName = "user@example.com";
        String issuer = "ExampleIssuer";
        String qrCodeUrl = GoogleAuthenticatorQRGenerator.getOtpAuthURL(issuer, userName, key);
        System.out.println("QR Code URL: " + qrCodeUrl);

        // Simulate the OTP that the user would provide from their Google Authenticator app
        int otp = gAuth.getTotpPassword(secret);
        System.out.println("Generated OTP: " + otp);

        // Verify the provided OTP
        boolean isCodeValid = gAuth.authorize(secret, otp);
        System.out.println("Is the OTP valid? " + isCodeValid);
    }
}

getOtpAuthURL로부터 알아낸 URL을 실제로 접속하면 QR코드로서 확인해 볼 수 있습니다. 그런데 이렇게 하면 api.qrserver.com에 귀속된 URL을 알려주게 되는데, api.qrserver.com은 구글 서버가 아닙니다. api.qrserver.com은 goqr.me라는 웹사이트에서 제공하는 QR 코드 생성 서비스의 API 라고하네요. goqr.me는 QR 코드를 생성하고 관리하는 데 특화된 서비스로, 다양한 형태의 QR 코드를 쉽게 만들 수 있도록 도와줍니다. 하지만, 이런 의존적인 방식이 마음에 안들경우 직접 QR을 생성할 필요성이 있는데, 이 부분에 대해서는 ZXing 라이브러리를 사용하여 자체적으로 QR을 생성하는 방식으로 해결하였습니다!

 

QR 코드 자체생성하기

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.google.zxing</groupId>
        <artifactId>core</artifactId>
        <version>3.4.1</version>
    </dependency>
    <dependency>
        <groupId>com.google.zxing</groupId>
        <artifactId>javase</artifactId>
        <version>3.4.1</version>
    </dependency>
</dependencies>

 

QR을 만들어내는 서비스 로직을 아래와 같이 만들어봅시다.

import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import org.springframework.stereotype.Service;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Service
public class QRCodeService {

    public byte[] generateQRCode(String text, int width, int height) throws WriterException, IOException {
        QRCodeWriter qrCodeWriter = new QRCodeWriter();
        Map<EncodeHintType, Object> hints = new HashMap<>();
        hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");

        BitMatrix bitMatrix = qrCodeWriter.encode(text, BarcodeFormat.QR_CODE, width, height, hints);
        ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream();
        MatrixToImageWriter.writeToStream(bitMatrix, "PNG", pngOutputStream);
        return pngOutputStream.toByteArray();
    }
}

 

이제, 컨트롤러 입니다.

import com.google.zxing.WriterException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

@RestController
public class QRCodeController {

    @Autowired
    private QRCodeService qrCodeService;

    @GetMapping("/generateQRCode")
    public ResponseEntity<byte[]> generateQRCode(@RequestParam String text) {
        try {
            byte[] qrCodeImage = qrCodeService.generateQRCode(text, 350, 350);
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.IMAGE_PNG);
            return ResponseEntity.ok().headers(headers).body(qrCodeImage);
        } catch (WriterException | IOException e) {
            return ResponseEntity.status(500).body(null);
        }
    }
}

 

이제 http://localhost:8080/generateQRCode?text={인코딩하고자 하는 데이터} 를 요청하면, 자체적으로 생성된 QR PNG를 확인해 볼 수 있습니다 :)

어때요? 근사하죠? 읽어주셔서 감사합니다.

얼마전 회사에서 후배 개발자의 이슈를 함께 고민하던 중 특이점을 발견했습니다.

몇년이 지난 과거 제가 진행했던 프로젝트에 대한 이슈였는데요

GET 방식의 API를 @RequestBody를 이용하여 요청을 받아내고 있었습니다.

애초 제가 해당 프로젝트를 할 때는 그 데이터를 받을 때는 QueryPram으로 받도록 설계했었습니다.

RequestBody로 변경된 이유를 묻자, 취약점 점검 시 해당 부분이 문제점으로 발견되어 고치게 되었다고 하네요...

이슈 처리 바빠서 그 후배에게는 아직 설명을 못해주었지만, GET API를 QueryPram을 RequestBody로 변경한 것은 적절치 못합니다. 오늘은 그 이야기에 대해 포스팅 해보려고 합니다.

 

과거 프로젝트를 진행할 때 당시 개발자들과의 고민은 "어떻게 해야 MSA 서비스에서 RestFul한 API를 제공할 수 있을지"에 대해서 였습니다. API의 기본 아키텍쳐에 대한 고민이었죠. 그 중 가장 컸던 부분은 필터 기능에 대한 것이었습니다.

간단한 예시를 들자면 게시판에서 여러가지 조건검색을 할 수 있는데 이 필터 조건들을 어떻게 API에게 전달할지에 대한 고민이었죠. 곧장 저희도 RequestBody를 떠올렸습니다. 예를 들면 이러한 JSON구조를 생각해 볼 수 있겠네요.

{
    "filterId": "subject", // 어떤 타입의 필터인지? 제목검색, 날짜검색 등등
    "filterTarget": "개발", // 어떤 대상으로 필터링할것인지? 내용, 날짜 등등
    "filterOrder": "like", // 대상에 대해 어떤 조건으로 필터링할것인지? 포함돼는 문자열, 특정 날짜 범위 등등
}

흠... 확인히 RequestBody로 하면 깔끔하게 보이긴하네요, 쓰고싶어지는 유혹(?)이 듭니다. 하지만 저희는 당시 RequestBody방식을 선택하지 않았습니다. 왜냐하면 이건 RestFul하지 않기 때문이죠. 왜일까요?

기본적으로 RequestBody는 GET에서 사용하길 권장하지 않아요 아래와 같은 이유 때문입니다.

 

  • HTTP 표준 준수: HTTP/1.1 표준(RFC 7231)에 따르면, GET 요청은 요청 본문을 포함하지 않아야 합니다. GET 요청의 목적은 리소스를 조회하는 것이며, 본문을 포함하는 것은 표준을 벗어난 사용입니다.
  • 캐싱 문제: GET 요청은 캐시될 수 있어야 합니다. 캐시는 요청 URL을 기준으로 동작하므로, 본문을 포함한 GET 요청은 캐시 시스템에서 올바르게 처리되지 않을 수 있습니다. 본문을 포함한 요청은 캐시 일관성을 유지하기 어렵게 만듭니다.
  • 안전성과 멱등성: GET 요청은 안전하고 멱등적이어야 합니다. 즉, GET 요청은 서버 상태를 변경하지 않아야 하고, 동일한 GET 요청을 여러 번 실행해도 결과가 동일해야 합니다. 본문을 포함하는 GET 요청은 이러한 특성을 깨뜨릴 수 있습니다.
  • 클라이언트와 서버의 지원 부족: 많은 HTTP 클라이언트 라이브러리 및 서버 프레임워크는 GET 요청의 본문을 지원하지 않거나 무시합니다. 예를 들어, 일부 브라우저는 GET 요청 본문을 보내지 않으며, 일부 서버는 본문을 처리하지 않습니다.
  • 표준 툴 및 라이브러리 호환성: 많은 개발 도구와 라이브러리는 GET 요청에 본문이 없다는 가정을 하고 설계되어 있습니다. 따라서, GET 요청에 본문을 포함하면 이러한 도구들과의 호환성 문제를 일으킬 수 있습니다.

또한 첨언을 더 해보자면, GET은 기본적으로 URL에 데이터가 포함되길 기대되며, URL에 요청 파라미터가 포함된 형태 + Body에도 포함된 형태는 일관적이지않으며 API의 복잡도를 오히려 더 늘어나게 된다고 판단했기 때문입니다. 예를들어 다음과 같은 API를 상상해보세요. 

URL
[GET] http://localhost:8080/api/board/100

Requst Body

{
    "filterId": "subject", // 어떤 타입의 필터인지? 제목검색, 날짜검색 등등
    "filterTarget": "개발", // 어떤 대상으로 필터링할것인지? 내용, 날짜 등등
    "filterOrder": "like", // 대상에 대해 어떤 조건으로 필터링할것인지? 포함돼는 문자열, 특정 날짜 범위 등등
}

 

100이라는 boardSeq 게시판에 대해 RequestBody로 검색하는 기능인데... pathParam도 있고, Body에도 조건이 포함되어 있어요. 이러한 구조는 API의 직관성을 해친다고 생각합니다.

그럼 저희는 어떻게 했을까요? 저희는 JSON데이터를 URL인코딩하여 QueryPram으로 처리하였습니다.

URL
[GET] http://localhost:8080/api/board/100?filter=%7B%22filterId%22%3A%22subject%22%2C%22filterTarget%22%3A%22%EA%B0%9C%EB%B0%9C%22%2C%22filterOrder%22%3A%22like%22%7D

Requst Body

없음

 

URL하나만으로 모든 것을 표현할 수 있습니다. 또한 이러한 특징은 사용자가 URL를 타인에게 공유하거나 응용하여 사용 할 때도 도움을 줄 수 있습니다. :) 훨씬 직관적이죠?

그렇다면 보안취약점에 해당 부분이 문제가 된다면 어떻게 해결해야 할까요? 만약 저였다면 문제로 지적된 파라미터에 대해 암호화 처리 등을 통해 해결하였을 것 같아요. ㅎㅎ 아니면 문제가 될 부분이 아닌데 탐지 되었을 가능성도 있으니 그 부분도 고려를 해서 예외처리를 하던가 해야겠지요? 그 후배가 틀렸다는 이야기를하는 것은 결코 아닙니다 :) 개발에는 좋은 방향성은 존재하지만 정답이 있다고 할 수 없기 때문입니다. 어쨌거나 보안취약점은 해결이 되었으니까요 ㅎㅎ 과거의 깊은 고민과 철학들이 담당자가 달라지며 조금씩 조금씩 사라져가는 것은 아주 조금 슬프긴합니다. ㅋㅋ 사람이 변하고 시간이 흐르면 과거의 고민들도 함께 사라져가는 것 같아요.

여러분들의 생각은 어떤가요? 글 읽어주셔서 감사합니다!

 

 

+ Recent posts