•
개발에는 정답이 없지만 “이렇게 개발하면 유지보수나 확장성 관점에서 좋지 못하다”라고 알려진 안티패턴은 존재한다.
1. 스마트 UI
•
스마트 UI 패턴은 에릭 에반스의 저서 도메인 주도 설계에서 소개되어 유명해진 안티패턴으로 다음과 같은 특징을 가진 코드를 말한다.
1.
스마트 UI는 데이터 입출력을 UI 레벨에서 처리한다.
2.
스마트 UI는 비즈니스 로직도 UI 레벨에서 처리한다.
3.
스마트 UI는 데이터베이스와 통신하는 코드도 UI 레벨에서 처리한다.
•
스마트 UI란 시스템의 UI 레벨에서 너무 많은 업무를 처리하고 있는 경우를 의미한다.
•
백엔드에서 UI 레벨이라 하면 표현 계층인 컨트롤러를 의미하는 것임을 알 수 있을 것이다.
•
UI는 일반적으로 사용자의 입출력을 위한 창구로만 사용되어야 한다. 입력을 받고 이를 뒷단으로 넘겨 비즈니스 로직을 실행하는 역할 정도만 해야 한다.
◦
그러나 스마트 UI 패턴을 따르는 코드에서는 그렇지 않다. 컨트롤러 같은 UI 코드에 과한 책임이 할당되어 있다. 즉 스마트 UI는 비즈니스 로직을 UI 수준에서 갖고 있는 경우를 뜻한다.
•
스마트 UI 방식으로 개발된 어플리케이션은 설계에 구조라 부를 만한 것이 존재하지 않으며 모든 코드가 오롯이 기능을 동작하게 만드는 데만 초점을 맞춰 작성된다.
◦
그러한 탓에 사실상 모든 API는 어떤 스크립트를 실행하고 응답하는 수준에 그친다. 그래서 이러한 코드는 당연히 확장성이 떨어지고 유지보수성도 떨어진다. 이러한 이유로 스마트 UI는 안티패턴이다.
•
컨트롤러 같은 UI 코드는 사용자의 입출력을 받고 어떤 비즈니스 로직을 실행할지 결정하는 역할만 수행해야 한다. 즉 컨트롤러의 역할을 재정의하면 다음과 같다.
◦
API 호출 방식을 정의한다.
◦
어떤 비즈니스 로직을 실행할 것인지 결정한다.
◦
API 호출 결과를 어떤 포맷으로 응답할지 정의한다.
•
컨트롤러의 가장 큰 역할은 엔드포인트를 정의하고 API 사용자의 요청을 받아 그 결과를 응답 포맷에 맞춰 반환하는 것이다.
◦
컨트롤러에 비즈니스 로직이 있어서는 안 된다. 데이터베이스 관련 로직이 있어도 안 된다.
◦
이러한 로직을 컨트롤러에 두는 것이 컨트롤러의 목적과 다르기 때문이다. 이는 명백히 책임을 과하게 부여하는 행동이다.
•
스마트 UI에 단점만 있는 것은 아니다.
◦
이 방식을 사용하면 빠르게 개발할 수 있어 생산성이 높다.
◦
모든 코드가 하나의 메소드에 집중되어 있으므로 이해하기도 쉽고 작성하기도 쉽다.
◦
어플리케이션의 흥망성쇠가 어떨지 감이 오지 않아 조직에서 시스템을 빠르게 개발하고 결과를 확인한 뒤 의사결정을 하려한다면 유용할 수 있다.
◦
즉, 이 방식은 흔히 이야기하는 MVP를 만들때 유용하다.
•
하지만 실무에서 클라이언트의 요구사항들은 이 방법을 이용해 지속적으로 해결할 수 있을만큼 단순하지 않다.
◦
처음에는 빠르게 개발할 수 있어 좋을지 몰라도 시스템어 상장하면 필연적으로 시스템의 사용자 기대치가 더 높아지고 요구사항은 점점 더 복잡해진다.
◦
스마트 UI는 객체지향보단 절차지향 프로그래밍에 가까운 사례다.
•
우리의 목표가 유지보수 가능한 코드와 객체지향이라면 스마트 UI나 스마트 컨트롤러 같은 코드가 만들어지는 상황은 피해야 한다.
2. 양방향 레이어드 아키텍처
•
이는 레이어드 아키텍처를 지향하는 프로젝트에서 많이 발생하는 안티패턴으로 레이어들의 의존 관계에 양방향 의존이 발생하는 경우를 칭한다.
•
먼저 레이어드 아키텍처는 소프트웨어 시스템을 설계하는 방식 중 하나로, 레이어라는 분류 체계를 사용한다. 보편적으로 다음 3개의 레이어를 사용한다.
◦
표현 계층
▪
사용자와의 상호작용을 처리하고 결과를 표시하는 역할을 담당한다.
▪
이 역할을 처리하는 대표적인 스프링 컴포넌트가 컨트롤러 컴포넌트다.
◦
비즈니스 계층
▪
어플리케이션의 비즈니스 로직을 처리하는 역할을 한다.
▪
데이터의 유효성 검사, 데이터 가공, 비즈니스 규칙 적용 등의 일이 이 게층에서 이뤄진다.
▪
이러한 성격 때문에 스프링에서는 주로 서비스 컴포넌트가 이곳으로 모인다.
◦
인프라스트럭쳐 계층
▪
이 레이어에서는 외부 시스템과의 상호작용을 담당한다.
▪
대표적으로 데이터베이스가 있으며 외에도 외부 API 등이 있다.
◦
레이어드 아키텍처를 구성했을 때 얻을 수 있는 장점은 뭘까? 가장 큰 장점은 단순하고 직관적인 구조라는 것이다.
•
양방향 레이어드 아키텍처란 레이어드 아키텍처를 지향해 개발했지만 레이어드 아키텍처가 반드시 지켜야 할 기초적인 제약을 위반할 때를 지칭하는 말이다.
◦
여기서 가장 기초적인 제약이란 레이어 간 의존 방향은 단방향이어야 한다는 점이다.
•
간혹 레이어드 아키텍처를 사용하는 조직에서는 편의상 하위 레이어에 있는 컴포넌트가 상위 레이어에 존재하는 모델을 이용하는 경우가 발생한다.
◦
대표적으로 표현 계층에서 API 요청을 받는 모델을 서비스에서도 사용하는 경우가 있다.
•
이 경우, 비즈니스 레이어에 위치한 서비스 컴포넌트가 표현 계층에 위치한 객체에 의존하는 바람에 두 레이어 간에 양방향 의존 관계가 생겼다. 이처럼 양방향 의존성이 생긴 상황을 가리켜 양방향 레이어드 아키텍처라고 부른다.
•
이는 좋게 말해 양방향 의존이지 실은 순환 참조이다.
1. 레이어별 모델 구성
•
첫 번째 해결 방법은 레이어별로 모델을 따로 만드는 것이다. 비즈니스 계층에서 사용할 모델을 추가로 만드는 것이다.
•
Request, Command 모델을 구분하고 컨트롤러는 서비스에 요청을 보낼 때 Request를 Command 클래스로 변경해 서비스의 메소드를 호출한다.
•
하지만 이 방식에도 분명한 단점이 있다. 대표적인 단점 중 하나는 작성해야 하는 코드의 양이 늘어난다는 것이다.
2. 공통 모듈 구성
•
공통으로 참조하는 코드를 별도의 모듈로 분리하는 것이다. 다시 말해 모든 레이어가 단방향으로 참조하는 공통 모듈을 만들고 Request 클래스를 그곳에 배치해두는 것이다.
◦
core와 같은 디렉토리에 놔두는 것이다. 이는 레이어가 아니다. 모듈이다.
3. 완화된 레이어드 아키텍처
•
그렇다면 컨트롤러가 레포지터리를 사용하는 것은 좋은 것일까? 이는 안티패턴이다. 2개 이상의 레이어를 건너 뛰어 통신하는 구조이기 때문이다.
•
이처럼 상위 레이어에 모든 하위 레이어에 접근할 수 있는 권한을 주는 구조를 가리켜 완화된 레이어드 아키텍처라고 부른다.
◦
이는 레이어드 아키텍처이긴 한데 조금 제약을 완화했다는 의미이다.
•
컨트롤러가 레포지터리를 사용해서는 안 된다. 다시 말해 표현 계층이 인프라 계층에 의존하게 해서는 안 된다.
4. 트랜잭션 스크립트
•
트랜잭션 스크립트는 비즈니스 계층에 위치하는 서비스 컴포넌트에서 발생하는 안티패턴이다. 트랜잭션 스크립트는 서비스 컴포넌트의 구현이 사실상 어떤 트랜잭션이 걸려있는 스크립트를 실행하는 것처럼 보일 때를 말한다.
◦
어쩌면 이들은 스마트 서비스라고 부를 수도 있다.
•
이는 객체지향보다는 절차지향에 가까운 사례이기 때문에 절차지향의 문제점을 그대로 가진다. 즉 변경과 확장에 취약하며 업무가 병렬 처리 되기 어렵다.
◦
이러한 트랜잭션 스크립트 같은 코드는 서비스의 역할이 무엇인지, 아니면 객체지향을 스프링에 어떻게 적용해야 하는지 모르는 개발자들이 개발할 때 많이 만들어진다.
•
이 같은 패턴을 피하려면 서비스의 역할이 무엇인지를 재고해야 한다. 서비스란 무엇이고 서비스의 역할이 어떤 것인지 이해해야 이 패턴을 피할 수 있다.
•
그렇다면 비즈니스 로직은 어디에 있어야 할까?
◦
단순하게 생각하면 비즈니스 로직은 서비스 컴포넌트에 있는 것이 맞는 것처럼 보인다. 왜냐하면 서비스 컴포넌트는 비즈니스 계층에 위치하고 비즈니스 계층은 비즈니스 로직이 있어야 하는 공간이기 때문이다.
◦
서비스에 위치하는 것보다 더 나은 구조는 도메인 모델에 비즈니스 로직이 위치하는 것이다.
•
비즈니스 로직이 처리되는 주 영역은 도메인 모델이어야 한다. 서비스 컴포넌트가 아니다. 서비스는 도메인을 데려와서 도메인으로 하여금 일을 시키는 정도의 역할만 해야 한다.
•
트랜잭션 스크립트가 발생하는 이유는 간단하다. 개발자가 서비스는 비즈니스 로직을 처리하는 곳이라고 생각하기 때문이다.
◦
이러한 생각을 가지기 때문에 어플리케이션의 모든 비즈니스 로직이 서비스에 작성된다.
◦
그리고 객체 모델을 서비스의 비즈니스 로직을 실행하기 위한 데이터 저장 공간 수준으로만 인식한다. 그래서 트랜잭션 스크립트와 같은 절차지향적인 코드가 만들어진다.
•
어플리케이션의 본질은 도메인이다. 서비스가 아니다. 서비스는 도메인을 위한 무대일 뿐이다. 그러니 서비스는 도메인이 협력할 무대만 제공하고 그 이상의 역할은 하지 않는 것이 좋다.
•
서비스는 도메인 객체나 도메인 서비스라고 불리는 도메인에 일을 위임하는 공간이어야 한다.