21.

【Spring】@Serviceアノテーションとは

編集
この記事の要点
  • @Serviceビジネスロジック層を表す Spring アノテーション
  • 機能的には @Component と同等だが意味付けで区別
  • AOP のポイントカットや、レイヤ命名で @Service 専用処理を書ける
  • 型による DI、@Transactional との相性が良い
  • 使うべき場面: コントローラから呼ぶサービスクラス

 

@Service の基本

@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User findById(Long id) {
        return userRepository.findById(id)
            .orElseThrow(() -> new EntityNotFoundException());
    }

    @Transactional
    public User createUser(String name, String email) {
        User user = new User(name, email);
        return userRepository.save(user);
    }
}

@Service vs @Component の違い

機能的には同じ(どちらも Bean 登録)。違いは意味付けだけ:

項目@Service@Component
Bean 登録
意味付け「ビジネスロジック層」「汎用 Bean」
AOP ターゲットポイントカット @within(...@Service) で対象に普通の対象
使う場面サービス層全般ヘルパー・ユーティリティ

結論: ビジネスロジックを書くなら @Service を選ぶ(コード読む人にも意図が伝わる)。

典型的なレイヤ構成

// 1. Controller 層 (Web)
@RestController
@RequestMapping("/api/users")
public class UserController {
    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping
    public UserDto create(@RequestBody @Valid UserCreateRequest req) {
        return UserDto.from(userService.createUser(req.getName(), req.getEmail()));
    }
}

// 2. Service 層 (ビジネスロジック)
@Service
public class UserService {
    private final UserRepository userRepository;
    private final NotificationService notificationService;

    public UserService(UserRepository ur, NotificationService ns) {
        this.userRepository = ur;
        this.notificationService = ns;
    }

    @Transactional
    public User createUser(String name, String email) {
        // バリデーション
        if (userRepository.existsByEmail(email)) {
            throw new BusinessException("メールアドレス重複");
        }

        // 保存
        User user = userRepository.save(new User(name, email));

        // 通知 (別サービス呼び出し)
        notificationService.sendWelcomeEmail(user);

        return user;
    }
}

// 3. Repository 層 (DB アクセス)
@Repository
public interface UserRepository extends JpaRepository {
    Optional findByEmail(String email);
    boolean existsByEmail(String email);
}

@Service と @Transactional

サービス層に @Transactional を付けるのが定石:

@Service
@Transactional(readOnly = true)  // クラスレベルは読み取り専用 (推奨)
public class UserService {

    public User findById(Long id) {
        // readOnly = true (デフォルト)
        return userRepository.findById(id).orElseThrow();
    }

    @Transactional  // 書き込み系はメソッドで上書き
    public User updateUser(Long id, String name) {
        User user = findById(id);
        user.setName(name);
        return user;  // dirty checking で UPDATE
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logActivity(Long userId, String action) {
        // 親トランザクションと別に独立トランザクションで実行
    }

    @Transactional(rollbackFor = Exception.class)  // checked exception でも rollback
    public void riskyOperation() throws IOException {
        // ...
    }
}

@Service のテスト

// 単体テスト (Mockito)
@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void createUser_success() {
        // given
        User saved = new User("Alice", "alice@test.com");
        saved.setId(1L);
        when(userRepository.existsByEmail("alice@test.com")).thenReturn(false);
        when(userRepository.save(any(User.class))).thenReturn(saved);

        // when
        User result = userService.createUser("Alice", "alice@test.com");

        // then
        assertEquals(1L, result.getId());
        verify(userRepository).save(any(User.class));
    }

    @Test
    void createUser_duplicateEmail() {
        when(userRepository.existsByEmail("dup@test.com")).thenReturn(true);

        assertThrows(BusinessException.class, () ->
            userService.createUser("Bob", "dup@test.com")
        );
    }
}

インタフェース + 実装パターン

// インタフェース
public interface UserService {
    User findById(Long id);
    User createUser(String name, String email);
}

// 実装
@Service
public class UserServiceImpl implements UserService {
    private final UserRepository userRepository;

    public UserServiceImpl(UserRepository ur) {
        this.userRepository = ur;
    }

    @Override
    public User findById(Long id) {
        return userRepository.findById(id).orElseThrow();
    }

    @Override
    public User createUser(String name, String email) {
        // ...
    }
}

// メリット:
// - テストでモック化しやすい
// - 実装差し替え可能 (本番 / モック)
// - DI でインタフェース型で受け取れる

// 注: Spring Boot 2+ ではプロキシ生成のため
//     インタフェース不要でも動く (CGLIB proxy)
//     シンプルなら実装クラスだけで OK

命名規則

  • サービス名: UserService / OrderService / EmailService
  • 実装クラス: UserServiceImpl (インタフェース有り) または単に UserService
  • メソッド名: find / create / update / delete (ビジネス操作の動詞)
  • パッケージ: com.example.service 等で分離

関連記事

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. @After
  2. @Autowired
  3. @Bean
  4. @Before
  5. @Column
  6. @Component
  7. @Configuration
  8. @Controller
  9. @Data
  10. @Entity
  11. @GeneratedValue
  12. @Id
  13. @Modifying
  14. @PathVariable
  15. @PropertySource
  16. @Repository
  17. @RequestBody
  18. @RequestMapping
  19. @ResponseBody
  20. @RestController
  21. @Service
  22. @SpringBootApplication
  23. @Table
  24. @Transactional
  25. @Value