이전에 들었던 스프링 강의에서는 Spring Data JPA를 사용하기 위해서 H2 데이터베이스를 사용했는데, 이게 용량이 작을뿐더러 실제 생활에서 많이 사용되는 데이터베이스는 아니라고 하셨다. 

그래서 Spring Data JPA를 Mysql과 연동하여 사용해 보려고 한다. 

 

우선 스프링에서 제공하는 '스프링과 mysql 연동하기' 가이드 코드를 참고했다. 

mysql 프로그램은 이전에 xampp를 통해 apache 서버와 함께 다운받아 뒀었어서 추가로 설치할 것은 없었다. 

 

Driver 클래스와 Driver 연동 에러

그런데 application.properties 파일 코드에서 에러가 났다.

 

해당 코드에서 에러가 발생한 것이었다. (지금은 에러를 해결한 상태라 파란색이지만, 원래는 붉은 글씨로 ClassException 에러가 났었다.) 찾아보니 application.properties에 명시한 driver 클래스가 실제 driver랑 연동이 되지 않아 class를 불러올 수 없던 것이었다.

 

그런데 나는 이미 mysql과 java의 driver인 connector 파일을 maven에서 import해서 gradle build 파일에 명시해 둔 상태였는데도 에러가 났다. 

 

select version(); 이라는 SQL문으로 mysql 버전을 확인해 보니, 내가 쓰는 mysql의 버전은 10.0 이상이었는데 maven에서 import한 코드는 mysql 8.0 이상의 버전에 대해서는 적용할 수 없었다. (java:5.1.6 버전의 connector는 mysql 8.0 이상을 지원하지 않는 것으로 보이는데, mysql 버전이 10.4.28이었다.)

 

그러므로 따로 mysql 홈페이지에 가서 mysql connector 8.0을 새로 다운로드 받았다. (connector 버전은 8.0이지만 mysql의 가장 최신 버전까지 java와 연동할 수 있다.)

 

다운받을 connector를 선택할 때는 드롭다운 메뉴에서 platform independent를 선택해 주고, tar과 zip 중에서는 아무거나 선택하면 된다. 우리가 필요한 것은 해당 파일에 있는 .jar 파일이다. 다운받은 파일에서 jar 파일만 따로 뺀 뒤, jar 파일의 디렉토리를 복사해 두자. 

 

Intellij에서 jar 파일 새로 적용하기

그리고 Intellij를 열어서 File -> Project Structure로 들어간 뒤 왼쪽 탭에서 Module을 클릭하고, 가운데 탭 중에서 Dependencies를 클릭한다. 그리고 작은 + 버튼을 누르고 '1 JARS or directories'를 누른 뒤 jar 파일을 찾아서 등록하면 된다. 

 

그리고 다시 application.properties 파일을 확인하면 클래스를 찾지 못해서 붉게 나타나던 코드가 파란색으로 정상적으로 보이게 된다. 

 

Mysql Dialect 설정 에러

 

그런데 여기서 또 다른 에러가 발생했다. 

JDBC 메타데이터 없이는 Dialect을 결정할 수 없다-는 에러였고, 해결하려면 javax.persistence.jdbc.url / hibernate.connection.url / hibernate.dialect 값들 중 하나를 설정해야 하는 것으로 보였다. 

 

위 세 값 중 javax.persistence.jdbc.url과 hibernate.dialect 값을 설정해서 오류를 해결할 수 있었다. 

javax.persistence.jdbc.url 값은 jdbc:mysql://localhost:포트번호를 적어주면 된다. 

hibernate.dialect 값은 org.hibernate.dialect.MySQLDialect로 입력하면 된다. 다른 DB를 사용한다면 다른 DB 버전으로 입력해주면 되겠다. 

 

위와 같이 두 에러를 해결했더니 잘 실행되는 모습을 볼 수 있다. 

 

Dialect란?

SQL 언어를 사용하는 많은 데이터베이스가 있다. 각 DB가 언어를 사용할 때는 SQL 언어를 그대로 사용하는 게 아니라 SQL에는 없는 부분을 따로 정의하기도 하고, SQL의 특정 부분은 구현하지 않거나, 조금씩 다른 형식의 문법을 사용한다. 이처럼 각 DB마다 사용하는 문법을 SQL Dialect라고 한다. SQL을 표준어, 각 DB에서 사용하는 방식을 방언에 비유한 것 같다. 

 

 

참고한 포스트

https://www.youtube.com/watch?v=_7R46uVZTyc 

https://velog.io/@k_ms1998/JPA-MySQL-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0

https://dev-coco.tistory.com/85 

https://spring.io/guides/gs/accessing-data-mysql/#initial

https://learnsql.com/blog/what-sql-dialect-to-learn/#:~:text=SQL%20Is%20the%20Language%20for%20Talking%20to%20Databases&text=PostgreSQL%2C%20MySQL%2C%20Oracle%2C%20and,call%20these%20variants%20SQL%20dialects.

 

Definition

Signal은 전체 프레임워크 내에서 어떤 이벤트가 발생했을 때, 그 이벤트의 발생을 알려주는 notification의 역할을 한다.

 

모든 signal은 django.dispatch.Signal 클래스의 인스턴스들이며, 사용자는 signal을 받을 수도 있고, 직접 signal을 만들 수도 있고, 많이 사용되는 signal 객체들을 받을 수도 있다. (주로 django.core.signals, django.db.models.signals 등 ~signals. 파일에 명시되어 있다.)

 

signal을 받거나 준다는 것은 무엇일까? signal을 받는다는 것은 어떤 이벤트가 발생했을 때 알림(notification)을 받는다는 것이고, signal을 준다는 것은 어떤 이벤트가 발생했을 때 알림을 보낸다는 것이다. signal을 받을 때는 해당 알림을 받아서 어떤 일을 할지를 receiver function으로 정의한다. 

 

반면 signal을 줄 때는 위에서 언급한 것처럼 django.dispatch.Signal 클래스의 인스턴스를 만든 뒤 Signal의 send() 함수를 이용해서 signal을 보낸다. 

 

Example

예시를 통해 살펴보자. 한 피자집에서 주문한 피자가 만들어졌을 때 소비자한테 알림을 보내려고 한다. 이때 피자집에서는 Signal을 만들고, 손님은 해당 signal을 받게 된다. 

 

우선 피자집의 입장에서 보면, 피자가 만들어졌을 때 signal 인스턴스를 생성한 뒤 send 함수를 이용해서 보내면 된다. 이때 누구에게 보내는지는 중요하지 않다. Signal을 보내는 쪽은 발신자를 특정하지 않고 보내고, 받는 쪽에서 자신이 원하는 signal만 골라서 받게 된다. 

# pizzastore.py
import django.dispatch

pizza_done = django.dispatch.Signal()
pizza_soldout = django.dispatch.Signal()


class PizzaStore:

    def send_pizza(self, toppings, size):
        pizza_done.send(sender=self.__class__, toppings=toppings, size=size)

    def notify_soldout(self, toppings):
        pizza_soldout.send(sender=self.__class__, toppings=toppings)

 

손님의 입장에서는 주문한 피자가 완성되었다는 signal을 받아야 한다.

 

이때, 위에서는 간단한 예시로 PizzaStore 클래스만 정의하였지만 실제로는 여러 가게들이 메뉴가 완성되었을 때 signal을 보낼 것이다. 손님이 원하는 건 자신이 주문한 피자 가게에서 보낸 signal이므로 해당 피자 가게가 보낸 signal만 받도록 설정할 수 있다. 즉 특정 발신자(sender)가 보낸 signal만 받도록 설정할 수 있다. 

 

아래 코드에서는 PizzaStore에서 보낸 pizza_done signal에 대해서만 notify_pizza 함수를 signal과 연결시킨다. 

# customer.py
from pizzastore import PizzaStore, pizza_done
from django.dispatch import receiver

@receiver(pizza_done, sender=PizzaStore)
def notify_pizza(sender, **kwargs):
    print("Pizza is done!")
    # code

 

위에서 언급한 @(decorator)를 사용하는 방법 말고도 signal과 receiver 함수를 연결할 수 있다. 

# customer.py
from pizzastore import PizzaStore, pizza_done
from django.dispatch import receiver

def notify_pizza(sender, **kwargs):
    print("Pizza is done!")
    # code
    
pizza_done.connect(notify_pizza)

 

즉 signal과 receiver 함수를 연결하는 방법은 두 가지가 있다.

1. @(decorator)를 사용한 연결

2. connect 함수를 사용한 연결

 

sender 옵션은 필수 옵션은 아니다. sender 옵션을 지정하지 않을 경우, notify_pizza는 PizzaStore가 아닌 다른 클래스에서 생성한(sender가 다른 클래스로 되어 있는) pizza_done signal이 발생할 때에도 실행될 것이다. 

 

Signal.send(). vs Signal.send_robust()

signal을 보낼 때는 .send() 함수 외에도 .send_robust() 함수를 사용할 수 있다. 두 함수 모두 (해당 signal을 받는 receiver 함수, 해당 함수가 리턴하는 값)의 튜플 리스트를 리턴한다. 

 

다만 .send() 함수는 개별 receiver 함수로 signal을 보낼 때 에러가 발생할 경우 그 에러를 처리하지 않는다. 그래서 일부 receiver 함수에서 오류가 발생할 경우, 그 함수는 signal을 받지 못할 수 있다. 

 

반면 .send_robust() 함수는 signal을 보내는 도중 발생한 모든 에러를 처리한다(catch로 처리하는 것으로 보인다). 그래서 일부 receiver 함수에 signal을 보내다가 에러가 발생한 경우, .send_robust() 함수의 값으로 리턴되는 tuple 중 일부는 (에러가 발생한 함수, 발생한 에러의 종류)로 값이 리턴된다. 어떻게 해서든지 signal을 보낸다는 의미에서 send_robust라고 이름 붙여진 것 같다. 

 

Receiver 함수의 실행 횟수 제한

receiver 함수는 기본적으로 signal을 받을 때마다 실행되는데, 이 방식이 문제가 될 수도 있다. 

예를 들어 어떤 모델 인스턴스가 저장될 때마다 사용자에게 이메일이 가는 것보다, 한 번만 가는 것이 더 바람직하다. 

이때는 receiver 함수에 dispatch_uid라는 새로운 옵션을 선언하고 그 값으로는 해싱이 가능한 문자열이면 어떤 것이든 선언할 수 있다. 

pizza_done.connect(notify_pizza, dispatch_uid="evening_pizza")

 

참고한 포스트

https://docs.djangoproject.com/en/4.2/topics/signals/

 

'server-side > Django' 카테고리의 다른 글

models and databases  (0) 2023.09.16
python - poetry 사용하기  (0) 2023.07.12
Model.select_related() vs Model.prefetch_related()  (0) 2022.07.11
Model.objects.filter() vs Model.objects.get()  (0) 2022.07.11
models: on_delete  (0) 2022.07.05

php를 실행하기 위해서는 XAMPP라는 외부 프로그램이 필요하다. XMAPP는 Apache, Mysql 등의 프로그램을 모아서 설치해주고 apache와 mysql의 실행을 관리해 준다. xmapp에서 설치해주는 apache 서버를 사용해서 php를 로컬에서 실행할 수 있다. 

 

xampp 설치는 해당 링크에서 할 수 있다. installer 파일이 실행되면 설치 창이 뜰 텐데, 추천하는 선택지(recommend)를 선택해서 클릭해서 설치해주면 된다. 

 

사실 php파일의 실행에서 mysql은 필요하지 않다. 다만 대부분의 백엔드에서는 데이터베이스를 사용하는 것이 일반적이므로 필요할 경우 xampp를 사용하면 mysql과 php를 연결한 뒤, apache와 mysql을 같이 실행할 수 있다는 장점이 있다. 

 

php는 xampp가 설치된 디렉토리를 /xampp라고 하면, /xampp/htdocs 폴더 내부를 기본 실행 디렉토리로 한다. (변경하는 방법이 분명 있겠지만 여기서는 그 방법을 모르므로 이 디렉토리로 파일들을 옮겨서 실행했다.)

 

영상에서는 여기에 폴더를 하나 만들고, 그 디렉토리 내부에 여러 개의 php 프로젝트를 두던데 이렇게 진행하면 관리하기에 편리할 것 같았다. 

 

apache와 필요에 따라서 mysql까지 구동하고 나면(start 버튼을 눌러서 간단히 구동할 수 있다), localhost/dashboard url을 통해 기본 페이지를 로딩할 수 있다. 이 경로가 /xampp/htdocs 경로에 해당한다. 앞서 폴더를 하나 만들고 그 안에 여러 프로젝트 디렉토리를 만들었으므로, /localhost/dashboard/folder_name/project_name/... 이런 식으로 url을 입력하면 원하는 php 파일을 브라우저에 띄울 수 있다. 

 

 

참고한 포스트

https://www.youtube.com/watch?v=tcoIVp1eNgM

 

프로그래머스에서 SQL 문제를 풀면서 나왔던 함수들을 정리해 보았다. 

 

☑️SQL 함수/필터링 기능 예시

 

 IS NULL / IS NOT NULL

: WHERE 컬럼명 IS NOT NULL

위 SQL문의 경우 컬럼명이 NULL이 아닌 데이터만 조회한다. 

 

 LIKE

: WHERE 컬럼명 LIKE "글자 형식"

WHERE 절의 조건으로 문자열 타입에 대해 더 구체적인 조건으로 필터링할 때 사용한다. 

예를 들어 특정 단어를 포함하는 경우에도 글자 형식을 잘 지정해서 사용할 수 있다. 아래 SQL문은 ADDRESS의 값이 서울-로 시작하는 레코드만 조회한다. 

WHERE ADDRESS LIKE "서울%"

LIKE의 자세한 사용에 대해서는 문서를 참고하자. 

LIKE는 주로 어떤 단어를 앞이나 뒤에 포함하는 레코드를 필터링할 때 많이 사용된다. 

 

"단어%" : '단어'로 시작하는 레코드

"%단어" : '단어'로 끝나는 레코드

"%단어%" : '단어'를 포함하는 레코드

 

 DATE_FORMAT

: DATE_FORMAT(컬럼명, "바꾸려는 형식")

컬럼이 날짜 타입일 경우 문제에서 요구하는 출력 형식이 따로 있을 때 주로 사용한다. 

예를 들어 '2022-03-05 01:13:00' 인 날짜 형식을 '2022-03-05'로 출력해야 하는 경우가 있다. 

두 번째 매개변수에서는 날짜 타입의 컬럼명을 어떤 형식으로 출력하고 싶은지를 명시해 준다. 자세한 정보는 공식문서에 나와 있지만 많이 사용되는 값들은 정해져 있다. 대소문자가 구별되니 주의하자. 

 

%Y: 연도를 4자리 값으로 출력

%m: 월을 숫자로 출력

%d: 일을 숫자로 출력

 

앞선 예시처럼 출력하려면 다음과 같이 입력하면 된다. 

DATE_FORMAT(DATETIME, "%Y-%m-%d")

 

 

✅ YEAR, MONTH, DAY

: YEAR(컬럼명), MONTH(컬럼명), DAY(컬럼명)

날짜 데이터에서 년, 월, 일 데이터를 추출해 준다. 시간이나 분 정보를 추출해 주는 함수(HOUR, MINUTE 등)도 있지만 위의 함수들이 더 많이 쓰인다. 

# DATETIME = '2022-05-26'
YEAR(DATETIME)	# 2022
MONTH(DATETIME) # 5
DAY(DATETIME) # 26

 

 

 IFNULL

: IFNULL(컬럼명, "출력할 값")

NULL값인 데이터를 다른 값으로 출력해 준다. 

아래 예시의 경우 NAME의 값이 NULL이면 "NO NAME"으로, NULL이 아니면 원래 값으로 출력한다. 

IFNULL(NAME, "NO NAME")

 

MAX, MIN

: MAX(컬럼명), MIN(컬럼명)

해당 컬럼에서 가장 큰 값, 가장 작은 값을 출력해 준다. 

아래 예시의 경우 STUDENT_SCORE 테이블의 SCORE 컬럼 데이터들 중 가장 큰 값을 출력하고, 이때 컬럼 이름은 MAX_SCORE로 출력한다. 

SELECT MAX(SCORE) AS MAX_SCORE FROM STUDENT_SCORE

 

 SUM, AVG, COUNT

: SUM(컬럼명), AVG(컬럼명), COUNT(컬럼명)

해당 컬럼의 합, 평균, 레코드 개수를 구할 수 있다. 

아래 예시의 경우 STUDENT_SCORE 테이블의 SCORE 컬럼 데이터들의 평균값을 반올림한 값을 보여주고, 컬럼 이름은 AVG_SCORE로 출력한다. 

SELECT ROUND(AVG(SCORE)) AS AVG_SCORE FROM STUDENT_SCORE

 

참고한 포스트

https://www.w3schools.com/sql/sql_like.asp

 

'server-side > SQL' 카테고리의 다른 글

SQL 기본 문법 정리  (0) 2023.03.05

☑️TIP

더 많은 SQL 문제들 찾아보기

 

☑️계기

원래 SQL과는 접점이 없는 사람이었는데 이번에 코테를 준비하면서 처음 SQL 문제를 접하게 되었다. 단기간에 공부하는 데는 프로그래머스의 SQL 고득점 Kit가 효과적이라는 말을 들어서 무작정 연습문제를 풀면서 공부했다. 

 

그 결과 어렵고 복잡한 문제는 아직 못 풀지만 초급-초중급 난이도까지의 문제는 간단한 문법으로 풀리는 것 같아서 까먹기 전에 내용을 정리해 보려고 한다. 

 

SQL 문법 정리

1. 기본 조회

특정 조건을 걸지 않고 모든 데이터를 조회한다. 

 

✅ 전체 컬럼의 전체 데이터 조회

: SELECT * FROM 테이블명

데이터베이스의 모든 컬럼에 대해서 모든 데이터를 조회하는 SQL문이다.

*을 사용하면 모든 컬럼 이름을 나열하지 않고 전체를 불러올 수 있다. 

SELECT * FROM ANIMAL_INS

 

 

✅ 특정 컬럼의 전체 데이터 조회

: SELECT 컬럼1, 컬럼2, ... 컬럼N FROM 테이블명

해당 SQL문을 실행하면 USER_INFO 테이블의 컬럼 중에서 USER_ID, USER_NAME, USER_ADDRESS 컬럼의 데이터만 불러온다. 

SELECT USER_ID, USER_NAME, USER_ADDRESS FROM USER_INFO

 

✅ 특정 컬럼의 이름을 바꿔서 조회

: SELECT 컬럼1 AS C1, 컬럼2 AS C2, ... 컬럼N AS CN FROM 테이블명

위의 SQL문과 리턴하는 데이터는 같지만 리턴할 때의 컬럼 명이 다르다. 

SELECT USER_ID AS UID, USER_NAME AS UNAME, USER_ADDRESS AS UADDRESS FROM USER_INFO

보통 컬럼 이름을 그냥 바꾸지는 않는다. 컬럼에 다른 SQL 함수를 적용하면 컬럼 이름이 변경되는데, 다시 원래 컬럼 이름으로 출력하기 위해서도 사용한다. 

 

2. 조건 추가

주어진 조건에 맞는 데이터만 불러온다. 

WHERE 키워드를 사용하고, SELECT와 FROM 바로 뒤에 조건을 붙인다. 

 

✅ 조건 명시하기

: SELECT 컬럼명 FROM 테이블명 WHERE 조건

아래 예시의 경우, 전체 데이터들 중 나이가 9세 이상인 유저 데이터만 불러온다. 

SELECT UID, UNAME, UAGE FROM USER_INFO WHERE UAGE >= 9

 

✅ 조건 여러 개를 동시에 만족하는 데이터 추출하기

: SELECT 컬럼명 FROM 테이블명 WHERE 조건1 AND 조건2

아래 예시의 경우, ID가 10 미만이고 나이가 9세 이상인 유저 데이터만 불러온다. 

SELECT UID, UNAME, UAGE FROM USER_INFO WHERE UID < 10 AND UAGE >= 9

 

✅ 조건 여러 개 중 하나 이상 만족하는 데이터 추출하기

: SELECT 컬럼명 FROM 테이블명 WHERE 조건1 OR 조건2

아래 예시의 경우, ID가 10 미만이거나 나이가 9세 이상인 유저 데이터만 불러온다.

SELECT UID, UNAME, UAGE FROM USER_INFO WHERE UID < 10 OR UAGE >= 9

 

3. 정렬

ORDER BY 키워드를 사용한다. 여러 컬럼을 기준으로 오름차순(ASC) 또는 내림차순(DESC)으로 정렬할 수 있다. 

지정된 순서가 있는 것은 아니지만 보통 맨 마지막에 쓰는 것이 일반적이다. 

: ORDER BY 컬럼A ASC, 컬럼B DESC

위의 커맨드는 컬럼A의 값을 오름차순대로 정렬한 다음, 컬럼A의 값이 같은 데이터가 있는 경우 컬럼B의 값을 기준으로 내림차순으로 정렬해 준다.

 

4. 그룹별로 데이터 묶기

GROUP BY 키워드를 사용한다.

데이터를 추출한 뒤 특정 컬럼의 값이 같은 레코드들을 묶을 수도 있다. 보통 컬럼별로 그룹을 묶어서 제시할 때 사용한다. 

: GROUP BY 컬럼명

GROUP BY를 사용하는 경우 맨 처음에 SELECT 뒤에는 일반 컬럼명보다는 SQL 함수를 적용한 컬럼들이 오는 경우가 많다. 보통 그룹별로 데이터를 제시할 때는 그룹별 데이터의 평균, 합, 최댓값 등을 제시하기 때문이다. 

 

아래 예시에서는 상품코드별로 가장 비싼 상품의 가격을 조회한다. 

SELECT PRODUCT_CODE, MAX(PRICE)
FROM PRODUCTS
GROUP BY PRODUCT_CODE

 

5. 여러 개의 테이블 데이터 묶기

JOIN 키워드를 사용한다. 

테이블 A와 테이블 B가 있을 때, 두 테이블이 같은 컬럼을 공유하는 경우 사용할 수 있다. 

:

SELECT A.컬럼1, B.컬럼2

FROM 테이블A (AS) A

JOIN 테이블B (AS) B ON A.공유컬럼 = B.공유컬럼

위의 SQL문은 '공유컬럼'이라는 같은 컬럼을 공유한 테이블A와 테이블B를 조인해서 A의 컬럼1과 B의 컬럼2에 대한 데이터를 조회한다. 

 

아래 SQL문은 BOOK_ID라는 컬럼을 공유하는 BOOK_INFO 테이블과 BORROW_INFO 테이블을 조인한 뒤, BOOK_NAME과 BORROW_ID 두 컬럼에 대한 데이터를 조회한다. 

SELECT A.BOOK_NAME, B.BORROW_ID
FROM BOOK_INFO A
JOIN BORROW_INFO B ON A.BOOK_ID = B.BOOK_ID

 

사실 JOIN에 대해서는 INNER JOIN, OUTER JOIN 등 여러 가지가 있고 내용이 많지만 초중급 단계의 문제는 그 이상을 다루지는 않았던 것 같다. 기회가 된다면 JOIN 기능에 대해서도 자세히 정리해 보고 싶다. 

 

6. 그룹별 데이터 필터링

HAVING 키워드를 사용한다. 

필터링이라는 점에서 WHERE과 기능이 유사하지만, HAVING은 항상 GROUP BY 이후에 사용한다는 점이 다르다. 

즉 WHERE 문은 데이터를 그룹별로 나타내기 전에, 어떤 조건의 데이터를 GROUP BY에 적용할지를 필터링한다. 

반면 HAVING문은 데이터를 그룹별로 나타낸 이후, 어떤 조건의 그룹화된 데이터를 최종 조회할지를 필터링한다. 

WHERE과 HAVING의 자세한 차이는 여기를 참고하자. 

 

 

참고한 포스트

https://cocoon1787.tistory.com/684

https://www.javatpoint.com/where-vs-having

https://www.w3schools.com/sql/func_mysql_date_format.asp

 

'server-side > SQL' 카테고리의 다른 글

SQL 함수 정리  (0) 2023.03.05

해당 게시물은 유튜브 생활코딩 채널의 WEB2-OAuth 강의를 듣고 작성한 포스트입니다.

 

WEB2 - OAuth 2.0 : 1.수업소개 - YouTube

 

강의 목표

OAuth 개념 이해하기

 

 

1. OAuth의 개념


OAuth를 사용하는 대표적인 예시로는 소셜로그인이 있다. 하지만 소셜로그인만이 전부가 아니다.

OAuth는 다른 서비스(보통은 신뢰할 수 있는 서비스. ex) google, facebook)와 원래 서비스를 연동한다. 
그러려면 사용자가 사용하는 해당 서비스 계정에 접근할 수 있도록 허가를 받아야 한다. 

가장 쉬운 방법은 사용자의 개인정보(아이디, 비밀번호) 등을 전달받아서 이를 SNS 계정에 접근할 때 이용하는 것이다.
그러나 사용자 입장에서는 자신의 개인정보를 처음 보는 서비스에게 맡기는 것은 불안하고, 보안상 문제가 있을 수 있다. 

OAuth는 이런 방법 말고, 토큰(Token)을 사용해서 안전하게 서로 다른 두 서비스가 상호작용할 수 있도록 해 준다. 

OAuth의 장점

1. 사용자의 실제 개인정보를 본래 사이트에서 사용하지 않는다. 즉 보안 면에서의 장점이 있다. 
2. 액세스 토큰으로 이용할 수 있는 SNS 서비스를 제한할 수 있다.

바로 위의 언급한 방법처럼 사용자의 실제 개인정보를 통으로 넘겨주게 되면, 해당 사이트에서 사용자 계정의 모든 권한을 갖는다는 점에서 보안 문제가 있다. 반면 액세스 토큰으로는 할 수 있는 일과 없는 일이 제한되어 있다. 마찬가지로 보안에서의 장점이 있다.

 

OAuth를 사용하는 방법

사용자의 개인정보를 사용해서, 사용하려는 다른 서비스의 사이트에서 로그인한다. 

사용자가 로그인하면 사용자의 실제 개인정보 대신 액세스 토큰을 발급하고, 본래 사이트에서 그 토큰으로 다른 서비스와 상호작용할 수 있다. 

 

2. 역할

 

Oauth에 등장하는 3개의 주체

resource owner : 사용자 
resource server : 기존 사이트에서 제어하고 싶은 자원을 갖고 있는 서버
client : 리소스 서버의 자원을 이용하려는 사이트

OAuth 공식 문서에서는 리소스 서버(resource server)를 resource serverauthentication server로 분리한다. 
resource server : 인증에 필요한 데이터를 갖고 있는 서버
authentication server : 인증 처리 및 작업을 하는 서버


*간단하게 보면 둘을 묶어서 그냥 리소스 서버로 보기도 한다. 


3. 등록


리소스 서버에 클라이언트를 등록하는 절차

클라이언트가 리소스 서버의 리소스를 사용하려면 사전에 미리 등록(register)을 해야 한다.

(Create app 이라는 과정으로도 나온다.)

등록에 필요한 정보

client id : 리소스 서버에서 개별 클라이언트에게 부여하는 id. 노출이 되어도 상관없다. 
client secret : 클라이언트가 리소스 서버에 자신을 인증할 때 사용하는 비밀번호. 노출되어서는 안 된다. 
authorized redirect urls : 리소스 서버는 이 url로 클라이언트에게 authorization code를 보낸다. 
만약 이 url이 아닌 다른 url에서 리다이렉트(redirect)요청이 들어온다면, 리소스 서버는 해당 요청에 대해서 응답을 보내지 않는다. 

실제로 등록하는 방법

사용하려는 리소스 서버 서비스의 developers 사이트에 가서, create app 또는 비슷한 메뉴를 찾아보자. 

 

ex)
Facebook : developers facebook 사이트에서 create app 메뉴 선택
Google : cloud platform 사이트에서 select/create project 메뉴 선택


4. Resource Owner의 승인


리소스 서버에 클라이언트가 사전 등록 작업을 마쳤다고 해 보자. 

클라이언트 서비스에서 리소스 오너의 정보로 리소스 서버의 서비스나 리소스를 이용하려면 추가적인 작업이 필요하다. 

우선 리소스 오너의 승인이 필요하다. 

ex. 리소스 유저가, 클라이언트 사이트가 나의 정보를 가지고 리소스 서버 사이트의 특정 기능을 사용하는 것을 승인

 

그 다음엔 리소스 서버의 승인도 필요할 것이다. 

ex. 리소스 서버 사이트에서 액세스 토큰과 같이 보낸 요청을 승인

과정

우선 등록이 완료된 이후, 클라이언트와 리소스 서버가 oauth에 필요한 어떤 정보를 갖고 있는지를 보자. 

클라이언트가 리소스 서버에 등록했을 때 사용한 client id, client secret, redirect URL의 정보를 둘 다 갖고 있다. 

 

만약 리소스 오너가 클라이언트 사이트를 이용하면서 소셜로그인 등의 '리소스 서버의 리소스를 필요로 하는 서비스'를 사용하려고 한다면, 흔한 소셜로그인 버튼 등이 나타날 것이다. 

리소스 오너가 그 버튼을 누르면(http 요청을 하면), 클라이언트 사이트는 응답과 함께 리다이렉트 URL을 리턴한다. 

리다이렉트 URL 예시

https://resource.server/?client_id=1&scope=B,C&redirect_url=https://client/callback



리다이렉트 URL은 리소스 서버와 클라이언트가 공통으로 가진 3개의 정보(client_id, client_secret, scope)중에서 client_secret을 제외한 2개의 정보를 쿼리 스트링으로 포함하고 있다. 

해당 url을 받은 리소스 오너는 받은 리다이렉트 url으로 리소스 서버에게 GET 요청을 보낸다.

(POST 처럼 따로 데이터를 담아서 보내지는 않는다.)

 

만약 해당 리소스 오너가 이미 리소스 서버에 로그인이 되어 있지 않은 경우(관련 토큰이 헤더에 없는 경우), 리소스 서버는 리소스 유저에게 리소스서버 로그인을 요청한다. 

참고로 로그인을 하지 않은 상태에서는 리소스 서버는 url 파라미터인 client_id, redirect_uri, scope는 아직 보지 않는다. 

여기서 리소스 오너가 로그인을 하면 다음 단계로 넘어간다. 

또는 이미 로그인이 되어 있었다면(관련 토큰을 헤더에 같이 넣어서 보냈다면), 리소스 서버는 이때 client_id와 redirect_uri를 확인한다. 

(만약 리소스 서버가 갖고 있는 client_id, redirect_uri 파라미터가 맞지 않는다면, 리소스 서버는 여기서 응답을 종료한다.)

만약 클라이언트가 보낸 값이 리소스 서버의 값과 일치한다면, 리소스 서버는 리소스 유저에게 클라이언트 사이트에서 scope에 해당하는 권한을 클라이언트에게 부여해도 되는지를 물어본다. 

(선택하는 작은 폼이 뜰 것이다.)

 

클라이언트가 해당 폼에서 allow를 누르면, 리소스 서버에는 리소스 유저의 id해당 리소스 유저가 허용한 scope 변수의 값이 저장된다. 

 

저장하는 이유는, 앞으로 해당 리소스 유저의 액세스 토큰으로 리소스 서버의 서비스나 리소스를 이용할 때, 해당 유저가 어떤 scope의 권한을 허용했는지를 알 수 있게 정보를 저장하는 것이다. 


5. Resource Server의 승인

 

리소스 유저가 scope에 대한 권한을 클라이언트에게 부여하는 걸 허용했다고 해도, 리소스 서버가 바로 액세스 토큰을 부여하지는 않는다. 

절차가 하나 더 있다.

리소스 유저가 scope에 대한 권한을 승인하고, 리소스 서버가 user_id와 scope를 저장한 이후, 리소스 서버는 리소스 유저에게 authorization_code를 리턴한다. 

authorization_code : 리소스 서버가 클라이언트를 인증하는 임시 비밀번호 역할을 한다. 

authorization_code는 아까 리소스 유저가 리소스 서버에게 보냈던 redirect_uri 파라미터 주소의 뒤에 쿼리 스트링을 추가한 형식으로 붙어서 보내진다. 

https://client/callback/?code=3

 

이 정보를 리소스 유저에게 응답으로 보낼 때, 리소스 서버는 이 정보를 헤더의 Location 파라미터에 넣어서 보낸다. 

그러면 리소스 유저의 브라우저는 location 파라미터에 있는 주소로 리다이렉트를 하게 된다. 

그러면 리소스 유저는 클라이언트에게 해당 url로 GET 요청을 보내는 셈이다. 

(redirect_uri의 도메인이 클라이언트의 도메인이기 때문이다)

그러면 클라이언트는 이제 authorization_code의 값도 알게 된다. 

이제 클라이언트는 리소스 유저를 통하지 않고, 리소스 서버에게 직접 요청을 보낼 수 있다. 

쿼리 스트링 파라미터로 url에 값을 넣은 형식이고, GET 방식으로 요청을 보낸다. 

 

url 예시

https://resource.server/token?grant_type=authorization_code&code=3&redirect_url=https://client/callback&client_id=1&client_secret=2

 

해당 url에는 grant_type, code, redirect_url, client_id, client_secret 정보가 포함되어 있다. 

이 중에는 2개의 비밀번호(auth_code, client_secret) 정보도 포함되어 있다.

+

강의에서 다루진 않았지만, 리소스 서버가 클라이언트를 인증하는 방법은 authorization code로 인증하는 방법 말고도 여러 개가 있다고 한다. 그래서 어떤 인증방법을 사용하는지에 대한 정보를 알려주기 위해서 

grant_type=authorization_code

라는 쿼리스트링이 붙는다. 

 


개인적인 Q

더보기

client_secret은 노출되면 안 되는 정보라고 했는데 저렇게 url 쿼리스트링으로 보내도 괜찮은 걸까?

 

리소스 정보는 자신의 DB에서 client_secret, auth_code 정보가 클라이언트가 보낸 정보와 일치하는지를 보고(ex. client_secret=2인 계정의 auth_code는 3이 맞는지), client_id, redirect_url 등의 나머지 정보가 맞는지도 확인한다. 

이제 다음 단계에서 액세스 토큰을 지급한다.


6. 액세스 토큰 발급

 

이제는 리소스 서버가 클라이언트에게 직접 액세스 토큰을 발급한다. 

해당 액세스 토큰은 리소스 서버와 클라이언트의 DB에 각각 저장되며, 각 리소스 유저마다 당연히 액세스 토큰의 값이 다르다. 

처음에 리소스 유저가 한번 '클라이언트에서 자신의 리소스 서버 계정에서 특정 기능들(scope)의 접근 권한을 얻는 것'에 동의하면, 그 이후로는 별도의 동의를 받지 않고 액세스 토큰을 사용할 수 있다. 

순서

1. 리소스 유저가 클라이언트 사이트에서 '소셜로그인' 등의 버튼을 클릭한다(클라이언트에게 요청을 보낸다).

2. 클라이언트는 리소스 유저에게 리다이렉트 응답을 보낸다(리소스 서버 로그인으로 리다이렉트).

3. 정보를 맞게 입력한 경우 기존에는 authorization_code를 보내서 추가 인증을 진행하였지만, 이제는 그러지 않는다. 

셀프 Q&A

더보기

Q. 리소스 유저가 리소스 서버의 로그인 정보를 맞게 입력한 경우 리소스 서버는 리소스 유저에게 어떤 응답을 리턴할까?

A(추측). 클라이언트로 리다이렉트 or (액세스 토큰 유효기간이 지난경우) 새 액세스 토큰 발급할 것 같다. 

 

 

7. API 호출

 

oauth를 이용하는 가장 중요한 목적

액세스 토큰 발급 이후 리소스 서버의 일부 기능을 사용하려면 리소스 서버의 API를 사용해야 한다. 

리소스 서버의 API리소스 서버에서 제공하는 여러 기능을 어떻게 사용할 것인지에 대한 표준 규격이라고 할 수 있다. 

각 리소스 서버는 API들을 어떤 형식으로, 어떤 파라미터 등을 넣어서 사용해야 하는지에 대한 정보를 공식문서로 제공하고 있다. 

ex. "구글 캘린더 API" 를 검색해서 나온 공식문서를 참고해서, 해당 형식으로 요청 보내면 데이터를 받을 수 있다. 

액세스 토큰 보내는 방법(2가지)

1. GET 요청으로 url 쿼리스트링에 access_token={access_token} 값 넣어서 보내기

상대적으로 간편하지만 보안 측면 때문에 덜 사용된다고 한다. 

 

2. GET 요청으로 보내되, 액세스 토큰은 헤더에 Authorization 값으로 넣고, Bearer 토큰 방식으로 보내기

보안 측면에서 더 좋다. 다만 일반적인 url 접근이 불가능하고, curl이나 postman같은 프로그램을 사용해야 한다. 


8. 리프레쉬 토큰 Refresh Token

 

액세스 토큰은 유효기간이 있다. 

리프레쉬 토큰은 처음 액세스 토큰이 발급될 때 액세스 토큰과 같이 발급되는데, 액세스 토큰이 만료되었을 때 새 액세스 토큰을 발급하는 데 사용된다. 

액세스 토큰을 발급받을 때는 보통 액세스 토큰, 리프레쉬 토큰, 만료기간의 값을 같이 리턴하는 것이 일반적이다. 

리프레쉬 토큰이 어떻게 발급되는지도 마찬가지로 사용하려는 각 리소스 서버의 공식문서를 참조하면 알 수 있다. 

ex. 구글의 경우 Google Identity Platform에서 관련 정보를 제공한다. 보통은 특정 url로 필요한 정보(client_id, grant_type, refresh_token 등)를 POST 방식으로 보내면 새 액세스 토큰을 발급하는 방식을 많이 쓴다. 

또한 리프레쉬 토큰의 경우, 새 액세스 토큰의 발급에 사용되면:

(1) 리프레쉬 토큰 값도 새로 발급해 주는 경우도 있고, 

(2) 리프레쉬 토큰 값은 그대로인 경우도 있다. 


9. 수업을 마치며

 

앞으로 공부해 볼 만한 주제

federated identity : 다른 서비스와의 연합을 통해 사용자를 식별하는 인증체계

RESTful API : 많은 형식의 API는 이 방식을 따르고 있다. 

 

'server-side > server' 카테고리의 다른 글

Mac 환경설정  (0) 2024.07.15
Software Release Life Cycle  (0) 2023.07.15
인증(Authentication)  (0) 2022.07.14
linux: cron 사용해서 자동으로 스케줄 실행하기  (0) 2022.07.09
Git: clone, single-branch, checkout  (0) 2022.06.28

💎목차

✔️인증의 정의

✔️세션과 토큰의 차이점

✔️API를 인증하는 여러 방법들


인증(Authentication)

인증 - 사용자의 개인정보를 사용하여 사용자가 누구인지를 판단

인가 - 사용자가 어떤 일을 할 수 있는지,어떤 권한을 갖고 있는지를 판단

사용자의 개인정보(아이디나 비밀번호)를 직접 인증에 사용하거나 인증을 위해 주고받는 방법은 정보 노출의 위험이 있어서 잘 사용하지 않는다. 대신 사용자의 개인정보를 인코딩 하거나, 해쉬 알고리즘을 통해 암호화한 뒤 이 정보를 주고받을 수는 있다. 


암호화에는 여러 가지 방법이 있다. Base64 인코딩을 거친 정보를 암호화하는 방법도 있고, 클라이언트와 서버만 아는 난수를 생성한 뒤 이를 해쉬 알고리즘에 넣어서 암호화하는 방법도 있다. 

 

그럼 암호화한 정보를 어떻게 사용할까? 클라이언트가 매번 서버에 리소스를 요청할 때마다 암호화를 통해 인증을 하는 것은 매우 번거롭고 시간도 오래 걸린다. 보통은 로그인 등으로 한 번 인증 절차를 거치면 일정 기간동안은 별도의 인증 절차 없이 리소스에 접근할 수 있도록 되어 있다. 

 

이처럼 사용자가 이미 인증 절차를 거쳤음을 증명하는 데 사용하는 것이 세션(session), 토큰(token) 등의 개념이다. 

 

세션과 토큰

토큰과 세션은 모두 사용자를 일정 기간동안 유효하게 인증하는 데 사용하는 방식인데, 인증하는 방식에 따라서 차이가 있다. 예전에는 세션을 많이 사용했다면 요즘은 세션의 단점을 보완한 토큰을 더 많이 사용하는 추세이고, 그 중에서도 json 형식으로 되어있는 JWT(json web token)를 많이 사용한다. JWT에 대해서는 뒤 부분에서 구체적으로 다뤄 보자. 

 

🌟JWT와 세션의 차이

JWT가 활성화되기 전에는 세션을 통한 인증이 활발했으나, 세션은 요청할 때 인증을 위해서 DB를 탐색해야 하는 단점이 있었다. 세션 관련 정보는 DB에 저장되었기 때문이다. 
물론 캐시(cache memory)를 이용해서 세션 정보를 브라우저에 임시로 저장할 수 있긴 하지만, 만료되거나 신규 요청이 들어오면 결국 DB를 탐색해야 했다. 
만약 세션 정보를 저장하는 DB가 분산되어 있다면 각각의 DB를 탐색해야 하는데, 이는 규모가 커질 경우 과정이 복잡해질 수도 있었다. (물론 요즘은 세션을 백엔드에서 편리하게 관리해 주는 프레임워크도 있다.)

JWT는 이런 세션의 단점을 보완한다. 

대략적인 인증 과정은 비슷하다.

클라이언트가 인증 요청을 하고, 서버가 인증을 처리하고 세션이나 JWT를 생성하면 이때 생성한 정보를 만료되기 전까지 일어나는 모든 인증에 사용하는 방식이다.

그러나 세션은 인증 정보를 DB에 저장하지만 JWT는 인증 정보를 클라이언트에 전달하고, 클라이언트가 브라우저에 JWT 토큰을 임시 메모리 형태로 저장한다. 

 

그렇다면 클라이언트가 API에 대해서 인증 요청을 할 때, 토큰을 어떻게 생성하고 인증할까? 

 

🌟API에서 토큰을 생성 및 인증하는 방법

순서

1. 사용자의 개인정보를 이용하여 사용자가 맞는지 판단하고(로그인), 사용자가 맞다면 토큰을 발급한다. 

2. 발급한 토큰은 API 접근 토큰 테이블에 등록된다. 

3. 사용자가 인증이 필요한 API를 요청할 경우, 서버는 사용자가 API 토큰이 있는지, 그리고 토큰이 유효한지(위조된 토큰인지, 유효 기간이 지났는지 등)를 확인해서 토큰이 유효하다면 사용자에게 API나 리소스를 제공한다. 

장점

API 토큰을 주고받는 중 해킹이 일어나도 사용자의 개인정보는 탈취되지 않는다. 토큰은 사용자의 정보와는 관계없는 임의의 문자열이기 때문이다. 

API 토큰 인증의 세부 방법(여러가지가 있음)

 

🌟Base 64 인코딩

클라이언트가 처음에 사용자가 맞는지를 개인정보로 인증할 때, 당연히 그 개인정보를 '직접' 보내지는 않는다. 
클라이언트는 자신의 개인정보를 Base64 인코딩을 거친 뒤, 토큰에 넣어서 서버에게 인증 요청을 보낸다. 그러면 서버가 그 정보를 바탕으로 사용자를 인증한다. 

다만 Base64는 별도의 키 값이 없기 때문에 인코딩한 문자열을 그대로 디코딩할 수 있다. 따라서 이 방법을 쓰려면 반드시 HTTPS 프로토콜을 이용해야 한다. 
안 그러면 해커가 중간에서 정보를 탈취할 수 있고, 탈취한 정보를 그대로 디코딩하면 사용자의 정보가 그대로 노출된다. 

🌟Digest Access Authentication

Base64의 단점을 보완한다. 
클라이언트가 서버에게 인증을 요청할 때, 서버는 클라이언트에게 임의의 난수 값을 준다. 
클라이언트는 이 난수 값을 해쉬 함수의 키 등으로 이용하여, 암호화한 결과를 토큰에 실어서 서버로 전송한다. 

이 경우 토큰 안에 일반 문자열(평문)으로 정보가 담겨 있지 않다. 또한 해커가 인증에 사용된 해쉬 알고리즘을 안다고 하더라도, 난수 키를 모르기 때문에 해시된 값에서 반대로 개인정보를 추출해 내기가 더 어렵다. 

 

📋인증 범위(Realm)

여러 API에 대해서 인증 범위(realm)을 다르게 설정할 수 있다. 
realm은 보호되는 영역인데, API 전체를 부분적으로 나눠서 다른 realm에 위치시킬 수 있다. 
realm을 사용하면 서버가 보호하고 있는 API를 여러 영역으로 나눌 수 있고, 각 영역(realm)마다 요구되는 정보(사용자 이름이나 비밀번호)를 다르게 지정할 수 있다. 

예를 들면 project APIA realm, homework APIB realm에 위치시킨다고 해 보자.
만약 클라이언트가 project API에 대해 인증을 요청한다면, 서버는 사용자가 A 영역(realm)에 접근을 시도했다고 보고 그에 맞는 개인정보를 받아 사용자가 맞는지 판단한다. 
반면 클라이언트가 homework API에 대해 인증을 요청한다면, 서버는 사용자가 B 영역(realm)에 접근을 시도했다고 보고 A영역과는 다른 개인정보를 받아 사용자가 맞는지 판단한다. 

만약 project API와 homework API가 모두 같은 A realm에 위치해 있었다면, 클라이언트가 project, homework API에 대해 각각 인증을 요청할 때 서버는 사용자가 모두 A 영역에 접근을 시도했다고 보고, 토큰을 발급하는 데 같은 정보를 사용했을 것이다. 

realm이 다르다는 것은 클라이언트가 각각 여러 API에 인증 요청을 보낼 때, 클라이언트는 각각 다른 정보를 사용하여 다른 유저로 인증될 수 있다는 의미이다. 

🌟화이트 리스트

API를 호출하는 클라이언트의 API가 일정할 때, 클라이언트가 고정 IP를 사용할 때 사용할 수 있다. 

서버는 특정 API URL에 대해서 들어오는 IP 주소를 화이트 리스트로 유지할 수 있다. 

 

화이트 리스트

기본 정책이 모두 차단인 상황에서 예외적으로 접근이 가능한 대상을 지정하는 방식이다. 
화이트 리스트에 등록된 IP가 아니면 모두 접근을 허용하지 않고, 화이트 리스트에 등록된 IP에 대해서만 인증 절차를 거쳐서 접근이 가능하도록 배치한다. 

 

🌟Oauth

제 3자 인증 방식 중 하나이다. 소셜로그인이 대표적인 예시이다. 
사용자 A가 웹 서비스 B의 리소스(API 등)에 접근하기 위해서, 다른 API 서비스 제공자 C(구글, 페이스북 등 소셜로그인이 가능한 서비스들)에게 인증 요청을 보낸다. 
A가 알맞은 정보로 인증하면, C는 A에게 유효기간이 있는 액세스 토큰을 지급한다. 
그러면 A는 C에게서 받은 토큰을 가지고 B의 리소스를 이용할 수 있다. 

🌟JWT(Json Web Token)

웹에서 토큰을 보낼 때 json 형식으로 주고 받는 토큰이다.

JWT는 claim 기반을 사용한다. claim(클레임)이란 유저의 속성을 의미하는데, 말 그대로 토큰 자체에 유저에 대한 정보가 포함되어 있다. 
이와 달리 Oauth는 토큰에 아무 의미가 없는 랜덤 문자열을 넣는다. 

따라서 JWT 토큰을 발급하면 토큰 내에는 사용자 정보가 포함되기 때문에(정확히는 Base64로 인코딩한 정보가 포함) 서버가 사용자의 정보를 다른 곳에서 추가로 찾거나 가져오지 않아도 된다는 장점이 있다.

반면 토큰 내에 모든 정보가 들어 있기 때문에, 토큰을 잘못 발행했어도 중간에 수정할 수 없다.

따라서 JWT 토큰에는 꼭 유효 기간을 지정해야 하고, 중간마다 리프레시 토큰으로 토큰을 재발행 해야 한다.

뿐만 아니라 토큰은 Base64 인코딩만 된 상태이기 때문에 중간에 누군가가 가로챌 경우 사용자의 정보가 노출될 수 있다.

이런 경우엔 토큰 자체를 암호화하는 JWE를 사용하기도 한다. JWT는 JWE 등으로 암호화를 하더라도, 복호화가 가능하다. 

 

☑️JWT 토큰의 구조

크게 세 가지 영역: 헤더(Header), 페이로드(Payload), 시그니처(Signature)로 나뉜다. 

 

토큰 예시: hhhhh.ppppp.sssss

 

1. Header(헤더)

토큰의 메타 정보가 담겨져 있다. 

어떤 방식으로 인코딩 되었는지, 토큰이 어떤 타입(JWT, Oauth 등)인지 등을 json 방식으로 작성한 뒤 해당 방식에 맞춰서 인코딩한다. 

2. Payload(페이로드)

사용자의 정보, 토큰의 만료 시간 등이 담겨져 있다. 

이때 사용자의 정보는 id 등 사용자를 특정할 수 있는 정보이다. (유저의 아주 중요한 정보(주민번호 등)가 담기진 않았지만, 하나의 유저를 특정할 수 있는 정보가 들어 있다.)

3. Signature(시그니처)

변조 문제를 해결하기 위한 영역이다. 

시그니처 영역을 만들기 위해서는 인코딩 된 헤더, 인코딩 된 페이로드, 그리고 secret_key(장고 프로젝트마다 사용되는 값) 값이 필요하다. 

시그니처는 이 세 값을 합친 뒤, 헤더에 지정한 알고리즘으로 해싱한다. 

그러므로 하나라도 값과 다르다면 결과값이 달라진다. 

☑️실제 인증하는 과정

기존에 만들어진 JWT 토큰은 브라우저에 캐시 형태로 저장되어 있다. 클라이언트가 서버에 특정 리소스에 대해서 인증을 요청한다고 하자. 

 

그러면 [클라이언트가 요청과 함께 보낸 토큰의 시그니처 값] == ([토큰 헤더 인코딩한 값] + [토큰 페이로드 인코딩한 값] + [장고 각 프젝마다 있는 secret_key 값])=>해시 알고리즘 적용

 

이 두 값이 같은지를 비교해서, 같다면 사용자 인증을 허가한다. 

 

 

참고한 포스트

https://dongwooklee96.github.io/post/2021/03/28/rest-api-%EB%B3%B4%EC%95%88-%EB%B0%8F-%EC%9D%B8%EC%A6%9D-%EB%B0%A9%EC%8B%9D/
https://hamait.tistory.com/416
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate
https://www.qu3vipon.com/django-jwt

 

'server-side > server' 카테고리의 다른 글

Mac 환경설정  (0) 2024.07.15
Software Release Life Cycle  (0) 2023.07.15
OAuth 2.0 기본원리  (0) 2022.09.26
linux: cron 사용해서 자동으로 스케줄 실행하기  (0) 2022.07.09
Git: clone, single-branch, checkout  (0) 2022.06.28

데이터베이스 성능을 효율적으로 관리하는 방법 중 하나로 '서브쿼리는 조인(join)으로 작성해라'는 말을 들은 적이 있다. 서브쿼리(sub-query)란 기존에 날린 쿼리의 캐시 데이터를 사용하는 또 다른 쿼리인데, 이런 서브 쿼리를 작성할 때는 join을 사용하라는 의미이다. 

 

join을 사용하지 않으면 작성자는 기존에 만든 쿼리의 데이터(캐시 메모리)를 이용해서 쿼리를 만드려고 했지만, 실제로는 데이터베이스에 또 다른 쿼리를 날리게 된다. 즉 한 번 날릴 수 있는 쿼리를 두 번 날리게 되므로 자원을 낭비하는 셈이다. 

 

select_related()prefetch_related() 모두 장고 ORM에서 데이터베이스에 접근할 때 사용하는 메소드이다. 또한 join을 사용해서 데이터를 합하고, 쿼리셋(QuerySet)을 리턴한다는 점에서 비슷하다. 하지만 두 메소드는 엄연히 다르다. 둘의 차이점에 대해서 알아보자. 

 

공통점

1. 장고 ORM에서 데이터베이스에 접근할 때 사용하는 메소드

2. 결과가 합해진 쿼리셋을 리턴

 

✅개별 특징 - select_related()

✔️사용 방법

사용하려는 모델[A]이 다른 모델[B]을 외래키(ForeignKey)로 참조하고 있을 때 사용한다. 

select_related('참조하는 외래키 필드명')

A의 데이터를 불러오는 쿼리를 작성할 때, A가 참조하는 외래키인 B의 데이터도 같이 캐시 데이터로 불러온다. 그러면 나중에 해당 데이터에서 외래키 정보를 사용해야 할 때, 추가로 DB에 쿼리를 날리지 않아도 된다. 

 

예시를 보자. 

# models.py
class Person(models.Model):
	name = models.CharField()
	age = models.IntegerField()
	home = models.ForeignKey(Home, on_delete=models.CASCADE)
    
class Home(models.Model):
	address = models.CharField()
# ORM query without select_related
p = Person.objects.get(id=22)
h = p.home
# ORM query with select_related
p = Person.objects.get(id=22).select_related('home')
h = p.home

select_related()를 사용하지 않은 경우, 해당 객체의 외래키에 대한 캐시 데이터가 없으므로 p 객체의 home 속성을 조회할 때 한번 더 데이터베이스로 쿼리를 날리게 된다. 

 

그러나 select_related()를 사용할 경우, p 객체를 불러올 때 이미 해당 객체의 외래키 데이터도 캐시 데이터로 불러온다. 따라서 데이터베이스에 쿼리를 날리지 않고 이미 불러온 데이터를 사용할 수 있다. 

 

✔️외래키 관계

외래키로 엮인 두 모델은 one-to-one, many-to-one, many-to-many 셋 중 하나의 관계를 갖는다. 

select_related()의 경우, many-to-many 관계인 모델의 데이터는 불러올 수 없다. select_related()는 SQL 쿼리에서 JOIN 문을 사용해서 해당 모델이 참조하는 다른 모델의 컬럼 데이터들을 불러오는데, many-to-many 관계인 모델의 데이터까지 불러오게 된다면 SQL 쿼리로 불러오는 결과 데이터 양이 너무 많아질 수 있기 때문이다. 따라서 one-to-one 관계나 many-to-one(해당 모델이 many 쪽) 관계로 참조하는 모델 데이터만 불러올 수 있다. 

 

✔️이중 참조

A가 참조하는 모델[B]이 참조하는 또 다른 모델[C]의 데이터를 가져오는 것도 가능하다. 

 

예시를 보자. 

# models.py
class Menu(models.Model):
	name = models.CharField()

class Dessert(models.Model):
	name = models.CharField()
	dessertType = models.ForeignKey(Menu, on_delete=models.CASCADE)
    
class Chocolate(models.Model):
	name = models.CharField()
	chocolateType = models.ForeignKey(Dessert, on_delete=models.CASCADE)
# ORM query
c1 = Chocolate.objects.filter(name__contains='white').select_related('chocolateType__dessertType')

c1 객체에는 name에 white를 포함한 Chocolate 객체들의 데이터가 포함되고, 해당 객체들의 chocolateType 필드가 참조하는 Dessert 객체들의 데이터와, 해당 객체들의 dessertType 필드가 참조하는 Menu 객체들의 데이터까지 포함되게 된다. 

 

✅개별 특징 - prefetch_related()

select_related()와 달리 many-to-many, many-to-one의 관계인 모델의 데이터도 가져올 수 있다. 

prefetch_related('참조하는 외래키 필드명')

 

다음 예시를 보자. Student과 Course는 many-to-many 관계이다. 

# models.py
class Course(models.Model):
	id = models.IntegerField()
	name = models.CharField()
    
class Student(models.Model):
	name = models.CharField()
	course = models.ManyToManyField(Course)
# ORM query
student = Student.objects.prefetch_related('course')

해당 쿼리는 Student 전체의 데이터와 함께, 개별 student 객체가 참조하는 Course 객체에 대한 데이터도 캐시 데이터로 같이 불러온다. 

 

prefetch_related()를 사용하지 않는다면 총 db에 등록된 students 객체 수 만큼의 쿼리가 실행되어야 할 것이다. 그러나 prefetch_related()를 사용하면 총 두 번의 쿼리로 같은 작업을 할 수 있다. 

 

✔️외래키 관계

prefetch_related()는 select_related()와 마찬가지로 이중 참조가 가능하다. select_related()에서 작성한 방법과 같은 방식으로 쿼리를 작성하면 된다. 

 

✔️사용할 수 없는 경우

prefetch_related()를 사용해서 기존 모델이 참조하는 다른 모델의 데이터를 가져왔지만, 가져온 데이터를 사용할 수 없는 경우도 있으니 주의하자. 

 

1. 가져온 데이터셋에 추가 메소드를 적용하였을 경우

student = Student.objects.prefetch_related('course')
student = students.filter(name__contains='Kim')		# 기존 prefetched 데이터 변경

students 변수에는 prefetch_related()를 사용해서 외래키 모델에 대한 데이터까지 저장되어 있었다. 그러나 추가로 filter() 메소드를 사용하면서 데이터가 변경되었다. 기존의 데이터에 추가 메소드를 적용한 경우, 장고에서는 추가로 메소드를 적용한 students.filter() 쿼리셋을 아예 다른 쿼리셋으로 인식한다. 

따라서 새 students 쿼리셋에는 prefetch로 불러왔다고 생각했던 데이터가 없는 상태이다. 

 

2. 기존 DB의 데이터가 변경되었을 경우

student = Student.objects.prefetch_related('course')
Student.objects.create(
	# code
)

기존 student 객체에는 prefetch_related()로 불러온 캐시 데이터가 있었으나, 이를 사용하기 전에 create, delete, update 등으로 기존 데이터가 변경되었다. 이렇게 DB 데이터가 변경된 경우, 기존에 저장되었던 캐시 데이터는 삭제되어서 이용할 수 없다. 

 

⚠️차이점 1 - 사용 가능한 외래키 참조 관계

외래키로 엮인 두 모델은 one-to-one, many-to-one, many-to-many 셋 중 하나의 관계를 갖는다. 

select_related()의 경우, one-to-one, many-to-one(해당 모델이 many 쪽) 관계인 모델의 데이터만 불러올 수 있다. (many-to-many 관계는 불러올 수 없다!)

반면 prefetch_related()의 경우, 참조 관계에 상관 없이 참조하는 모든 모델의 데이터를 불러올 수 있다. 

 

⚠️차이점 2 - JOIN 방식

select_related()는 DB에 엑세스할 때 변환되는 SQL 쿼리에서 JOIN문을 생성하고, 참조하는 모델의 다른 필드들을 SELECT문에 추가하는 방식으로 참조하는 다른 모델의 데이터를 가져온다. 그렇기 때문에 한 번에 너무 많은 데이터를 가져올 수 없어서 one-to-one 관계에만 사용하도록 제한된다. 

 

반면 prefetch_related()는 개별적으로 SQL 쿼리를 날린다. 그리고 DB에서 가져온 쿼리셋을 파이썬에서 합한다. 개별 쿼리에서 JOIN이 발생하지 않기 때문에, 한 번에 적은 데이터를 가져오지 않아도 된다. 따라서 many-to-many, many-to-one 관계에도 적용할 수 있다.

 

두 메소드 모두 JOIN 과정이 발생한다. select_related()는 SQL 쿼리에서 JOIN을 통해 데이터를 가져오고, prefetch_related()는 개별 쿼리로 데이터를 가져온 뒤 파이썬에서 별도의 JOIN 과정을 통해 데이터를 합한다. 즉 JOIN 과정이 언제 일어나는지의 차이가 있다. 

 

 

참고한 포스트

QuerySet API reference | Django documentation | Django (djangoproject.com)

Django에서 DB 액세스 최적화하기 – Myungseo Kang

MySQL 쓰면서 하지 말아야 할 것 17가지 – Lael's World

 

'server-side > Django' 카테고리의 다른 글

python - poetry 사용하기  (0) 2023.07.12
signals  (0) 2023.06.21
Model.objects.filter() vs Model.objects.get()  (0) 2022.07.11
models: on_delete  (0) 2022.07.05
admin: Inline, InlineModelAdmin  (0) 2022.07.02

+ Recent posts