본문 바로가기
프로그래밍

만들면서 배우는 클린아키텍처 => 엔티티와 도메인의 분리

by 방구석개발자 2024. 3. 6.
반응형

얼마전 만들면서 배우는 클린아키텍처 책을 봤다.

사실 예전에도 본적이 있는데 그때는 서평을 적지 않았다.

 

저는 책 내용을 정리하는게 아니며 다른 개발자분들이 정리한 블로그글이 있으니 참고하면 됩니다.

한가지 가르침을 받아서 내용을 정리하려고 한다.

엔티티와 도메인의 분리

그동안 업무와 공부를 하며 엔티티와 도메인을 분리해본적이 없다.

엔티티에 직접 비즈니스 코드를 채우고 서비스가 적절히 호출하여 만든 api가 다수였다.

 

예를 들어 Q&A 서비스를 만든다는 가정하에 간단한 질문테이블과 엔티티(question)를 코드이다.


@Entity
@Table(name = "question")
public class Question extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String title;

    @Lob
    private String contents;

    @ManyToOne
    @JoinColumn(name = "writer_id", foreignKey = @ForeignKey(name = "fk_question_writer"))
    private User writer;

    @Column(nullable = false)
    private boolean deleted = false;

    @Embedded
    private Answers answers = Answers.empty();

    protected Question() {
    }

    public Question(String title, String contents) {
        this(null, title, contents);
    }

    public Question(Long id, String title, String contents) {
        this.id = id;
        this.title = title;
        this.contents = contents;
    }

    public Question writeBy(User writer) {
        this.writer = writer;
        return this;
    }

    public List<DeleteHistory> delete(User loginUser) throws CannotDeleteException {

        if (!isOwner(loginUser)) {
            throw new CannotDeleteException("질문을 삭제할 권한이 없습니다.");
        }
        List<DeleteHistory> result = new ArrayList<>();
        deleted = true;
        result.add(new DeleteHistory(ContentType.QUESTION, id, writer, this.getUpdatedAt()));
        result.addAll(answers.delete(loginUser));
        return result;
    }

    protected List<Answer> getAnswers() {
        return answers.getAnswerGroup();
    }

    private boolean isOwner(User writer) {
        return this.writer.matchId(writer);
    }

    void addAnswer(Answer answer) {
        if (!answers.contains(answer)) {
            answers.add(answer);
        }
    }

    public Long getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }

    public String getContents() {
        return contents;
    }

    public User getWriter() {
        return this.writer;
    }

    public boolean isDeleted() {
        return deleted;
    }

}

 

detele() 메서드를 통해 삭제하기 기능을 만들고 테스트코드를 만들고 적절히 deleteService를 호출하는 방식으로 만들었었는데

 

만들면서 배우는 클린아키텍처에서는 엔티티와 도메인 클래스를 분리한다.

이를 테면 QustionEntity.java 와 Qustion.java 로 분리하는 것이다.

 

만들면서 배우는 클린아키텍처의 코드를 참고하면 아래와 같다.

@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Account {

	/**
	 * The unique ID of the account.
	 */
	@Getter private final AccountId id;

	/**
	 * The baseline balance of the account. This was the balance of the account before the first
	 * activity in the activityWindow.
	 */
	@Getter private final Money baselineBalance;

	/**
	 * The window of latest activities on this account.
	 */
	@Getter private final ActivityWindow activityWindow;

	/**
	 * Creates an {@link Account} entity without an ID. Use to create a new entity that is not yet
	 * persisted.
	 */
	public static Account withoutId(
					Money baselineBalance,
					ActivityWindow activityWindow) {
		return new Account(null, baselineBalance, activityWindow);
	}

	/**
	 * Creates an {@link Account} entity with an ID. Use to reconstitute a persisted entity.
	 */
	public static Account withId(
					AccountId accountId,
					Money baselineBalance,
					ActivityWindow activityWindow) {
		return new Account(accountId, baselineBalance, activityWindow);
	}

	public Optional<AccountId> getId(){
		return Optional.ofNullable(this.id);
	}

	/**
	 * Calculates the total balance of the account by adding the activity values to the baseline balance.
	 */
	public Money calculateBalance() {
		return Money.add(
				this.baselineBalance,
				this.activityWindow.calculateBalance(this.id));
	}

	/**
	 * Tries to withdraw a certain amount of money from this account.
	 * If successful, creates a new activity with a negative value.
	 * @return true if the withdrawal was successful, false if not.
	 */
	public boolean withdraw(Money money, AccountId targetAccountId) {

		if (!mayWithdraw(money)) {
			return false;
		}

		Activity withdrawal = new Activity(
				this.id,
				this.id,
				targetAccountId,
				LocalDateTime.now(),
				money);
		this.activityWindow.addActivity(withdrawal);
		return true;
	}

	private boolean mayWithdraw(Money money) {
		return Money.add(
				this.calculateBalance(),
				money.negate())
				.isPositiveOrZero();
	}

	/**
	 * Tries to deposit a certain amount of money to this account.
	 * If sucessful, creates a new activity with a positive value.
	 * @return true if the deposit was successful, false if not.
	 */
	public boolean deposit(Money money, AccountId sourceAccountId) {
		Activity deposit = new Activity(
				this.id,
				sourceAccountId,
				this.id,
				LocalDateTime.now(),
				money);
		this.activityWindow.addActivity(deposit);
		return true;
	}

	@Value
	public static class AccountId {
		private Long value;
	}

}

 

 


@Entity
@Table(name = "account")
@Data
@AllArgsConstructor
@NoArgsConstructor
class AccountJpaEntity {

	@Id
	@GeneratedValue
	private Long id;

}

 

이로써 얻는 이점이 여러개가 있는데

  1. 디비에 컬럼이 추가되어도 도메인코드는 변경되지 않는다.
  2. 도메인은 jpa 패키지를 의존하지 않는다. 즉 다른라이브러리나 DB 연결방식을 쉽게 변경할 수있다.
  3. 도메인 코드만 가지고 빠르게 새로운 프레임워크나 새로운 아키텍처의 전환을 빠르게 진행 할 수 있다.

 

참고

https://github.com/wikibook/clean-architecture/tree/main

 

GitHub - wikibook/clean-architecture: 《만들면서 배우는 클린 아키텍처》 예제 코드

《만들면서 배우는 클린 아키텍처》 예제 코드. Contribute to wikibook/clean-architecture development by creating an account on GitHub.

github.com

 

반응형

댓글