아키텍처 구조 고민하면서 적용하면 좋을 패턴들에 대해 정리해보겠습니다.
검증
검증하는 위치는 유스케이스가 아닌 유스케이스의 입력 모델의 생성자에서 입력 유효성을 검증하는 것이 좋습니다.
- 유스 케이스 구현체가 자신의 로직에 집중할 수 있다.
- 모든 어뎁터에서 동일한 규칙을 적용할 수 있다.
유스케이스의 입력 모델은 생성된 순간부터 항상 유효해야 한다.
검증 책임은
- Controller / Adapter
- 형식검증 (@Valid)
- 외부 입력 신뢰성
- UseCase Input Model
- 생성 시 검증
- UseCase Service
- UseCase 규칙
- 흐름 제어
- Domain Model
- 핵심 비즈니스 규칙
위와 같이 입력은 컨트롤러에서, 의미는 유스케이스에서, 규칙은 도메인이 책임을 갖습니다.
추가적으로 @Validation은
- Http 요청 검증
- 외부 입력 필터링
이러한 역할을 합니다.
생성자 & 빌더
@Builder는 객체 생성 문법을 편하게 해주지만, 검증 책임은 생성자에 두기.
@Getter
public class SendMoneyCommand {
private final AccountId sourceAccountId;
private final AccountId targetAccountId;
private final Money money;
@Builder
public SendMoneyCommand(
AccountId sourceAccountId,
AccountId targetAccountId,
Money money
) {
this.sourceAccountId = Objects.requireNonNull(sourceAccountId);
this.targetAccountId = Objects.requireNonNull(targetAccountId);
this.money = Objects.requireNonNull(money);
if (money.isNegativeOrZero()) {
throw new IllegalArgumentException("금액은 0보다 커야 합니다.");
}
if (sourceAccountId.equals(targetAccountId)) {
throw new IllegalArgumentException("출금/입금 계좌는 같을 수 없습니다.");
}
}
}
Setter
Setter는 갹체의 규칙을 파괴하기 쉽습니다. -> 값을 바꿔주지만 객체가 왜, 언제, 어떻게 변해야 하는지 알기 어렵게 만듭니다.
- 객체 상태를 언제든 무너트릴 수 있음
- 검증 책임이 사라짐
- 아키텍처 경계를 흐림
만약 Setter를 써야한다면, 호출할 때 마다 검증을 해야합니다.
DTO
게층간 데이터 전달을 할 때 DTO를 사용합니다.
하나의 DTO로 두 계층을 연결하려 하지 말고, 따로 따로 requestDto, responseDto (혹은 toEntity, fromEntity)만들어서 쓰는게 좋다고 합니다.
이번 프로젝트를 진행할 때 Controller -> Service 흐름에서 동일 Dto를 그대로 받아서 사용하였는데, 이렇게되면 Service가 받고 싶은 포맷이 Controller에 종속적이게 되는 문제가 있습니다.
Client 요청 이후 Service 레이어를 호출하기 전 다른 작업으로 인해 포맷이 달라질 수 있는데 위와 같이 Service가 Controller에 의존하고 있다면 문제가 될 수 있습니다.
애플리케이션 조립
크게 3가지 방법이 있습니다.
1. 평범한 코드로 조립
메인 함수에서 각각 명시적으로 new로 주입해줍니다.
장점
- 직관적
단점
- 객체가 많아질수록 main이 복잡해짐
- 설정 변경을 하려면 코드를 변경해야함
2. 위 조립 과정을 스프링 프레임워크에게 맡기기
@Component로 등록해두면 스프링이 알아서 찾아 조립해줍니다.
장점
- 빠르고 간결하게 구현할 수 있다.
- 스코프, 트랜잭션, AOP등 생명주기 관리를 쉽게 적용할 수 있다.
단점
- 클래스를 열어봐야 의존성을 파악할 수 있다.
- 테스트가 Spring에 의존하여 단위 테스트가 통합 테스트로 변질됨
일반적인 웹 애플리케이션 개발에 적합합니다.
3. 스프링의 Java Config 사용
@Configuration에서 직접 @Bean 등록해서 뭘 고를지 정합니다.
장점
- 객체 생성과 연결이 직관적이다
- 설정 변경이 쉽다.
단점
- 설정 코드 증가, 번거로움
장기 유지보수 할 프로젝트에 적합합니다.
도메인 모델링과 Aggregate
하나의 도메인만 만들어 두면 해당 도메인에 요구사항이 많아져 혼란스럽게 됩니다.
관심사를 분리한다. 적절한 범위로 쪼개는 것을 DDD에서는 바운디드 컨텍스트 (해결영역) 이라고 합니다.
Aggregate
- 시스템이 기대하는 책임을 수행하면서 일관성을 유지하는 단위
- 명령을 수행하기 위해 함께 조회하고 업데이트해야 하는 최소 단위
Aggregate는 경계를 가집니다. (얘내 단위로 일관성을 관리하여 도메인 복잡도를 낮춤)
- 경계를 설정하는데 기본이 되는 것은 도메인 규칙과 요구사항
- 주로 함께 생성되거나 함께 변경되는 구성요소는 같은 애그리거트에 속할 가능성이 높다.
- 외부에서 내부의 객체에 접근하려면 Aggregate Root의 식별자를 통해서만 접근해야한다.
동시성 제어
여러 사용자가 같은 데이터를 동시에 수정하면 잔고가 잘못 계산이 되는등의 비즈니스 규칙 위반이 발생할 수 있습니다.
Aggregate로 나누면, Aggregate 내부에서는 하나의 트랜잭션 안에서 일관성을 지키게 됩니다.
또한 아래에서 설명하는 충돌이 일어나더라도 범위를 줄일 수 있습니다.
잠금
잠금이란, 다중 사용자 환경에서도 비즈니스 규칙이 깨지지 않게 하는 방법입니다.
비관적 잠금
한번에 한 트랜잭션만 처리하도록 데이터베이스 레코드를 독점합니다. 다른 트랜잭션은 대기합니다.
장점
- 충돌 거의 없음
- 단순
- 단점*
- 성능 저하
- 데드락 위험 (두 개 이상의 트랜잭션이 서로의 작업이 완료되기를 영원히 기다리는 상황)
낙관적 잠금
Aggregate 내에서 하나라도 변경을 하면 버전을 반드시 증가시키는 방법입니다.
데이터에 version 필드를 따로 둡니다.여러 작업들이 모두 같은 root 객체의 version을 읽고, 저장할 때 version이 달라 commit시 충돌이 나게되면 JPA가 OptimisticLockException를 던집니다.
Reference
https://github.com/Meet-Coder-Study/Get-Your-Hands-Dirty-on-Clean-Architecture
'Dev > Architecture' 카테고리의 다른 글
| 아키텍처 구조에 관한 고민 (1) - 아키텍처 비교 (0) | 2026.01.30 |
|---|