Published:
Updated:

Movlit 프로젝트에 대한 설명입니다.

서론Permalink

NoSQL의 경우 id를 ObjectId로 관리하는 경우를 많이 봤을 것이다. 하지만, RDB를 사용한다면 보통 id는 Long으로 두고 @GeneratedValue로 자동으로 증가시키게끔 하였을 것이다.

현재 프로젝트의 채팅 메시지 관련 부분을 제외하고는 전부 MySQL을 사용하고 있지만, 프로젝트 초반 설계부터 id는 Long 값을 그 Entity의 id 객체로 관리하게끔 하였다.

프로젝트 구조Permalink

Screenshot 2025-01-18 at 15 41 07

  • 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 문자열을 생성하는 핵심 로직을 담당하고, IdFactoryIdGenerator를 활용하여 특정 타입의 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 객체로 관리하는 것이 연동하는 데 조금 더 자연스럽다는 느낌도 받을 수 있었다.

Tags: ,

Categories:

Published:
Updated:

Leave a comment