1. 인증 (Authentication) vs 인가 (Authorization)
본 포스팅에서 인증에 관한 내용을 다루기 전에, 먼저 인증과 인가의 차이를 알아보자. 먼저, 인증(Authentication)이란 서비스로부터 일정 권한을 부여받은 사용자임을 인증받는 절차를 말한다. 조금 더 간단하게 말하면, 가입된 계정으로 로그인을 하는 절차를 말한다. 다음으로, 인가(Authorization)란 이미 인증받은, 즉 로그인을 한 사용자가 서비스의 특정 기능을 사용하기 위한 권한을 허가받는 절차를 말한다. 즉, 특정 기능을 사용하려 할 때 사용자가 이미 로그인을 한 상태인지, 그렇다면 그 기능을 사용할 권한이 있는지 검증하는 절차를 말한다.
그중 본 포스팅에서 다룰 내용은 바로 인가(Authorization)이다. 인가의 핵심은 현재 로그인되어 있는 사용자의 상태를 기억하는 것이다. 그래야만 로그인 상태를 유지해줄 수 있고, 특정 기능을 사용하기 위한 권한을 가지고 있는지 검사하는 것도 가능하기 때문이다. 그런데 HTTP 프로토콜은 기본적으로 Stateless 특성을 갖는다(참고). 즉, 서버와 클라이언트의 통신이 종료되면 그 전의 상태 정보는 특별히 저장되지 않는다는 것이다.
따라서 로그인되어 있는 사용자의 상태를 기억하기 위해서는 별도의 구현이 필요하다. 그 구현 방식, 즉 서버가 인가 과정을 처리하는 구현 방식은 인증 방식에 따라 달라진다. 이번 포스팅에서는 세션 기반 인증에서의 인가 방식과 토큰 기반 인증에서의 인가 방식을 알아볼 것이다. (세션/토큰 기반 인증의 기본 개념은 여기를 참고하자.) 그러고 나면 토큰 기반 인증에서 사용하는 대표적인 토큰에 해당하는 JWT의 개념 및 한계에 관해 알아볼 것이다.
여기서 잠깐, 인증을 할 때처럼 단순히 아이디와 비밀번호를 이용하여 인가 과정을 처리할 순 없을까? 즉, 매 요청마다 아이디와 비밀번호를 전송하는 것이다.
결론부터 말하자면, 절대로 해서는 안 되는 방법이다. 먼저, 로그인은 의외로 복잡한 작업이다. DB에 저장된 사용자의 해시 값 등을 꺼내오고, 사용자의 비밀번호를 복잡한 알고리즘으로 계산한 값과 그것이 일치하는지 검사하는 연산이 필요하기 때문이다. 또한, 데이터베이스 쿼리 자체도 어느 정도의 시간과 자원을 잡아먹는다. 그리고 무엇보다, 매번 아이디와 비밀번호를 전송하는 것은 보안상 굉장히 위험하다. 따라서 이 방식은 사용해선 안 된다.
2. 인증 방식에 따른 인가 방식의 구분: 세션 기반 vs 토큰 기반
인증 방식으로 무엇을 사용하느냐에 따라 인가 방식도 달라진다. 여기서는 세션 기반 인증을 사용할 때의 인가 방식과 토큰 기반 인증을 사용할 때의 인가 방식을 알아보고, 각각의 장단점에 대해서도 다뤄볼 것이다.
2-1. 세션 기반 인증
세션 기반 인증에서는 로그인한 사용자의 상태 정보를 서버 단의 저장소에 세션의 형태로 저장한다. 그리고 클라이언트가 세션 ID를 쿠키로 서버에 전송하면, 서버는 해당 세션 ID를 이용하여 적절한 세션을 탐색함으로써 해당 사용자가 이미 로그인을 한 상태인지, 그렇다면 요청된 기능을 사용할 권한이 있는지 검증한다. 이것이 곧 인가 과정이다.
2-1-1. 세션 기반 인증의 장점
세션 기반 인증의 가장 큰 장점은 로그인한 사용자의 상태 정보를 서버 단에 저장하기에 상대적으로 안전하다는 것이다. 보안은 서버가 클라이언트보다 튼튼하기 때문이다. 또한, 로그인한 사용자의 상태 정보를 서버 단에서 유연하게 관리 가능하다. 만약 누군가의 세션 ID가 탈취되었다면, 서버는 해당 세션을 폭파시키기만 하면 된다. 그러면 더 이상 탈취된 세션 ID를 가지고 아무것도 할 수 없다.
2-1-2. 세션 기반 인증의 단점
반면, 세션 기반 인증은 여러 가지 단점들도 갖는다. 먼저, 로그인한 사용자의 상태 정보를 서버 단에 저장하기에 여러 사용자들이 동시에 많이 접속하면 서버 단의 저장소에 오버헤드가 심해질 수 있다. 또한, 세션을 저장하는 저장소에 문제가 생겨서 세션들이 전부 날아가면, 로그인한 사용자들은 전부 다시 로그인을 해야 한다. 이는 세션을 메모리형 저장소에 저장할 때 쉽게 생길 수 있는 문제인데, 그렇다고 해서 세션을 데이터베이스나 하드디스크 등에 저장하면 매번 수행하는 읽기 연산이 매우 부담될 것이다. 마지막으로, 여러 대의 서버들로 운영되는 웹사이트의 경우 세션을 반드시 공용 저장소에 저장해야 한다. 서버별로 존재하는 저장소에 저장하면, 한 서버에서 로그인을 해도 다른 서버에서는 로그인되지 않은 상태로 인식하기 때문이다. 이렇듯, 복잡한 구성과 환경에서 로그인한 사용자의 상태 정보를 기억해야 한다는 것 자체가 세션 기반 인증의 치명적인 단점이 된다.
2-2. 토큰 기반 인증
토큰 기반 인증에서는 로그인한 사용자의 상태 정보를 클라이언트 단의 저장소(쿠키, 로컬 스토리지, 세션 스토리지 중 하나)에 토큰의 형태로 저장한다. 그리고 클라이언트가 토큰을 서버에 전송하면, 서버는 해당 토큰에 담긴 정보를 이용하여 해당 사용자가 이미 로그인을 한 상태인지, 그렇다면 요청된 기능을 사용할 권한이 있는지 검증한다. 이것이 곧 인가 과정이다. JWT는 토큰 기반 인증에서 사용하는 토큰의 한 종류이다.
2-2-1. 토큰 기반 인증의 장점
토큰 기반 인증의 장점은 곧, 위에서 설명한 세션 기반 인증의 단점들이 없다는 것이다. 즉, 로그인한 사용자의 상태 정보를 애초에 서버 단에 저장하지 않기에 서버 단 저장소의 오버헤드를 신경 쓸 필요 없으며, 복잡한 구성과 환경에서 로그인한 사용자의 상태 정보를 기억해야 하는 부담도 없으니 서버 확장에 훨씬 유리하다.
2-2-2. 토큰 기반 인증의 단점
그러나 토큰 기반 인증도 단점이 없는 것은 아니다. 서버를 대신해서 클라이언트가 로그인한 사용자의 상태 정보를 기억하는 것이기 때문에, 토큰 자체에 로그인한 사용자의 상태 정보가 담겨 있다. 따라서 상대적으로 서버보다 보안이 튼튼하지 못한 클라이언트에서 토큰이 탈취당하면 몇몇 개인정보까지 탈취당할 위험이 있다. 더군다나, 세션 기반 인증과 달리 서버는 이미 한 번 발급한 토큰을 무효화시킬 방법이 없기 때문에 탈취된 토큰은 그 유효 기한이 지나기 전까지 마음껏 사용할 수 있다.
세션 ID는 신용카드(또는 체크카드), 토큰은 신분증에 비유할 수 있다.
세션 기반 인증과 토큰 기반 인증을 조금 더 쉽게 이해하기 위한 비유이다. 신용카드(또는 체크카드)의 경우, 실수로 분실했을 때 카드사에 신고를 하면 해당 카드를 즉시 정지시킬 수 있다. 그러면 나쁜 사람이 그 카드를 줍더라도 아무것도 할 수가 없다. 이는 세션 ID가 탈취되었을 때 서버가 즉시 해당 세션을 폭파시키기만 하면 되는 세션 기반 인증과 매우 유사하다.
다음으로, 신분증이 분실된 경우도 생각해보자. 카드와 달리, 신분증은 분실해도 이를 정지시킬 방법이 없다. 따라서 신분증의 유효 기한이 지나기 전까지는 그 신분증을 주운 사람이 자신의 신분을 위조할 수 있게 된다. 또한 카드 자체로 개인정보를 알아낼 수는 없지만, 신분증은 그 자체로 개인정보를 담고 있기 때문에 분실 시 개인정보가 유출될 위험도 크다. 이는 토큰이 탈취되었을 때 서버가 해당 토큰을 무효화시킬 방법이 전혀 없는 토큰 기반 인증과 매우 유사하다. 따라서 토큰에는 민감한 정보를 최대한 담지 않도록 해야 한다.
3. JWT의 개념
JWT(JSON Web Token)은 토큰 기반 인증에서 사용하는 대표적인 토큰이다. JWT는 총 세 부분으로 이뤄져 있다. 헤더(Header), 페이로드(Payload), 서명(Signature)이 바로 그것이다. 각 부분을
Base64
로 인코딩하고 이들을 마침표(.)를 이용하여 차례대로 연결하면 그것이 곧 JWT이다. 다음 그림에서 빨간색 부분이 헤더, 보라색 부분이 페이로드, 하늘색 부분이 서명이다.참고로,
Base64
인코딩에 대한 이해가 없다면 이 포스팅을 참고하여 먼저 숙지하고 오기를 권장한다.그렇다면 본격적으로 각 부분이 의미하는 것이 무엇인지 한 번 상세히 알아보도록 하자.
3-1. 헤더 (Header)
헤더는 총 두 가지 정보를 갖는 JSON 값이다. 하나는 토큰의 타입을 지정하는
type
, 다른 하나는 서명 계산 및 토큰 검증에 사용할 암호화 알고리즘을 지정하는 alg
이다. JWT의 경우에는 type
이 무조건 JWT
이고, alg
는 HS256(SHA256)
혹은 RSA
등으로 지정된다. alg
에 지정되는 알고리즘을 이용하여 서명을 계산하고 토큰을 검증하는 원리에 대해서는 뒤에서 다룬다.type
: 토큰의 타입 (무조건JWT
)
alg
: 서명 계산 및 토큰 검증에 사용할 암호화 알고리즘 (EX.HS256(SHA256)
,RSA
등)
3-2. 페이로드 (Payload)
로그인한 사용자의 상태 정보를 갖는 JSON 값이다. 누가 누구에게 발급한 토큰인지, 유효 기한은 언제까지인지, 서비스가 이 토큰을 통해 사용자에게 공개하고자 하는 정보들은 무엇인지 등이 담긴다. 이처럼 토큰에서 사용하는 각 정보의 조각을 클레임(Claim)이라고 부른다. 그리고 로그인한 사용자의 상태 정보가 토큰 자체에 담겨 있기 때문에, 이러한 유형의 토큰을 Self-Contained 토큰이라고 부른다. 서버는 이러한 유형의 토큰을 받으면 그 토큰 자체로부터 사용자의 상태 정보를 얻을 수 있기 때문에 불필요한 데이터베이스 쿼리 등을 최소화시킬 수 있다.
3-3. 서명 (Signature)
헤더와 페이로드를
Base64
로 인코딩하고, 이 둘을 마침표(.
)로 연결한 것을 서버의 비밀 키와 함께 (헤더의 alg
에 지정된) 암호화 알고리즘에 넣어서 돌리면 서명을 얻을 수 있다. 암호화 알고리즘 특성상, 헤더나 페이로드가 아주 조금만 바뀌어도 서명은 크게 달라진다. 서명은 서버가 토큰의 유효성을 검증하는 데 사용한다. 그 과정은 다음과 같다.- 서버가 클라이언트로부터 토큰을 받는다.
- 해당 토큰의 헤더, 페이로드와 자신의 비밀 키를 이용하여 서명을 계산한다.
- 해당 토큰의 서명이 방금 계산한 값과 동일한지 검사한다.
- 만약 동일하지 않다면 요청을 거부하고, 동일하다면 해당 토큰의 유효 기한을 확인한다.
- 해당 토큰의 유효 기한이 아직 지나지 않았다면, 요청이 허가된다(= 인가).
만약 해커가 페이로드의 일부를 수정한 토큰을 서버에 전송한다고 해보자. 예를 들어, 사용자의 관리자 여부를 나타내는 값을
True
로 바꾼 토큰을 서버에 전송함으로써 관리자 기능을 수행하고 싶은 것이다. 그런데 이와 같이 페이로드를 조금이라도 바꾸는 경우, 반드시 서명도 다시 계산해서 바꿔줘야 한다. 그렇지 않으면 위와 같은 절차에 의해 서버한테 요청이 거부당하기 때문이다. 그러나 서명의 계산을 위한 암호화 알고리즘은 일방향 암호화이기 때문에 서명을 통해 역으로 서버의 비밀 키를 알아내는 것을 거의 불가능하다. 즉, 해커는 서버의 비밀 키를 모른다. 따라서 서명을 다시 계산하는 것은 사실상 불가능하다. 이러한 원리로, 서명은 악의적 목적의 토큰 변조를 방지한다.결론적으로, 서버는 토큰의 유효성 검증을 위해 오직 비밀 키만 안전하게 관리하면 된다. 따라서 현재 로그인되어 있는 모든 사용자의 상태 정보를 서버 단에 저장하고 있어야 하는 세션 기반 인증에 비해, 서버 단 저장소의 오버헤드를 신경 쓸 필요 없이 다수 사용자의 동시 접속을 더욱 효율적으로 처리할 수 있게 된다.
4. JWT의 한계
지금까지 설명한 내용만 보면, JWT 중심의 토큰 기반 인증은 세션 기반 인증에 비해 월등히 좋아 보인다. 그러나 앞서 세션 기반 인증과 토큰 기반 인증의 장단점을 비교하면서 설명했듯이, 토큰 기반 인증도 몇 가지 단점을 가지고 있다. 그리고 이는 곧 JWT의 한계이기도 하다. 크게 두 가지의 문제점이 존재한다.
먼저, JWT는 상대적으로 보안이 취약한 클라이언트 단에 저장이 된다. 따라서 서버 단에 저장되는 경우보다 더욱 탈취당하기 쉽다. 그런데 문제는 JWT 자체가 로그인한 사용자의 상태 정보를 갖고 있다는 점이다. 조금 더 구체적으로는, JWT의 페이로드를
Base64
로 디코딩만 하면 해당 사용자의 정보를 손쉽게 얻어낼 수 있다. 따라서 JWT의 페이로드에는 필요한 최소한의 정보만을 담고, 지나치게 민감한 개인정보 등은 담지 않도록 주의해야 한다.다음으로, 이미 발급된 JWT는 무효화를 하기가 어렵다. 이를 이해하기 위해, 먼저 세션 기반 인증을 다시 떠올려 보자. 만약 세션 ID가 탈취되었다면, 해커가 해당 세션 ID를 악용하기 전에 서버가 해당 세션을 폭파시키기만 하면 된다. 즉, 로그인한 사용자의 상태 정보를 서버가 유연하게 관리할 수 있는 것이다. 하지만 토큰 기반 인증에서는 이것이 불가능하다. 서버는 한 번 발급한 JWT를 무효화할 방법이 없다. 굳이 방법을 떠올려 보자면, 무효화할 JWT의 목록을 블랙 리스트와 같은 형태로 데이터베이스에서 별도로 관리해줘야 한다. 하지만 이는 매번 데이터베이스 쿼리를 필요로 한다는 점에서 비효율적인 듯하다. 결국, 서버는 한 번 발급한 JWT의 경우 그 유효 기한이 지나기 전까지는 유효한 토큰으로 판단한다는 문제가 발생한다. 따라서 한 번 JWT가 탈취당하면 해커는 그 JWT의 유효 기한이 지나기 전까지 마음껏 악의적인 동작을 수행할 수 있게 된다.
이제 이러한 한계를 한 번 극복해보자. 바로 리프레시 토큰(Refresh Token)을 이용해서 말이다.
5. JWT의 한계 극복하기: 리프레시 토큰 (Refresh Token)
앞서 알아보았듯이, JWT도 몇몇 한계점들을 지니고 있다. 지금부터 소개할 리프레시 토큰은 그러한 JWT의 한계점들을 (완전히는 아니더라도) 어느 정도 극복하기 위한 수단으로 기능한다. 그렇다면 리프레시 토큰의 개념부터 알아보자.
일단, 리프레시 토큰을 사용한다는 것은 서버가 인증 완료 시 다음과 같은 두 종류의 토큰을 발급한다는 것을 의미한다.
- 액세스 토큰(Access Token): 우리가 지금까지 다룬, 인가가 필요한 요청에 실어 서버에게 전송하는 토큰을 의미한다. 즉, 현재 로그인된 사용자의 상태를 기억하며 로그인 상태 유지 및 권한 검사를 위해 사용되는 토큰이다.
- 리프레시 토큰(Refresh Token): 이미 발급받은 액세스 토큰을 다시 발급받기 위해 서버에게 전송하는 토큰을 의미한다. 말 그대로 액세스 토큰을 갱신(Refresh)하는 것이다.
액세스 토큰은 기본적으로 유효 기간이 굉장히 짧게 설정된다. 이때 만약 리프레시 토큰이 없다면, 로그인을 한 지 얼마 지나지 않아 유효 기한이 지나 다시 로그인을 해야 하는 상황이 반복될 것이다. 그러나 리프레시 토큰이 있기 때문에, 금방 액세스 토큰의 유효 기한이 지나더라도 리프레시 토큰을 서버에게 전송하면 즉시 액세스 토큰을 재발급받을 수 있다. 따라서 다시 로그인을 할 필요가 없다. 이를 위해, 일반적으로 리프레시 토큰은 액세스 토큰보다 유효 기간이 훨씬 더 길게 설정된다. 만약 리프레시 토큰의 유효 기한마저 지난다면 그때는 정말로 로그인을 다시 해야 할 것이다. 즉, 실제 로그인 상태 유지 기간은 리프레시 토큰의 유효 기간이 결정한다.
리프레시 토큰은 인증 완료 시 발급되어, 서버 단의 데이터베이스에 사용자별로 저장된다(물론 액세스 토큰처럼 클라이언트 단에도 저장이 된다). 만약 액세스 토큰이 만료되어 리프레시 토큰을 서버에게 전송하면, 서버는 해당 사용자의 리프레시 토큰을 데이터베이스에서 꺼내온 후 클라이언트가 전송한 것과 일치하는지 검사한다. (액세스 토큰의 페이로드를 통해 어떤 사용자인지 식별이 가능하다.) 만약 일치한다면 액세스 토큰을 새로 발급해준다.
물론 리프레시 토큰을 이용한 액세스 토큰의 재발급 절차는 구현 방식에 따라 조금씩 다르다. 클라이언트가 미리 액세스 토큰의 페이로드에 담긴 유효 기한을 확인하여 서버에게 액세스 토큰의 재발급을 요청할 수도 있고, 일단은 액세스 토큰을 서버에게 보내서 액세스 토큰의 만료 여부는 서버가 판단하게 할 수도 있다. 따라서 여기서는 그 구현 상세까지는 설명하지 않는다.
지금까지 리프레시 토큰의 개념을 알아보았다. 그렇다면 리프레시 토큰이 JWT의 한계점들을 어떻게 보완해주는지, 즉 리프레시 토큰의 장점은 무엇인지 한 번 알아보자. 크게 세 가지로 나눠 설명하도록 하겠다.
5-1. 리프레시 토큰의 장점 ①: 액세스 토큰의 유효 기간을 짧게 유지
리프레시 토큰을 사용하는 가장 큰 이유는 바로 액세스 토큰의 유효 기간을 짧게 유지시키는 것이다. 그래야 액세스 토큰이 탈취당하더라도 해커가 그 토큰을 오래 사용할 수 없기 때문이다. 그 유효 기간이 길수록 해커는 해당 토큰을 더 자유롭게 쓸 수 있을 것이다.
5-2. 리프레시 토큰의 장점 ②: 로그인한 사용자의 상태 정보를 서버가 유연하게 관리 가능
앞서 말했듯, 리프레시 토큰은 클라이언트 단뿐만 아니라 서버 단의 데이터베이스에도 저장된다. 그리고 세션 기반 인증의 장점에서도 설명했지만, 어떠한 데이터를 서버 단에 저장한다는 것은 서버가 그 데이터를 유연하게 관리할 수 있음을 의미한다. 즉, 특정 사용자의 토큰이 탈취당했다면 서버는 그 사용자의 리프레시 토큰을 데이터베이스에서 삭제함으로써 문제를 예방할 수 있다. 이는 리프레시 토큰이 서버 단에 저장되어 있기 때문에 가능한 것이며, 만약 클라이언트 단에만 저장되어 있다면 이러한 관리 및 통제가 불가능해진다.
물론 리프레시 토큰을 삭제해도 액세스 토큰의 짧은 유효 기간 동안은 그 액세스 토큰을 사용할 수 있다. 그래서 리프레시 토큰도 완전한 보완책은 아니다. 하지만 설명의 편의를 위해, 여기서는 액세스 토큰의 유효 기간이 아주 짧다고 가정하자.
5-3. 리프레시 토큰의 장점 ③: 여러 기기의 로그인(동시 로그인) 관리 가능
마찬가지로 리프레시 토큰을 데이터베이스에 저장하기 때문에 가능한 것이다. 여러 기기의 로그인을 허용하고 싶다면 리프레시 토큰이 데이터베이스에서 기기별로 나뉘어 저장되도록 하거나 동일한 리프레시 토큰을 공유하도록 하면 되고, 허용하기 싫다면 로그인을 할 때마다 새로운 리프레시 토큰을 발급하여 그 전의 리프레시 토큰을 덮어버리면 된다.
6. 클라이언트에서 토큰을 저장하는 위치
토큰 기반 인증에서는 로그인한 사용자의 상태 정보를 클라이언트 단의 저장소(쿠키, 로컬 스토리지, 세션 스토리지 중 하나)에 토큰의 형태로 저장한다. 그렇다면 토큰은 쿠키, 로컬 스토리지, 세션 스토리지 중 어디에 저장하는 게 좋을까? 정답이 있는 것은 아니지만, 세션 스토리지에 저장하는 경우는 많지 않고 보통은 쿠키나 로컬 스토리지에 저장한다. 그리고 이 둘은 각각 장단점이 존재한다.
6-1. 쿠키에 저장하는 경우
클라이언트 단에 저장되는 만큼 토큰은 탈취되기 쉽다. 따라서 보안을 더욱 강화하는 것이 중요한데, 다행히도 쿠키는 보안을 위한 몇몇 옵션들을 설정하는 것이 가능하다. 보안을 위한 쿠키의 옵션들과 관련한 기본적인 개념은 여기를 참고하자.
먼저, XSS 공격에 의해 쿠키가 탈취당할 수도 있으므로
HttpOnly
옵션을 설정하여 JavaScript로는 해당 쿠키에 접근할 수 없도록 해야 한다. 다음으로, CSRF 공격에 의해 의도치 않게 쿠키가 전송되는 것을 막기 위해 SameSite
옵션도 Lax
혹은 Strict
로 설정해줘야 한다. 또한, Secure
옵션도 설정하여 HTTPS 프로토콜을 사용할 때만 쿠키가 전송될 수 있도록 해야 한다.6-2. 로컬 스토리지에 저장하는 경우
쿠키가 아닌 로컬 스토리지에 토큰을 저장할 수도 있다. 일단, 로컬 스토리지와 같은 웹 스토리지의 데이터는 매 요청마다 서버에게 자동으로 전송되는 것이 아니기 때문에 CSRF 공격에 상대적으로 안전하다. 그리고 웹 스토리지의 데이터를 서버에게 전송하려면 일단 웹 스토리지의 데이터에 접근이 가능해야 하는데, 애초에 웹 스토리지는 도메인별로 독립되어 있어 A 도메인에서는 B 도메인의 웹 스토리지 데이터에 접근할 수 없기도 하다.
다만,
HttpOnly
옵션으로 XSS 공격을 방지할 수 있는 쿠키와 달리 웹 스토리지는 그러한 보안 옵션들을 설정할 수 없다. 따라서 이스케이프 처리 등으로 XSS 공격에 대한 대응을 면밀히 해줄 필요가 있다. XSS 취약점이 발생하면 동일한 도메인 내에서 CSRF 공격까지 발생할 수도 있기 때문에 더욱 주의해야 한다. 다행히 대부분의 현대 웹 어플리케이션이나 라이브러리들은 자동 이스케이프 처리를 해주는 경우가 많다. 하지만 어찌 됐든 한 번 설정해주면 그만인 쿠키에 비해서는 신경 써줄 부분이 많다는 단점이 있다.본 글은 아래 링크의 내용을 참고하여 학습한 내용을 나름대로 정리한 글임을 밝힙니다.