728x90

정규화와 반정규화

데이터 베이스의 가장 큰 기능인 CRUD 중에서도 읽기와 쓰기의 성능을 기준으로 정규화와 반정규화를 통해 엔터티를 구성한다.

정규화(쓰기) 반정규화(읽기)
중복 제거 중복 허용
데이터 정합성 유지가 쉬움 데이터 정합성 유지가 어려움
읽기 시 참조 필요 참조 없이 읽기

 


 

728x90

MySQL이란?

 

MySQL은 전세계에서 가장 많이 활용되고 있는 RDBMS다.

RDBMS는 관계형 데이터베이스를 의미하는데, 이는 데이터가 하나 이상의 열과 행의 테이블에 저장되어 서로 간의 사전 정의된 관계로 데이터를 구성하고 있다.

3 Tier Architecture -> N Tier Architecture로 변화

간단한 어플리케이션은 가장 전통적으로 3계층 아키텍처로 구성되어 왔다. 이 구조는 Client의 요청을 Server를 통해 Database와 통신하여 처리했으며, 반드시 Server를 거쳐야 하고 Client와 Database가 직접 소통하지 않는다.

Database서버는 관계형 데이터베이스(MySQL, Postgre, MariaDB 등) 또는 NoSQL 데이터베이스 서버 (MongoDB, Cassandra 등)로 구성되어 있다.

 

 

그러나, 어플리케이션이 복잡해지면서 캐싱, 메시징, 데이터 스토리지 등 다양한 기능이 필요해졌고, N계층 아키텍처로 변화되어 왔다.

 

N계층 구조로 변화해온 이유는?

 

어플리케이션이 복잡해지고, 다양한 기능이 필요한 이유는 많은 양의 데이터를 처리해야 하기 때문이다.

즉, 대용량 시스템을 구축하고 트래픽을 처리할 수 있도록 설계하는 것이 오늘날의 아키텍처를 선택하는 중요한 기준이다.

그리고 대용량 데이터를 처리하는 방식으로 Scale UP (성능을 늘리기)와 Scale OUT (서버 늘리기)가 있다.

 

Cache와 Load Balancer

 

Scale UP은 무한정으로 늘릴 수 있는 것이 아니고, Scale UP과 OUT모두 비용적인 측면을 고려해서 설계해야 한다. 이러한 관점에서 각 어플리케이션은 적정한 갯수의 서버를 가지고 있을 것이며, 다시 이 서버들은 Client요청을 받아 데이터 베이스와 통신한다.

 

Load Balancer

로드 밸런싱은 대규모 트래픽을 받기 위해 구축된 여러 대의 서버에게 트래픽을 분산하여 전달하는 역할을 한다.

 

Cache

캐시는 DB에 접근하여 데이터를 가져오는 시간을 절약하기 위해 임시적으로 데이터 값을 가지고 있는 저장소다. 임시 저장소이기 때문에 저장하고 있는 데이터 양과 시간적 제약 조건을 가진다. 따라서, 조건 범위를 적절하게 책정하는 것이 캐싱 기능을 활용하는 핵심이다.

 

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 객체를 생성한다. 즉, 이 객체를 통해 신규 회원 정보를 저장할 수 있다.

+ Recent posts