Spring framework는 자바 기반의 오픈 소스 애플리케이션 프레임워크로서 J2EE 환경에서 시스템 개발을 쉽게 하고 유지보수성을 높이기 위해 개발됐죠. 스프링 이전에 사용하던 J2EE의 EJB는 엔터프라이즈 환경에서 안정성과 확장성을 제공하는 훌륭한 기술 스택이었지만 그만큼 무겁고 사용하기 어려워 비효율적이었습니다. 더불어 EJB는 EJB의 핵심 기술인 컨테이너 기술이 EJB의 제공 업체마다 구현이 달랐고, 그로 인해 사용하는 프로그램 자체가 특정 Vendor의 기술에 종속되는 문제가 발생할 수 있었죠.
스프링은 이러한 한계와 극복을 달성하기 위해 개발됐습니다. 경량화된 구조, 간소화된 설정, 제어 역전, 자동화된 의존성 주입 등 기능을 제공함으로써 EJB 단점을 해결했죠.
하지만 누구나 쉽게 서버를 만들 수 있게 된 만큼 잘못 사용하는 사례도 늘었습니다. 2부에서는 자주 저지르는 실수와 안티패턴을 살펴보고, 스프링과 관련해서 자주 오해하는 개념인 '서비스 컴포넌트'에 의해 얘기해보죠.
나아가 스프링을 사용하는 대부분의 조직에서 사용 중인 레이어드 아키텍처와 잘못 적용한 사례, 더 나은 방식, 추가로 아키텍처를 어떻게 진화시킬 수 있는지 살펴보죠.
가장 먼저 소개할 내용은 스프링 개발자가 많이 저지르는 '구조적 실수'입니다. 개발에는 정답이 없지만 '이렇게 개발하면 유지보수나 확장성 관점에서 좋지 못하다'라고 알려진 안티패턴은 있습니다.
6.1 스마트 UI
스마트 UI 패턴은 에릭 에반스의 저서 <도메인 주도 설계>에서 소개해 유명해진 안티패턴입니다. 다음과 가진 특징을 가진 코드를 얘기하죠.
1. 스마트 UI는 데이터 입출력을 UI 레벨에서 처리합니다.
2. 스마트 UI는 비즈니스 로직도 UI 레벨에서 처리합니다.
3. 스마트 UI는 데이터베이스와 통신하는 코드도 UI 레벨에서 처리합니다.
즉, 시스템의 UI 레벨에서 너무 많은 업무를 처리하는 경우를 의미합니다. 백엔드 개발자의 UI는 바로 백엔드 API를 의미합니다. API는 애플리케이션 프로그래밍을 위해 마련된 인터페이스를 뜻합니다. UI는 사용자를 위해 마련된 인터페이스를 의미하죠. 이 둘은 누군가에게 소프트웨어를 사용하는 방법을 알려주기 위해 만들어진 인터페이스라는 점에서 같지만 목표하는 대상이 다릅니다.
하지만 백엔드 개발자에게 이 두 용어는 이야기가 다릅니다. 백엔드 개발자에게 API는 UI라고 했습니다. 그리고 컨트롤러는 API를 만드는 컴포넌트입니다. 그렇다면 컨트롤러는 스프링에서 UI를 만드는 도구라고 볼 수 있습니다. 한편 스마트 UI는 UI 수준에 너무 많은 로직이 들어가 있는 것을 뜻한다고 했는데, 컨트롤러의 핸들러 메서드에 많은 로직이 들어가있는 것을 뜻하죠.
일반적으로 UI는 사용자의 입출력을 위한 창구로만 사용돼야 합니다. 입력을 받고 이를 뒷단으로 넘겨 비즈니스 로직을 실행하는 역할 정ㄹ도만 해야 합니다. 스마트 UI 패턴을 따르는 코드는 컨트롤러 같은 UI 코드에 과한 책임이 할당돼 있습니다. 즉, 비즈니스 로직을 UI 수준에서 갖고 있는 경우를 얘기하죠. 즉, 컨트롤러의 역할을 재정의하면 다음과 같다고 말할 수 있죠.
1. API 방식을 호출을 정의합니다.
2. 어떤 비즈니스 로직을 실행할 것인지 결정합니다.
3. API 호출 결과를 어떤 포맷으로 응답할지 정의합니다.
컨트롤러의 가장 큰 역할은 엔드포인트를 정의하고, API 사용자의 요청을 받아 그 결과를 응답 포맷에 맞게 반환하는 것입니다. 스마트 UI 방식은 흔히 MVP(Minimum Viable Product)를 만들 때 유용합니다.
6.2 양방향 레이어드 아키텍처
이는 레이어드 아키텍처를 지향하는 프로젝트에서 많이 발생하는 안티패턴이며, 레이어드 아키텍처에서 정의한 레이어들의 의존 관계에 양방향 의존이 발생하는 경우를 칭합니다. 레이어드 아키텍처는 소프트웨어 시스템을 설계하는 방식 중 하나로, 이름에서도 알 수 있듯이 '레이어'라고 불리는 분류 체계를 사용하죠. 이 아키텍처를 따르는 애플리케이션은 개발하기 전에 레이어를 먼저 정의하는데, 보편적으로 3개의 레이어를 사용합니다.
1. Presentation layer : 사용자와의 상호작용을 처리하고 결과를 표시하는 역할 담당. 대표적으로 스프링 컴포넌트가 하나 있는데, 바로 컨트롤러 컴포넌트입니다. 쉽게 얘기해서 컨트롤러 같은 컴포넌트가 모이는 곳이라 볼 수 있습니다.
2. Business layer : 애플리케이션의 비즈니스 로직을 처리하는 역할을 합니다. 데이터의 유효성 검사, 데이터 가공, 비즈니스 규칙 적용 등의 일이 이뤄지고, 주로 서비스 컴포넌트가 이곳으로 모입니다.
3. Infrastructure later : 외부 시스템과의 상호작용을 담당합니다. 대표적인 외부 시스템으로 데이터베이스가 있습니다. 스프링에서 데이터에 접근하는 기술인 JDBC나 ORM 프레임워크인 JPA, 하이버네티으 관련 코드들이 이 레이어에 배치됩니다. 이 레이어에 주로 들어가는 코드는 데이터를 저장하거나 조회하는 등의 일을 수행하는데, 그러한 이유로 이 레이어는 좁은 의미에서 '영속성 레이어'라고 부르기도 하죠.
이외에도 외부 시스템과 통신하기 위해 WebClient 관련 컴포넌트가 배치되기도 합니다.
레이어드 아키텍처는 이렇게 구성한 레이어들을 바탕으로 각 유형에 해당하는 코드를 각기 대응하는 레이어에 배치합니다. 그리고 레이어의 상하 관계를 나누고, 상위 레이어가 하위 레이어를 사용하는 방식으로 코드를 작성하기 위해서입니다. 레이어드 아키텍처를 구성할 때 장점은 단순하고 직관적인 구조입니다. 어떤 컴포넌트를 개발하거나 찾을 때 어디에 위치시켜야 할지 고민할 필요가 없습니다. 기능 개발이 쉬워지죠.
해당 방식이 올바른 접근법인가요? 이 질문은 나중에 얘기하고, 양방향 레이어드 아키텍처로 다시 들어가보죠. 양방향 레이어드 아키텍처는 레이어드 아키텍처를 지향해 개발했지만, 레이어드 아키텍처가 반드시 지켜야 할 가장 기초적인 제약을 위반할 때를 지칭하는 말입니다. 가장 기초적인 제약이란 '레이어 간 의존 방향은 단방향을 유지해야 한다'라는 것입니다.
비즈니스 레이어에 위치한 서비스 컴포넌트가 프레젠테이션 레이어에 위치한 객체에 의존하는 바람에 두 레이어 간 양방향 의존 관계가 생겼습니다. 이처럼 레이어 간 양방향 의존성이 생긴 상황을 '양방향 레이어드 아키텍처'라고 부릅니다. 이렇게 될 경우 레이어의 역할이 의미 없어지고, 계층이 무너져버립니다. 5장 순환 참조에서 컴포넌트 간 순환 참조가 발생했다는 것은 사실상 '해당 컴포넌트들이 하나의 컴포넌트가 됐다는 선언이다'라고 설명했습니다. 그러므로 양방향 레이어드 아키텍처는 레이어드 아키텍처에서 제일 중요한 계층 관계가 사라진 상황이라고 표현할 수 있습니다.
그럼 이를 해결하는 방법을 찾아보죠. 대표적인 방법을 2가지가 있습니다.
6.2.1 레이어별 모델 구성
레이어별로 모델을 따로 만드는 것입니다. 예로 들자면 비즈니스 레이어에서 사용할 모델을 추가로 만드는 것입니다. PostCreateCommand 모델을 추가로 만들고, 이는 PostCreateRequest 모델에 대응하는 모델입니다. 이는 비즈니스 레이어에 위치하고, 다음과 같은 명명 규칙을 사용합니다.
1) ~Request 클래스는 API 요청을 처리하는 모델입니다.
2) ~Command 클래스는 서비스에 어떤 생성, 수정, 삭제 요청을 보낼 때 사용하는 DTO입니다.
이처럼 구분하고, 컨트롤러는 서비스에 요청을 보낼 때는 PostCreateRequest 클래스를 PostCreateCommand 클래스로 변경해 서비스의 메서드를 호출합니다. 의존 방향이 단방향이 되고 순환 참조가 사라졌습니다. 이 방법은 다른 장점이 또 있는데, 바로 클라이언트가 API 요청을 보내는 시점의 요청 본문(request body)과 서비스 컴포넌트에서 사용하는 DTO를 분리할 수 있게 됐다는 점이죠. 더 정확히는, PostCreateRequest 클래스에 writerId라는 멤버 변수가 있다고 가정합시다. 서버 입장에서 클라이언트가 보내는 이 값을 신뢰할 수 있을까요? 악의적인 사용자가 이 값을 조작해 API 요청을 보낼 수 있습니다.
따라서 PostCreateRequest 클래스에는 작성자를 뜻하는 writerId 같은 멤버 변수가 있으면 안됩니다. 이러한 상황을 막기 위한 방어 로직이 시스템에 있냐 없냐의 문제가 아니라 아예 존재하지 않는 것이 좋습니다. 신뢰할 수 없는 값이니까요.
하지만 동시에 이러한 값은 분명 비즈니스 레이어에 필요한 값이고, 이를 해결하기 위해 가장 간단한 해결 방안은 클라이언트의 요청을 받는 모델과 실제로 객체를 생성하는 DTO 모델을 분리하는 것입니다. 그리고 그 해결책이 바로 '레이어별 모델 구성하기'입니다.
다시 말해, PostCreateCommand 클래스에 필요한 정보를 꼭 PostCreateRequest 클래스에서만 가져올 필요는 없습니다. 이것은 객체의 역할을 분리한 것입니다. PostCreateRequest 클래스는 '@RequestBody 애너테이션을 처리한다'는 역할에만 집중하게 합니다. 한편 PostCreateCommand 클래스는 프로그램에 필요한 도메인 객체를 만들기 위해 필요한 정보를 가지고 있는 것에만 집중하도록 합시다.
하지만 단점도 있죠. 코드의 양이 늘어납니다. 작성해야 하는 코드가 늘어난다는 것은 조직 관점에서 비용이 증가한다는 의미입니다. 모델은 적당히 세분화되고 통합돼야 하는데, 균형을 잡는 것이 매우 어렵습니다. 안타까운 사실로 DTO를 어디까지 세분화할 것이냐는 문제에 마땅한 정답이 없다는 것이죠.
코드 중복과 코드 유사성은 다른 것입니다. 필요에 따라 만들어진 코드는 말 그대로 필요하니까 존재하는 것이 당연합니다. 중복은 역할과 책임, 구현 등이 비슷할 때를 보고 중복이라고 부릅니다. 그래서 데이터 형태가 유사하다고 해서 중복이라고 보기 어렵습니다. 목적, 해결 방법이 같을 때 중복이라고 부릅니다. 유사한 데이터를 여러 개 만드는 것을 두려워하지 마세요. 몇몇 멤버 변수가 겹친다고 데이터 모델을 어정쩡하게 공유하는 것보다 역할과 책임에 따라 확실하게 모델을 구분하는 편이 훨씬 낫습니다.
6.2.2 공통 모듈 구성
이는 5장에서 '순환참조'를 해결하는 방법을 설명하며 소개했던 것과 같습니다. 공통으로 참조하는 코드를 별도의 모듈로 분리하는 것입니다. 모든 래이어가 단방향으로 참조하는 공통 모듈을 만들고, PostCreateRequest 클래스 같은 모댈을 거기에 배치하는 것입니다. PostCreateRequest 클래스를 core 패키지로 옮기고, 모든 레이어가 이 core라는 모듈에 의존하도록 변경하면 어떻게 될까요?
공통 모듈로 분리한다는 전략은 범용적으로 사용할 수 있는 유틸성 클래스들을 한곳에 모아둘 때 유용하죠. 여기서 질문
1) core 모듈은 레이어라고 봐야 할까요?
2) core 모듈이 레이어라면 모든 레이어가 바라보는 하나의 레이어를 두는 것은 괜찮을까요?
명확하게 대답하기 어렵습니다. '공통 모듈 구성'을 설명하기 위해 core 모듈의 역할이 현재 모호하기 때문이죠. 'core 모듈은 레이어인가?'에 대한 질문을 먼저 바라보죠. '공통 모듈 구성'에서 제시한 해결책으로 공통 코드를 한 곳에 모으라는 것은 공통으로 참조할 수 있는 모듈을 만들어 보라는 것이지 공통된 레이어를 만들라는 의미가 아니기에 core는 모듈이며 레이어가 아닙니다. 자세한 것은 뒤에서 설명하죠.
6.3 완화된 레이어드 아키텍처
Controller가 Repository를 사용하는 것은 괜찮을까요? 결론은 '이렇게 사용할 수 있게 하는 것은 좋지 못합니다' 왜냐하면 일반적으로 이처럼 2개 이상의 레이어를 건너뛰어 통신하는 구조도 안티패턴으로 분류하기 때문입니다. 그래서 상위 레이어에 모든 하위 레이어에 접근할 수 있는 권한을 주는 구조를 가리켜 '완화된 레이어드 아키텍처'라고 부릅니다.
완화된 레이어드 아키텍처란 완화됐다는 표현 그대로 '레이어드 아키텍처이기는 한데 제역을 조금 완화했다'라는 의미입니다. 여기서 말하는 제약은 '레이어간 통신은 인접한 레이어에서만 이뤄져야 한다'입니다. 왜 이러한 유형이 안티패턴으로 규정되었을까요? 왜냐하면 이러한 코드는 스마트 UI 코드가 만들어지기 때문입니다. 게다가 이런 구조에서는 기능 개발을 위한 코드가 어디에 어떻게 들어가야 할지 한눈에 파악하기 힘듭니다. 비즈니스 로직이 역할과 책임에 따라 유의미한 객체 한 곳으로 모이는 것이 아니라 중구난방 위치하게 되죠.
레이어드 아키텍처에는 '레이어 간 통신은 인접한 레이어끼리 이뤄져야 한다' 같은 제약이 있고, 결론은 Controller가 Repository를 사용해서는 안 욉니다.
6.4 트랜잭션 스크립트
앞선 6.1절 '스마트 UI'에서 이를 사용하는 애플리케이션은 '모든 API는 어떤 스크립트를 실행하고 응답하는 수준의 역할만 한다' 기억나시나요? 트랜잭션 스크립트는 그 내용의 연장입니다. 트랜잭션 스크립트는 비즈니스 레이어에 위치하는 서비스 컴포넌트에서 발생하는 안티패턴입니다. 서비스 컴포넌트의 구현이 사실상 어떤 '트랜잭션이 걸려있는 스크립트'를 실행하는 것처럼 보일 때를 말합니다.
서비스 컴포넌트의 동작이 사실상 트랜잭션이 걸려있는 거대한 스크립트를 실행하는 것처럼 보입니다. 어떻게 보면 '스마트 서비스'라고 부를 수 있습니다. 트랜잭션 스크립트 코드는 객체지향보다 절차지향에 가깝고, 변경에 취약, 확장에 어려워 업무가 병렬처리 되지 않습니다.
이러한 트랙잭션 스크립트 같은 코드는 서비스의 역할이 무엇인지, 객체지향을 스프링에 어떻게 적용해야 하는지 모르는 개발자들이 개발하며 많이 만듭니다. 이 같은 패턴을 피하려면, 서비스의 역할이 무엇인지 재고해야 합니다. 그래서 서비스란 무엇이고, 역할이 어떤 것인지 이해해야 이 패턴을 피할 수 있습니다. 이에 관한 내용이 다음 7장에 소개됩니다.
1) 비즈니스 로직은 어디에 위치해야 할까요?
단순하게 비즈니스 로직은 서비스 컴포넌트에 있는 것이 맞는 것처럼 보입니다. 왜냐하면 서비스 컴포넌트는 비즈니스 레이어에 위치하는 컴포넌트고, 비즈니스 레이어는 비즈니스 로직이 있는 공간이기 때문입니다. 하지만 반은 맞고, 반은 틀렸습니다.
더 나은 답변은 '비즈니스 로직은 도메인 모델에 위치해야 한다'입니다. 도메인 모델이라는 말이 아직 익숙하지 않다면 Cafe, Post, Board, User 같은 객체를 떠올리면 됩니다. 비즈니스 로직은 이러한 객체들이 갖고 있어야 합니다. 객체가 협력하는 것을 강조했던 객체지향을 떠올려보세요. 트랜잭션 스크립트 같은 코드가 발생하는 이유는 간단하죠. 개발자가 '서비스는 비즈니스 로직을 처리하는 곳'이라 생각하기 때문입니다.
조금 더 정확하게 들어가보죠. 개발자 A는 비즈니스 로직을 서비스 컴포넌트에서 처리, 개발자 B는 비즈니스 로직을 처리하는 곳을 도메인에서 처리한다고 생각해보죠.
개발자 A
1. 리포지터리에서 데이터를 불러온다.
2. 데이터를 보고 비즈니스 로직을 처리한다.
3. 리포지터리에 데이터를 저장한다.
4. 컨트롤러에 응답한다.
A에게 객체는 그저 데이터베이스에 있는 데이터와 매핑하기 위한 존재일 뿐입니다. 애플리케이션에는 도메인 모델이라고 부를 만한 것이 없습니다. 이는 서비스의 로직이 점점 길어지고, 비대해져서 서비스가 뚱뚱해지고 애플리케이션은 트랜잭션 스크립트에 가까워집니다. 서비스의 역할이 무엇인지와 애플리케이션의 본질이 무엇인지 모른다면 이런 현상은 계속 반복됩니다. 애플리케이션의 본질은 도메인입니다. 서비스가 아닙니다. 서비스는 도메인이 협력할 무대만 제공하고 그 이상의 역할을 하지 않는 것이 좋습니다.
객체지향과 관련된 내용은 도메인 수준에서 적용하면 됩니다. 애플리케이션에 도메인 객체가 없다면 도메인 객체를 먼저 만드는 작업을 해야 합니다. 데이터 덩이리로 간주되는 구조체를 도메인 객체로 만들고, 서비스 로직에 있는 비즈니스 로직을 도메인으로 옮겨야 합니다. 디자인패턴이나 SOLID를 논하기 전에 능동적인 도메인을 만드는 것이 먼저입니다. 그러고 나서 도메인끼리 협력하게 만들어야 합니다.
개발자 B
B의 비즈니스 로직을 처리하는 공간은 도메인입니다. 서비스는 도메인을 실행하는 역할만 합니다. B의 리포지터리의 역할은 단순히 데이터를 불러오는 곳이 아니라 도메인 객체를 불러오는 곳이 됩니다.
1. 리포지터리에서 도메인 객체를 불러온다.
2. 도메인 객체에 일을 시킨다.
3. 리포지터리에 도메인 객체를 저장한다.
4. 컨트롤러에 응답한다.
큰 차이가 없어보이지만, 이 미묘한 생각의 차이가 전체 품질에 큰 차이를 만듭니다. 절차지향이었던 코드를 객체지향으로 만들고, 서비스가 서비스의 역할을 하게 만들며, 도메인 모델이 서로 협력하게 만듭니다. 서비스는 도메인 객체나 도메인 서비스라고 불리는 도메인에 일을 위임하는 공간이어야 합니다.
'독서 > 자바,스프링 개발자를 위한 실용주의 프로그래밍' 카테고리의 다른 글
7. 서비스 (0) | 2024.11.12 |
---|---|
8. 레이어드 아키텍처 (0) | 2024.11.10 |
5. 순환참조 (1) | 2024.11.09 |
4. SOLID (0) | 2024.11.09 |
2. 객체의 종류 (0) | 2024.11.05 |