DDD Repo1. Account

8694 단어 subDDDprojectDDD

서론

최근에 코딩 감이 떨어지는거 같아서 작은 프로젝트를 시작했다.
현재 기본적으로 Account 가입과 로그인 정도만 구현을 해놓은 상태이며
컨텐츠의 경우 좀 더 참조가 필요한 부분이 있어서 차후에 업로드할 예정이다.

처음에 DDD를 개념상으로 들었을때는 OOP와 그렇게 큰 차이를 느끼지 못했다.
아직까지도 엄청난 차이는 모르겠지만 딱 느끼는 차이점으로는
객체를 레이어 단계로 올리면서 래핑하는 느낌이 있다. ( 뭔가 한번 더 묶이는 느낌 )

각 모듈간의 응집성을 높이기 위해 고안된 여러 패턴들을 볼수 있는데
이 부분에 대해서 어떤 느낌인지 파악하기 어려워서 직접 서브 프로젝트로 접근해보았다.
하단의 코드들은 실제로 작성해본 코드이고 여러 문헌들과 내 뇌피셜을 합쳐서 만든 코드이기 때문에 허점이나 틀린 부분이 반드시 존재한다.

참고 문헌 - 도메인 주도 설계로 시작하는 마이크로서비스 개발


Package 구조

패키지 구조는 기본적으로 도메인을 기점으로 분리.
컨트롤러의 경우 따로 dto와 함께 따로 패키지 분리를 하였다 ( 참고 문헌 )
여기서 제일 고민한 부분은 service를 과연 인터페이스로 만들어야하는지에 대해서이다.

결론을 말하자면 SOLID 원칙중 OCP가 생각나서 인터페이스를 사용하기로 했다.


Entity 설계

기본적으로 Account를 aggregate 로 설정하여 Account가 유저 프로필과
연결된 인증에 대해서 엔티티를 가지는 모습으로 설계하였다.

객체를 만드는 로직의 경우 모든 엔티티가 static으로 가지고있음.
프로필의 경우 인증과 연계되어있는 부분이 없기에 밑에선 제외하였다.
전체 엔티티 코드 중에 일부분은 밑에 Logic 설명시 다시 나오니 참고.

Account

  /**
  *   @Author : Youn
  *   @Summary : Account
  *   @Memo : AbstractAggregateRoot
  **/
  @Entity
  @Getter
  @Table(name="account")
  @Builder(access = AccessLevel.PRIVATE)
  @AllArgsConstructor
  @NoArgsConstructor(access = AccessLevel.PROTECTED)
  @Cache(usage= CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
  public class Account extends AbstractAggregateRoot<Account> {

      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;

      @Column(name = "email", unique = true, nullable = false)
      private String email;

      @Column(name = "nick_name")
      private String nickName;

      @Column(name = "created_at")
      private LocalDateTime createdAt;

      @Column(name = "updated_at")
      private LocalDateTime updatedAt;

      @Column(name = "exited_at")
      private LocalDateTime exitedAt;

      @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, optional = false, orphanRemoval = true)
      @JoinColumn(name = "user_profile_id")
      @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
      private UserProfile userProfile;

      @OneToMany(mappedBy = "account", cascade = CascadeType.ALL, orphanRemoval = true)
      @Builder.Default
      @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
      private Set<LinkedAuth> linkedAuthSet = new HashSet<>();

      public static Account createAccount(AccountCreateForm createForm) {
          return Account.builder()
                  .email(createForm.getEmail())
                  .nickName(createForm.getName())
                  .createdAt(LocalDateTime.now())
                  .updatedAt(LocalDateTime.now())
                  .exitedAt(null)
                  .build();
      }

      // SignUp User 
      public static Account signUp(SignUpData data) {
          Account account = createAccount(data.getAccountCreateForm());

          account.setLinkedAuthSet(LinkedAuth.createLinkedAuth(data.getLinkedAuthCreateForm()));
          account.setUserProfile(UserProfile.createUserProfile(data.getUserProfileCreateForm()));
          return account;
      }

      // Login User 
      public void login(AccountType type, String password) {
          // Find Target Auth
          LinkedAuth targetAuth = linkedAuthSet.stream()
                  .filter(auth -> auth.getAccountType() == type)
                  .findFirst()
                  .orElseThrow(() -> new CommonException(ErrorCode.LINKED_AUTH_NOT_FOUND));

          // Check Password
          targetAuth.checkPassword(password);

          UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                  new UserAccount(this, targetAuth),
                  password,
                  List.of(new SimpleGrantedAuthority("ROLE_USER")));
          SecurityContextHolder.getContext().setAuthentication(authentication);
      }

      public String getJwtToken() {
          return JwtUtil.encodeJwt(this.email);
      }

      public boolean isExited() {
          return Objects.nonNull(this.exitedAt);
      }

      private void setLinkedAuthSet(LinkedAuth linkedAuth) {
          linkedAuth.setAccount(this);
          this.linkedAuthSet.add(linkedAuth);
      }

      private void setUserProfile(UserProfile userProfile) {
          this.userProfile = userProfile;
      }
  }

LinkedAuth

  @Entity
  @Getter
  @Table(name = "linked_auth")
  @Builder(access = AccessLevel.PRIVATE)
  @AllArgsConstructor
  @NoArgsConstructor(access = AccessLevel.PROTECTED)
  @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
  public class LinkedAuth {

      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;

      @Enumerated(EnumType.STRING)
      @Column(name = "account_type")
      private AccountType accountType;

      @Column(name = "password")
      private String password;

      @Column(name = "confirmed")
      private LocalDateTime confirmedAt;

      @ManyToOne(fetch = FetchType.LAZY)
      @JoinColumn(name = "account_id")
      private Account account;

      public static LinkedAuth createLinkedAuth(LinkedAuthCreateForm createForm) {
          return LinkedAuth.builder()
                  .accountType(createForm.getAccountType())
                  .password(PasswordUtil.encode(createForm.getPassword()))
                  .confirmedAt(null)
                  .build();
      }

      public void checkPassword(String aPassword) {
          if (!PasswordUtil.matches(aPassword, this.password)) throw new CommonException(ErrorCode.LOGIN_PROCESS_PASSWORD_NOTMATCH);
      }

      public void setAccount(Account account) {
          this.account = account;
      }
  }

Logic

제작 당시 도메인 레이어에 맞게 최대한 기능에 중점을 맞춰서 작성하였다.
현재 로직은 가입과 로그인 절차만 만들어놓아서 두 로직만 확인이 가능하다.

SignUp

유저 가입절차로 기본 값으로는 프로필값과 연결된 인증값을 가진다
기본 가입의 경우 패스워드를 사용해서 인증을 설정.

위에 코드중에 Service 로직이 없지만 없어도 상관이 없다 .
Service에서는 Assertion을 이용해서 기존 유저가 존재하는지 확인을 하고 바로 Signup메서드를 호출하게 된다.
호출하고 나온 값은 영속시켜서 로직을 마무리한다.

    // SignUp User 
    public static Account signUp(SignUpData data) {
        Account account = createAccount(data.getAccountCreateForm());

        account.setLinkedAuthSet(LinkedAuth.createLinkedAuth(data.getLinkedAuthCreateForm()));
        account.setUserProfile(UserProfile.createUserProfile(data.getUserProfileCreateForm()));
        return account;
    }

Login

로그인이 무엇인지 생각을 해보는 시간이었다.
한 아이디에 할당된 고유 인증을 진행한다는 생각을 가지고 도메인에 녹여보았다.

로그인도 Signup과 마찬가지로 Service 로직에는 Assertion을 통한 유저 유무와
Email을 통해 Account를 가지고 오는 로직이 선행이 된다.

	  // AggregateEntity - Account
      // Login User 
      public void login(AccountType type, String password) {
          // Find Target Auth
          LinkedAuth targetAuth = linkedAuthSet.stream()
                  .filter(auth -> auth.getAccountType() == type)
                  .findFirst()
                  .orElseThrow(() -> new CommonException(ErrorCode.LINKED_AUTH_NOT_FOUND));

          // Check Password
          targetAuth.checkPassword(password);

          UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                  new UserAccount(this, targetAuth),
                  password,
                  List.of(new SimpleGrantedAuthority("ROLE_USER")));
          SecurityContextHolder.getContext().setAuthentication(authentication);
      }
      // Entity - LinkedAuth
      public void checkPassword(String aPassword) {
          if (!PasswordUtil.matches(aPassword, this.password)) throw new CommonException(ErrorCode.LOGIN_PROCESS_PASSWORD_NOTMATCH);
      }

자세히 살펴보자면 다음과 같은 Flow를 가진다.

1. Find Auth - 타입에 맞는 인증을 찾는다
2. 인증과정을 찾는 Auth에 위임시켜 처리를한다. 
3. 인증이 된다면 Security에 저장시켜서 로그인을 마무리한다. 

마무리

아직 불안정한 부분이 많다.
그리고 구조적으로 좀더 보완될 부분이 있다고 생각이 든다.

좋은 웹페이지 즐겨찾기