초기 과업/BackEnd
[미소 클론코딩] 회원, 비회원 도메인 구현
알 수 없는 사용자
2023. 8. 21. 00:31
728x90
반응형
안녕하세요. 기깔나는 사람들에서 백엔드를 맡고있는 선호입니다.
이번 포스팅에서는 청소업체 미소를 클론코딩을 진행하면서 구현한 회원과 협력업체 도메인 객체를 어떻게 설계했는지 살펴보겠습니다.
BaseTimeEntity
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public abstract class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime lastModifiedAt;
}
- 먼저 모든 엔티티에서 사용할 생성, 수정 시간을 기록하는
BaseTimeEntity
입니다. - JPA Auditing 기능을 사용하여 엔티티 생성, 수정시 자동으로 값이 생성/변경 되어 일반적으로 위와 같이 많이 쓰입니다.
@EnableJpaAuditing
@SpringBootApplication
public class MisoApplication {
public static void main(String[] args) {
SpringApplication.run(MisoApplication.class, args);
}
}
- JPA Auditing 을 사용하기 위해서는 위와 같이 @SpringBootApplication 이 있는 애플리케이션 클래스에 @EnableJpaAuditing 어노테이션을 붙여주면 됩니다.
Member
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Member extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
@Embedded
private AuthInfo authInfo;
@Embedded
private PersonalInfo personalInfo;
@Enumerated(value = EnumType.STRING)
private BlacklistStatus blacklistStatus;
@Builder
private Member(AuthInfo authInfo, PersonalInfo personalInfo, BlacklistStatus blacklistStatus) {
this.authInfo = authInfo;
this.personalInfo = personalInfo;
this.blacklistStatus = blacklistStatus;
}
public static Member defaultOf(AuthInfo authInfo, PersonalInfo personalInfo) {
return Member.builder()
.authInfo(authInfo)
.personalInfo(personalInfo)
.blacklistStatus(BlacklistStatus.DISABLED)
.build();
}
public void changePassword(String password) {
this.authInfo = authInfo.withNewPassword(password);
}
}
- 회원 도메인 객체인 엔티티는 이렇게 작성해봤습니다. 여러 엔티티에서 사용될 수 있는 회원의 기본 정보 등은 값 타입으로 설계했습니다.
- 생성자가 아닌 팩토리 메서드로 객체를 생성하는 이유는 메서드명으로 의도를 드러낼 수 있고, 원하는 파라미터를 선택적으로 받을 수 있기 때문입니다.
- 위 코드에서 defaultOf() 로 명명한 것은, 회원 객체가 생성될 때 기본적으로 블랙리스트 상태를 지정할 필요가 있기 때문입니다.
- 생성자에서 기본적으로 지정하면 되지 않을까? 라고 생각할 수 있지만, 생성자는 일반적으로 인자로 넘어온 값을 필드에 그대로 적용되는 것을 기대하기에, 팩토리 메서드에서 ‘기본적으로 Member 가 생성될 때 블랙리스트가 아니다.’ 라는 비즈니스 로직을 드러내고 싶었습니다.
- 위 코드에서 defaultOf() 로 명명한 것은, 회원 객체가 생성될 때 기본적으로 블랙리스트 상태를 지정할 필요가 있기 때문입니다.
- 빌더를 사용한 이유는, 어떤 필드를 어떤 순서로 넣어야하는지 고민하지 않아도 되기 때문입니다. 또한 어떻게 필드를 할당해야하는지 메서드를 통해서 하다보니 실수할 확률도 줄여준다고 생각합니다.
- 물론 코드양은 좀 늘지만, 코드가 느는 것에 비해 장점이 더 뛰어나다고 생각해 트레이드 오프를 고려해서 선택했습니다.
BlacklistStatus
@RequiredArgsConstructor
public enum BlacklistStatus {
ENABLED("활성화"),
DISABLED("비활성화");
private final String description;
}
- 회원/비회원의 블랙리스트 상태는 Enum 을 사용하여 표현했습니다.
- boolean flag 를 사용하지 않은 것은, 이후에 상태가 추가 될 때 flag 로는 전부 표현할 수 없기 때문입니다.
- 반면에 Enum으로 관리하면 필드만 추가하면 되므로, 추가적으로 드는 리소스가 적습니다. 즉, 유지보수를 고려하다 보니 이렇게 설계하게 되었습니다.
PersonalInfo
@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class PersonalInfo {
@Column(nullable = false)
private String name;
private String phoneNumber;
private String email;
@Embedded
private Signature signature;
@Builder
private PersonalInfo(String name, String phoneNumber, String email, Signature signature) {
this.name = name;
this.phoneNumber = phoneNumber;
this.email = email;
this.signature = signature;
}
public static PersonalInfo of(String name, String phoneNumber, String email, Signature signature) {
return PersonalInfo.builder()
.name(name)
.phoneNumber(phoneNumber)
.email(email)
.signature(signature)
.build();
}
}
- 값 타입으로 name, phone, email, signature 를 한 번에 관리하도록 했습니다.
- 연관성이 높은 필드들이고, 비회원 도메인에서도 사용할 정보들이라 재사용성을 위해 값타입으로 설계했습니다.
- 동일한 personalInfo 레퍼런스를 사용할 때 생길 수 있는 ‘의도치 않은 멤버변수 변경’ 문제를 방지하기 위해 변경사항이 생길 경우 새로운 객체를 만들어 리턴하도록 구현했습니다.
- ‘의도치 않은 멤버변수 변경’이란?
- A와 B가 같은 personalInfo 레퍼런스를 사용한다고 가정해봅시다.
- 이 때 B의 멤버변수를 변경하게 되면 같은 레퍼런스를 갖는 A 역시 멤버변수가 바뀌는 형태가 됩니다. 물론 같은 레퍼런스를 갖으니까 당연한 결과입니다.
- 이 같은 문제를 미연에 방지하기 위해 값타입을 불변으로 설계하는 과정에서 값이 변경되면 새로운 객체를 만들어 리턴해주는 구현방식이 필요합니다.
- ‘의도치 않은 멤버변수 변경’이란?
Signature
@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Signature {
@Column(name = "signature_image_url")
private String imageUrl;
@Builder
private Signature(String uploadUrl) {
this.imageUrl = uploadUrl;
}
public static Signature from(String uploadUrl) {
return Signature.builder()
.uploadUrl(uploadUrl)
.build();
}
public Signature withNewUploadUrl(String uploadUrl) {
return from(uploadUrl);
}
}
- Signature 역시 여러 엔티티에서 사용가능하고, 엔티티로 관리될 필요가 없으므로 값 타입으로 구현했습니다.
- @Column(name = "signature_image_url") 처럼 컬럼명을 별도로 지정해준 것은 값타입으로 사용될 때 서명 이미지 라는 것을 명시적으로 드러내기 위함입니다.
- 그렇게 하지 않으면 어떤 이미지 URL 인지 파악하기 어려워 가독성을 저해합니다.
Guest
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Guest extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "guest_id")
private Long id;
@Embedded
private PersonalInfo personalInfo;
@Enumerated(value = EnumType.STRING)
private BlacklistStatus blacklistStatus;
@Builder
private Guest(PersonalInfo personalInfo, BlacklistStatus blacklistStatus) {
this.personalInfo = personalInfo;
this.blacklistStatus = blacklistStatus;
}
public Guest defaultOf(PersonalInfo personalInfo) {
return Guest.builder()
.personalInfo(personalInfo)
.blacklistStatus(BlacklistStatus.DISABLED)
.build();
}
}
- 비회원은 회원과 다른 도메인 객체로 구성했습니다.
- 비즈니스 정책 상 행위가 명확히 구분되며, 비회원은 인증 정보를 갖지 않기 때문에 회원과 같은 엔티티를 사용하게 된다면 불필요한 필드를 가지게 되기 때문입니다.
- 회원과 마찬가지로 팩토리 메서드를 이용하여 처음 비회원 등록 시 블랙리스트가 아닌 상태로 등록하게 두었습니다.
회고
기초적이지만 중요한 도메인 설계시 고려사항들을 고민해봤습니다. 특히 불변, 도메인의 행위에 대해서 고민해봤는데 더 좋은 설계를 할 수 없었는지 구현하며 계속 고민해봐야겠습니다.
728x90
반응형