티스토리 뷰

728x90
반응형

서론

Amazon API Gateway는 AWS가 제공하는 Managed Service이다. API의 흐름을 제어하고 인증, 로깅 등의 서비스를 제공한다.

보다 자세한 API Gateway 설명은 다음을 참고한다.

 

[MSA] External LoadBalancer API Gateway

 

본 포스팅에서는 Amazon API Gateway를 구축하고 활용하는 방법에 대해 알아보도록 하자.


API Gateway 구성과정

1) 샘플 애플리케이션 Deploy

2) Amazon API Gateway API 등록

3) Amazon API Gateway API 배포 및 테스트

4) Amazon API Gateway 인증 테스트

5) API Request / Response 조작하기

6) API URL 관리


샘플 애플리케이션 Deploy

1) Deployment, Service, Ingress yaml 확인

Deployment & Service (bookService, authorService)

[book-deployment.yaml]
apiVersion: apps/v1
kind: Deployment
metadata:
  name: book-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: book
  template:
    metadata:
      labels:
        app: book
    spec:
      containers:
      - name: nginx
        image: nginx:1.17.3
        ports:
        - containerPort: 80
        volumeMounts:
        - name: workdir
          mountPath: /usr/share/nginx/html
  # These containers are run during pod initialization
      initContainers:
      - name: install
        image: bitnami/git
        command: ["git"]
        args: ["clone", "https://github.com/aws-samples/amazon-apigateway-ingress-controller-blog.git", "/var/lib/data/"]
        volumeMounts:
        - name: workdir
          mountPath: "/var/lib/data"
      dnsPolicy: Default
      volumes:
      - name: workdir
        emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  name: bookservice
  annotations:
    alb.ingress.kubernetes.io/healthcheck-path: /api/book/list
spec:
  type: ClusterIP
  selector:
    app: book
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80

[author-deployment.yaml]
apiVersion: apps/v1
kind: Deployment
metadata:
  name: author-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: author
  template:
    metadata:
      labels:
        app: author
    spec:
      containers:
      - name: nginx
        image: nginx:1.17.3
        ports:
        - containerPort: 80
        volumeMounts:
        - name: workdir
          mountPath: /usr/share/nginx/html
  # These containers are run during pod initialization
      initContainers:
      - name: install
        image: bitnami/git
        command: ["git"]
        args: ["clone", "https://github.com/aws-samples/amazon-apigateway-ingress-controller-blog.git", "/var/lib/data/"]
        volumeMounts:
        - name: workdir
          mountPath: "/var/lib/data"
      dnsPolicy: Default
      volumes:
      - name: workdir
        emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  name: authorservice
  annotations:
    alb.ingress.kubernetes.io/healthcheck-path: /api/author/list
spec:
  type: ClusterIP
  selector:
    app: author
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80

Ingress

[alb-ingress.yaml]
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: "apigateway-ingress"
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/scheme: internet-facing
spec:
  rules:
   - http:
      paths:
        - path: /api/book/*
          backend:
            serviceName: "bookservice"
            servicePort: 80
        - path: /api/author/*
          backend:
            serviceName: "authorservice"
            servicePort: 80

애플리케이션 배포

[root@ip-192-168-114-198 apigateway_test]# kubectl apply -f author-deployment.yaml 
deployment.apps/author-deployment created
service/authorservice created
[root@ip-192-168-114-198 apigateway_test]# kubectl apply -f book-deployment.yaml 
deployment.apps/book-deployment created
service/bookservice created
[root@ip-192-168-114-198 apigateway_test]# kubectl apply -f alb-ingress.yaml 
ingress.extensions/apigateway-ingress created
[root@ip-192-168-114-198 apigateway_test]#

애플리케이션 확인

[root@ip-192-168-114-198 apigateway_test]# kubectl get all
NAME                                    READY   STATUS    RESTARTS   AGE
pod/author-deployment-f6db476d4-kg5xm   1/1     Running   0          54s
pod/book-deployment-6575446c9d-r5cgz    1/1     Running   0          30s
pod/my-nginx-75897978cd-lsjd5           1/1     Running   0          137m
pod/my-nginx-75897978cd-xmbbp           1/1     Running   0          137m

NAME                    TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
service/authorservice   ClusterIP   10.100.82.229    <none>        80/TCP    54s
service/bookservice     ClusterIP   10.100.167.193   <none>        80/TCP    30s
service/kubernetes      ClusterIP   10.100.0.1       <none>        443/TCP   5h11m
service/my-nginx        ClusterIP   10.100.201.178   <none>        80/TCP    136m

NAME                                READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/author-deployment   1/1     1            1           54s
deployment.apps/book-deployment     1/1     1            1           30s
deployment.apps/my-nginx            2/2     2            2           137m

NAME                                          DESIRED   CURRENT   READY   AGE
replicaset.apps/author-deployment-f6db476d4   1         1         1       54s
replicaset.apps/book-deployment-6575446c9d    1         1         1       30s
replicaset.apps/my-nginx-75897978cd           2         2         2       137m
[root@ip-192-168-114-198 apigateway_test]# kubectl get ingress
NAME                 HOSTS   ADDRESS                                                                      PORTS   AGE
apigateway-ingress   *       7c4d00a7-default-apigatewa-7382-427740522.ap-northeast-2.elb.amazonaws.com   80      8s
ingress-app          *       7c4d00a7-default-ingressap-cae2-552022937.ap-northeast-2.elb.amazonaws.com   80      114m
[root@ip-192-168-114-198 apigateway_test]#

위와 같이 구성/배포하며, apigateway-ingress가 구성되어 연동된 것을 확인할 수 있다.

해당 ingress는 alb ingress controller이며 다음에서 확인할 수 있다.

대상그룹은 서비스 당 하나가 등록된다.

bookService에는 /api/book/list가 authorService에는 /api/author/list가 RestAPI로 서비스 된다.

Amazon API Gateway를 구성하기전 해당 Sample을 호출해 보자.

API는 여러 형태로 테스트할 수 있지만, PostMan을 사용하여 다음과 같이 확인해 보도록 한다.

위와 같이 api가 서비스 되고 있음을 확인했으니 이제 Amazon API Gateway를 등록해 보도록 하자.


Amazon API Gateway API 등록

API Gateway에서 제공하는 API 유형은 4가지가 있다. (HTTP API, WebSocket API, REST API, PRIVATE REST API)

이중 REST API 구축을 선택한다.

다음으로 새 API를 선택하고, API 이름, 설명, 엔드포인트 유형(지역)을 기입하고 API 생성을 선택한다.

API는 다음과 같은 순서로 등록한다.

1) 리소스 생성

리소스는 실제 api를 호출하는 api url을 정의한다.

작업 > 리소스 생성 > 리소스 이름, 경로 > 리소스 생성 순으로 생성하며, 이를 반복적으로 구성하여 다음과 같이 앞서 테스트한 API URL을 구성한다.

이때, {authorid}, {bookid}와 같은 괄호를 사용하여 리소스(경로 파라미터)를 추가할 수 있다. 예를 들어, 리소스 경로 {username}은(는) 'username'이라는 경로 파라미터를 나타낸다. /api/book/{proxy+}을(를) 프록시 리소스로 구성하면 그 하위 리소스에 대한 모든 요청이 포착된다. 이 경로는 예를 들어 /api/book/foo에 대한 GET 요청에 대해 작동한다.

2) 메서드 생성

메서드를 지정한다. 메서드는 호출되는 마지막 url을 선택하고 작업 > 메서드 생성을 선택한다.

- 통합 유형 : HTTP

- 엔드포인트 URL : http://7c4d00a7-default-apigatewa-7382-427740522.ap-northeast-2.elb.amazonaws.com/api/author/ 

마지막으로 저장한다.

이와 동일하게 bookid에 GET 메서드를 생성하고 등록한다.

메서드 등록이 완료되면 위와 같은 메서드 실행 환경으로 접근할 수 있다.

테스트 및 각종 HTTP Request를 관리할 수 있으며, 여기서는 테스트 버튼을 클릭하여 테스트를 진행해보자.

authorid는 제공하는 api에 대한 경로를 입력할 수 있으며, 현재 authorService는 list라는 api를 제공하고 있다. list를 입력하고 테스트 버튼을 클릭하면 오른쪽에 이에 대한 응답 결과를 사전에 테스트할 수 있다.

요청 URL, 상태, 지연 시간 그리고 HTTP Response가 나타난다.


Amazon API Gateway API 배포 및 테스트

테스트까지 완료되면 다음과 같이 API를 API Gateway에 배포할 수 있다.

작업 > API 배포를 선택하여 API를 스테이지에 등록한다.

등록이 완료되면 위와 같이 호출가능한 URL을 확인할 수 있으며 이와 API URI의 조합으로 전체 호출 API의 URL이 결정된다. 

(https://kotf3vnzm9.execute-api.ap-northeast-2.amazonaws.com/PROD/api/author/list)


Amazon API Gateway 인증 및 테스트

Amazon API Gateway는 여러가지 인증 방법을 제공하지만, 자체적으로 api key를 생성하여 api 호출 시 인증을 처리하는 로직을 수행할 수 있다.

API Key를 통해 Limit late, Quota 등을 관리할 수 있다.

사용량 계획 > 생성

위와 같이 사용량 계획을 위해 초당 요청 수, 요청 건, 할당량 활성화 등을 구성한 뒤 다음을 선택한다.

다음으로 API 스테이지를 추가한다. 생성한 API - 스테이지를 선택하고 등록한 후 다음을 선택한다.

마지막으로 API 키 생성 후 사용량 계획에 추가를 선택하여 API-KEY를 생성한 후 완료 버튼을 선택한다.

다음으로 생성한 api key를 요청 시 헤더에 포함시키기 위한 구성을 진행한다.

먼저 api key는 다음에서 확인할 수 있다.

API 키 > 생성한 API KEY 선택 > API 키 옆 표시 클릭

다음으로 앞서 생성한 API 리소스의 메서드를 선택하고 메서드 요청을 클릭한다.

이후 API 키가 필요함을 선택하여 false를 true로 변경한다. book도 동일하게 변경한다.

변경이 완료되면 앞서 API 배포 절차에 맞게 API를 배포하고 다시 호출해 보도록 한다.

앞서 정상적으로 호출되던 API가 다음과 같이 "message": "Forbidden"이 나타나는 것을 볼 수 있다.

앞서 API KEY에서 확인한 키값을 Header에 등록하고 다시 호출해 보자.

- header key : x-api-key

- header value : API KEY

구성이 완료되면, 위와 같이 처리되는 것을 확인할 수 있다.

실제 API KEY는 API를 호출하는 Client를 인증하는 역할을 담당하는 key로 사용한다.


API Request / Response 조작하기

1) Http Request / Response Header 조작하기

앞서 생성한 author와 book api의 GET Method를 자세히 살펴보자.

Amazon API Gateway는 메서드 요청에서 통합 요청으로 또는 통합 응답에서 메서드 응답으로 페이로드를 매핑한다.

이때 API Gateway는 매핑 템플릿을 사용하여 수신되는 요청을 전환한 후에 통합 백엔드로 전송한다. API Gateway를 통해 가능한 컨텐츠 유형별로 매핑 템플릿을 하나씩 정의할 수 있다.

요청 페이로드의 경우 API Gateway에서는 요청의 Content-Type 헤더 값을 키로 사용하여 요청 페이로드에 대한 매핑 템플릿을 선택한다. Content-Type 헤더가 요청에 없는 경우 API Gateway에서는 기본값이 application/json인 것으로 가정한다.

응답 페이로드의 경우 API Gateway에서는 수신 요청의 Accept 헤더 값을 키로 사용하여 매핑 템플릿을 선택한다. 마찬가지로 Accept 헤더가 응답에 지정되어 있지 않은 경우 API Gateway에서는 기본값이 application/json인 것으로 가정한다.

요청 페이로드의 매핑 테이블은 API > my_api > 리소스 > 메소드 > 통합 요청에서 구성할 수 있다.

응답 페이로드의 매핑 테이블은 API > my_api > 리소스 > 메소드 > 통합 응답에서 구성할 수 있다.

예를 들어 다음과 같은 매핑 테이블이 존재한다고 가정해 보자.

- 요청 페이로드 : application/json

- 응답 페이로드 : html/xml

클라이언트가 요청 헤어에 다음과 같이 설정한 경우

- Content-Type : application/json

- Accept : html/xml

요청 페이로드와 응답 페이로드는 모두 해당 매핑 템플릿에 따라 처리된다. Accept:html/xml 헤더가 없는 경우 html/xml 매핑 템플릿을 사용하여 응답 페이로드를 매핑한다. 매핑되지 않은 응답 페이로드를 대신 반환하려면 application/json에 대해 빈 템플릿을 설정해야 한다.
MIME 유형은 매핑 템플릿을 선택할 때 Accept 및 Content-Type 헤더에서만 사용된다.

2) HTTP GET Method Query String 백엔드로 전송하기

Query String은 HTTP GET Method에서 사용하는 방식으로 메서드 요청에서 추가할 수 있다.

API > my_api > 리소스 > 메소드 > 메서드 요청 > URL 쿼리 문자열 파라미터

위와 같이 Author의 name과 age를 Query String으로 전달받아야 통합 엔드포인트로 전달하기 위해 URL 쿼리 문자열 파라미터를 구성해야 한다.

해당 필드가 구성되어 있지 않을 경우 요청에 쿼리스트링은 엔드포인트로 전달되지 않는다.


API URL 관리

Amazon API Gateway에서 Deploy 된 API는 다음과 같은 URL 형태를 갖는다.

https://{restapi-id}.execute-api.{region}.amazonaws.com/{stage_name}

- restapi-id : api deploy 시 무작위로 생성되며, 지역 내 고유한 도메인을 부여하기 위함이다.

- region : 지역 코드가 들어간다.

- stage_name : api deploy를 위한 stage 명이 들어간다.

즉, seoul region에서 testapi라는 stage로 api를 배포할 경우

https://asdqwefgrt.execute-api.ap-northeast-2.amazonaws.com/testapi 라는 형태의 api gateway를 호출하는 도메인이 생성된다.

이는 사실 사용자에게 굉장히 불친절한 도메인으로 그대로 사용할 경우 Client에게 혼선을 줄 수 있다.

이와 같은 문제를 해결하기 위해 AWS는 사용자 지정 도메인을 사용하여 도메인을 매핑할 수 있도록 도와 준다.

사용자 도메인과 조합하여 매핑할 경우 도메인은 다음과 같이 변경될 수 있다.

https://{api.nrson.com}/{basePath}

- api.nrson.com : 도메인 이름은 임의로 지정이 가능하지만, AWS Route53 또는 그밖의 DNS 서비스를 제공하는 인터넷 호스팅 업체를 통해 도메인이 등록되어야 한다.

- basePath : stage name을 api의 default path로 사용한다.

예를 들어 위에 예시로 들은 api는 각각 /api/book/{bookid}, /api/author/{authorid}이며, 이들의 default path는 /api이다. api를 stage name으로 지정하고 실제 api 리소스를 /book/{bookid}, /author/{authorid}로 등록하면, 호출되는 url은 의도한대로 https://api.nrson.com/api/book/list가 된다.

이를 활용하여 API의 구조를 설계하고 API Gateway에 배포되어야 한다.

1) Route 53 등록

먼저 Route 53에 다음과 같이 도메인을 등록한다.

등록된 도메인은 nrson.com으로 멀티 도메인으로 발급되었으며, 원하는 도메인을 지정하여 등록할 수 있다.

2) AWS Certificate Manager(ACM) 공인 인증서 발급

AWS에서는 공인 인증서를 무료로 발급받을 수 있는 ACM 서비스를 제공한다.

ACM 서비스는 사용자 지정 도메인이 https 서비스만 지원하여 인증서를 발급 받아 적용해야 사용할 수 있다. 기존 사용하던 인증서가 있을 경우 ACM으로 발급받지 않아도 적용할 수 있다.

먼저 ACM > 인증서 요청을 통해 인증서 생성을 진행한다. 공인인증서 발급은 무료이다.

a. 도메인 이름 추가

인증서 요청은 총 5단계로 구분되어 진행된다. 첫번째 단계인 도메인 이름 추가에서는 인증서를 적용할 도메인 이름을 정의한다. 현재 Route 53에 등록한 도메인이 nrson.com이므로 api.nrson.com으로 도메인을 등록하도록 하자. (물론 각자 사용하는 도메인에 맞게 등록해야 한다. 또한 멀티 도메인으로 인증서를 발급 받을 수도 있다. *.nrson.com)

두번째 단계인 검증 방법 선택에서는 DNS 검증을 선택한다.

세번째 단계에서는 원하는 태그명을 넣어주고, 검토 및 요청, 검증 단계를 순서대로 진행한다.

이때 검증 단계에서 위와 같이 Route 53에서 레코드 생성이라는 버튼이 생성되는지 여부를 확인해야 한다. 위 버튼이 활성화 또는 나타나지 않을 경우 해당 도메인을 등록할 수 있는 Route 53에 등록된 도메인이 없다는 의미이다.

즉 Route53에 nrson.com을 발급받았고, ACM에서는 api.nrson.com 인증서를 신청했기에 레코드를 생성할 수 있지만, api.narason.com을 신청했다면, Route 53에서 레코드 생성 버튼이 활성화되지 않는다는 것이다.

위와 같이 정상적으로 요청이 완료되면 수분에서 최대 30분까지 등록 시간이 발생할 수 있다.

정상 발급이 완료되면 검증 보류에서 검증 상태가 발급 완료로 변경된 것을 확인할 수 있다.

3) API Gateway 사용자 지정 도메인 이름

인증서 발급이 완료되었으며 API Gateway로 돌아와 사용자 지정 도메인 이름을 등록해 보도록 하자. 현재 API Gateway에는 스테이지 : api, RestAPI : book, author 2개가 등록되어 있다.

먼저 왼쪽 상단의 사용자 지정 도메인 이름을 선택한 후 생성을 선택한다.

위와 같이 API Gateway에서 공통으로 사용할 도메인 이름을 지정한다. 이때 해당 도메인 이름은 앞서 발급 받은 ACM 인증서 도메인에 포함되는 도메인이어야 한다.

TLS는 1.2를 권장하며, 엔드포인트 구성은 지역과 엣지 최적화 중 사용하고자 하는 엔드포인트 구성을 지정한다. 현재 가이드에서는 지역 엔드포인트를 사용한다. 마지막으로 앞서 발급 받은 ACM 인증서를 등록하고 하단의 도메인 이름 생성 버튼을 클릭한다.

정상적으로 생성이 완료되면 위와 같이 도메인 정보가 나타나는 것을 확인할 수 있다.

다음으로 해당 도메인을 매핑할 API 매핑 정보를 등록한다. book-api의 api 스테이지와 연결하며 해당 api 스테이지의 context는 api이다는 의미로 매핑 정보를 추가한 후 저장을 선택한다.

여기까지 등록이 완료되면 API Gateway에서 작업할 내용은 마무리가 된 것이다.

사용자 지정 도메인까지 적용된 전체 도메인 호출 경로는 다음과 같다. 이때는 스테이지 명이 무시가 된다.

https://api.nrson.com/[API_매핑_경로]/[API_리소스]  

즉, https://api.nrson.com/api/book/list의 의 형태가 된다. 대체로 API 매핑의 경로를 스테이지 이름과 동일하게 구성하면, 스테이지에서 사용하던 방식과 동일한 URL 형태를 도출해 낼 수 있다.

4) Route 53에 API Gateway 도메인 이름 등록마지막으로 Route 53으로 넘어가 API Gateway 도메인 이름을 등록한다. API Gateway 도메인 이름은 사용자 지정 도메인 이름의 구성 정보에서 확인할 수 있다.

앞서 인증서를 발급받으며 Route 53에서 레코드 생성 버튼을 클릭하여 자동으로 생성된 CNAME 유형의 레코드를 확인할 수 있다. 해당 레코드는 1회성 인증용으로만 사용된 후 재사용되지는 않는다.

레코드 생성을 클릭하여 다음과 같이 API Gateway 도메인 이름을 등록한다.

(레코드 생성 > 라우팅 정책 선택 (단순 라우팅) > 레코드 구성 (단순 레코드 정의 클릭))

단순 레코드 정의

- 레코드 이름 : 즉 도메인 이름이다.

- 값/트래픽 라우팅 대상 : API Gateway API에 대한 별칭, Region, API Gateway 도메인 이름

위와 같이 등록 후 단순 레코드 정의를 클릭한 후 레코드를 생성한다.

테스트를 진행해 보면 다음과 같다. 먼저 API Gateway에 등록된 도메인을 호출할 경우 다음과 같이 나타난다.

도메인이 가독성이 다소 떨어지게 http://{restapi-id}.execute-api.{region}.amazonaws.com/{stage_name} 형태로 호출되는 것을 확인할 수 있다.

반대로 사용자 지정 도메인 이름을 통해 등록된 도메인을 호출해 보자.

위와 같이 https://api.nrson.com/{stage_name} 형태로 가독성 높은 호출 도메인을 생성할 수 있다.

이를 효과적으로 활용하기 위해서는 API를 구성할때 구조화하고 호출에 용이하도록 API를 체계화 하는 것이 중요할 것이다.


API Gateway 아키텍처

마지막으로 api gateway 구성 아키텍처에 대해 살펴보도록 하자.

api gateway는 Back End에 존재하는 대상 그룹을 연결해 주는 단일 포인트로써 존재한다. 따라서 대상 그룹이 어떤 위치에 있느냐에 따라 아키텍처가 변경될 수 있다.

대체로는 2가지 형태의 api gateway를 배치할 수 있다.

[API Gateway - ALB - Target Group - EKS, ECS, EC2 등]

먼저 Public Subnet에 존재하는 EKS, ECS, EC2 등과 연동하기 위한 아키텍처이다.

ALB와 대상 그룹이 Public Subnet에 존재할 경우 Client는 ALB와 API Gateway에 모두 접속할 수 있도록 구성할 수 있다. 대체로 이 구성은 POC 환경 등 사전 구성하는 용도와 테스트가 여러 케이스에서 이루어 질 때 또는 REST API외에 다른 호출 방식이 이용되어야 할 경우 등에 이용된다.

Public Subnet에 존재하기 때문에 반드시 ALB에 대한 보안구성을 꼼꼼히 해야 한다.

[API Gateway - VPCLink - NLB - Target Group - EKS, ECS, EC2 등]

다음으로 Private Subnet에 존재하는 EKS, ECS, EC2 등과 연동하기 위한 아키텍처이다.

대상 그룹이 Private Subnet에 존재할 경우 보안측면에서 강점이 있지만 VPCLink를 사용하기 위해 NETWORK LOAD BALANACER를 사용해야한다. ALB는 APPLICATION 로드밸런서로 경로 기반 라우팅이 가능하지만, NLB는 포트 기반 라우팅을 지원하여 라우팅 처리해야 한다. 이는 결국 서로 다른 포트를 사용해야 한다는 문제가 있다. 다만 ALB 대비 대용량 API 처리에 용이하고, 고정 IP를 사용할 수 있다는 장점이 있다.

앞서 살펴본 ALB 형태와 연결하는 방식이외에 NLB Type의 로드밸런서와 연결하기 위해서는 다음과 같이 VPCLink를 사용해야 한다.

> 먼저 REST API에 대한 VPC 링크를 생성한다.

> 다음으로 위와 같이 VPC 링크에 대한 세부 정보를 기입한다. 이때 대상 NLB를 엔드포인트로하는 EKS NLB 로드밸런서를 지정해 주면된다.

> 마지막으로 생성한 VPC 링크를 통합 유형으로 하는 API Method를 위와 같이 생성한다. 이때 VPC 링크는 앞서 생성한 TEST-VPC-LINK / 엔드포인트는 NLB를 통해 전달되는 실제 URL Fully 형태로 기입해 주면된다.

구성이 완료되면 API Gateway는 Public Subnet - EKS NLB 로드밸런서는 Private Subnet에 존재하게 되며, 이 구간을 VPC 링크로 연결하여 통신할 수 있게 된다.

# 상세 설명 참조

https://aws.amazon.com/ko/blogs/korea/amazon-api-gateway-vpc-link/

728x90
반응형