Project/Nuwa

Nuwa Project - JPA 상속 관계 맵핑

Llimy1 2024. 2. 9. 14:28
반응형
SMALL
반응형
SMALL

프로젝트를 진행하면서 다양한 채팅 채널 구현이 필요해졌습니다.

필요한 채널은 단순하게 채팅만 이루어지는 채널, 1:1 다이렉트 채널, 음성 채널

이렇게 나누어서 구현을 해야하는데

맨 처음엔 구현 클래스마다 테이블을 생성을 했습니다.

구현 클래스 별 테이블 생성(필드 내용은 따로 적지 않았습니다.)

 

이렇게 구현을 하다보면 중복이 되는 내용이 많아졌습니다.

예를 들면 채널 이름, 채널 이름 등 공통으로 가진 속성이 많았습니다.

 

그래서 단일 테이블로 작성을 하게 되면 문제가 해결이 되지 않을까 생각을 했습니다.

단일 테이블

그래서 단일 테이블로 생성을 하고 Enum으로 각 채널의 타입을 나눠서 관리를 하려고 하고

ChannelMember 테이블을 따로 생성하여 멤버를 따로 작성을 하려고 했습니다.

그런데 다이렉트 메세지 같은 경우 두 명의 인원이 존재하도록 테이블을 설계를 해야한다고 생각했습니다.

그러기 위해서 다이렉트만 따로 빼서 OneToOne 관계를 맺는건 불필요한 일이라고 생각했고

 

김영한 강사님의 JPA 강의에서 JPA 상속관계 맵핑에 대해서 설명을 해주신 것이 생각이 났습니다.

그 중에서도 조인 전략을 사용하여 상속 관계로 맵핑을 하게된다면

채팅 채널, 다이렉트 채널, 보이스 채널의 각각 필드를 따로 관리를 할 수 있고

제가 현재 고민을 하던 부분이 해소가 되지 않을까 생각했고 조인 전략을 적용 하기로 했습니다.

 

가장 상위가 되는 Channel 도메인에

@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn // 하위 테이블의 구분 컬럼 생성(default = DTYPE)

 

해당 어노테이션을 적용을 했습니다.

 

그리고 Direct 도메인을 생성을 했습니다.

 

Channel.java

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn // 하위 테이블의 구분 컬럼 생성(default = DTYPE)
@Entity
public abstract class Channel extends BaseTimeJpa {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "channel_id")
    private Long id;

    @Column(name = "room_id")
    private String roomId;

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

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "workspace_id")
    private WorkSpace workSpace;

    protected Channel(String name, WorkSpace workSpace) {
        this.roomId = UUID.randomUUID().toString();
        this.name = name;
        this.workSpace = workSpace;
    }
}

 

Direct.java

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Direct extends Channel {

    private Member sender;
    private Member receiver;

    @Builder
    private Direct(String name, WorkSpace workSpace, Member sender, Member receiver) {
        super(name, workSpace);
        this.sender = sender;
        this.receiver = receiver;
    }

    // 다이렉트 채널 생성
    public static Direct createDirectChannel(WorkSpace workSpace, Member sender, Member receiver) {
        return Direct.builder()
                .workSpace(workSpace)
                .sender(sender)
                .receiver(receiver)
                .build();
    }
}

 

다음과 같이 도메인을 생성을 했습니다.

 

그 후 기존의 사용하던 서비스 코드를 보면

 

DirectChannelService.java

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class DirectChannelService {

    private final WorkSpaceMemberRepository workSpaceMemberRepository;
    private final WorkSpaceRepository workSpaceRepository;

    private final ChannelMemberRepository channelMemberRepository;
    private final ChannelRepository channelRepository;


    // TODO: 테이블 설계를 다시 해야 할 필요성이 있음. 채널 멤버 생성에 insert문 2번은 불필요
    // 다이렉트 채널 생성
    @Transactional
    public String createDirectChannel(DirectChannelRequest directChannelRequest) {
        log.info("다이렉트 채널 생성");
        String directSender = directChannelRequest.sender();
        String directReceiver = directChannelRequest.receiver();
        Long workSpaceId = directChannelRequest.workSpaceId();;

        // 워크스페이스가 존재하는지 확인
        WorkSpace workSpace = workSpaceRepository.findById(workSpaceId)
                .orElseThrow(() -> new NotFoundException(WORK_SPACE_NOT_FOUND));

        // 워크스페이스에 멤버가 존재 하는지 확인
        WorkSpaceMember sender = workSpaceMemberRepository.findByName(directSender)
                .orElseThrow(() -> new NotFoundException(WORK_SPACE_MEMBER_NOT_FOUND));

        // 워크스페이스에 멤버가 존재 하는지 확인
        WorkSpaceMember receiver = workSpaceMemberRepository.findByName(directReceiver)
                .orElseThrow(() -> new NotFoundException(WORK_SPACE_MEMBER_NOT_FOUND));

        // 각각 멤버 테이블 가져오기
        Member senderMember = sender.getMember();
        Member receiverMember = receiver.getMember();

        // 워크스페이스 존재하고 멤버도 전부 존재하면 채널 저장
        Channel directChannel = Channel.createDirectChannel(workSpace);

        Channel saveChannel = channelRepository.save(directChannel);

        // 저장한 채널로 채널 멤버 생성
        ChannelMember directSenderMember = ChannelMember.createChannelMember(senderMember, saveChannel);
        ChannelMember directReceiverMember = ChannelMember.createChannelMember(receiverMember, saveChannel);

        log.info("채널 멤버 저장");
        // 채널 멤버 저장
        channelMemberRepository.save(directSenderMember);
        channelMemberRepository.save(directReceiverMember);

        // RoomId 반환
        return saveChannel.getRoomId();
    }

}

 

워크스페이스에 존재를 하는지 여부를 판단하고

채널을 저장하고 그 후에 각각 채널 멤버에 저장을 했습니다.

 

조인 전략으로 설계를 한 후

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class DirectChannelService {

    private final WorkSpaceMemberRepository workSpaceMemberRepository;
    private final WorkSpaceRepository workSpaceRepository;

    private final DirectChannelRepository directChannelRepository;
    
    // 다이렉트 채널 생성
    @Transactional
    public String createDirectChannel(DirectChannelRequest directChannelRequest) {
        log.info("다이렉트 채널 생성");
        String directSender = directChannelRequest.sender();
        String directReceiver = directChannelRequest.receiver();
        Long workSpaceId = directChannelRequest.workSpaceId();;

        // 워크스페이스가 존재하는지 확인
        WorkSpace workSpace = workSpaceRepository.findById(workSpaceId)
                .orElseThrow(() -> new NotFoundException(WORK_SPACE_NOT_FOUND));

        // 워크스페이스에 멤버가 존재 하는지 확인
        WorkSpaceMember sender = workSpaceMemberRepository.findByName(directSender)
                .orElseThrow(() -> new NotFoundException(WORK_SPACE_MEMBER_NOT_FOUND));

        // 워크스페이스에 멤버가 존재 하는지 확인
        WorkSpaceMember receiver = workSpaceMemberRepository.findByName(directReceiver)
                .orElseThrow(() -> new NotFoundException(WORK_SPACE_MEMBER_NOT_FOUND));

        // 각각 멤버 테이블 가져오기
        Member senderMember = sender.getMember();
        Member receiverMember = receiver.getMember();

        // 워크스페이스 존재하고 멤버도 전부 존재하면 채널 저장
        DirectChannel directChannel = DirectChannel.createDirectChannel(workSpace, senderMember, receiverMember);

        DirectChannel saveDirectChannel = directChannelRepository.save(directChannel);

        // RoomId 반환
        return saveDirectChannel.getRoomId();
    }

}

 

다음과 같이 코드 줄 수도 줄고 채널을 생성하고 멤버를 또 저장하는 일이 줄어들었습니다.

다만 channel을 저장하고 directChannel도 저장을 하는 insert가 두 차례 발생하지만

이전의 코드보다 insert문이 줄어들고 각각 테이블에서 필요한 필드를 관리할 수 있다는 점에서

현재 프로젝트에 더욱 적합하다고 생각이 들었습니다.

반응형
LIST