소셜 네트워크 그래프 스키마 설계 Designing a Social Network Graph Schema
Designing a Social Network Graph Schema
Before writing any code, the graph schema needs careful thought. A social network is fundamentally about connections—users connect to other users, create content, and interact with that content. This maps naturally to a graph structure.
1. Core Entities
A minimal social network needs three node types:
@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
}
}
The third entity—relationships between users—is where graph databases differ from relational ones. Instead of a join table, relationships are first-class citizens.
2. Relationship Modeling
Social networks have two primary relationship patterns: symmetric (friendships) and asymmetric (follows).
Asymmetric: Following
@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<>();
}
When Alice follows Bob, there’s a single FOLLOWS edge from Alice to Bob. The direction matters—Alice can see Bob’s posts, but Bob doesn’t automatically see Alice’s.
Symmetric: Friendship
Friendships are trickier. Two options exist:
Option A: Two directed edges (simpler queries, more storage)
(alice)-[:FRIENDS_WITH]->(bob)
(bob)-[:FRIENDS_WITH]->(alice)
Option B: One undirected edge with status (less storage, slightly complex queries)
(alice)-[:FRIENDS_WITH {status: 'ACCEPTED', since: datetime()}]->(bob)
Option B is more common in production. The relationship properties track state:
@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. Content Relationships
Users create content. This is a simple outgoing relationship:
@Node
public class User {
// ...
@Relationship(type = "AUTHORED", direction = Direction.OUTGOING)
private List<Post> posts = new ArrayList<>();
}
For interactions like likes, the design choice is whether to use a relationship or a node.
Relationship approach (simpler, recommended for basic use):
(user)-[:LIKED {at: datetime()}]->(post)
Node approach (when likes need their own properties or querying):
(user)-[:CREATED]->(like:Like)-[:ON]->(post)
For most social networks, the relationship approach suffices. Reserve nodes for complex interactions that need their own lifecycle.
4. Indexing Strategy
Without indexes, Neo4j scans all nodes. For a social network, these indexes are essential:
@Node
public class User {
@Id
@GeneratedValue
private Long id; // Auto-indexed
// Add unique constraints via schema
}
Create indexes in 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);
Unique constraints automatically create indexes. Composite indexes help for common query patterns:
CREATE INDEX post_visibility_created IF NOT EXISTS
FOR (p:Post) ON (p.visibility, p.createdAt);
5. Spring Data Neo4j Configuration
Configure the entity scan and transaction management:
@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 Layer
Repositories extend Neo4jRepository with custom queries:
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. Schema Visualization
The complete schema looks like this:
┌──────────┐
│ User │
└────┬─────┘
│
┌────────┼────────┬──────────┐
│ │ │ │
▼ ▼ ▼ ▼
FOLLOWS FRIENDS AUTHORED LIKED
│ WITH │ │
│ │ ▼ │
│ │ ┌──────┐ │
└────────┴───│ Post │◄──────┘
└──────┘
This schema handles the core social network features. Additional nodes like Comment, Interest, or Location can be added as needed, following the same patterns.
8. Conclusion
Graph schema design for social networks centers on modeling relationships correctly. Use directed edges for asymmetric relationships (follows), and relationship properties for symmetric ones with state (friendships). Index heavily on fields used in WHERE clauses and ORDER BY. The schema established here forms the foundation for implementing friend requests, feeds, and recommendations in the following posts.
소셜 네트워크 그래프 스키마 설계
코드를 작성하기 전에 그래프 스키마를 신중하게 설계해야 한다. 소셜 네트워크는 근본적으로 연결에 관한 것이다—사용자가 다른 사용자와 연결되고, 콘텐츠를 만들고, 그 콘텐츠와 상호작용한다. 이것은 그래프 구조에 자연스럽게 매핑된다.
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에서 사용되는 필드에 인덱스를 충분히 생성한다. 여기서 확립된 스키마는 다음 포스트에서 친구 요청, 피드, 추천을 구현하기 위한 기반이 된다.
댓글남기기