개요

로버트 C 마틴은 소프트웨어의 가치를 행위 가치와 구조 가치로 나누었다.
행위 가치는 소프트웨어의 기능을 말하고 구조 가치는 소프트웨어를 정말로 부드럽게 만드는 것을 의미한다.
마틴은 토끼와 거북이의 경주를 예로 들며 가장 빨리가는 방법은 제대로 가는 것이며 코드와 설계의 구조를 깔끔하게 만들려는 생각을 하지 않고 기능 구현만 목적으로 삼으면 소프트웨어가 엉망이 된 상황에 대처하는 데 더 많은 비용이 든다고 말한다.

이번에는 소프트웨어의 구조가치인 소프트웨어 아키텍처에 대해 알아보자.

3-Layered-Architecture

웹의 경우 사용자의 요청에 따라 다른 로직의 흐름을 실행해야한다. 이 역할을 하는 것이 바로 컨트롤러다.

또한 사용자의 활동을 기록해야하기 때문에 데이터베이스가 필요하다. 이 둘은 서비스를 운영하는데에 필수적인 요소로 이 둘만 가지고도 서비스의 운영이 가능하다는 것을 의미한다.

그러나 우리가 사용하는 웹 페이지를 보면 중복되는 부분이 있다.
예를 들어 메인 화면에서도 사용자가 받은 메일의 갯수를 보여줘야하고 검색 결과 페이지에서도 메일의 갯수를 보여줘야한다면 컨트롤러의 로직이 중복된다.

//컨트롤러
@Controller
@ReqiredArgsConstructor
public class SampleController{
	private final SampleRepository sampleRepository;
...
	@GetMapping("/main")
	public String mainPage(HttpSession session, Model model) {
		LoginInfo loginInfo = session.getAttribute("loginInfo");
		int mailCount = sampleRepository.selectMailCountByUsername(loginInfo.getUsername());
		model.addAttribute("mailCount" , mailCount);
		...
		return "mainPage";
	}
	@GetMapping("/search")
	public String searchPage(HttpSession session, Model model) {
			LoginInfo loginInfo = session.getAttribute("loginInfo");
			int mailCount = sampleRepository.selectMailCountByUsername(loginInfo.getUsername());
			model.addAttribute("mailCount" , mailCount);
			...
			return "searchPage";
	}
	...
}

만약 이 코드 뭉치가 수정되어야하는 일이 생긴다면 이 로직이 쓰인 부분을 돌아다니며 수정해줘야할 것이다.
중복되고 반복되는 코드는 개발자를 불안하게한다.

Q) 중복되는 부분을 해결하려면?
A) 별도의 객체에서 구현하고 컨트롤러에서 사용한다.

image 이렇게 사용자에게 응답하는 데이터를 만드는 로직은 해당 서비스의 핵심 로직으로 비즈니스 로직이라고 한다. 어렵게 말하면 시스템의 목적인 비즈니스 영역의 업무 규칙, 흐름, 개념을 표현하는 용어다.

서비스 계층에서는 여러 건의 DB호출이 하나의 동작으로 묶여야하므로 서비스 계층의 메소드는 하나의 트랜잭션 단위로 동작한다. image 따라서 3계층은 이렇게 구성된다.
프레젠테이션 계층은 우리의 서비스와 유저의 경계에 있는 계층으로 주요 관심사는 화면의 표현이다. 비즈니스 로직 계층의 관심사는 서비스의 핵심 비즈니스를 정의하고 관리하는 것이다.
데이터 엑세스 계층의 관심사는 데이터베이스 연결과 데이터 처리이다.

레이어드 아키텍처의 장점을 극대화하고 유지보수성을 위해 몇 가지 규칙을 둔다.

  • 상위 계층이 하위 계층을 호출하는 단방향성을 가져야한다.
  • 상위 계층은 하위의 여러 계층을 알 필요 없이 바로 밑의 계층만 사용한다.
  • 상위 계층이 하위 계층에 영향을 받지 않게 해야한다.
  • 하위 계층은 자신을 사용하는 상위 계층을 알지 못하게 구성해야한다.
  • 계층 간의 호출은 인터페이스를 통해 호출하는 것이 바람직하다.(구체화에 의존하지 않게 하여 계층간 약한 결합을 유지하기 위해서다.)

계층간 인터페이스를 사용하는 구조는 상위 계층이 직접 하위 계층을 호출하지 않고 추상적인 인터페이스에 의존하게해서 하위 계층에서는 추상적 인터페이스를 이행하는 여러 구현체를 선택적으로 적용할 수 있다. 이를 DIP라고 한다.

그러나 OCP는 달성하지 못한다. OCP란 개체의 행위는 확장할 수 있어야하지만 이 때 개체를 변경해서는 안된다는 의미를 담고있다.

이는 각 계층이 각자 자신이 제공하는 기능에 대한 인터페이스를 직접 정의하고 소유하고있기 때문이다.
이런 구조에서는 의존성이 상위 계층에서 하위 계층으로 생기게 된다.
따라서 하위 계층의 유형이 추가되어 확장될 때 닫혀있어야 할 상위 계층이 하위 계층에서 정의한 특성에 영향을 받게 된다.

데이터 엑세스 계층이 변경되었을 때 비즈니스 로직 계층이 변경되지 않아야만 OCP를 달성했다고 할 수 있을 것인데 만약 구현체가 A에서 B로 변경된 경우라면 상위 계층에 영향을 주지 않지만 인터페이스가 변경되면 비즈니스 로직 계층이 데이터 엑세스 계층의 변경에 영향을 받을 수 밖에 없다.

해결 방법으로는 DIP를 적용해서 데이터 엑세스 계층에서 정의한 인터페이스를 상위 계층으로 옮기는 방법을 생각해볼 수 있다.

이렇게 되면 위에서 아래로 흘렀던 의존관계를 역전시키고 고수준 영역이 저수준 영역의 변경에 영향을 받지 않게 될 수 있다. 스크린샷 2024-07-01 시간: 22 39 54

데이터베이스 중심 아키텍처의 문제

데이터베이스 중심 아키텍처란 특정 RDB에 의존한 데이터 모델링을 수행한 뒤 이 모델을 중심에 두고 애플리케이션을 구현하는 것을 말한다.

데이터베이스 중심 아키텍처의 문제는 DB와 비즈니스 로직이 끈끈하게 결합되어있다는 것이다.

스프링을 이용하여 개발할 경우 컨트롤러, 서비스, 레포지토리, DTO를 메인으로 구성되고 비즈니스 개념과 규칙들은 테이블과 SQL쿼리에 존재한다. 이러한 구조에서 만약 NoSQL로 변경하려고 해도 쉽게 변경할 수 없다.

성능도 데이터베이스의 성능에 의존하게된다. 사실상 애플리케이션이 할 일이 없고 데이터가 증가됨에 따라 성능은 지속적으로 떨어지고 이를 최적화하기 위해서 데이터베이스의 사양과 용량을 증가시키고 쿼리 튜닝에 집중할 수밖에 없다. 사실상 정말 바쁜 것은 데이터베이스이기 때문에 스케일아웃을 해봤자 큰 의미가 없다.

데이터베이스 중심 아키텍처에서 도메인 중심 아키텍처로의 이동

위에서 살펴본 문제점으로 인해 새로운, 말하자면 차세대 아키텍쳐들이 등장했다.

헥사고날, 클린 아키텍처 등이 그 대표적인 예다.
도메인 중심의 아키텍처는 비즈니스 규칙이 세부사항에서 더 추상화되고 분리될수록 유연하게 대처가능하다는 것이 핵심이다.

트랜잭션 스크립트, 도메인 모델

트랜잭션 스크립트란 하나의 트랜잭션으로 구성된 로직을 단일 함수에서 처리하는 구조를 말한다.
일반적인 3티어 아키텍쳐의 구조를 생각하면 된다.

레포지토리는 단순히 데이터베이스의 테이블과 대응되며 레포지토리를 이용해서 비즈니스 로직을 구성한다.
이런 구조에서는 모든 비즈니스 행위의 책임은 서비스 계층에 있고 시간이 지남에 따라 서비스의 스케일이 커지게 되면 도메인 객체는 정보 묶음의 역할만 수행하게된다.

그리고 비슷한 요구사항의 경우 서비스에 중복되는 코드가 계속 생겨나게되는 문제가 있다.

도메인 모델 패턴은 도메인 객체가 데이터와 비즈니스 로직을 담고있다.
이렇게 도메인 객체는 각 비즈니스에 대한 책임을 가지고있고 서비스의 행위를 도메인 객체에게 위임해서 처리한다.

서비스의 책임이 도메인 객체에 분산되어 서비스의 스케일이 커지더라도 서비스의 크기가 비대해지지 않고 이해하기 쉽고 재사용성이 용이한 구조가 된다.

헥사고날 아키텍처

image

핵사고날 아키텍처는 그 이름에서도 알 수 있듯이 다양한 면을 가진다. 이렇게 외부에는 열려있고 도메인 영역은 중앙에 집중되어있는 구조라고 할 수 있다.

앞에서 살펴본 3티어 아키텍처는 각 계층의 사이를 부드럽게 만들기 위해 인터페이스를 두어 결합도를 낮춘다. 그러나 현대의 애플리케이션에는 다양한 계층이 존재할 수 있다. 그렇기 때문에 계층 구조에서는 이를 지원하기가 힘들어지고 다방면으로 열려있는 헥사고날 아키텍처가 이러한 문제를 해결할 수 있다.

헥사고날 아키텍처는 고수준의 내부 영역과 저수준의 외부 영역으로 나뉜다. 내부 영역에는 변화에 가장 영향을 적게 받아야하는 비즈니스 로직이 존재하며 기술 독립적인 영역이다.

외부 영역은 외부에서 들어오는 요청을 처리하는 인바운드 어댑터와 외부에서 호출되어 응답하는 아웃바운드 어댑터로 이루어진다.

쉽게 생각하면 외부에서 들어오니까 인(in)인 것이고 내부에서 나가니까 아웃(out)인 것이다.

그리고 외부 영역과 내부 영역의 인터페이스가 필요하다. 이를 포트라고 부른다.

포트도 인/아웃바운드 포트로 구분되는데 인바운드 포트는 내부 영역의 사용을 위해 표출된 API고 외부 영역의 인바운드 어댑터에 의해 호출된다. 아웃 바운드 포트는 내부 영역이 외부 영역을 호출하는 방법을 정의한다.

지금 까지 개념을 정리해봤을 때 인바 운드 어댑터는 REST 컨트롤러, 이벤트 구독 핸들러 등이 있어야하고 아웃바운드 어댑터는 데이터베이스 엑세스, 이벤트 메시지를 발행하는 클래스 등을 위치시킬 수 있다.