๑ `⌃´ ๑
Controller
	↓
Application Service
	↓
Domain
	↓
Repository Interface
	↓
Infrastructure / Repository 구현체

 

각 계층은 왜 존재하는가?
Application Service와 Domain은 무엇이 다른가?
Repository Interface는 왜 두는가?
DIP는 왜 필요한가?

 

 

1. Layered Architecture란?

계층형 아키텍처. 프로그램의 역할을 여러 층으로 나누는 구조.

Spring Boot에서 흔히 보는 구조는 `Controller → Service → Repository → Database`이다.

이것도 대표적인 Layered Architecture인데, 각 계층에 다른 책임을 준다.

 

 

2. 왜 굳이 계층을 나누나?

아주 극단적인 코드를 생각해보자. Controller 하나가 모든 일을 한다고 하자.

OrderController

HTTP 요청 받음
↓
DB에서 상품 조회
↓
재고 검사
↓
쿠폰 검사
↓
포인트 검사
↓
재고 차감
↓
쿠폰 사용 처리
↓
포인트 차감
↓
SQL 실행
↓
HTTP 응답 생성

기능 자체는 만들 수 있다.

그런데 이 Controller는 이제 `HTTP`, `DB`, `상품 규칙`, `쿠폰 규칙`, `포인트 규칙`, `주문 규칙`을 전부 알고 있다.

그러면 수정 하나가 여러 가지 이유로 발생한다.

HTTP 요청 형식 변경 → Controller 수정
재고 정책 변경 → Controller 수정
DB 기술 변경 → Controller 수정
쿠폰 정책 변경 → Controller 수정

하나의 클래스가 너무 많은 이유로 변경된다.

계층을 나누는 핵심 이유 중 하나는 "서로 다른 종류의 책임을 분리하기 위해서"이다.

 

 

3. 전체 구조

                외부 요청
                   │
                   ▼
             Controller
                   │
                   ▼
          Application Service
                   │
          ┌────────┼────────┐
          ▼        ▼        ▼
       Product   Coupon    Point
             Domain
                   │
                   ▼
         Repository Interface
                   ▲
                   │ 구현
                   │
          JPA Repository 구현
                   │
                   ▼
                 DB

 

 

 

4. Controller의 역할

Controller는 웹가 애플리케이션 사이의 입구라고 생각하면 된다.

예를 들어 사용자가 `POST /orders`라는 HTTP 요청을 보낸다.

Controller가 하는 일은 대략 `HTTP 요청을 받는다.`  → `요청 데이터를 Java에서 사용할 형태로 바꾼다.` `Application Service`를 호출한다. `결과를 HTTP Response`로 반환한다. 정도이다.

 

Controller가 하지 않는 것이 좋은 일

if (product.getStock() <= 0) {
    ...
}

Controller가 직접 비즈니스 규칙을 처리하기 시작하면 안쪽의 업무 규칙이 HTTP 계층에 묶인다.

그러면 같은 주문 기능을 나중에 `웹 API`, `배치`, `메시지 소비자`, `관리자 기능` 등 다른 경로에서 사용하기 어려워질 수 있다.

따라서 Controller는 "요청을 받고 업무를 호출하는 입구" 정도로 두는 것이 좋다.

 

 

5. Application Service의 역할

Application Service는 하나의 유스케이스를 실행하는 전체 흐름을 조율한다.

OrderService
1. 상품을 조회한다.
2. 쿠폰을 조회한다.
3. 포인트를 조회한다.
4. Product에게 재고 차감을 요청한다.
5. Coupon에게 사용을 요청한다.
6. Point에게 포인트 사용을 요청한다.
7. Order를 생성한다.
8. 주문을 저장한다.

여기서 중요한 것은 Application Service가 모든 규칙을 직접 구현하는 것이 아니라는 점이다.

 

 

6. Application Service와 Domain의 차이

Application Service

누구에게 어떤 일을 어떤 순서로 시킬까?

 

Domain

내 상태에서 이 행동이 가능한가? 그리고 내 상태를 어떻게 바꿀까?

 

주문 예제

Application Service
"상품 가져와."
"쿠폰 가져와."
"상품아, 재고 줄여."
"쿠폰아, 사용 처리해."
"주문 만들자."
"저장하자."
Product
"재고가 0이면 차감할 수 없어."
"가능하면 내가 stock을 1 줄일게."
Coupon
"이미 사용됐으면 사용할 수 없어."
"가능하면 내가 used 상태를 바꿀게."

 

따라서 `Application Service = 흐름`, `Domain = 규칙`이라고 기억하는 것이 좋다.

 

 

7. Repository는 무엇을 하는가?

Repository는 저장소를 다루는 역할이다.

`상품을 찾는다.` `쿠폰을 찾는다.` `주문을 저장한다.`

Product product = productRepository.findById(productId);

orderRepository.save(order);

 

 

8. Repository Interface는 왜 둘까?

예를 들어 Service가 `OrderService → JpaProductRepository` 이렇게 되어있다고 치자.

Service가 JPA 구현체를 직접 알고 있는 것인데, 그러면 Service가 `JPA`, `Hibernate`, `EntityManager`, `Spring Data JPA`와 같은 기술에 의존하게 된다.

그런데 OrderService가 본질적으로 알고 싶은 것은, "상품을 ID로 찾을 수 있느냐?"이다.

JPA로 찾는지는 그 다음 문제로, 그래서 중간에 추상적인 계약을 둘 수 있다.

OrderService → ProductRepository ⇠ JpaProductRepository

` ProductRepository` : "상품 저장소라면 ID로 상품을 찾을 수 있어야 한다."

구현체 : "그걸 JPA로 구현하겠다."

 

 

9. Interface를 식당 주문으로 비유하면

식당에서 직원에게 "아메리카노 한 잔 주세요." 라고 말하는 상황을 상상해보자.

우리는 직원이 내부적으로 어떤 커피 머신을 쓰는지, 핸드드립을 하는지 알 필요가 없다.

우리가 의존하는 것은 "커피를 주문하면 받을 수 있다"는 계약이다.

 

Repository Interface도 비슷하게 볼 수 있다.

Application Service는 "상품을 조회할 수 있어야 해."라는 기능에 의존한다.

구현 방식은 `JPA`, `MyBatis`, `메모리`, `외부 API` 중 무엇이 될지 별도로 결정할 수 있다.

 

 

10. DIP

Dependency Inversion Principle (의존성 역전 원칙)

 

"중요한 비즈니스 코드가 구체적인 기술 구현에 직접 의존하지 말고, 추상화된 계약에 의존하게 하자."

 

 

11. DIP가 없는 구조

OrderService
    ↓
JpaProductRepository
    ↓
JPA
    ↓
Database

OrderService가 직접 구체적인 JPA 구현을 알고 있다.

의존 방향은 `비즈니스 로직 → 기술 구현`이다.

 

 

12. DIP를 적용한 구조

중간에 Interface를 둔다.

             ProductRepository
               ▲
               │ implements
               │
OrderService    │    JpaProductRepository
     │          │
     └──────► Interface
OrderService
    ↓
ProductRepository
    ↑
JpaProductRepository

 

일반적인 호출 방향은 `Service → Repository 구현`인데,

의존관계는 `Service → Repository Interface`, `Repository 구현 → Repository Interface`가 된다.

즉 구현체 쪽도 Interface에 맞춰서 구현한다.

그래서 의존 방향이 뒤집힌 것처럼 보인다고 해서 `Dependency Inversion`이라고 부른다.

 

 

13. "역전"이라는 말 이 왜 붙나?

DIP가 없으면 `비즈니스 → DB 구현`의 형태가 된다.

DB 구현이 바뀌면 비즈니스 코드가 영향을 받을 가능성이 크다.

DIP를 적용하면 아래와 같은 형태가 된다.

        비즈니스가 원하는 계약
        ProductRepository
             ▲
             │
DB 구현 ─────┘

즉 "DB 구현이 비즈니스가 정의한 계약에 맞춘다."는 구조가 되는 것이다. 

기존의 비즈니스가 기술에 맞추는 관점에서 기술이 비즈니스 쪽 계약에 맞추는 것으로 관점이 뒤집힌다.

 

 

14. 그런데 Spring Data JPA에서는 Repository가 Interface 아닌가?

public interface ProductRepository
        extends JpaRepository<Product, Long> {
}

Spring Data JPA에서는 흔히 위와 같이 작성한다. 이미 Interface이다.

그러면 이게 DIP 아닌가? 라고 생각할 수 있다.

하지만 이 Interface는 `JpaRepository`라는 JPA 기술 추상화에 직접 의존한다.

즉 순수한 도메인 관점의 Repository 계약이라기보다는 Spring Data JPA Repository이기도 하다.

 

 

15. 그래서 Repository Interface를 무조건 따로 만들어야 하나?

아주 단순한 CRUD 프로젝트에서 ` ProductRepository`, ` extends JpaRepository`만으로 충분하다면 굳이 계층을 여러 겹 만들 필요는 없다.

추상화는 비용이 있다. 파일이 늘어나고 구조도 복잡해진다.

중요한 것은 "비즈니스 코드가 어떤 구체적인 기술에 지나치게 묶여 있는지 생각해 보는 것"이다.

 

 

16. DIP를 적용하면 테스트에도 도움이 된다

`OrderService`가 `ProductRepository`라는 Interface만 알고 있다고 하자.

테[스트할 때 꼭 실제 DB를 준비할 필요 없이 `FakeProductRepository` 같은 구현을 넣을 수도 있다.

                 ProductRepository
                  ▲            ▲
                  │            │
JpaProductRepository     FakeProductRepository
       실무                 테스트

Application Service 입장에서는 둘 다 `ProductRepository`이다.

이런 구조는 테스트하기 쉬운 설계에도 도움이 된다.

 

 

17. Infrastructure

비즈니스 로직을 실제 시스템 환경과 연결하는 기술적인 구현

예 : `MyBatis`, `JPA`, `Redis`, `외부 API Client`, `파일 시스템`, `메일 발송` 등

 

Repository의 JPA 구혅체도 Infrastructure에 들어갈 수 있다.

Domain / Application

ProductRepository
      ▲
      │
Infrastructure

JpaProductRepository

 

 

18. 왜 Domain이 JPA나 외부 API에 끌려가지 않는 게 좋을까?

`Product`가 내 재고 규칙 + EntityManager 사용법 + SQL + HTTP Client까지 직접 안다고 예를 들자.

그러면 Product는 더 이상 상품 규칙만 표현하는 객체가 아니다.

기술적인 이유로 계속 수정된다.

  • 재고 정책 변경 → Product 수정
  • JPA 변경 → Product 수정
  • 외부 API 변경 → Product 수정

도메인 객체가 너무 많은 이유로 변하는 것이다.

반대로 Product는 재고, 재고 차감 규칙에만 집중한다면 훨씬 안정적이다.

 

 

19. 도메인 모델링과 연결하면

Controller → HTTP 처리
Application Service → 업무 흐름 조율
Domain → 비즈니스 규칙
Repository Interface → 저장소와의 계약
Infrastructure → JPA 등 실제 기술 구현

 

전체는 아래와 같이 된다.

HTTP
 │
 ▼
Controller
 │
 ▼
OrderService
 │
 ├──── Product
 │
 ├──── Coupon
 │
 └──── Point
 │
 ▼
Repository Interface
 ▲
 │
Jpa Repository
 │
 ▼
Database

 

 

20. 예제1

아래 코드를 개념적으로 생각해본다.

OrderController
   ↓
OrderService
   ↓
ProductRepository
   ↓
JpaProductRepository

 

Controller
- POST /orders 요청 받기
- 요청에서 productId 읽기
- Application Service 호출
- HTTP 201 응답 반환

Application Service
- 상품 조회를 Repository에 요청
- Product에게 재고 차감을 요청
- 주문 생성 흐름 조율
- 주문 저장을 Repository에 요청

Domain
- 상품 재고가 부족한지 판단
- 상품 재고 차감

Repository
- 실제 상품 조회
- 실제 주문 저장

 

 

21. 예제2

구조 A

OrderService
    ↓
JpaProductRepository

 

구조 B

OrderService
    ↓
ProductRepository
    ↑
JpaProductRepository

 

1. A에서 `OrderService`가 직접 의존하는 것은 무엇인가?

    - `JpaProductRepository`라는 구체적인 JPA Repository 구현체

2. B에서 `OrderService`가 직접 의존하는 것은 무엇인가?

    - `ProductRepository`

3. JPA 구현을 다른 방식으로 바꿨을 때 어느 구조가 Application Service에 미치는 영향을 줄이기 쉬울까?

    - B

4. 그렇다면 모든 프로젝트에서 B처럼 별도 Interface를 만들어야 할까?

    - 작고 단순한 프로젝트면 굳이 별도의 Interface를 만들지 않아도 된다. 무조건 Interface를 만들게 되면 파일만 많이 생기고 복잡해지기 때문이다.

 

 

22. 정리

Layered Architecture의 핵심은 폴더를 나누는 것이 아니라 서로 다른 책임과 의존 관계를 분리하는 것이다.
Application Service는 유스케이스의 흐름을 조율하고, Domain 객체는 자기 상태와 비즈니스 규칙을 책임진다.
DIP는 비즈니스 코드가 JPA 같은 구체적인 기술 구현보다 추상화된 계약에 의존하도록 만드는 원칙이다.

 

🎵 Playlist
loading...
00:00 / 00:00