이벤트 기반, 서비스간 강결합 문제 해결하기 - ApplicationEventPublisher

LJH·2022년 7월 22일
16
post-thumbnail

0. 이 글을 쓰는 이유

회사에서 주문 관련(결제, 취소, 환불 등) 알림톡 기능을 개발했다.
별 문제 없이 상용환경에 배포했고, 잘 동작했다.

그리고 추가 알림톡 기능을 개발하면서 개발환경에서 테스트 해보던 중,
알림톡 관련 로직에서 NPE가 발생했고 주문까지 롤백되어 취소됐다.

주문과 알림톡 발송이 한 서비스 로직안에 하나의 트랜잭션으로 묶여있었기 때문인데
알림톡이 발송되지 않아도 사용자의 편의성이 떨어질 뿐, 주문하는데 문제는 없다.
그러므로 알림톡이 주문에 영향을 미쳐서는 안된다.

이를 해결하기 위해 마침 YAPP 프로젝트에서 비슷한 요소가 있어서
YAPP 프로젝트에 먼저 적용해보고, 회사 프로젝트에 적용해 보고자 한다.


1. 강한 결합도 → 느슨한 결합으로 (트랜잭션 분리)

1-1. 왜? - 강결합으로 인한 문제들

1-1-1. 의존성 문제

위에서도 이야기 했지만, 부가적인 기능의 실패로 핵심 기능에 영향을 미쳐서는 안된다.

하지만 반대로 핵심 기능이 실패하면, 부가적인 기능도 같이 실패해야 한다.
핵심 기능(주문)이 실패했는데, 부가적인 기능(알림톡 발송)이 성공하면 안된다.

또한 단순 코드레벨에서의 에러뿐만 아니라, 알림톡을 발송하는 서버(외부 요인)에서 장애가
발생한다면, 알림톡 발송이 실패할 것이고 이 영향으로 쇼핑몰(본래의 서비스)에서
주문까지 실패하게 된다. (알림톡때문에 주문이 실패해서는 안된다.)

1-1-2. 성능 문제

  • 예로 주문 로직을 처리하는 시간이 5초, 알림톡을 발송하는 로직이 10초 걸렸다고 해보자.
    사용자가 주문하는데 총 15초를 기다려야 하는 셈이다.

  • 핵심 기능(주문)은, 부가적인 기능(알림톡 발송)을 기다릴 필요가 없다.

1-2. 정책 및 요구사항

  • 위에서 주문과 알림톡을 예로 이야기 했지만, 적용해볼 프로젝트의 상황은 아래와 같다.

  • 구현해야 하는 기능

    • 회원가입 (회원 정보 등록 + 이미지 등록)
  • 핵심 기능

    • 회원 정보 등록 (닉네임, 나이, 주소 등)
  • 부가적인 기능이자 외부 요소

    • AWS S3및 DB에 이미지 정보 저장
  • 정책(요구사항)

    • 회원가입 진행 시, 사용자는 자신의 프로필 이미지를 등록할 수 있다.

    • 이 때 이미지 등록이 실패하더라도, 회원 정보 등록은 성공해야 한다. (알림톡 전송이 실패해도, 주문은 완료되어야 하기 때문에)

    • 반대로 회원 정보 등록이 실패하면, 이미지 등록도 실패해야 한다.
      (주문이 실패했는데 주문 완료 알림톡이 전송되는 상황은 피해야 하기 때문에)

이런것들은 기획에 따라 달라질 수 있는 내용이지만, 이런 정책이 있다고 가정했다.

1-3. 현재 상황과, 만족해야 하는 조건은?

  • 현재 상황은 위 그림과 같고, 만족해야 하는 조건은 아래와 같다.
  • 회원 정보 등록 실패시, 이미지 저장도 실패해야 한다. (필수)
    • 지금은 한 트랜잭션에 묶여있기 때문에 1번 조건은 만족한다.
  • 이미지 저장이 실패해도, 회원 정보 등록은 성공해야 한다. (필수)
  • 회원 가입 완료 이후에 이미지를 저장한다. (선택)

2. 의존성을 분리하는 방법 (내가 생각한..)

2-1. 컨트롤러에서 각각 호출해서 트랜잭션 분리하기

@RestController
public class AccountController {
		...
    @PostMapping("회원가입")
    public void signUp(){
    	응답 = 회원 정보 등록();

		if(응답 성공하면){
        	이미지저장(); 
        }
    }
}
  • 가장 처음에 떠올린 방법이였다. 그리고 가장 간단한 방법이기도 하다.

성능문제는 해결하지 못하지만 필수 조건 1,2번을 만족한다.

  • 회원 정보 등록 실패시, 이미지 저장도 실패해야 한다. (필수)
  • 이미지 저장이 실패해도, 회원 정보 등록은 성공해야 한다. (필수)
  • 회원 가입 완료 이후에 이미지를 저장한다. (선택)

2-2. 트랜잭션 전파옵션으로 분리?

  • 두 번째로 떠오른 생각은, 한 트랜잭션으로 묶여있는게 문제니까 트랜잭션만
    분리해주면 되지 않을까? 라는 생각이 들었다.

  • 스프링의 @Transactional은 여러 전파 옵션을 제공해주는데
    요구사항을 만족시킬 수 있는 옵션이 있는지 살펴보자

2-2-1. Propagation.REQUIRES_NEW 옵션

  • 부모 트랜잭션이 있어도 이를 무시하고, 무조건 새 트랜잭션을 생성한다.
    호출한 곳(부모), 호출된 곳(자식) 두 트랜잭션이 독립적으로 동작한다.
    따라서 롤백도 전파되지 않는다.

  • 현재는 구조가 간단해서 별 문제 없어 보이지만, 회원 정보 등록 이후
    별도의 과정이 더 있다고 생각해보면, 새 트랜잭션B가 생성된 이후에
    트랜잭션 A에서 에러가 발생해도, 독립적인 트랜잭션 B를 제어할 수 없다.

  • 회원 정보 등록 실패시, 이미지 저장도 실패해야 한다는 조건을 만족시킬 수 없다.

2-2-2. Propagation.NESTED 옵션

  • 부모 트랜잭션이 존재하면, 중첩 트랜잭션을 시작한다.

    • 중첩 트랜잭션이란 말 그대로, 트랜잭션 안에 트랜잭션을 만드는 것이다.
    • REQUIRES_NEW 옵션 처럼 독립적인 새 트랜잭션을 만드는 것과는 다르다.
  • REQUIRES_NEW과 다른점은 중첩된 트랜잭션부모 트랜잭션의 커밋과 롤백에는 영향을
    받지만 중첩된 트랜잭션의 커밋과 롤백은 부모 트랜잭션에게 영향을 미치지 않는다.
    (부모 → 자식 단방향)

  • 따라서 회원 정보 등록 실패시, 이미지 저장도 실패해야 한다 , 이미지 저장이 실패해도, 회원 정보 등록은 성공해야 한다 는 두 필수 조건을 만족시킬 수 있다.

  • 단 주의점은 SAVEPOINT를 지정한 시점까지만 롤백이 가능하다.
    즉 SAVEPOINT 기능을 지원하는 DBMS만 사용 가능한 기능이다.

그러나 문제는 문서에 따르면 hibernate는 중첩된 트랜잭션을 지원하지 않는다.
중첩된 트랜잭션을 위해 JDBC 3.0과 SAVEPOINT를 지원하는 DBMS를 이용해야 한다.

JDBC 3.0 사용시 성능문제는 해결하지 못하지만 필수 조건 1,2번을 만족한다.

  • 회원 정보 등록 실패시, 이미지 저장도 실패해야 한다. (필수)
  • 이미지 저장이 실패해도, 회원 정보 등록은 성공해야 한다. (필수)
  • 회원 가입 완료 이후에 이미지를 저장한다. (선택)

2-3. Spring의 ApplicationEventPublisher

ApplicationEventPublisher (Spring Framework 5.3.22 API)

  • Spring에서 어노테이션을 기반으로 이벤트 처리를 지원해준다.

  • 직접 Publisher와 Listener를 커스텀해서 기능을 구현할 수도 있다.

  • 자세한 내용은 구현하면서 살펴보자.

세 방법 중에 2-3(이벤트 리스너)번 방법을 사용하고자 한다.

이유는 2-1번은 굳이 해보지 않아도 가능하다라는걸 알 수 있고
2-1, 2-2 모두 성능문제를 해결할 수 없기 때문이다.

그리고 여기서 이야기한 방법이 전부는 아니며
메시징 큐를 이용하는 방법도 있다. 이부분은 마지막에 다시 이야기 한다.


3. ApplicationEventPublisher로 이벤트 처리하기

3-1. ApplicationEventPublisher 간단한 흐름

  1. 이벤트를 발행하면 ApplicationEventPublisher가 Event를 받는다.

  2. Event를 EventMultiCaster에게 전달한다.

  3. EventMultiCaster는 각 Linster들에게 브로드캐스팅하듯이 Event를 뿌린다.

  4. Linster들은 자신의 파라미터 이벤트 타입과 맞지 않으면 무시하고, 맞으면 실행합니다.

  • 그러면 우리는 전달할 Event와, Listener를 만들어주면 된다.

  • 자세한 과정은 해당 블로그를 참고하자.

3-2. 이벤트 클래스 생성

@Getter
@NoArgsConstructor
public class SavedImageEvent{

    private MultipartFile imageFile;

    private Account account;

    public SavedImageEvent(MultipartFile imageFile, Account account) {
        this.imageFile = imageFile;
        this.account = account;
    }

    public static SavedImageEvent of(MultipartFile imageFile, Account account){
        return new SavedImageEvent(imageFile, account);
    }
}
  • 이벤트는 상태가 변경된 이후에 발생하므로, 클래스 이름은 과거형이어야 한다.

3-3. 이벤트 핸들러 생성

@Component
@RequiredArgsConstructor
public class AccountEventHandler {

    private final AccountImageService accountImageService;

    @EventListener
    public void saveImage(SavedImageEvent event){
        accountImageService.create(event.getImageFile(), event.getAccount());
    }

}
  • 발행한 이벤트를 핸들링할 이벤트 핸들러를 만들었다.

  • 빈으로 등록해주고, @EventListener 어노테이션을 이용하면
    해당 메서드가 Listener 역할을 수행하게 된다.

  • 파라미터에 있는 이벤트로 해당 리스너에게 전달된 이벤트인지 아닌지 구분한다.

3-4. 이벤트 발행

3-4-1. 변경 전 코드

public class AccountService {

		...
    private final AccountImageService accountImageService;

    ...

    public Long signUp(Account account, AccountSignUpRequest request) {

        Account updateAccount = accountMapper.toEntity(request);

        account.signUp(updateAccount);

        if (hasImageFile(signUpRequest.getImageFile())) {
            AccountImage accountImage = accountImageService.create(signUpRequest.getImageFile());
            account.addImage(accountImage);
        }

        return account.getId();
    }

    private boolean hasImageFile(MultipartFile imageFile){
        return imageFile != null && !imageFile.isEmpty();
    }

		...

}
  • AccountImageService를 주입받아서, 이미지를 처리하고 있다.

    • 강한 결합도의 범인(?)이다.
  • AccountImageService에서 S3에 이미지를 및 DB에 이미지를 저장한다.

3-4-2. 변경 후 코드

public class AccountService {

		...
    private final ApplicationEventPublisher eventPublisher;

    ...

		public Long signUp(Account account, AccountSignUpRequest request) {

        Account updateAccount = accountMapper.toEntity(request);

        account.signUp(updateAccount);
        System.out.println("회원 정보 등록"); // 순서를 확인하기 위해 임시로 추가

        if (hasImageFile(request.getImageFile())) {
            eventPublisher.publishEvent(
                    SavedImageEvent.of(request.getImageFile(), account)
            );
        }

        System.out.println("회원 가입 완료"); // 순서를 확인하기 위해 임시로 추가
        return account.getId();
    }

		...
}
  • ApplicationEventPublisher를 주입받아서 사용하면 된다.

  • 그리고 publishEvent()를 통해 원하는 시점(여기서는 회원가입 이후)에 이벤트를 발행하면 된다.

  • 중요한점은 AccountImageService를 더 이상 주입받지 않는다는 점이다. (코드레벨 의존성 분리)

3-5. 정상 동작 확인

  • 테스트 통과, S3에도 저장 완료

3-6. 여전히 남은 문제점

  • 기본적인 이벤트 기반 구현은 완료했다.
    하지만 한 트랜잭션에 묶여있다는 문제점은 여전히 남아있다.

여전히 필수 조건을 만족하지 못한다.

  • 회원 정보 등록 실패시, 이미지 저장도 실패해야 한다. (필수)
  • 이미지 저장이 실패해도, 회원 정보 등록은 성공해야 한다. (필수)
  • 회원 가입 완료 이후에 이미지를 저장한다. (선택)

4. @TransactionalEventListener 사용하기

4-1. @TransactionalEventListener 옵션

  • phase 옵션을 통해 트랜잭션에 따른 이벤트처리를 지원한다.
  1. @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)

    • default 값이며, 트랜잭션이 Commit 됐을 때 이벤트를 실행한다.
  2. @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)

    • 트랜잭션이 Rollback 됐을 때 이벤트를 실행한다.
  3. @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)

    • 트랜잭션이 COMPLETION 됐을 때 이벤트를 실행한다.
    • AFTER_COMMIT 또는 AFTER_ROLLBACK이 발생했을 때를 의미한다.
      위 1,2번을 합친 기능이다.
  4. @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)

    1. 트랜잭션이 COMMIT 되기 전에 이벤트를 실행한다.

4-2. 구현하기

@Component
@RequiredArgsConstructor
public class SignedUpEventHandler {

    private final AccountImageService accountImageService;

	@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void saveImage(SignedUpEvent event){
        accountImageService.create(event.getImageFile(), event.getAccount());
    }

}
  • 회원가입 성공시(트랜잭션 성공시) 이미지 이벤트를 생성하도록 디폴트 phase를 사용했다.

    • 생략 가능하지만 나에게는 생소한 옵션이므로 명시했다.
  • 기존 코드에서 바뀐부분은 핸들러에서의 @TransactionalEventListener 어노테이션 하나이다.

  • 트랜잭션 A가 commit 되면(회원 정보 등록하는데 문제가 없으면) 이벤트를 발행하고
    새 트랜잭션B를 열어서 이벤트를 처리하게 된다.

4-3 [검증] 정상 동작 확인

  • 테스트코드 실행시 이벤트가 정상적으로 발행되지 않아서, 직접 애플리케이션을 실행하고
    API를 호출해서 테스트했다..

  • 어쨋든 회원도 잘 저장되고, S3에 이미지도 잘 저장됐다.

  • 그런데 문제는 DB에 이미지 정보가 저장 되지 않았다. (insert 쿼리가 발생하지 않았다.)

4-4. @TransactionalEventListener 사용시 주의점

  • 트랜잭션이 commit되면 당연히 사라지는줄 알았다.
    그래서 4-2 그림처럼 동작할 것을 예상했지만, 실제로는 위 그림처럼 기존 트랜잭션에 참여하게 된다.

  • commit된 트랜잭션을 다시 commit해서 update, insert, delete등 행위를 하는것은 불가능하다.
    트랜잭션이 commit되어야 DB로 쿼리를 날릴텐데, 이미 commit을 수행했기 때문이다.
    하지만 `commit된 트랜잭션`에 참여하여 select 행위는 가능하다.

  • 그래서 회원 정보 등록하는 과정에서 트랜잭션을 commit했고,
    이후 commit된 트랜잭션`에 참여하여 이미지를 저장 하려고 하기 때문에 DB에 저장이 되지 않는 것이다.

4-4-1. 해결

  • 해결 방법은 간단하다.

  • 새 트랜잭션을 만들어서 이벤트(이미지 저장)를 처리할 수 있도록
    @Transactional(propagation = Propagation.REQUIRES_NEW) 를 추가해주면 된다.

4-5. [검증] 조건1 - 회원가입 실패 → 이미지도 등록 실패

@Test
@DisplayName("회원가입 - 추가 정보 입력 (이미지 포함)")
void signUpAddInfoWithImage() {
    //given
    AccountSignUpRequest req = new AccountSignUpRequest();
    req.setAge(null);
    req.setCity(null);
    req.setDetail(null);
    req.setNickname(null);
    req.setSex(null);

	//when
    Long accountId = accountService.signUp(accountWithoutToken, req);
		...
}

---

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveImage(SignedUpEvent event){
    System.out.println("==이미지 저장 이벤트 실행==");
    accountImageService.create(event.getImageFile(), event.getAccount());
}
  • 요청값에 null 넣어서, 회원가입 로직에서 NPE를 발생시킬 것이다.

  • 이 때 이미지도 등록이 실패하는지, 즉 이벤트를 실행시키는지 확인해보자.

    • 로그에 ==이미지 저장 이벤트 실행== 이 찍히면 안된다.

  • account 조회 이후, NPE가 발생했고 당연히 이벤트는 발행 및 실행되지 않았다.

4-6. [검증] 조건2 - 이미지 등록 실패 → 회원 정보 등록은 성공

public void create(MultipartFile imageFile, Account account) {

    System.out.println(2/0);

    // s3에 이미지 등록
		// DB에 이미지 정보 저장
}
  • 이미지를 저장하는 로직에서 고의로 예외를 발생시켰다. (숫자를 0으로 나누기..)

  • 회원정보는 이미 저장하고, 별도의 트랜잭션에서 이미지를 등록하면서 실패하기 때문에
    회원정보는 잘 저장되야 한다.

  • 회원 정보를 저장하는 update 쿼리가 날라가서, 잘 저장 됐다.

    • 여기서 insert 쿼리가 아닌 update 쿼리인 이유는, 앱의 회원 가입 플로우는
      소셜 로그인(여기서 회원 생성 insert 쿼리) → 회원가입(회원 정보 저장, 이미지 등록 update 쿼리) 형태이기 때문이다.
  • 그리고 맨 아래 보면 이벤트는 실행 됐지만, 예외가 발생해 rollback 된걸 볼 수 있다.
    의도대로 S3와 DB에 모두 이미지는 저장되지 않았다.

성능 문제는 해결하지 못하지만 필수 조건을 모두 만족한다.

  • 회원 정보 등록 실패시, 이미지 저장도 실패해야 한다. (필수)
  • 이미지 저장이 실패해도, 회원 정보 등록은 성공해야 한다. (필수)
  • 회원 가입 완료 이후에 이미지를 저장한다. (선택)

5. 성능 문제 해결하기

의존성을 제거해 조건을 모두 만족했지만, 처음에 이야기한 성능문제는 여전히 남아있다.

@TransactionalEventListener를 사용하면 트랜잭션이 분리돼서 따로 처리되니까

성능문제가 해결된거 아닌가? 라고 착각할 수 있다. (내가 그랬다..)

하지만 스프링에서 하나의 요청은 하나의 쓰레드가 처리한다. 중간에 새 트랜잭션이 생성되어도

새 쓰레드가 처리하는것이 아닌, 기존 쓰레드가 계속 처리한다. 즉 동기적으로 처리되고 있다는

이야기다. 사용자는 여전히 회원가입하는데 이미지를 저장하는 시간까지 같이 기다려야 한다.

5-1. 정말 같은 Thread가 처리할까?

  • 회원 정보를 저장할 때의 Thread와, 이미지를 저장할 때의 Thread가 같은걸 볼 수 있다.

5-2. 비동기로 처리하기

...
@EnableAsync // 추가
public class PetApplication {

	public static void main(String[] args) {
		SpringApplication.run(PetApplication.class, args);
	}

}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Async
public void saveImage(SignedUpEvent event){
    System.out.println("==이벤트 실행==");
    accountImageService.create(event.getImageFile(), event.getAccount());
    System.out.println("==이벤트 완료==");
}
  • 역시 스프링은 매우 간단하게 어노테이션으로 지원해준다.

  • 비동기 처리를 위한 @EnableAsync 어노테이션을 메인 클래스에 추가하고
    비동기로 동작시키고자 하는 메서드에 @Async어노테이션만 추가해주면 된다.

  • @Async 사용시 주의할점이 있다.

    1. 메서드는 반환값을 가지면 안된다.

      • 서로 다른 쓰레드가 처리하므로 반환값을 처리할 수 없어서 그런가?
    2. private 메서드이면 안된다.

    3. 내부 호출이 발생하면 안된다.

      • 2,3번은 @Async가 AOP로 동작하기 때문이다.
        AOP를 이용해 로깅 처리할때와 같은 주의사항이다.
    4. 별도로 설정하지 않는다면 매번 새로운 Thread를 생성한다

      • 수많은 요청이 발생하는 웹 애플리케이션 특성상 치명적이다.
        아래처럼 Thread-pool을 이용하도록 꼭 설정해주자.

5-3. Thread-pool 설정

@Configuration
@EnableAsync
public class AsyncThreadConfiguration {

    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5); // thread-pool에 항상 살아있는 thread 최소 개수
        executor.setMaxPoolSize(5); // thread-pool에서 사용 가능한 최대 thread 개수
        executor.setQueueCapacity(500); // thread-pool에서 사용할 최대 queue 크기
        executor.setThreadNamePrefix("ToGaether");
        executor.initialize();
        return executor;
    }

}
  • Spring 가이드에 나온 코드를 거의 그대로 가져왔다.

  • 비동기로 처리할 때 마다, 새로운 Thread를 생성하지 않고 Thread-pool에 있는 Thread를 가져와서 사용하고, 처리하고 나면 다시 Thread-pool에 반납하게 된다.

  • 내부적으로 어떻게 돌아가는지에 대한 글은 해당 블로그에 자세히 정리되어있다.

spring:
  task:
    execution:
      pool:
        core-size: 5
        max-size: 5
  • Spring Boot 2.0 이상인 경우 yaml 파일에서도 설정할 수 있다.

5-4. [검증] 정말 다른 Thread로 처리할까?

  • 회원 정보를 저장할 때의 Thread와, 이미지를 저장할 때의 Thread가 다른걸 볼 수 있다.

원했던 모든 조건을 만족했다.

  • 회원 정보 등록 실패시, 이미지 저장도 실패해야 한다. (필수)
  • 이미지 저장이 실패해도, 회원 정보 등록은 성공해야 한다. (필수)
  • 회원 가입 완료 이후에 이미지를 저장한다. (선택)

6. 마치면서

6-1. ApplicationEventPublisher의 한계

  • 스프링이 제공해주는 이벤트 기능으로, 스프링 Bean으로 동작한다.
    그렇다는 것은 외부 시스템과의 연동은 불가능하다는 점이다.

  • 또한 다른 서버로 이벤트를 전달 할 수 없으므로
    MSA 구조처럼 각 도메인 서버가 분리되어 있는 경우에도 당연히 사용 불가능하다.

    • 여기서 착각하면 안되는 점은 서버가 여러대인 경우에 사용 불가능 하다고 생각하면
      안된다. 어차피 요청을 받은 한 서버에서만 ApplicationEventPublisher로 처리되기
      때문이다. 예로 주문, 알림톡 서버가 분리되어 있는 경우에 사용 불가능 하다는 점을
      주의하자.
  • 이런 경우에는 Kafka, RabbitMQ와 같은 메시징 큐 소프트웨어나, AWS SQS 같은 것들을 이용해야 한다.

6-2. 비동기 이벤트 처리하는 다양한 방법

  • 스프링이 제공해주는 @Async 어노테이션 이외에 여러 방법들이 존재한다.

  • 이벤트를 비동기로 처리할 수 있는 상황은 A하면 이어서 B를 해라의 요구사항을
    A하면 최대 언제까지 B를 해라 로 바꿀 수 있다면, 비동기로 처리할 수 있다.

  • 비동기 이벤트 처리 방법은 다양하다.

    1. 로컬 핸들러를 비동기로 실행
    2. 메시지 큐 사용
    3. 이벤트 저장소와 이벤트 포워더 사용하기
    4. 이벤트 저장소와 이벤트 제공 API 사용하기
  • DDD Start! 라는 책에 나오는 내용으로, 자세히 알고싶다면 해당 책을 참고하자.

퇴근하고 조금씩 하다보니 흐름도 끊기고, 글을 쓰면서 하다보니 며칠이 걸렸다..
마침 오전에 일이 있어서 연차 사용한김에 시간이 남아서 마무리 했다.

강의를 보며 RabbitMQ를 간단하게 따라해본적은 있었지만,
나름 제대로(?) 이벤트 처리를 처음 해보았다.

다음에는 AWS SQS, Kafka, RabbitMQ 등과 같은 것들을 이용해 이벤트를 처리해보는게 목표이다.


Ref

0개의 댓글