Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

이벤트에 Transactional Outbox Pattern 적용 #757

Closed
wants to merge 5 commits into from

Conversation

mcodnjs
Copy link
Collaborator

@mcodnjs mcodnjs commented Nov 22, 2023

📄 Summary

#754

🙋🏻 More

관련된 테스트와 TripDeleteEvent 외 다른 도메인 이벤트는 아직 적용 전입니다 ..
구조적으로 먼저 리뷰 받고 다음 이벤트들에 대해서 적용하겠습니다 !!
궁금한거 있으시면 언제든 @hgo641 에게 문의하시면 됩니다.

기존에 이벤트 호출 로직(TripService.deleteTrip())에서 이벤트를 바로 발행하고 있었습니다.
이벤트 발행 로직과 이벤트를 같은 트랜잭션에 묶어서 원자성을 지키기 위해 트랜잭셔널 아웃 박스 패턴을 적용해봤습니다!
(트랜잭셔널 아웃 박스 패턴에 대한 설명은 링크를 참고해주세요)

  • 이벤트 호출 로직(ex. TripService.deleteTrip())에서 아웃 박스를 저장
    • 아웃 박스 : DB에 저장될 이벤트 정보
    • Id, EventType, Payload, BaseEntity
  • 2초마다 아웃 박스를 조회해 EventQueue에 저장
  • 2초마다 EventQueue에 있는 아웃박스를 꺼내 이벤트 실행
    • 이벤트가 성공적으로 수행되었다면, AOP를 사용해 아웃박스를 삭제한다.
    • 이벤트 수행중 예외가 발생했다면, 삭제하지 않는다.

Copy link

📝 Jacoco Test Coverage

Total Project Coverage 77.52%
File Coverage [92.65%]
EventType.java 100% 🍏
DeleteEventListener.java 100% 🍏
TripController.java 100% 🍏
TripService.java 97.72% 🍏
Outbox.java 89.66% 🍏
EventQueue.java 80.95% 🍏
CompletedEventChecker.java 77.5%
Event.java 71.43%
PayloadToEventMapper.java 66.67%
TripDeleteEvent.java 47.83%
TripDeleteEventMapper.java 40%

Copy link
Member

@jjongwa jjongwa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

멋지네요 라온 & 홍고

추가적으로 몇가지 궁금한게 있어 질문 남깁니다.

우선 해당 작업이 왜 필요했는지, 기존의 어느 부분에서 크리티컬한 문제점이 있어 개선을 하게 되었는지 설명이 조금 부족한것 같아요.!(연결된 이슈에도 설명은 딱히 없더라구요..)

그리고 이벤트 발행 로직과 이벤트를 같은 트랜잭션에 묶어서 원자성을 지키기 위해 이 부분도 잘 이해가 가지 않습니다.! 이벤트 발행과 해당 이벤트를 같은 트랜잭션에 묶는다는 건가요? 원자성이란 키워드를 어떤 의미에서 썼는지는 대충 이해가 가지만 그게 정말 원자성이 맞는지는 잘 모르겠습니다.! 이 부분도 조금 더 설명해 주시면 좋을 것 같아요..!

현재 저희의 아키텍처 구조에서 이벤트 큐를 사용한 아웃박스 패턴이 최선의 방법이었는지, 다른 선택지는 어떤게 있었고, 각각 어떤 단점이 있어 반려하게 되었는지 궁금합니다!

지금 저희는 모듈 하나에 모든 로직이 구현되어 있는데요, 그 말은 이벤트 발행과 처리를 모두 같은 서버에서 처리하고 있다는 의미입니다.
이러한 상황에서 굳이 아웃박스라는 테이블을 만들고, 이벤트 큐를 만들어서 주기적으로 쿼리를 실행해 이벤트를 가져와 해당 로직을 실행해야 할 이유가 있을까요?
트랜잭션 아웃박스 패턴은 이벤트의 발행과 실행 로직이 서로 다른 서버에 있고, 해당 서버 간의 통신 중 이벤트가 유실될 경우를 대비한 방안이라고 이해하고 있습니다.(분산 아키텍처 -> 더 나아가면 MSA 환경이겠죠..!)

때문에 같은 서버에서 처리하는 현 상황에서는 큰 효과가 없이 그저

  1. delete 로직에서 일괄적으로 DB 쿼리가 2번씩 추가된다.
  2. 이벤트에 대한 처리가 (최대)2초 뒤에 실행된다

는 단점밖에 생기지 않을 것 같아요.

그리고 굳이 이렇게 DB와 연동하지 않고 인메모리에 큐를 만들어 스케줄러로 처리해도 될 것 같구요.!
아니면 접근속도가 더 빠른 레디스에 저장하는 것도 방법이 될 수 있을 것 같습니다.
이렇게 다양한 방안이 있을텐데 굳이 DB에 데이터를 추가하고 삭제하는 방식으로 구현한 이유가 궁금합니다.!

그리고 저희는 다중 서버 구조인데 이렇게 아웃박스 테이블을 만들고 이벤트 큐에서 주기적으로 findAll을 해오게 되면 각각의 서버에서 동일한 이벤트 처리를 하게 되는 상황도 오지 않을까 싶은데 이 부분은 어떻게 생각하시나요?(제가 코드를 완벽하게 이해하지 못했을 수도 있습니다..!)

이벤트 큐가 물론 현업에서 많이 쓰이고 학습해보고 싶은 로직임은 이해합니다.

하지만 지금 저희 서비스의 입장에서 봤을땐 해당 로직의 추가가 오히려 성능상 마이너스 요소가 된다고 생각하는 입장이기에 dev로의 머지는 반대합니다.!

조금만 더 의견 나눠보고 디벨롭해서 반영하는 쪽으로 진행해 보면 좋겠습니당


@AfterReturning("@annotation(org.springframework.transaction.event.TransactionalEventListener)")
public void check(final JoinPoint joinPoint) {
System.out.println("CompletedEventChecker.check");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chk

final Outbox outbox = queue.poll();
final OutboxToEventMapper outboxToEventMapper = mappers.stream().filter(mapper -> mapper.is(outbox.getEventType()))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("이벤트 타입에 해당하는 매퍼가 없어요오"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이대로 놔둘건가요?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

레전드

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exception code 하나 만드시죠.!


@Component
@RequiredArgsConstructor
public class EventQueue {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오홍 신기합니다!!!!

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@SQLDelete(sql = "UPDATE outbox SET status = 'DELETED' WHERE id = ?")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아웃박스 테이블도 soft delete를 하는 이유가 있을까요? 본래 아웃박스의 의도와는 조금 달라질 수도 있다는 생각이 들어서요.!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

status가 필요해서 재활용했는데, baseEntity의 status대신 아웃박스의 Status를 별도로 만드는 게 좋을까요?!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 그럼 그대로 가도 될 듯 합니다

publisher.publishEvent(new TripDeleteEvent(tripId));

final String payload = PayloadToEventMapper.toJson(new TripDeleteEvent(tripId));
outboxRepository.save(new Outbox(EventType.TRIP_DELETE, payload));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 되면 결국 삭제 로직에서 DB 접근이 추가적으로 필요한 상황인데 트랜잭션 보장을 위해서 해당 방법이 가장 합리적일까요?

이 외에 다른 방식을 고민해 본게 있을까요?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일단 외부 db에 저장한다는 것은 필수 조건으로 깔고 갔습니다! 아래 댓글 참고오오오

Comment on lines +22 to +25
@Scheduled(fixedRate = 2000)
public void offerSavedEvent() {
queue.addAll(outboxRepository.findAll());
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. findAll의 호출 기준을 2초로 둔 이유가 궁금합니다!

  2. 지금 저희는 다중 서버인데 그럼 각 서버마다 큐를 운영하는건가요?
    그러면 큐에서 빼면서 호출하는 로직도 각각 서버마다 진행되는건가요?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아래 댓글에 한 번에 써둠!!!

Comment on lines +7 to +8
boolean is(EventType type);
T toEvent(Outbox outbox) throws JsonProcessingException;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 final 못붙이나요

@hgo641
Copy link
Collaborator

hgo641 commented Nov 22, 2023

@jjongwa
장문의 코멘트 굉장합니다 디노!!!!!!!!!!!!!! 설명이 부족했던 것 같아서 추가합니다!!!!!!!!!!!!!!!!!! 많은 관심 감사합니다!!!!!!!!!!!!!!!!

원자성을 지킨다는 것이 무슨 의미인가?

그리고 이벤트 발행 로직과 이벤트를 같은 트랜잭션에 묶어서 원자성을 지키기 위해 이 부분도 잘 이해가 가지 않습니다.! 이벤트 발행과 해당 이벤트를 같은 트랜잭션에 묶는다는 건가요? 원자성이란 키워드를 어떤 의미에서 썼는지는 대충 이해가 가지만 그게 정말 원자성이 맞는지는 잘 모르겠습니다.! 이 부분도 조금 더 설명해 주시면 좋을 것 같아요..!

이벤트 발행과 해당 이벤트를 같은 트랜잭션에 묶는다는 건가요? <- 맞습니다!!
이벤트 발행 로직이 deleteTrip()이고 이벤트가 TripDeleteEvent일 때를 예시로 가정해보겠습니다!
deleteTrip()에서 사용되는 쿼리와 이벤트를 발행하는 로직이 같은 트랜잭션으로 묶여있지 않으면, 한 쪽만 수행이 되는 일이 생길 수 있습니다.

  1. deleteTrip()호출로 인해 Trip은 삭제되었으나, 이벤트는 발행되지 않음.
  2. Trip은 모종의 예외로 인해 삭제되지 않았으나, 이벤트는 발행됨.

deleteTrip()이 제대로 수행되었다면, Trip도 삭제되고, 이벤트도 제대로 발행되어야한다는 의미에서 원자성이라는 단어를 언급했습니다.

아웃박스 테이블을 만든 이유

물론 2번의 경우는 Trip을 삭제하는 코드 다음에 이벤트를 발행하기에 발생하지 않긴 합니다.ㅎㅎ
저희는 1번에 중심을 두고 진행해보았습니다... deleteTrip()이 커밋된 후, 비동기로 이벤트를 처리하는 도중에 예외가 발생하거나, 서버가 다운된다면?
이벤트가 처리되지 않고, 영원히 사라지게 됩니다.
그래서 이벤트를 외부 db에 저장할 필요성을 느꼈습니다! 인메모리 큐는 고려 대상에서 아예 제외했습니다!

  • mysql대신 redis를 안되는가?

    • 이 태스크를 수료전에 피드백 받는 게 목표였습니다! 그래서 가장 빠르게 구현할 수 있는 mysql을 사용했습니다. redis로 구현하기에는 학습 비용이 추가로 들고, redis또한 결국엔 인메모리이기 때문에 이벤트를 안전하게 저장할 수 있는 방식인지 의문이 들었습니다! 물론 redis가 다운될 때를 대비하여 백업정책이 있을 것 같지만... 학습 비용을 줄이고 수료전까지 기능을 구현하고 가고 싶었습니다...
  • nosql은 안되는가?
    각 이벤트가 가지고 있는 필드가 다양하기 때문에 nosql을 활용하는 것을 고민하였으나 학습 비용으로 인해 이후 고려해볼 이슈로 남겨둠

delete 로직에서 일괄적으로 DB 쿼리가 2번씩 추가된다.

  • 쿼리가 적을수록 이득인 것은 동의하지만, delete로직에서 일괄적으로 DB 쿼리가 2번씩 추가되는 것이 큰 비용이라고 생각되지 않았습니다. deleteTrip()보다 더 많은 쿼리가 발생하는 커뮤니티 페이지 조회 API를 200명이 동시에 요청하는 부하테스트를 했을 때 안정적이었기 때문

이벤트에 대한 처리가 (최대)2초 뒤에 실행된다

  • 이건 단점인지 모르겠습니다! 이벤트 로직이 빨리 처리되는 것이 중요할까요? 유저에게 영향을 주지 않는 로직이니 근시일내로 수행된다. 라는 보장만 있으면 괜찮다고 생각했습니다!

각각의 서버에서 동일한 이벤트를 처리하게 되는 것 아닌가?

그리고 저희는 다중 서버 구조인데 이렇게 아웃박스 테이블을 만들고 이벤트 큐에서 주기적으로 findAll을 해오게 되면 각각의 서버에서 동일한 이벤트 처리를 하게 되는 상황도 오지 않을까 싶은데 이 부분은 어떻게 생각하시나요?(제가 코드를 완벽하게 이해하지 못했을 수도 있습니다..!)

맞습니다!!! 일단, 이벤트가 delete만 있기에 이벤트가 두 번 처리될 때 치명적인 오류가 발생하지는 않아 이대로 구현했습니다!
물론 이벤트 쿼리가 두 배로 날아간다는 단점이 있습니다!!! 그런데 실행되어야 할 이벤트가 증발하는 것보다는 두 번씩 수행하더라도 db에 저장하고 스케줄링 돌리는 게 좋을 것 같아서 이렇게 했는데!!! 마음에 걸리시나요? 십분이해합니다!!!

그렇다면 이벤트 테이블에 파티셔닝을 적용하고, 해시값을 기준으로 적합한 테이블에 이벤트를 저장하는 방식은 어떨까요? 각 서버가 조회해올 테이블을 지정해준다면, 이벤트가 두 번씩 처리되는 것을 막을 수 있을 것 같습니다! 너무 오바인가요?! 하하하!!!

정말 트랜잭셔널 아웃박스 패턴이 최선이었는가? 다른 방식은 없었나?

음... 일단 delete이벤트만 있는 현재 상태에서는 이벤트 유실을 막기위해 트랜잭셔널 아웃박스 패턴 대신, 주기적으로 스케줄링을 돌리며 delete가 되지 않은 엔티티를 찾아 지우는 방식이 떠올랐었는데요!
ex) TripDeleteEvent유실을 막기 위해, 주기적으로 삭제된 Trip과 연관된 Usable한 Daylog, Item, Place, Image, Expense, TripCity 엔티티를 찾아 삭제한다.

예시만 봐도... 너무 복잡하죠! Trip과 연관된 객체가 너무 많을 뿐더러 인덱스가 설정되지 않은 Status를 기준으로 조회를 한다면 쿼리 비용이 많이 들거라고 생각했습니다. 전 이 방법과 트랜잭셔널 아웃박스 패턴외에 이벤트 유실을 막기 위한 방법이 떠오르지 않았긴합니다ㅠ.ㅠ

만약 현재 방법보다 더 좋은 방법이 있다면 바꿀 의향 10000% 입니다!

@hgo641
Copy link
Collaborator

hgo641 commented Nov 22, 2023

디노가 맛깔나게 반박해줄것같아 설레네요1! 기대하겠습니다!!!!!!!!!!!!1

@jjongwa
Copy link
Member

jjongwa commented Nov 25, 2023

빨리 답글을 적었어야 하는데 제가 너무 늦게 왔죠..죄송합니다ㅡㅜ(예비군도 했고 저희 수료식도 있었으니까 봐주시죠 하하)

반박?추가설명? 차근차근 들어가 보겠습니다..

1. deleteTrip()호출로 인해 Trip은 삭제되었으나, 이벤트는 발행되지 않음.
2. Trip은 모종의 예외로 인해 삭제되지 않았으나, 이벤트는 발행됨.

이렇게 두 가지 경우를 예를 들어 설명해 주셨는데요.!
그 중 물론 2번의 경우는 Trip을 삭제하는 코드 다음에 이벤트를 발행하기에 발생하지 않긴 합니다.ㅎㅎ 라고 해주셨고, 1번 경우에 중심을 두고 진행했다고 했지만,
제가 궁금한 부분은 지금 행록의 서비스 구조에서 1번 상황이 일어날 수 있는가? 입니다..!
전 1,2 모두 일어나지 않는 일이라고 생각하거든요.
현재 저희는 하나의 모듈에 모든 기능이 구현되어 있습니다. 과연 이벤트가 발행되지 않는 경우가 있을까요?

Transactional Outbox Pattern (이하 top라 하겠습니다..) 도입에는 데이터 분산 처리 환경정합성 보정 이 두 키워드가 전제되어 있어야 한다고 생각합니다.
다시 말하면, 홍고가 앞에서 언급했던 1번 상황은
1. deleteTrip()호출로 인해 Trip은 삭제되었으나, 이벤트는 발행되지 않음. 이 아니라
1. deleteTrip()호출로 인해 Trip이 삭제되고, 이벤트도 발행되었으나, 이벤트 처리기에서 해당 요청을 전달받지 못함
의 상황을 가정했을 때 의미가 있다는 겁니다.
이벤트는 항상 발행되지만, 발행하는 모듈과 처리하는 모듈이 달라 해당 이벤트를 전송하는 시점 중에 문제가 생겨 요청이 유실되는 상황을 방지하기 위한 방안 중 하나가 top니까요.!

자 그럼 이제 deleteTrip()이 커밋된 후, 비동기로 이벤트를 처리하는 도중에 예외가 발생하거나, 서버가 다운된다면? 의 상황에 대해서도 생각해 보겠습니다.

먼저,

  1. 비동기로 이벤트를 처리하는 도중에 예외가 발생
    catch로 받아 재실행 or 예외가 발생한 해당 시점에 DB 테이블에 따로 값 저장의 경우로 해결할 수 있습니다. 굳이 아웃박스 패턴을 적용해 데이터를 생성 -> 삭제 할 필요가 없다고 생각합니다.

  2. 서버가 다운된다면?
    이 부분은 이제 제가 이전에 질문했던 부분을 다시 물어보고 싶습니다. 왜 findAll의 호출 기준을 2초로 두었나요? 다시 말하면 왜 근시일 내에 해당 로직이 수행되어야 하나요?
    저희가 delteTrip에서 몇몇 삭제 로직을 이벤트 처리한 이유 중 하나가 바로 해당 로직이 수행되지 않아도 사용자 입장에서는 아무런 변화가 없다는 것입니다.
    그래서 결국 비동기로 이벤트가 처리되고 있는 상황에 서버가 다운되는 굉장히 희박한 상황만을 위해 의존성을 추가하고 11개의 클래스를 추가로 생성하면서까지 top를 적용해야 하는건가 싶은 생각이 듭니다.
    그냥 status가 deleted인 trip을 대상으로 일주일에 한 번(기간은 상관 없습니다. hard delete 시점보다 짧게만 가져가면 됨) 정도 tripDelete를 하도록 스케줄링 로직을 짜는게 훨씬 더 간편해 보여서요..!

제가 앞선 리뷰에서 이벤트에 대한 처리가 (최대)2초 뒤에 실행된다를 단점으로 둔 건 현재 바로바로 진행되고 있는 이벤트 처리 로직을 굳이 2초의 텀을 두게 만들어 서버 다운 시 유실될 가능성을 더 크게 만든다는 느낌이 들어서였습니다.!

기존의 로직 -> tripDelete 완료 & 이벤트 발행 (0.3초) -> 이벤트 처리 (1.7초)
==> 총 2초면 해당 로직 완전 끝, 그 대신 1.7초가 진행되는 중간에 서버 다운되면 이벤트 처리 task는 사라져 버림

변경한 로직 -> tripDelete 완료 & 이벤트 발행 (0.3초) -> 2초마다 스케줄링 로직 실행(최대 2초) -> 이벤트 처리 (1.7초)
==> 3.7초가 진행되는 중간에 서버 다운되면 이벤트 처리 task 사라지는데 -> top 도입해서 안사라짐!

뭔가 기능 추가를 위해 문제가 될 만한 상황을 더 크게 만들고 이를 해결하려는 것처럼 보여서 조금 찝찝합니다..
차라리 top를 도입했으니 이벤트 처리 task가 사라질 일이 0%이고, 해당 작업은 나중에 천천히 해도 좋으니 스케줄링 텀을 2초보다 훨씬 길게 가져갔으면 그나마 말이 좀 될거 같다는 생각도 드네요.

저는 오히려 홍고가 마지막에 적어 놓은 TripDeleteEvent유실을 막기 위해, 주기적으로 삭제된 Trip과 연관된 Usable한 Daylog, Item, Place, Image, Expense, TripCity 엔티티를 찾아 삭제한다. 의 방법이 더 낫다고 생각하고 있었습니다. 쿼리 비용도 그렇게 많이 들지 않을꺼라 생각하거든요.!(성능 테스트를 해 보면 정확히 알 수 있겠죠..?)

결론적으로 요약하자면

  • top 이럴 때 쓰는거 아님
  • top 적용을 통해 해결할 수 있는 상황 전제가 너무 극단적임

이 제 의견입니다.

그래서 저는 hard delete의 기간을 고려해 pollEvent 로직의 실행 주기를 재조정 하는 작업을 거친다면 조금 더 이번 PR에 의미가 생길 것 같아요! 물론 그냥 저의 의견입니다!!!

이해가 안되거나 궁금한게 있는데 빠르게 답을 주고받고 싶다면 dm으로 연락 주세요..!

@jjongwa
Copy link
Member

jjongwa commented Nov 25, 2023

아아 그렇다고 뭐
이거 머지하는건 내 눈에 흙이 들어와도 안됨
뭐 이정도까진 아닙니다.!!!! 그냥 의견임 의견!!!!!!!

@mcodnjs
Copy link
Collaborator Author

mcodnjs commented Nov 26, 2023

저도 리뷰에 대한 답이 많이 늦었네요ㅠ
홍고와 이 이후로 추가적인 이야기를 해본 적이 없기에 의견이 각기 다를 수 있습니다!
디노의 큰 관심 덕분에 다시 정신 차리고 보는중. 디노의 추가 답글에 대한 이해는 1000% 했습니다.

위에 디노가 말씀주셨던 데이터 분산 처리 환경이라는 전제. 라는 점은 모두 이해했습니다. 반박?할 말은 떠오르지 않네요 ^_^

deleteTrip()이 커밋된 후, 비동기로 이벤트를 처리하는 도중에 예외가 발생하거나, 서버가 다운된다면? 의 상황

그렇다면, 이벤트 처리 도중 예외 발생 상황에 대해서만 생각해볼 수 있겠네요.
저는 예외 발생 상황을 처리하기 위한 방법으로만 봤을 때도 outbox 테이블 적용이 굿하다고 생각해요
예외를 catch 해서 재실행 로직만 생각해도 몇 번 재실행할건지? 모든 이벤트에 대해서 재실행 로직이 필요한게 아닌지? 하는 등의 고민들이 생길거 같아요.

그래서구현 방법을 단순하게 보자면 아래와 같아요

  • Outbox 테이블로의 저장 (top)
  • 큐를 통한 스케줄링 처리

top 패턴에 대한 반박?을 하셨는데, 새로 올라온 PR에서는 아웃박스 패턴은 그대로 적용되어 있어서 어느 정도 동의한다고 봐도 될까요 .. ?
큐를 사용하지 않은 것이 차이점인거 같은데, 이 부분에 대해서만 이야기 해볼게용

저도 큐를 통한 스케줄링 처리는 부가적인 선택이라고 생각해요. 스케줄링 주기를 얼마로 가져갈까도 어떤 로직이냐에 따라 달라질 수 있겠죠!

큐 도입.
여러 개의 삭제되지 않은 이벤트에 대해서 스케줄링을 돌면서 가져와 삭제하는 로직은 필요한 로직인데, 이를 어떻게 구현할까의 문제이인거 같아요.
메세지 큐의 역할보다는 그냥 자료구조 큐라고 생각하면 좋을거 같아요, 여러 개의 이벤트를 가져와서 담는 자료구조인데, 그 자료구조로 리스트 대신 큐를 사용하여 앞에서부터 처리한다. 사실 순서를 고려하는 이벤트면 큐에서 순차적으로 하니씩 빼서 계속해서 갱신하며 처리해야겠지만, 저희는 이벤트에 대한 순서가 중요하지 않기에 상관없을거 같아요. 어떤 Collection이든 상관없다!
(근데 모종의 이유로 계속에서 이벤트 처리가 실패하는 경우 .. 처리되지 않고 쌓이는 경우가 발생할 수 있으려나요 .. ?)

디노가 올려주신 PR에서는 TripSerivce에서 삭제되지 않는 이벤트를 처리하고 있어요, 물론 패키지 의존성 신경쓰지 말라고 하셨지만, Trip 이외 Member, Image에 대한 스케줄링 로직도 필요한데, 이 역할을 담당하는 클래스 또한 필요하다고 생각하는데 여기서 그냥 큐로 가져와서 앞에서부터 하나씩 처리한다! 라는 생각에서 나왔어요!!

스케줄링 주기 2초.
저도 2초까지 가지갈 필요는 없다고 생각해요. 실시간으로 반영이 필요한 부분도 아니기에 이 부분은 의견을 맞춰보면 좋을거 같아요!
어느 정도가 적당할까요? 저는 시간 단위도 괜찮다고 생각합니다 ㅎㅎ .. ~

추가적으로, 클래스가 많아진 이유에 대해서 변명을 해보자면, 이는 Outbox를 추상화하면서 발생한 문제라고 생각해요.
Trip 뿐만 아니라, Image, Member를 삭제하는 이벤트 처리 과정에서도 이 로직을 적용하고자 했어요
각 이벤트마다 여러개의 outbox를 두는 방법도 있겠지만 이건 단편적으로만 봐도 좋은 방법은 아니지 않나요 ?!!?!


올려주신 디노의 PR과 달아주신 커멘트들을 읽어보며 답글을 남겨봤는데 ..
더 이야기하면 좋을 부분이 스케줄링인거 같아서 그 부분에 대해서만 답글 남겼습니다 ㅎㅎ

Copy link
Collaborator

@waterricecake waterricecake left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다!!! 홍고 라온!

@jjongwa
Copy link
Member

jjongwa commented Nov 28, 2023

라온의 답글과 홍고가 DM으로 보내주신 질문에 대해 추가 설명 해보겠습니다..!

라온

1. top 패턴 적용 동의 여부

top 패턴에 대한 반박?을 하셨는데, 새로 올라온 PR에서는 아웃박스 패턴은 그대로 적용되어 있어서 어느 정도 동의한다고 봐도 될까요 .. ?

저는 top 패턴에 대한 무조건적인 반박이 아니라 분산환경에서 사용할 법한 적용 방식에 반대한다는 입장이었습니다.!
그래서 첫 리뷰에서도
현재 저희의 아키텍처 구조에서 이벤트 큐를 사용한 아웃박스 패턴이 최선의 방법이었는지, 다른 선택지는 어떤게 있었고, 각각 어떤 단점이 있어 반려하게 되었는지 궁금합니다!
라고 질문한 거였구요.!
top 구조를 저희 프로젝트에 적용하는 아이디어를 제공한건 저 아닌가요..! (홍고랑 라온이 저한테 놀러 왔을때 제가 얘기한 것 같은데 하하하)
패턴 적용을 반대할 이유는 없습니다! 그저 저희 상황에 맞게 변경해서 도입하는걸 원했을 뿐...

2. 큐 도입

이 부분은 라온이 어떤 Collection이든 상관없다! 답하셨는데, 그래서 굳이 이벤트 큐라는 키워드를 가지고 온 이유가 궁금했습니다.
2초 주기마다 findAll해서 담긴 순서대로 처리하는 건데, 그럼 결국 리스트에 담아서 순차적으로 처리하는 스케줄링 로직과 다를게 없다는 생각이 들었어요.
결국 이번 PR의 핵심 로직은 이벤트를 추상화해 OutBox라는 테이블에 담는다 인 것 같습니다.
해당 로직을 보고 바로 다음과 같은 의문이 들었습니다.

3. OutBox의 추상화

추가적으로, 클래스가 많아진 이유에 대해서 변명을 해보자면, 이는 Outbox를 추상화하면서 발생한 문제라고 생각해요.
Trip 뿐만 아니라, Image, Member를 삭제하는 이벤트 처리 과정에서도 이 로직을 적용하고자 했어요
각 이벤트마다 여러개의 outbox를 두는 방법도 있겠지만 이건 단편적으로만 봐도 좋은 방법은 아니지 않나요 ?!!?!

이 부분이 너무 아쉽습니다. 결국 이벤트 큐 라는 키워드에 너무 사로잡혀 있지 않나 하는 생각이 들었어요.
먼저, 저는 여러개의 OutBox를 두어도 상관 없다고 생각합니다. 이렇게 OutBox를 두어야 하는 경우가 앞으로도 그렇게 많지 않을거라 생각이 들어서요.(그리고 현 상황에서 Image를 따로 삭제하는 부분은 없긴 합니다. 이후 진행 상황에 따라 분리될 수 있겠지만요..!)

하지만 이건 그냥 제 추측일 뿐이고 서로 생각이 다를 수 있습니다.
그러면 라온의 말대로 하나의 OutBox를 두고 모든 이벤트를 처리하려면 각 이벤트를 테이블에 저장하는 방법밖에 없을까요?
지금 로직에서는 이벤트를 PayloadToEventMapper.toJson을 통해 String 값으로 변환해 저장하는데요. 이 방식에는 어떤 문제점이 있을까요?

우선 TripDeleteEvent만 적용하고 이후에 MemberDeleteEvent를 적용한다고 했는데, MemberDeleteEvent의 필드는 다음과 같습니다.

public class MemberDeleteEvent {

    private final List<Long> tripIds;
    private final Long memberId;
}

tripId들을 리스트 형태로 가지고 있는데요, 리스트의 사이즈가 커지면 이걸 그대로 직렬화해 OutBox에 저장할 수 있을까요?
OutBox 테이블의 eventPayload필드는 어느 정도의 길이까지 커버할 수 있을까요?
이 부분을 한번 더 생각해보면 좋을 것 같아요.. (설마 그래서 사용자가 가질 수 있는 최대 여행 수를 100개로 제한한건가요..!)

그래서 저는 굳이 이벤트 큐를 써야 할까? 다시 말하면, 굳이 이벤트를 저장해야 할까? 라는 입장입니다.
그냥 OutBox 테이블에 Enum 형의 필드를 추가하고, 스케줄링 로직에서 분기 처리를 하는 방식이 가장 간단하다고 생각했거든요.
또 말로만 하면 제 의도가 올바르게 전달되지 않을 수 있을 것 같아 제가 올린 옆 PR에 추가 커밋 남겨놨습니다..! 참고해 주시면 감사하겠습니다..ㅎ

홍고

1. top 적용을 통해 해결할 수 있는 상황 전제가 너무 극단적임

이 부분에 대해서는 충분히 의견 차이가 있을 수 있다고 생각합니다.

정말 극단적인가요? 전 이벤트 소실되는거 막기위해 db에 저장한다. 자연스러운 흐름이라고 생각했는데
이게 극단적이면 로드밸런싱, db replication전부 극단적인 대안아닐까요...

하지만 이 부분에 대해서는 할 말이 좀 있을 것 같네요..
로드밸런싱, DB복제와 해당 문제를 같은 선 상에 놓는 건 말도 안된다고 생각합니다!!!!

가장 중요한 차이점은 서비스의 가용성에 직결되는지 여부 아닐까요?
이벤트가 소실된다고 해서 사용자가 서비스를 사용하는데 전혀 영향이 없습니다.(애초에 그래서 스레드와 트랜잭션 분리도 괜찮다고 판단했던 거고, 두 트랜잭션의 경계 기준이 되었었죠..!) 이 부분은 다시 생각해 보면 좋을 것 같아요..

결국 저는 굳이 처리하지 않아도 문제 없던 상황을 고려하기 위해 너무 과도한 기능을 추가하게 되는 것 아닌가? 정도의 느낌이었습니다.!

2. 스케줄링 주기

이건 2초보다 더 크게 돌려도 좋다고 생각합니다
근데, 2초면 안되는 이유가 있어요?! 2초마다 수행해도 서버에 부하가 걸릴 일이 없자나요????? 왜 늘려야 하죠?
조금이라도 리소스를 줄이는 게 낫다는 의미인가요?!

이 부분도 약간 당황스럽습니다.. 제 첫 질문은

findAll의 호출 기준을 2초로 둔 이유가 궁금합니다!

이었고, 이에 대한 홍고의 답은

이건 단점인지 모르겠습니다! 이벤트 로직이 빨리 처리되는 것이 중요할까요? 유저에게 영향을 주지 않는 로직이니 근시일내로 수행된다. 라는 보장만 있으면 괜찮다고 생각했습니다!

이거였는데요.. 제가 궁금한건 말 그대로 1초도 아니고 3초도 아닌 왜 2초로 두었는가 였습니다!
분명히 어떠한 판단 기준이 있었기에 2초라는 시간이 나왔을텐데, 그 부분에 대한 명확한 답변을 듣지 못한 것 같네요..

어떠한 기준을 정하는 데에는 이를 뒷받침할 근거가 있어야 한다고 생각합니다.!
그런 의미에서 어떠한 근거로 2초를 잡았는지 궁금했던 거구요.

어짜피 실시간으로 처리할 필요가 없는 상황에서 제가 생각한 기준은 Hard Delete의 시점이었고, 그래서 Hard Delete 주기가 기준이 되어 스케줄링 주기를 이보다 짧게만 가져가면 되지 않을까 의견을 제시한 겁니다. (ex. Hard Delete 주기가 1달이면 미처 삭제 처리되지 않은 데이터를 처리하는 작업은 이보다 짧게 스케줄링을 가져가야 한다 -> 그래서 2주 주기로 스케줄링 텀을 정했다. 라고 설명 가능)

질문 주신 부분에 대한 답이 어느정도 되지 않을까 싶어요..! 아니라면 홍고는 왜 2초를 기준으로 잡았는지 알려주시면 좋을 것 같습니다,,,

3. top 꼭 분산환경에서만 써야 하는가

그리고 트랜잭셔널 아웃박스패턴 꼭 분산 아키텍처에서 사용해야 의의가 있는걸까요?
이벤트 호출로직과 이벤트 수행 로직이 같은 트랜잭션에 묶일 수 없는 경우, 아웃박스를 통해서 같은 트랜잭션으로 묶는거에 의미가 있는거 아니에요?!
진짜 모르겠어서 물어봅니다!

이 부분은 저도 아직 헷갈리네요..
제가 올린 PR도 top를 적용했다고 볼 수 있을 것 같은데 또 개념적인 의미에서는 아닌 것 같기도 하고 그래서요..
우선 지피티 선생님(무려 과금 버전..!!)의 정리글 첨부합니다...
image

@hgo641
Copy link
Collaborator

hgo641 commented Nov 29, 2023

@jjongwa
늦었습니다!!!!!!!!!!! 죄송합니다!!!!!!!!!!!!!!!!11
장문의 리뷰 감사합니다. 디노! 덕분에 트랜잭셔널 아웃박스 패턴 도입에 대해서 더 깊게 생각해보게 되었습니다!!
ㅋㅋㅋㅋㅋㅋㅋ... dm으로 여쭤본 것 까지 PR로 답변달아주실줄은 몰랐네요... dm답장이 오는 걸 기다리고 있었는데 여기에 달아주셨군요 🧐

1. top 적용을 통해 해결할 수 있는 상황 전제가 너무 극단적임

이건 제가 예시를 잘못들었던 것 같네요! 디노말대로 서버 가용성에 즉결되는 로드밸런싱, DB Replication과 비동기 로직은 다른 비교 선상에 두는 게 맞다고 생각합니다!!!

2. 스케줄링 주기

이 부분도 약간 당황스럽습니다.. 제 첫 질문은
findAll의 호출 기준을 2초로 둔 이유가 궁금합니다!
이었고, 이에 대한 홍고의 답은
이건 단점인지 모르겠습니다! 이벤트 로직이 빨리 처리되는 것이 중요할까요? 유저에게 영향을 주지 않는 로직이니 근시일내로 수행된다. 라는 보장만 있으면 괜찮다고 생각했습니다!
이거였는데요.. 제가 궁금한건 말 그대로 1초도 아니고 3초도 아닌 왜 2초로 두었는가 였습니다!
분명히 어떠한 판단 기준이 있었기에 2초라는 시간이 나왔을텐데, 그 부분에 대한 명확한 답변을 듣지 못한 것 같네요..

여긴 소통의 오류가 있었던 것 같네요! 디노가 현재 PR에 대한 단점으로 이벤트에 대한 처리가 (최대)2초 뒤에 실행된다를 꼽아주셔서, findAll의 호출 기준을 2초로 둔 이유가 궁금합니다! 이 질문도 왜 더 빨리 처리하지 않고, 2초뒤에 수행하느냐를 물어보시는 줄 알았습니다!

그래서 이벤트 로직은 빨리 처리되지 않고, 근시일내로 수행되면 될 것 같다고 말씀드린겁니다! (근시일 이라는 표현도 수정하겠습니다... 저도 db에 부하가 가능하지 않는 선이라면 언제든 이벤트를 수행해도 괜찮다는 입장입니다...)

2초로 둔 이유는, hard delete가 수행되기 이전 & 서버에 부하가 걸리지 않는 로직 반복 횟수를 만족한다면 몇 초든 괜찮다고 생각했기에 임의로 둔 값입니다. 기간을 크게 잡지 않은 이유는 행록이 hard delete를 수행하는 시간을 정하지 않았기 때문에 혹시라도 이후에 hard delete가 수행될 때 그 기간을 넘기는 것을 우려했습니다.
2초라는 주기가 너무 짧다고 생각하시면 논의를 통해 맞춰가고 싶습니다!

3. top 꼭 분산환경에서만 써야 하는가

ㅎㅎ 챗지피티 센세 첨부 감사합니다
결국 top맞잖아요!!!!!!!!!!!!!!!!
top 이럴 때 쓰는거 아님 이라 하셔서 굉장히 띠용이었습니다!!!!!!!!!!!!
아예 top를 반대하시는 건 아니고 디노 PR버전처럼 이벤트큐 제거 & Outbox 형태 변경인 top를 원하시는 거 맞죠?!!!!!!!!!!!!!1

이벤트 처리의 방향은 ... ?!

이 부분이 너무 아쉽습니다. 결국 이벤트 큐 라는 키워드에 너무 사로잡혀 있지 않나 하는 생각이 들었어요.

  • 이벤트큐 없애고 스케줄링로직에서 바로 이벤트 발행하자 동의!
    이벤트 큐가 없어도 될 것 같다는 의견에 동의합니다! 이거 처음에 구현할 때는, 기왕이면 이벤트의 순서도 지키면 좋지 않을까? 하는 마음에서 한 거였는데 저희는 순서가 중요한 이벤트가 없으니 디노의 예시 pr처럼 스케줄링로직에서 바로 이벤트를 발행하는 게 좋을 것 같네요! 너무 이후의 확장성을 고려해서 큐를 만든 것 같네요!

먼저, 저는 여러개의 OutBox를 두어도 상관 없다고 생각합니다. 이렇게 OutBox를 두어야 하는 경우가 앞으로도 그렇게 많지 않을거라 생각이 들어서요.(그리고 현 상황에서 Image를 따로 삭제하는 부분은 없긴 합니다. 이후 진행 상황에 따라 분리될 수 있겠지만요..!)

  • Outbox에 모든 이벤트 정보를 넣지 않고, 타겟 도메인과 이벤트 type만 저장하자는 말 동의!

하핫... 라온이 image삭제를 언급한 거 저 때문인 것 같네요! s3에 이미지 리스트를 올리다가 예외가 나면 이미지 삭제 이벤트를 추가하는 것을 고려하고 있었습니다! 디노와 라온에게 한 번 언급했었는데 라온이 이걸 염두하고 하신 말 같네요

여러개의 OutBox를 두어도 상관 없다고 생각합니다. 이거 event마다 각자의 outbox테이블을 만들자는 의미가 아니라 pr처럼 type만 구분해서 하나의 outbox테이블에 저장하자는 의미맞죠?

굳이 이벤트를 저장해야 할까? 이것도 그럼 outbox 테이블 자체를 없애자는 말이 아니라, 이벤트의 정보 전부를 저장하지 말고, 타겟도메인아이디와 이벤트type만 저장하자는 말 맞죠?!!!!

디노 PR 잘 보고 왔습니다!

  • Outbox를 사용해서 이벤트를 저장하되, Outbox에는 타겟 도메인 아이디와 이벤트 type만 저장한다.
  • 저장된 Outbox는 스케줄링을 통해 조회해 이벤트로 만들어 발행한다.

이게 디노의 최종의견인거 맞나요? 전 좋습니다!!!
이전 코멘트에서는 top를 아예 없애고 trip은 deleted되었지만 item은 usable한 데이터들을 주기적으로 조회하는 것을 더 선호하시는 것 같았는데, pr로 코멘트를 여러번 주고받다보니 디노의 최종 의견이 무엇인지 살짝 헷갈리기 시작했습니다!!!!!!!!! 최종 의견만 다시 전해주시면 매우 감사하겠습니다!!!!!!!!!

+ 혹시 더 논의가 필요하신 것 같으면 아예 날을 잡아서 다같이 게더로 만나는 게 어떨까요?!!!!!!!!1

@mcodnjs
Copy link
Collaborator Author

mcodnjs commented Nov 30, 2023

소통이 좀 느리지만, 패키지 의존성 뒤로 진지한 토론을 하는거 같아 재밌네영
이번에도 장문의 답장 감사합니다 디노 ^_^
또 몇가지 의견과 궁금한 점 남겼습니다! 디노가 올린 PR에는 따로 의견 안남겼지만, 모두 참고해서 말씀 드립니다 ~~

스케줄링 주기

이거는 저희 셋 모두 특정 기준을 통해 주기를 정하자 까지 결론이 도달한거 같네요!!!

OutBox의 추상화

디노가 말씀해주신 부분이 모두 추상화와 관련된 부분인거 같아서 섹션을 좀 나눠서 의견을 보충해봤습니다 ~

1. 이미지 삭제

(그리고 현 상황에서 Image를 따로 삭제하는 부분은 없긴 합니다. 이후 진행 상황에 따라 분리될 수 있겠지만요..!)

이미지 관련해서는 홍고가 말씀해주신게 맞아요! 현재 있는 로직은 아니지만,
아이템이 저장되기 전에 이미지가 먼저 저장되는걸로 알고 있는데, 이미지를 모두 업로드하였는데 아이템 저장에 실패할 경우 기존에 업로드된 이미지가 삭제될 필요가 있다는 의견이 있었어요. 그래서 해당 부분도 이벤트+아웃박스를 통해 처리하고자 했습니다! (홍고랑 이해한 바가 다를 수 있음^^ ..) 설명이 부족했네요 ㅠ

2. 큐

이 부분이 너무 아쉽습니다. 결국 이벤트 큐 라는 키워드에 너무 사로잡혀 있지 않나 하는 생각이 들었어요.
먼저, 저는 여러개의 OutBox를 두어도 상관 없다고 생각합니다. 이렇게 OutBox를 두어야 하는 경우가 앞으로도 그렇게 많지 않을거라 생각이 들어서요

전 코멘트에서 말한 것처럼 큐에 가져와서 처리하는 방법이 아닌 그냥 리스트로 스케줄링을 통해 수행하는거는 동의! 인데
이건 그냥 궁금해서 여쭤보는건데 ..

  1. outbox를 추상화한 부분이 왜 이벤트 큐랑 관련있다고 생각하셨는지 여쭤봐도 될까요 ?!?!
  2. 또, 여러개의 아웃박스를 둔다는 말이 홍고가 이거 event마다 각자의 outbox테이블을 만들자는 의미가 아니라 pr처럼 type만 구분해서 하나의 outbox테이블에 저장하자는 의미라고 정리해주셨는데 이것도 맞을까요 ??

3. 페이로드에 대한 직렬화

tripId들을 리스트 형태로 가지고 있는데요, 리스트의 사이즈가 커지면 이걸 그대로 직렬화해 OutBox에 저장할 수 있을까요?
OutBox 테이블의 eventPayload필드는 어느 정도의 길이까지 커버할 수 있을까요?

테이블 필드에 어디까지 저장할 수 있을까! 이 부분은 한번도 고민해보지 않았네요..
그치만!!! 이벤트 페이로드에 담는 값이 커지는 경우는 어케할까?이벤트 페이로드에 다양한 필드가 들어올 수 있다 보다 우선순위가 낮았던거 같아요. 지금 생각해도 페이로드 크기가 커질 때를 고려하기 보다 다양한 타입을 담을 수 있었으면 좋겠다가 먼저라고 생각이 들어요. 페이로드엔 많은 데이터가 들어가지 않는게 일반적? 이라고 생각했어요. 그래서 MemberDeleteEvent를 예시로 들어주셨는데, 이 이벤트도 현재는 tripId 리스트를 가지고 있지만 올려주신 PR처럼 memberId만 가지고 있는거죠!

물론 디노가 올려주신 PR처럼 Long 타입으로 제한해두면 편하겠지만 .. 앞서 말씀드렸던 것처럼 이벤트에 다양한 필드가 들어올 수 있을거라고 생각해서 직렬화 로직을 넣었습니다! 위에서 언급한 이미지 삭제 이벤트도 List으로 imageId 리스트를 필드로 가지면 좋을거 같다고 생각했어요. (물론 imageId를 하나씩 저장하는 방법도 있긴함) 그치만 무조건적으로 필요한 로직은 아닌 것에 동의, 하지만 추상화해놓으면 좋지 않을까요 ?!

(설마 그래서 사용자가 가질 수 있는 최대 여행 수를 100개로 제한한건가요..!)

이건 부하테스트 수행 시, 사용자가 가질 수 있는 최대 여행 수를 모르니 적절한 기준이 없어 테스트하는데 어려움을 겪어서 정한 수치였습니다!
이번 PR과는 관련 없습니다!!!

4. 분기문을 통한 로직 처리

그래서 저는 굳이 이벤트 큐를 써야 할까? 다시 말하면, 굳이 이벤트를 저장해야 할까? 라는 입장입니다.
그냥 OutBox 테이블에 Enum 형의 필드를 추가하고, 스케줄링 로직에서 분기 처리를 하는 방식이 가장 간단하다고 생각했거든요.

첫번째 문장은 이해가 잘 안가요 히히히 .. 이 부분도 홍고가 Outbox를 사용해서 이벤트를 저장하되, Outbox에는 타겟 도메인 아이디와 이벤트 type만 저장한다. 이렇게 해석하고 정리해주셨는데 맞을까요 ???

디노 말처럼 스케줄링 로직에서 분기 처리하는 방식이 저도 당연히 간단하다고 생각합니다! 근데 이벤트가 늘어날수록 분기 처리 로직이 늘어날 것이라고 생각해서 추상화를 한거였어요! 저희 추천 알고리즘도 사실 지금 당장 좋아요 순으로 정렬하는 알고리즘 밖에 없음에도 불구하고 추상화가 되어있잖아요, 그냥 그런 의도였다고 생각해주시면 좋을거 같아요. 그래서 저는 if문을 통한 분기처리보단, 타입에 따라 올바른 이벤트 로직이 처리되도록 수행되면 좋을거 같아요 🙃 (현재 PR에서는 outboxToEventMapper.toEvent(outbox))


어쨌든 이 코멘트의 결론은 물론 간단간단하게 구현할 수 있다! 하지만 난 추상화하고 싶다 ~
이 추상화 로직이 현재 우리의 상황에서 굳이굳이굳이 싶다? 이 측면에서 어디까지가 ok인지 의견 남겨주시면 감사하겠습니다 gg

  • 저는 분기문보다는 현재 방식(PR)대로 처리하는게 더 좋다고 생각하고,
  • 이벤트 페이로드를 Long 타입으로 강제하는 것까진 좋습니다아아

그리고 저도 이관 관련해서 게더에서 함 얘기하면서 이것도 같이 이야기해보면 좋을거 같아요 ~~! ~!

Copy link
Member

@jjongwa jjongwa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

답장이 늦었네요.. 몸이 좋지 않아 좀 앓아 누웠습니다..
제 의견에 대해 두 분 다 충분히 이해한 것 같구 제가 더 이상 추가로 주장할 내용도 없어서 나머지는 두 분의 선택에 맡기겠습니다.!
맨날 대면으로 토론하다가 이렇게 PR에서 의견 남기니까 나름 재밌었습니다ㅎㅎ
이만 approve 할께요~~!!
(물론 게더에서 추가 논의하는 것도 좋아요~ 적극적 참여 가능)

그ㅡㅡㅡ래도 마지막으로 말하자면
지금 상황에서 추상화는 오버엔지니어링이다 는 입장......................!하하하

@hgo641 hgo641 closed this Apr 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants