Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: News 엔티티 추가 (#2) #6

Merged
merged 10 commits into from
Dec 8, 2023
Merged

feat: News 엔티티 추가 (#2) #6

merged 10 commits into from
Dec 8, 2023

Conversation

seokjin8678
Copy link
Contributor

@seokjin8678 seokjin8678 commented Dec 4, 2023

관련 이슈

close #2

PR 세부 내용

News, Content 엔티티를 정의하고 추가했습니다.

테이블 구조는 다음과 같습니다.

erDiagram
    NEWS ||--|{ CONTENT : contains
    NEWS {
        uuid id PK
        bigint origin_id
        timestamp published_at
        varchar excerpt
        varchar news_type
        varchar support_languages
        varchar title
    }
    CONTENT {
        bigint sequence PK
        uuid news_id FK
        varchar excerpt
        varchar language
        varchar title
        clob content
    }
Loading

News

News 그 자체인 엔티티 입니다.
Content와 1:N 관계를 가지고 있습니다.
NewsType, originId, publishedAt, newsInformation, mutableSupportLanguages, contents를 필드로 가지고 있습니다.

NewsType

열거형으로, PATCH_NOTE, NEWS를 가지고 있습니다.
추후 개발자 트래커 혹은 그 외의 형식이 추가될 가능성을 염두했습니다.

originId

다음은 뉴스 API의 JSON 형식입니다.

{
    "id": 19478,
    "publish_start": "2023-09-15 19:00:04",
    "time_created": "2023-09-15 18:32:53",
    "excerpt": "You asked. We're answering! Join us today for a live Q&A show with the Vehicle Gameplay team.",
    "title": "Star Citizen Live ",
    "url": "https://robertsspaceindustries.com/comm-link/transmission/19478-Star-Citizen-Live"
}

여기서, id 필드가 정수로 된 고유한 식별자라는 것을 알 수 있습니다.
만약, 저희만의 뉴스 혹은 그 외 서비스에서 제공하는 뉴스라면 id가 중복될 수 있으므로 Provider를 추가하여 중복된 식별자를 가지지 않도록 해야할 것 같습니다.
우선 지금은 RSI의 정보만 받기 때문에 똑같이 Long 타입으로 두었습니다.

publishedAt

위의 JSON 형식에서 publish_start 필드를 나타냅니다.

newsInformation

밑에서 Content를 설명할 때 자세히 얘기하겠습니다.
News에도 해당 필드를 가지게 한 이유는 역정규화 관점에서 생각하시면 될 것 같습니다.
또한 여러 개의 Content를 소유할 수 있으므로 여러 개의 Content를 대표하는 정보라고 보시면 될 것 같습니다.
(영어와 한글로 된 Content를 소유했을 때 어떤 것을 미리보기에서 보여줄 지)
지금은 처음 등록된 Content의 newsInformation을 설정되도록 했습니다.

mutableSupportLanguages

Language 열거형을 가지고 있는 EnumSet 입니다.

News를 보여줄 때 어떤 언어를 제공하는지 알려주는 필드입니다.

참고로 supportLanguages 필드가 있는데, 둘의 차이는 방어적 복사본을 제공하냐의 차이라고 보면 될 것 같습니다.
(실제 내부에서 사용하는 비즈니스 로직은 mutableSupportLanguages를 사용해야 합니다!)
이렇게 사용한 이유는 EnumSet을 사용하기 때문입니다..
mutableSupportLanguages을 get으로 원본을 제공했을 때 외부에서 변경이 있으면 사이드 이펙트가 발생할 수 있습니다.
따라서 방어적 복사본을 제공하려고 get() = mutableSupportLanguages.toHashSet()와 같이 정의했으나...
mutableSupportLanguages가 EnumSet 이므로, 타입 매칭이 되지 않아 컴파일 에러가 발생하더군요. 😂
따라서 mutableSupportLanguages 타입을 Set으로 변경하면 타입 매칭은 되지만, add() 메서드를 사용할 수 없기 때문에 또한 애로사항이 발생합니다.
결국 그렇게 해서 supportLanguages를 정의하여 사용했습니다.

contents

밑에 설명드릴 Content 엔티티를 리스트로 가지고 있는 필드입니다.
외부에는 제공하지 않고 내부에서 Content 엔티티를 영속하기 위한 필드입니다.
해당 필드를 사용하여 addContent() 메서드를 제공하고 있습니다.


Content

News의 상세 내용을 가지는 엔티티 입니다.
News와 N:1 관계를 가지고 있습니다.
sequence, news, newsInformation, language, content를 필드로 가지고 있습니다.

sequence

식별자로 사용되는 필드이고, DB의 자동 증가 컬럼에 의존합니다.
id라는 이름을 사용하지 않고 sequence라는 이름을 사용한 이유는 Content의 식별자를 식별자로 사용하지 않게 의도하기 위해서 입니다.
Content는 News의 생명 주기를 따릅니다. (생성 시점은 다르지만, Content가 생성되려면 News가 필요합니다.)
또한 Content 조회 시 News를 먼저 조회해야 합니다. (어떤 언어의 Content가 제공되는지 알아야 하므로)
sequence를 PK로 사용하지 않고 News의 PK를 Content의 PK로 사용하면 될 것 같지만, N:1 관계이므로 PK의 중복이 발생합니다.
따라서 위와 같은 이유로 식별자를 사용했다고 이해하시면 될 것 같습니다.
Content의 조회는 News의 식별자와 Language를 통하면 될 것 같습니다.

news

News의 참조 관계를 설정하기 위한 필드입니다.
N:1 관계로 매핑되어 있습니다.

newsInformation

title, excerpt를 가지고 있는 값 타입 입니다.
값 타입이기 때문에 불변하며, 가지고 있는 필드는 nullable 합니다.
왜냐하면 excerpt 필드가 패치 노트에서는 null 값으로 주어지더군요... 😂
글을 작성할 때 생략할 수 있는 부분이므로 null 값을 사용했습니다.
title 또한 nullable 합니다.
글에서 title은 필수적인 값이지만, null을 허용하지 않는다면 빈 문자열을 두기 애매하여 nullable 하게 하였습니다.
null을 허용하지 않게 하고, 빈 문자열 대신 "제목이 없습니다." 라는 문자열을 허용하게 할 지 고려가 필요합니다.

language

Content가 어떤 언어로 작성되었는지 알려주는 필드입니다.
추후 조회 기능 제공 시 Language와 News의 식별자로 조회할 때 사용할 목적입니다.


Content 엔티티를 영속하려면 무조건 NewsaddContent() 메서드를 사용해야 합니다.
이것은 검증 규칙과 비즈니스 로직을 강제하기 위함입니다. (중복된 언어, supportLanguage 추가 등)

비즈니스 로직에 대한 명세는 테스트 코드 확인하시면 될 것 같습니다.

질문 사항 있으시면 리뷰로 남겨주세요!

@seokjin8678 seokjin8678 added the 뉴스 뉴스 도메인 label Dec 4, 2023
@seokjin8678 seokjin8678 requested a review from Laeng December 4, 2023 06:12
@seokjin8678 seokjin8678 self-assigned this Dec 4, 2023
Copy link

github-actions bot commented Dec 4, 2023

Test Results

  3 files    3 suites   0s ⏱️
12 tests 12 ✔️ 0 💤 0
13 runs  13 ✔️ 0 💤 0

Results for commit 1ef615c.

♻️ This comment has been updated with latest results.

@seokjin8678 seokjin8678 changed the title feat: NewsEntity 추가 (#2) feat: News 엔티티 추가 (#2) Dec 4, 2023
- addContent 호출 시 Content의 News가 null이면 안 된다.
- 따라서 News.addContent() 메서드는 일반적으로 사용할 수 없다.
- content.initialNews() 메서드로만 News.addContent() 메서드를 호출 할 수 있다.
- News.addContent() 메서드로 Content 영속하도록 변경
Comment on lines +14 to +52
@MappedSuperclass
abstract class PrimaryKeyEntity : Persistable<UUID> {

@Id
@Column(columnDefinition = "uuid")
private val id: UUID = UlidCreator.getMonotonicUlid().toUuid()

@Transient
private var _isNew = true

override fun getId(): UUID = id

override fun isNew(): Boolean = _isNew

override fun equals(other: Any?): Boolean {
if (other == null) {
return false
}
if (other !is HibernateProxy && this::class != other::class) {
return false
}
return id == getIdentifier(other)
}

private fun getIdentifier(obj: Any): Any {
return when (obj) {
is HibernateProxy -> obj.hibernateLazyInitializer.identifier
else -> (obj as PrimaryKeyEntity).id
}
}

override fun hashCode(): Int = Objects.hashCode(id)

@PostPersist
@PostLoad
protected fun load() {
_isNew = false
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 클래스는 식별자를 UUID로 제공하기 위함인데 링크 참고하시면 이해되실 겁니다!

Comment on lines +16 to +23
describe("convertToDatabaseColumn") {
context("요소가 한 개 이면") {
val attribute = EnumSet.of(Language.ENGLISH)

it(",가 없는 문자열을 반환한다.") {
enumSetLanguageConverter.convertToDatabaseColumn(attribute) shouldBe "ENGLISH"
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 프로젝트에서 Describe 패턴을 사용하여 테스트를 작성해볼까 합니다.
Describe 패턴의 설명은 링크 참조하시면 됩니다.
또한, Kotest를 사용하여 테스트 코드를 작성했는데 사용법은 링크 참조해주시면 될 것 같습니다.

Comment on lines +10 to +30
fun from(language: Language, news: News): Content = when (language) {
Language.ENGLISH -> Content(
newsInformation = NewsInformation(
title = "Star Citizen Live",
excerpt = "You asked. We're answering! Join us today for a live Q&A show with the Vehicle Gameplay team."
),
language = language,
content = "blah blah",
news = news,
)

Language.KOREAN -> Content(
newsInformation = NewsInformation(
title = "스타 시티즌 뉴스",
excerpt = "물어보셨죠? 저희가 답해드리겠습니다! 지금 바로 차량 게임플레이 팀과 함께하는 라이브 Q&A 쇼에 참여하세요."
),
language = language,
content = "어쩌구 저쩌구",
news = news,
)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코틀린 문법에 아직 익숙하지는 않지만, 최대한 컨벤션을 맞추려고 하고 있습니다.
해당 링크 참조하시면 좋을 것 같네요.

- lazy 로딩 이슈 해결을 위함
Copy link
Member

@Laeng Laeng left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생 많으셨습니다!!!!!
말씀하신 사항 대로 따라가겠습니다!!!

코멘트 남겨드린 부분 한번 검토해주시면 감사드립니다!

newsType: NewsType,
originId: Long,
publishedAt: LocalDateTime,
) : PrimaryKeyEntity() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

원문의 URL 를 담을 수 있는 컬럼이 있다면 좋을 것 같습니다.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

원문 링크를 함께 제공하여 다음과 같은 효과를 얻을 수 있습니다.

  1. 원문 링크를 함께 게시하여 출처를 명시할 수 있습니다.

  2. 모든 소식의 내용을 모두 번역하여 보여주면 좋겠지만 Iea 2953 와 같은 대규모 프로모션 컨텐츠는 개발에 많은 시간이 소요될 것 입니다. 이런 경우 요약본만 번역하고 원문 링크를 게시하여 원문을 보도록 안내할 수 있습니다.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

한번 검토해주시면 감사드립니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

originUrl 이라는 컬럼을 추가하면 되겠네요!

class News(
newsType: NewsType,
originId: Long,
publishedAt: LocalDateTime,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

publishedAt 의 타입을 Instant 을 사용하는 것을 제안합니다.

  1. 서버 시간과 상관없이 UTC 기준으로 시간이 생성되며, 텍스트에서 parse 할 때 시간대를 설정해줘야 하므로 DB 에 저장된 날짜와 시간에 대한 시간대가 뒤섞이는 것을 방지할 수 있을 것으로 사료됩니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instant 보다는, 더 다양한 사용을 위해 ZonedDateTime이 좋을 것 같네요!

{
    "id": 19478,
    "publish_start": "2023-09-15 19:00:04",
    "time_created": "2023-09-15 18:32:53",
    "excerpt": "You asked. We're answering! Join us today for a live Q&A show with the Vehicle Gameplay team.",
    "title": "Star Citizen Live ",
    "url": "https://robertsspaceindustries.com/comm-link/transmission/19478-Star-Citizen-Live"
}
  • RSI에서 제공하는 API 포맷인데, 여기선 시간대가 보이지 않는데 이 경우에는 어떻게 처리하는게 좋을까요?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일단 GMT 나 UTC 로 지정해주시면 됩니다!

@seokjin8678 seokjin8678 requested a review from Laeng December 8, 2023 08:04
@Laeng Laeng merged commit 76666c7 into dev Dec 8, 2023
3 checks passed
@seokjin8678 seokjin8678 added the 💎 핵심기능 핵심 기능에 관한 작업 label Dec 12, 2023
@seokjin8678 seokjin8678 deleted the feat/#1 branch December 12, 2023 14:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
뉴스 뉴스 도메인 💎 핵심기능 핵심 기능에 관한 작업
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feat: News 엔티티를 정의한다.
2 participants