240701 Spring 심화 - 과제 해설 세션, Event

노재원·2024년 7월 1일
0

내일배움캠프

목록 보기
72/90
post-custom-banner

과제 해설 세션

과제를 마무리하고 제출한 후 각 과제에 대한 해설 세션이 있었다. 이번엔 중요하지 않고 자주 했던 부분들을 제외하고 새로 공부하는 부분에 대해서만 집중했는데도 고생이었다.

개선 과제, 복습 과제 모두 진행됐고 개선 과제의 경우 과제에 해당하는 부분은 영상으로 이미 촬영되어 있고 라이브는 사전에 어려운 부분에 대한 질문을 받은 내용에서 진행됐다.

과제 해설

  • AWS 서비스를 어플리케이션에 구현할 때 AWS Client를 사용하면 아주 친절하고 쉽게 서비스 이용이 가능하다.
  • txt 파일의 타입만 변환해서 업로드하는 경우같은 걸 방지하려면 Apache Tika등으로 Meta data를 확인할 수 있다.
  • AOP의 프록시 방식은 크게 JDK dynamic 방식과 CGLib 방식이 있다고 한다.
    • JDK dynamic은 원본 객체를 건드리지 않고 Proxy가 AOP와 해당 Interface를 생성자로 주입받아 실제 구현체의 메소드를 호출하기 전후로 AOP를 적용하는 방법이다. 런타임에 해당 인터페이스를 구현하는 프록시 객체가 생성되고 원래 인터페이스를 호출하던 클라이언트 객체는 같은 인터페이스를 구현했을 프록시 객체를 호출하게 바뀌면서 인터셉트 하는 방식으로 인터페이스가 없으면 사용할 수 없다.
    • CGLib은 인터페이스가 아닌 타깃이 클래스인 경우에 아예 해당 클래스를 상속받아 사용하고 현재 Spring boot의 기본 설정값이기도 하다. 아예 바이트코드를 조작하여 프록시 클래스를 생성하는 방법이다. 타겟의 서브 클래스를 생성하는 방식이라 Open / Protected 메소드만 가로챌 수 있고 클라이언트는 프록시 클래스를 바라보게 조작된다.
      • 이 방식을 리모콘이 없으면 조작할 수 없는 TV vs TV 자체의 버튼으로 조작할 수 있는 TV로 적용 방식과 메모리 사용량을 비유하기도 했다.
  • BDD는 TDD에서 파생된 개념으로 테스트 코드에 들어가는 리소스를 줄이고 유저 시나리오의 흐름에 친화적이게 바꿔서 작성하는 테스트 코드다.
    • 로직의 시나리오, 사용자 측면의 이해는 BDD가 이해하기 쉽지만 모듈의 안정성은 TDD가 더 보장을 잘한다.
  • QueryDSL을 사용하려면 SQL을 잘 알아야 하고 SQL문이 어떻게 나가는지를 알기 위해 추상화된 인터페이스의 구현체를 파악해두는게 좋다.

챌린지반 - EDA, Spring Event

이번 주차는 Event 관련한 주제를 다뤘다.

Event Driven Architecture

아키텍쳐는 범위를 어떻게 생각하느냐에 따라 사용하는 기술과 생각해야하는 부분도 달라진다.

더 큰 규모의 EDA도 가능하지만 강의는 가장 작은 범위인 Spring application 내에서의 EDA를 다뤘다.

EDA의 최종 목적은 Event를 이용한 비동기 통신이다.

EDA의 목적

  • 서비스간의 결합도를 낮출 수 있다. (A와 B가 있다면 둘은 서로 몰라도 된다.)
    • 서비스간 변경의 전파를 막는다
    • 두 서비스의 관심사가 명확히 분리된다
    • 장애의 전파를 막는다 (A -> B로 호출시 B에 장애가 난 상태면 A에도 타임아웃등의 에러가 날 수 있다.)
    • 각 서비스의 독립적인 확장(Scale-out)이 가능하다
  • 높은 확장성을 가진다. (Subscribe만 추가하면 여러 서비스로 확장이 된다.)

이벤트를 처리하는 Event Queue는 Kafka, Redis Pub/Sub, AWS SNS/SQS, Spring event(단일 Application만) 등이 역할을 수행할 수 있다.

Event를 다룰때 생각할 포인트

  • Event는 중복으로 발행, 구독이 가능하다
  • Event의 순서가 바뀔 수 있다.
  • Event가 누락될 수 있다.

Spring event에서 위 2개는 Spring event에서는 크게 와닿을 내용은 아니다.

반드시 기억해야 할 키워드는 멱등성이다.

멱등성이란 동일한 연산을 여러 번 수행해도 결과가 달라지지 않는 성질

Event의 단점

  • 전체적인 시스템 복잡도를 높이고 흐름을 파악하기 어렵다.
    • 코드를 읽다가 갑자기 Event를 발행하고 흐름을 끊어버리면 파악하기 어려워진다.
  • 발행되는 Event의 포맷이 결정되고 난 후에 변경이 어렵다.
    • Publisher 말고 Event Queue, Subscriber가 모두 변경되어야 한다.
  • Event 발행/구독을 위한 별도의 인프라가 필요하다.
  • Event 중복, 순서등 시스템적으로 고려할 부분이 많아진다.

Spring Event

하나의 Spring application에서 Event를 이용하기 쉽게 준다.

Spring Event가 Event Queue의 역할을 맡아주고 Publisher, Subscriber는 객체가 된다. 이렇게 해서 손쉽게 EDA의 구조를 녹여낼 수 있다. 추가로 IntelliJ의 지원까지 받을 수 있다.

ApplicationEventPublisher

Bean으로 등록한 ApplicationEventPublisher의 .publish에 Event object를 담아서 발행하면 Publisher의 역할이 끝난다.

Event Object

Event의 내용이 들어가는 객체로 Publisher가 발행하고 Subscriber가 구독한다.

  • Event는 과거에 일어난 행위에 대한 결과라고 할 수 있다. (이전에 발행되었다)
    • 누가 이벤트를 받을지 고려하지 않고 순수하게 Publisher에 맞춰져야 한다.
    • 이런 사양에 따라 MemberRegistered 처럼 과거형으로 작성되는 것이 일반적이다.

Spring 4.2부터 POJO도 Event object로 사용이 가능해졌다.

EventListener

@EventListener를 이용하면 쉽게 구현할 수 있다.
@EventListener fun listener(event: Type) 의 형태가 된다.

구현 자체는 매우 쉬운 편이니 결합도, 장애의 전파, 관심사의 분리 측면에서 어디에 써야할지 신경쓰는 것에 집중함이 좋다.

Spring event의 동기 비동기

Spring Event는 이 구조가 특이해서 기본적으로 동기(Sync) 로 동작한다.

위에서 다룬 내용대로면 비동기로 동작하는 걸 기대했겠지만 Spring Event는 기본적으로 동기기 때문에 @Async 등을 활용해서 직접 해당 메소드가 비동기로 동작되도록 수정해야 한다.

Transaction 범위

위에서 다룬 동기식의 다른 의미는 같은 Thread에서 동작하게 된다는 의미이고 Event를 Publish하는 메소드에 Transaction이 걸려있다면 Subscriber 메소드도 하나의 트랜잭션이 된다.

그렇기에 Event Listener의 로직에서 에러가 나면 롤백 이 되기 때문에 주의해야 한다.

Event Listener에서 격리된 트랜잭션을 처리하고 싶다면 @Transactional(propagation = Propagation.REQUEST_NEW) 전파 수준을 고려해볼 수 있다.

단, @Async를 통해 비동기로 처리했다면 이 이슈를 회피할 수 있는데 우리가 다루는 Transaction은 ThreadLocal을 기반으로 동작하게 되고 @Async를 통해 쓰레드가 분리되면 동일한 Transaction으로 묶을 수 없게 된다.

Transaction Synchronization

EventLister와 유사한 어노테이션으로 Spring Transaction에서 트랜잭션 동기화를 위한 Synchronization 이라는 기능을 제공한다.

이를 이용한 Listener로 Transaction을 모니터링 하고 있다가 특정 시점에 호출되는 Callback 메소드다.

이 또한 ThreadLocal 기반으로 동작하기 때문에 비동기를 다루고 있었다면 사용에 주의해야한다.

TransactionSynchronizationManager.registerSynchronization(
object: TransactionSynchronization {
 // override afterCommit /* etc.. */
})

TransactionSynchronization 기능은 afterCommit, afterCompletion, beforeCommit, beforeCompletion 등의 생명 주기 시점을 제공한다.

Event와 결합해 응용

이를 Event와 결합해서 Publish한 Transaction이 Commit 된 후 Event Listener 동작하기 같은 요구사항을 구현할 수 있다.

@EventListener
fun listener(event: SomethingEvent) {
	TransactionSynchronizationManager.registerSynchronization(
	object: TransactionSynchronization {
	 	// override afterCommit /* etc.. */
	})
}

이런 구조를 반복해서 쓰기 복잡하기 때문에 @TransactionalEventListener 가 등장했다.

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
fun listener(event: SomethingEvent) { /*etc*/ }

Zero-payload

Event의 단점을 보완해주는 방식으로 Event를 발행 / 구독하는 과정에서 아무런 정보도 없이 Event를 Callback처럼 사용해서 실제로 구독자가 해당 시점에 API를 호출하게 만드는 방식이다.

복잡도는 동일하지만 Payload의 변경이 일어날 때 전파를 최소화할 수 있는 방법이라는 생각이 든다.

post-custom-banner

0개의 댓글