소셜 네트워크 그래프 스키마 설계
Neo4j와 Spring Boot로 소셜 네트워크 만들기
소셜 네트워크 그래프 스키마 설계
코드를 작성하기 전에 그래프 스키마를 신중하게 설계해야 한다. 소셜 네트워크는 근본적으로 연결에 관한 것이다—사용자가 다른 사용자와 연결되고, 콘텐츠를 만들고, 그 콘텐츠와 상호작용한다. 이것은 그래프 구조에 자연스럽게 매핑된다.
1. 핵심 엔티티
최소한의 소셜 네트워크에는 세 가지 노드 유형이 필요하다:
@Node
public class User {
@Id
@GeneratedValue
private Long id;
private String username;
private String email;
private String displayName;
private String bio;
private UserStatus status;
private LocalDateTime createdAt;
public enum UserStatus {
ACTIVE, SUSPENDED, DELETED
}
}@Node
public class Post {
@Id
@GeneratedValue
private Long id;
private String content;
private Visibility visibility;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public enum Visibility {
PUBLIC, FRIENDS_ONLY, PRIVATE
}
}세 번째 엔티티인 사용자 간의 관계가 그래프 데이터베이스와 관계형 데이터베이스의 차이점이다. 조인 테이블 대신 관계가 일급 시민(first-class citizen)이다.
2. 관계 모델링
소셜 네트워크에는 두 가지 주요 관계 패턴이 있다: 대칭(친구)과 비대칭(팔로우).
비대칭: 팔로우
@Node
public class User {
// ...
@Relationship(type = "FOLLOWS", direction = Direction.OUTGOING)
private Set<User> following = new HashSet<>();
@Relationship(type = "FOLLOWS", direction = Direction.INCOMING)
private Set<User> followers = new HashSet<>();
}앨리스가 밥을 팔로우하면 앨리스에서 밥으로 향하는 단일 FOLLOWS 엣지가 생긴다. 방향이 중요하다—앨리스는 밥의 게시물을 볼 수 있지만, 밥은 자동으로 앨리스의 게시물을 보지 못한다.
대칭: 친구
친구 관계는 더 복잡하다. 두 가지 옵션이 있다:
옵션 A: 두 개의 방향성 엣지 (쿼리가 간단, 저장 공간 더 필요)
(alice)-[:FRIENDS_WITH]->(bob)
(bob)-[:FRIENDS_WITH]->(alice)옵션 B: 상태가 있는 하나의 무방향 엣지 (저장 공간 절약, 쿼리가 약간 복잡)
(alice)-[:FRIENDS_WITH {status: 'ACCEPTED', since: datetime()}]->(bob)옵션 B가 프로덕션에서 더 일반적이다. 관계 프로퍼티가 상태를 추적한다:
@RelationshipProperties
public class Friendship {
@Id
@GeneratedValue
private Long id;
@TargetNode
private User friend;
private FriendshipStatus status;
private LocalDateTime since;
private LocalDateTime requestedAt;
public enum FriendshipStatus {
PENDING, ACCEPTED, REJECTED, BLOCKED
}
}3. 콘텐츠 관계
사용자가 콘텐츠를 만든다. 이것은 단순한 발신 관계다:
@Node
public class User {
// ...
@Relationship(type = "AUTHORED", direction = Direction.OUTGOING)
private List<Post> posts = new ArrayList<>();
}좋아요 같은 상호작용의 경우, 관계를 사용할지 노드를 사용할지 선택해야 한다.
관계 접근 방식 (더 단순, 기본 사용에 권장):
(user)-[:LIKED {at: datetime()}]->(post)노드 접근 방식 (좋아요에 자체 프로퍼티나 쿼리가 필요할 때):
(user)-[:CREATED]->(like:Like)-[:ON]->(post)대부분의 소셜 네트워크에서는 관계 접근 방식으로 충분하다. 자체 생명주기가 필요한 복잡한 상호작용에만 노드를 사용한다.
4. 인덱싱 전략
인덱스가 없으면 Neo4j는 모든 노드를 스캔한다. 소셜 네트워크에서 필수적인 인덱스:
@Node
public class User {
@Id
@GeneratedValue
private Long id; // 자동 인덱싱
// 스키마를 통해 유니크 제약 추가
}Neo4j에서 인덱스 생성:
CREATE CONSTRAINT user_username IF NOT EXISTS
FOR (u:User) REQUIRE u.username IS UNIQUE;
CREATE CONSTRAINT user_email IF NOT EXISTS
FOR (u:User) REQUIRE u.email IS UNIQUE;
CREATE INDEX user_status IF NOT EXISTS
FOR (u:User) ON (u.status);
CREATE INDEX post_created IF NOT EXISTS
FOR (p:Post) ON (p.createdAt);
CREATE INDEX post_visibility IF NOT EXISTS
FOR (p:Post) ON (p.visibility);유니크 제약 조건은 자동으로 인덱스를 생성한다. 복합 인덱스는 일반적인 쿼리 패턴에 도움이 된다:
CREATE INDEX post_visibility_created IF NOT EXISTS
FOR (p:Post) ON (p.visibility, p.createdAt);5. Spring Data Neo4j 설정
엔티티 스캔과 트랜잭션 관리를 설정한다:
@Configuration
@EnableNeo4jRepositories(basePackages = "com.example.social.repository")
@EnableTransactionManagement
public class Neo4jConfig {
@Bean
public Neo4jTransactionManager transactionManager(Driver driver,
DatabaseSelectionProvider databaseSelection) {
return new Neo4jTransactionManager(driver, databaseSelection);
}
}6. Repository 계층
Repository는 Neo4jRepository를 확장하고 커스텀 쿼리를 추가한다:
public interface UserRepository extends Neo4jRepository<User, Long> {
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
@Query("""
MATCH (u:User {username: $username})
OPTIONAL MATCH (u)-[:AUTHORED]->(p:Post)
OPTIONAL MATCH (u)-[:FOLLOWS]->(following:User)
OPTIONAL MATCH (u)<-[:FOLLOWS]-(follower:User)
RETURN u,
count(DISTINCT p) as postCount,
count(DISTINCT following) as followingCount,
count(DISTINCT follower) as followerCount
""")
UserStats findUserWithStats(String username);
}public interface PostRepository extends Neo4jRepository<Post, Long> {
@Query("""
MATCH (author:User {username: $username})-[:AUTHORED]->(p:Post)
RETURN p, author
ORDER BY p.createdAt DESC
SKIP $skip LIMIT $limit
""")
List<Post> findByAuthor(String username, int skip, int limit);
}7. 스키마 시각화
전체 스키마는 다음과 같다:
┌──────────┐
│ User │
└────┬─────┘
│
┌────────┼────────┬──────────┐
│ │ │ │
▼ ▼ ▼ ▼
FOLLOWS FRIENDS AUTHORED LIKED
│ WITH │ │
│ │ ▼ │
│ │ ┌──────┐ │
└────────┴───│ Post │◄──────┘
└──────┘
이 스키마는 핵심 소셜 네트워크 기능을 처리한다. Comment, Interest, Location 같은 추가 노드는 필요에 따라 동일한 패턴을 따라 추가할 수 있다.
8. 결론
소셜 네트워크의 그래프 스키마 설계는 관계를 올바르게 모델링하는 것이 핵심이다. 비대칭 관계(팔로우)에는 방향성 엣지를 사용하고, 상태가 있는 대칭 관계(친구)에는 관계 프로퍼티를 사용한다. WHERE 절과 ORDER BY에서 사용되는 필드에 인덱스를 충분히 생성한다. 여기서 확립된 스키마는 다음 포스트에서 친구 요청, 피드, 추천을 구현하기 위한 기반이 된다.