Spring Bootで簡易SNSを作成してみた

# はじめに
プログラミングを学び始めた頃、RailsやReactを使って簡易SNSを作成した経験がありました。
今回は、その経験を踏まえ、現場で使用している Java と Spring Boot を使ってSNSをブラッシュアップし、改めて学び直すことを目標にしました。

また、学んだ内容を記事にまとめることで、これからSpring Bootを学ぶ人にも役立つ情報を提供したいと考えています。

# 開発環境と技術スタック
#### 以下の環境と技術を使用しました：

開発環境: IntelliJ
言語: Java 17
フレームワーク: Spring Boot 3.1
テンプレートエンジン: Thymeleaf
データベース: PostgreSQL
セキュリティ: Spring Security

# 機能の設計
各機能の概要とデータベース設計
今回実装する主な機能は以下の3つです。

1.認証機能:
Spring Securityを使ったログイン/ログアウト機能。
ログイン後はユーザー固有のタイムラインを表示。

2.フォロー機能:
ユーザー同士のフォロー関係を管理。
フォロー/フォロワーのリストを確認。

3.タイムライン機能:
フォローしているユーザーの投稿を時系列で表示。

# データベース設計
以下は、主要な3つのテーブル構造です

users: ユーザー情報を管理します。
follows: ユーザー間のフォロー関係を管理します。
posts: 投稿情報を管理します。

sql:
```
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE follows (
    follower_id BIGINT NOT NULL REFERENCES users(id),
    followee_id BIGINT NOT NULL REFERENCES users(id),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (follower_id, followee_id)
);

CREATE TABLE posts (
    id SERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL REFERENCES users(id),
    content TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
# 主要な実装ポイント
### Spring Securityを使った認証
Spring Securityを使って認証機能を実装しました。
SecurityConfig クラスでセキュリティ設定を行います。

```
package com.example.sns.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // CSRFを無効化
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/login", "/css/**", "/js/**").permitAll() // ログインページは許可
                .anyRequest().authenticated() // その他のリクエストは認証が必要
            )
            .formLogin(form -> form
                .loginPage("/login") // ログインページ
                .defaultSuccessUrl("/home", true) // ログイン後のリダイレクト先
                .permitAll()
            )
            .logout(logout -> logout.permitAll()); // ログアウトを許可
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
```
### タイムラインのSQLクエリ
タイムラインは、フォローしているユーザーの投稿を取得する機能です。
以下のSQLクエリを利用してデータを取得します。

```
SELECT p
FROM posts p
JOIN follows f ON p.user_id = f.followee_id
WHERE f.follower_id = :currentUserId
ORDER BY p.created_at DESC;
```

#### リポジトリ実装
PostRepository にカスタムクエリを定義します。
```
package com.example.sns.repository;

import com.example.sns.model.Post;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface PostRepository extends JpaRepository<Post, Long> {

    @Query("SELECT p FROM Post p WHERE p.user.id IN " +
           "(SELECT f.followee.id FROM Follow f WHERE f.follower.id = :userId) " +
           "ORDER BY p.createdAt DESC")
    List<Post> findTimelineByUserId(Long userId);
}
```
### フォロー機能のロジック
フォロー機能は、ユーザー間のフォロー関係を管理するシンプルな実装です。
```
package com.example.sns.controller;

import com.example.sns.model.Follow;
import com.example.sns.model.User;
import com.example.sns.repository.FollowRepository;
import com.example.sns.repository.UserRepository;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/follow")
public class FollowController {

    private final FollowRepository followRepository;
    private final UserRepository userRepository;

    public FollowController(FollowRepository followRepository, UserRepository userRepository) {
        this.followRepository = followRepository;
        this.userRepository = userRepository;
    }

    @PostMapping("/{followeeId}")
    public String followUser(@PathVariable Long followeeId, @RequestParam Long followerId) {
        User follower = userRepository.findById(followerId).orElseThrow(() -> new RuntimeException("Follower not found"));
        User followee = userRepository.findById(followeeId).orElseThrow(() -> new RuntimeException("Followee not found"));

        Follow follow = new Follow();
        follow.setFollower(follower);
        follow.setFollowee(followee);
        followRepository.save(follow);

        return "Successfully followed user: " + followee.getUsername();
    }
}
```

# まとめ
#### 学び
- 昔は1ヶ月ほどかかって苦労して作成したアプリケーションが、現場での約2年間の経験や自己学習の成果を活かすことで、今回はスムーズに実装できました。復習しながら構築できたことは、自分の成長を実感できる良い機会でした。
- Spring Bootのシンプルな構成でも、SNSの基本機能を効率よく実装できることが改めて理解できました。
- Spring Securityは、デフォルト機能でログイン画面を提供してくれるため、最初に他の基本的な部分を実装してから後で本格的にログイン画面をカスタマイズして実装することも可能なので、初学者にはおすすめだとも思いました。
#### 改善案
- テストコードを追加して品質を向上
- タイムラインの投稿に画像や動画を添付する機能を追加
- UIをリッチにして、見た目の向上

また今後も自己学習を通してこのアプリケーションをブラッシュアップしていけたらと思います。
最後まで読んでいただき、ありがとうございました！