[세미나 리뷰] 우아한 객체지향(2)

Doccimann·2022년 5월 3일
0

세미나 리뷰

목록 보기
2/3

본격적으로 글을 작성하기 전에

이전 포스트에서 저희는 우아한 객체지향 세미나를 리뷰한 바 있습니다. 이걸 2부작으로 나눠서 설명하려는 이유는, 세미나의 양이 워낙 방대해서 한번 호흡을 끊어주고 시작하는 것도 나쁘지 않겠다는 생각이 들어서 였습니다. 이전 포스트는 아래에 링크를 남겨두겠습니다.

우아한 객체지향(1)

이번 포스트에서는 주로 의존관계가 주로 어떠한 문제를 야기하며, 어떻게 해결을 할수 있을까 에 대해서 주로 다룰 예정입니다. 깊이는 이전의 포스트보다 더 할 예정입니다. 살려줘


🤔 과연 Dependency를 관리했다고 모든게 끝이야?

이전 포스트에는 Dependency 간의 cycle을 해결하는 방법에 대해서 주로 다뤘습니다. 이번 포스트에서는 이전 포스트에서 언급하였던 Association 에 대해서 주로 다룰 예정입니다.

일단 아래의 그림을 보고 이야기를 시작하겠습니다.

위의 그림을 보시게되면, shop, menu, OptionGroupSpecification, OptionSpecification이 모두 Association 관계에 의해 엮여있는 모습을 확인할 수 있습니다.

데이터베이스의 경우 foreign key를 통해서 어디든지 table을 탐색할 수 있다고하지만, JPA같은 ORM의 같은 경우에는 이야기가 다릅니다.

연관관계를 설정하게 되면, ORM에서는 그것이 트랜잭션의 탐색범위로 묶여버리는 현상이 벌어지기 때문입니다.

예를 들어서 설명을 드리겠습니다. 위의 도메인에서 배달 기능을 추가시켜보려고 합니다. 배달이라는 로직을 처리하기 위해서는 적어도

  • 우선 Order에서 상태를 갱신한다
  • 배달이 완료가 되면, shop에 수수료를 누적시킨다

라는 과정 등이 필요할 수 있습니다. 따라서 위의 로직을 타게되면 아래의 문제가 발생할 수 있습니다.

🔥 트랜잭션의 탐색 범위가 필요없이 넓어진다


이 글과는 관련은 없는 사진이지만, 비슷한 맥락이라 가져옴

Delivery라는 로직을 처리하기 위해서는 위에서도 언급했듯이 Order을 호출해야하고, 그리고 Shop을 호출해야합니다.

그런데 Order을 호출할 때는, Order와 연결된 모든 객체들이 Lazy Loading이 되었건, Eager Loading이 되었건 간에 로딩이 되어버리는 현상이 발생해버립니다.

그런데 이거는 진짜 커다란 문제를 불러일으킵니다. 흔히들 JPA의 N+1 이슈 라고 부르는 문제인데요. Order의 경우 연결된 모든 객체가 생명주기를 함께 해버리기 때문에 보통은 Eager Loading을 하게되는데 매번 배달 쿼리를 실행할 때마다 Order와 연결된 모든 객체를 호출하는 쿼리들이 동시에 호출되어서 실행이 되어버립니다.

사실 Lazy Loading을 하면 사정이 그나마 낫기는한데(Lazy Loading의 경우 한 번 객체를 호출하면 Proxy의 형태로 Context에 보관이 됩니다) Order의 경우 생애주기 자체가 짧은 도메인이기 때문에 Eager Loading, Lazy Loading을 구분짓는게 크게 의미가 없습니다. 게다가 어차피 Lazy Loading을 하여도 N+1 문제에 대해서는 자유롭지 못합니다.

그런데 이거는 Shop을 호출할 때도 똑같이 적용이 되는 문제입니다.

위와 비슷한 맥락에서 아래의 문제도 동시에 발생합니다

🔥 트랜잭션에 Lock이 걸려버린다. (트랜잭션 처리 과정에서 경합상태(Race Condition)이 발생한다)

우선 트랜잭션 자체의 대략적인 개념은, 분리가 불가능한 작업의 단위 입니다. 분리가 불가능하지만, 트랜잭션끼리는 임팩트를 끼치지 말라는 법은 없습니다.

일단 상황을 가정하겠습니다. 고객이 배달을 신청했습니다. 그런데 배달을 처리하는 과정에서 중간에 사장님이 shop의 정보를 바꾸려고 시도를 했습니다.

위의 상황에서는 충분히 Transaction 간의 Race Condition이 발생할 수 있습니다. 아래의 두 가지 상황을 겪을수도 있다는 것입니다.

  • 만일 Delivery 로직이 비관적 락(Pessimistic Lock)을 기반으로 동작을 하는 로직이었다면, Shop이라는 엔티티 자체에 Lock이 걸려버리기 때문에 사장님은 Delivery 로직이 종료되기 까지 Shop의 정보를 고칠 수가 없다. 그런데 Delivery 로직 자체가 언제 끝날지는 장담이 불가능하다.
  • 만일 Delivery 로직이 낙관적 락(Optimistic Lock)을 기반으로 동작하는 로직이었다면, 배달 로직이 진행되는 도중에 Shop의 정보가 바뀌어 데이터의 원자성이 보장되지 않는 문제가 발생한다. (동시성 이슈가 발생한다는 뜻이다)

따라서 우리는 위의 두가지 이슈때문에 객체간의 결합도를 낮출 필요도 존재합니다.


👉 객체의 분리 기준

객체는 아래의 기준에 따라서 분리를 해야합니다.

  • Life cycle을 함께하는 엔티티들을 묶는다
  • Domain Constraint(도메인 제약사항)을 함께하는 도메인끼리는 묶는다
  • 가능하면 최대한 분리하는게 좋다

위의 원칙에 따라서 도메인을 분리시키면 아래와 같아집니다.

그리고 이걸로 모자랍니다. Shop과 Menu 그리고 Order와 Delivery의 경우에는 서로 간에 시스템을 분리하는 것을 원합니다. 따라서 저희는 또다른 결합도를 낮출 방법을 찾아야합니다.

그런데 JPA를 사용하는 사람들은 무의식적으로 방법을 알고있습니다. JPA를 사용하는데 이걸 사용안 할 리가 없거든요!

🔨 Repository를 이용해서 객체 간의 결합도를 낮춘다 (시스템을 분리시킨다)

Repository에는 이미 객체간의 의존성을 가지고있는 메소드들이 충분히 많이 구현이 되어있는 상태입니다. 그리고 id를 통해서 객체를 호출해내는게 가능합니다.

따라서 Repository를 이용하면 아래와 같이 분리가 가능합니다.

사실 위의 방법도 완벽한건 아닙니다. 시스템이 확장이 될수록 조회에 대해서 로직이 매우 복잡해지기 때문입니다. 시스템이 크게 확장이 되어서 양방향의 의존성이 발생할 우려가 생기게되면, 저희는 CQRS 패턴을 통해서 해결을 지어야만합니다.

CQRS 패턴의 경우에는 명령 로직과 조회 로직을 분리시켜서 구현하는 방법입니다. 매우 난이도가 높은 패턴이기 때문에 필요할 경우에 알려드리겠습니다. 사실 저도 몰라요


👉 객체를 다 분리했다구요? 이제 컴파일 에러를 처리해야죠

저희는 주문 로직을 기준으로 처리를 진행하고 있었습니다. 주문 로직은 대략 아래와 같았습니다.

그런데 기존의 구조에서는, validation 로직들이 모두 각자의 엔티티에 위치했었으며, 동시에 어떤 validation method에서는 객체를 직접 참조하여 검증 을 수행하는 로직도 존재했었습니다.

그런데 도메인을 최대한 찢어놓았기 때문에, 객체를 직접 참조하던 메소드에서는 컴파일 에러가 펑펑펑 터져나올 수 밖에 없습니다. 이걸 어떻게든 해결을 지어야합니다.

🔨 Validation Logic을 모두 한군데에 몰아서 구현합니다.

위와 같이 각각의 entities에 찢겨져있던 모든 validation logic을 한 군데에 모아버리면 가독성도 좋고, 동시에 모든 컴파일 에러들이 해결이 되는 모습을 확인할 수 있습니다.

그런데 이러한 의문이 들수도 있습니다.

🤔 아니, 검증 로직에서는 각자의 역할과 책임이란게 존재하는건데, 객체지향에서 저래도 되는거에요?

물론 맞는 말입니다. 객체지향에서는 각자의 역할과 책임 이라는 것이 매우 중요한 키워드입니다. 그런데 이렇게 생각해볼수도 있습니다.

객체지향이 만능은 아니다. 가끔은 절차지향적인 구현이 좋을 때도 있다

위의 방식은 분명히 절차지향적인 구현 방식이라고 볼 수도 있습니다. 그런데 객체지향의 특징을 버리고 절차지향을 버려서 얻는 이점이 사실 더 큰 경우라고 볼 수 있습니다.

  • Validation logic을 확인하기 위해서 여러 entity들을 탐색할 필요가 없어졌다
  • 객체들의 응집성(Coherence)이 높이는 효과를 가져온다

여기서 객체의 응집성에 대해서 이야기를 해보겠습니다. 객체의 응집성이란 하나의 클래스 내부에서 변경의 주기가 같은 코드들이 몰려있을 수록 응집성이 높다 라고 정의가 됩니다.

그런데 이렇게 생각해봅시다. 과연 검증 로직이 객체 내부의 로직들과 Life cycle이 같을까요?

저는 아니라고 생각합니다. 검증 로직은 그렇게 자주 호출되는 것도 아니고, 다른 로직들은 자주 호출이 될수도 있거든요. 따라서 검증 로직이 객체에 퍼져있다면, 모든 entity들은 응집성이 낮아진다고 볼 수 있습니다.

🔨 Domain Event를 발행해서 문제를 해결한다

사실 이 부분은 제가 이해를 하지 못해서...다음에 공부해서 이거는 따로 설명을 드리도록 할게요!


지금까지 우아한 테크세미나의 우아한 객체지향을 리뷰해보았습니다.

다음 포스트에서는 우아한 객체지향에서 배운 내용들을 토대로 YUMarket의 도메인을 설계하는 내용을 다루도록 하겠습니다.

다음 포스트에서 뵙겠습니다. 감사합니다!

profile
Hi There 🤗! I'm college student majoring Mathematics, and double majoring CSE. I'm just enjoying studying about good architectures of back-end system(applications) and how to operate the servers efficiently! 🔥

0개의 댓글