💋 인트로
이 글은 우테코 5기 코치 제이슨의 강의를 듣고, 깃짱이 작성한 글입니다.
💋 레거시 코드
✔️ 레거시 코드란?
레거시 코드란, 이해할 수 없고 수정하기도 힘든 코드를 지칭하는 속어처럼 사용될 때가 많다.
하지만, 레거시 코드는 그 당시로는 최선의 선택이었다. 레거시 코드는 모든 개발자가 극복해야 할 난제이고, 어쩌면 그 레거시 코드가 있기에 내가 일하고 있는 기업의 서비스가 현재 사용되고 있는 것일 수도 있다.
그런데, 레거시 코드가 없이 서비스를 개발할 수는 없을까?
왜 시스템은 부패해가는 것일까? 왜 시스템은 깨끗한 상태에 머물러 있지 않을까?
✔️ 레거시 코드가 생기는 과정
기획자가 있고, 개발자가 있다.
기획자와 개발자가 함께 논의해서 기획을 한다면 베스트겠지만, 대부분 상황에서는 기획자가 일방적으로 결정한다. 최악이지만 일반적인 상황으로, 개발을 거치면서 기획은 계속 달라진다.
서비스 코드에서는 기획이 수시로 변하기 때문에 테스트 코드를 제대로 짜지 못한다.
새로운 기획에서 기존 테이블에 새로운 컬럼 몇 개만 추가하면 될 것 같다.
getter, setter를 남발하게 되면서 서비스는 점점 커진다.
이렇게 계속 기획이 추가되고 변경되면서 일어난 것이 레거시 코드다.
💋 소프트웨어의 복잡성을 다루는 지혜
도메인 주도 설계의 철학을 꿰뚫고 있는 문장이다.
✔️ 비즈니스 규칙을 한 곳에 모아보자!
컨트롤러 코드가 1000줄 이상 있다면 어떨까?
요구사항이나 제약조건이 하나 추가될 때마다 코드 4~5줄이 추가되고 있었다.
코드의 양은 굉장히 많았지만, 기획자가 실제로 알고 있던 것과 다른 부분이 있어서 항상 ‘코드가 이렇게 돌아가고 있는 것을 아시나요?’를 기획자에게 확인해야만 했다.
기획자가 알고있는 비즈니스 규칙은 자바 뿐만 아니라 자바스크립트와 SQL에도 구현되어 있었으며, 코드가 아닌 운영 정책으로 존재하기도 했다. 예를 들어서, 고객 쪽에 표시되는 말로는 ‘상품의 이름을 30자 이상으로 할 수 없습니다.’라고 적혀 있지만, 실제로 이를 검증하는 로직은 전혀 없는 경우이다.
정산팀의 경우에, 월간 정산과 일간 정산을 하는데 1원이 달라 사람이 미치는 경우가 있다. 어디에서 더하다가 잘못되었는지, 알아내야 한다. 1원의 차이를 다시 보고하기 위해서는 금융감독원의 검토를 거쳐야 하기 때문에 굉장히 번거롭다.
비즈니스 규칙을 한 곳에 모아야 한다는 고민을 결국 하게 되었다.
이렇게 다양한 아키텍처를 찾아보게 되었고, 결국 클린 아키텍처를 발견했다.
바깥쪽이 무엇을 하는지, 안쪽의 도메인은 몰라야 한다는 것이다.
도메인은 바깥이 웹이던지 앱이던지, 어떤 형태던지 상관이 없어야 한다는 뜻이다.
✔️ 아키텍처는 정답이 없다.
쿠팡과 11번가의 비즈니스 로직은 동일하지 않다. 각자의 도메인 로직에 따라서 각자에게 맞는 아키텍처가 존재한다.
우리 서비스에 맞는 아키텍처가 존재한다. 도메인 주도 설계는 이런 비즈니스 로직을 겪어보고, 이후에 공부하는 편이 낫기 때문에 우테코 기간 동안은 해당 내용을 공부하지 말라고 했었다.
✔️ 소프트웨어의 존재 가치
우리가 만들고자 하는 것은 애플리케이션이고, 사용자의 문제를 해결하는 것이 가장 중요하다.
아무리 기술적으로 정교하고 성능이 좋다고 하더라도, 사용자의 문제를 해결하지 못하면 실패한 소프트웨어라고 할 수 있다.
✔️ 본질적 복잡성 VS 우발적 복잡성
- 본질적 복잡성: 문제 자체에서 발생하는 복잡성으로, 문제의 범위를 줄이지 않고서는 제거할 수 없다.
- 우발적 복잡성: 솔루션으로 인해 발생하는 복잡성으로, 프레임워크, 데이터베이스 또는 기타 인프라가 될 수 있다.
- docker-compose를 이용하면서 yml 파일을 잘 작성해야 하는 경우, 우발적 복잡성이다.
본질적 복잡성은 우리 서비스가 크게 확장되지 않는 한 꾸준하다. 소프트웨어 초창기에는 본질적 복잡성과 우발적 복잡성이 구분되지 않는다.
✔️ 문제의 정의를 제대로 내리자!
이 문제는 누구의 문제인지 왜 문제인지에 대한 정의가 되어 있지 않다.
문제에 대한 정의를 하지 않고, 엘리베이터 속도를 높인다, 빌딩 내로 들어오는 사람의 수를 제한한다 등등 솔루션을 제시하려고만 하는 것이 문제이다.
건물주 입장에서는 차라리 빌딩에 불을 질러서 태워버리고, 보험금을 받는 것이 더 적절할 수도 있다.
💋 도메인
✔️ 도메인이란?
도메인은 소프트웨어가 해결하고자 하는 문제 영역
이다.
소프트웨어는 사용자의 활동이나 관심사에 관련되어 있는데, 대부분 소프트웨어 산업 내에서 발생하는 문제가 아니라 다른 산업에서 발생하는 문제를 해결하려고 한다는 것이 독특하다.
조영호님의 말에 따르면, 소프트웨어는 사람의 욕망과 욕구를 해결하려고 만든 창조물이며, 사람들의 욕망과 욕구가 개발자에게 전달되었을 때 우리는 그것을 도메인이라고 부른다.
✔️ 도메인 모델이란?
도메인 모델은 다이어그램이 전달하려는 아이디어이자 목적을 가진 의사소통 수단이다.
이 의사소통은 회의, 기획, 디자인, 개발에 사용되어야 한다.
항공권 예약 시스템에서는 항공기, 승객 등 물리적으로 존재하는 것들을 모델링할 수 있고, 회계 시스템에서는 화폐, 금융 등 물리적으로 존재하지 않는 것들도 모델링할 수 있다.
모데인 모델을 사용하면 여러 관계자가 이야기를 하면서 동일한 것(호랑이)들을 떠올릴 수 있다.
💋 도메인 주도 설계
도메인 주도 설계라는 책은 2003년에 나왔는데, 최근에 MSA가 유행하면서 다시 유행하기 시작했다.
MSA로 쪼갤 때 ‘잘’ 쪼개기 위해서 DDD라는 개념이 필요하게 되었다,.
✔️ DDD의 세 개의 기둥
- 유비쿼터스 언어
- 전략적 설계
- 전술적 설계
이 중 일부만을 도입하는 것을 Light DDD라고 하는데, 안티패턴이다.
✔️ 분할 정복하자!
우리가 해결해야 하는 문제의 영역은 너무 크다!
분할해서 정복하자! Divide and Conquer!
큰 서비스를 상품, 정산, 결제로 쪼개 관심사를 분리하고 격리해서, 각자 문제 해결에 집중할 범위를 정한다.
우리는 그래서 이렇게 쪼갠 하나하나의 개념을 Context
라고 부른다.
이 Context
는 서비스 별로 매우 다르기 때문에 정답이 없다.
도메인이 문제의 영역이라면, Context
는 해결의 영역이다.
DDD에서의 Context
기준으로 쪼개게 되면 MSA를 추진할 때 굉장히 유리하다.
Micro Service는 Bounded Context
기준으로 생각하면 된다.
✔️ 각각의 관심사는 다르다
Sales
가 고객들이 생일을 맞이할 때, 할인 쿠폰을 보내려고 한다. 이 일에 Support
는 관심이 있을까? 관심이 없다!
✔️ 같은 저장소를 공유하는 서로 다른 Context
각 컨텍스트 내 양방향 의존성을 떼도록 하고, 독립적인 패키지 단위로 관리할 수 있을 것이다.
그러다가, 패키지 단위가 각자 더 커지게 되면, 모듈 단위로 관리해서 MSA로 만들 수 있을 것이다.
✔️ 유비쿼터스 언어
유비쿼터스는 고대 로마에서 시작된 말로, 언제 어디에서나 존재하는
의미로, 신을 설명하기 위해서 사용되었다. 그렇다면, 유비쿼터스는 언제 어디에서나 존재하는 언어
이다.
도메인에서 사용하는 용어를 코드에 반영하지 않으면, 그 코드는 개발자에게 코드 의미를 해석해야 하는 부담이 생긴다. 코드의 가독성을 높여서 코드를 분석하고 이해하는 시간을 절약할 수 있다.
용어가 정의될 때마다 용어 사전에 기록하고, 명확하게 정의한다. 이런 과정을 통해서 다른 사람들도 공통된 언어를 사용할 수 있도록 하는 것이 좋다.
사용자와 개발자가 모두 동일한 언어로 이야기하는가를 통해서 우리가 유비쿼터스 언어를 제대로 사용하고 있는지 확인할 수 있다.
💋 도메인 주도 설계의 기본 요소
✔️ Value Object
우편번호를 String
으로 표현할 수 있을까?
Value Object는 equals()
, hashcode()
를 재정의해서, 다른 객체더라도 필드의 내용이 같다면 같은 객체라는 ‘동등성’을 보장해 줘야 한다.
시스템이 성숙하면서, 값 객체를 만드는 것이 필요해진다.
값 객체는 불변객체이다.
불변객체로 만들다 보니, 모든 상태를 생성자에서만 초기화할 수 있다. 따라서 불변객체의 크기는 점점 작아진다. 단순성이 높아지고, 객체가 더 단순해질 수록 응집도는 더 높아지고, 유지보수는 더 쉬워진다.
모든 것을 불변객체로 만들면 완벽할 것이다. 그런데, 어떤 순간에는 가변이 필요하게 될 수도 있다.
✔️ Entity
값이 바뀌고 있더라도, 이 객체를 계속해서 추적해야 하는 상황이 필요하다. 예를 들어서, 내가 이사를 가서 ‘주소’가 변경되더라도 나라는 사람을 계속해서 추적해야 한다.
Entity는 필드의 모든 상태가 식별자가 아닌 객체를 말한다. Entity는 별도로 식별자를 생성하는데, 식별자는 객체의 상태 중에서 객체의 ‘고유한 성질’을 표현할 수 있는 상태이다. 식별자는 특정한 규칙에 따라 생성하는데, UUID
나 AUTO_INCREMENT
과 같은 자동 증가 칼럼을 사용해서 생성하기도 한다. 급진적인 사람들은 이 식별자 조차 인프라의 영역이 아니냐고 주장하기도 한다.
Value Object 중에서 일부의 상태가 변경되더라도 계속해서 추적해야 하는 객체들은 Value Object에서 Entity로 승격이 된다고 이해한다면, 좋을 것 같다.
✔️ Aggregate
이제는 클래스 하나하나를 보면서, 코드를 파악하는 것보다 이것을 한 단계 위에서 묶어서 바라보고 싶어진다.
관련 객체를 하나로 묶은 군집을 Aggregate라고 한다.
Aggregate로 묶으면 좀 더 상위 수준에서 도메인 모델 간의 관계를 파악할 수 있게 된다.
Aggregate는 하나의 반이라고 볼 수 있다.
하나의 반에는 ‘반장’과 ‘학생들’이 있다. 이 ‘반장’을 Aggregate Root 혹은 Root Entity라고 한다.
위의 예시에서, Order
, OrderItem
이라는 두 개의 엔티티가 있는데 이중 왜 Order
가 Root Entity가 되었을까?
외부에서 참조하고 있을 법 한 엔티티가 Root Entity가 되는 것이다.
우리는 이런 Root Entity의 식별자를 Global 식별자
라고 한다. 그리고 Child Entity가 가지는 식별자는 Local 식별자
라고 한다. Local 식별자
는 외부에서 참조하지 못한다.
왜 객체를 한 개의 묶음으로 묶어야 하는지 알기 위해서는, 불변식
을 위해서라고 이해하면 쉽다.
불변식이 뭘까? 식당에서 위해 하나의 스시 우동 정식이라는 세트메뉴를 생성했다. 비즈니스 규칙 상, 세트메뉴의 가격은 세트에 포함되어 있는 ‘우동’과 ‘스시’의 가격의 합보다 작아야 한다는 규칙이 있다고 하자. 이 규칙을 불변식이라고 한다. 객체를 한 묶음으로 묶게 되면, 이런 규칙을 지킬 수 있다.
Aggregate는 마치 데이터베이스의 트랜잭션과 같이, 우리의 도메인 내에서의 정합성을 보장하는 것이다.
ID를 사용한 참조 방식을 통해서 복잡도를 낮추고, 한 Aggregate에서 다른 Aggregate를 수정하는 문제를 방지할 수 있다.
✔️ Repository
Aggegate가 함께 다닐 수 있도록 보장하기 위해서 Repository가 필요하다.
우리가 평소에 무심코 만드는 Repository는 Aggregate가 아닌 객체를 위해 만들기도 한다. (그래도 된다.)
하지만, 오리지널 Repository는 Aggregate를 위해 탄생한 말이고, Spring의 공식문서에도 아래와 같이 말하고 있다.
여러 가지 Aggregate가 필요한 기능이 있다.
결제 금액 계산 로직을 생각해보면, 여러 Aggregate가 필요하기도 하다.
- 상품 Aggregate에서 구매하는 상품 가격, 배송비가 필요하다.
- 주문 Aggregate에서 상품별 구매 개수가 필요하다.
- 할인 쿠폰 Aggregate에서 쿠폰별로 지정한 할인 금액, 비율에 따라 총 주문 금액을 할인할 수 있고, 쿠폰 중복 사용 여부에 따라서 로직이 복잡해진다.
- 회원 Aggregate에서 회원 등급에 따라 추가 할인이 가능할 수 있기 때문에 회원 등급이 필요하다.
✔️ Domain Service
하나의 Aggregate에 넣기 애매한 도메인 개념이 있다면, Aggregate에 억지로 넣기보다는 도메인 서비스를 이용해 도메인의 개념을 명시적으로 드러낼 수 있다.
✔️ Factory
객체를 생성하는 일이 복잡하다면, Factory를 사용해 객체 생성을 캡슐화할 수 있다.
생성자, 팩토리 클래스 등등 연관된 애그리거트에서 생성할 수 있다.
결제와 환불이 있다고 생각해보자. 환불 객체를 만드려면, 결제에 대한 정보가 필요할 것이다.
💋 계층형 아키텍처
✔️ Application Layer VS Domain Layer
객체에 메세지를 보내도록 잘 설계했다면, 모든 비즈니스 로직은 Domain Layer에서 수행하기 때문에, Application Layer는 굉장히 가벼워진다.
Application Layer는 로직을 직접 수행하기보다는 도메인 모델에 로직 수행을 위임하게 된다. 따라서 도메인 객체를 호출하고, 사용하고, 실행 흐름을 제어하고, 트랜잭션을 처리하며, 도메인 영역에서 발생한 이벤트를 처리하는 역할만 담당하게 된다. 이렇게 되면 Application Layer는 클라이언트의 영향을 받는 코드가 된다.
그렇다면 아래의 코드는 Application Layer일까 도메인 로직일까..?
실제 통장과 통장 사이의 거래가 가능한지 확인하는 로직이고, 현재 코드에서는 단순히 통화가 맞는지 잔액이 있는지만 특정 시간에 따라 거래 가능 여부가 달라질 수도 있고, 해외 거래도 가능해진다면 더 많은 검증 로직이 추가될 수 있다. 도메인 로직이라고 볼 수 있을 것이다.
✔️ Validation
검증은 표현 영역과 애플리케이션 두 곳에서 모두 수행할 수 있다.
원칙적으로 모든 값에 대한 검증은 애플리케이션에서 처리한다.
표현 영역에서 필수 값과 값의 형식을 검사하고, 애플리케이션에서는 비즈니스 로직과 관련된 부분을 검증한다.
✔️ DTO
응용 서비스가 굉장히 가벼워진 상황에서, 도메인 모델을 그대로 출력에 사용할지 별도의 DTO를 사용할 지에 대해 고민이 된다.
- Domain Model Everywhere
- 도메인 모델을 모든 계층에서 사용한다.
- 이미 가지고 있는 클래스를 사용하기 때문에 DTO 변환이 필요하지 않다.
- OSIV(Open Session In View)를 사용하고, 출력을 하기 위해서 Getter, Setter 메서드를 만들어야 할 수 있다.
- Pure Domain Model
- DTO를 사용해 도메인 모델을 분리한다.
- DTO(Data Transfer Object)는 계층 간 데이터를 전달하는 객체이다.
- 클라이언트와 도메인 모델간의 분리를 할 수 있어, 애플리케이션 외부 요구사항과 내부 요구사항을 완벽히 분리할 수 있다.
- 하지만, 항상 DTO를 만드는 것은 실용적이지 못하다.
Pure Domain Model을 사용하다 보면, 정말 무수히 많은 DTO가 생길 것이다. 어떻게 이 문제를 해결할 건가???
Value Object를 많이 만들고, 적극적으로 활용하면 된다.
예를 들어, Post
객체가 있다고 하자. Title
, Body
, Writer
가 있다고 하자.
Post
객체를 만들려면, Request로부터 Title
, Body
를 받아야 할 것이다. (Writer는 ArgumentResolver에서 가져올 수 있을 것이다.)
Post
객체를 수정한다고 해도, Title
, Body
를 받아와야 할 것이다.
Title
, Body
를 묶어서, Content
라고 부를 수 있지 않을까?
Post
에 Content
라고 하는 Value Object를 만들고, Request DTO 대신에 Content
를 사용해서 대체할 수 있을 것이다.
무수히 많은 DTO를 만드는 것이 불편하다면 Value Object를 사용할 수 있을 것이다. (이것도 트레이드오프가 있을 것이기 때문에 잘 따져봐야 할 것이다.)
💋 DIP (Dependency Inversion Principle)
결국 이렇게 도메인을 보호하려고 애쓰다보면, 아래와 같은 구조로 가게 된다.
위와 같은 아키텍처를 핵사고날 아키텍처라고 하는데, 이건 우리 프로젝트에 어울릴 수도 있고, 아닐 수도 있다.
핵사고날 아키텍처를 하겠다고 작정을 하고 시작하는 것보다, 도메인을 보호하기 위해서 구조를 수정하다가 위와 같은 모양으로 가는 것이 바람직하다.
💋 수강신청 도메인 예시를 통한 적용
✔️ 요구사항
아래와 같은 비즈니스 규칙이 있다고 하자.
이 요구사항을 분석해서, 엔티티를 만든다면 아래와 같이 설계를 할 수 있을 것이다.
✔️ 엔티티 설계
이렇게 수강 신청이 시작된다.
✔️ 문제 발생: 동시성 이슈
하지만, 동시성 이슈 때문에 아래와 같은 문제가 발생할 수 있다.
개발자라면, 이런 문제가 발생할 수 있다는 것을 알 수 있따.
그런데 개발자가 아닌 기획자는 이런 문제를 이해할 수 있을까?
다중 사용자 환경에서 ‘동시성 제어 실패’라고 말하지만, 설계 모델과 구현 모델 간의 불일치, 도메인 전문가와 개발자와의 멘탈 모델 불일치이다.
이를 해결하기 위해 강의 애그리거트를 만들어 보았다.
✔️ 문제 발생: 트랜잭션 오류
고객 시나리오는 세 가지다. 수강신청을 하는 동시에 강의정보 수정, 수강생 정보 수정이 일어날 수 있다.
강의에 있는 ‘버전’ 때문에, 우리는 트랜잭션 오류를 만나게 된다. ㅠㅠ
수강신청, 강의정보 수정, 수강생 정보 수정의 세 가지 시나리오 중에서 우리에게 가장 중요한 시나리오를 선정해야 한다. ‘수강 신청’이 돈을 쥐고 있기 때문에 가장 중요한 시나리오라고 볼 수 있다.
그러면, 아래와 같이 도메인 지식이 추가된다.
✔️ 도메인 지식 추가
결론을 짓게 된다.
‘수강생을 별도의 애그리거트로 분리하자!’
그러면, 기껏 하나로 합쳤더니 분리를 해야 한다니, 기존의 불변식은 어떻게 보장할 수 있을까?
✔️ 애그리거트 분리
위의 모델에서는 수강생의 변경에 따라서 강의가 영향을 받게 된다.
강의 수강생이라는 VO를 추가해보자!
강의라는 비즈니스를 보호하기 위해, 수강생에 대한 ‘최소한의 필드’만으로 구성된 강의 수강생 VO를 생성했다.
하지만, 이렇게 하면 또 문제가 발생한다.
강의 수강생의 승인 여부와 수강생의 승인 여부가 일치하도록 보장해야 한다.
근데 안 묶여있는데 어떻게 보장할 수 있지?
✔️ 결과적 일관성
애그리거트의 단위는 그 즉시의 일관성을 보장해야 한다는 단위라면, 최종적으로만 일관성을 지킬 수 있다면 그것을 결과적 일관성이라고 부른다.
일시적으로는 불변식이 깨지지만, 최종적으로만 지킬 수 있도록 노력하는 것이다.
배치 방식과 이벤트 방식으로 이후에 두 가지 필드의 내용이 일치하도록 맞춰주는 방식으로 구현할 수 있을 것이다.
큰 애그리거트를 사용할 때와 작은 애그리거트로 사용할 때의 장단점에 대해서, 아래와 같이 생각해볼 수 있을 것이다.
✔️ 결론
결론이다.
문제가 누구의 문제인지, 어떤 문제인지, 왜 문제인지에 대해 질문을 던져보자.
계층형 아키텍처의 계층을 나누는 것만큼 해결 영역을 나누는 것도 중요하다.
데이터베이스의 동시성 제어보다 애플리케이션 내에서의 동시성 제어가 더 한 눈에 들어온다. 해결을 무조건 데이터베이스로 끌고 가기보다는, 한 번 더 애플리케이션의 관점에서 문제를 해결할 수 있을지에 대해 고민해보자.
도메인 전문가와 함께 모델을 검증하는 것이 코드를 작성하고 테스트하는 것보다 빠를 수 있다.
💋 참고자료
- 제이슨의 강의
- https://wikidocs.net/115268
도움이 되었다면, 공감/댓글을 달아주면 깃짱에게 큰 힘이 됩니다!🌟
'우아한테크코스5기' 카테고리의 다른 글
[우테코] 리팩터링 미션 회고 (feat. 우아한 객체지향) (6) | 2023.10.30 |
---|---|
[우테코] 점진적 리팩터링: 달리는 기차의 바퀴를 갈아끼우기 (0) | 2023.09.19 |
[우테코] 톰캣(HTTP Server) 구현하기 미션 회고 (0) | 2023.09.15 |
[우테코] 레벨3 레벨인터뷰: 인터뷰 실제 대화 스크립트, 피드백 (0) | 2023.08.29 |
[우테코] 레벨로그: 레벨3 동안 공부한 내용들을 정리하며 (2) | 2023.08.29 |