내 입맛대로 헥사고날 아키텍쳐

Kevin·2023년 12월 26일
0

? 내 생각에는!

목록 보기
5/5
post-thumbnail

요구 사항

  1. 유저는 닉네임, 성별, 이메일들을 등록할 수 있다.
  2. 유저는 게시글을 작성할 수 있다.
  3. 유저는 게시글의 본문을 수정할 수 있다.
  4. 유저는 여러 게시글을 가질 수 있다.
  5. 게시글은 작성자인 유저 하나를 가질 수 있다.
  6. 게시글을 제목, 본문을 가질 수 있다.

참고 레퍼런스

무의식적으로 코드를 작성하다보니 Entity부터 작성하려는 나를 발견하게 되었다.

DB 중심적인 개발로부터 벗어나기 위해서 도메인부터 먼저 작성을 해보자.

Member

@Getter
@Builder
public class Member {

    private Long id;

    private String nickName;

    private String sex;

    private String email;

    public void updateNickname(String nickName){
        this.nickName = nickName;
    }
}

Post

@Getter
@Builder
public class Post {

    private Long id;

    private String title;

    private String content;

    private Member writer;

    public void updatePost(String content){
        this.content = content;
    }
}

뭔가 어색할정도로 깔끔하고, 편안해지는 느낌이 든다.

DB, JPA와 관련된 어노테이션이나 로직이 없으니 비즈니스 로직과 도메인에 더 집중이된다.

이 참에 어노테이션 선언 순서도 내 나름대로 정했는데, DB에 가까워지거나 더 어노테이션의 개념이 무거워질수록(=객체에 더 큰 영향을 끼치는) 아래에 작성하도록 정하였다.

그 다음으로는 엔티티를 작성해보자.

MemberEntity

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@Table(name = "MEMBER")
@Entity
public class MemberEntity {

    @Id
    @GeneratedValue
    private Long id;

    private String nickName;

    private String sex;

    private String email;
}

PostEntity

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@Table(name = "POST")
@Entity
public class PostEntity {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    private String content;

    @ManyToOne
    private MemberEntity writer;

}

엔티티도 마찬가지로 비즈니스 로직 없이, DB, JPA에 관련된 어노테이션, 로직만 있어서 깔끔해보인다.

내가 생각한 도메인과 엔티티를 분리했을 때의 대표적인 장, 단점은 다음과 같다.

장점

→ 명확한 역할 분리를 할 수 있다.

  • DB 테이블을 생성하고, 영속 계층에서의 엔티티와 비즈니스 로직을 수행하는 도메인의 명확한 역할을 분리를 할 수 있다.
  • 이전에 엔티티가 위 두 책임을 모두 가지고 있었을 때는 객체가 너무 비대해지거나 영속 계층에 비즈니스 로직이 있어서, 비즈니스 계층과 영속 계층이 불필요하게 서로 교류했었다.
  • 이제는 객체의 코드를 줄일 수 있고, 영속 계층은 엔티티가 비즈니스 계층은 도메인이 책임질 수 있다.

단점

→ 생산성이 떨어진다.

  • 현재는 정말 작디 작은 프로젝트이지만, 할 일이 2배 가까이 늘어난 느낌이다. 이는 프로젝트의 규모가 커질수록 분리되었을 때의 장점도 돋보이겠지만, 각기 달리 유지보수해야하는 단점도 부각될 것 같다.

그 다음으로는 Entity와 도메인, 도메인과 Entity간을 매핑 시켜줄 Mapper를 작성하자.

MemberMapper

@Component
public class MemberMapper {

    public MemberEntity toEntity(Member member){
        return MemberEntity.builder()
                .nickName(member.getNickName())
                .email(member.getEmail())
                .sex(member.getSex())
                .build();
    }

    public Member toDomain(MemberEntity memberEntity){
        return Member.builder()
                .id(memberEntity.getId())
                .nickName(memberEntity.getNickName())
                .email(memberEntity.getEmail())
                .sex(memberEntity.getSex())
                .build();
    }
}

PostMapper

@Component
@RequiredArgsConstructor
public class PostMapper {

    private final MemberMapper memberMapper;

    public Post toDomain(PostEntity postEntity){
        return Post.builder()
                .id(postEntity.getId())
                .title(postEntity.getTitle())
                .content(postEntity.getContent())
                .writer(memberMapper.toDomain(postEntity.getWriter()))
                .build();
    }

    public PostEntity toEntity(Post post){
        return PostEntity.builder()
                .title(post.getTitle())
                .content(post.getContent())
                .writer(memberMapper.toEntity(post.getWriter()))
                .build();
    }
}

추후 비즈니스 계층에서 DI 받아 사용하기 위해서 @Component를 명시해두었다.

이제 기본적인 도메인과 엔티티 구현이 끝났으니 Adapter를 구현해야한다.

Adapter 중 Web으로부터 요청을 받는 in Adapter, 즉 Controller를 먼저 구현해보자.

Controller를 구현하기 위해서는 먼저 Port(or Usecase)를 먼저 작성해야한다.

여기서는 보통 2가지 방법을 선택하는 것 같다.

  1. Port 단위에 맞는 크기의 Usecase를 각각 구현하기

  2. Port대신 Usecase들을 인터페이스로 추상화하고, 하나의 Service에서 구현하기
    1. 단 너무 광범위하게는 말고, ex:Post에 관련된 Usecase들만

내가 찾아본 코드들은 2번 방식이 많은 것 같다.

→ 2번 방식이 1번 방식에 비해서 더 빠르게 개발을 할 수 있어서, 생산성을 더 키울 수 있기에 다들 이런 방식으로 사용하지 않나 싶다.

구조도를 그려보면 현재 프로젝트는 아래와 같은 구조를 가지고 있다.

SaveMemberUsecase

public interface SaveMemberUseCase {

    Member saveMember(SaveMemberRequestDTO saveMemberRequestDTO);

}

만약 위에서 1번 방식을 택했으면, Usecase는 클래스 네이밍을 통해서 본인의 역할을 충분히 설명하기에 메서드 이름은 단순하게 execute로 지정해도 된다.

DTO는 Usecase에 의존적이라고 생각해서, Usecase와 같은 경로에 두었다.

이제 Adapter를 이어서 작성해보자.

@RestController
@RequestMapping("/api/member")
@RequiredArgsConstructor
public class MemberController {

    private final SaveMemberUseCase saveMemberUseCase;

    // 회원가입
    @PostMapping("")
    void saveMember(@RequestBody SaveMemberRequestDTO requestDTO){
        saveMemberUseCase.execute(requestDTO);
    }
}

이제 useCase를 구현한 MemberService를 구현해보자.

MemberService

@Service
@RequiredArgsConstructor
public class MemberService implements SaveMemberUseCase {

    private final MemberMapper memberMapper;

    private final SaveMemberPort saveMemberPort;

    @Override
    public void saveMember(SaveMemberRequestDTO saveMemberRequestDTO) {
        saveMemberPort.saveMember(memberMapper.toEntity(saveMemberRequestDTO));
    }
}

이제 out Port를 구현해보자.

saveMemberPort

public interface SaveMemberPort {

    void saveMember(Member member);

}

이제 이 Port를 구현한 영속 Adapter를 작성하면 된다.

@Repository
@RequiredArgsConstructor
public class MemberPersistetenceAdapter implements SaveMemberPort {

    private final MemberMapper memberMapper;

    private final MemberSpringDataRepository repository;

    @Override
    public void saveMember(Member member) {
        repository.save(memberMapper.toEntity(member));
    }
}

내 궁금증들

  1. Mapper를 별도로 이용한 이유
    1. 도메인에서 변환을 진행하게 되면 외부에서 도메인까지 접근할 수 있고, 의존을 하게 하기 때문에 영속성 계층에서 별도로 Mapper를 두어서 외부의 일은 외부에서 의존해서 해결하기로 하였다.
  2. out 패키지에서 만약 엔티티가 더 많이 생기게 된다면 클래스가 너무 많아지지 않을까? 이 때 다른 개발자들은 어떻게 해결하나?
    1. 내가 참고했던 레퍼런스들에서는 별도의 레이아웃 정리를 하지 않았다.

    2. 엔티티, 즉 도메인 별로 패키지를 나누는 것이 아닌 각 객체에 맞게 패키지를 구성한 레퍼런스를 찾았다.

      1. 이 방식으로 구현하면, 계층형 아키텍쳐의 단점을 그대로 또 따라가지 않을까?
        -> Nope, 계층형 아키텍쳐의 주 단점은 도메인 레이어가 영속 계층, 특히 DB를 의존하게 된다면, 도메인 레이어와 애플리케이션 레이어가 변경에 쉽게 영향을 받을 수밖에 없다는 것이다.
        -> 그러나 헥사고날에서는 도메인 레이어가 영속 계층을 의존하지 않기에 이는 성립하지 않는다.

내 예시 코드

profile
Hello, World! \n

0개의 댓글