728x90

트랜잭션


7편에서 엔터티 객체를 통해 JPA기술로 도메인과 데이터베이스를 매핑하고, createMember메서드를 통해 저장할 수 있도록 구조를 설계했다.

 

createMember메서드를 호출하고 트랜잭션하여 저장을 완료하기 위한 Service의 비즈니스 로직을 마무리해야 한다.

@WebAdapter
@RestController
@RequiredArgsConstructor
public class RegisterMembershipController {

    private final RegisterMembershipUseCase registerMembershipUseCase;

    @PostMapping(path = "/membership/register")
    Membership registerMemeber(@RequestBody RegisterMembershipRequest request) {

//        request -> UseCase(inbounded port) -> Service (biz logic)
        RegisterMembershipCommand command = RegisterMembershipCommand.builder()
                .name(request.getName())
                .address(request.getAddress())
                .email(request.getEmail())
                .isValid(true)
                .isCorp(request.isCorp())
                .build();

        return registerMembershipUseCase.registerMembership(command);
    }
}
@UseCase
@Transactional
@RequiredArgsConstructor
public class RegisterMembershipService implements RegisterMembershipUseCase {

        private final RegisterMembershipPort registerMembershipPort;
        private final MembershipMapper membershipMapper;

    @Override
    public Membership registerMembership(RegisterMembershipCommand command) {
//Port --> Adapter (createMember -> save) --> new JpaEntity --> toDomain 
        MembershipJpaEntity jpaEntity = registerMembershipPort.createMember(
                new Membership.MembershipName(command.getName()),
                new Membership.MembershipAddress(command.getAddress()),
                new Membership.MembershipEmail(command.getEmail()),
                new Membership.MembershipIsValid(command.isValid()),
                new Membership.MembershipIsCorp(command.isCorp())
        );
        return membershipMapper.mapToDomainEntity(jpaEntity);
    }
}

 

정리해보면,

  1. 클라이언트에게 HTTP요청을 Controller에서 Request객체로 받는다.
  2. 빌더 패턴을 사용해 Command 명령객체를 생성하여 UseCase 인터페이스 어댑터에 보낸다.
  3. UseCase를 구현한 Service 계층은 Port의 메서드를 호출한다.
  4. Adapter는 Port의 구현체로 도메인 객체를 받아 매핑된 엔터티를 생성한다.
  5. 새로운 엔터티는 주입받은 Spring Data JPA의메서드에 의해 DB에 저장된다.
  6. Service는 저장된 엔터티를 받아와 Mapper의 mapToDomainEntity 메서드를 통해 도메인 객체로 변환한다.
  7. 변환된 객체는 HTTP 응답으로 반환된다.
728x90

들어가며


6편에 내용에서는 Port와 Adapter의 createMembership 메서드를 통해 DB에 회원정보를 저장하려고 했다.

 

createMembership 메서드에 앞서서 이번편에서는 JPA를 활용해서 도메인 객체와 데이터베이스 테이블 간의 매핑하고, Adapter로 DB와 상호작용하는 방식은 SpringDataJpaRepository를 통해 CRUD를 간단하게 조작할 수 있도록 한다.

 

또한, Mapper를 통해 DB로부터 반환된 객체를 도메인 객체로 변환해 Service에서 사용하기도 할 것이다.

도메인 객체와 데이터베이스 테이블 간의 매핑


JpaEntity는 매핑을 관리하는 JPA 기술로, 여기서는 Membership 도메인 객체를 DB에 테이블로 매핑한다.

@Entity
@Table(name = "membership")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MembershipJpaEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long membershipId;
    private String name;
    private String address;
    private String email;
    private boolean isValid;
    private boolean isCorp;

    @Override
    public String toString() {
        return "MembershipJpaEntity{" +
                "membershipId=" + membershipId +
                ", name='" + name + '\'' +
                ", address='" + address + '\'' +
                ", email='" + email + '\'' +
                ", isValid=" + isValid +
                ", isCorp=" + isCorp +
                '}';
    }
}

 

구성은 필드값, 생성자, toString으로 구현된다.

먼저 필드는 DB의 테이블 컬럼과 매핑되며, toString으로 메서드를 구현하면서 가독성을 높였다.

Spring Data JPA 기술로 CRUD 자동 조작하기


public interface SpringDataJpaRepository extends JpaRepository<MembershipJpaEntity, Long> {

}

 

SpringDataJpaMembershipRepository는 기본적으로 Spring Data JPA에서 제공하는 JpaRepository 인터페이스를 확장하면서 기본 메서드를 통해 CRUD를 조작할 수 있게 되었다.

이때, MembershipJpaEntity와 Long을 파라미터로 받아 해당 엔터티를 다루는데 간편하다.

 

SpringDataJpaMembershipRepository를 구현하기


@RequiredArgsConstructor
public class RegisterMembershipAdapter implements RegisterMembershipPort {

    private final SpringDataJpaRepository membershipRepository;


    @Override
    public MembershipJpaEntity createMember(
            Membership.MembershipName membershipName,
            Membership.MembershipAddress membershipAddress,
            Membership.MembershipEmail membershipEmail,
            Membership.MembershipIsValid membershipIsValid,
            Membership.MembershipIsCorp membershipIsCorp
    ) {
        return membershipRepository.save(
                new MembershipJpaEntity(
                        membershipName.getNameValue(),
                        membershipAddress.getAddressValue(),
                        membershipEmail.getEmailValue(),
                        membershipIsValid.isValidValue(),
                        membershipIsCorp.isCorpValue())
        );

 

다시 Port를 구현한 Adapter로 돌아와서 DB에 저장하는 기능을 수행하기 위해 DDD원칙에 따라 createMember메서드를 정의해야 한다.

 

createMember 메서드를 호출할 때, 도메인 객체 Membership를 파라미터로 전달 받아 Spring Data JPA의 save 메서드를 통해 새로운 엔터티를 DB에 저장하는 것이다.

public MembershipJpaEntity(String name, String address, String email, boolean isValid, boolean isCorp) {
    this.name = name;
    this.address = address;
    this.email = email;
    this.isValid = isValid;
    this.isCorp = isCorp;
}

 

여기서 createMember의 비즈니스 로직에서 new 엔터티를 생성하기 위해 따로 객체를 초기화할 필요가 있다. 기본 값들은 private으로 불변하며, 외부에서 사용하기 위해 생성자를 둔다.

 

이렇게 함으로써 DDD 원칙에 따라 도메인 객체를 사용하여 비즈니스 로직을 수행하고, 그 결과로 엔터티를 생성하여 저장하는 구조를 가진다.

 

728x90

Controller 컨트롤러에서 만든 UseCase 객체를 구현하기


 

Service 클래스는 Usecase를 상속받은 구현체로 비즈니스 로직 동작을 위해 존재한다.

@Service
@Transactional
public class RegisterMembership implements RegisterMembershipUseCase {
   @Override
    public Membership registerMembership(RegisterMembershipCommand command) {
        return null;
    }
}

Service의 비즈니스 로직은?


Command를 통해서 UseCase는 Membership 객체를 반환하는 registerMember (회원 등록) 메서드를 가지고 있다.

그러므로, Service의 목적은 생성된 Membership 객체를 DB에 전달하는 것이다.

그러나 Hexagonal 아키텍처이기 때문에 Port와 Adapter를 통해서 외부 시스템인 DB에 접근할 수 있다.

DB와 매핑하기 위한 관계 설정


다시 의존성 관계를 설정하는 과정에서 Port 역시 실제 데이터베이스와 직접 연결되지 않고 Adapter를 통해 의존관계를 가진다.

public interface RegisterMembershipPort {

    MembershipJpaEntity createMember(
            Membership.MembershipName membershipName,
            Membership.MembershipAddress membershipAddress,
            Membership.MembershipEmail membershipEmail,
            Membership.MembershipIsValid membershipIsValid,
            Membership.MembershipIsCorp membershipIsCorp
    );
}
public class RegisterMembershipAdapter implements RegisterMembershipPort {


    @Override
    public MembershipJpaEntity createMember(
            Membership.MembershipName membershipName,
            Membership.MembershipAddress membershipAddress,
            Membership.MembershipEmail membershipEmail,
            Membership.MembershipIsValid membershipIsValid,
            Membership.MembershipIsCorp membershipIsCorp
    ) {
        
    }
}

 

Port와 Adapter의 createMember 메서드는 도메인 객체를 받아와 JpaEntity를 반환한다. 

이 Jpa엔터티가 DB와 매핑하기 위한 핵심 기술로 7편에서 구현할 예정이다.

728x90

들어가며


4편에서 외부에서 내부로 데이터를 보낼 것이라고 예고했다.

//        request -> UseCase(inbounded port) -> Service (biz logic)

 

 

 

Hexagonal 아키텍처로 회원정보 인바운드 어댑터 개발 MSA프로젝트[4]

들어가며 개발의 흐름과 Hexagonal 아키텍처 구조에서 각 클래스와 인터페이스가 어떻게 상호작용하는지를 확실하게 이해하기 위해 기록하려고 한다. 어댑터 개발 앞선 3편에서는 Membership 도메인

wooltech.tistory.com

UseCase 인터페이스는 뭐하는 곳인가?


우선 인바운드 포트는 UseCase와 Command로 구성된다.

 

Controller에서 Request를 Port로 전달하는데, Port에서 이를 전달 받는 곳이 UseCase다.

그리고 UseCase는 Service가 비즈니스 로직을 수행할 수 있게 도움을 주는 역할을 한다.

public interface RegisterMembershipUseCase {
    Membership registerMembership(RegisterMembershipCommand command);

}

 

registerMembership메서드는 Command 명령객체를 받아 Membership객체를 만든다고 정의되어 있다.

갑자기 Command라는 객체가 등장했다!

 

좋은 객체지향 프로그래밍 DIP 준수


Controller에서는 Request를 바로 UseCase로 전달하지 않고 Command 패턴을 사용한다.

이유는 의존성 역전 원칙을 준수하기 위해서다.

 

좋은 객체 지향 SOLID를 위한 config 구성창 Jav Spring 프레임워크의 기본 (4)

좋은 객체 지향 프로그래밍 SOLID 용어 개념 SRP 단일 책임 원칙 = 하나의 클래스는 하나의 책임을 가짐 OCP 개방 및 폐쇄 원칙 = 확장에는 열려 있으나, 변경에는 닫힘 LSP 리스코프 치환 원칙 = 객체

wooltech.tistory.com

 

좋은 객체 프로그래밍은 서로간의 구체적인 의존을 피하고, 추상화에 의존해야 한다.

즉, Controller의 request가 변경되더라도 UseCase에는 영향을 주지 않아야 한다.

 

회원 등록을 위해 정보 캡슐화하기


@Builder
@Data
@EqualsAndHashCode(callSuper = false)
public class RegisterMembershipCommand extends SelfValidating<RegisterMembershipCommand> {
    // except membershipId, isValid

    @NotNull
    private String name;
    @NotNull
    private String address;
    @NotNull
    private String email;
    @AssertTrue
    private boolean isValid;
    private boolean isCorp;


    public RegisterMembershipCommand(String name, String address, String email, boolean isValid, boolean isCorp) {
        this.name = name;
        this.address = address;
        this.email = email;
        this.isValid = isValid;
        this.isCorp = isCorp;

        this.validateSelf();
    }
}

 

명령 객체를 사용하기 위해 RegisterMembershipCommand의 생성자를 초기화한다.

이제 RegisterMembershipCommand는 값을 받을 수 있다.

값으로 받을 name, address, email은 @NotNull로 빈값을 가지지 않음을 SelfValidating하고 있다.

 

빌더 패턴을 사용해 Command 객체 구현


RegisterMembershipCommand command = RegisterMembershipCommand.builder()
        .name(request.getName())
        .address(request.getAddress())
        .email(request.getEmail())
        .isValid(true)
        .isCorp(request.isCorp())
        .build();

 

다시 Controller로 돌아와서 request객체의 값을 get해서 빌더패턴을 통해 Command객체를 생성한다.

빌더 패턴은 선택적으로 필드값을 사용할 수 있고 순서에 영향을 받지 않는 등 유연한 장점을 갖는다.

 

생성자 주입을 통해 관계 설정


UseCase를 생성자 주입하여 만들어 둔 command객체를 통해 registerMembership메서드를 전달한다.

의존성 주입 방식으로 @RequiedArgsConsrtructor를 통해 생성자 주입을 사용하고 있다.

즉, Controller 클래스에서 UseCase 객체를 만들 수 있도록 한다.

@RequiredArgsConstructor
public class RegisterMembershipController {
    
    private final RegisterMembershipUseCase registerMembershipUseCase;
    
   	registerMembershipUseCase.registerMembership(command);

 

728x90

들어가며 


개발의 흐름과 Hexagonal 아키텍처 구조에서 각 클래스와 인터페이스가 어떻게 상호작용하는지를 확실하게 이해하기 위해 기록하려고 한다.

 

어댑터 개발


앞선 3편에서는 Membership 도메인 개발을 통해 회원정보에서 사용되는 값들을 정의했다.

이제 어댑터를 통해 외부와 통신을 어떻게 하는지 코드를 개발할 계획이다.

 

포트와 어댑터는 동시에 개발되며, 어댑터는 포트의 구현체이므로 먼저 개발한다.

 

인바운드 어댑터 층은 Controller와 Request 2가지로 구성된다. 

HTTP요청을 어떻게 처리할 것인가


@WebAdapter
@RestController
public class RegisterMembershipController {

    @PostMapping(path = "/membership/register")
    Membership registerMemeber() {
    }

 

스프링 프레임워크에서 제공하는 @PostMapping 어노테이션을 사용하여 /memberships/register 경로로 들어오는 HTTP POST 요청을 처리하기 위한 Controller이다.

registerMembership 메서드는 회원가입 요청을 외부로부터 받아 Service에 보내기 위해 HTTP POST 본문에 있는 데이터를 '어떤 객체'로 받아올 것인지 정해야 한다.

회원가입 registerMember를 위해 어떤 요청이 필요한가? 


@Data
@AllArgsConstructor
@NoArgsConstructor
public class RegisterMembershipRequest {
    private String name;
    private String address;
    private String email;
    private boolean isCorp;
}

 

회원가입 registerMember를 위한 객체 값은 name, address, email, isCorp를 받아오기로 한다.

@Data - Getter와 Setter 자동으로 생성

@AllArgsConstructor - 모든 필드의 생성자 생성

@NoArgsConstructor - 매개변수가 없는 기본 생성자 생성

Controller는 외부 요청을 받고 전달하는 클래스


Membership registerMemeber(@RequestBody RegisterMembershipRequest request) {

//        request -> UseCase(inbounded port) -> Service (biz logic)

 

@RequestBody 어노테이션은 HTTP 요청의 본문(body)에 담겨 있는 데이터를 메서드의 파라미터로 매핑/

request 객체의 구조로 전달되는 본문 데이터를 매핑해서 registerMember메서드 실행

 

request는 UseCase라는 Service가 biz logic을 구현할 수 있도록 연결하는 인터페이스로 전달될 것이다.

 

 

728x90

도메인 모델 정의


회원정보 도메인은 데이터 구조상 무결성과 불변성을 가지고 있어야 한다.

@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class Membership {

    private String membershipId;
    private String name;
    private String email;
    private String address;
    private boolean isValid;
    private boolean isCorp;

 

1. @AllArgsConstructor의 사용 :

@AllArgsConstructor(access = AccessLevel.PRIVATE)은 클래스의 필드를 초기화하는 생성자를 자동으로 생성하고, private로 접근 수준을 지정하여 클래스 외부에서 직접 생성자를 호출하지 못하도록 막는다.

 

    @Value
    public static class MembershipId {
        public MembershipId(String value) {
            this.membershipId = value;
        }
        String membershipId;
    }
    @Value
    public static class MembershipName {
        public MembershipName(String value) {
            this.nameValue = value;
        }
        String nameValue;
    }
    @Value
    public static class MembershipAddress {
        public MembershipAddress(String value) {
            this.addressValue = value;
        }
        String addressValue;
    }
    @Value
    public static class MembershipEmail {
        public MembershipEmail(String value) {
            this.emailValue = value;
        }
        String emailValue;
    }
    @Value
    public static class MembershipIsValid {
        public MembershipIsValid(boolean value) {
            this.isValidValue = value;
        }
        boolean isValidValue;
    }
    @Value
    public static class MembershipIsCorp {
        public MembershipIsCorp(boolean value) {
            this.isCorpValue = value;
        }
        boolean isCorpValue;
    }

2. @Value로 내부 클래스 생성 :

불변성을 갖춘 값을 생성. 외부에서 사용할 수 있는 객체를 생성하고 읽을 수 있도록 한다.

 

    public static Membership generateMember(
            MembershipId membershipId,
            MembershipName membershipName,
            MembershipAddress membershipAddress,
            MembershipEmail membershipEmail,
            MembershipIsValid membershipIsValid,
            MembershipIsCorp membershipIsCorp
    ) {
        return new Membership(
                membershipId.membershipId,
                membershipName.nameValue,
                membershipAddress.addressValue,
                membershipEmail.emailValue,
                membershipIsValid.isValidValue,
                membershipIsCorp.isCorpValue
        );

    }

3.  generate 메서드:

generateMembership메서드는 정적 메서드로 외부에서 호출이 가능하다. 이 메서드를 통해 입력된 값을 사용하여, 새로운 Membership 객체를 생성한다. 즉, 이 객체를 통해 신규 회원 정보를 저장할 수 있다.

728x90

멀티 모듈이란?


 

모듈은 하나의 독립적인 배포 단위를 의미하기 때문에, 각 모듈은 build 과정에서 jar파일을 배포한다.

따라서, 멀티 모듈은 하나의 프로젝트 내에서 여러 개의 모듈을 가짐으로써, 각 모듈간의 역할, 의존성을 분리하는 것이다.

 

각 모듈을 나누는 방법


 

 

멀티 모듈을 통해 모듈 간의 역할과 의존성을 분리하였다.

Hexaonal구조에서 격리된 모듈의 기준은 도메인(비즈니스 로직)을 통해 구분된다. 그렇기에 도메인을 중심으로 설계하는 DDD방식이 주로 사용된다.

 

DDD가 MSA설계에 사용되는 이유는 무엇인가?

  • 유비쿼터스 언어 : 복잡한 도메인을 정의하기 위해서는 SW개발자 외에도 도메인 전문가가 필요하다. 도메인 전문가와 SW개발자 모두 이해할 수 있는 공통 언어를 사용한다.
  • 애그리게잇: 객체들의 그룹을 단일 단위로 취급하여, 서비스의 일관성을 유지한다. 객체는 엔터티로 치환되며, 엔터티를 그룹화한 단위가 애그리게잇이고 이는 하나의 서비스와 일치한다.
  • Bounded Context: 도메인 간의 관계에서 각 도메인마다 배타적인 문제를 해결해야 한다. 따라서 Bounded context에 따라 하위 도메인 (Sub-domain)을 분리한다.
  • Context Mapping: Bounded Context간의 상호작용 및 통합을 정의한다.

 

 

 

728x90

모놀리식과 MSA 구조


 

MSA가 등장하기 전에는 모놀리식이라는 구조라고 할 것도 없이 모놀리식 구조로 개발해왔다고 한다.

단, 프로젝트의 규모가 커짐에 따라 하나의 어플리케이션을 도메인 성격에 따라 여러 개의 작은 서비스로 나누어 개별적으로 관리하는 MSA구조가 등장하게 되었다.

Hexagonal 구조 vs Layered 구조 


모놀리식이건 MSA건 각 서비스가 가져야할 구조는 어떤 것들이 있을까?

먼저 모놀리식 아키텍처가 개발의 기본이었던 것과 마찬가지로 Layered 구조(오른쪽)이 가장 기본적인 형태의 구조였다. Layered 구조는 데이터베이스를 중심으로 중심으로 설계했다. HTTP요청(User)를 DB에 저장하는 방법에 대해 고민하였고, 이를 가장 명료하게 구현하는 방식은  MVC패턴이 대표적인 디자인 패턴이 아닐까 싶다.

 

 

그러나 MSA가 등장한 이유와 같이, 요즘의 복잡해진 어플리케이션들은 비즈니스 로직이 수시로 변경되기도 하고 추가적인 종속성이 끊임없이 발생한다. 이러한 과정에서 Layered 구조는 종속성 혼란을 겪기도 한다.

 

 

이에 대한 대비책은 Hexagonal 구조를 채택하는 것이다. Hexagonal 구조는 비즈니스 로직을 중심으로 외부와 내부를 격리한다. 즉, 내부에 비즈니스 로직을 수행하는 계층을 두고 Port를 통해 외부와 통신하며 동작한다.

 

 

결과적으로 Hexagonal 구조를 사용하면 쉽게 수정할 수 있고, 경계를 명확하게 구분지어 격리된다.

[이 부분은 도메인 주도 설계인 DDD의 내용과도 밀접한 관계를 가지고 있으므로, 다음 편에서 조금 더 설명한다.]

 

MSA구조를 선택하면서 중요시해야 할 점들은?


모든 구조는 시스템 성능을 위해 존재한다. 그리고 시스템 성능을 평가하는 지표는 Thoughput과 Latency로 설명할 수 있다.

 

  • Thoughput : 처리량을 측정한다. 단위 시간당 얼마나 많은 작업을 처리할 수 있는지를 나타낸다.
  • Latency : 지연시간을 의미한다. 특정 작업을 완료하기 위해 걸리는 시간을 나타낸다.

 

MSA에서는 각 서비스 별로 관리하기 때문에 확장이 쉽다. 따라서 Thoughput관점에서는 장점을 가진다고 할 수 있다.

반면, Latency관점에서는 서비스 간 통신, DB 액세스 같은 외부 통신에서 기존의 모놀리식 구조에 비해 약점을 가지고 있다.

 

그러므로 아키텍처 설계 과정에서 MSA를 선택하는 것이 무한정 좋은 것은 아니며, Thoughput과 Latency를 최적화하는 지점을 찾는 것이 중요하다.

 

 

+ Recent posts