티스토리 뷰

반응형
자세한 내용은 Github를 참고해주시기 바랍니다.

 

자 이제 엔티티를 개발할 차례다. 사전에 먼저 용어 부터 정리하도록 하자. 

 

도메인 모델패턴 

DDD에서 정의한 도메인 모델을 먼저 간략하게 보면 다음과 같습니다. 

전 글인 서비스 로직 구현에서 도메인 계층에 도메인 정보와 비즈니스 규칙을 정의한다고 했는데 그렇다 이렇게 정의 된 모델을 도메인 모델이라고 한다. 그리고 이러한 개발 패턴을 도메인 모델 패턴이라고 하는데 이러한 패턴은 객체 지향 언어를 이용해 개념 모델에 가깝게 구현할 수 있고 도메인 개념을 모델을 통해 한눈에 이해할 수 있게 할 수 있다는 장점이 있다. 

 

JPA 값 타입

JPA에서는 내장 타입을 이용해 식별관계 테이블을 정의할 수 있습니다. 이것을 이용해서 읽기 전용인 VO객체를 만들어 도메인을 더 잘게 쪼갤 수 있었는데요 우선 이건 코드로 보겠습니다. 

public class Member extends BaseTimeEntity {

    // ... //

    @Embedded
    private Password password;

다음과 같이 @Embedded 어노테이션을 이용하여 선언할 수 있습니다. 또한 내부 구현부를 살펴보면 

@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Password {
    @NotBlank(message = "필수 값입니다. - password")
    @Column(nullable = false, length = 120)
    private String password;

    public static Password from(final String password) {
        return new Password(password);
    }

    public static Password encode(final String rawPassword, final PasswordEncoder encoder) {
        validatePassword(rawPassword);
        return new Password(encoder.encode(rawPassword));
    }

    private static void validatePassword(final String rawPassword) {
        if (Objects.isNull(rawPassword) || rawPassword.isBlank()) {
            throw new PasswordNullException(ErrorCode.PASSWORD_NULL_ERROR);
        }
    }

    @JsonValue
    public String password() {
        return password;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Password password = (Password) o;
        return Objects.equals(password(), password.password());
    }

    @Override
    public int hashCode() {
        return Objects.hash(password());
    }
}

단순 값 타입이지만 내부에 많은 비즈니스 로직이 구현되어있습니다. 생성 메소드인 from부터 인코딩하는 것과 검증하는 validatePassword 메소드도 함께 정의되어 있습니다. 객체지향을 극도로 끌어올린다면 당연히 Password에 대한 책임은 Password 도메인 모델인 Password객체에 맡겨야하니 이런 식의 정의가 가능해집니다. 

또한 Json으로 데이터를 직렬화 또는 역직렬화할 때에는 String Type으로 해줘야하기 때문에 @JsonValue로 String Type을 이용해 반환하게 설계하였습니다. 또한 validation도 내부에서 일어나고 있습니다. 

 

Equals와 Hashcode

Object에서의 equals는 서로 두 개의 객체가 동일한지  검사합니다. 그렇기 때문에 equals를 재정의하여 서로의 객체가 같음에 대한 정의를 다시 해줬다면 두 객체는 동일한 해시코드를 가져야하기 때문에 hashcode도 재정의를하여야 합니다. 만약 이를 지키지 않을 시 HashMap이나 HashSet같은 hash값을 이용하여 값에 대한 중복을 체크하고 있는데 이 때 제대로된 equals랑 hashcode랑 다르게 동작하기 때문에 동등성과 중복체크가 다른 방식으로 이루어지는 아이러니한 상황이 생길 수 있습니다. 객체에 많은 프로퍼티가 존재할 경우 이 모든 값을 가지고 hashcode를 생성하는데 이 경우 불필요하게 많은 hash 함수가 생기기 때문에 충돌나기 쉽습니다. 그렇기 때문에 특정 객체마다의 고유한 값을 갖고 hashcode를 생성하면 예방할 수 있습니다.

 

https://golf-dev.tistory.com/13

 

HashMap은 어떤 구조일까?

우리는 종종 Key - Value 구조의 HashMap을 사용한다. HashMap은 Key 중복이 없어 중복을 거르고 Key 값으로 빠르게 값을 찾아오기에 조회하는데에 이 점이 있다. 필자는 본 내용을 Map에 대한 사전 지식에

golf-dev.tistory.com

자세한건 이 글을 참고해주시기 바랍니다.

 

그러면 이제 사전 준비가 끝났습니다. 코드를 보도록 하겠습니다. 

@Getter
@Entity
@DynamicUpdate
@DynamicInsert
@Where(clause = "activated = true")
@Table(indexes = @Index(name = "i_email", columnList = "email"))
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends BaseTimeEntity {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id", nullable = false, updatable = false)
    private Long id;

    @Embedded
    private Email email; 

    @Embedded
    private Password password;

    @Embedded
    private Name name;

    @Embedded
    private Nickname nickname;

    @Column(nullable = false)
    private LocalDate birth;

    @Enumerated(value = EnumType.STRING)
    @Column(nullable = false)
    private RoleType role;

    @Column(name = "activated")
    private Boolean activated = true;

    @Embedded
    private MemberCount memberCount;

    @Builder
    private Member(Long id, Email email, Password password, Name name, Nickname nickname,
                   LocalDate birth, RoleType role) {
        this.id = id;
        this.email = email;
        this.password = password;
        this.name = name;
        this.nickname = nickname;
        this.birth = birth;
        this.role = role;
        this.activated = true;
        this.memberCount = new MemberCount();
    }

    // == 비즈니스 로직 == //
    public Member encode(final PasswordEncoder encoder) {
        password = Password.encode(password.password(), encoder);
        return this;
    }

    public Member update(final Member member, final PasswordEncoder encoder) {
        this.password = Password.encode(member.getPassword().password(), encoder);
        this.nickname = member.getNickname();
        this.name = member.getName();
        return this;
    }

    public void delete() {
        activated = false;
        recordDeleteTime();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Member member = (Member) o;
        return Objects.equals(id, member.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

Id AutoIncreament 전략은 MySQL DB를 사용할 것이기 때문에 IDENTITY를 사용하였습니다. 또한 프로퍼티들에 값 타입을 통해 선언하여 값을 명시하였고 삭제에 대한 flag 값인 activated를 선언하였습니다. 

또한 위에서 미리 말씀드린 도메인 모델 패턴을 적용하여 도메인 모델 내부에 업데이트와 삭제에 대한 로직을 구현하였습니다. 이렇게 구현을 하였을 때 장점은 수정이나 삭제 생성에 대한 요구사항이 변경이 되더라도 서비스 로직은 전혀 영향받지 않고 도메인 모델 구현부만 수정해주면 되니 좀 더 유지보수에 유리한 작업이 됩니다. 

 

값 타입에 대한건 Password를 제외하곤 거의 값만 들고 있는 VO객체에 가깝습니다. 자세한건 Github를 참고해주시기 바랍니다.

 

Entity구현부 까지 이제 회원에 대한 기능 구현은 끝이 났습니다. 다음 시간에는 게시판 기능으로 돌아오도록 하겠습니다. 읽어주셔서 감사합니다~!

 

 

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
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 31
글 보관함