Nuwa Project - JPA 상속 관계 맵핑
프로젝트를 진행하면서 다양한 채팅 채널 구현이 필요해졌습니다.
필요한 채널은 단순하게 채팅만 이루어지는 채널, 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문이 줄어들고 각각 테이블에서 필요한 필드를 관리할 수 있다는 점에서
현재 프로젝트에 더욱 적합하다고 생각이 들었습니다.