[Spring] 2. MSA 구조에서의 로그인

Beanzinu·2022년 10월 11일
2

스프링부트

목록 보기
6/7

글을 쓰기에 앞서 msa 구조에서 흔히 메시지 교환을 위해 사용하는 메시지 큐 시스템은 아니고 필요한 경우 API 호출을 통해 DB 간의 정합성을 조금이나마 해결해보려고 혼자 공부한 기록임을 밝힙니다.

MSA란?

MicroService Architecture의 줄임말
: 마이크로서비스 아키텍처에 대한 정확한 정의는 없다. 하지만 마이크로서비스란 작고, 독립적으로 배포 가능한 각각의 기능을 수행하는 서비스로 구성된 프레임워크라고 할 수 있다. 마이크로서비스는 완전히 독립적으로 배포가 가능하고, 다른 기술 스택(개발 언어, 데이터베이스 등)이 사용 가능한 단일 사업 영역에 초점을 둔다.

Monolithic Architecture는 소프트웨어의 모든 구성요소가 한 프로젝트에 통합되어 있는 형태이다. 웹 개발을 예로 들면 웹 프로그램을 개발하기 위해 모듈별로 개발을 하고, 개발이 완료된 웹 어플리케이션을 하나의 결과물로 패키징하여 배포되는 형태를 말한다. 이런 어플리케이션을 모놀리식 어플리케이션이라 하며, 웹의 경우 WAR파일로 빌드되어 WAS에 배포하는 형태를 말한다. 주로 소규모 프로젝트에서 사용된다.
하지만 일정 규모 이상의 서비스, 혹은 수백명 이상의 개발자가 투입되는 프로젝트에서 Monolithic Architecture는 한계를 보인다

Monolithic 구조의 단점

  1. 부분 장애가 전체 서비스의 장애로 확대될 수 있다. 

    개발자의 잘못된 코드 배포 또는 갑작스런 트래픽 증가로 인해 성능에 문제가 생겼을 때, 서비스 전체의 장애로 확대될 수 있다.

  2. 부분적인 *Scale-out(여러 server로 나누어 일을 처리하는 방식)이 어렵다.

    Monolithic Architecture에서는 사용되지 않는 다른 모든 서비스가 Scale-out되어야 하기 때문에 부분 Scale-out이 어렵다.

  1. 서비스의 변경이 어렵고, 수정 시 장애의 영향도 파악이 힘들다. 

    여러 컴포넌트가 하나의 서비스에 강하게 결합되어 있기 때문에 수정에 대한 영향도 파악이 힘들다.

  1. 배포 시간이 오래 걸린다. 

    최근 어플리케이션 개발 방법은 CI/CD를 통한 개발부터 배포까지 빠르게 반영하는 추세이다. 그러나 규모가 커짐에 따라 작은 변경에도 높은 수준의 테스트 비용이 발생하기도 하며, 많은 사람이 하나의 시스템을 개발하여 배포하기 때문에 영향을 줄 수 밖에 없다.

  2. 한 Framework와 언어에 종속적이다.

    Spring framework를 사용할 경우, blockchain 연동 모듈을 추가할 때 node.js를 사용하는 것이 일반적이며 강세이다. 그러나 java를 이용하여 해당 모듈을 작성할 수 밖에 없다. 선정했던 framework가 Spring이기 때문이다.

MSA구조에서의 로그인

  1. 유저는 인증서버 를 통해 회원가입을 할 것이다.
    인증서버 DB에는 유저정보가 저장되고 회원가입은 완료되고 로그인 또한 인증서버를 통해 가능하다.

    유저 <-> 인증서버 - 인증서버DB

  2. 만약 게시물서버 가 있다고 하자.
    게시물서버에서 유저가 인증된 유저인지는 토큰기반 인증을 통해 알 수 있다.
    하지만 게시물을 누가 작성했는지의 여부를 저장해보려고 하자.
    보통 우리는 Monolothic 구조에서 유저와 게시물과의 연관을 통해 해결했을 것이다.
    MSA 구조에서는 현재 유저정보를 인증서버에서 관리하고 있기 때문에 위와 같은 연관을 맺기 어렵다. 만약 토큰을 통해 유저정보를 받아와 연관을 맺는다면 토큰 안에 민감한 유저정보도 포함시켜 보안에 매우 취약할 것 같다고 생각이 들었다.

    Table(게시물) - Table(유저)

DB간의 정합성 해결시도

  1. 유저가 회원가입할때 인증서버에서 게시물서버와 같은 비인증서버로 유저정보를 HTTP 요청으로 보낸다.

    HTTP 요청으로 보낼 시 결국 보안문제가 생기므로 HTTPS를 통해 데이터를 암호화해서 보내면 좋을 것 같다.

    인증서버(localhost:8080) -> 다른서버(localhost:8090) 코드예시 (HTTP)

    RestTemplate restTemplate = new RestTemplate();
    
    String apiURL = "http://localhost:8090/api/user";
    
    HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
    
    JSONObject requestJson = new JSONObject();
            requestJson.put("email",email);
            requestJson.put("profileUrl",profileUrl);
    
    HttpEntity<String> entity = new HttpEntity<>(requestJson.toJSONString(), headers);
    ResponseEntity<String> response = restTemplate.postForEntity(apiURL, entity,String.class);입력하세요
  2. 다른서버가 해당 API요청을 처리를 못했을 수도 있다.
    이를 위해 한가지 상황을 더 가정했다.
    게시물서버에서 비지니스 로직 수행 중 유저를 조회해야할 때가 있을 것이다.
    만약 인증서버로부터 유저정보를 제대로 받아오지 못했다면 조회를 실패할 수 있을 것이다. 이 경우에는 토큰에 들어있는 유저정보는 인증서버에는 존재하지만 비게시물서버에는 존재하지 않는 것이다.

    유저조회에 실패할 경우 인증서버로부터 토큰에 있는 유저 PK등과 부합하는 유저정보를 요청하고(1번과 같지만 방향이 반대) 저장한다.

    현재 비지니스 로직에 대해 작성하지는 않아서 유저정보가 필요한 상황을 외부에서 API를 호출하면서 진행했다.

    2-1) 게시물서버에서 유저를 찾음.

    유저를 찾을 수 없기 때문에 NoResultException이 잡힌다.

    	// UserController.java
      @GetMapping("/api/user")
      public ResponseEntity<String> getUser(@Param(value = "email")String email){
            User findUser = userService.getUserByEmail(email);
            return ResponseEntity.ok(findUser.getId().toString());
      }
      
      // UserService.java
      public User getUserByEmail(String email) {
          User findUser = userRepository.findByEmail(email).orElseThrow(()->{throw new NoResultException(email);});
          return findUser;
      }

    2-2) NoResultException을 잡는 ExceptionHandler를 설정.
    해당 Handler는 조회가 실패한 유저의 이메일을 Exception Message로 가지고 있다.
    해당 이메일을 인증서버로 전달하여 유저정보를 받아온다.
    성공 : 게시물서버 DB에 유저정보를 저장한다.
    실패 : 인증서버에도 유저가 없다는 것인데 사실상 토큰에서 유저정보를 가져오고 토큰은 인증서버로부터 발급되기 때문에 토큰의 유효성이 검증되었다면 HTTP 에러일 가능성이 높다.

    @ExceptionHandler(NoResultException.class)
      public ResponseEntity<String> getUserAdvice(Exception e) {
          RestTemplate restTemplate = new RestTemplate();
    
          URI uri = UriComponentsBuilder
                  .fromUriString("http://localhost:8080")
                  .path("/msa/user")
                  .queryParam("email",e.getMessage())
                  .encode()
                  .build()
                  .toUri();
    
          ResponseEntity<JSONObject> responseEntity = restTemplate.getForEntity(uri,JSONObject.class);
          if (responseEntity.getStatusCode().isError()){
              return ResponseEntity.ok("인증서버에도 없는 유저/HTTP에러");
          }
          JSONObject data = responseEntity.getBody();
          JSONObject body = new JSONObject((LinkedHashMap) data.get("data"));
    
          userService.addUser( UserRequestDto.jsonObjectToUserRequestDto(body) );
          // 임시
          return ResponseEntity.ok("인증서버로부터 저장성공");
      }

Kafka란?

Apache Kafka는 빠르고 확장 가능한 작업을 위해 데이터 피드의 분산 스트리밍, 파이프 라이닝 및 재생을 위한 실시간 스트리밍 데이터를 처리하기 위한 목적으로 설계된 오픈 소스 분산형 게시-구독 메시징 플랫폼입니다.

Kafka는 서버 클러스터 내에서 데이터 스트림을 레코드로 유지하는 방식으로 작동하는 브로커 기반 솔루션입니다. Kafka 서버는 여러 데이터 센터에 분산되어 있을 수 있으며 여러 서버 인스턴스에 걸쳐 레코드 스트림(메시지)을 토픽으로 저장하여 데이터 지속성을 제공할 수 있습니다. 토픽은 레코드 또는 메시지를 키, 값 및 타임 스탬프로 구성된 일련의 튜플, 변경 불가능한 Python 객체 시퀀스로 저장합니다.

동기적으로 API 요청을 통해 해결하지 않고 메시지큐 기반 시스템인 Kafka라는 기술이 있다. 이는 인증서버가 메시지 큐에 정보를 제공하고 비인증서버들이 구독한 큐에 대해서 메시지를 소모하는 방식이다. 이후 프로젝트를 진행하면서 공부해보면 좋을 것 같다.

참고 :
https://www.tibco.com/ko/reference-center/what-is-apache-kafka
https://wooaoe.tistory.com/57

profile
당신을 한 줄로 소개해보세요.

0개의 댓글