ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java][JPA] 추상 클래스 개념을 적용해 알림 객체 설계 개선하기
    Today I Learned 2023. 6. 17. 18:36

     
    로버트 C. 마틴이 언급한 객체지향 설계를 위한 5대 원칙 중 하나로 개방-폐쇄 원칙(Open-closed Principle)이 있다. '소프트웨어의 구성 요소는 확장에는 열려 있고, 변경에는 닫혀 있어야 한다'는 원칙으로, 간단히 이야기하자면 소프트웨어의 특정 모듈의 기능이 추가되어야 할 경우, 기존에 작성한 코드를 가급적 변경하지 않으면서 기능을 추가할 수 있어야 한다는 것이다.
     

    기존 프로젝트의 알림 구조

    프로젝트 애플리케이션에는 '알림'과 관련된 기능들과 Entity들이 존재했다. 특정 기능들을 수행했을 때 (이를테면 어떤 경기에 참가신청했다던가, 참가신청이 작성자에 의해 수락되었다던가와 같은 것들) 그와 관련된 정보를 담은 Notice 객체를 생성해 저장하는 부가적인 기능이었다.
     
    본래 알림 기능을 처음 만들면서 의도했던 것은 사용자가 알림을 확인하면서 그 알림과 관련된 콘텐츠에 빠르게 접근할 수 있는 것이었다. 예를 들어 사용자에게 누군가가 자신이 모집하고 있는 경기에 참가신청을 했다는 알림을 확인하면, 그 알림 화면에서 자신이 작성한 경기 글로 바로 이동할 수 있는 것을 의도했었다. 다만 구현 당시에는 기술적 역량의 한계...로 인해 관련된 텍스트만 담아 전달하는 수준으로 갈음한 상태였다. 그러다보니 구현 자체도 단순했다. 알림 Entity는 단순히 사용자 식별자와 문자열만을 담고 있는 형태였다.
     

    // models/notice/Notice.java
    
    @Entity
    @Table(name = "notices")
    public class Notice {
        @Id
        @GeneratedValue
        private Long id;
    
        private Long userId;
    
        // 알림 내용을 String 타입의 title, detail 변수에 포함하고 있는 값 객체이다.
        @Embedded
        private NoticeContents contents;
    
        @Enumerated(EnumType.STRING)
        private NoticeStatus status;
    
        @CreationTimestamp
        private LocalDateTime createdAt;
    
        // constructors, getters
        // ...
    
        public boolean active() {
            return status.equals(NoticeStatus.UNREAD)
                || status.equals(NoticeStatus.READ);
        }
    
        public boolean unread() {
            return status.equals(NoticeStatus.READ);
        }
    
        public void read() {
            status = NoticeStatus.READ;
        }
    
        public void delete() {
            status = NoticeStatus.DELETED;
        }
    
        public NoticeDto toNoticeDto() {
            return new NoticeDto.Builder()
                .id(id)
                .title(contents.title())
                .detail(contents.detail())
                .status(status.value())
                .createdAt(createdAt)
                .build();
        }
    }

     
    알림 Entity를 이용해 알림을 조회하거나, 상태를 변경하는 기능을 수행하는 Service들 중 특정 사용자의 모든 알림을 조회하는 기능인 GetNoticesService를 살펴보자.
     

    // services/notice/GetNoticesService.java
    
    @Service
    @Transactional(readOnly = true)
    public class GetNoticesService {
        private final UserRepository userRepository;
        private final NoticeRepository noticeRepository;
    
        public GetNoticesService(UserRepository userRepository,
                                 NoticeRepository noticeRepository) {
            this.userRepository = userRepository;
            this.noticeRepository = noticeRepository;
        }
    
        public NoticesDto getNotices(Long userId) {
            List<Notice> notices = noticeRepository
                .findAllByUserId(userId);
    
            List<NoticeDto> noticeDtos = notices.stream()
                .filter(Notice::active)
                .map(Notice::toNoticeDto)
                .toList();
    
            return new NoticesDto.Builder()
                .notices(noticeDtos)
                .build();
        }
    }

     
    지금은 알림이 하나의 class 타입이고, 알림 내용은 단순 문자열이기 때문에 모든 알림에 대해 active()나 toNoticeDto() 동작을 수행하는 것이 가능하다.
     
    그러나 알림에서 확인할 수 있는 경기 게시글로 바로 이동한다거나, 전달된 사용자의 구체적인 리소스를 확인한다거나 하려면 구체적인 식별자가 필요하다. 해당 기능을 가장 단순하게 추가할 수 있는 방법은 각 알림 정보에 필요한 식별자를 직접 들고 있는 Notice들을 각각 만드는 것이다.
     

    단순히 Notice 형태의 class들을 추가하는 방식의 문제 해결

    @Entity
    @Table(name = "notices_application_accepted")
    public class NoticeApplicationAccepted {
        @Id
        @GeneratedValue
        private Long id;
    
        private Long userId;
    
        @Embedded
        private NoticeContents contents;
    
        @Enumerated(EnumType.STRING)
        private NoticeStatus status;
    
        // 참가신청이 수락된 경기 정보 리소스 조회 요청을 위한 경기 식별자
        private Long gameId;
    
        @CreationTimestamp
        private LocalDateTime createdAt;
    
        // 생성자에는 필요한 식별자에 대한 정보만 추가해 전달하면 된다.
        // 동작에 필요한 메서드들은 기존의 Notice와 모두 같다.
        // ...
    
        public NoticeApplicationAcceptedDto toNoticeApplicationAcceptedDto() {
            return new NoticeDto.Builder()
                .id(id)
                .title(contents.title())
                .detail(contents.detail())
                .status(status.value())
                .gameId(gameId)
                .createdAt(createdAt)
                .build();
        }
    }
    @Entity
    @Table(name = "notices_new_application_created")
    public class NoticeNewApplicationCreated {
        @Id
        @GeneratedValue
        private Long id;
    
        private Long userId;
    
        @Embedded
        private NoticeContents contents;
    
        @Enumerated(EnumType.STRING)
        private NoticeStatus status;
    
        // 새로운 참가신청이 생성된 경기 정보 리소스 조회 요청을 위한 경기 식별자
        private Long gameId;
    
        // 신청자 정보 리소스 조회 요청을 위한 사용자 식별자
        private Long appliedUserId;
    
        @CreationTimestamp
        private LocalDateTime createdAt;
    
        // 여기도 마찬가지.
        // ...
    
        public NoticeNewApplicationCreatedDto toNoticeNewApplicationCreatedDto() {
            return new NoticeDto.Builder()
                .id(id)
                .title(contents.title())
                .detail(contents.detail())
                .status(status.value())
                .gameId(gameId)
                .appliedUserId(appliedUserId);
                .createdAt(createdAt)
                .build();
        }
    }

     
    중복이 다소 거슬리지만, 뭐 어쨌든 단순 문자열뿐만 아니라 각자에게 필요한 식별자 정보까지도 담고 있는 알림 객체들을 만들었다. 이제 이 알림 객체들을 활용하는 GetNoticesService를 확인해보자.
     

    @Service
    @Transactional(readOnly = true)
    public class GetNoticesService {
        private final UserRepository userRepository;
        private final NoticeNewApplicationCreatedRepository noticeNewApplicationCreatedRepository;
        private final NoticeApplicationAcceptedRepository noticeApplicationAcceptedRepository;
    
        public GetNoticesService(
            UserRepository userRepository,
            NoticeNewApplicationCreatedRepository noticeNewApplicationCreatedRepository,
            NoticeApplicationAcceptedRepository noticeApplicationAcceptedRepository
        ) {
            this.userRepository = userRepository;
            this.noticeNewApplicationCreatedRepository = noticeNewApplicationCreatedRepository;
            this.noticeApplicationAcceptedRepository = noticeApplicationAcceptedRepository;
        }
    
        public NoticesDto getNotices(Long userId) {
            List<NoticeNewApplicationCreated> noticesNewApplicationCreated
                = noticeNewApplicationCreatedRepository
                .findAllByUserId(userId);
            
            List<NoticeApplicationAccepted> noticesApplicationAccepted
                = noticeApplicationAcceptedRepository
                .findAllByUserId(userId);
    
            List<NoticeNewApplicationCreatedDto> noticeNewApplicationCreatedDtos
                = noticesNewApplicationCreated.stream()
                .filter(NoticeNewApplicationCreated::active)
                .map(NoticeNewApplicationCreated::toNoticeNewApplicationCreatedDto)
                .toList();
            
            List<NoticeApplicationAcceptedDto> noticeApplicationAcceptedDtos
                = noticesApplicationAccepted.stream()
                .filter(NoticeApplicationAccepted::active)
                .map(NoticeApplicationAccepted::toNoticeApplicationAcceptedDto)
                .toList();
    
            return new NoticesDto.Builder()
                .noticesNewApplicationCreated(noticeNewApplicationCreatedDtos)
                .noticesApplicationAccepted(noticeApplicationAcceptedDtos)
                .build();
        }
    }

     
    네이밍이나 비슷한 기능의 반복으로 인해 내용이 길어지고 복잡해진 것뿐만 아니라, 내용이 변경되었다는 사실 자체에 한번 주목해보자.

    Notice 계열의 객체를 추가했더니 Notice를 활용하는 Service의 로직에서 서로 다른 객체들을 쿼리하고, 리소스로 조합하는, 기존의 동작과 형태가 거의 비슷한 동작들이 추가되었다. 그런데 여기서는 GetNoticesService의 예시만 들었지만, 그 외에 여러 Notice들을 활용하는 Service에서도 모두 비슷한 방식으로 로직이 추가되어야 할 것이다. 객체 설계를 변경했더니, 기존에 동작하는 기능이 같은 방식으로 동작되도록 유지시키기 위해 코드의 많은 부분들이 수정되어야 하는 상황이다. 이런 방식이라면 알림의 종류가 하나 늘어날 때마다 비효율적인 작업들이 매번 수반되어야 할 것이다. 기능을 추가하거나 설계를 변경하더라도 기존에 존재하고 있던 로직에 가는 영향이 최소화되도록 할 수는 없을까?
     
     

    abstract class를 정의하고 해당 class를 상속받아 구현하는 객체들을 활용하는 방식의 문제 해결

    각 Notice 객체들을 살펴보면, 데이터에서나 동작에서나 중복되는 부분들이 많다는 것을 확인할 수 있다.
     

    데이터
        - id
        - userId
        - status
        - createdAt
    동작
        - status 상태 확인
        - status 상태 변경

     
    이렇게 겹치는 부분들을 추상화해서 어떤 알림 객체던지 공통적으로 가져야 하는 데이터나 동작을 갖는 하나의 추상 객체를 정의해놓고, 추상 객체를 구현한 객체에는 각각의 알림에 필요한 Entity의 식별자를 부여하는 방식을 적용해볼 수 있다.
     

    abstract class

    @Entity
    @Table(name = "notices")
    public abstract class Notice {
        @Id
        @GeneratedValue
        private Long id;
    
        private Long userId;
    
        @Enumerated(EnumType.STRING)
        private NoticeStatus status;
    
        @CreationTimestamp
        private LocalDateTime createdAt;
    
        Notice() {
    
        }
    
        Notice(Long userId, NoticeStatus status) {
            this.userId = userId;
            this.status = status;
        }
        
        // getters
        // ...
    
        public boolean active() {
            return status.equals(NoticeStatus.UNREAD)
                || status.equals(NoticeStatus.READ);
        }
    
        public boolean unread() {
            return status.equals(NoticeStatus.UNREAD);
        }
    
        public void read() {
            status = NoticeStatus.READ;
        }
    
        public void delete() {
            status = NoticeStatus.DELETED;
        }
    }

     
    모든 Notice의 하위 클래스들에서 사용될 공통된 데이터와 수행할 수 있는 기능을 정의한 Notice를 abstract class로 정의했다. abstract class는 그 자체가 인스턴스로 생성될 수 없기 때문에, 해당 클래스를 상속받은 자식 클래스만을 인스턴스로 생성할 수 있다.

    이때, abstract class를 JPA를 통해 Entity로 적용하기 위해서는 몇 가지 조건들을 설정해줘야 한다. 데이터베이스의 테이블 간에는 별도의 상속 개념이 존재하지 않기 때문이다. 조건 설정을 위해 다음의 어노테이션들을 클래스 정의부에 추가했다.
     

    @Entity
    @Table(name = "notices")
    // 다음의 두 어노테이션을 추가한다.
    @Inheritance(strategy = InheritanceType.JOINED)
    @DiscriminatorColumn(discriminatorType = DiscriminatorType.STRING)
    public abstract class Notice {
        // ...
    }

    @Inheritance

    상속 전략에 따라 데이터베이스의 테이블을 어떻게 구성할 것인지 결정한다. strategy에 부여할 수 있는 전략들은 다음과 같다.

    • InheritanceType.JOINED: 부모 Entity의 테이블, 각각의 자식 Entity의 테이블들을 별도로 구성한다. 자식 테이블은 부모 테이블의 특정 Row의 식별자를 자신의 테이블의 기본 키이자 외래 키로 갖는다.

    • InheritanceType.SINGLE_TABLE: 부모 Entity와 모든 자식 Entity가 갖는 필드 값들을 Column으로 갖는 단 하나의 테이블을 구성한다. 하나의 Row는 부모 Entity와 특정 자식 Entity가 가져야 하는 값 외에는 모두 null로 구성된다.

    • InheritanceType.TABLE_PER_CLASS: 자식 Entity들로만 테이블들을 각각 구성한다. 자식 테이블에는 부모 Entity가 갖는 필드 값들과 자신이 갖는 필드 값에 대한 Column이 존재한다.

    여기서는 테이블들이 갖는 Column의 중복을 최소화하고, Row에 의도하지 않은 null 값이 최대한 들어가지 않도록 할 수 있는 InheritanceType.JOINED 전략을 선택했다.
     

    @DiscriminatorColumn

    InheritanceType.JOINED 전략을 사용하는 경우, 부모 Entity의 Table에 어떤 자식 Entity의 Table에 연결되는지 식별할 수 있는 Column을 추가한다. 이때 'discriminatorType' 속성을 부여해야 해당 Column이 자동으로 생성된다. 여기서는 DiscriminatorType.STRING 속성을 부여했다. Column의 이름은 'name = 지정하고 싶은 이름' 속성을 같이 전달하는 것으로 부여할 수 있으며, 별도로 지정하지 않을 경우 "DTYPE"으로 지정된다.
     
    자식 Table에는 @DiscriminatorColumn(부모 Table의 식별 Column에 추가될 이름) 어노테이션을 추가해 해당 식별 Column에 어떤 이름으로 추가되게 할 것인지 지정할 수 있다. 해당 어노테이션을 별도로 부여하지 않을 경우 기본적으로 자식 클래스의 이름이 식별 Column에 부여된다.
     
     
     
    그리고 Notice의 동작이기는 하나, 동작의 세부 사항은 Notice 추상 클래스를 상속받아 구현하는 객체들에서 직접 구현하도록  하는 abstract 메서드 시그니쳐를 다음과 같이 정의했다.
     

    public abstract class Notice {
        // 다음의 abstract 메서드 시그니쳐들을 추가한다.
    
        abstract public NoticeDto toNoticeDto();
    
        abstract public String title();
    
        abstract public String detail();
    }

     
    이제 이들을 바탕으로 Notice 추상 클래스를 상속받아 구현한 구체 클래스들을 살펴보자.
     

    abstract class를 상속받는 구현 class

    // models/notice/NoticeApplicationAccepted.java
    
    @Entity
    @Table(name = "notices_application_accepted")
    @DiscriminatorValue(NoticeType.APPLICATION_ACCEPTED)
    public class NoticeApplicationAccepted extends Notice {
        @ManyToOne(fetch = FetchType.EAGER)
        @JoinColumn(name = "game_id")
        private Game game;
    
        private NoticeApplicationAccepted() {
    
        }
    
        public NoticeApplicationAccepted(Long userId, NoticeStatus status, Game game) {
            super(userId, status);
            this.game = game;
        }
    
        @Override
        public NoticeApplicationAcceptedDto toNoticeDto() {
            return new NoticeApplicationAcceptedDto.Builder()
                .id(id())
                .type(NoticeType.APPLICATION_ACCEPTED)
                .status(status().value())
                .title(title())
                .detail(detail())
                .createdAt(createdAt())
                .gameId(game.id())
                .build();
        }
    
        @Override
        public String title() {
            return "참가신청한 경기의 신청이 수락되었습니다.";
        }
    
        @Override
        public String detail() {
            return "경기 시작시간: " + formatDateTime(game.startDateTime()) + "\n" +
                "경기 종료시간: " + formatDateTime(game.endDateTime());
        }
    
        private String formatDateTime(LocalDateTime dateTime) {
            return dateTime.format(
                DateTimeFormatter.ofPattern("yyyy년 M월 d일 HH:mm")
            );
        }
    }

     
    해당 객체의 구현 내용을 하나씩 살펴보도록 하자.

    클래스 명세

    @Entity
    @Table(name = "notices_application_accepted")
    @DiscriminatorValue(NoticeType.APPLICATION_ACCEPTED)
    public class NoticeApplicationAccepted extends Notice {
        // ...
    }

     
    자식 Entity의 Table에 필요한 속성을 명세했다. Table명을 지정하고, 부모 Entity의 구현 객체 식별 Column에 부여될 값을 정의했다. 전달되는 상수는 다음과 같이 정의했다.
     

    // models/notice/NoticeType.java
    
    public class NoticeType {
        public static final String NEW_APPLICATION_CREATED = "NEW_APPLICATION_CREATED";
    
        public static final String APPLICATION_ACCEPTED = "APPLICATION_ACCEPTED";
    }

     

    데이터

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "game_id")
    private Game game;

     
    자식 Entity만이 독자적으로 가져야 할 데이터를 정의했다. NoticeApplicationAccepted는 참가신청이 수락되어 참가가 확정된 경기에 대한 정보를 갖고 있으므로, Game Entity를 멤버로 들고 있도록 했다. 이때 Notice를 쿼리하는 시점에서 Game Entity도 바로 쿼리해 Notice 객체가 생성되는 시점부터 보유하고 있도록 했다. (FetchType.EAGER)
     

    동작

    자식 Entity가 갖는 데이터를 잘 살펴보면, 별도의 알림 내용을 갖고 있는 데이터 필드가 따로 없는 것을 확인할 수 있다. 알림 내용은 다음과 같이 구현 객체가 abstract 메서드를 구현하는 과정에서 직접 생성하도록 했다.
     

    @Override
    public NoticeApplicationAcceptedDto toNoticeDto() {
        return new NoticeApplicationAcceptedDto.Builder()
            .id(id())
            .type(NoticeType.APPLICATION_ACCEPTED)
            .status(status().value())
            .title(title())
            .detail(detail())
            .createdAt(createdAt())
            .gameId(game.id())
            .build();
    }
    
    @Override
    public String title() {
        return "참가신청한 경기의 신청이 수락되었습니다.";
    }
    
    @Override
    public String detail() {
        return "경기 시작시간: " + formatDateTime(game.startDateTime()) + "\n" +
            "경기 종료시간: " + formatDateTime(game.endDateTime());
    }

     
    알림을 생성하는 시점에서 알림 구체가 식별되므로, Table에 직접 데이터를 기록할 필요 없이 Entity 객체가 직접 특정 문자열을 반환하도록 할 수 있다. 구현 객체의 toNoticeDto() 메서드의 반환 타입을 보면 NoticeApplicationAcceptedDto인 것을 확인할 수 있는데, 해당 DTO 객체도 NoticeDto를 abstract class로 지정한 뒤, gameId를 추가적으로 갖고 있는 구현 객체를 정의하는 방식으로 구현했다. 상위 타입으로 정의된 변수나 인자, 반환값에는 하위 타입의 인스턴스를 전달할 수 있기 때문이다.
     

    확인할 수 있었던 이점

    그렇다면 Notice를 상속받아 구현한 다른 객체인 NoticeNewApplicationCreated를 보면서 구현 객체마다 어떻게 갖는 데이터나 동작에 차이를 줄 수 있는지 확인해보자.
     

    // models/notice/NoticeNewApplicationCreated.java
    
    @Entity
    @Table(name = "notices_new_application_created")
    @DiscriminatorValue(NoticeType.NEW_APPLICATION_CREATED)
    public class NoticeNewApplicationCreated extends Notice {
        @ManyToOne(fetch = FetchType.EAGER)
        @JoinColumn(name = "game_id")
        private Game game;
    
        @ManyToOne(fetch = FetchType.EAGER)
        @JoinColumn(name = "applied_user_id")
        private User appliedUser;
    
        private NoticeNewApplicationCreated() {
    
        }
    
        private NoticeNewApplicationCreated(Long userId, NoticeStatus status, Game game, User appliedUser) {
            super(builder);
            this.game = builder.game;
            this.appliedUser = builder.appliedUser;
        }
    
        @Override
        public NoticeDto toNoticeDto() {
            return new NoticeNewApplicationCreatedDto.Builder()
                .id(id())
                .type(NoticeType.NEW_APPLICATION_CREATED)
                .status(status().value())
                .title(title())
                .detail(detail())
                .createdAt(createdAt())
                .gameId(game.id())
                .applicantId(appliedUser.id())
                .build();
        }
    
        @Override
        public String title() {
            return "새로운 경기 참가신청이 있습니다.";
        }
    
        @Override
        public String detail() {
            return "신청자: " + appliedUser.name();
        }
    }

     
    작성한 경기 인원 모집 글에 참가신청한 사용자 정보를 확인할 수 있는 알림 구현 객체인 NoticeNewApplicationCreated는 Game 객체와 User 객체를 보유하고 있고, 이들을 이용해 클라이언트에 전달할 NoticeDto 리소스를 NoticeApplicaiontAccepted 구현 객체와는 다른 방식으로 생성한다.
     
    이처럼 구현 객체에 따라 같은 Notice라 하더라도 보유하는 정보에 차이점을 두고, 공통된 동작을 자신에게 맞게 정의하는 방식으로 유연하게 Notice 구현 객체를 추가할 수 있다.
     
    마지막으로 해당 방식으로 Notice 객체들을 설계했을 때, Notice 객체를 이용해 리소스를 생성하는 GetNoticesService의 소스코드를 살펴보자.
     

    // services/notice/GetNoticesService.java
    
    @Service
    @Transactional(readOnly = true)
    public class GetNoticesService {
        private final UserRepository userRepository;
        private final NoticeRepository noticeRepository;
    
        public GetNoticesService(UserRepository userRepository,
                                 NoticeRepository noticeRepository) {
            this.userRepository = userRepository;
            this.noticeRepository = noticeRepository;
        }
    
        public NoticesDto getNotices(Long userId) {
            List<Notice> notices = noticeRepository
                .findAllByUserId(userId);
    
            List<NoticeDto> noticeDtos = notices.stream()
                .filter(Notice::active)
                .map(Notice::toNoticeDto)
                .toList();
    
            return new NoticesDto.Builder()
                .notices(noticeDtos)
                .build();
        }
    }

     
    Notice 객체의 구현체는 모두 서로 다르지만, Notice 타입의 Entity를 사용하는 기능에는 전혀 영향을 끼치지 않는다. 모든 Notice 구현 객체들은 상위 타입이 Notice이고, 각 비즈니스 로직에서는 상위 타입인 Notice에 정의된 메서드들을 호출해 기능을 수행하기 때문이다. 이제는 어떤 다른 Notice 구현 객체가 추가되더라도, Notice 타입 객체를 사용하는 비즈니스 로직을 전혀 건드리지 않거나, 수정하더라도 수정을 최소화할 수 있게 되었다.
     

    고민했던 점

    그동안 Entity를 설계할 때 객체 생성 시점부터 다른 Entity에 대한 직접적인 연관관계를 부여하지 않고, 연관관계가 필요한 Entity의 식별자만을 갖고 있으면서 필요 시 해당 식별자를 이용해 다른 Entity를 쿼리하는 방식으로 줄곧 설계했었지만, 이번 Notice 구현 객체를 정의하는 과정에서는 객체 생성 시점부터 다른 Entity 객체를 직접 참조하는 방식을 적용했다.
     
    abstract 메서드인 toNoticeDto()를 생성하는 로직은 Notice의 구현 객체마다 차이가 존재한다. 이때 리소스를 생성하기 위해 참조해야 하는 객체들이 구현 객체마다 서로 달랐고, 리소스를 생성하기 위한 toNoticeDto()에 전달하는 다른 Entity 인자들을 일치시킬 수 없었다. 생각할 수 있었던 해결책은 모든 구현 객체들이 사용하는 인자들을 모두 전달받는 식으로 구현하는 것이었지만, 특정 구현 객체가 사용하지 않는 인자를 굳이 드러내야 하는가 싶어 적용하지 않았다.
     
    toNoticeDto()를 abstract 메서드로 두지 않고 자식 메서드가 알아서 구현하는 방식은 적용할 수 없었다. Service에서 Notice를 사용할 때는 상위 타입 객체인 Notice를 활용하고 있었기 때문에 toNoticeDto() 메서드는 부모 메서드에 정의된 공통 메서드여야 했다. 따라서 인자로 전달되는 객체 없이 객체 내부에서 다른 객체를 직접 참조할 수 있는 방식을 적용했다.
     
    Game과 User Entity는 다른 Entity를 직접 참조하고 있지는 않기 때문에 당장은 N+1 문제가 발생하지는 않을 것이지만, 추후 더 나은 방식으로 코드를 개선할 수 있을지는 차근차근 고려해봐야 할 것 같다.

     

    2023.7.13 추가

    특정 Entity다른 Entity를 직접 참조할 시, 참조되는 Entity를 데이터베이스에서 삭제하기 위해서는 해당 Entity를 참조하는 모든 Entity들을 먼저 삭제해야만 한다. 내 예시에서는 어떤 Game Entity를 삭제하기 위해서는 그 Game Entity를 참조하고 있는 그 어떤 Notice 구현 객체 Entity도 존재하지 않아야 한다. 만약 자신을 참조하고 있는 Notice Entity가 존재하는 상황에서 Game Entity를 삭제하려는 경우 JdbcSQLIntegrityConstraintViolationException이 발생한다. Notice 구현체 Table의 game_id Column은 Game Table의 특정 Tuple의 기본 키를 참조하고 있는데, 존재하지 않는 Game Tuple의 기본 키를 참조할 수 없기 때문이다.


     

    References

    - Hibernate Tips Book Excerpt: How to Map an Inheritance Hierarchy to One Table
    - [JPA 기본] 상속관계 매핑
    - Hibernate Inheritance: Table Per Class Hierarchy (Annotation & XML Mapping)
    - ChatGPT
     
     
     
     

    댓글

Designed by Tistory.