[SpringBoot] 2024 게시판 만들기 실습 ④ - Service, Repository

SihoonCho·2024년 11월 21일
0
post-thumbnail

※ 읽기에 앞서


본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 과장되거나 생략된 부분이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.


📌 패키지 구조


├── src
│   ├── main
│   │   ├── java
│   │   │   └── com.companyname.projectname
│   │   │       ├── post
│   │   │       │   ├── dto
│   │   │       │   │   ├── PostCreateRequestDto.java
│   │   │       │   │   ├── PostUpdateRequestDto.java
│   │   │       │   │   ├── PostDetailResponseDto.java
│   │   │       │   │   └── PostListResponseDto.java
│   │   │       │   ├── model
│   │   │       │   │   └── Post.java
│   │   │       │   ├── repository					// 패키지 추가
│   │   │       │   │   └── PostRepository.java			// 인터페이스 추가
│   │   │       │   └── service						// 패키지 추가
│   │   │       │       ├── PostReadService.java			// 인터페이스 추가
│   │   │       │       ├── PostReadServiceImpl.java		// 클래스 추가
│   │   │       │       ├── PostWriteService.java		// 인터페이스 추가
│   │   │       │       └── PostWriteServiceImpl.java	// 클래스 추가
│   │   │       └── ProjectNameApplication.java
│   │   └── resources
│   └── test

📌 Repository


package com.companyname.projectname.post.repository;

import com.companyname.projectname.post.model.Post;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
    // 1. 이름 기반 메서드 (쿼리 메서드)

    // 기본 메서드
    List<Post> findByTitle(String title);
    List<Post> findByContent(String content);
    List<Post> findByTitleContaining(String title);
    List<Post> findByContentContaining(String content);
    List<Post> findByTitleContainingIgnoreCase(String title);
    List<Post> findByContentContainingIgnoreCase(String content);

    // 복합 메서드
    List<Post> findByTitleAndContent(String title, String content);
    List<Post> findByTitleContainingOrContentContaining(String title, String content);
    List<Post> findByTitleContainingAndContentContaining(String title, String content);
    List<Post> findByTitleContainingIgnoreCaseOrContentContainingIgnoreCase(String title, String content);
    List<Post> findByTitleContainingIgnoreCaseAndContentContainingIgnoreCase(String title, String content);

    // 날짜 기반 메서드 (생성 일자 기준)
    List<Post> findByCreatedDateAfter(LocalDateTime date);
    List<Post> findByCreatedDateBefore(LocalDateTime date);
    List<Post> findByCreatedDateBetween(LocalDateTime sDate, LocalDateTime eDate);

    // 날짜 기반 메서드 (수정 일자 기준)
    List<Post> findByUpdatedDateAfter(LocalDateTime date);
    List<Post> findByUpdatedDateBefore(LocalDateTime date);
    List<Post> findByUpdatedDateBetween(LocalDateTime sDate, LocalDateTime eDate);
}
  • JPAEntity 클래스의 필드명을 기준으로, 메서드명을 통해 JPA 원시 쿼리를 자동으로 생성
  • 필요에 따라 메서드를 추가하되, 반드시 메서드명, 매개변수명에 주의할 것

📌 Service


확장성과 유지보수성을 고려하여 Interface로 구현하였으며,
@Transactional 조회 성능 최적화를 위해 읽기, 쓰기 작업을 분리


📖 PostReadService


package com.companyname.projectname.post.service;

import com.companyname.projectname.post.dto.PostDetailResponseDto;
import com.companyname.projectname.post.dto.PostListResponseDto;

import java.time.LocalDateTime;
import java.util.List;

public interface PostReadService {
    PostDetailResponseDto findById(Long id);
    List<PostListResponseDto> findAllLists();
    List<PostDetailResponseDto> findAllDetails();

	// 예시, 필요한 경우 작성
    List<PostListResponseDto> findByTitle(String title);
    List<PostListResponseDto> findByContent(String content);
    List<PostListResponseDto> findByTitleContaining(String title);
    List<PostListResponseDto> findByContentContaining(String content);
    List<PostListResponseDto> findByTitleContainingIgnoreCase(String title);
    List<PostListResponseDto> findByContentContainingIgnoreCase(String content);

    List<PostListResponseDto> findByTitleAndContent(String title, String content);
    List<PostListResponseDto> findByTitleContainingOrContentContaining(String title, String content);
    List<PostListResponseDto> findByTitleContainingAndContentContaining(String title, String content);
    List<PostListResponseDto> findByTitleContainingIgnoreCaseOrContentContainingIgnoreCase(String title, String content);
    List<PostListResponseDto> findByTitleContainingIgnoreCaseAndContentContainingIgnoreCase(String title, String content);

    List<PostListResponseDto> findByCreatedDateAfter(LocalDateTime date);
    List<PostListResponseDto> findByCreatedDateBefore(LocalDateTime date);
    List<PostListResponseDto> findByCreatedDateBetween(LocalDateTime sDate, LocalDateTime eDate);

    List<PostListResponseDto> findByUpdatedDateAfter(LocalDateTime date);
    List<PostListResponseDto> findByUpdatedDateBefore(LocalDateTime date);
    List<PostListResponseDto> findByUpdatedDateBetween(LocalDateTime sDate, LocalDateTime eDate);
}
  • Interface
    • 클래스 간 공통된 메서드의 규격을 정의
    • 다형성을 활용하여 유연하고 확장 가능한 코드 설계 가능
    • 메서드 선언부만 존재하며, 구현부는 구현 클래스에서 정의
    • 다중 상속을 지원하여 다양한 인터페이스를 한 클래스에서 구현 가능
  • Interface 주의할 점
    • 구현 클래스에서 반드시 메서드를 @Override해야 함
    • 다중 상속 시 중복된 메서드가 존재하는 경우 충돌에 주의
    • 구현해야할 기능의 정의는 결정되었으나, 로직은 결정되지 않은 경우 사용
    • 필요 이상으로 많은 메서드를 정의하면 구현 클래스의 복잡도를 증가시킬 수 있음

📖 PostReadServiceImpl


package com.companyname.projectname.post.service;

import com.companyname.projectname.post.dto.PostDetailResponseDto;
import com.companyname.projectname.post.dto.PostListResponseDto;
import com.companyname.projectname.post.model.Post;
import com.companyname.projectname.post.repository.PostRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PostReadServiceImpl implements PostReadService {

    private final PostRepository postRepository;

    @Override
    public PostDetailResponseDto findById(Long id) {
        Post entity = postRepository.findById(id).orElseThrow(
                () -> new IllegalArgumentException("Post not found with id: " + id));
        return new PostDetailResponseDto(entity);
    }

    @Override
    public List<PostListResponseDto> findAllLists() {
        return postRepository.findAll().stream().map(PostListResponseDto::new).toList();
    }

    @Override
    public List<PostDetailResponseDto> findAllDetails() {
        return postRepository.findAll().stream().map(PostDetailResponseDto::new).toList();
    }

    @Override
    public List<PostListResponseDto> findByTitle(String title) {
        return postRepository.findByTitle(title).stream().map(PostListResponseDto::new).toList();
    }

    @Override
    public List<PostListResponseDto> findByContent(String content) {
        return postRepository.findByContent(content).stream().map(PostListResponseDto::new).toList();
    }
    
    // ... 중략 ...
}
  • @Transactional(readOnly = true)
    • Spring 제공 어노테이션, 조회 전용 메서드에 설정하여 성능 최적화를 도모
      쓰기 작업을 막고 읽기 전용으로 동작, 일부 데이터베이스에서 캐시를 활용하도록 도움
    • 트랜잭션 범위를 설정하고, 데이터베이스 작업의 일관성과 원자성을 보장
    • 데이터 수정/삭제/저장 작업에서 예외 발생 시, 롤백을 자동으로 처리
      • 클래스 레벨에 선언하면 해당 클래스의 모든 메서드에 트랜잭션 속성 적용
      • 메서드 레벨에 선언하면 해당 메서드에만 트랜잭션 속성 적용
      • 클래스 레벨과 메서드 레벨에 모두 선언하면
        메서드 레벨 설정이 클래스 레벨 설정을 재정의
  • @RequiredArgsConstructor
    • 필드 주입 대신 생성자 주입으로 PostRepository 자동으로 의존성 주입
    • Spring DI(Dependency Injection) 컨테이너에서 안전하게 의존성 관리
    • 선언된 final 필드는 생성자에서 자동으로 초기화
  • IllegalArgumentException
    • 데이터가 없을 경우, orElseThrow()를 사용하여 명시적으로 예외를 던짐
    • 추후 별도의 예외 클래스를 생성하여 더 구체적인 예외 처리가 가능하도록 변경

📖 PostWriteService


package com.companyname.projectname.post.service;

import com.companyname.projectname.post.dto.PostCreateRequestDto;
import com.companyname.projectname.post.dto.PostDetailResponseDto;
import com.companyname.projectname.post.dto.PostUpdateRequestDto;

public interface PostWriteService {
    PostDetailResponseDto create(PostCreateRequestDto requestDto);
    PostDetailResponseDto update(PostUpdateRequestDto requestDto);
    void delete(Long id);
}
  • Interface
    • 클래스 간 공통된 메서드의 규격을 정의
    • 다형성을 활용하여 유연하고 확장 가능한 코드 설계 가능
    • 메서드 선언부만 존재하며, 구현부는 구현 클래스에서 정의
    • 다중 상속을 지원하여 다양한 인터페이스를 한 클래스에서 구현 가능
  • Interface 주의할 점
    • 구현 클래스에서 반드시 메서드를 @Override해야 함
    • 다중 상속 시 중복된 메서드가 존재하는 경우 충돌에 주의
    • 구현해야할 기능의 정의는 결정되었으나, 로직은 결정되지 않은 경우 사용
    • 필요 이상으로 많은 메서드를 정의하면 구현 클래스의 복잡도를 증가시킬 수 있음

📖 PostWriteServiceImpl


package com.companyname.projectname.post.service;

import com.companyname.projectname.post.dto.PostCreateRequestDto;
import com.companyname.projectname.post.dto.PostDetailResponseDto;
import com.companyname.projectname.post.dto.PostUpdateRequestDto;
import com.companyname.projectname.post.model.Post;
import com.companyname.projectname.post.repository.PostRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class PostWriteServiceImpl implements PostWriteService {

    private final PostRepository postRepository;

    @Override
    public PostDetailResponseDto create(PostCreateRequestDto requestDto) {
        return new PostDetailResponseDto(postRepository.save(requestDto.toEntity()));
    }

    @Override
    public PostDetailResponseDto update(PostUpdateRequestDto requestDto) {
        Post entity = postRepository.findById(requestDto.getId()).orElseThrow(
                () -> new IllegalArgumentException("Post not found with id: " + requestDto.getId()));
        return new PostDetailResponseDto(postRepository.save(entity.update(requestDto)));
    }

    @Override
    public void delete(Long id) {
        postRepository.deleteById(id);
    }
}
  • @Transactional
    • Spring 제공 어노테이션
    • 트랜잭션 범위를 설정하고, 데이터베이스 작업의 일관성과 원자성을 보장
    • 데이터 수정/삭제/저장 작업에서 예외 발생 시, 롤백을 자동으로 처리
      • 클래스 레벨에 선언하면 해당 클래스의 모든 메서드에 트랜잭션 속성 적용
      • 메서드 레벨에 선언하면 해당 메서드에만 트랜잭션 속성 적용
      • 클래스 레벨과 메서드 레벨에 모두 선언하면
        메서드 레벨 설정이 클래스 레벨 설정을 재정의
  • @RequiredArgsConstructor
    • 필드 주입 대신 생성자 주입으로 PostRepository 자동으로 의존성 주입
    • Spring DI(Dependency Injection) 컨테이너에서 안전하게 의존성 관리
    • 선언된 final 필드는 생성자에서 자동으로 초기화
  • IllegalArgumentException
    • 데이터가 없을 경우, orElseThrow()를 사용하여 명시적으로 예외를 던짐
    • 추후 별도의 예외 클래스를 생성하여 더 구체적인 예외 처리가 가능하도록 변경


본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 제공하고자 작성되었습니다.
실습 중심의 이해를 목표로 작성되었기 때문에, 다소 과장되거나 생략된 부분이 있을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 유의하시고 양해 부탁드립니다.

또한, Spring Boot 기반의 Backend 개발에 중점을 두고 설명하고 있으므로,
Frontend와 관련된 내용은 별도의 참고자료를 검색/활용하실 것을 권장드립니다.
profile
개발을 즐길 줄 아는 백엔드 개발자

0개의 댓글