💡 소개
이 세션에서는 가장 먼저 아키텍처를 구성할 때 가장 중요한 것이 무엇인지 돌이켜봅니다. 그리고 대부분의 개발자들에게 익숙한 계층형 아키텍처가 갖는 문제점은 무엇인지 이야기해 보고, 조금이나마 유지 보수하기 쉬운 아키텍처를 구성할 수 있는 방법에 대해 소개합니다.
아키텍처에서 중요한 것은 무엇인가?
프레임워크, DB, 테이블 설계, 캐시 등은 세부 사항이다. 중요한 것은 핵심 비즈니스 로직과 유스케이스이다.
- 핵심 비즈니스 로직: 사업적으로 의미 있는 규칙 또는 절차로써, 시스템의 유무와 상관없이 존재하는 것. ex) 대출을 해주고 이자를 받는 것은 은행이 돈을 버는 업무 규칙
- 핵심 비즈니스 데이터: 핵심 비즈니스 로직을 처리하기 위한 데이터
이를 가지고 도메인 엔티티를 뽑아낼 수 있음. (핵심 비즈니스 데이터 + 핵심 비즈니스 로직)
유스케이스: 시스템이 있어야 유효한 비즈니스 로직
핵심 비즈니스 로직은 시스템 없이도 유효하므로 가장 중요함. 유스케이스는 시스템이 있어야만 유효하므로 그 다음으로 가장 중요하다.
→ 핵심은 비즈니스와의 연관성이다. 세부 사항(웹, DB, 프레임워크) 내용은 없음
가장 중요한 도메인과 덜 중요한 인프라 (세부사항)가 있다.
- 덜 중요한 인프라: 웹, 캐시, DB, 마이크로서비스 등
- 가장 중요한 도메인: 도메인 엔티티, 유스케이스 등
도메인은 세부사항에 의존하면 안된다.
다음 코드의 경우, JDBC에 대한 의존성이 발생한다.
JDBC에서 JPA로 바꿀 때, 바꾸고 싶은 건 DB 접근 기술이지만 비즈니스 로직도 같이 바뀌게 된다.
잘못된 계층형 아키텍처의 형태
일반적인 계층형 아키텍처에 대한 내용은 다음과 같다.
- 계층형 아키텍처는 우리에게 가장 익숙하고 잘 알려진 아키텍처 중 하나임.
- 계층형 아키텍처의 구성에 따라 몇 가지 문제점이 생길 수 있으며, 일부 개선할 부분이 있을 수 있음.
하지만 계층형 아키텍처 자체가 나쁜 것이 아니라, 구현 방식에 따른 문제일 뿐이다.
계층형 아키텍처가 가지는 문제점?
데이터베이스 중심의 설계와 개발이 된다.
- 계층형 아키텍처에서 의존성의 방향은 항상 다음 계층을 향한다.
- 가장 중요한 비즈니스 계층은 영속성 계층에 의존하게 된다.
- 가장 마지막 의존성인 DB 레이어는 의존 대상이 없으므로 작업이 가장 수월하게 되고, 자연스럽게 설계와 개발이 DB로부터 시작된다.
넓은 서비스로 인해 유스케이스 파악이 어렵다.
- 계층형 아키텍처에서 비즈니스 로직은 하나의 Service에 구현된다.
- 예를 들어, 멤버 관련 비즈니스 로직은 MemberService에 몰빵된다.
MemberService만 보았을 때 얘의 유스케이스가 드러나지 않는다. - 요구사항은 주로 유스케이스 단위로 들어온다.
(ex. 멤버 추가 시에 동일한 이메일은 등록이 불가능하게 해주세요) - 넓은 서비스로 인해 코드를 찾는 시간이 길어져서 생산성이 떨어진다.
- 테스트 작성 및 관리가 어려워진다.
- 테스트 관리: 서비스가 넓어지면서 테스트 역시 넓어지므로 관리가 어렵다.
- 테스트 작성: 주로 빈약한 엔티티 모델을 사용하기 때문에 번거로워진다.
- 가장 마지막 의존성인 DB 레이어는 의존 대상이 없으므로 작업이 가장 수월하게 되고, 자연스럽게 설계와 개발이 DB로부터 시작된다.
- 빈약한 엔티티 모델이란? 객체에 정보만 담겨있고 아무런 기능도 갖고 있지 않는 모델. 이런 모델을 사용할 경우 비즈니스 로직은 서비스 계층에 구현된다.
- 빈약한 엔티티 모델을 사용할 경우 응집도가 떨어지고, 로직 재사용과 테스트는 어려워진다. 서비스에 대한 의존관계 주입의 어려움으로 테스트가 어려워질 수도 있다.
- 따라서 풍부한 엔티티 모델을 사용해서 객체지향적으로 개선할 수 있다. 엔티티의 데이터만 필요로 하는 로직은 엔티티 내부에 구현하고 메시지를 통해 재사용하도록 한다. 풍부한 엔티티 모델을 통해 응집도는 높고, 테스트는 쉽고, 객체지향적인 코드를 구현할 수 있음
memberService.disableMember(member)
→member.disable()
- 서로 다른 이유로 변경되는 코드들이 결합하기 쉽다.
- SRP = 하나의 일만 해야 한다 (X), 하나의 변경 이유만 가져야 한다 (O)
- 일반적인 계층형 아키텍처는 SRP를 위반하기 쉽다.
- 서로 다른 속도(수준)로 변경되는 코드들이 결합되기 쉽다.
- 서로 다른 속도란 서로 다른 수준(level)으로, 수준은 ‘입력과 출력까지의 거리’이다.
- 고수준: 입력과 출력으로부터 먼 것
저수준: 입력(HTTP, 웹소켓 등)과 출력(캐시, DB, 외부 API 등)으로부터 가까운 것 - 고수준과 저수준 영역은 서로 다른 속도로 변경된다.
- 일반적인 계층형 아키텍처는 DIP를 위반하기 쉽다. 아래와 같이 JDBC를 JPA로 바꿀 경우, 서비스 계층에서 바뀌는 영역은 다음과 같다. 서비스 계층이 DB 계층의 영역의 수정에 영향을 받는다.
클린 아키텍처란?
테스트하기 쉬운 아키텍처
테스트의 용이성은 좋은 아키텍처가 지녀야 할 속성이다.
외부 요인(프레임워크, 웹, DB) 없이도 유스케이스 전부를 단위 테스트 할 수 있어야 한다.
@SpringBootTest
같은 경우는 스프링 없이는 테스트가 불가능하다는 의미이니 좋은 테스트와 아키텍처는 아니다.
소리치는 아키텍처
어떤 프레임워크나 DB를 사용하는지는 드러나지 않아도 되는 세부사항이다.
새롭게 합류한 프로그래머는 어떤 시스템인지, 어떤 유스케이스인지를 손쉽게 파악할 수 있어야 한다.
플러그인 아키텍처
핵심이 되는 부분을 세부 사항으로부터 분리하고, 이들을 플러그인 형태로 손쉽게 바꿔칠 수 있는 구조.
코어 시스템(핵심 비즈니스)을 중심으로 플러그인을 붙여나가는 형태로, 이를 통해 세부사항의 교체가 수월해진다. 세부 사항이 바뀌더라도 코어 시스템인 핵심 비즈니스는 영향을 받지 않는다.
이를 구현하기 위해서는 제대로 의존성을 관리하는 것이 중요하다.
도메인 중심 아키텍처
DIP: 고수준 모듈이 저수준 모듈에 의존해서는 안되며, 저수준 모듈이 고수준 모듈에 의존해야 한다.
DIP를 적용하면 다형성을 통해 어떤 의존성도 그 방향을 역전 시킬 수 있다.
다음 코드의 경우, DIP를 통해 영속성의 의존성을 제거해야 한다. Member 영속성 엔티티는 의존성 제거가 불가능하다.
이런 의존성을 끊기 위해서는 인터페이스를 사용해야 한다. Repository 인터페이스를 사용해야 한다. 또, ORM-Managed Entity를 그대로 사용하는 것은 영속성 컨텍스트에 의한 보이지 않는 결합이 존재하는 것이기 때문에 이것도 끊어내야 한다.
도메인 엔티티와 Persistence 엔티티를 따로 분리하는 것이 좋다.
하지만 이러한 작업들은 비용이 상당히 크다. 필요한 경우가 아니라면 엔티티는 하나로 쓰는 것이 낫긴 하다.
아키텍처를 구성할 때 중요한 것
좋은 아키텍처를 만드는 코드 스타일
- 서비스 계층(가능하면 프레젠테이션 계층까지)을 유스케이스 단위로 분리하라. 그럴 경우 테스트도 분리되게 된다.
- 추상화(PSA)되지 않은 세부 사항에 의존한다면 DIP를 적용하라
- 객체의 데이터만 필요로 하는 로직들은 객체의 내부로 이동하라
- package-private 가시성을 적극 활용하라
세부 결정 사항들은 결정을 최대한 미루자
늦장 부리기.
- 테이블 설계 → 테이블에 확신이 들 때까지 설계를 미룸
- 추상화 → 추상화 도입으로 인해 비용이 더 저렴해지는 시점까지 미룸
추상화가 너무 심해지면 독이 될 수 있다 (무슨 구현체를 쓰는지 알기 어려움) - 서비스 분리 → 생존을 분리하지 않으면 안되는 시점까지 미룸
- 캐시 적용 → slow 요청들에 의해 필요해지는 시점까지 미룸
좋은 아키텍트는 결정되지 않은 사항의 수를 최대화한다.
- Robert C. Martin
'개발 일반 > 아키텍처' 카테고리의 다른 글
Service 계층에 적당한 무게의 Repository 의존성 지우기 (1) | 2023.04.13 |
---|