DEV/Architecture

헥사고날 아키텍처 시리즈 6. 영속성 계층

행운개발자 2024. 3. 16. 08:51
728x90

헥사고날 아키텍처의 영속성 계층

영속성 계층 Adapter의 동작 방식

일반적인 의존성 역전을 사용한다면 Service 계층에서 Repository 인터페이스를 호출하고 실질적으로는 RepositoryImpl이 동작합니다.

  • Service -호출→ Repository -구현→ RepositoryImpl

이러한 의존성 역전을 한 번 더 꼬은 것이 Adapter입니다. Adapter는 애플리케이션 계층의 port 인터페이스를 상속한 Repository가 사용되는 곳입니다.

  • Service -호출→ Port -구현→ Adapter -호출→ Repository -구현→ RepositoryImpl
package io.lucky.user.persistence.adapter;

import io.lucky.user.application.port.CreateUserPort;
import io.lucky.user.application.port.SearchUserPort;
import io.lucky.user.domain.User;
import io.lucky.user.domain.UserId;
import io.lucky.user.persistence.entity.UserEntity;
import io.lucky.user.persistence.mapper.UserMapper;
import io.lucky.user.persistence.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
@RequiredArgsConstructor
public class UserAdapter implements CreateUserPort, SearchUserPort {
    private final UserMapper userMapper;
    private final UserRepository userRepository;

    @Override
    public UserId create(User user) {
        UserEntity entity = userMapper.toEntity(user);
        UserEntity savedEntity = userRepository.save(entity);
        return new UserId(savedEntity.getId());
    }

    @Override
    public Optional<User> findById(UserId userId) {
        return userRepository.findById(userId.getId())
                .map(userMapper::toDomain);
    }
}

영속성 계층의 Adapter의 역할

영속성 어뎁터는 포트 인터페이스를 통해서 데이터베이스와 통신을 수행합니다. 이 과정에서 어뎁터에 전달된 입력 모델 JPA Entity로 매핑합니다. 입력 모델을 Entity로 변경하는 과정이 불필요하게 느껴질 수 있지만, 영속성 어뎁터의 입력 모델이 Repository 계층이 아닌 Application 계층에 있기 때문에 Adapter 내부를 변경하는 것이 Application Core에 영향을 주지 않도록 하기 위해 감수해야하는 부분이라고 생각합니다.

우아콘 2020에서 권용근님께서 말씀해주셨던 도메인 계층을 향하는 의존성이 헥사고날 아키텍처에서는 Port와 Adapter를 중심으로 이루어집니다.

[우아콘2020] 배민 프론트서버의 사실과 오해 (22:14), [우아콘2020] 배민 프론트서버의 사실과 오해 (27:25)

영속성 계층의 Mapper

Mapper는 기본적으로 Entity를 Domain 객채로 변경하는 역할을 합니다. 만약 연관관계 또는 비즈니스를 이유로 여러 개의 Entity를 조합해서 하나의 Domain 객체를 생성할 수도 있습니다.

영속성 계층을 외부 MSA 구조에서 데이터를 조회하는 역할로 보면 Mappder의 역할이 깊어질 수 있습니다. 외부에서 조회된 데이터는 상대적으로 정적인 반면에 애플리케이션 계층의 도메인 객체는 비즈니스 요건에 따라서 상대적으로 빠르게 변합니다. 아래의 사진처럼 비즈니스의 구현과 데이터의 조립을 분리한다면, 넓어지는 Service 의 문제를 해소하고, 많은 비즈니스 로직을 Domain 객체에 채울 수 있게 됩니다.

[우아콘2020] 배민 프론트서버의 사실과 오해 (28:18)

Adapter의 분리

하나의 모듈에서 세부 도메인이 여러개일 수 있습니다. 만약 서로 다른 맥락을 가진 Adapter가 있다면, Adapter 클래스도 분리하는 것이 좋습니다. 분리된 Adapter 사이에서 결합이 필요하다면, Adapter를 직접 호출하는 것이 아니라 Port를 통해야 합니다.

트랜잭션 처리

영속성 계층이라서 트랜잭션 처리를 이제 이야기합니다만, 트랜잭션 처리는 Application 영역에서 이루어져야 합니다. “한 사용자가 받은 좋아요 수는 항상 User 정보와 함께 저장된다.”라는 비즈니스 요건이 추가된 경우를 가정해보겠습니다. 아직 User 정보와 좋아요 수가 비정규화되지 않아서 서로 다른 테이블에 존재한다면, CreateUserPort를 구현한 UserAdapter 내부에서 SaveLikePort를 호출할 수도 있습니다. 인터페이스 너머의 변화까지 신경쓰지 않으려면 Service에서 트랜잭션 처리가 이루어져야 합니다.

@Service
@RequiredArgsConstructor
@Transactional
public class CreateUserService implements CreateUserUseCase {
    private final CreateUserPort createUserPort;
    @Override
    public UserId create(CreateUserCommand command) {
        User user = new User(null,
                command.getEmail(),
                command.getNickname(),
                command.getPassword());
        return createUserPort.create(user);
    }
}
728x90