기존의 프로필 이미지 기능
이번 포스팅은 MSA 작업을 위한 기능 분리를 시작해볼까 한다.
기능을 분리하기에 앞서 어떤 기능을 분리할지 선택해야 되는데 SSM 프로젝트에 있는 여러 기능 중에서 프로필 이미지 기능을 분리하기로 정했다.
프로필 이미지 기능을 분리하는 이유는 3 가지가 있다.
1. 채팅 기능과 관련된 프로필 이미지
나는 SSM 프로젝트를 진행했을 당시 채팅 기능을 주로 개발하였다. 해당 기능을 개발할 때 사용자가 등록한 프로필 이미지가 보여야 하는데 그러지 못하고 기본 프로필 이미지로 등록되는 순간이 종종 있었다.
2. 조금 더 효율적인 코드를 작성해 보자
기존의 프로필 이미지 기능은 일 대 다 관계로 형성되어 진행했었다. 하지만 자세히 살펴보니 프로필 이미지를 추가하는 기능은 따로 없었고 이를 통해 일 대 다 관계가 별로 의미 없을 것이라고 판단했다.
오히려 프로필 이미지를 담을 ArrayList를 사용하기 때문에 메모리만 차지하는 상황이 발생하고 있다.
3. 전체 서비스에 가장 영향력이 적은 기능
MSA를 적용할 때 메인이 되는 기능을 독립된 모듈로 분리시키면 전체 서비스에 큰 영향을 미치게 된다. 따라서 최대한 영향을 안 끼치는 기능부터 차례대로 분리를 해야 되는데 SSM 프로젝트에서는 프로필 이미지 기능이 적합하다고 판단했다.
당장 프로필 이미지가 없다고 하더라도 전체 서비스를 이용하는 데 있어 문제 되지 않는다. 그냥 단순히 내 프로필 이미지만 안 보일 뿐이다.
SSM 프로젝트에서 핵심이 되는 일정, 채팅 기능을 분리한다고 하면 고려해야 될 사항이 많아지고 무엇보다 문제가 발생했을 때 서비스에 미치는 영향이 너무 크다. 따라서 핵심 기능들은 충분히 고려하고 아키텍처를 잘 설계하여 분리해야 한다.
일 대 다 관계를 일 대 일 관계로 개선하기
먼저 해당 기능을 MSA로 분리하기 전 일 대 일 관계로 변경해 주는 작업을 진행하였다.
바로 MSA 분리하면 되지 왜 굳이 일 대 일로 만들지라는 의문이 들 수 있겠지만 일 대 일로 개선했을 때와 MSA로 분리했을 때를 비교해보고 싶었다.
이제부터 어떻게 일 대 일로 변경했는지 살펴보자
@OneToOne
내가 만든 프로필 이미지 기능을 생각해 보면 한 명의 멤버는 단 하나의 프로필 이미지만 가질 수 있고, 멤버가 먼저 생성이 되고 나서 프로필 이미지가 등록돼야 한다.
일 대 다 관계에서는 다 측에서 외래키를 가지지만 일 대 일은 둘 다 외래키를 보유할 수 있다. 그러면 어느 테이블이 외래키를 가질 것인지 생각해봐야 하는데 2가지 경우의 수가 있다.
1. Member 테이블에서 ProfileImage 외래키를 가진다.
2. ProfileImage에서 Member 외래키를 가진다.
프로필 이미지를 조회할 때 Member를 통해서 조회하게 되는데 따라서 Member 테이블에서 ProfileImage의 외래키를 가지면 된다.
이제 일 대 일 관계로 변경해 보면서 결과를 확인해 보자.
먼저 Member와 ProfileImage의 관계를 기존의 @ManyToOne에서 @OneToOne으로 변경해 줬다.
Member Entity
// @OneToMany(mappedBy = "member") // 기존의 일 대 다 코드
// private List<ProfileImage> profileImage; // 기존의 일 대 다 코드
@OneToOne(fetch = FetchType.LAZY) // 일 대 일 관계로 변경한 코드
@JoinColumn(name = "profileIdx")
private ProfileImage profileImage;
먼저 Member 엔티티에서 ProfileImage 엔티티의 외래키를 지정해 준다.
외래키를 지정해 줌으로 Member 엔티티는 연관관계의 주인이 된다.
ProfileImage Entity
// @ManyToOne(fetch = FetchType.LAZY) // 기존의 일 대 다 코드
// @JoinColumn(name = "member_idx")
// private Member member;
@OneToOne(mappedBy = "profileImage") // 일 대 일 관계로 변경한 코드
private Member member;
ProfileImage 테이블에서는 mappedBy를 통해서 Member 엔티티와 매핑을 해준다.
MemberService
// 일 대 다 관계에서의 프로필 이미지 조회 코드
public BaseResponse<List<GetProfileImageRes>> getMemberProfile(GetProfileImageReq getProfileImageReq) {
Member member = memberRepository.findByMemberId(getProfileImageReq.getMemberId()).orElseThrow(() ->
MemberNotFoundException.forMemberId(getProfileImageReq.getMemberId()));
List<GetProfileImageRes> getProfileImageRes = new ArrayList<>();
for (ProfileImage profileImage : member.getProfileImage()) {
getProfileImageRes.add(GetProfileImageRes.buildProfileImage(profileImage.getImageAddr()));
}
return BaseResponse.successRes("CHATTING_008", true, "프로필이미지 조회가 성공했습니다.", getProfileImageRes);
}
일 대 다 관계 코드에서는 Member를 통해 프로필 이미지를 불러오고 forEach 문을 통해서 List에 담아 DTO를 생성해 주고 return을 해줬다.
// 일 대 일 관계에서의 프로필 조회 코드
@Transactional
public BaseResponse<GetProfileImageRes> getMemberProfile(GetProfileImageReq getProfileImageReq) {
Member member = memberRepository.findByMemberId(getProfileImageReq.getMemberId()).orElseThrow(() ->
MemberNotFoundException.forMemberId(getProfileImageReq.getMemberId()));
GetProfileImageRes getProfileImageRes = GetProfileImageRes.buildProfileImage(member.getProfileImage().getImageAddr());
return BaseResponse.successRes("CHATTING_008", true, "프로필이미지 조회가 성공했습니다.", getProfileImageRes);
}
일 대 일 관계로 변경된 코드에서는 Member가 존재하는지 확인해 보고 해당 멤버의 프로필 이미지를 불러와 DTO를 바로 생성해서 반환해 주게 된다.
어차피 한 명의 멤버는 하나의 프로필 이미지만 가지고 있기 때문에 굳이 반복문을 사용할 필요가 없고 관계를 변경해 줌으로 해당 반복문을 지울 수 있게 되었다.
@OneToOne에서 주의할 점
JPA를 사용하다 보면 항상 N + 1문제를 조심해야 한다. 일 대 일 관계에서도 해당 문제가 발생할 수 있는데 한 번 알아보자.
일 대 일 관계에서는 연관관계의 주인이 아닌 다른 엔티티에서 데이터를 조회할 시 지연 로딩이 적용되지 않고 즉시 로딩이 된다고 한다. 이에 따라서 N + 1 문제가 발생한다.
지연 로딩이 적용되지 않고 즉시 로딩이 되는 이유는 연관관계의 주인이 아니면 외래키를 가지고 있지 않아서 해당 테이블이 존재하는지 모르게 된다.
Member와 ProfileImage를 가지고 예를 들어보면 Member 테이블은 ProfileImage의 외래키를 가지고 있기 때문에 해당 키를 통해서 ProfileImage가 null인지, 혹은 값이 있는지를 알 수 있다.
이를 통해서 알게 된 ProfileImage 값을 proxy에 넣어주고 해당 proxy를 Member로 넣어줄 수 있게 된다.
하지만 ProfileImage 입장에서 생각해 보면 Member의 외래키를 가지고 있지 않아서 해당 테이블이 null인지, 혹은 값이 있는지 알 수 없게 된다.
따라서 무조건 Member 테이블을 조회해서 값을 확인해야 돼서 지연 로딩을 할 수 없게 되고 즉시 로딩이 이루어져 N + 1 문제가 발생하게 된다.
이처럼 일 대 일 관계에서 주의할 점은 추후 확장할 때 고려하면 좋을 것 같아서 따로 찾아보았다.
jmeter로 진행한 성능 테스트
기능을 개선해 보며 일 대 다 관계와 일 대 일 관계의 성능이 어떻게 차이 나는지 확인해보고 싶었다.
처음에 nGrinder를 통해서 성능 테스트를 진행해 보려고 시도를 했었지만 내가 사용하는 노트북의 성능 이슈(?)로 인해 에이전트의 리소스를 많이 줄 수 없었고 그러면서 vuser가 일정 이상 올라가게 되면 죽어버리는 상황이 발생했다.
nGrinder의 아쉬움을 뒤로하고 jmeter를 통해서 다시 성능 테스트를 진행해 봤다.
성능 테스트를 진행할 기능으로는 프로필 이미지를 불러오는 기능으로 선택하였다.
스레드(유저)는 3000으로 지정하고 Ramp-up period는 0.1초로 지정하여 거의 동시에 요청을 보내게 설정하였다.
마지막으로 Loop Count는 100으로 지정해 반복해서 요청을 보낼 수 있게 설정하였다.
구분 | 샘플 | TPS | Error | 총 소요 시간 |
일 대 다 | 3000000 | 545/sec | 0% | 9분 12초 |
일 대 일 | 3000000 | 413/sec | 0% | 12분 5초 |
성능 테스트 결과를 확인해 보면 일 대 다 관계가 일 대 일 관계보다 더 좋은 성능을 보여준다.
왜 일 대 다 관계가 더 좋은 성능을 보여주는지 확인해 보니 Member 테이블과 ProfileImage 테이블을 따로 조인하지 않고 조회하고 있었다.
반면에 일 대 일 관계는 프로필 이미지를 조회할 때 Member 테이블과 ProfileImage 테이블이 left outer join을 하기 때문에 조금 낮은 성능을 보여주고 있다.
jmeter로 테스트 결과를 보니 일 대 다 관계에서 일 대 일 관계로 바꿨지만 성능적인 측면에서는 좋은 결과를 얻지 못했다.
소요 시간도 예상했던 것보다 오래 걸리고 TPS도 너무 낮은 수치를 보여줬다. 에러는 없었지만 애초에 프로필 이미지를 불러오는 기능은 그리 큰 기능이 아니어서 큰 의미는 없다고 생각한다.
MSA 적용하기
이번 SSM 프로젝트에서는 MSA를 적용해 보려고 생각했었다. 물론 대용량 트래픽이 발생하지 않아서 MSA의 필요성이 떨어질 수도 있지만 최근 서비스 기업에서는 MSA를 많이 요구하고 미니 프로젝트에서 사용했었는데 조금 더 깊게 사용해보고 싶어서 공부할 겸 적용해보려고 한다.
MSA를 적용하기 전에 간단히 이해하고 싶으면 밑의 글을 참고해 보자.
앞서 진행했던 성능 테스트 결과를 보면 그리 좋은 성능을 보여주지 못하는데 MSA로 분리하면 테이블 간의 관계가 사라지기 때문에 어느 정도 속도 측면에서 개선이 이뤄질 것이라고 생각한다.
먼저 이벤트 스토밍(Event Storming)을 진행해서 DDD 설계를 진행해봤다.
Command로 프로필 이미지 조회와 등록을 만들고 이벤트로는 프로필 이미지 조회됨, 프로필 이미지 등록됨으로 나눴다.
외부 시스템으로는 AWS S3를 사용하기 때문에 따로 핑크색 스티커를 사용하여 따로 분리하였다.
Aggregate로는 Profile Image 데이터를 받아서 처리하게끔 구성하였다.
확실히 이벤트 스토밍을 먼저 그려보고 개발을 진행하니 헥사고날 아키텍처를 어떻게 구성하면 좋을지 이해할 수 있었다.
이제 MSA 적용을 시작하기 위해 헥사고날 아키텍처 그림을 그려보며 어떻게 프로필 이미지 기능을 분리할지 생각해 보았다.
헥사고날 아키텍처를 적용함에 따라 Adapter, Application, Domain으로 구성을 진행했다.
Adapter
먼저 Adapter에는 API 요청이 들어오는 WebAdapter와 DB에 접근하기 위한 Persistence Adapter, 프로필 이미지를 업로드하기 위해 S3에 접근하기 위한 External Adapter가 존재한다.
WebAdapter는 입력 값에 대한 유효성 검증이 완료된 요청을 Service로 넘겨주기 위해 입력 모델인 Command로 매핑하여 ProfileImageInPort를 통해 Service에게 전달한다.
Persistence Adapter는 Service에서 ProfileImageOutPort로 전달받은 값을 엔티티로 매핑하여 MySQL에 저장한다. 그런 다음 반환된 출력 값을 애플리케이션 포맷으로 매핑하여 Service로 전달해 준다.
External Adapter는 Service에서 ProfileImageUploadS3OutPort로 전달받은 프로필 이미지를 AWS S3에 업로드하고 반한 되는 프로필 이미지 url을 다시 Service로 전달해 준다.
Application
Application에서는 ProfileImageService를 생성하여 WebAdapter에서 전달받은 Command를 작성한 비즈니스 로직에 따라서 검증을 거치게 되는데 하나씩 과정을 살펴보자.
먼저 프로필 이미지를 AWS S3에 업로드를 해야 되기 때문에 ProfileImageUploadS3OutPort로 command를 전달하여 이미지 url을 반환받는다.
반환받은 이미지 url과 회원의 Email을 통해서 ProfileImage 도메인을 생성하고 DB에 저장하기 위해 PersistenceAdapter로 해당 도메인을 전달해 준다.
PersistenceAdapter에서 반환된 값을 다시 WebAdapter로 전달해 준다.
Domain
프로필 이미지 기능에서 사용할 Domain은 회원의 Email과 S3에 업로드된 프로필 이미지 경로로 지정하였다.
기능 분리하기
이번에는 실제 코드를 살펴보면서 어떻게 기능을 분리했는지 알아보자.
Adapter
먼저 살펴볼 부분은 Adapter이다.
총 3개의 Adapter가 존재하는데 처음 요청을 받게 될 WebAdapter부터 코드를 확인해 보자.
WebAdapter
package com.profile.profileimage.adapter.in.web;
import com.profile.profileimage.application.port.in.GetProfileImageCommand;
import com.profile.profileimage.application.port.in.GetProfileImageInPort;
import com.profile.profileimage.application.port.in.PostProfileImageCommand;
import com.profile.profileimage.application.port.in.PostProfileImageInPort;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequiredArgsConstructor
@RequestMapping("/profile")
public class ProfileImageController {
private final PostProfileImageInPort postProfileImageInPort;
private final GetProfileImageInPort getProfileImageInPort;
@RequestMapping(method = RequestMethod.POST, value = "/register")
public ResponseEntity<Object> register(@RequestPart String memberEmail, MultipartFile profileImage) {
PostProfileImageCommand command = PostProfileImageCommand.builder().memberEmail(memberEmail).profileImage(profileImage).build();
return ResponseEntity.ok().body(postProfileImageInPort.registerProfile(command));
}
@RequestMapping(method = RequestMethod.GET, value = "/{memberEmail}")
public ResponseEntity<Object> getProfileImage(@PathVariable String memberEmail) {
GetProfileImageCommand command = GetProfileImageCommand.builder().memberEmail(memberEmail).build();
return ResponseEntity.ok().body(getProfileImageInPort.getProfileImage(command));
}
}
WebAdapter 코드를 보면 기존의 SSM 프로젝트에서 작성했던 Controller 코드와 동일하다.
다른 부분이 있다면 Service로 데이터를 넘겨줄 Port와 입력 모델인 Command를 확인할 수 있다.
PersistenceAdapter
이번에는 DB에 접근해서 데이터를 처리할 어댑터인 PersistenceAdapter에 대해서 살펴보자.
Spring Data JPA를 활용하여 ProfileImage Entity를 생성하고, 이미지 주소 저장과 불러오는 쿼리 작업을 진행할 수 있게 코드를 작성하였다.
package com.profile.profileimage.adapter.out.persistence;
import com.profile.profileimage.application.port.out.ProfileImageOutPort;
import com.profile.profileimage.domain.ProfileImage;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
@RequiredArgsConstructor
public class ProfileImagePersistenceAdapter implements ProfileImageOutPort {
private final ProfileImageRepository profileImageRepository;
@Override
public ProfileImage registerProfile(ProfileImage profileImage) {
ProfileImageEntity profileImageEntity = profileImageRepository.save(ProfileImageEntity.builder()
.memberEmail(profileImage.getMemberEmail())
.imagePath(profileImage.getImagePath())
.build());
return ProfileImage.builder()
.memberEmail(profileImageEntity.getMemberEmail())
.imagePath(profileImage.getImagePath())
.build();
}
@Override
public ProfileImage findProfile(String memberEmail) {
Optional<ProfileImageEntity> profileImage = profileImageRepository.findByMemberEmail(memberEmail);
if (profileImage.isPresent()) {
return ProfileImage.builder()
.memberEmail(profileImage.get().getMemberEmail())
.imagePath(profileImage.get().getImagePath())
.build();
}
return null;
}
}
어댑터에서는 ProfileImageOutPort 인터페이스를 구현하여 각각 이미지 주소를 저장하는 로직과 이미지 주소를 조회하는 로직을 작성하였다.
package com.profile.profileimage.adapter.out.persistence;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class ProfileImageEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long idx;
private String memberEmail;
private String imagePath;
}
Spring Data JPA를 사용하기 때문에 프로필 이미지 테이블을 생성할 ProfileImageEntity 클래스를 작성하였다.
package com.profile.profileimage.adapter.out.persistence;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface ProfileImageRepository extends JpaRepository<ProfileImageEntity, Long> {
Optional<ProfileImageEntity> findByMemberEmail(String memberEmail);
}
마지막으로 회원의 이메일을 통해서 프로필 이미지 주소를 불러올 수 있게 Repository에 findByMemberEmail 메서드를 생성하였다.
ExternalAdapter
External Adapter는 외부의 AWS S3에 접근하기 위한 외부 어댑터 역할이다.
코드는 기존의 AWS S3 버킷에 이미지를 업로드하는 부분을 그대로 가져와서 사용하였다.
package com.profile.profileimage.adapter.out.aws;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.profile.profileimage.application.port.out.ProfileImageUploadS3OutPort;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
@Component
@RequiredArgsConstructor
public class ProfileImageUploadToS3 implements ProfileImageUploadS3OutPort {
@Value("${cloud.aws.s3.profile-bucket}")
private String bucket;
private final AmazonS3 s3;
@Override
public String uploadToS3(MultipartFile profileImage) {
return uploadProfileImage(profileImage);
}
private String makeFolder() {
String str = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
return str.replace("/", File.separator);
}
private String uploadProfileImage(MultipartFile profileImage) {
String originalName = profileImage.getOriginalFilename();
String folderPath = makeFolder();
String uuid = UUID.randomUUID().toString();
String saveFileName = folderPath + File.separator + uuid + "_" + originalName;
try {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(profileImage.getSize());
metadata.setContentType(profileImage.getContentType());
s3.putObject(bucket, saveFileName.replace(File.separator, "/"), profileImage.getInputStream(), metadata);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
// 로컬 파일 시스템에서 파일 삭제
File localFile = new File(saveFileName);
if (localFile.exists()) {
localFile.delete();
}
return s3.getUrl(bucket, saveFileName.replace(File.separator, "/")).toString();
}
}
}
Application
Application의 구조를 보면 내부, 외부 포트와 서비스로 구성되어 있다.
내부 포트부터 하나씩 코드를 확인해 보며 흐름을 머릿속으로 그려보자.
public interface PostProfileImageInPort {
ProfileImage registerProfile(PostProfileImageCommand command);
}
public interface GetProfileImageInPort {
ProfileImage getProfileImage(GetProfileImageCommand command);
}
위의 코드는 프로필 이미지를 저장하는 기능과 불러오는 기능에 대해서 Service로 요청을 넘겨줄 내부 포트로 매개 변수로는 이전에 WebAdapter에서 만든 Command를 받고 있다.
내부 또는 외부 포트는 interface로 구현하게 되는데 이를 통해서 중앙에 있는 애플리케이션 부분은 외부의 영향을 받지 않게 된다. (어댑터를 다른 걸로 변경해도 큰 영향이 없음)
@Builder
@Data
public class GetProfileImageCommand {
private final String memberEmail;
}
@Builder
@Data
public class PostProfileImageCommand {
private final String memberEmail;
private final MultipartFile profileImage;
}
다음으로는 Service로 값을 넘기기 위한 입력 모델인 Command 코드이다.
아직은 기본 구성을 하기 위한 코드이기 때문에 유효성 검증을 하는 코드가 따로 존재하지 않는다. (추후 Validation을 통해서 추가할 예정)
public interface ProfileImageOutPort {
ProfileImage registerProfile(ProfileImage profileImage);
ProfileImage findProfile(String memberEmail);
}
public interface ProfileImageUploadS3OutPort {
String uploadToS3(MultipartFile profileImage);
}
외부 포트는 2가지의 포트가 존재하는데 하나는 PersistencAdapter를 호출하는 외부 포트이고, 나머지 하나는 ExternalAdapter를 호출하는 외부 포트이다.
package com.profile.profileimage.application.service;
import com.profile.profileimage.application.port.in.GetProfileImageCommand;
import com.profile.profileimage.application.port.in.GetProfileImageInPort;
import com.profile.profileimage.application.port.in.PostProfileImageCommand;
import com.profile.profileimage.application.port.in.PostProfileImageInPort;
import com.profile.profileimage.application.port.out.ProfileImageOutPort;
import com.profile.profileimage.application.port.out.ProfileImageUploadS3OutPort;
import com.profile.profileimage.domain.ProfileImage;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ProfileImageService implements PostProfileImageInPort, GetProfileImageInPort {
private final ProfileImageOutPort profileImageOutPort;
private final ProfileImageUploadS3OutPort profileImageUploadS3OutPort;
@Override
public ProfileImage registerProfile(PostProfileImageCommand command) {
String imagePath = profileImageUploadS3OutPort.uploadToS3(command.getProfileImage());
ProfileImage profileImage = ProfileImage.builder().memberEmail(command.getMemberEmail()).imagePath(imagePath).build();
return profileImageOutPort.registerProfile(profileImage);
}
@Override
public ProfileImage getProfileImage(GetProfileImageCommand command) {
return profileImageOutPort.findProfile(command.getMemberEmail());
}
}
마지막으로 살펴볼 코드는 Service 부분으로 내부 포트로 전달받은 요청을 처리하는 코드이다.
Domain
import lombok.Builder;
import lombok.Data;
@Builder
@Data
public class ProfileImage {
private final String memberEmail;
private final String imagePath;
}
Domain으로는 회원의 이메일과 AWS S3에 업로드된 프로필 이미지 경로를 저장할 수 있게 지정하였다.
jmeter로 성능 테스트
MSA 구성을 완료하고 아까와 동일하게 프로필 이미지를 불러오는 기능을 jmeter로 성능 테스트를 진행해 봤다.
구분 | 샘플 | TPS | Error | 소요 시간 |
MSA 적용 후 | 3000000 | 1329/sec | 0% | 3분 45초 |
성능 테스트 결과를 살펴보면 기존의 일 대 일 관계에서 진행했던 결과보다 약 3배 정도의 개선이 이뤄진 것을 확인할 수 있다.
확실히 테이블 간 관계가 사라지게 되면서 쿼리가 단순해져 높은 성능을 낼 수 있었다.
물론 해당 기능은 여기서 조금 떨어질 것으로 예상하고 있는데 추후 Spring Cloud Gateway를 적용하게 되면서 성능이 조금 떨어질 것 같다.
Spring Cloud Gateway를 적용해 보고 다시 테스트를 진행해 보며 얼마나 차이가 있을지 확인해 봐야겠다.
아쉬웠던 점
이번에 MSA를 적용하면서 아직 많은 부분에서 부족하다고 느꼈다.
헥사고날 아키텍처를 바로 그려보는 것이 아닌 밑바탕이 될 수 있는 DDD나 이벤트 스토밍을 먼저 진행하고 헥사고날 아키텍처를 그려봤어야 했는데 그러지 못한 부분이 너무 아쉬웠다.
그래도 원래 MSA에 대해서 지식이 부족하기도 했었고, 계속 관심을 가졌던 분야라서 오히려 부족한 부분을 채울 수 있는 좋은 기회라고 생각한다.
지금 작성한 블로그도 설명이 많이 부족해 보여서 MSA를 계속 공부하면서 블로그 내용을 추가할 예정이다.
'프로젝트' 카테고리의 다른 글
[SSM_프로젝트] Spring Netflix Eureka 적용하기 (0) | 2024.06.13 |
---|---|
[SSM_프로젝트] - Spring Cloud Gateway 적용하기 (0) | 2024.06.12 |
[SSM_프로젝트] 다시 처음으로... (0) | 2024.06.03 |
[SSM_프로젝트] - GitHub Actions와 AWS Code Deploy를 활용한 CI/CD 적용 - (2) (0) | 2024.06.01 |
[SSM_프로젝트] - GitHub Actions와 AWS Code Deploy를 활용한 CI/CD 적용 - (1) (0) | 2024.05.21 |