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: 장소 좋아요 기능 도메인 설계 및 뼈대코드 작성 #344

Merged
merged 9 commits into from
Oct 2, 2023
95 changes: 95 additions & 0 deletions backend/src/main/java/com/now/naaga/like/domain/PlaceLike.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.now.naaga.like.domain;

import com.now.naaga.common.domain.BaseEntity;
import com.now.naaga.place.domain.Place;
import com.now.naaga.player.domain.Player;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import java.util.Objects;

@Entity
public class PlaceLike extends BaseEntity {

@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
private Long id;

@ManyToOne
@JoinColumn(name = "place_id")
private Place place;

@ManyToOne
@JoinColumn(name = "player_id")
private Player player;

@Enumerated(EnumType.STRING)
private PlaceLikeType placeLikeType;

protected PlaceLike() {
}

public PlaceLike(final Place place,
final Player player,
final PlaceLikeType placeLikeType) {
this(null, place, player, placeLikeType);
}

public PlaceLike(final Long id,
final Place place,
final Player player,
final PlaceLikeType placeLikeType) {
this.id = id;
this.place = place;
this.player = player;
this.placeLikeType = placeLikeType;
}

public Long getId() {
return id;
}

public Place getPlace() {
return place;
}

public Player getPlayer() {
return player;
}

public PlaceLikeType getType() {
return placeLikeType;
}

@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final PlaceLike placeLike = (PlaceLike) o;
return Objects.equals(id, placeLike.id);
}

@Override
public int hashCode() {
return Objects.hash(id);
}

@Override
public String toString() {
return "PlaceLike{" +
"id=" + id +
", placeId=" + place.getId() +
", playerId=" + player.getId() +
", placeLikeType=" + placeLikeType +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.now.naaga.like.domain;

public enum PlaceLikeType {

LIKE,
DISLIKE,
;
Comment on lines +3 to +7
Copy link
Collaborator

Choose a reason for hiding this comment

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

이넘 굿이에요~!

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.now.naaga.like.repository;

import com.now.naaga.like.domain.PlaceLike;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PlaceLikeRepository extends JpaRepository<PlaceLike, Long> {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.now.naaga.placestatistics;

import com.now.naaga.common.domain.BaseEntity;
import com.now.naaga.place.domain.Place;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import java.util.Objects;

@Entity
public class PlaceStatistics extends BaseEntity {

@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
private Long id;

@OneToOne
@JoinColumn(name = "place_id")
private Place place;

private Long likeCount;

Comment on lines +20 to +25
Copy link
Collaborator

Choose a reason for hiding this comment

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

likeCount를 place 테이블에 두지 않고 따로 테이블을 뺀 이유가 궁금합니다!

Copy link
Collaborator

Choose a reason for hiding this comment

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

역정규화로 집계를 나타내는 컬럼이 빠졌다는 것은 위에서 설명이 되어있으니 왜 테이블로 뒀는지 설명드리겠습니다!!

2가지 관점에서 볼 수 있을 것 같은데요.

  1. Place 테이블에 좋아요 집계까지 둔다면, Place 테이블의 수정이 좋아요가 한번 눌릴 때마다 일어나는데요. 이러면 각종 수정에서 조건문등으로 인해서 테이블의 락이 발생할 수 있을 것으로 우려되어 집계를 나타내는 테이블을 분리하였습니다
  2. 현재로서는 place의 통계를 나타내는 컬럼이 likeCount만 있습니다.
    하지만 통계를 나타내는 컬럼이 많아질 수록 순수하게 Place 정보를 나타내는 테이블과 통계를 나타내는 테이블로 분리를 할 수 있으므로 조금 더 확장성 있다고 볼 수 있을 것 같습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

특정 장소에 대한 좋아요 요청이 오면,

  1. placeId로 장소를 찾고
  2. placeId와 좋아요를 누른 플레이어의 id로 playerlike 객체를 만들고 저장
  3. placeId로 적합한 placestatistics를 조회한 후, 개수 update를 하는 흐름이 맞나요?

만약 위의 흐름대로 진행된다면
PlaceStatistics 테이블의 좋아요 수 객체는 PlaceLike테이블과 논리적인 연관관계는 있지만 직접적인 연관관계는 존재하지 않네요.
PlaceStatiscs의 좋아요 개수와 PlaceLike에 저장된 실제 좋아요 개수에 차이가 날 수도 있을까요?
= 데이터 정합성의 문제가 발생할 수도 있을까요?

또,
placeLike에서 placeId를 인덱스로 달면, 쉽게 count를 조회해 올 수도 있을 것 같은데요.
이러한 문제들로 PlaceStatistics 테이블이 없다면, 장소 통계에서 좋아요 개수를 조회해올 때,
placeStatisticsService.findPlaceStatisticsByPlaceId()라는 메서드가
내부적으로 PlaceLikeService.findPlaceLikeCountByPlaceId()라는 메서드를 사용할 것 같은데요.
여기서 성능적인 문제가 발생하나요?

정리하자면

  1. 정합성 문제가 발생할 가능성이 있는지? (PlaceLike와 PlaceStatistics의 likeCount는 아무런 프로그래밍적인 관계가 없다)
  2. PlaceStatistics 테이블을 없앴을 때, 발생하는 성능문제가 뭐가 있는지?

가 궁금합니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

1번

트랜잭션을 잘 짜놓지 않으면 정합성 문제가 발생할 수 있습니다.

2번

PlaceStatistics를 제거하면 PlaceLike의 FK만으로는 인덱싱이 불가합니다.
PlaceLike에는 싫어요 타입도 담기기 떄문에
(FK, Type)으로 인덱스가 걸릴테고 Type은 일단 좋아요 싫어요 두 상태가 골고루 퍼져있는 값입니다. 인덱싱의 효율이 안나오는것으로 알고있어요.(확실하지 않음 리마큐참고)
그리고 원래는 없어도 될 인덱스 때문에 쓰기 성능도 저하될 수 있겠죠? 좋아요는 쓰기가 매우 빈번하게 일어날 수 있는 작업입니다
스태티스틱스를 하나올리는 작업이 더 간결할 것 같습니다.

protected PlaceStatistics() {

}

public PlaceStatistics(final Place place,
final Long likeCount) {
this(null, place, likeCount);
}


public PlaceStatistics(final Long id,
final Place place,
final Long likeCount) {
this.id = id;
this.place = place;
this.likeCount = likeCount;
}

public Long getId() {
return id;
}

public Place getPlace() {
return place;
}

public Long getLikeCount() {
return likeCount;
}

@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final PlaceStatistics that = (PlaceStatistics) o;
return Objects.equals(id, that.id);
}

@Override
public int hashCode() {
return Objects.hash(id);
}

@Override
public String toString() {
return "PlaceStatistics{" +
"id=" + id +
", placeId=" + place.getId() +
", likeCount=" + likeCount +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.now.naaga.placestatistics.repository;

import com.now.naaga.placestatistics.PlaceStatistics;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PlaceStatisticsRepository extends JpaRepository<PlaceStatistics, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.now.naaga.common.builder;

import com.now.naaga.like.domain.PlaceLike;
import com.now.naaga.like.domain.PlaceLikeType;
import com.now.naaga.like.repository.PlaceLikeRepository;
import com.now.naaga.place.domain.Place;
import com.now.naaga.player.domain.Player;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class PlaceLikeBuilder {

@Autowired
private PlaceLikeRepository placeLikeRepository;
Comment on lines +13 to +16
Copy link
Collaborator

Choose a reason for hiding this comment

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

코코닥의 명작..!


@Autowired
private PlaceBuilder placeBuilder;

@Autowired
private PlayerBuilder playerBuilder;

private PlaceLikeType placeLikeType;

private Optional<Place> place;

private Optional<Player> player;

public PlaceLikeBuilder init() {
this.placeLikeType = PlaceLikeType.LIKE;
this.place = Optional.empty();
this.player = Optional.empty();
return this;
}

public PlaceLikeBuilder placeLikeType(final PlaceLikeType placeLikeType) {
this.placeLikeType = placeLikeType;
return this;
}

public PlaceLikeBuilder place(final Place persistedPlace) {
this.place = Optional.ofNullable(persistedPlace);
return this;
}

public PlaceLikeBuilder player(final Player persistedPlayer) {
this.player = Optional.ofNullable(persistedPlayer);
return this;
}

public PlaceLike build() {
final Place persistePlace = place.orElseGet(this::getPersistedPlace);
final Player persistedPlayer = player.orElseGet(this::getPersistedPlayer);
final PlaceLike placeLike = new PlaceLike(persistePlace, persistedPlayer, placeLikeType);
return placeLikeRepository.save(placeLike);
}

private Player getPersistedPlayer() {
return playerBuilder.init()
.build();
}

private Place getPersistedPlace() {
return placeBuilder.init()
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.now.naaga.common.builder;

import com.now.naaga.place.domain.Place;
import com.now.naaga.placestatistics.PlaceStatistics;
import com.now.naaga.placestatistics.repository.PlaceStatisticsRepository;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class PlaceStatisticsBuilder {

@Autowired
private PlaceStatisticsRepository placeStatisticsRepository;

@Autowired
private PlaceBuilder placeBuilder;

private Long likeCount;

private Optional<Place> place;

public PlaceStatisticsBuilder init() {
this.place = Optional.empty();
this.likeCount = 0L;
return this;
}

public PlaceStatisticsBuilder likeCount(final Long likeCount) {
this.likeCount = likeCount;
return this;
}

public PlaceStatisticsBuilder place(final Place persistedPlace) {
this.place = Optional.ofNullable(persistedPlace);
return this;
}

public PlaceStatistics build() {
final Place persistePlace = place.orElseGet(this::getPersistedPlace);
final PlaceStatistics placeStatistics = new PlaceStatistics(persistePlace, likeCount);
return placeStatisticsRepository.save(placeStatistics);
}

private Place getPersistedPlace() {
return placeBuilder.init()
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.now.naaga.common.fixture;

import com.now.naaga.like.domain.PlaceLike;
import com.now.naaga.like.domain.PlaceLikeType;

public class PlaceLikeFixture {

public static PlaceLike PLACE_LIKE() {
return new PlaceLike(PlaceFixture.PLACE(), PlayerFixture.PLAYER(), PlaceLikeType.LIKE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.now.naaga.common.fixture;

import com.now.naaga.placestatistics.PlaceStatistics;

public class PlaceStatisticsFixture {

public static PlaceStatistics PLACE_STATISTICS() {
return new PlaceStatistics(PlaceFixture.PLACE(), 0L);
}
}
Loading