6장 안티패턴

개발 99·2025년 4월 21일

6.1 스마트 UI

  1. 스마트 UI는 데이터 입출력을 UI 레벨에서 처리한다.

  2. 스마트 UI는 비즈니스 로직도 UI 레벨에서 처리한다.

  3. 스마트 UI는 데이터베이스와 통신하는 코드도 UI 레벨에서 처리한다.

스마트 UI란 시스템의 UU레벨에서 너무 많은 업무를 처리하고 있는 경우를 의미한다.

백엔드 개발자도 UI를 신경써야 하는데, 그게 바로 백엔드 API를 뜻한다.

(API는 애플리케이션 프로그래밍을 위해 마련된 인터페이스를 뜻한다.)

API를 통해서 소통을 하기 때문에, API 설계는 중요하다.

스마트 UI는 스마트 컨트롤러로 볼 수 있다.

Ex. 스마트 UI

public class PostController{

	@PostMapping()
    public Success<PostViewModel> create(...){
    
    	// 1. 카페 회원 검증 로직
        ...
        
        // 2. API 호출자 게시물의 작성자로 입력하는 로직
        post.setWriter(user);
        
        // 3. 게시물의 작성 시간에 현재 시각을 입력하는 로직
        post.setCreatedTimestamp(currentTimestamp)
        
        // 4. 카페의 가장 최신 게시물 시간을 현재 시각으로 변경하는 로직
        cafe.setNewPostTimestamp(currentTimestamp);
        
    }
}

하나의 핸들러 메서드에서 처리하는 내용이 너무 많다.

  1. 카페의 회원인지 검증

  2. API 호출자를 게시물의 작성자로 입력

  3. 게시물의 작성 시간에 현재 시각을 입력

  4. 카페의 가장 최신 게시물 시간을 현재 시각으로 변경

이런 로직은 UI 수준인 컨트롤러 컴포넌트에 위치해서는 안된다.

일반적으로 UI는 사용자의 입출력을 위한 창구로만 사용돼야 한다!

스마트 UI는 이를 어기고 비즈니스 로직을 UI 수준에서 갖고 있는 경우를 뜻한다.

  • 확정성 및 유지 보수성 저하

이를 고려하면, 컨트롤러는 다음과 같이 정의할 수 있다.

  • API 호출 방식을 정의한다.

  • 어떤 비즈니스 로직을 실행할 것인지 결정한다.

  • API 호출 결과를 어떤 포맷으로 응답하지를 결정한다.

컨트롤러의 핵심은 엔드포인트를 정의하고 API 사용자의 요청을 받아 그 결과를 응답 포맷에 맞춰 변환한다는 것에 있다.

(컨트롤러 단에는 비즈니스 로직은 없어야 한다.)

MVP 방식에 유리.

하지만,

  • 절차지향에 가까운 사례.

  • 객체지향적이지 않다.(전부다 데이터 덩어리로 취급)

  • 단순 객체는 CRUD만 수행을 한다.

6.2 양방향 레이어드 아키텍처

레이어드 아키텍처를 지향하는 프로젝트에서 많이 발생함.
(레이어드 아키텍처에서 정의한 레이어들의 의존 관게에 양방향 의존이 발생하는 경우)

보편적으로 다음과 같이 구성되어 있다.
Presentation -> Business -> Infrastructure

  1. 프레젠테이션 레이어
    사용자와의 상호작용을 처리하고 결과를 표시하는 역할을 수행한다.
    컨트롤러 컴포넌트를 의미한다.

  2. 비즈니스 레이어
    애플리케이션의 비즈니스 로직을 처리하는 역할을 수행한다.
    서비스 컴포넌트를 의미힌다.

  3. 인프라스트럭처 레이어
    외부 시스템과의 상호작용을 담당한다.
    대표적으로 스프링 외부인 DB와 상호작용을 한다.
    (JDBC, JPA, Hiberanate 등...)
    주로 데이터를 저장하거나 조회하는 등의 일을 수행한다.

레이어드 아키텍처는 상위 레이어가 하위 레이어를 사용하는 방식으로 코드가 작성된다.

Ex) 레이어드 아키텍처 패키지 구조

com.demo.cafe
	.presentation
    	.UserController
		.CafeController
        .BoardController
    .business
    	.UserService
        .CafeService
        .BoardService
    .infrastructure
    	.UserJpaRepository
        .CafeJpaRepository
        .BoardJpaRepository
    .core
    	.User
        .Cafe
        .Post
        

직관적이고 단순하다. 하지만, 잘못된 구조이다.

레이어드 아키텍처는 반드시! 레이어 간 의존 방향은 단방향을 유지해야 한다.

Ex. API 레이어의 모델을 비즈니스 레이어에서 사용하고 있다.

public class PostService{
	
    ...
    
    @Transactional
    public Post create(
    	...,
        PostCreateRequest postCreateRequest){ // API 요청을 받는 모델인데 비즈니스 레이어에서 사용함.
        	...
        }
    )
}

Presentaion(PostCreateRequest) <-> Business -> Infrastructure와 같은 구조가 생성된다.

이는 두 레이어 간에 양방향 의존 관계가 생성된 것으로 반드시 피해야 하는 구조이다.
(계층간 구분의 의미가 없어진다.)

이는 순환 참조와 다를 바가 없다.

6.2.1 레이어별 모델 구성(해결책 1)

레이어별로 모델을 따로 만든다.
( 비즈니스 레이어에서 사용할 PostCreateCommand 모델을 추가로 만드는데,
이는 PostCreateRequest 모델(@RequestBody로 받음)에 대응하는 모델이다.)

```java
@Data
@JsonIgnoreProperties(ignoreUnkown = true)
public class PostCreateRequest{
	private String title;
    private String content;
}

@Data
@Builder
public class PostCreateCommand {

	private String title;
    private String content;
    private Long writeId;
    
}

PostCreateCommand 클래스는 비즈니스 레이어에 위치한다.

  • Request는 API 요청을 처리하는 모델을 의미한다.

  • Command는 서비스에 어떤 생성,수정,삭제 요청을 보낼 때 사용하는 DTO이다.

PostCreateRequest-> PostCreateCommand에 의존하지, Business에 의존하지 않는다.

추가로 식별자 Id가 변조될 수 있으므로, @RequestBody에는 식별자 값을 넣어서는 안된다.
( 방어로직 )

그러나 필요하기도 하므로, 요청을 받는 모델과 DTO모델을 분리한다.
(즉, DTO가 식별자 Id를 가질 수 있게 한다.)

PostCreateCommand.builder()
	.title(...)
    .content(...)
    .writerId(...)
    .build();

PostCreateRequest는 @RequestBody 어노테이션을 처리한다에 집중한다.

그러나, DTO 객체가 기하급수적으로 늘어난다.

따라서, 적당히 DTO을 쓴다.

6.2.2 공통 모듈 구성

순환 참조를 해결하는 방식과 동일한 방법을 사용한다.
( 공통으로 참조하는 코드를 별도의 모듈로 분리하는 것이다. )

모든 레이어가 방향으로 참조하는 공통 모듈을 만들고, PostCreateRequest 클래스 같은 모델을
거기에 배치하는 것이다.
(모든 레이어가 공통 모듈에 의존한다.)

Presentaion ->
Business -> PostCreateRequest
Infrastructure >

6.3 완화된 레이어드 아키텍처

컨트롤러가 서비스가 아닌 바로 레포지토리를 참조해도 되나? -> 좋지 못함.
(2개 이상의 레이어를 건너뛰어 통신하는 구조도 안티패턴임.)

이와 같이 상위 레이어에 모든 하윈 레이어 접근 권한을 주는 구조 = "완화된 레이어드 아키텍처"라고 부름.
(여기서, 제약을 완화했다는 의미는 반드시 인접한 레이어에서만 통신을 안해도 됨을 의미한다.)

public class PostController{

	private final CafeMemberJpaRepository cafeMemberJapRepository;
    private final BoardJpaRepository boardJpaRepository;
    ...

}

이는 스마트 UI와 동일한 모습을 띄고 있으므로, 피해야 한다.
따라서, 완화된 레이어드 아키텍처는 안티패턴이다.

결론적으로 컨트롤러가 레포지토리를 사용해서는 안된다.

(레이어 간 통신은 인접한 레이어끼리 이뤄져야 한다.)

참고로, 공통 모듈을 사용한다고 완화된 레이어드 아키텍처가 되는 것은 아니다.

완화된 레이어는 아키텍처는 레이어 간 관계이지, 모듈하고 레이어 관계를 설명하는 것은 아니다.

6.4 트랜잭션 스크립트

비즈니스 레이어에 위치하는 서비스 컴포넌트에서 발생하는 안티패턴이다.
( 트랜잭션이 걸려있는 스크립트를 실행하는 것처럼 보일 때를 말한다.)

Ex. 모든 비즈니스 로직을 갖고 있는 서비스

public class PostService {
	...
    
    @Transactional
    public Post creat(...){
    
    	// 1. User 찾기.
       	...
        
        // 2. 작성자 찾기
        ...
        
        // 3. 카페 찾기
        ...
        
        // 4. 게시글 찾기
        ...
        
        ...
    }
}

이런식은 하나의 메소드내에 여러개의 비즈니스 로직이 중첩되었는데 앞선 스마트 UI와 다른게 없다.

스크립트 코드에 @Transactional 말고는 없다.
( 트랜잭션이 걸려있는 거대한 스크립트를 실행하는 것과 다를 바 없고, 절차지향적이다. )

"스마트 서비스"라고 부를 수 있다

그래서, 서비스의 역할에 대해서 재고를 해야한다.

비즈니스 로직은 도메인 모델에 위치해야 한다. <- 명심

(Cafe, Post, Board, User와 같은 객체)

서비스는 도메인을 불러와서 도메인에 일을 시키는 정도의 역할만 수행해야 한다.

(서비스는 비즈니스 로직을 처리하는 곳이 아니며, 객체는 데이터 저장 공간이 절대 아니다.)

애플리케이션의 본질은 도메인임을 명심해라.

(서비스는 도메인을 위한 무대일 뿐이다. = 도메인 호출)

능동적인 도메인을 만든 후, 디자인 패턴이나 SOLID를 적용해라.

(비즈니스 로직을 도메인이 처리하게 해야한다.)

USER -> Controller -> Service -> Repository( 도메인 객체를 불러온다. )
-> Domain ( 도메인 객체의 비즈니스 로직을 실행 )

서비스는 도메인을 실행하는 역할만 수행한다.

(리포지토리는 단순히 데이터를 불러오는 곳이 아니라 도메인 객체를 불러오는 곳이다.)

  1. 리포지토리에서 도메인 객체를 불러온다.

  2. 도메인 객체를 일시킨다.

  3. 리포지토리에 도메인 객체를 저장한다.

  4. 컨트롤러에 응답한다.

                          
profile
구구구구구!

0개의 댓글