Day_52 ( MSA - 2 )

HD.Y·2024년 1월 11일
0

한화시스템 BEYOND SW

목록 보기
45/58
post-thumbnail

🦁 Kafka를 이용한 이메일 인증 기능 구축하기

  • 어제 헥사고날 아키텍처와, Kafka 서버 설치 및 테스트하는 것을 진행해 봤고,

  • 오늘은 회원가입 시 회원의 상태인 "Status"false 로 회원 DB에 저장되도록 하고, 이메일 인증을 통해서 인증이 완료되면 "Status"true로 변경해주는 기능을 구현해봤다.

  • 먼저, MSA로 구현을 할 때 고려해야될점이 각각의 쪼개진 서비스들끼리 통신을 하는 방식이다. 서비스와 서비스 간 통신을 하는 방법으로는 "동기 방식""비동기 방식"이 있다.
    동기 방식 : HTTP 프로토콜을 통한 요청/응답 사용
    비동기 방식 : Kafka를 이용한 메시지 큐 방식 사용

  • 회원의 이메일 인증과 관련되서는 회원 서비스이메일 서비스 간 통신을 통해 이루어지는데, 이때 회원 가입 시 회원가입 시 입력한 이메일로 인증 메일을 보내는 과정은 비동기 방식이 될 것이다. 왜냐하면, 인증 이메일을 보내고 그에 대한 응답을 기다리고 있을 필요가 없기 때문이다.

  • 반면에, 인증 메일을 받고 인증 절차를 처리하는 과정은 동기 방식이 될 것이다. 왜냐하면, 인증과 관련된 요청을 보내고 그에 대한 응답을 받아야지만 인증 처리를 할 수 있기 때문이다.

  • 이처럼, MSA를 할 때는 각각의 서비스가 상호 통신하는 과정에서 어떤 방식으로 구현할지를 고려해봐야 할 것이다.


  • 먼저, 이메일 인증 기능을 구현한 코드는 아래와 같다. ( 헥사고날 아키텍처로 구현 )
    코드의 순서를 회원가입 요청부터 이메일 인증을 거쳐서 회원 DB에 저장된 회원의 Status를 false에서 true로 바뀌는 과정을 순서대로 적어보겠다.

    1) 회원 MSA 와 이메일 MSA의 pom.xml 파일에 필요한 라이브러리 추가
      ➡ 기존의 파일에 오늘 수업 간 새로 추가한 라이브러리만 적어본다.

    회원 MSA

         <dependency>
              <groupId>org.springframework.kafka</groupId>
              <artifactId>spring-kafka</artifactId>
              <version>2.8.11</version>
          </dependency>

    이메일 MSA

         <dependency>
    		  <groupId>org.springframework.boot</groupId>
    		  <artifactId>spring-boot-starter-mail</artifactId>
         </dependency>
         
          <dependency>
              <groupId>org.springframework.kafka</groupId>
              <artifactId>spring-kafka</artifactId>
              <version>2.8.11</version>
          </dependency>

    2) 회원 MSA 와 이메일 MSA의 application.yml 파일에 필요한 설정을 추가

회원 MSA

server:
  port: 8081

spring:
  kafka:
    producer:
      bootstrap-servers: 77.77.77.114:9092  // Kafka 브로커 서버 IP

이메일 MSA

server:
  port: 8082

spring:
  kafka:
    consumer:
      bootstrap-servers: 77.77.77.114:9092  // Kafka 브로커 서버 IP
  mail:
    host: smtp.gmail.com
    port: 587
    username: [구글 계정명]
    password: [앱 비밀번호]
    properties:
      mail:
        smtp:
          starttls:
            enable: true
            required: true
          auth: true
          connectiontimeout: 5000
          timeout: 5000
          writetimeout: 5000  

3) 클라이언트가 회원가입 요청("/member/register")을 보내면 가장 먼저 회원 MSA의
  Web Adapter로 요청이 들어온다.

// 요청을 받는 Dto 객체
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class RegisterMemberReq {

  private String email;
  private String password;
  private String nickname;
}

// 검증용 Dto 객체
@Getter
@Setter
@Builder
public class RegisterMemberCommand {
  @NotNull
  private final String email;

  @NotNull
  private final String password;

  @NotNull
  private final String nickname;

  public RegisterMemberCommand(String email, String password, String nickname) {
      this.email = email;
      this.password = password;
      this.nickname = nickname;

      // TODO : 검증하는 코드 추가 예정
  }
}


// Web 어댑터 ( 컨트롤러 )
@RestController
@RequiredArgsConstructor
@WebAdapter
public class RegisterMemberController {

  private final RegisterMemberUseCase registerMemberUseCase;

  @RequestMapping(method = RequestMethod.POST, value = "/member/register")
  public Member registerMember(@RequestBody RegisterMemberReq registerMemberReq) {
      RegisterMemberCommand registerMemberCommand = RegisterMemberCommand.builder()
              .email(registerMemberReq.getEmail())
              .password(registerMemberReq.getPassword())
              .nickname(registerMemberReq.getNickname())
              .build();

      Member result = registerMemberUseCase.registerMember(registerMemberCommand);
      return result;
  }
}

4) 요청을 받은 "WebAdapter" 는 "Input Port(UseCase)" 를 통해 애플리케이션 서비스로
  요청을 전달한다.

// Input Port 인터페이스  (회원 가입)
public interface RegisterMemberUseCase {
    Member registerMember(RegisterMemberCommand registerMemberCommand);
}

// Member 도메인
@Getter
@Setter
@AllArgsConstructor
@Builder
public class Member {
    private final Long id;
    private final String email;
    private final String password;
    private final String nickname;
    private final Boolean status;
}

// 회원가입 서비스
@Service
@RequiredArgsConstructor
public class RegisterMemberService implements RegisterMemberUseCase {

    private final RegisterMemberPort registerMemberPort;
    private final CreateEmailCertEventPort createEmailCertEventPort;
    @Override
    public Member registerMember(RegisterMemberCommand registerMemberCommand) {

        Member member = Member.builder()
                .email(registerMemberCommand.getEmail())
                .password(registerMemberCommand.getPassword())
                .nickname(registerMemberCommand.getNickname())
                .status(false)
                .build();

        MemberJpaEntity memberJpaEntity = registerMemberPort.createMember(member);
        
        // ------------- 여기까지 회원가입에 대한 어댑터 호출 및 결과 반환-----------------
        
        // 이메일 전송을 위한 어댑터 호출
        createEmailCertEventPort.createEmailCertEvent(member);
        
        return Member.builder()
                .id(memberJpaEntity.getId())
                .email(memberJpaEntity.getEmail())
                .password(memberJpaEntity.getPassword())
                .nickname(memberJpaEntity.getNickname())
                .status(memberJpaEntity.getStatus())
                .build();
    }
}

5) 애플리케이션 서비스에서는 회원 가입과 관련되서 PersistenceAdapter를 호출하는것과
 이메일 보내는 것과 관련해서 CreateEmailCertEventAdapter를 호출하는 것 2개의 처리가
 있을 것이다.
 ➡ 각각의 어댑터를 호출하기 위해서는 "Output Port" 를 통해서 가야한다.

회원가입 처리

// Output Port
public interface RegisterMemberPort {
    MemberJpaEntity createMember(Member member);
}

// Persistence 어댑터 ( DB에 접근하여 회원정보 저장 )
@Component
@RequiredArgsConstructor
@PersistenceAdapter
public class MemberPersistenceAdapter implements RegisterMemberPort {
    private final MemberJpaRepository memberJpaRepository;

    @Override
    public MemberJpaEntity createMember(Member member) {
        MemberJpaEntity memberJpaEntity = MemberJpaEntity.builder()
                .email(member.getEmail())
                .nickname(member.getNickname())
                .password(member.getPassword())
                .status(member.getStatus())
                .build();
        memberJpaRepository.save(memberJpaEntity);

        return memberJpaEntity;
    }
}

// 회원 엔티티
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
public class MemberJpaEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String email;
    private String password;
    private String nickname;
    private Boolean status;

}

// 회원 레포지토리
@Repository
public interface MemberJpaRepository extends JpaRepository<MemberJpaEntity, Long> {
    public Optional<MemberJpaEntity> findByEmail(String email);
}

인증메일 발송 처리를 위한 요청

// Output Port
public interface CreateEmailCertEventPort {
    void createEmailCertEvent(Member member);
}

// 인증메일 발송 어댑터 ( 카프카 라이브러리 사용 )
@ExternalSystemAdapter
@RequiredArgsConstructor
public class CreateEmailCertEventAdapter implements CreateEmailCertEventPort {
    private final KafkaTemplate kafkaTemplate;
    @Override
    public void createEmailCertEvent(Member member) {
        ProducerRecord<String, String> record = new ProducerRecord<>("emailcert", "email", member.getEmail());

        kafkaTemplate.send(record);
    }
}

6) 여기까지 하면 회원가입 요청에 따라 회원정보가 DB에 저장(Status=false)되었을 것이고,
  인증메일 발송을 위해서 Kafka의 브로커 서버로 회원의 이메일 주소를 포함하여 메시지를
  보낸 것이다.

  그럼 이제, 이메일 MSA에서 회원 MSA가 브로커 서버로 보낸 메시지를 수신받도록
  작성해준다.

// 메시지를 수신받기 위한 컨트롤러 역할 클래스 작성
@ExternalSystemAdapter  // 커스텀 어댑터(별도 기능이 있는것은 아님)
@RequiredArgsConstructor
public class CreateEmailCertConsumer {

   private final CreateEmailCertUseCase createEmailCertUseCase;

   @KafkaListener(topics = "emailcert", groupId = "emailcert-group-00")
   void createEmailCert(ConsumerRecord<String, String> record) {

       CreateEmailCommand createEmailCommand = CreateEmailCommand.builder()
               .email(record.value())
               .build();

       createEmailCertUseCase.createEmailCert(createEmailCommand);
   }
}

➡ 이 코드는 Kafka 라이브러리에 구현되어 있는 코드들이다. 중요한것은 프로듀서에서
  메시지를 보낼때의 토픽컨슈머가 메시지를 받을때 토픽을 반드시 같게 적어줘야된다는
  점이다.

  수신한 "record" 에는 사용자의 email 주소가 value 값으로 들어있다.


7) WebAdapter에서 Input Port(UseCase)를 통해 애플리케이션 서비스로 요청을 보낸다.

// 검증용 Dto객체
@Getter
@Setter
@Builder
public class CreateEmailCommand {
    @NotNull
    private final String email;

    public CreateEmailCommand(String email) {
        this.email = email;
    }
}

// Input Port
public interface CreateEmailCertUseCase {
    EmailCert createEmailCert(CreateEmailCommand createEmailCommand);
}

// EmailCert 도메인
@Getter
@Setter
@AllArgsConstructor
@Builder
public class EmailCert {
    private final String email;
    private final String uuid;
}

// 인증메일 발송 서비스
@Service
@RequiredArgsConstructor
public class CreateEmailCertService implements CreateEmailCertUseCase {

    private final CreateEmailCertPort createEmailCertPort;
    private final SendEmailPort sendEmailPort;
    @Override
    public EmailCert createEmailCert(CreateEmailCommand createEmailCommand) {

        String uuid = UUID.randomUUID().toString();

        EmailCert emailCert = EmailCert.builder()
                .email(createEmailCommand.getEmail())
                .uuid(uuid)
                .build();

        EmailCertJpaEntity emailCertJpaEntity = createEmailCertPort.createEmailCert(emailCert);
        // ------------- 여기까지 이메일 저장을 위한 DB 접근 어댑터 호출 및 결과 반환-----------------
        
        // 이메일 발송 어댑터 호출
        sendEmailPort.sendEmail(emailCert);

        return EmailCert.builder()
                .id(emailCertJpaEntity.getId())
                .email(emailCertJpaEntity.getEmail())
                .uuid(emailCertJpaEntity.getUuid())
                .build();
    }
}

8) 애플리케이션 서비스에서는 추후 이메일 검증을 위한 이메일과 UUID(중복되지 않은
  랜덤한 문자열)을 DB에 저장하기 위해 PersistenceAdapter
를 호출하는것과
  이메일 발송하는 것과 관련해서 SendEmailAdapter를 호출하는 것 2개의 처리가
  있을 것이다.
  ➡ 각각의 어댑터를 호출하기 위해서는 "Output Port" 를 통해서 가야한다.

이메일 인증을 위한 정보 DB 저장 처리

// Output Port
public interface CreateEmailCertPort {
   EmailCertJpaEntity createEmailCert(EmailCert emailCert);
}

// Persistence 어댑터
@Component
@RequiredArgsConstructor
@PersistenceAdapter
public class EmailCertPersistenceAdapter implements CreateEmailCertPort, VerifyEmailCertPort {
   private final EmailCertJpaRepository emailCertJpaRepository;


   @Override
   public EmailCertJpaEntity createEmailCert(EmailCert emailCert) {

       EmailCertJpaEntity emailCertJpaEntity = EmailCertJpaEntity.builder()
               .email(emailCert.getEmail())
               .uuid(emailCert.getUuid())
               .build();

       emailCertJpaRepository.save(emailCertJpaEntity);

       return emailCertJpaEntity;
   }
}

// 인증 관련 엔티티
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
public class EmailCertJpaEntity {

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;

   private String email;
   private String uuid;
}

// 인증 관련 레포지토리
@Repository
public interface EmailCertJpaRepository extends JpaRepository<EmailCertJpaEntity, Long> {
   public Optional<EmailCertJpaEntity> findByEmail(String email);
}

인증메일 발송을 위한 처리

// Output Port
public interface SendEmailPort {
    void sendEmail(EmailCert emailCert);
}

// SendEmail 어댑터
@RequiredArgsConstructor
@ExternalSystemAdapter
public class SendEmailAdapter implements SendEmailPort {
    private final JavaMailSender emailSender;

    public void sendEmail(EmailCert emailCert) {
        SimpleMailMessage message = new SimpleMailMessage();

        message.setTo(emailCert.getEmail());
        message.setSubject("회원가입을 완료하기 위해서 이메일 인증을 진행해 주세요"); // 메일 제목
        message.setText("http://localhost:8081/member/verify?email=" + emailCert.getEmail() + "&uuid=" + emailCert.getUuid());    // 메일 내용

        emailSender.send(message);
    }
}

9) 여기까지 하면 이메일 검증을 위한 이메일과 UUID(중복되지 않은 랜덤한 문자열)가
  DB에 저장되었을 것이고, 회원의 이메일로 인증메일이 발송 되었을 것이다.

  그렇다면 이제 회원이 수신한 인증메일에 포함되어 있는 URL을 클릭하면 이메일 인증을
  위한 GET 방식의 HTTP 요청이 다시 "회원 MSA의 Web Adapter" 로 전달받게끔 만든다.

// 요청을 받는 Dto
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class VerifyMemberReq {
    private String email;
    private String uuid;
}

// 검증용 Dto
@Getter
@Setter
@Builder
public class VerifyMemberCommand {
    @NotNull
    private final String email;
    @NotNull
    private final String uuid;

    public VerifyMemberCommand(String email, String uuid) {
        this.email = email;
        this.uuid = uuid;
    }
}

// Web Adapter
@RestController
@RequiredArgsConstructor
@WebAdapter
public class VerifyMemberController {
    private final VerifyMemberUseCase verifyMemberUseCase;

    @RequestMapping(method = RequestMethod.GET, value = "/member/verify")
    public Boolean verifyEmail(VerifyMemberReq verifyMemberReq) {
        VerifyMemberCommand verifyMemberCommand = VerifyMemberCommand.builder()
                .email(verifyMemberReq.getEmail())
                .uuid(verifyMemberReq.getUuid())
                .build();

        Boolean result = verifyMemberUseCase.verifyMember(verifyMemberCommand);

        return result;
    }
}

10) WebAdapter는 InputPort(UseCase)를 통해 애플리케이션 서비스를 호출한다.

// Input Port
public interface VerifyMemberUseCase {
    Boolean verifyMember(VerifyMemberCommand verifyMemberCommand);
}

// 인증용 VerifyMember 도메인
@Getter
@Setter
@AllArgsConstructor
@Builder
public class VerifyMember {
    private final String email;
    private final String uuid;
}

// 인증 서비스
@Service
@RequiredArgsConstructor
public class VerifyMemberService implements VerifyMemberUseCase {
    private final VerifyMemberPort verifyMemberPort;


    @Override
    public Boolean verifyMember(VerifyMemberCommand verifyMemberCommand) {

        VerifyMember verifyMember = VerifyMember.builder()
                .email(verifyMemberCommand.getEmail())
                .uuid(verifyMemberCommand.getUuid())
                .build();

        return verifyMemberPort.verifyMember(verifyMember);
    }
}

➡ 여기서 기존의 Member 도메인이 아닌 새로운 VerifyMember 도메인을 생성하였는데,
  이것에 대한 고민이 많았다.

  도메인이란 개념을 내가 이해한 바로는 하나의 MSA 서비스에 해당하는 비지니스 로직들을
  포괄적으로 처리할 수 있는 모든 변수들을 포함하고 있다고 생각하여, 처음에는 기존의
  Member 도메인에 인증을 위한 "uuid" 변수만 추가해줘서 Member 도메인을 쓰면 되지
  않을까? 하는 생각을 하였다.

  하지만, MSA를 구현하는데 있어서 의존성을 최소화 시키기 위해 작업을 하고 있는데
  기존의 Member 도메인에 갑자기 새로운 변수를 추가하면 Member 도메인과 연관되는
  비지니스 로직 처리가 회원가입과 이메일 인증 2가지가 되어버리는 것이다.

  따라서, 이것을 별도의 도메인으로 분리해주는게 보다 더 MSA적으로 구현하는 것이라는
  것을 깨달았다. 하지만, 또 너무 세분화되어 도메인을 계속 만드는 것도 그리 좋지는 않을
  것이다. 따라서 위의 상황에서 "uuid" 1개 정도는 변수로 저장해서 처리하는 등의 방법이
  있을 수 있듯이 명확한 정답은 없고, 최대한 클린 아키텍처의 목적을 얼마나 따르는지를
  생각해보면 좋을 것 같다.


11) 인증서비스는 입력받은 회원의 email과 UUID 값이 DB에 저장된 값과 일치하는지
    확인하기 위해 OutPut Port를 통해 이메일 MSA로 확인 요청을 보내게 된다.

// OutputPort
public interface VerifyMemberPort {
   Boolean verifyMember(VerifyMember verifyMember);
}

// 인증 확인 요청을 보내기 위한 어댑터
@WebAdapter
@RequiredArgsConstructor
@RestController
public class VerifyMemberAdapter implements VerifyMemberPort {

   private final OpenFeignVerifyEmailCertClient openFeignVerifyEmailCertClient;
   private final ModifyMemberStatusPort modifyMemberStatusPort;
   
   @Override
   public Boolean verifyMember(VerifyMember verifyMember) {

       Boolean response = openFeignVerifyEmailCertClient.call(verifyMember.getEmail(), verifyMember.getUuid());
// ------------------------여기까지 인증 확인 요청을 이메일 MSA로 보냄------------------

// ----여기부터는 인증 확인 결과를 이메일 MSA로 부터 받아서 회원의 상태를 바꾸기 위한 요청---
       if(response == true) {
           return modifyMemberStatusPort.modifyMemberStatus(Member.builder()
                   .email(verifyMember.getEmail())
                   .status(true)
                   .build());
       } else {
           return false;
       }
   }
}

➡ 여기서, 이제 회원 MSA에서 이메일 MSA로 동기 방식의 HTTP 요청을 보내야 되는데,
  그것을 쉽게 보낼 수 있게 스프링 부트에 라이브러리가 또 있다. 바로 "OpenFeign" 이다.

➡ 회원 MSA의 pom.xml 파일에 라이브러리를 추가해주고,

        <dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-starter-openfeign</artifactId>
           <version>3.1.3</version>
       </dependency>

➡ 회원 MSA의 메인 애플리케이션 클래스에 @EnableFeignClients 을 달아준다.

➡ 그런 다음 위의 어댑터에서 구현한 OpenFeignVerifyEmailCertClient 는 아래와 같이
  작성할 수 있다.

@FeignClient(name="EmailCert", url="http://localhost:8082/emailcert/verify")
public interface OpenFeignVerifyEmailCertClient {

   @GetMapping
   Boolean call(@RequestParam String email, @RequestParam String uuid);
}

12) 그러면 다시 이메일 MSA의 Web Adapter에 이 GET 방식의 HTTP 요청을 받을 수 있도록
    작성해준다.

// 요청받을 Dto
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class VerifyEmailCertReq {
    private String email;
    private String uuid;
}

// 검증용 Dto
@Getter
@Setter
@Builder
public class VerifyEmailCertCommand {
    @NotNull
    private final String email;

    @NotNull
    private final String uuid;

    public VerifyEmailCertCommand(String email, String uuid) {
        this.email = email;
        this.uuid = uuid;
    }
}

// Web Adapter
@RestController
@RequiredArgsConstructor
@WebAdapter
public class VerifyEmailCertController {
    private final VerifyEmailCertUseCase verifyEmailCertUseCase;

    @RequestMapping(method = RequestMethod.GET, value = "/emailcert/verify")
    public Boolean verifyEmailCert(VerifyEmailCertReq verifyEmailCertReq) {
            VerifyEmailCertCommand verifyEmailCertCommand = VerifyEmailCertCommand.builder()
                    .email(verifyEmailCertReq.getEmail())
                    .uuid(verifyEmailCertReq.getUuid())
                    .build();

        Boolean result = verifyEmailCertUseCase.verifyEmailCert(verifyEmailCertCommand);

        return result;
    }
}

13) Web Adapter는 Input Port(UseCase)를 통해 인증 서비스를 호출한다.

@Service
@RequiredArgsConstructor
public class VerifyEmailCertService implements VerifyEmailCertUseCase {
    private final VerifyEmailCertPort verifyEmailCertPort;
    @Override
    public Boolean verifyEmailCert(VerifyEmailCertCommand verifyEmailCertCommand) {
        EmailCert emailCert = EmailCert.builder()
                .email(verifyEmailCertCommand.getEmail())
                .uuid(verifyEmailCertCommand.getUuid())
                .build();

        return verifyEmailCertPort.verifyEmailCert(emailCert);
    }
}

14) 인증 서비스는 Output Port를 통해 Persistence 어댑터를 호출한다.

// Output Port
public interface VerifyEmailCertPort {
    Boolean verifyEmailCert(EmailCert emailCert);
}

// 기존 rsistence 어댑터에 인증 기능 추가
@Component
@RequiredArgsConstructor
@PersistenceAdapter
public class EmailCertPersistenceAdapter implements CreateEmailCertPort, VerifyEmailCertPort {
    private final EmailCertJpaRepository emailCertJpaRepository;


    @Override
    public EmailCertJpaEntity createEmailCert(EmailCert emailCert) {

        EmailCertJpaEntity emailCertJpaEntity = EmailCertJpaEntity.builder()
                .email(emailCert.getEmail())
                .uuid(emailCert.getUuid())
                .build();

        emailCertJpaRepository.save(emailCertJpaEntity);

        return emailCertJpaEntity;
    }
    
    //--------------------여기부터 추가한 부분-------------------------
    @Override
    public Boolean verifyEmailCert(EmailCert emailCert) {

        Optional<EmailCertJpaEntity> result = emailCertJpaRepository.findByEmail(emailCert.getEmail());

        if(result.isPresent()) {
            EmailCertJpaEntity emailCertJpaEntity = result.get();

            if(emailCert.getUuid().equals(emailCertJpaEntity.getUuid())) {
                return true;
            } else {
                return false;
            }
        }
        return false;
    }
}

15) Persistence 어댑터는 DB로 접근하여 회원의 이메일에 해당하는 데이터가 있는지
   확인하고, 있으면 저장된 UUID값과 요청이 들어온 값이 일치하면 true를 반환 시켜주고,
   일치하지 않으면 false를 반환시켜준다.

   반환한 값은 11번의 "VerifyMember 어댑터" 로 들어오게 되고 "true" 가 반환됬다면,
   Output Port를 통해서 다시 Persistence 어댑터를 호출하게 된다.

// Output Port
public interface ModifyMemberStatusPort {
       Boolean modifyMemberStatus(Member member);
}

// 기존 Persistence 어댑터에 내용 추가
@Component
@RequiredArgsConstructor
@PersistenceAdapter
public class MemberPersistenceAdapter implements RegisterMemberPort, ModifyMemberPort, ModifyMemberStatusPort {
   private final MemberJpaRepository memberJpaRepository;

   @Override
   public MemberJpaEntity createMember(Member member) {
       MemberJpaEntity memberJpaEntity = MemberJpaEntity.builder()
               .email(member.getEmail())
               .nickname(member.getNickname())
               .password(member.getPassword())
               .status(member.getStatus())
               .build();
       memberJpaRepository.save(memberJpaEntity);
       System.out.println(memberJpaEntity);

       return memberJpaEntity;
   }

   // -------------------- 여기부터 추가한 부분------------------------------
   @Override
   public Boolean modifyMemberStatus(Member member) {
       Optional<MemberJpaEntity> result = memberJpaRepository.findByEmail(member.getEmail());

       MemberJpaEntity memberJpaEntity = result.get();
       memberJpaEntity.setStatus(true);
       memberJpaRepository.save(memberJpaEntity);

       return true;
   }
}

  • 여기까지 해서 회원가입부터 이메일 인증 과정까지 끝이 난다. 딱 봐도, MSA 적으로 구현하는 것이 쉽지 않은 것을 볼 수 있었다. 하지만 코드를 자세히 보면 원리는 동일하게 동작하고 있는 것을 알 수있다.

  • 헥사고날 아키텍처의 개념과 구조를 잘 이해하고 있다면, 코드적으로 구현하는 것은 크게 어려운 일은 아닐 것이다. 하지만 MSA는 아직까지도 명확한 답이 없듯이, 고려해야할 부분도 많고, 파고 들어가면 아직 해결하지 못한 부분도 많이 발생할 것이라 생각한다.


🐶 Eureka 설치 및 설정하기

  • Eureka 란❓

    Eureka는 AWS와 같은 클라우드 시스템에서 서비스의 로드 밸런싱과 실패처리 등을 유연하게 가져가 위해 각 서비스들의 IP / Port / InstanceId를 가지고 있는 REST 기반의 미들웨어 서버이다.

    Eureka는 마이크로 서비스 기반의 아키텍처의 핵심 원칙 중 하나인 Service Discovery의 역할을 수행한다. MSA에서는 Service의 IP와 Port가 일정하지 않고 지속적을 변화하기 때문에, Client에 Service의 정보를 수동으로 입력하는 것은 한계가 분명하다

    하지만, Eureka를 사용하면 디스커버리 서버에 IP와 관련된 값들을 저장 시켜놓고, 각각의 MSA에서는 디스커버리 서버에서 필요한 URL을 불러와서 사용하는 것이 가능해진다.

  • Eureka 설정방법
    현재 나는 member-service, email-cert-service, gateway 가 각각 모듈로 구성되어 있고 여기에 discovery 모듈을 추가해준다.

    💻 Eureka 서버 설정

    1) Eureka 서버 역할을 할 discovery 모듈의 pom.xml 파일에 라이브러리를 추가한다.

        <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
             <version>3.1.3</version>
         </dependency>

    2) application.yml 파일에 설정을 추가한다.

    server:
      port: 8761
     spring:
       application:
         name: discovery-server
     eureka:
       client:
         register-with-eureka: false
         fetch-registry: false

    3) DiscoveryApplication 메인 클래스에 @EnableEurekaServer 어노테이션을
       달아준다.


    💻 member-service / email-cert-service / gateway 각각에 공통 설정

    1) pom.xml 파일에 클라이언트 라이브러리 추가

        <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
             <version>3.1.3</version>
             <exclusions>
                 <exclusion>
                     <groupId>javax.servlet</groupId>
                     <artifactId>javax.servlet-api</artifactId>
                 </exclusion>
             </exclusions>
         </dependency>

    2) application.yml 파일에 설정 추가

    // member-service
    eureka:
     client:
       register-with-eureka: true
       fetch-registry: true
       service-url:
         defaultZone: http://localhost:8761/eureka
    spring:
      application:
        name: MEMBER-SERVICE
        
    // email-cert-service    
    eureka:
     client:
       register-with-eureka: true
       fetch-registry: true
       service-url:
         defaultZone: http://localhost:8761/eureka
    spring:
      application:
        name: EMAIL-CERT-SERVICE
        
    // gateway
    server:
      port: 9999
    
    spring:
      application:
        name: gateway
    cloud:
     gateway:
       routes:
         - id: member-service
           uri: lb://MEMBER-SERVICE    // lb : 로드 밸런서 의미
           predicates:
             - Path=/member/**
         - id: email-cert-service
           uri: lb://EMAIL-CERT-SERVICE
           predicates:
             - Path=/emailcert/**

    3) 메인 Application 클래스에 @EnableDiscoveryClient 달아준다.


  • 여기까지 하고, localhost:8761 로 접속하면 아래와 같이 출력된다.


  • 그러면 이제 모든 요청을 게이트웨이의 포트인 9999번으로 보내고, 게이트웨이에서 요청을 보낼때 디스커버리 설정에 추가되있는 url을 불러올 수 있게 된다.

profile
Backend Developer

0개의 댓글