군만두의 IT 공부 일지

[스터디2] 04. 안티패턴 및 서비스 본문

프로그래밍/객체지향

[스터디2] 04. 안티패턴 및 서비스

mandus 2025. 1. 4. 09:49

목차

     

    이 책의 2부에서는 저처럼 스프링에 대해서 배운지 얼마 안 된 개발자들이 놓치는 부분을 알려줍니다. 책을 공부하면서, '내가 지금까지 트랜잭션 스크립트 같은 안티패턴을 사용하고 있었구나'하고 반성하게 되는 것 같습니다. 각종 컴포넌트와 DTO 구현에 대한 것도 많이 배워가는 것 같습니다.

    6. 안티패턴

    6.1 스마트 UI

    • 스마트 UI(User Interface) 패턴: 시스템의 UI 레벨에서 너무 많은 업무를 처리하고 있는 경우
      • 스마트 UI는 데이터 입출력을 UI 레벨에서 처리함.
      • 스마트 UI는 비즈니스 로직도 UI 레벨에서 처리함.
      • 스마트 UI는 데이터베이스와 통신하는 코드도 UI 레벨에서 처리함.
      • → 백엔드 개발자도 백엔드 개발자의 UI(백엔드 API)를 신경써야 함.
        • 컨트롤러(Controller)는 API를 만드는 컴포넌트임. 따라서 스마트 UI는 '스마트 컨트롤러'라고 볼 수 있고, 다음과 같은 경우를 의미함.
          1. 컨트롤러의 핸들러 메서드에 지나치게 많은 로직이 들어있는 경우
          2. 비즈니스 로직을 컨트롤러 수준에서 갖고 있는 경우
        • 컨트롤러 같은 UI 코드는 사용자의 입출력을 받고 어떤 비즈니스 로직을 실행할지 결정하는 역할만 해야 함.
        • 컨트롤러의 역할 재정의
          • API 호출 방식을 정의함.
          • 어떤 비니스 로직을 실행할 것인지 결정함.
          • API 호출 결과를 어떤 포맷으로 응답할지 정의함.
        • 스마트 UI 장단점
          • 장점: 빠르게 개발할 수 있어 생산성이 높고, 이해하기 쉬우며 작성하기 쉬움. → MVP(최소 기능 제품)를 만들 때 유용함.
          • 단점: 확장성과 유지보수성이 떨어짐.

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

    • 양방향 레이어드 아키텍처(bidirectional layered architecture): 레이어드 아키텍처를 지향하는 프로젝트에서 많이 발생하는 안티패턴. 레이어드 아키텍처에서 정의한 레이어들의 의존 관계에 양방향 의존이 발생하는 경우.
      • 레이어드 아키텍처: 레이어라고 불리는 분류 체계를 사용함. 레이어드 아키텍처의 형태는 프로젝트마다 다름.
        • 프레젠테이션 레이어(presentation layer): 사용자와의 상호작용을 처리하고 결과를 표시하는 역할을 담당함. 스프링에서 컨트롤러와 같은 컴포넌트가 이곳으로 모임.
        • 비즈니스 레이어(business layer): 애플리케이션의 비즈니스 로직을 처리하는 역할을 함. 스프링에서 주로 서비스 컴포넌트가 이곳으로 모임.
        • 인프라스트럭처 레이어(infrastucture layer): 외부 시스템과의 상호작용을 담당함. 스프링에서 JDBC나 JPA, 하이버네이트 관련 코드들이 이곳에 배치됨.
      • 양방향 레이어드 아키텍처: 레이어드 아키텍처가 반드시 지켜야 할 가장 기초젝인 제약(레이어 간 의존 방향은 단방향을 유지해야 함)을 위반할 때
        • 하위 레이어에 있는 컴포넌트가 상위 레이어에 존재하는 모델을 이용하는 경우 → 양방향 의존 관계. 즉, 순환 참조가 생김.
    /* 레이어드 아티켁처 패키지 구조(책 예제 요약) */
    com.example.cafeapp
    │
    ├── presentation
    │   └── Controller			// 웹 요청을 처리하는 컨트롤러
    │
    ├── business
    │   └── Service				// 비즈니스 로직을 수행하는 서비스
    │
    └── infrastructure
    │   └── JpaRepository		// 데이터베이스 접근을 위한 리포지토리 구현체
    │
    └── core
        ├── User.jav			// 사용자 도메인 모델
        ├── Cafe.java			// 카페 도메인 모델
        ├── Board.java			// 게시판 도메인 모델
        └── Post.java			// 게시글 도메인 모델

    레이어 간에 양방향 참조가 생겼을 때 해결하는 방법으로 아래 2가지가 있음.

    6.2.1 레이어별 모델 구성

    • 레이어별로 모델을 따로 만드는 것
    • 예) 하위 레이어인 서비스 계층에서 상위 레이어인 API 레이어의 모델에 접근하는 경우, 비즈니스 레이어에서 사용할 모델을 추가로 만드는 것임. 클라이언트 요청을 받는 모델과 실제로 객체를 생성하는 DTO 모델을 분리하면 해결할 수 있음.
      • ~Request 클래스는 API 요청을 처리하는 모델임.
      • ~Command 클래스는 서비스에 어떤 생성, 수정, 삭제 요청을 보낼 때 사용하는 DTO임.

    ▲ 레이어별 모델 구성 방식 적용 전후

    • 장점: 클라이언트가 API 요청을 보내는 시점의 요청 본문(request body)과 서비스 컴포넌트에서 사용하는 DTO를 분리할 수 있음.
    • 단점: 작성해야 하는 코드의 양이 늘어남. 즉, 비용이 증가함.
      • 따라서 모델을 적당히 세분화하고 통합해야 함. 하지만 몇몇 멤버 변수가 겹친다고 데이터 모델을 애매하게 공유하는 것보다 역할과 책임에 따라 확실하게 모델을 구분하는 편이 나음.

    6.2.2 공통 모듈 구성

    • 공통으로 참조하는 코드를 별도의 모듈로 분리하는 것
    • 예) 모든 레이어가 단방향으로 참조하는 공통 모듈을 만들고, PostCreateRequest 클래스 같은 모델을 거기에 배치하는 것. core는 모듈이며 레이어가 아님.

    ▲ 공통 모듈 구성 방식 적용 전후

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

    Q. 컨트롤러가 리포지터리를 사용하는 것은 괜찮을까?
    • A. 컨트롤러가 리포지터리를 사용해서는 안 됨.
    • 2개 이상의 레이어를 건너뛰어 통신하는 구조도 안티패턴으로 분류함.
    • 완화된 레이어드 아키텍처(relaxed layered architecture): 상위 레이어에 모든 하위 레이어에 접근할 수 있는 권한을 주는 구조. 레이어드 아키텍처에서 제약(레어어드 간 통신은 인접한 레이어에서만 이뤄저야 함)을 완화함.
    • 예) 프레젠테이션 레이어에 위치한 컨트롤러가 인프라스트럭처 레이어에 위치한 JpaRepository를 멤버 변수로 가지고 있음.
    • 단점: 기능 개발을 위한 코드가 어디에 들어가는지 한눈에 파악하기 힘듦.

    6.4 트랜잭션 스크립트

    • 트랜잭션 스크립트(transaction script): 비즈니스 레이어에 위치하는 서비스 컴포넌트에서 발생하는 안티패턴. 서비스 컴포넌트의 구현이 사실상 어떤 트랜잭션이 걸려있는 스크립트를 실행하는 것처럼 보일 때. 스마트 UI와 비슷하게 '스마트 서비스'라고 볼 수 있음.
    • 단점: 객체지향보다 절차지향에 가깝기 때문에 절차지향의 문제점을 가짐. 변경 및 확장에 취약하며, 업무가 병렬 처리되기 어려움.
    Q. 비지니스 로직은 어디에 위치해야 할까요?
    • A1. 비즈니스 로직은 서비스 컴포넌트에 있어야 함.(△)
    • A2. 비즈니스 로직은 도메인 모델에 위치해야 함.(○)
      • 비즈니스 로직이 처리되는 주(main) 영역은 서비스 컴포넌트가 아닌 도메인 모델이어야 함. 서비스는 도메인을 불러와서 도메인에 일을 시키는 정도의 역할만 해야 함.
      • 서비스는 도메인 객체나 도메인 서비스라고 불리는 도메인에 일을 위임하는 공간이어야 함.

    ▲ 개발자가 비즈니스 로직을 도메인이 처리하게 만든 코드의 시퀀스 다이어그램

    7. 서비스

    • 서비스의 역할
      1. 도메인 객체를 불러옴.
      2. 도메인 객체나 도메인 서비스에 일을 위임함.
      3. 도메인 객체의 변경 사항을 저장함.

    7.1 Manager

    • 스프링에서 컨트롤러는 제어부이고, 리포지터리는 저장소이며, 컴포넌트는 구성 요소임. 그리고 서비스는 DDD(Domain-Driven Design, 도메인 주도 설계)에서 영감을 받아 파생된 개념임.
    • DDD: 도메인을 중심에 놓고 소프트웨어를 설계하는 개발 방법론
      • 도메인(domain): 비즈니스 영역이자 우리가 해결하고 싶은 문제 영역
      • 예) 은행 시스템의 도메인은 은행임. DDD에서 개발자는 도메인(은행)에 대해서도 잘 알고 있어야 함. 하지만 이는 어렵기 때문에 도메인 탐색 과정이 도메인 전문가(은행원)과 소통하면서 이루어짐.
    • 서비스는 도메인 객체가 처리하기 애매한 연산 자체를 표현하기 위한 컴포넌트임.
      • 예) 물건을 파는 사이트의 도메인에 상품(Product), 쿠폰(Coupon), 사용자(User)의 마일리지(Mileage)가 있음. 물건의 가격을 계산하는 로직을 표현하기 위해 PriceManager 클래스를 만듦.
      • 가격 계산 로직 같은 모든 도메인 객체가 처리하기 애매한 연산이나 행동을 매니저 클래스를 만들어 해결함. 이렇게 만든 매니저(Manager) 클래스가 서비스임.(7.2에서 이어짐)
      • ~Manager 클래스는 접두어에 있는 모델을 관리하는 클래스임.
    • 스프링의 서비스 컴포넌트의 역할
      1. 저장소에서 데이터를 불러옴.
      2. 네트워크 호출 결과를 정리해서 객체에 넘겨줌.
      3. 저장소에 데이터를 저장함.
    • 도메인 서비스: 도메인 개발에 필요하지만 객체로 표현하기 애매한 로직을 처리하는 서비스
    • 애플리케이션 서비스: 애플리케이션 개발에 필요하지만 객체로 표현하기 애매한 로직을 처리하는 서비스
    • 예) PriceManager은 도메인에 필요한 비즈니스 업무 규칙을 가진 도메인에 가까운 로직(도메인 서비스)이지만, 스프링에서 @Service 애너테이션으로 만든 ProductService는 애플리케이션이 돌아가는 데 필요한 연산을 갖고 있는 서비스(애플리케이션 서비스)임.
    분류 역할 주요 행동 예시
    도메인 비즈니스 로직을 처리 - 도메인 역할을 수행함.
    - 다른 도메인과 협력함.
    User, Product, Coupon
    도메인 서비스 비즈니스 연산 로직을 처리 - 도메인 협력을 중재함.
    - 도메인 객체에 기술할 수 없는 연산 로직을 처리함.
    PriceManager
    애플리케이션 서비스 애플리케이션 연산 로직을 처리 - 도메인을 저장소에서 불러옴.
    - 도메인 서비스를 실행함.
    - 도메인을 실행함.
    ProductManager

    7.2 서비스보다 도메인 모델

    • 앞에서 설명한 가격 계산 로직은 새로운 도메인 객체를 만들어 도메인 객체로 표현할 수 있음.
    • 예) PriceManager를 Cashier(점원)으로 표현하여, 도메인 모델을 만들어 가격을 계산하는 로직을 만들었음.
    • 객체지향으로 보는 서비스
      1. 서비스는 가능한 한 적게 만들고, 얇게 유지해야 함.
      2. 서비스보다 풍부한 도메인 모델을 만들어야 함.
    • 개발 우선순위: 도메인 모델 > 도메인 서비스 > 애플리케이션 서비스

    7.3 작은 기계

    • 서비스는 항번 생성하면 여러 번 사용하지만 그 자신은 바꿀 수 없음. → 불변성
      • 서비스를 필드 주입이나 수정자 주입을 이용해서 초기화하지 말고 생성자 주입을 사용해야 함.
        1. 생성자 주입을 사용하면 명시적으로 의존성을 표현할 수 있음.
        2. 생성자 주입을 사용하면 테스트하기가 쉬워짐.
        3. 생성자 주입을 사용하면 순환 의존성을 방지할 수 있음.
      • 필드 주입을 사용하는 이유 중 하나는 생성자가 존재하는 것이 미관상 깔끔하지 않다고 함.@RequiredArgsConstructor 애너테이션과 final 키워드를 이용하면 코드를 미관상 좋게 만들 수 있음.
    • 서비스는 작은 기계처럼 영원히 실행할 수 있음.

    7.4 조언

    • 서비스와 관련된 행동 조언
      1. 서비스의 멤버 변수는 모두 final로 만듦.
      2. 서비스에 세터가 존재한다면 지움.
      3. 서비스는 반드시 생성자 주입으로 바꿈.
      4. 서비스의 비즈니스 로직을 도메인에 양보함.
      5. 서비스를 얇게 유지함.

     

    스마트 서비스처럼 서비스가 비즈니스 로직을 다 처리하게 코드를 짰었는데, 도메인에서 비즈니스 로직을 처리할 수 있도록 개선해야 할 것 같습니다. 책을 학습할 때마다 책에서 말하는 개발자의 실수를 제가 다 저지르고 있었다는 걸 알게 되네요.

     

    이 글은 『자바/스프링 개발자를 위한 실용주의 프로그래밍』 책을 학습한 내용을 정리한 것입니다.
    Comments