Clean Architecture란 무엇인가
왜 클린 아키텍처가 필요한지, 그리고 한 줄로 요약하면 무엇을 약속해주는지부터 정리합니다.
Clean Architecture란 무엇인가
Clean Architecture는 로버트 C. 마틴이 제안한 아키텍처 방식이다.
핵심은 단순하다.
비즈니스 로직을 프레임워크와 외부 기술로부터 분리한다.
여기서 외부 기술은 다음과 같은 것들을 의미한다.
Spring
JPA
MySQL
Redis
Kafka
외부 API
Web Framework
UI
파일 시스템Clean Architecture는 애플리케이션의 중심에 비즈니스 규칙을 두고, 바깥쪽에 기술을 둔다.
즉, 기술이 중심이 아니라 비즈니스가 중심이다.
1. Clean Architecture의 등장 배경
일반적인 Spring 프로젝트는 보통 다음과 같은 구조로 시작한다.
Controller
↓
Service
↓
Repository
↓
Database이 구조는 단순하고 이해하기 쉽다.
처음에는 충분하다.
하지만 프로젝트가 커지면 다음과 같은 문제가 생긴다.
Service가 너무 커진다
비즈니스 로직이 JPA에 묶인다
Controller DTO가 Service까지 내려간다
Entity가 API 응답으로 직접 나간다
외부 API 호출 코드가 비즈니스 로직과 섞인다
테스트할 때 DB와 Spring Context가 필요해진다예를 들어 주문 취소 기능이 있다고 해보자.
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderJpaRepository orderJpaRepository;
private final PaymentClient paymentClient;
private final NotificationClient notificationClient;
@Transactional
public void cancelOrder(Long orderId) {
OrderEntity order = orderJpaRepository.findById(orderId)
.orElseThrow();
if (order.getStatus() == OrderStatus.SHIPPED) {
throw new IllegalStateException("배송 완료된 주문은 취소할 수 없습니다.");
}
order.setStatus(OrderStatus.CANCELED);
paymentClient.cancelPayment(order.getPaymentId());
notificationClient.sendCancelMessage(order.getMemberId());
}
}이 코드는 동작한다.
하지만 많은 책임이 하나의 Service 안에 섞여 있다.
JPA Repository
주문 취소 규칙
결제 API 호출
알림 API 호출
트랜잭션
상태 변경처음에는 괜찮아 보이지만, 기능이 많아질수록 유지보수하기 어려워진다.
Clean Architecture는 이런 문제를 줄이기 위해 등장한 구조다.
2. Clean Architecture의 핵심 목표
Clean Architecture의 핵심 목표는 다음과 같다.
비즈니스 로직을 보호한다
프레임워크에 덜 의존하게 만든다
외부 기술 변경의 영향을 줄인다
테스트하기 쉬운 구조를 만든다
계층 간 책임을 명확히 나눈다가장 중요한 목표는 이것이다.
안쪽 계층은 바깥쪽 계층을 몰라야 한다.
즉, 도메인과 유스케이스는 Spring, JPA, DB, Web을 몰라야 한다.
반대로 Spring, JPA, DB, Web 같은 외부 기술은 안쪽 계층에 맞춰져야 한다.
3. 안쪽 계층과 바깥쪽 계층
Clean Architecture는 애플리케이션을 여러 계층으로 나눈다.
대표적으로 다음과 같다.
Entities
Use Cases
Interface Adapters
Frameworks & Drivers안쪽으로 갈수록 핵심 비즈니스 규칙에 가깝고,
바깥쪽으로 갈수록 기술과 구현 세부사항에 가깝다.
가장 안쪽
- Entity
- 핵심 비즈니스 규칙
중간
- Use Case
- 애플리케이션 기능 흐름
바깥쪽
- Interface Adapter
- Controller, Presenter, Repository 구현체
가장 바깥쪽
- Framework, DB, Web, 외부 API간단히 보면 다음과 같다.
[Framework & Driver]
↓
[Interface Adapter]
↓
[Use Case]
↓
[Entity]의존성 방향은 항상 안쪽을 향해야 한다.
Controller → UseCase → Entity
Repository 구현체 → UseCase/Domain의 Interface
JPA → Domain에 맞춰짐반대로 이런 방향은 좋지 않다.
Entity → JPA
UseCase → Spring MVC
Domain → HTTP Response
Domain → DB Table4. Entity
Clean Architecture에서 Entity는 가장 안쪽 계층이다.
여기서 Entity는 단순히 JPA Entity를 의미하지 않는다.
Clean Architecture의 Entity는 핵심 비즈니스 규칙을 가진 객체다.
예를 들어 주문 도메인을 보자.
public class Order {
private Long id;
private OrderStatus status;
private List<OrderItem> orderItems;
public void cancel() {
if (status == OrderStatus.SHIPPED) {
throw new IllegalStateException("배송 완료된 주문은 취소할 수 없습니다.");
}
if (status == OrderStatus.CANCELED) {
throw new IllegalStateException("이미 취소된 주문입니다.");
}
this.status = OrderStatus.CANCELED;
}
public int calculateTotalPrice() {
return orderItems.stream()
.mapToInt(OrderItem::getPrice)
.sum();
}
}이 객체의 핵심은 필드가 아니다.
중요한 것은 비즈니스 규칙이다.
배송 완료된 주문은 취소할 수 없다
이미 취소된 주문은 다시 취소할 수 없다
주문 총액은 주문 항목의 합이다이런 규칙이 Entity 안에 들어간다.
Clean Architecture에서 Entity는 Spring이나 JPA를 몰라도 된다.
Entity가 몰라야 하는 것
- Controller
- Request DTO
- Response DTO
- JPA Repository
- Spring Annotation
- HTTP Status
- 외부 API ClientEntity는 비즈니스 규칙에 집중해야 한다.
5. Use Case
Use Case는 애플리케이션이 제공하는 기능이다.
예를 들어 쇼핑몰에는 다음과 같은 Use Case가 있을 수 있다.
회원가입
주문 생성
주문 취소
결제 요청
배송 시작
정산 완료Use Case는 Entity를 사용해서 하나의 기능 흐름을 처리한다.
예를 들어 주문 취소 Use Case를 보자.
public interface CancelOrderUseCase {
void cancelOrder(Long orderId);
}구현체는 Application Service가 될 수 있다.
@Service
@RequiredArgsConstructor
public class CancelOrderService implements CancelOrderUseCase {
private final OrderRepository orderRepository;
@Override
@Transactional
public void cancelOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new IllegalArgumentException("주문을 찾을 수 없습니다."));
order.cancel();
orderRepository.save(order);
}
}여기서 Use Case가 하는 일은 다음과 같다.
주문을 조회한다
주문 도메인 객체의 cancel()을 호출한다
변경된 주문을 저장한다주문 취소 규칙 자체는 Order 안에 있다.
정리하면 다음과 같다.
Use Case = 기능 흐름
Entity = 비즈니스 규칙6. Interface Adapter
Interface Adapter는 안쪽 계층과 바깥쪽 기술 사이를 변환하는 역할을 한다.
대표적으로 다음과 같은 것들이 있다.
Controller
Presenter
Repository 구현체
DTO Mapper
External API Adapter예를 들어 Controller는 HTTP 요청을 Use Case에 맞게 변환한다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/orders")
public class OrderController {
private final CancelOrderUseCase cancelOrderUseCase;
@PostMapping("/{orderId}/cancel")
public ResponseEntity<Void> cancelOrder(@PathVariable Long orderId) {
cancelOrderUseCase.cancelOrder(orderId);
return ResponseEntity.noContent().build();
}
}Controller는 HTTP를 알고 있다.
하지만 Use Case는 HTTP를 몰라야 한다.
그래서 Controller가 외부 요청을 내부 Use Case 호출로 바꿔준다.
Repository 구현체도 Interface Adapter로 볼 수 있다.
@Repository
@RequiredArgsConstructor
public class OrderPersistenceAdapter implements OrderRepository {
private final OrderJpaRepository orderJpaRepository;
@Override
public Order save(Order order) {
return orderJpaRepository.save(order);
}
@Override
public Optional<Order> findById(Long orderId) {
return orderJpaRepository.findById(orderId);
}
}이 Adapter는 JPA 기술을 내부 Repository Interface에 맞춰준다.
7. Framework & Driver
Framework & Driver는 가장 바깥쪽 계층이다.
여기에는 구체적인 기술들이 들어간다.
Spring Boot
Spring MVC
Spring Data JPA
MySQL
Redis
Kafka
외부 결제 API
메일 서버
파일 저장소이 계층은 바뀔 수 있는 기술이다.
예를 들어 MySQL을 PostgreSQL로 바꿀 수 있다.
JPA를 MyBatis로 바꿀 수도 있다.
외부 결제사를 바꿀 수도 있다.
물론 실제로 기술을 바꾸는 것은 쉽지 않다.
하지만 Clean Architecture의 목표는 최소한 이런 변경이 핵심 비즈니스 로직까지 퍼지지 않게 하는 것이다.
기술 변경의 영향을 바깥쪽 계층에 최대한 가둔다.
8. 의존성 규칙
Clean Architecture에서 가장 중요한 규칙은 의존성 규칙이다.
소스 코드 의존성은 항상 안쪽을 향해야 한다.
즉, 바깥쪽 계층은 안쪽 계층을 알 수 있다.
하지만 안쪽 계층은 바깥쪽 계층을 몰라야 한다.
가능한 의존성은 다음과 같다.
Controller → UseCase
UseCase → Entity
PersistenceAdapter → Repository Interface
Infrastructure → Domain반대로 이런 의존성은 피해야 한다.
Entity → Controller
Entity → JPA Repository
UseCase → HTTP Request
UseCase → ResponseEntity
Domain → Spring MVC
Domain → 외부 API Client예를 들어 다음 코드는 좋지 않다.
public class Order {
public ResponseEntity<Void> cancel() {
// 주문 취소 규칙
return ResponseEntity.noContent().build();
}
}Order는 도메인 객체다.
도메인 객체가 HTTP 응답을 알면 안 된다.
HTTP 응답은 Controller의 책임이다.
좋은 방식은 이렇게 나누는 것이다.
public class Order {
public void cancel() {
if (status == OrderStatus.SHIPPED) {
throw new IllegalStateException("배송 완료된 주문은 취소할 수 없습니다.");
}
this.status = OrderStatus.CANCELED;
}
}@PostMapping("/{orderId}/cancel")
public ResponseEntity<Void> cancelOrder(@PathVariable Long orderId) {
cancelOrderUseCase.cancelOrder(orderId);
return ResponseEntity.noContent().build();
}도메인은 비즈니스 규칙만 알고, Controller는 HTTP 응답만 처리한다.
9. 정리
Clean Architecture는 비즈니스 로직을 중심에 두고, 외부 기술을 바깥쪽으로 밀어내는 구조다.
계층은 보통 다음과 같이 나눈다.
Entity
- 핵심 비즈니스 규칙
Use Case
- 애플리케이션 기능 흐름
Interface Adapter
- Controller, Repository 구현체, Mapper
Framework & Driver
- Spring, JPA, DB, 외부 API가장 중요한 규칙은 이것이다.
의존성은 항상 안쪽을 향해야 한다.
즉, 도메인과 Use Case는 Spring, JPA, DB, Web을 몰라야 한다.
Clean Architecture의 목적은 구조를 복잡하게 만드는 것이 아니다.
핵심은 이것이다.
비즈니스 로직을 외부 기술 변경으로부터 보호하는 것