JDON 아직 할 이야기가 많이 남았어요...ㅎㅎ
Software Architecture ?
소프트웨어 아키텍처는 모든 소프트웨어 시스템의 기본 구조를 말하며 시스템이 제대로 기능하고 작동하도록 하는 모든 측면을 말한다. 구성요소의 설계, 구성 요소 간의 관계, 사용자 상호 작용 및 시스템에 대한 사용자의 요구를 모두 포함하는 단어이다.
Layered Architecture 도입하기
왜 이런 고민을 하게 되었을까요?
돌아만 가면 되는거 아닐까? 서비스가 제공하고자 하는 기능을 사용자에게 잘 전달할 수만 있으면 되는거 아닐까?ㅎㅎ
겠냐고~
지금의 아키텍처 이전에는 물론 나름의 계층이랄까... 그냥 패키지랄까...ㅎㅎ 나눠져있긴했지만 책임이 불분명하여 서비스를 열고 나서 사용자의 피드백을 반영하거나 새로운 기능을 추가하게 되었을 때 수정해야 할 코드를 찾기 위한 시간이 불필요하게 소비되었고, 또 코드 수정으로 인한 사이드 이펙트를 예상하기 어렵다는 문제를 겪게 되었다. 그래서 우리는 계층별로 명확한 책임을 나누어 서비스에 대한 유지보수성을 높이고자 하였다.
Layered Architecture 가 뭔데요..
소프트웨어 개발에서 일반적으로 가장 많이 사용되는 아키텍처로 Layer의 수에 따라 N Layered Architecture라고 불려지는데 단일 소프트웨어 단위로 함께 기능하는 여러 개별 수평 Layer로 구성된다. 각 Layer은 애플리케이션 내에서의 특정 역할과 관심사 별로 구분된다.
주요한 특징은 아래 방향 즉, 아래 Layer로만 접근할 수 있다는 것과 관심사의 분리를 위해서 Layer를 격리했기 때문에 하나의 Layer에서 일어난 변경사항은 해당 Layer 내에 격리되어 다른 Layer와의 종속성을 낮출 수 있다는 것이다.
Layered Architecture에서 각 Layer 의 역할이나 명칭, 개수는 모두 동일하진 않지만 대부분 사용자의 요청 및 응답을 담당하는 Presentation, 애플리케이션의 흐름을 제어하는 Application, 도메인의 핵심 로직을 관리하는 Domain, 상위 계층을 지원하기 위한 Infrastructure 레이어로 구성된다.
Layered Architecture 장점
- 관심사의 분리
- 각 구성 요소의 단일 책임을 보장. ( 종속성 저하 )
- 각 Layer가 다른 Layer와 독립적이어서 변경 사항이 다른 Layer로 영향을 끼치지 않는다.
- 낮은 러닝 커브
- 가장 접하기 쉬운 구조
- 테스트가 쉬움
- 특정 Layer에 속한 구성요소들이 분리되어 있어 모든 Layer가 개별적으로 단위 테스트될 수 있다.
Layered Architecture, 물론 단점도 있습니다.
- 확장성
- 애플리케이션 복잡도가 증가하고 프로젝트에 더 많은 기능을 추가해야하는 경우 확장하는데 비용이 크다. (모놀리식 구현 경향)
- 특정 Layer에 대한 변경은 전체 시스템을 재배포해야 함을 의미한다. ( 큰 Application의 경우 더 문제가 된다. )
- 상호 의존성
- 하나의 계층이 데이터 수신을 위해선 상위 Layer에 의존하기에 상호 의존성이 존재한다.
- 성능
- 비즈니스 요청을 이행하기 위해 Architecture의 여러 Layer를 거쳐야 하는 비효율성으로 인해 고성능 Appication에 적합하지 않음. (병렬처리가 불가능)
우리가 원했던 것...
[ JDON의 소프트웨어 아키텍처의 목표 ]
- 유지 보수하기 좋은 시스템을 구축하자
- 남의 코드라도 어떤 로직이 어느 계층에 있을지 빠르게 파악하고 싶어요
- 수정하려는 로직을 구현한 코드에 빠르게 도달하고 싶어요
- 코드를 수정해도 사이드이펙트를 추측하기 쉬웠으면 좋겠어요
- Test Code 작성이 쉬웠으면 좋겠다
- 제발~~~ 의존성 때문에 test 짜기가 겁나요;; → 안해;
우리의 요구사항을 만족시킬 수 있는 것은 Layered Architecture 뿐이었을까?
Hexagonal Architecture
입력 포트와 출력 포트를 구분하고 낮은 결합도로 유연한 코드 구성과 테스트의 용이성을 위한 아키텍처
의존성을 역전시켜 도메인 코드가 다른 바깥쪽 코드에 의존하지 않게함으로써 영속성과 UI와 관련된 문제로부터 도메인 로직을 분리하여 코드를 변경할 이유를 줄인다. 도메인, 영속성, UI의 계층이 각 계층의 문제에만 집중하여 자유롭게 모델링할 수 있고 변경할 이유가 줄어 유지보수성이 높아진다.
- Adapter : 특정 포트에 연결되어 외부와 도메인간 통신을 가능하게 함
- Input Port : 동작을 유발하는 외부 요청 처리
- Output Port : 동작의 결과를 외부로 전달
- Use Case, Entity : 애플리케이션의 핵심 기능, 비즈니스 로직이나 규칙을 캡슐화
장점 1) SRP, OCP, DIP
- SRP는 인바운드,아웃바운드, 어댑터 등의 역할을 통해 확인할 수 있었다.
- OCP는 이와 같은 맥락으로, 예를 들어 내부에서 상품명을 가져올 때, 어떻게 가져오는지는 선언부가 알 필요가 없다. 아웃바운드 포트가 이에 대한 구현을 담당하고 있으므로, 이는 곧 확장에 대한 유연함을 증명하기도 한다.
- DIP는 외부 인터페이스들이 내부 로직에 의존하고 있다. 때문에 비즈니스 로직이 외부의 변화(컨트롤러, 서비스 등)에 영향을 받지 않는다.
장점 2) 비즈니스 로직에 집중하여 개발이 가능
- 구현하고자 하는 목표만 개발하면된다. 외부에서 어떻게 호출하는지는 개발과 업무 프로세스만 알고 있으면 됨.
- 나중에 방식이 바뀌더라도 부품을 갈아끼우는 것처럼 해당 포트만 바꿔주면 된다. 선언부는 알 필요가 없음.
단점 1) 초기 학습 곡선이 높다.
- 설계를 하려면 전반적인 업무 프로세스와 인프라 구조를 모두 알고있어야함. 따라서 설계 난이도가 어렵다.
단점 2) 코드의 복잡도가 올라간다. 추상화 레벨이 올라감, 불필요한 오버헤드의 발생 가능성이 있다.
- 코드의 양이 많아지고 구조가 복잡해진다
- 새로운 모델들(포트, 어댑터)이 추가된 만큼 레이어들 간에 전달해줘야 하는 맵핑객체(DTO 같은거)가 추가된다. 이에 따른 비용이 발생하기 마련.
Layered Architecture를 선택하겠습니다. 근데 이제 SOLID를 곁들인...
Layered Architecture는 데이터베이스 주도 설계가 되기 쉽고, 하위 레이어를 바라보다보니 레이어의 책임을 명확하게 하지 않으면 특정 레이어가 많은 역할을 담당하며 뚱뚱해지거나, 중간의 레이어를 건너뛰고도 손쉽게 구현할 수 있기 때문에 (의존성에 대한 제한이 없음) 막짜고 싶은 유혹에 빠질 위험성이 있지만 우리가 가진 리팩토링 시간(약 일주일?)을 보았을 때 현실적으로 다른 아키텍처에 비해 낮은 학습 곡선을 가진 Layered Architecture를 선택하기로 하였다.
대신! 우리는 우리의 목표를 달성하기 위해서 SOLID에 의거하여 각 Layer에 대해 명확한 책임분리를 하고 엄격한 규칙을 세우고, 코드리뷰를 열심히 하기로 했다
SOLID 란?
로버트 마틴이 제안한 객체지향 프로그래밍, 설계를 위한 다섯가지 설계 원칙으로 이해하기 쉽고 유지보수와 확장이 쉬운 유연한 시스템을 만드는 것을 목표로 한다.
SOLID의 다섯가지 설계 원칙은 Single Responsibility, Open-closed, Liskov Substitution, Interface Segregation, Dependency Inversion 이 있다.
[ Single Responsibility, 단일 책임 원칙 ]
컴포넌트를 변경하는 이유는 오직 하나뿐이어야 한다. (한 클래스는 하나의 책임만 가진다.)
[ Open-closed, 개방 폐쇄 원칙 ]
클래스는 확장을 위해 열려있지만 수정에는 닫혀있어야 한다.
[ Liskov Substitution, 리스코프 치환 원칙 ]
클래스 A가 클래스 B의 하위 유형인 경우 프로그램의 동작을 방해하지 않고 B를 A로 대체할 수 있어야 힌다.
[ Interface Segregation, 인터페이스 분리 원칙 ]
클라이언트가 오로지 자신이 필요로 하는 메서드만 알면 되도록 넓은 인터페이스를 특화된 인터페이스로 분리해야한다.
[ Dependency Inversion, 의존성 역전 원칙 ]
코드상의 어떤 의존성이든 그 방향을 바꿀 수 있다.
객체지향......
시스템을 상호작용하는 자율적인 객체들로 구성하는 것으로 적절한 책임을 수행하는 역할 간의 유연하고 견고한 협력 관계를 구축하는 것이다. 자율적인 객체는 상태와 행위를 함께 가지며 협력 내에서 정해진 역할을 수행한다.
- 캡슐화, Encapsulation
- 객체를 캡슐로 싸서 내부를 보호하고 볼 수 없게 하는 것
- 객체는 캡슐화가 기본 원칙이지만 외부와의 접속을 위해 몇 부분만 공개 노출한다.
- 자바에서의 객체는 클래스라는 캡슐화를 사용하며, 필드(멤버 변수)와 메소드(멤버 함수)로 구성된다.
- 상속, Inheritance
- 자식 클래스가 부모 클래스의 속성을 물려받고 기능을 추가하여 확장(extends)하는 개념
- 부모 클래스를 슈퍼 클래스라고 부르고 자식 클래스를 서브 클래스라고 부른다.
- 상속은 슈퍼 클래스의 필드와 메소드를 물려받아 코드를 재사용함으로써 코드 작성에 드는 시간과 비용을 줄인다.
- 다형성, Polymorphism
- 같은 이름의 메소드가 클래스 혹은 객체에 따라 다르게 동작하도록 구현되는 것
- 동물 클래스를 상속받은 고양이, 강아지 클래스의 메소드, speak()은 각자 다른 울음소리를 출력한다.
- 메소드 오버라이딩
- 슈퍼 클래스에서 구현된 메소드를 서브 클래스에서 동일한 이름으로 자신의 특징에 맞게 재정의한 것
- 메소드 오버로딩
- 클래스 내에서 이름이 같지만 서로 다르게 동작하는 메소드를 여러 개 만드는 것
- 같은 이름의 메소드가 클래스 혹은 객체에 따라 다르게 동작하도록 구현되는 것
길고 긴 서론 지나 이제 진짜 JDON의 소프트웨어 아키텍처를 설명해볼게요
[ Presentation ]
사용자의 요청 및 응답을 처리해요.
Controller, Dto
[ Application ]
애플리케이션의 흐름을 제어해요.
Facade
[ Domain(Core) ]
도메인의 핵심 로직을 관리해요.
Service, ServiceImpl, Reader/Store, Factory, Info/Command
[ Infrastructure ]
상위 계층을 지원하는 일반화된 기술적 기능을 제공해요.
JpaRepository, Reader/Store Impl, Factory Impl
SOLID를 어떻게 고려했는지 좀 더 말해볼게요.
- SRP : 각 layer가 특정한 역할과 책임을 가지도록 설계하였다.
- OCP : 각 계층의 제공 메서드를 Interface로 추상화함으로써 확장은 쉬워지고 주변의 변화에는 영향을 받지 않는다.
- ISP : 영속성 제공 인터페이스를 Reader, Store로 분리하여 서비스 계층에서 클라이언트가 사용하고자 하는 메서드에만 의존하도록 하였다.
- DIP : 구현체에 의존하지 않고 추상화에 의존하게되어 내부의 비즈니스 로직(Core)이 외부의 변화(Presentation, Infrastructure)에 영향을 받지 않는다.
목표했던 결과를 얻었을까요??
- 도메인 프로세스를 고수준 모듈로 추상화하고 구현체는 저수준 모듈로 구현하여 도메인 프로세스를 파악하기 쉬워졌다.
- 계층별로 관심사를 분리하여 버그를 수정하거나, 기능 자체를 수정하거나, 새로운 기능을 추가하려고 할 때 어떤 계층에서 어떤 부분을 추가하면 될 지 빠르게 파악할 수 있었다.
- test code를 짜기 편해졌다. (더는 두렵지 않다...!)
JDON 의 아키텍처, 완벽한가요??
ㅎㅎ 그렇다면 좋겠지만~
- Service 인터페이스가 너무 많은 역할을 가지고 있는 것 같아서 Service Interface 내에서도 좀 더 세분화하여 Interface를 분리했으면 더 책임 분리가 잘 되고 클라이언트 입장에서도 관심가져야하는 메서드에만 관심을 가질 수 있었겠다는 아쉬움이 있습니다.
- 또한 다른 도메인의 Service간 순환참조 방지, 작은 규모의 트랜잭션, 동시성 로직 분리를 위해 Facade 계층을 고려하면서, 이런 부분이 굳이 필요하지 않은 도메인이더라도 패키지간 통일성을 위해서 전부 Facade 계층을 도입하였는데 이런 부분이 오히려 싱크홀 안티패턴이 아닌지 여전히 고민이 되는 부분이다. 여기서두~~~~ Service Interface가 더 책임분리되었으면 Facade가 더 제 역할을 할 수 있었을 것 같긴하지만서두.....
- 싱크홀 안티패턴이란 특정 레이어가 아무런 로직도 수행하지 않고 들어온 요청을 그대로 다시 하위 레이어로 내보내는 경우를 의미한다. 이런 흐름은 불필요한 리소스 낭비를 초래한다. 전체 흐름 중에서 약 20%가 싱크홀이라면 그럭저럭 나쁘지 않은 수준이라고 한다. [ 참고 ]
- 추상화가 좀 과도했을 수 있다.ㅎㅎ Service랑 Factory 까지는......
'Trouble Shooting' 카테고리의 다른 글
Redisson의 pub/sub 분산락을 사용하여 동시성 문제 해결하기 (0) | 2024.07.17 |
---|---|
커버링 인덱스를 활용하여 페이지네이션 조회 속도 개선하기 (0) | 2024.07.17 |
납득이 가는 멀티모듈 구조 도입기 (0) | 2024.07.06 |
브라우저에서 10개 이상의 동시 요청이 보내지지 않는다면 HTTP1.1을 사용하고 있지 않은지 확인해보세요 (^__ ^).. (0) | 2024.06.20 |
댓글