우리는 DDD와 Layered Architecture 잘 쓰고 있는걸까?(JPA는 Infra 계층인걸까?)

심규민·2024년 3월 10일
1

요즘 도메인 주도 개발 시작하기을 통해 DDD를 공부하고 있다.

책에서 DDD가 일반적으로 가져가는 Layered Architecture의 구성을 다음과 같이 설명하고 있다.

영역설명
사용자 인터페이스 또는 표현사용자의 요청을 처리하고 사용자에게 정보를 보여준다. 여기서 사용자는 소프트웨어를 사용하는 사람뿐만 아니라 외부 시스템일 수도 있다.
응용사용자가 요청한 기능을 실행한다. 업무 로직을 직접 구현하지 않으며 도메인 계층을 조합해서 기능을 실행한다.
도메인시스템이 제공할 도메인 규칙을 구현한다.
인프라스트럭처데이터베이스나 메시징 시스템과 같은 외부 시스템과의 연동을 처리한다.

이 표를 보면 인프라스트럭처 계층은 데이터베이스와의 연동을 처리한다고 설명하고 있다.

지금까지 도메인 계층에서 JPA를 사용하고있었으며 이를 다시 돌아보게 됐다.

기존 아키텍처

.
└── src/
    ├── presentation/
    │   └── controller
    ├── application/
    │   └── application-service
    └── domain/
        ├── domain-service
        ├── repository
        └── entity

기존의 방식은 도메인 계층에서 JPA를 사용해 엔티티와 JpaRepository를 만들어서 사용했다. 그래서 자연스럽게 데이터베이스 접근 기술이 도메인 서비스에 녹아들어 있었다.

그런데 DDD에서는 도메인을 시스템이 제공할 도메인 규칙을 정의한다고 설명하고 있다. 그리고 인프라스트럭처 계층은 데이터베이스와의 연동을 처리한다고 설명하고 있다.

지금까지 개발 편의성을 위해 도메인 계층에 JPA를 사용했다는 생각이 들었다. 그래서 JPA를 인프라스트럭처 계층으로 내려보기로 생각했다.

그러면 도메인 계층에서 JPA를 걷어내면 무슨 장점, 단점이 있을까??

장점

  • presentation, application, domain 계층은 특정 기술에 종속적이지 않게 되서 java <-> kotlin와 같이 언어의 변경이 더 자유로워진다. (JPA를 사용하면... 코틀린 사용하기 어려우니..인프런 JPA 자바와 코틀린 함께 사용하기)
  • 데이터 접근 기술이 변경되더라도 상위 계층들의 영향은 적다.
  • RDB에서 NoSQL 기반 데이터베이스로 변경을 하더라도 상위 계층들의 영향은 적다.

단점

  • 같은 필드를 가지는 도메인 객체와 엔티티를 각각 만들어줘야 한다.
  • 도메인 객체와 엔티티간의 변환을 해줄 매퍼 클래스가 필요하다.

앞선 단점으로 인해 책에서는 편의성을 위해 인프라 계층이 아닌 도메인 계층에서 JPA를 사용할 수 있다고 설명하고 있다! 하지만 나는 장점에서 말하는 강력한 유연함이 더 중요하다 생각하기에 JPA를 인프라 계층으로 내려보았다.

JPA 인프라계층에서 사용해보기

변경한 아키텍처

우선 JPA를 인프라스트럭처 계층에서 사용함에 따라, 도메인 계층에서는 도메인 클래스와 리포지토리 인터페이스 그리고 도메인 서비스가 위치하게 됐다.

인프라스트럭처 계층에서는 엔티티와 JpaRepository를 구현한 인터페이스, 그리고 도메인 계층의 리포지토리를 구현한 클래스가 위치하게 됐다.

.
└── src/
    ├── presentation/
    │   └── controller
    ├── application/
    │   └── application-service
    ├── domain/
    │   ├── domain-service
    │   ├── domain-repository
    │   └── domain
    └── infra/
        ├── entity
        ├── jpa-repository
        ├── mapper
        └── domain-repository-implementation

코드로 살펴보기

아키텍처의 구조만으로는 이해하기 어려울 수 있다. 이를 코드로 살펴볼려 한다.

설명할 클래스는 도메인 계층의 도메인 클래스, 리포지토리 그리고 인프라스트럭처 계층의 엔티티, 리포지토리 구현체, 매퍼이다.

도메인

도메인 클래스

도메인 클래스 내부에는 편의성을 위한 롬복을 제외하고 순수 자바 코드로만 이뤄져 있다. 또한 다양한 도메인 로직들이 있다.

@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class User {
    private Long id;
    private String email;
    private String password;

    public static User of(Long userId, String email, String password) {
        return new User(userId, email, password);
    }
    
    public Boolean isMatchingEmail(final String email){
    	...
    }
    
    public Boolean isMatchingPassword(BiFunction<String,String, Boolean> matcher, String password){
    	...
    }
    
    ...
}

리포지토리

리포지토리 인터페이스 또한 도메인 서비스에서 사용할 메소드만이 정의되어 있다.

public interface UserRepository {
    Boolean existsByEmailAndPassword(String email, String password);
    Optional<User> findByEmail(String email);
    Optional<User> findById(Long id);

}

인프라스트럭처

인프라스트럭처 계층에는 JPA를 활용하는 다양한 클래스들이 위치하게 된다.

엔티티

엔티티는 도메인과 똑같은 필드들을 가지게 된다. 다른점으로는 도메인 로직없이 getter와 생성자만 위치하게 된다.

@Entity
@Table(name = "user")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserEntity {
    @Id@GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String email;
    private String password;

    private UserEntity(Long id, String email, String password) {
        this.id = id;
        this.email = email;
        this.password = password;
    }

    public static UserEntity of(String email, String password) {
        return new UserEntity(null, email, password);
    }

    public static UserEntity from(Long id){
        return new UserEntity(id, null, null);
    }
}

JpaRepository

public interface UserJpaRepository extends JpaRepository<UserEntity, Long> {
    Boolean existsByEmailAndPassword(String email, String password);
    Optional<UserEntity> findByEmail(String email);
}

도메인 Repository 구현체

도메인 계층에서 정의한 리포지토리를 구현한 클래스이다. 이때, 의존성으로 JpaRepository, Mapper를 가진다. 만약 QueryDsl과 같이 추가적인 기술이 들어간다면 의존성이 추가될 수 있다.

@Repository
@RequiredArgsConstructor
public class UserRepositoryImpl implements UserRepository {
    private final UserJpaRepository userJpaRepository;
    private final UserMapper userMapper;
    @Override
    public Boolean existsByEmailAndPassword(String email, String password) {
        return userJpaRepository.existsByEmailAndPassword(email, password);
    }

    @Override
    public Optional<User> findByEmail(String email) {
        return userJpaRepository.findByEmail(email)
            .map(userMapper::toDomain);
    }

    @Override
    public Optional<User> findById(Long id) {
        return userJpaRepository.findById(id)
            .map(userMapper::toDomain);
    }
}

Mapper(매퍼)

매퍼 클래스는 도메인 객체를 엔티티로, 엔티티를 도메인 객체로 변환해주는 역할을 가진다. 이때, 메소드들이 static으로 선언하여 사용할 수 있지만 명시적인 의존성 관리를 위해 빈으로 등록하고 메소드를 호출하도록 구현하였다.

@Component
public class UserMapper {

    public UserEntity toEntity(User user){
        return UserEntity.of(
            user.getEmail(),
            user.getPassword()
        );
    }

    public User toDomain(UserEntity userEntity){
        return User.of(
            userEntity.getId(),
            userEntity.getEmail(),
            userEntity.getPassword()
        );
    }
}

구현해보니

확실히 작성해야하는 코드의 양이 평소의 2배가 되니 개발시간이 길어진다. 하지만 이는 기술적 유연성을 확보하기 위한 트레이드오프라고 생각된다.

그리고 이러한 아키텍처를 멀티모듈을 도입하여 구성한다면 좀 더 많은 이점이 있을 것 같다. 특히 인프라스트럭처 모듈을 구성한다면 다른 기술로의 이전에서 모듈 의존성만 변경하면 돼 엄청난 유연성을 얻을 수 있을거라 예상된다.

결론은 JPA는 인프라스트럭처에 위치하는게 맞는 것 같다. 하지만 개발시간이 부족하며, 여러 기술이 적용되지 않는다면, 도메인 계층에서 사용해도 무방하다 생각한다.

관련 깃허브 레포지토리는 아래의 링크를 통해 들어가면 된다. 급하게 개발한거라 코드 퀄리티가 많이 떨어지지만, 이를 감안하고서 봐줬으면 좋겠다.

https://github.com/Kusitms-29th-Kobaco-A/Backend

2개의 댓글

comment-user-thumbnail
2024년 3월 10일

매퍼 클래스를 DDD에만 사용해야 하나요? 이번에 모놀리틱아키텍처로 진행한 프로젝트에서 dto<-> entity 변환하는 코드가 너무 더럽다고 생각했는데, 이 코드를 serviceImpl에 전부 때려박았어요.
다른 팀원은 이 변환코드를 dto 클래스에 때려박았어요.

이 문제도 매퍼 클래스를 사용하여 해결할 수 있을까요?

1개의 답글