Notice
Recent Posts
Recent Comments
Link
관리 메뉴

look-forest

도메인 모델 본문

도메인 모델

소프트웨어에서의 도메인

사용자가 프로그램, 또는 소프트웨어 서비스를 적용하는 주제 영역.

소프트웨어는 도메인의 핵심 개념과 요소들을 통합하고, 그 관계를 정확하게 구현해야 한다.

 

도메인 모델이란?

도메인은 현실 세계의 일부이고, 단순히 코드로 직접 옮길 수 없다.
따라서, 도메인의 추상화인 도메인 모델을 만들어야 한다.

도메인에 존재하는 중요한 개념과 이들 사이의 관계, 그리고 규칙을 표현한다.

 

도메인 모델은 복잡한 문제 영역을 핵심 개념 중심으로 단순화하여 개발팀의 이해를 돕는 지도 역할. 코드나 UI 설계와는 다른 목적.

 

도메인 주도 설계(DDD, Domain Driven Design)

복잡성을 해결하기 위해 소프트웨어 개발의 중심에 도메인 모델을 두는 방법론.

기술 사용이나 속도보다 도메인 이해와 모델 반영이 중요.

도메인 모델이 설계와 코드까지 이어져야 한다(모델 주도 설계)

 

유비쿼터스 언어(보편 언어): 팀 안에서 도메인 모델에 기반한 단일 어휘체계를 만들고, 이를 문서, 회의, 대화, 그리고 코드까지 일관되게 사용한다.

도메인 모델과 보편 언어를 프로젝트에 기록

 


도메인 모델 패턴

도메인 모델 패턴이란?

도메인/비즈니스 로직을 구성하는 아키텍처 패턴의 한 가지로,

도메인 모델의 속성과 행위를 모두 포함하는 도메인의 오브젝트 모델.

 

반대로 트랜잭션 스크립트하나의 업무절차(TX)를 처리하기 위한 스크립트(메소드)를 만들고 비즈니스 로직을 순서대로 코드로 작성하는 방법. 주로 서비스 계층에서 도메인 정보를 가져다가 길게 쓰는 방식으로, 중복이 발생하거나 도메인 변경 시 영향이 크다.

 

도메인 로직의 API 개발

도메인 모델 패턴은 트랜잭션 스크립트 처럼 작업 단위의 절차형 API를 만들기가 어렵기 때문에,
도메인 로직의 명확한 작업 단위 API를 제공하는 애플리케이션 서비스가 필요하다.

애플리케이션 서비스는 도메인 모델의 기능을 조합하여 사용자나 외부 시스템의 '요청(작업)'을 처리하는 역할.
도메인 로직 자체는 도메인 계층에 있다.

도메인 로직을 서비스에서 가져다 쓰는 형태. 얇은 서비스가 된다.

/**
 * 도메인 로직과 외부 세계와의 상호작용을 절차적으로 구현
 */
@Service
@RequiredArgsConstructor
public class MemberService implements MemberRegister { //서비스가 커지면 port가 서비스 분리의 기준이 된다.
	private final MemberRepository memberRepository;
	private final EmailSender emailSender;
	private final PasswordEncoder passwordEncoder;

	@Override
	public Member register(MemberRegisterRequest registerRequest) {
		// check
		checkDuplicateEmail(registerRequest);

		// domain model -> 주요 로직
		Member member = Member.register(registerRequest, passwordEncoder);

		// repository
		memberRepository.save(member);

		// post process
		sendWelcomeEmail(member);

		return member;
	}

	// 디테일한 내용은 한번 감싸자.
	private void sendWelcomeEmail(Member member) {
		emailSender.send(member.getEmail(), "등록을 완료해주세요", "아래 링크를 클릭해서 등록을 완료해주세요.");
	}

	// 복잡해서 한눈에 안들어온다.
	private void checkDuplicateEmail(MemberRegisterRequest registerRequest) {
		if (memberRepository.findByEmail(new Email(registerRequest.email())).isPresent()) {
			throw new DuplicateEmailException("이미 사용중인 이메일입니다: " + registerRequest.email());
		}
	}
}

 

도메인 모델 패턴의 구성 요소

1. 엔티티

도메인 안에 있는 대상이나 개념으로, 고유 식별자와 생명주기를 가진다.

@Entity
@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@NaturalIdCache //natural id도 persistence context에 캐시
public class Member {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@Embedded
	@NaturalId //비즈니스 적으로 의미있는 식별자
	private Email email;

	private String nickname;

	private String passwordHash;

	@Enumerated(EnumType.STRING)
	private MemberStatus status;

	// 정적 팩토리 메소드 -> new 클래스()를 안써서 이름을 통해 의도를 들어낼 수 있음.
	public static Member register(MemberRegisterRequest createRequest, PasswordEncoder passwordEncoder) {
		Member member = new Member();

		member.email = new Email(createRequest.email());
		member.nickname = requireNonNull(createRequest.nickname());
		member.passwordHash = requireNonNull(passwordEncoder.encode(createRequest.password()));

		member.status = MemberStatus.PENDING;

		return member;
	}

	public void activate() {
		state(status == MemberStatus.PENDING, "PENDING 상태가 아닙니다.");

		this.status = MemberStatus.ACTIVE;
	}

	public void deactivate() {
		state(status == MemberStatus.ACTIVE, "ACTIVE 상태가 아닙니다.");

		this.status = MemberStatus.DEACTIVATED;
	}

	public boolean verifyPassword(String password, PasswordEncoder passwordEncoder) {
		return passwordEncoder.matches(password, passwordHash);
	}

	public void changeNickname(String nickname) {
		this.nickname = requireNonNull(nickname);
	}

	public void changePassword(String password, PasswordEncoder passwordEncoder) {
		this.passwordHash = passwordEncoder.encode(requireNonNull(password));
	}

	public boolean isActive() {
		return status == MemberStatus.ACTIVE;
	}
}

 

2. 값 객체(Value Object)

식별자가 필요하지 않고 속성/값으로만 구별되는 오브젝트.

엔티티가 너무 많은 책임을 가지는 것을 방지하고, 특정 속성 관련 행위를 분리해서 엔티티를 더 집중된 상태로 유지하게 한다

원시 타입보다는 도메인 개념을 더 명시적으로 나타내서 모델의 명확성을 높인다.

@Embeddable
public record Email(String address) {
	private static final Pattern EMAIL_PATTERN =
		Pattern.compile("^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$");

	// record 생성자
	public Email {
		if (!EMAIL_PATTERN.matcher(address).matches()) {
			throw new IllegalArgumentException("이메일 형식이 바르지 않습니다." + address);
		}
	}
}

 

3. 도메인 서비스(Domain Service)

여러 도메인 객체 관련 복잡한 로직을 처리한다.

특정 엔티티나 값 객체에 속하기 어려운 로직,

즉 여러 도메인 객체를 조율하거나 외부 시스템과 통합하는 복잡한 도메인 작업을 처리.

 

가령, passwordEncoder에서 사용하는 암호화 기술은 도메인에 필요하지만, 외부 기술이 포함되어야 하므로, 외부에서 주입받을 수 있도록 인터페이스로 만든다.

/**
 * 도메인에서 사용하므로, 애플리케이션의 required port에 두지말자.
 * 어댑터에서 애플리케이션으로, 애플리케이션에서 도메인으로 의존하는 것은 되지만 반대는 안된다.
 * 도메인도 애플리케이션 안에 들어있으므로, 애플리케이션 안에 required port를 정의한다는 헥사고날 아키텍처의 기본 구현 방식에 부합.
 */
public interface PasswordEncoder {
	public String encode(String password);

	public boolean matches(String password, String passwordHash);
}

 

 

 

 

※ 참고 사항

Java 코드에서 @NonNull 같은 어노테이션과 SpotBugs 같은 정적 분석 도구는 널(null) 안정성 확보에 어떻게 기여하나?

=> 빌드 시점에 널 관련 문제 발견

@NonNull 어노테이션으로 널 허용 여부를 표시하고, SpotBugs 같은 도구는 이를 분석하여 코드가 널 제약을 위반하는 부분을 컴파일/빌드 시점에 미리 찾아내 준다.

 

 



참고 자료 & 이미지 출처
토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처