m1ndy5's coding blog

Ditto 프로젝트 멀티 모듈 도입 2편 - Entity 연관관계 끊기 본문

Toy Projects/Ditto - Discuss Today's Topic

Ditto 프로젝트 멀티 모듈 도입 2편 - Entity 연관관계 끊기

정민됴 2024. 2. 6. 15:09

드디어 멀티모듈 나누기의 길고도 험난한(?) 과정을 어느정도 정리했다.

아직 완벽하다고 할 수는 없지만 그래도 내가 고민했던 것들을 해결해 나가는 과정을 정리해보려고 한다!

 

https://m1ndy5.tistory.com/153

 

Ditto 프로젝트 멀티 모듈 도입 1편 - 모듈 나누기에 대한 고민

뭔가 실행으로 옮기는 데는 많은 시간이 걸리진 않았지만 정말 많은 고민을 했던 부분이었던 것같다. 기존 모놀리식 구조에서의 패키지는 auth(인증, 인가), member(회원가입, 팔로우/팔로잉), post(

m1ndy5.tistory.com

이전 글에서 나는 common 모듈에 모든 엔티티를 몰아넣고 사용을 했었고 그 과정에서

1. 도메인을 common에 몰아넣는 것이 진정하게 모듈이 잘 분리된 것이라고 얘기할 수 있을까??

2. 더 나아가서 common 모듈이 존재하는 것이 진정하게 모듈이 잘 분리된 것이라고 얘기할 수 있을까??

이 두가지를 고민했다.

 

그래서 제일 먼저한 것은 각 엔티티의 연관관계를 끊어주는 일이었다.

예를 들어 Comment Entity는 게시글(Post), 작성자(Member) Id를 FK로 들고 있다.

하지만 이렇게 다른 Entity와 조인이 필요하다면 연관관계를 끊을 수 없으니 Join 되어있는 부분은 모두 일반 Id 컬럼으로 바꿔주었다.

(하지만 Post의 경우 Comment와 같은 모듈에 있기 때문에 굳이 일반 Id 컬럼으로 바꿀이유는 없다.)

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Comment extends BaseEntity {

    @ManyToOne
    @JoinColumn(name = "post_id", nullable = false)
    private Post post;

    @Column(name = "member_id", nullable = false)
    private Long memberId;

    @Column(name = "content", nullable = false)
    private String content;

    @Column(name = "likes", nullable = false)
    @ColumnDefault("0")
    private long likes;

}

 

그 다음에는 해당 Id를 가지고 다른 모듈에 정보를 요청했다.

이전에는 조인을 하여 같은 데이터베이스에서 가져올 수 있었지만 현재는 조인을 사용할 수 없기 때문에(데이터베이스가 분리되어 있어서) 각 모듈 사이의 통신은 API를 통해 할 것이다.

이 때 사용했던 개념이 바로 @FeignClient이다.

@FeignClient 란?

Feign은 마이크로서비스 간의 통신을 간편하게 만들어주는 라이브러리로, Spring Cloud에서 제공하는 기술 중 하나이다.

Feign은 선언적 방식으로 HTTP 클라이언트를 생성하며, 서비스 간의 Restful API 호출을 쉽게 처리할 수 있다.

선언적 방식? 인터페이스를 사용하여 간단한 어노테이션으로 API를 정의하기 때문에 코드가 간결하고 가독성이 뛰어나다.

 

FeignClient를 사용하기 위해서는 먼저 gradle에 해당 의존성을 추가해주어야 하는데 이때 자신이 진행하고 있는 프로젝트의 버전에 맞춰주자

implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.1.0'

나는 스프링부트 3.2 버전의 프로젝트이기 때문에 4.1 버전으로 맞춰주었다.

@SpringBootApplication
@EnableFeignClients
public class ActivityApplication {
    public static void main(String[] args){
        SpringApplication.run(ActivityApplication.class, args);
    }
}

@FeignClient을 사용할 모듈에 @EnableFeignClients를 붙여주고

@FeignClient(name = "client1", url = "http://localhost:8080/api/members")
public interface MemberApi {
    @GetMapping("/{memberId}")
    BaseResponse<MemberDto> findById(@PathVariable("memberId") Long memberId);
}

이렇게 memberId로 MemberDto를 받아오는 api를 작성했다.

사용하는 방법은

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CommentServiceImpl implements CommentService{

    private final CommentRepository commentRepository;
    private final PostRepository postRepository;
    private final MemberApi memberApi;


    public List<CommentResponse> getComments(Long postId) {
        checkPostAvailability(postId);
        return commentRepository.findAllByPostId(postId).stream().map(
                comment -> {
                    MemberDto member = memberApi.findById(comment.getMemberId()).getResult();
                    return new CommentResponse(member.getMemberName(), comment);
                }
        ).collect(Collectors.toList());

    }
}

위와 같이 memberApi를 통해 받아온 MemberDto에서 필요한 부분을 꺼내쓰면 된다.

그리고 당연히 userModule에서 memberDto를 보내주는 api도 만들어줘야한다.

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

    private final MemberService memberService;

    @GetMapping("/members/{memberId}")
    public BaseResponse<MemberDto> getMember(@PathVariable("memberId")Long memberId){
        return new BaseResponse<>(memberService.getOneMember(memberId));
    }

}

이 때 MemberDto에는 꼭 Member엔티티 전체 필드를 넣지 않고 필요한 필드만 넣어도 된다.

(ex. 사용자의 비밀번호는 activity 모듈에서 굳이 필요없으니 memberdto에는 비밀번호를 같이 보내지 않아도 된다.)

 

이렇게 첫번째 생각해볼 점은 해결을 했고 2번째로 common 모듈의 필요성에 대해서는 생각해보았을 때

모든 모듈에서 사용되는 BaseResponse, Exception, Dto 들은 common에 정의하는 것이 합리적이라는 생각이 들었다.

굳이 똑같은 코드를 모든 모듈에 새롭게 만들 필요가 없다고 생각했다.

물론 나중에 msa로 변환한다면 다른 언어를 사용하여 서버가 구성될 수 있기 때문에 모든 마이크로 서비스에 커먼을 적용시키진 못할 수 있겠지만 일단 지금은 같은 개발 환경에서 개발되어지고 있으니 common을 굳이 없앨 필요는 없다는 생각이 들었다!