무의식적으로 코드를 작성하다보니 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에 관련된 어노테이션, 로직만 있어서 깔끔해보인다.
내가 생각한 도메인과 엔티티를 분리했을 때의 대표적인 장, 단점은 다음과 같다.
장점
→ 명확한 역할 분리를 할 수 있다.
단점
→ 생산성이 떨어진다.
그 다음으로는 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가지 방법을 선택하는 것 같다.
Port 단위에 맞는 크기의 Usecase를 각각 구현하기
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));
}
}
내가 참고했던 레퍼런스들에서는 별도의 레이아웃 정리를 하지 않았다.
엔티티, 즉 도메인 별로 패키지를 나누는 것이 아닌 각 객체에 맞게 패키지를 구성한 레퍼런스를 찾았다.