포스트

(멋쟁이사자처럼_백엔드스쿨플러스) Day32 스프링 리스너를 통한 종속성 제거

모놀리식 아키텍처에서 마이크로서비스 아키텍처 변환

  • 모놀리식에서 마이크로 서비스로 전환하는 단계는 다음과 같다.

서비스 -> 모듈 -> 종속성 분리 -> 마이크로 서비스

역할 기반 엔티티로 종속성 분리하기

  • GIT 레포
  • 모놀리식 아키텍처는 하나의 애플리케이션으로 모든 기능을 처리하는 방식이다.
  • 기본 스프링 세팅

모듈로 분리하기

  • GIT 레포
  • post는 멤버와 연관관계로 종속성을 가지고 있다.
  • post와 member를 모듈로 분리한다.

    1
    2
    3
    4
    5
    6
    7
    
      public class Post extends BaseEntity {
            private String title;
            private String content;
        
            @ManyToOne
            private Author author;
        }
    
  • member을 post 도메인에서 Author라는 entity로 변경한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    @Entity
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    @Getter
    @Setter
    @Table(name="member")
    public class Author extends BaseEntity {
      @Column(name="nickname")
      private String writer;
    }
    

    Image

member 등록하기

  • GIT 레포
  • 테스트를 위해 member를 등록한다.

member 종속성 분리하기

모놀리식 아키텍처

  • 모놀리식 아키텍쳐에서는 MemberService를 통해 Member를 받아 Post에 Author로 저장 할 수 있다.
  • 이는 각 domain 간에 종속성이 발생한다.

    1
    2
    3
    4
    5
    6
    7
    8
    
    Member member1 = memberService.join("user1", "1234", "user1").getData();
    Member member2 = memberService.join("user2", "1234", "user2").getData();
    Member member3 = memberService.join("user3", "1234", "user3").getData();
      
    // member domain에 종속성 발생
    Post post1 = postService.write(member1, "title1", "content1").getData();
    Post post2 = postService.write(member2, "title2", "content2").getData();
    Post post3 = postService.write(member3, "title3", "content3").getData();
    

종속성 분리하기

  • Post의 Authoer를 Member가 아닌 프록시 객체를 생성하여 DB에서 직접 가져온다.
  • 이를 위해서 EntityManager의 getReference를 활용한다.
  • Memebr의 Id를 참조 하기 때문에 종속성을 완전히 분리한 것은 아니다.
  • 각 결합 정도를 비교하면 다음과 같다.

    1
    2
    3
    4
    5
    6
    7
    8
    
    // Member 전체 객체를 직접 사용
    Author author = new Author(member);
      
    // Member의 ID만 사용하고, 프록시로 지연 로딩
    Author author = entityManager.getReference(Author.class, member.getId());
      
    // 순수 ID값만 사용(궁극적 목표)
    Author author = new Author(memberId);
    

    일반적인 엔티티 조회

    1
    2
    3
    
    Author author = entityManager.find(Author.class, id);
    // 즉시 DB 조회 발생
    // SELECT * FROM author WHERE id = ?
    

    프록시를 통한 지연로딩

    1
    2
    3
    4
    5
    6
    7
    8
    
    // 프록시 객체 생성 (DB 조회 없음)
    Author author = entityManager.getReference(Author.class, member.getId());
    // 이 시점까지는 DB 조회 없음
    Order order = Order.builder()
                       .author(author)  // ID만 필요해서 DB 조회 발생 안함
                       .build();
    // 이 시점에 실제 DB 조회 발생
    System.out.println(author.getName());  // DB 조회 발생!
    
  • 이를 통해 PostService에서 MemberService에 종속성을 줄일 수 있다.

    1
    2
    3
    4
    5
    6
    7
    
    Author author1 = postService.of(member1);
    Author author2 = postService.of(member2);
    Author author3 = postService.of(member3);
      
    Post post1 = postService.write(author1, "title1", "content1").getData();
    Post post2 = postService.write(author2, "title2", "content2").getData();
    Post post3 = postService.write(author3, "title3", "content3").getData();
    

PostCount 등록하기(member기반)

  • GIT 레포
  • 모놀리식으로 PostCount 증가는 PostService에서 MemberService를 호출하여 증가 시킨다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//member entity에 post 증가 로직 추가 
@Setter(PRIVATE)
private long postCount;
public void increasePostCount() {
  postCount++;
}

//memberservice에 post 증가 로직 추가
@Transactional
public void increasePostCount(long id) {
  findById(id).ifPresent(Member::increasePostCount);
}

private Optional<Member> findById(long id) {
  return memberRepository.findById(id);
}

//postservice에 post 증가 로직 추가
public RsData<Post> write(Author author, String title, String content) {
  //모놀리식으로 작성한 코드
  memberService.increasePostCount(author.getId());

  //....이후 return 동일
}

PostCount 종속성 제거

  • GIT 레포
  • postCount 증가로직을 Author에서 직접적으로 수행한다.
  • postService에서는 Author만으로도 postCount 증가가 가능하다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    @Table(name="member")
    public class Author extends BaseEntity {
      @Column(name="nickname")
      private String writer;
      
      @Column(columnDefinition = "BIGINT default 0")
      @Setter(PRIVATE)
      private long postsCount;
      
      public void increasePostsCount() {
        postsCount++;
      }
    }
    

역할 기반 엔티티 설계의 한계

  • 역할 기반 엔티티 설계는 역할에 따라 다른 동작을 수행하는 엔티티를 설계하는 방식으로 종속성을 분리할 수 있다.
  • 하지만 복잡한 엔티티를 다른 도메인에서 사용할때 복잡도가 증가하고 유지보수가 난해해진다.

스프링 이벤트

noti모듈 만들기

기본 Noti 모듈 생성

  • GIT 레포
  • 게시글이 생성되면 알림 로그를 출력하는 기본 모듈을 만든다.
  • 현재 Post와 Noti는 종속성이 존재한다.

게시글 생성시 Noti 추가하기

  • GIT 레포
  • Post가 발생하면 Noti 모듈에 post를 전달하여 Noti를 생성한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    
    //PostService에서 NotiService를 호출
    private void firePostCreateEvent(Post post) {
       notiService.postCreated(post);
    }
      
    //NotiService에서 Noti를 생성
    @Transactional
    public void postCreated(Post post){
      Member actor = postService.of(post.getAuthor());
      
      List<Member> receivers = memberService
        .findAll()
        .stream()
        .filter(member -> !member.equals(actor))
        .toList();
      
      receivers.forEach(receiver -> {
          Noti noti = Noti.builder()
            .actor(actor)
            .receiver(receiver)
            .relTypeCode(post.getModelName())
            .relId(post.getId())
            .typeCode("POST")
            .type2Code("CREATED")
            .read(false)
            .build();
          notiRepository.save(noti);
        }
      );
    }
    

스프링 이벤트로 종속성 제거하기

  • GIT 레포
  • 기존 PostService에서 NotiService를 호출하는 방식에서 스프링 이벤트를 사용하여 NotiService를 호출한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    //PostService에서 NotiService를 호출
    private void firePostCreateEvent(Post post) {
      notiService.postCreated(post);
    }
      
    // eventPublisher 주입
    private final ApplicationEventPublisher eventPublisher;
      
    public RsData<Post> write(Author author, String title, String content) {
      // ...기존코드 
        
      eventPublisher.publishEvent(new PostCreatedEvent(this, post));
      
      return RsData.of(post);
    }
    
  • global로 사용할 수 있도록 PostCreatedEvent를 생성한다.
1
2
3
4
5
6
7
8
9
10
11
// com/ll/sbrdk/global/event;

@Getter
public class PostCreatedEvent extends ApplicationEvent {
  private final Post post;
  public PostCreatedEvent(Object source, Post post) {
    super(source);
    this.post = post;
  }
}

  • Noti에서 Post이벤트를 감지한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
// com/ll/sbrdk/domain/noti/noti/eventListener;

@Component
@RequiredArgsConstructor
@Transactional
public class NotiEventListener {
  private final NotiService notiService;

  @EventListener
  public void listenPost(PostCreatedEvent event){
    notiService.postCreated(event.getPost());
  }
}
  • GIT 레포

    EnablaAsync를 통해 비동기로 이벤트를 처리할 수 있다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    @EnableAsync  // 클래스 레벨에 선언
    @Configuration
    public class AsyncConfig {
       // 비동기 설정
    }
    
    @Service
    public class EmailService {
       @Async  // 메서드 레벨에 선언 
       public void sendEmail() {
           // 시간이 걸리는 이메일 발송 작업
       }
    }
    
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.