[Project] id 타입을 객체로 관리하는 방법 - DDD
Movlit 프로젝트에 대한 설명입니다.
서론Permalink
NoSQL의 경우 id를 ObjectId로 관리하는 경우를 많이 봤을 것이다. 하지만, RDB를 사용한다면 보통 id는 Long으로 두고 @GeneratedValue
로 자동으로 증가시키게끔 하였을 것이다.
현재 프로젝트의 채팅 메시지 관련 부분을 제외하고는 전부 MySQL을 사용하고 있지만, 프로젝트 초반 설계부터 id는 Long 값을 그 Entity의 id 객체로 관리하게끔 하였다.
프로젝트 구조Permalink
- Id 객체를 관리하는
ids
폴더에 이 id 객체들을 저장하고 있다.
구현Permalink
package movlit.be.common.util.ids;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@MappedSuperclass
public abstract class BaseId implements Serializable {
private static final long serialVersionUID = 536871008L;
@Column(name = "id")
@JsonProperty("id")
@JsonValue
protected String value;
protected BaseId(String value) {
this.value = value;
}
@Override
public int hashCode() {
return Objects.hash(getValue());
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof BaseId)) {
return false;
}
BaseId baseId = (BaseId) o;
return Objects.equals(getValue(), baseId.getValue());
}
}
package movlit.be.common.util.ids;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Embeddable
@EqualsAndHashCode(callSuper = true)
public class MovieCommentId extends BaseId {
public MovieCommentId(String value) {
super(value);
}
}
BaseId
클래스를 상속하여 만든MovieComment
Entity의 id 객체이다.- id 객체를 생성할 때마다 클래스 이름만 다르게 생성을 해주면 된다.
Id 생성Permalink
package movlit.be.common.util;
public class IdGenerator {
private static final Random random = new Random();
// 각 섹션별 길이 설정
private static final int TIMESTAMP_LENGTH = 4; // 타임스탬프 길이: 4바이트
private static final int MACHINE_ID_LENGTH = 3; // 머신 식별자 길이: 3바이트
private static final int PROCESS_ID_LENGTH = 2; // 프로세스 식별자 길이: 2바이트
private static final int COUNTER_LENGTH = 3; // 카운터 길이: 3바이트
// 각 섹션별 시작 오프셋 설정
private static final int TIMESTAMP_OFFSET = 0;
private static final int MACHINE_ID_OFFSET = TIMESTAMP_OFFSET + TIMESTAMP_LENGTH;
private static final int PROCESS_ID_OFFSET = MACHINE_ID_OFFSET + MACHINE_ID_LENGTH;
private static final int COUNTER_OFFSET = PROCESS_ID_OFFSET + PROCESS_ID_LENGTH;
private static final int TOTAL_LENGTH = TIMESTAMP_LENGTH + MACHINE_ID_LENGTH + PROCESS_ID_LENGTH + COUNTER_LENGTH;
// 머신과 프로세스의 식별자를 생성하며, 카운터는 무작위 값으로 초기화
private static final int MACHINE_IDENTIFIER = createMachineIdentifier();
private static final int PROCESS_IDENTIFIER = createProcessIdentifier();
private static final AtomicInteger COUNTER = new AtomicInteger(random.nextInt(Integer.MAX_VALUE));
private static final int TIMESTAMP_MASK = 0xffffffff; // 타임스탬프 마스크
// 기본 생성자 - 이 유틸리티 클래스는 인스턴스화되어서는 안됨
private IdGenerator() {
throw new IllegalStateException(UTILITY_CLASS);
}
// 현재 시간을 기반으로 ID 생성
public static String generate() {
return generate(System.currentTimeMillis());
}
// 로컬 머신의 MAC 주소를 기반으로 머신 식별자를 생성
private static int createMachineIdentifier() {
try {
InetAddress ip = InetAddress.getLocalHost();
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
byte[] mac = network.getHardwareAddress();
return java.util.Arrays.hashCode(mac) & 0xffffff;
} catch (Exception e) {
// 예외 발생 시 무작위 값을 반환
return random.nextInt(0xffffff);
}
}
// 현재 실행중인 JVM 프로세스의 ID를 기반으로 프로세스 식별자를 생성
private static int createProcessIdentifier() {
try {
String processName = ManagementFactory.getRuntimeMXBean().getName();
return Integer.parseInt(processName.split("@")[0]) & 0xffff;
} catch (Exception e) {
// 예외 발생 시 무작위 값을 반환
return random.nextInt(0xffff);
}
}
// 주어진 정보를 기반으로 ID 생성
private static String generate(long timestamp) {
return generate(timestamp, MACHINE_IDENTIFIER, PROCESS_IDENTIFIER, COUNTER.getAndIncrement());
}
// 타임스탬프, 머신 식별자, 프로세스 식별자, 카운터를 이용해 ID 생성
private static String generate(long timestamp, int machineIdentifier, int processIdentifier, int counter) {
byte[] bytes = new byte[TOTAL_LENGTH];
// 각 섹션 인코딩
int time = (int) (timestamp & TIMESTAMP_MASK);
bytes[TIMESTAMP_OFFSET + 3] = (byte) (time);
bytes[TIMESTAMP_OFFSET + 2] = (byte) (time >>> 8);
bytes[TIMESTAMP_OFFSET + 1] = (byte) (time >>> 16);
bytes[TIMESTAMP_OFFSET] = (byte) (time >>> 24);
bytes[MACHINE_ID_OFFSET + 2] = (byte) machineIdentifier;
bytes[MACHINE_ID_OFFSET + 1] = (byte) (machineIdentifier >>> 8);
bytes[MACHINE_ID_OFFSET] = (byte) (machineIdentifier >>> 16);
bytes[PROCESS_ID_OFFSET + 1] = (byte) processIdentifier;
bytes[PROCESS_ID_OFFSET] = (byte) (processIdentifier >>> 8);
bytes[COUNTER_OFFSET + 2] = (byte) counter;
bytes[COUNTER_OFFSET + 1] = (byte) (counter >>> 8);
bytes[COUNTER_OFFSET] = (byte) (counter >>> 16);
// 바이트 배열을 16진수 문자열로 변환
return hexString(bytes);
}
// 바이트 배열을 16진수 문자열로 변환하는 유틸리티
private static String hexString(byte[] bytes) {
StringBuilder builder = new StringBuilder();
for (byte b : bytes) {
builder.append(String.format("%02x", b));
}
return builder.toString();
}
}
package movlit.be.common.util;
public class IdFactory {
private IdFactory() {
throw new IllegalStateException("Utility class");
}
public static MemberId createMemberId(String id) {
return createId(MemberId.class, id);
}
public static MemberId createMemberId() {
return createId(MemberId.class);
}
public static BookId createBookId(String id) {
return createId(BookId.class, id);
}
public static BookId createBookId() {
return createId(BookId.class);
}
public static MovieCommentId createMovieCommentId(String id) {
return createId(MovieCommentId.class, id);
}
public static MovieCommentId createMovieCommentId() {
return createId(MovieCommentId.class);
}
public static MovieCrewId createMovieCrewId(String id) {
return createId(MovieCrewId.class, id);
}
public static MovieCrewId createMovieCrewId() {
return createId(MovieCrewId.class);
}
public static MovieHeartId createMovieHeartId(String id) {
return createId(MovieHeartId.class, id);
}
public static MovieHeartId createMovieHeartId() {
return createId(MovieHeartId.class);
}
public static MovieHeartCountId createMovieHeartCountId(String id) {
return createId(MovieHeartCountId.class, id);
}
public static MovieHeartCountId createMovieHeartCountId() {
return createId(MovieHeartCountId.class);
}
public static MovieCommentLikeId createMovieCommentLikeId(String id) {
return createId(MovieCommentLikeId.class, id);
}
public static MovieCommentLikeId createMovieCommentLikeId() {
return createId(MovieCommentLikeId.class);
}
public static MovieCommentLikeCountId createMovieCommentLikeCountId(String id) {
return createId(MovieCommentLikeCountId.class, id);
}
public static MovieCommentLikeCountId createMovieCommentLikeCountId() {
return createId(MovieCommentLikeCountId.class);
}
public static ImageId createImageId(String id) {
return createId(ImageId.class, id);
}
public static ImageId createImageId() {
return createId(ImageId.class);
}
public static MemberGenreId createMemberGenreId(String id) {
return createId(MemberGenreId.class, id);
}
public static MemberGenreId createMemberGenreId() {
return createId(MemberGenreId.class);
}
private static <T extends BaseId> T createId(Class<T> idClass, String id) {
try {
Constructor<T> constructor = idClass.getConstructor(String.class);
return constructor.newInstance(id);
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException |
InvocationTargetException e) {
throw new IllegalArgumentException("Id 클래스 생성에 실패했습니다.");
}
}
private static <T extends BaseId> T createId(Class<T> idClass) {
try {
Constructor<T> constructor = idClass.getConstructor(String.class);
return constructor.newInstance(IdGenerator.generate());
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException |
InvocationTargetException e) {
throw new IllegalArgumentException("Id 클래스 생성에 실패했습니다.");
}
}
}
IdGenerator
는 고유한 ID 문자열을 생성하는 핵심 로직을 담당하고,IdFactory
는IdGenerator
를 활용하여 특정 타입의 ID 객체를 생성하는 팩토리 역할을 한다.- Id 객체를 생성한 후,
IdFactory
클래스에 create 메서드를 오버로딩하여 2가지 생성하면 끝이다. - 원래 같으면
@GeneratedValue
로 Entity가 직접 생성해주지만, 현재의 로직에서는 개발자가IdFactory
로 Id 객체를 생성해줘야 한다.
컬럼명 변경Permalink
package movlit.be.movie.domain.entity;
@Table(name = "movie_comment")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class MovieCommentEntity {
@EmbeddedId
private MovieCommentId movieCommentId;
@AttributeOverride(name = "value", column = @Column(name = "member_id"))
private MemberId memberId; // Member 하나에 Comment 하나
private Long movieId; // Movie 하나에 Comment 하나
private String comment;
private Double score;
private LocalDateTime regDt;
private LocalDateTime updDt;
/* 생략 */
}
MovieCommentId
를 적용한MovieComment
의 Entity 객체 클래스다.MemberId
를 가지고 있는데,BaseId
에서@Column(name = "id")
와@JsonProperty("id")
로 관리하고 있으니, 필드명을 구분해주기 위해@AttributeOverride(name = "value", column = @Column(name = "member_id"))
로 지정할 수 있다.
결론Permalink
DDD를 적용하여 공부 목적으로 하는 것은 괜찮은 선택이라고 현업자분의 피드백을 받았다. 하지만, 현업에서는 대부분 Long으로 관리하므로 이렇게 사이드 프로젝트에서 사용해 보면 좋을 것 같다.
아까 언급한 현재 프로젝트에서 채팅방 메시지 정보는 MongoDB를 사용한다는 것을 고려했을 때, Long으로 관리하는 것보다 이렇게 id 객체로 관리하는 것이 연동하는 데 조금 더 자연스럽다는 느낌도 받을 수 있었다.
Leave a comment