Showing

[스프링부트] 머스테치 화면 구성, 게시글 등록과 목록 확인, 수정, 삭제 화면 만들기 본문

JAVA, SPRING/스프링 부트와 AWS로 혼자 구현하는 웹서비스

[스프링부트] 머스테치 화면 구성, 게시글 등록과 목록 확인, 수정, 삭제 화면 만들기

RabbitCode 2023. 6. 3. 16:31

*이동욱 저, 스프링부트와 aws로 혼자 구현하는 웹서비스를 학습하면서 작성한 포스팅입니다.

1. 구현 사항

게시글 등록하면 원래 홈화면으로 돌아감
데이터h2에 로그 잘 남음

2. 전체 조회 화면 만들기

Controller, Service, Repository 코드를 작성하도록 한다.

(1) Repository

기존에 있던 PostsRepository 인터페이스에 쿼리를 추가한다. 

package com.jojo.book.springbootwebservice.domain.posts;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;

public interface PostsRepository extends JpaRepository<Posts, Long>{
    @Query("SELECT p FROM Posts p ORDER BY p.id DESC")
    List<Posts> findAllDesc();
}

SpringDataJpa에서 제공하지 않는 메소드는 위처럼 쿼리로 작성해도 된다.@Query("SELECT p FROM Posts p ORDER BY p.id DESC")

실제로 앞의 코드는 SpringDataJpa에서 제공하는 기본 메소드만으로 해결할 수 있다. 다만 @Query가 훨씬 가독성이 좋으므로 선택해서 사용하면 된다.

 

< 규모가 있는 프로젝트에서의 데이터 조회>

규모가 큰 프로젝트에서 데이터 조회는 FK의 조인, 복잡한 조건 등으로 인해 Entity 클래스만으로는 처리하기 어려워 조회용 프레임워크를 추가로 사용한다. 대표적인 예로 querydsl, jooq, MyBatis 등이 있다. 조회는 3가지 프레임워크 중 하나를 통해 조회하고, 등록/수정/삭제 등은 SpringDataJpa를 통해 진행한다. 저자는 querydsl를 추천한다.

querydsl를 추천하는 이유

1. 타입 안정성 보장 : 단순 문자열로 쿼리를 생성하는 것이 아니라, 메소드 기반으로 쿼리를 생성하므로 오타나 존재하지 않는 컬럼명을 명시할 경우 IDE에서 자동 검출된다. MyBatis에서는 지원하지 않는다.

2. 국내 많은 회사에서 사용중

3. 래퍼런스가 많다.

 

(2) PostsService

    @Transactional
    public List<PostsListResponseDto> findAllDesc(){
        return postsRepository.findAllDesc().stream()
                .map(PostsListResponseDto::new)
                .collect(Collectors.toList());
    }
package com.jojo.book.springbootwebservice.service.posts;
import com.jojo.book.springbootwebservice.domain.posts.Posts;
import com.jojo.book.springbootwebservice.domain.posts.PostsRepository;
import com.jojo.book.springbootwebservice.web.dto.PostsResponseDto;
import com.jojo.book.springbootwebservice.web.dto.PostsSaveRequestDto;
import com.jojo.book.springbootwebservice.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;

@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;

    @Transactional
    public Long save(PostsSaveRequestDto requestDto) {
        return postsRepository.save(requestDto.toEntity()).getId();
    }
    @Transactional
    public Long update(Long id, PostsUpdateRequestDto requestDto){
        Posts posts = postsRepository.findById(id)
                .orElseThrow(()->new IllegalArgumentException("해당 게시글이 없습니다. id="+id));
        posts.update(requestDto.getTitle(), requestDto.getContent());
        return id;
    }
    @Transactional
    public PostsResponseDto findById (Long id){
        Posts entity = postsRepository.findById(id)
                .orElseThrow(()->new IllegalArgumentException("해당 게시글이 없습니다. id="+id));
        return new PostsResponseDto(entity);
    }
    @Transactional
    public List<PostsListResponseDto> findAllDesc(){
        return postsRepository.findAllDesc().stream()
                .map(PostsListResponseDto::new)
                .collect(Collectors.toList());
    }
}

위의 메소드는 Posts 엔티티를 조회하고, 조회된 결과를 PostsListResponseDto로 변환하여 리스트로 반환하는 기능을 수행한다.

@Transactional(readOnly = true) 어노테이션은 해당 메소드를 읽기 전용 트랜잭션으로 설정하며, 데이터베이스의 상태를 변경하지 않는 읽기 작업만 수행한다는 의미이다.

postsRepository.findAllDesc()는 PostsRepository 인터페이스를 통해 데이터베이스에서 모든 Posts 엔티티를 내림차순으로 조회하는 메소드이다. 이 메소드는 데이터베이스와의 상호작용을 통해 실제 데이터를 가져온다.

.stream()은 조회된 Posts 엔티티들을 스트림으로 변환한다. 스트림은 자바 8에서 추가된 기능으로, 데이터 요소의 연속적인 흐름을 나타낸다. 스트림은 컬렉션(Collection)이나 배열 등의 데이터 소스를 처리하기 위한 기능을 제공하는 API로, 스트림을 사용하면 데이터를 다루는 다양한 작업을 편리하게 수행할 수 있다.

.map()은 자바에서 스트림(Stream)을 다룰 때 사용되는 중간 연산자 중 하나이다. .map() 스트림의 요소에 대해 특정 함수를 적용하여 새로운 값을 반환하는 역할을 합니다. 함수는 람다 표현식으로 작성되며, 스트림의 요소를 순회하면서 함수를 적용하고 결과를 새로운 스트림으로 반환합니다

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> multipliedNumbers = numbers.stream()
                                         .map(n -> n * 2)
                                         .collect(Collectors.toList());

System.out.println(multipliedNumbers);
위의 코드는 numbers 리스트의 각 요소에 대해 2를 곱하여 새로운 리스트 multipliedNumbers를 생성
 출력 결과는 `[2, 4, 6, 8, 10]'

.map(PostsListResponseDto::new)는 Posts 엔티티 스트림의 각 요소를 PostsListResponseDto 객체로 변환하는 매핑 작업을 수행한다. PostsListResponseDto::new는 PostsListResponseDto의 생성자를 참조하는 메소드 레퍼런스이다. 

.collect(Collectors.toList())는 스트림의 요소들을 리스트로 수집하는 작업을 수행한다. Collectors.toList()는 자바 8에서 추가된 Collectors 클래스의 정적 메소드로, 스트림의 요소들을 리스트로 수집한다. 이렇게 수집된 리스트가 최종적으로 findAllDesc() 메소드의 반환값이 된다.

따라서 findAllDesc() 메소드는 데이터베이스에서 Posts 엔티티를 조회하여 해당 엔티티들을 PostsListResponseDto로 변환하고, 이를 리스트로 수집하여 반환한다. 이렇게 변환된 PostsListResponseDto 객체들은 클라이언트에게 전달되거나 다른 작업에 활용될 수 있다.

조금 더 자세하게 서술하면 아래와 같다. 

findAllDesec 메소드의 트랜잭션 어노테이션(@Transactional)에 옵션이 하나 추가되었다. 

@Transactional(readOnly = true)

readOnly = true  옵션을 주면 트랜잭션 범위는 유지하되, 조회 기능만 남겨두어 조회 속도가 개선되기 때문에 등록, 수정, 삭제 기능이 전혀 없는 서비스 메소드에서도 사용하는 것을 추천한다. 

    @Transactional
    public List<PostsListResponseDto> findAllDesc(){
        return postsRepository.findAllDesc().stream()
                .map(PostsListResponseDto::new)
                .collect(Collectors.toList());
    }

 

.map(PostsListResponseDto::new) 이것은

.map(posts -> new PostsListResponseDto(posts)) 와 같다.
정리하면, postsRepository 결과로 넘어온 Posts의 Stream을 map을 통해 PostListResponseDto로 변환 -> List로 반환하는 메소드이다.

 

(3) PostsListResponseDto

아직 PostsListResponseDto 클래스가 없기 때문에 이 클래스 역시 생성하도록 한다.

package com.jojo.book.springbootwebservice.web.dto;

import com.jojo.book.springbootwebservice.domain.posts.Posts;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
public class PostsListResponseDto {
    private Long id;
    private String title;
    private String author;
    private LocalDateTime modifiedDate;
    public PostsListResponseDto(Posts entity){
        this.id = entity.getId();
        this.title = entity.getTitle();
        this.author = entity.getAuthor();
        this.modifiedDate = entity.getModifiedDate();
    }
}

(4) IndexController

마지막으로 Controller를 변경하도록 한다.

package com.jojo.book.springbootwebservice.web;

import com.jojo.book.springbootwebservice.service.posts.PostsService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@RequiredArgsConstructor
@Controller
public class indexController {
    private final PostsService postsService;
    @GetMapping("/")
    public String index(Model model){
        model.addAttribute("posts", postsService.findAllDesc());
        return "index";
    }
    @GetMapping("/posts/save")
    public String postsSave() {
        return "posts-save";
    }
}

코드 설명

package com.jojo.book.springbootwebservice.web;

import com.jojo.book.springbootwebservice.service.posts.PostsService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@RequiredArgsConstructor
@Controller
public class indexController {
    private final PostsService postsService;
    @GetMapping("/")
    public String index(Model model){ //Model: 서버 템플릿 엔진에서 사용할 수 있는 객체를 저장할 수 있다. 여기서는 postsService.findAllDesc()로 가져온 결과를 posts로 index.mustache에 전달한다. 
        model.addAttribute("posts", postsService.findAllDesc());
        return "index";
    }
    @GetMapping("/posts/save")
    public String postsSave() {
        return "posts-save";
    }
}

 

controller까지 모두 완성되었으므로 8080에 접속하여 등록 화면을 이용해 데이터를 등록해보고 화면 변화를 살펴보도록 한다.

 

3. 화면 플로우 점검

등록하는 대로 잘 올라간다.

4. 게시글 수정, 삭제 화면 만들기

IndexContrlloer에 다음과 같이 메소드를 추가한다. 

    @GetMapping("/posts/update/{id}")
    public String postsUpdate(@PathVariable Long id, Model model) {
        PostsResponseDto dto = postsService.findById(id);
        model.addAttribute("post", dto);

        return "posts-update";
    }
package com.jojo.book.springbootwebservice.web;

import com.jojo.book.springbootwebservice.service.posts.PostsService;
import com.jojo.book.springbootwebservice.web.dto.PostsResponseDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@RequiredArgsConstructor
@Controller
public class indexController {
    private final PostsService postsService;
    @GetMapping("/")
    public String index(Model model){
        model.addAttribute("posts", postsService.findAllDesc());
        return "index";
    }
    @GetMapping("/posts/save")
    public String postsSave() {
        return "posts-save";
    }

    @GetMapping("/posts/update/{id}")
    public String postsUpdate(@PathVariable Long id, Model model) {
        PostsResponseDto dto = postsService.findById(id);
        model.addAttribute("post", dto);

        return "posts-update";
    }
}

addAttribute는 Spring MVC에서 모델에 속성(attribute)을 추가하는 메서드이다. Model 객체는 뷰로 데이터를 전달하기 위해 사용되는 객체로, 컨트롤러에서 뷰로 데이터를 전달할 때 사용된다.

addAttribute 메서드는 두 개의 파라미터를 받는다. 첫 번째 파라미터는 속성의 이름이며, 두 번째 파라미터는 해당 속성에 대한 값이다. 속성의 이름을 통해 뷰에서 해당 속성을 참조하여 값을 사용할 수 있다.

예를 들어, index 메서드에서 model.addAttribute("posts", postsService.findAllDesc())라는 코드는 postsService.findAllDesc()로 얻은 결과를 posts라는 속성에 추가하여 모델에 담는 역할을 한다. 이후 index 뷰에서는 posts라는 속성을 사용하여 컨트롤러에서 전달한 데이터를 화면에 표시할 수 있다.

postsUpdate 메서드에서 model.addAttribute("post", dto)를 통해 dto 객체를 post라는 속성에 추가하여 모델에 담고 있다. 이후 posts-update 뷰에서는 post라는 속성을 사용하여 해당 데이터를 화면에 표시할 수 있다.

따라서, addAttribute는 모델에 속성을 추가하여 컨트롤러에서 뷰로 데이터를 전달하는 역할을 한다.

 

 

게시글 삭제

삭제 기능도 구현해본다. 삭제 API를 만들  차례이다.

 

서비스메소드(PostsService)

    @Transactional
    public void delete(Long id){
        Posts posts = postsRepository.findById(id)
                .orElseThrow(()->new IllegalArgumentException("해당 게시글이 없습니다. id=" + id));
        postsRepository.delete(posts);
    }

코드 설명

    @Transactional
    public void delete(Long id){
        Posts posts = postsRepository.findById(id)
                .orElseThrow(()->new IllegalArgumentException("해당 게시글이 없습니다. id=" + id));
        postsRepository.delete(posts); JpaRepository에서 이미 delete 메소드를 지원하고 있으니 이를 활용한다. 엔티티를 파라미터로 삭제할 수도 있고, deleteById 메소드를 이용하면 id로 삭제할 수도 있다. 존재하는 Posts인지 확인하기 위해 엔티티를 조회한 후에 그대로 삭제한다.
    }

위와 같이 서비스에서 만든 delete 메소드를 컨트롤러가 사용하도록 코드를 추가한다.

PostsApiController

    @DeleteMapping("/api/v1/posts/{id}")
    public Long delete(@PathVariable Long id){
        postsService.delete(id);
        return id;
    }

삭제되었다!

지금까지 기본적인 게시판 기능이 완성되었다.