이전에 이어서 service랑 controller를 작성을 전부 완료하였고 테스트도 완료하였습니다.
내일은 프론트단을 완료하고 현재  pc내에서 웹 작동해볼 예정이고 운영웹에는 테스트 조금더 한후에 적용할 예정입니다. 코드 관련해서 조금 공부할 필요가 있다고 생각했고요. 그건 내일 작성하도록 하겠습니다.
2023.10.09 - [웹/Spring vue 웹 개발] - spring vue 댓글 01

spring vue 댓글 01

2023.10.08 - [웹/Spring vue 웹 개발] - [리팩토링]게시판완료 (프론트단 및 백단 연결 변경 완료) [리팩토링]게시판완료 (프론트단 및 백단 연결 변경 완료)2023.10.02 - [웹/Spring vue 웹 개발] - [리팩토링]게

kwaksh2319.tistory.com

서비스:
CommentService

public interface CommentService {
    Comment save(Comment comment);
    List<Comment> findAll();
    Optional<Comment> findById(Long id);
    Comment update(Long id,Comment saveComment);
    void deleteAll();
}

CommentSerivceImpl

package kr.co.kshproject.webDemo.Applicaiton.Comment;

import kr.co.kshproject.webDemo.Domain.Comment.Comment;
import kr.co.kshproject.webDemo.Domain.Comment.CommentRepository;
import lombok.AllArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.util.List;
import java.util.Optional;

@Service
@AllArgsConstructor
public class CommentServiceImpl implements CommentService{
    private static final Logger logger = LoggerFactory.getLogger(CommentServiceImpl.class);
    private final CommentRepository commentRepository;

    @Override
    public Comment save(Comment comment) {
        LocalDate now = LocalDate.now();
        comment.setCreatedDate(now.toString());
        comment.setUserName("admin");
        return commentRepository.save(comment);
    }

    @Override
    public List<Comment> findAll() {
        return commentRepository.findAll();
    }

    @Override
    public Optional<Comment> findById(Long id) {
        return commentRepository.findById(id);
    }

    @Override
    public Comment update(Long id, Comment saveComment) {
        Optional<Comment> findComment =commentRepository.findById(id);
        if(findComment.isPresent()==false){
            return null;
        }
        Comment comment=findComment.get();
        comment.setContents(saveComment.getContents());

        return commentRepository.save(comment);
    }

    @Override
    public void deleteAll() {
        commentRepository.deleteAll();
    }
}

컨트롤러:
CommentController

package kr.co.kshproject.webDemo.interfaces.Comment;

import kr.co.kshproject.webDemo.Applicaiton.Comment.CommentService;
import kr.co.kshproject.webDemo.Domain.Comment.Comment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@Slf4j
@RestController
@RequestMapping("/comment")
public class CommentController {
    private final CommentService commentService;

    @Autowired
    public CommentController(CommentService commentService){
        this.commentService=commentService;
    }

    @PostMapping
    public ResponseEntity<Comment> save(@RequestBody Comment comment){
        return ResponseEntity.ok(commentService.save(comment));
    }

    @GetMapping
    public ResponseEntity<List<Comment>> findAll(){
        return ResponseEntity.ok( commentService.findAll());
    }

    @GetMapping("/{id}")
    public Optional<Comment> findById(@PathVariable Long id){
        return commentService.findById(id);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Comment> update(@PathVariable Long id, @RequestBody Comment comment){
        return ResponseEntity.ok(commentService.update(id,comment));
    }

    @DeleteMapping
    public void deleteAll(){
        commentService.deleteAll();
    }
}

 테스트 코드:
CommentSerive 테스트 코드

package kr.co.kshproject.webDemo.Applicaiton.Comment;

import kr.co.kshproject.webDemo.Applicaiton.Notice.NoticeServiceImpl;
import kr.co.kshproject.webDemo.Domain.Comment.Comment;
import kr.co.kshproject.webDemo.Domain.Comment.CommentRepository;
import kr.co.kshproject.webDemo.Domain.Notice.Notice;
import kr.co.kshproject.webDemo.Domain.Notice.NoticeRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

class CommentServiceImplTest {
    @Mock
    private NoticeRepository noticeRepository;

    @Mock
    private CommentRepository commentRepository;

    @InjectMocks
    private NoticeServiceImpl noticeService;
    
    @InjectMocks
    private CommentServiceImpl commentService;

    @BeforeEach
    void setUp(){
        MockitoAnnotations.openMocks(this);
    }

    @AfterEach
    void afterEach(){
        noticeService.deleteAll();
        commentService.deleteAll();
    }

    @Test
    void save() throws Exception{
        //생성
        // Notice 생성 및 저장
        Notice notice = new Notice(null, "test", "title", "contents", "email", "date", null);

        // 저장 동작을 모의화
        when(noticeRepository.save(any(Notice.class))).thenReturn(notice);

        // Comment 생성 및 저장
        Comment comment = new Comment(null, null, "test", "comment", "date");
        when(commentRepository.save(any(Comment.class))).thenReturn(comment);
        comment.setNotice(notice);

        Optional<Comment> findComment= Optional.ofNullable(commentService.save(comment));
        assertThat(findComment.get()).isEqualTo(comment);
    }

    @Test
    void findAll() throws Exception{
        //생성
        // Notice 생성 및 저장
        Notice notice= new Notice(null,"test1","title","cotenst","email","date",null);
        // 저장 동작을 모의화
        when(noticeRepository.save(any(Notice.class))).thenReturn(notice);

        // Comment 생성 및 저장
        Comment comment1 = new Comment(null, notice, "test1", "comment1", "date");
        Comment comment2 = new Comment(null, notice, "test2", "comment2", "date");

        when(commentRepository.findAll()).thenReturn(Arrays.asList(comment1, comment2));

        //find
        List<Comment> result = commentService.findAll();
        assertThat(result.size()).isEqualTo(2);
        assertThat(result).contains(comment1, comment2);
    }

    @Test
    void findById() throws Exception {
        Long id = 1L;
        Notice notice= new Notice(null,"test","title","cotenst","email","date",null);

        // 저장 동작을 모의화
        when(noticeRepository.findById(id)).thenReturn(Optional.of(notice));

        Comment comment = new Comment(null, notice, "test", "comment", "date");

        when(commentRepository.findById(id)).thenReturn(Optional.of(comment));

        Optional<Comment> foundComment = commentService.findById(id);
        assertThat(foundComment.get()).isEqualTo(comment);
    }

    @Test
    void update() throws Exception {
        //생성
        // Notice 생성 및 저장
        Long id = 1L;
        Notice notice= new Notice(null,"test","title","cotenst","email","date",null);

        // 저장 동작을 모의화
        when(noticeRepository.findById(id)).thenReturn(Optional.of(notice));

        Comment existingComment = new Comment(id, notice, "test", "comment", "date");
        Comment updatedComment = new Comment(id, notice, "test", "newContent", "date");
        //when
        when(commentRepository.findById(id)).thenReturn(Optional.of(existingComment));
        when(commentRepository.save(any(Comment.class))).thenAnswer(invocation -> invocation.getArgument(0));
        Comment result = commentService.update(id, updatedComment);

        //then
        assertThat(result.getId()).isEqualTo(id);
        assertThat(result.getContents()).isEqualTo("newContent");
    }
}

CommentController 테스트 코드 

package kr.co.kshproject.webDemo.interfaces.Comment;

import kr.co.kshproject.webDemo.Applicaiton.Comment.CommentService;
import kr.co.kshproject.webDemo.Applicaiton.Notice.NoticeService;
import kr.co.kshproject.webDemo.Domain.Comment.Comment;
import kr.co.kshproject.webDemo.Domain.Notice.Notice;
import kr.co.kshproject.webDemo.interfaces.Notice.NoticeController;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import java.util.Arrays;
import java.util.Optional;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;

class CommentControllerTest {
    private MockMvc mockMvc1;
    private MockMvc mockMvc2;

    @Mock
    private NoticeService noticeService;

    @InjectMocks
    private NoticeController noticeController;

    @Mock
    private CommentService commentService;

    @InjectMocks
    private CommentController commentController;

    @BeforeEach
    public void setUp() {
        MockitoAnnotations.openMocks(this);
        mockMvc1 = MockMvcBuilders.standaloneSetup(noticeController).build();
        mockMvc2=MockMvcBuilders.standaloneSetup(commentController).build();
    }

    @Test
    void save() throws Exception {
        //생성
        Notice notice= new Notice(null,"test","title","cotenst","email","date",null);

        //저장
        when(noticeService.save(any(Notice.class))).thenReturn(notice);

        //체크
        mockMvc1.perform(post("/notice")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{}"))
                .andExpect(status().isOk());

        Comment comment = new Comment(null, null, "test", "comment", "date");

        //저장
        when(commentService.save(any(Comment.class))).thenReturn(comment);
        comment.setNotice(notice);
        //체크
        mockMvc2.perform(post("/comment")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{}"))
                .andExpect(status().isOk());

    }

    @Test
    void findAll() throws Exception{
        //생성
        //생성
        Notice notice= new Notice(null,"test","title","cotenst","email","date",null);

        //저장
        when(noticeService.save(any(Notice.class))).thenReturn(notice);

        //체크
        mockMvc1.perform(post("/notice")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{}"))
                .andExpect(status().isOk());

        //생성
        Comment comment1 = new Comment(null, null, "test", "comment1", "date");
        Comment comment2 = new Comment(null, null, "test", "comment2", "date");

        //저장
        when(commentService.findAll()).thenReturn(Arrays.asList(comment1, comment2));
        comment1.setNotice(notice);
        comment2.setNotice(notice);
        //체크
        mockMvc2.perform(get("/comment"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON));
    }

    @Test
    void findById() throws Exception{
        Long id = 1L;
        Notice notice= new Notice(null,"test","title","cotenst","email","date",null);

        //저장
        when(noticeService.save(any(Notice.class))).thenReturn(notice);

        //체크
        mockMvc1.perform(get("/notice/"+id))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON));

        Comment comment = new Comment(null, null, "test", "comment", "date");

        //저장
        when(commentService.save(any(Comment.class))).thenReturn(comment);
        comment.setNotice(notice);
        //체크
        mockMvc2.perform(get("/comment/"+id))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON));
    }

    @Test
    void update() throws Exception{
        Long id = 1L;

        //생성
        Notice notice= new Notice(null,"test","title","cotenst","email","date",null);

        //저장
        when(noticeService.save(any(Notice.class))).thenReturn(notice);

        //체크
        mockMvc1.perform(post("/notice")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{}"))
                .andExpect(status().isOk());

        Comment existingComment = new Comment(id, notice, "test", "oldComment", "date");
        Comment updatedComment  = new Comment(id, notice, "test", "newComment", "date");

        when(commentService.findById(id)).thenReturn(Optional.of(existingComment));
        when(commentService.update(eq(id), any(Comment.class))).thenReturn(updatedComment);

        mockMvc2.perform(put("/comment/" + id)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"contents\":\"newComment\"}"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.contents").value("newComment"));
    }
}

2023.10.08 - [웹/Spring vue 웹 개발] - [리팩토링]게시판완료 (프론트단 및 백단 연결 변경 완료)

[리팩토링]게시판완료 (프론트단 및 백단 연결 변경 완료)

2023.10.02 - [웹/Spring vue 웹 개발] - [리팩토링]게시판03 update test code [리팩토링]게시판03 update test code 2023.10.01 - [웹/Spring vue 웹 개발] - [리팩토링]게시판02(service/controller) [리팩토링]게시판02(service/cont

kwaksh2319.tistory.com

기존 게시판 코드들을 전부 수정하였고요. 이번에 추가 기능으로 댓글 기능을 만들기 위해서 백단을 만들던 와중에 몇가지 부분이 막혀서 말씀 드리겠습니다. 먼저 코드부터 보여드리겠습니다.
 
'기존에는 쿼리문으로 댓글이나 게시판들 join문으로 해결 했을텐데요. jpa는 확실히 많은게 다르더군요.'
 
 Notice 

package kr.co.kshproject.webDemo.Domain.Notice;

import kr.co.kshproject.webDemo.Domain.Comment.Comment;
import lombok.*;

import javax.persistence.*;
import javax.validation.constraints.NotEmpty;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "Notice")
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Notice {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "NOTICE_SEQ")
    @SequenceGenerator(name = "NOTICE_SEQ", sequenceName = "NOTICE_SEQ", allocationSize = 1)
    @Column(name = "ID")
    private Long id; //key

    @NotEmpty
    @Setter
    private String username;

    @NotEmpty
    @Setter
    private String title;

    @NotEmpty
    @Setter
    private String contents;

    @NotEmpty
    @Setter
    private String email;

    @NotEmpty
    @Setter
    @Column(name = "created_date")
    private String createdDate;

    @OneToMany(mappedBy = "notice", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Comment> comments = new ArrayList<>();
}

Comment
 

package kr.co.kshproject.webDemo.Domain.Comment;
import kr.co.kshproject.webDemo.Domain.Notice.Notice;
import lombok.*;
import javax.persistence.*;
import javax.validation.constraints.NotEmpty;

@Entity
@Table(name = "Comment")
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Comment {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "COMMENT_SEQ")
    @SequenceGenerator(name = "COMMENT_SEQ", sequenceName = "COMMENT_SEQ", allocationSize = 1)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="notice_id")
    private Notice notice;

    @NotEmpty
    @Setter
    private String userName;

    @NotEmpty
    @Setter
    private String contents;

    @NotEmpty
    @Setter
    @Column(name = "created_date")
    private String createdDate;
}

CommentRepository

package kr.co.kshproject.webDemo.Domain.Comment;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface CommentRepository extends JpaRepository<Comment,Long> {
}

테스트코드

package kr.co.kshproject.webDemo.Domain.Comment;

import kr.co.kshproject.webDemo.Domain.Notice.Notice;
import kr.co.kshproject.webDemo.Domain.Notice.NoticeRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.transaction.Transactional;
import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
class CommentRepositoryTest {
    @Autowired
    CommentRepository commentRepository;

    @Autowired
    NoticeRepository noticeRepository;

    @AfterEach
    void afterEach(){
        commentRepository.deleteAll();
    }

    @Test
    @Transactional
    void save() {
        //생성
        // Notice 생성 및 저장
        Notice notice = new Notice(null, "test", "title", "contents", "email", "date", null);
        Notice savedNotice = noticeRepository.save(notice);

        // Comment 생성 및 저장
        Comment comment = new Comment(null, savedNotice, "test", "comment", "date");
        Comment savedComment = commentRepository.save(comment);

        // Comment 조회
        Optional<Comment> foundComment = commentRepository.findById(savedComment.getId());

        // 검증
        assertThat(foundComment.isPresent()).isTrue();
        assertThat(foundComment.get().getNotice()).isNotNull(); // 지연 로딩으로 인해 Notice 객체가 실제로 로드되었는지 확인합니다.
        assertThat(foundComment.get()).isEqualTo(savedComment);
    }

    @Test
    @Transactional
    void findAll() {
        //생성
        Notice notice = new Notice(null, "test", "title", "contents", "email", "date", null);
        Notice savedNotice = noticeRepository.save(notice);

        Comment comment1 = new Comment(null, savedNotice, "test1", "comment", "date");
        Comment comment2 = new Comment(null, savedNotice, "test2", "comment", "date");
        Comment savedComment1 = commentRepository.save(comment1);
        Comment savedComment2 = commentRepository.save(comment2);

        List<Comment> result = commentRepository.findAll();
        assertThat(result.size()).isEqualTo(2);
        assertThat(result).contains(savedComment1, savedComment2);
    }

    @Test
    @Transactional
    void findById(){
        long id=1L;
        Notice notice= new Notice(null,"test1","title","cotenst","email","date",null);
        //세이브
        Notice savedNotice=noticeRepository.save(notice);

        Comment comment1 = new Comment(null, savedNotice, "test1", "comment", "date");
        Comment comment2 = new Comment(null, savedNotice, "test2", "comment", "date");
        Comment savedComment1 = commentRepository.save(comment1);
        Comment savedComment2 = commentRepository.save(comment2);

        //체크
        Optional<Comment> findNotice1=commentRepository.findById(savedComment1.getId());
        assertThat(findNotice1.isPresent()).isTrue();
        assertThat(findNotice1.get()).isEqualTo(savedComment1);

        //체크
        Optional<Comment> findNotice2=commentRepository.findById(savedComment2.getId());
        assertThat(findNotice2.isPresent()).isTrue();
        assertThat(findNotice2.get()).isEqualTo(savedComment2);
    }

    @Test
    @Transactional
    void update(){
        Notice notice= new Notice(null,"test1","title1","cotenst2","email","date",null);

        Notice savedNotice=noticeRepository.save(notice);

        // Comment 생성 및 저장
        Comment comment = new Comment(null, savedNotice, "test", "comment", "date");
        Comment savedComment = commentRepository.save(comment);

        // Comment 조회
        Optional<Comment> foundComment = commentRepository.findById(savedComment.getId());

        foundComment.get().setContents("cotenst2");

        Comment updateComment=commentRepository.save(foundComment.get());
        assertThat(updateComment.getId()).isEqualTo(foundComment.get().getId());
        assertThat(updateComment.getContents()).isEqualTo(foundComment.get().getContents());
    }
}

이번에 키포인트가

@Transactional

이거 인데요.
@Transcational을 한 이유는 아래의 내용 lazy 때문입니다.

@ManyToOne(fetch = FetchType.LAZY)

Fetchtype에 대해서 간단히 알아보죠. 그리고 왜 lazy를 사용하였는지에 대해서도 말씀드리겠습니다.
 
Fetchtype은 'EAGER'와 'LAZY'기 있습니다. 이들의 장점과 단점에 대해서 간략히 말씀드리겠습니다.

1. FetchType.EAGER (즉시 로딩)

  • 장점:
    • 연관된 엔터티가 항상 함께 로딩되므로, 엔터티 사용 시 추가적인 쿼리 없이 바로 사용 가능.
  • 단점:
    • 항상 연관된 엔터티를 함께 불러오므로, 필요하지 않은 데이터까지 로드될 수 있다.
    • 결과적으로 성능 문제가 발생할 수 있으며, 불필요한 네트워크 및 메모리 리소스가 사용될 수 있다.

2. FetchType.LAZY (지연 로딩)

  • 장점:
    • 실제로 연관된 엔터티에 접근할 때만 데이터베이스로부터 로딩한다. 따라서 필요한 경우에만 쿼리가 발생.
    • 성능 최적화에 도움을 줄 수 있다. 불필요한 데이터를 로딩하지 않으므로 자원을 효율적으로 사용할 수 있다.
  • 단점:
    • 첫 접근 시 추가적인 쿼리가 실행됨. 즉, 처음 연관된 엔터티에 접근할 때마다 쿼리가 실행될 수 있다.
    • 트랜잭션 밖에서 지연 로딩을 시도하면 LazyInitializationException이 발생한다.

일반적으로는 EAGER가 비효율적입니다. one to many 형태의 테이블의 데이터를 불러올때 게시판을 열기전에 게시글들을 불러오면 이때 굳이 댓글들 데이터를 로딩시킬 필요가 없겟죠?(제가 이해한부분은 이거입니다. 틀릴수도 있으면 지적부탁드립니다.)
 
그래서 lazy를 사용해야 합니다. 문제는 test를 할때 

@Transactional

해주지 않으면 
LazyInitializationException 발생합니다.

could not initialize proxy [kr.co.kshproject.webDemo.Domain.Notice.Notice#36] - no Session
org.hibernate.LazyInitializationException: could not initialize proxy [kr.co.kshproject.webDemo.Domain.Notice.Notice#36] - no Session

CommentRepositoryTest > findAll() FAILED
    org.hibernate.LazyInitializationException at CommentRepositoryTest.java:63
2023-10-09 21:03:49.610  INFO 11312 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2023-10-09 21:03:49.612  INFO 11312 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2023-10-09 21:03:49.628  INFO 11312 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.

그래서 @Transcational를 반드시 추가해야만 
트랜잭션 범위 내에서는 영속성 컨탠츠가 활성화 되면서 테스트코드에 문제없이 됩니다. 
 
이것때문에 상당히 힘들었습니다.
 
다음에는 댓글 service, controller도 완성해보겠습니다.
 

2023.10.02 - [웹/Spring vue 웹 개발] - [리팩토링]게시판03 update test code

[리팩토링]게시판03 update test code

2023.10.01 - [웹/Spring vue 웹 개발] - [리팩토링]게시판02(service/controller) [리팩토링]게시판02(service/controller) 이전글: 2023.09.24 - [웹/Spring vue 웹 개발] - [리팩토링] 게시판01(data/repository) [리팩토링] 게시

kwaksh2319.tistory.com

일단 기존 게시판 코드를 전부 변경하였고 그리고 프론트단도 url 같은 경우도 notice로 전부 통일했습니다. 
session 적용을 할 예정이고요 문제없이 잘 작동됩니다. 추가 기능으로 댓글 기능도 넣어볼게요!
영상 아래 변경된 프론트 코드들도  작성했습니다. 백엔드 코드들은 이전링크를 참고해주세요!
 ps 일단 제가 프론트 vue쪽은 독학이어서 좀 부족한 부분이 많습니다. 백엔드도 사내 레거시 코드로 공부하거나 인강으로 공부하다보니 또 다른 회사들은 다르게 개발될수 있어서 부족한 부분 피드백 주시면 감사드립니다.

변경된 메서드
index.js 라우터 

{
      path: '/notice',
      name: 'notice',
      component: PostList,
      props: true
    },

PostList.vue

created() {
    this.axios = axios.create({
      baseURL: this.$myUrl
    });
    try {
      this.axios
        .get('/notice')
        .then(response => {
          this.posts = response.data;
        })
        .catch(error => {
        });
    } catch (error) {
    }
  },

PostForm.vue

try {
        this.axios
          .post('/notice', data,config)
          .then(response => {
            this.$alert('게시판', '등록 성공');
            this.$router.push('/notice');
          })
          .catch(error => {
            this.$alert('게시판', '등록 실패');

          });
      } catch (error) {
        this.$alert('에러', '등록 실패');
      }

PostModify.vue

try {
        this.axios.put(`/notice/${postId}`, data,config)
          .then(response => {
            this.$alert('게시판', '수정 성공');
            this.$router.push('/notice');
          })
          .catch(error => {
            this.$alert('게시판', '수정 실패');
          });
      } catch (error) {
        this.$alert('게시판', '수정 오류');
      }

 

2023.10.01 - [웹/Spring vue 웹 개발] - [리팩토링]게시판02(service/controller)

[리팩토링]게시판02(service/controller)

이전글: 2023.09.24 - [웹/Spring vue 웹 개발] - [리팩토링] 게시판01(data/repository) [리팩토링] 게시판01 앞서 말씀 드리자면 jpa를 조금 독학으로 하다보니 코드가 조금 중구난방이어서 김영한 개발자님

kwaksh2319.tistory.com

먼저 update가 jparepository에서는 직접적으로 제공하지 않기 때문에 만들어야합니다.
service 클래스의 update 입니다.

@Override
 public Notice update(Long id,Notice saveNotice) {
        //id 찾기
        Optional<Notice> findNotice = noticeRepository.findById(id);
        //존재여부 확인
        if(findNotice.isPresent()==false){
            return null;
        }
        //찾은후 데이터 업데이트 저장
        Notice notice=findNotice.get();
        notice.setTitle(saveNotice.getTitle());
        notice.setContents(saveNotice.getContents());
        return noticeRepository.save(notice);
 }

그다음은 controller 클래스 update입니다.

@PutMapping("/{id}")
 public ResponseEntity<Notice> update(@PathVariable Long id, @RequestBody Notice notice){
       return ResponseEntity.ok(noticeService.update(id,notice));
 }

테스트코드:
service  update테스트

@Test
    void update() throws Exception {
        //생성
        Long id = 1L;
        Notice existingNotice = new Notice(id, "test","oldTitle", "oldContent", "oldEmail", "oldDate");
        Notice updatedNotice = new Notice(id,"test", "newTitle", "newContent", "oldEmail", "oldDate");
        
        //when
        when(noticeRepository.findById(id)).thenReturn(Optional.of(existingNotice));
        when(noticeRepository.save(any(Notice.class))).thenAnswer(invocation -> invocation.getArgument(0));
        Notice result = noticeService.update(id, updatedNotice);

        //then
        assertThat(result.getId()).isEqualTo(id);
        assertThat(result.getTitle()).isEqualTo("newTitle");
        assertThat(result.getContents()).isEqualTo("newContent");
    }

controller update  테스트

 @Test
    void update() throws Exception {
        Long id = 1L;
        Notice existingNotice = new Notice(id, "test","oldTitle", "oldContent", "oldEmail", "oldDate");
        Notice updatedNotice = new Notice(id,"test", "newTitle", "newContent", "oldEmail", "oldDate");

        when(noticeService.findById(id)).thenReturn(Optional.of(existingNotice));
        when(noticeService.update(eq(id), any(Notice.class))).thenReturn(updatedNotice);

        mockMvc.perform(put("/notice/" + id)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"title\":\"newTitle\", \"contents\":\"newContent\"}"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.title").value("newTitle"))
                .andExpect(jsonPath("$.contents").value("newContent"));
    }

앞단 프론트가 조금 시간이 걸려서 일단 프론트쪽 완성하고 백단 마무리 짓고 운영에 올리겠습니다. 주말마다 하다보니 진도가 늦네요.

이전글:
2023.09.24 - [웹/Spring vue 웹 개발] - [리팩토링] 게시판01(data/repository)

[리팩토링] 게시판01

앞서 말씀 드리자면 jpa를 조금 독학으로 하다보니 코드가 조금 중구난방이어서 김영한 개발자님 동영상 강의를 보고 코드를 수정하고 있습니다. ㅠㅠ 독학의 장점은 많은걸 얻을수 있지만 그만

kwaksh2319.tistory.com

이전 글은 data/repository를 코드 변경과 테스트코드도 작성했는데요.
 
이번엔 service/contoller 부분을 코드와 테스트 코드를 작성하려고 합니다.
 
세션같은건 조금 나중에 추가할꺼라 일단 이렇게 작성하도록 하겠습니다.
 
1.service/controller 코드 
NoticeService: 인터페이스

package kr.co.kshproject.webDemo.Applicaiton.Notice;

import kr.co.kshproject.webDemo.Domain.Notice.Notice;

import java.util.List;
import java.util.Optional;

public interface NoticeService {
    Notice save(Notice notice);
    List<Notice> findAll();
    Optional<Notice> findById(Long id);
    void deleteAll();
}

NoticeServiceImpl : 서비스 클래스

package kr.co.kshproject.webDemo.Applicaiton.Notice;


import kr.co.kshproject.webDemo.Domain.Notice.Notice;
import kr.co.kshproject.webDemo.Domain.Notice.NoticeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class NoticeServiceImpl implements NoticeService {

    private final NoticeRepository noticeRepository;

    @Autowired
    public NoticeServiceImpl(NoticeRepository noticeRepository){
        this.noticeRepository=noticeRepository;
    }

    @Override
    public Notice save(Notice notice) {
        return noticeRepository.save(notice);
    }

    @Override
    public List<Notice> findAll() {
        return noticeRepository.findAll();
    }

    @Override
    public Optional<Notice> findById(Long id) {
        return noticeRepository.findById(id);
    }

    @Override
    public void deleteAll() {
        noticeRepository.deleteAll();
    }

}

NoticeController: 컨트롤러

package kr.co.kshproject.webDemo.interfaces.Notice;

import kr.co.kshproject.webDemo.Applicaiton.Notice.NoticeService;
import kr.co.kshproject.webDemo.Domain.Notice.Notice;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@Slf4j
@RestController
@RequestMapping("/notice")
public class NoticeController {
   private final NoticeService noticeService;

    @Autowired
    public NoticeController(NoticeService noticeService){
       this.noticeService=noticeService;
   }

   @PostMapping
   public ResponseEntity<Notice> save(Notice notice){
        return ResponseEntity.ok(noticeService.save(notice));
   }

   @GetMapping
    public ResponseEntity<List<Notice>> findAll(){
        return ResponseEntity.ok( noticeService.findAll());
   }

   @GetMapping("/{id}")
   public Optional<Notice> findById(@PathVariable Long id){
        return noticeService.findById(id);
   }

   @DeleteMapping
   public void deleteAll(){
        noticeService.deleteAll();
   }
}

2.service/controller 테스트 코드 
 
서비스 테스트 코드:

package kr.co.kshproject.webDemo.Applicaiton.Notice;

import kr.co.kshproject.webDemo.Domain.Notice.Notice;
import kr.co.kshproject.webDemo.Domain.Notice.NoticeRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

class NoticeServiceImplTest {

    @Mock
    private NoticeRepository noticeRepository;

    @InjectMocks
    private NoticeServiceImpl noticeService;

    @BeforeEach
    public void setUp(){
        MockitoAnnotations.openMocks(this);
    }

    @AfterEach
    void afterEach(){
        noticeService.deleteAll();
    }

    @Test
    void save() {
        //생성
        Notice notice= new Notice(null,"test","title","cotenst","email","date");

        // 저장 동작을 모의화
        when(noticeRepository.save(any(Notice.class))).thenReturn(notice);

        //체크
        Optional<Notice> findNotice= Optional.ofNullable(noticeService.save(notice));
        assertThat(findNotice.get()).isEqualTo(notice);
    }

    @Test
    void findAll() {
        //생성
        Notice notice1= new Notice(null,"test1","title","cotenst","email","date");
        Notice notice2= new Notice(null,"test2","title","cotenst","email","date");

        when(noticeRepository.findAll()).thenReturn(Arrays.asList(notice1, notice2));

        //find
        List<Notice> result = noticeRepository.findAll();
        assertThat(result.size()).isEqualTo(2);
        assertThat(result).contains(notice1, notice2);
    }

    @Test
    void findById() throws Exception {
        //생성
        Long id = 1L;
        Notice notice= new Notice(null,"test","title","cotenst","email","date");

        // 저장 동작을 모의화
        when(noticeRepository.findById(id)).thenReturn(Optional.of(notice));

        // When
        Optional<Notice> foundNotice = noticeService.findById(id);
        assertThat(foundNotice.get()).isEqualTo(notice);
    }
}

컨트롤러 테스트 코드 

package kr.co.kshproject.webDemo.interfaces.Notice;

import kr.co.kshproject.webDemo.Applicaiton.Notice.NoticeService;
import kr.co.kshproject.webDemo.Domain.Notice.Notice;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import java.util.Arrays;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

class NoticeControllerTest {

    private MockMvc mockMvc;

    @Mock
    private NoticeService noticeService;

    @InjectMocks
    private NoticeController noticeController;

    @BeforeEach
    public void setUp() {
        MockitoAnnotations.openMocks(this);
        mockMvc = MockMvcBuilders.standaloneSetup(noticeController).build();
    }

    @Test
    void save() throws Exception {
        //생성
        Notice notice= new Notice(null,"test","title","cotenst","email","date");

        //저장
        when(noticeService.save(any(Notice.class))).thenReturn(notice);

        //체크
        mockMvc.perform(post("/notice")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{}"))
                        .andExpect(status().isOk());
    }

    @Test
    void findAll() throws Exception {
        //생성
        Notice notice1= new Notice(null,"test1","title","cotenst","email","date");
        Notice notice2= new Notice(null,"test2","title","cotenst","email","date");

        //저장
        when(noticeService.findAll()).thenReturn(Arrays.asList(notice1, notice2));
        //체크
        mockMvc.perform(get("/notice"))
               .andExpect(status().isOk())
               .andExpect(content().contentType(MediaType.APPLICATION_JSON));
    }

    @Test
    void findById() throws Exception {
        //생성
        Long id = 1L;
        Notice notice= new Notice(null,"test","title","cotenst","email","date");

        //저장
        when(noticeService.save(any(Notice.class))).thenReturn(notice);

        //체크
        mockMvc.perform(get("/notice/"+id))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON));
    }

}

문제 없이 되었죠. 
이제 update 부분들 완성하고 세션 추가해서 운영서버에 올려 보도록 하겠습니다.

앞서 말씀 드리자면 jpa를  조금 독학으로 하다보니 코드가 조금 중구난방이어서 김영한 개발자님 동영상 강의를 보고 코드를 수정하고 있습니다. ㅠㅠ 독학의 장점은 많은걸 얻을수 있지만 그만큼 잘못된 방법을 얻을수 도 있네요 
지금 이방법도 틀릴수 있어서 계속해서 수정은 하도록 하겠습니다.
 
당장 운영서버에는 적용못하고요. 일단 현재 제  pc에서 잘 작동하면 운영 웹서버에 업로드 예정입니다.
그리고 이번엔 테스트 코드도 함께 작성하였습니다.

파일 위치는 Domain/Notice/

Notice 클래스  File 클래스는 조금 나중에 넣겠습니다.

package kr.co.kshproject.webDemo.Domain.Notice;

import lombok.*;
import javax.persistence.*;
import javax.validation.constraints.NotEmpty;

@Entity
@Table(name = "Notice")
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Notice {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "NOTICE_SEQ")
    @SequenceGenerator(name = "NOTICE_SEQ", sequenceName = "NOTICE_SEQ", allocationSize = 1)
    @Column(name = "ID")
    private Long id; //key

    @NotEmpty
    @Setter
    private String username;

    @NotEmpty
    @Setter
    private String title;

    @NotEmpty
    @Setter
    private String contents;

    @NotEmpty
    @Setter
    private String email;

    @NotEmpty
    @Setter
    @Column(name = "created_date")
    private String createdDate;

}

 
NoticeRepository 클래스

@Repository
public interface NoticeRepository extends JpaRepository<Notice,Long> {

}

참고로 JpaRepository 클래스는에는 유용하 메서드들이 있어서 NoticeRepository 인터페이스에서 상속받아서 사용하면됩니다. 
https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/JpaRepository.html

JpaRepository (Spring Data JPA Parent 3.1.4 API)

All Superinterfaces: CrudRepository , ListCrudRepository , ListPagingAndSortingRepository , PagingAndSortingRepository , QueryByExampleExecutor , Repository All Known Subinterfaces: EnversRevisionRepository , JpaRepositoryImplementation All Known Implement

docs.spring.io

 
테스트 코드 

package kr.co.kshproject.webDemo.Domain.Notice;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
class NoticeRepositoryImplTest {

    @Autowired
    NoticeRepository noticeRepository;

    @AfterEach
    void afterEach(){
        noticeRepository.deleteAll();
    }

    @Test
    void save() {
        //생성
        Notice notice= new Notice(null,"test","title","cotenst","email","date");

        //세이브
        Notice savedNotice=noticeRepository.save(notice);

        //체크
        Optional<Notice> findNotice=noticeRepository.findById(notice.getId());
        assertThat(findNotice.isPresent()).isTrue();
        assertThat(findNotice.get()).isEqualTo(savedNotice);
    }

    @Test
    void findAll() {
        //생성
        Notice notice1= new Notice(null,"test1","title","cotenst","email","date");
        Notice notice2= new Notice(null,"test2","title","cotenst","email","date");

        //세이브
        noticeRepository.save(notice1);
        noticeRepository.save(notice2);

        //find
        List<Notice> result = noticeRepository.findAll();

        //check
        assertThat(result.size()).isEqualTo(2);
        assertThat(result).contains(notice1, notice2);
    }
}

완료:

문제없이 되었죠.
기존에는 직접적으로 만들어서 EntityMaganger로 했는데요. 암튼 이 변경작업이 생각보다 어렵네요... ㅠㅠㅠㅠ 흠 JpaRepository가 훨씬 편하고 생산성을 높일수 있다고 생각이 듭니다. 천천히 일단 할거고요. 일단 게시판이 완성되면 운영서버에 올리도록 하겠습니다. 관리자 페이지 쪽도 변경을 해야해서 좀 변경할게 많네요. ;;; ㅎㅎ 

이번에 jpa쪽 조인에 대해서  알게 되었는데요.
생각보다 조금 어려웠습니다. 아직 db 설계에 대해서는 많이 미숙한듯 합니다.
진짜 db를 여러번 고쳤습니다.
 
내일은 프론트단을 완성시켜 장바구니리스트를 보이도록 할겁니다.
 

p.s
현재 회사에서는 procedure를 사용해서 직접쿼리문으로 join을 했습니다. 
회사의 테이블이 엄청나게 많고 너무 복잡하다보니 db쪽에 종속되는 방향으로 개발들이 되어 있습니다. (sql mapper 방식)
제 개인프로젝트는 orm 방식으로 했고요.  상당히 많은 it 회사들이 orm 방식으로 개발해서 일단 이쪽으로 공부를 하고 있습니다.
아직 모르는게 많고 배울게 엄청나게 많다고 느끼고 있습니다.
 
장바구니 리스트를 뽑아줄 jpa

//장바구니 리스트
    public List<BasketsWithProduct> findAll(String userName,String status, int page, int size) {
        List<BasketsWithProduct> basketsList = null;
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        try{
            CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
            CriteriaQuery<BasketsWithProduct> criteriaQuery = criteriaBuilder.createQuery(BasketsWithProduct.class);

            Root<Baskets> root = criteriaQuery.from(Baskets.class);

            Join<Baskets, Product> fileJoin = root.join("product", JoinType.INNER);

            // 복합키 검색 조건 생성
            Predicate compositeKeyPredicate = criteriaBuilder.equal(root.get("basketId").get("usersId"),userName );

            // 상태 검색 조건 생성
            Predicate statusPredicate = criteriaBuilder.equal(root.get("status"), status);

            // 검색 조건 결합
            Predicate finalPredicate = criteriaBuilder.and(compositeKeyPredicate, statusPredicate);

            //검색
            criteriaQuery.select(criteriaBuilder.construct( BasketsWithProduct.class, root, fileJoin ));
            criteriaQuery.where(finalPredicate);

            TypedQuery<BasketsWithProduct> typedQuery = entityManager.createQuery(criteriaQuery);
            typedQuery.setFirstResult((page - 1) * size);
            typedQuery.setMaxResults(size);

            // 페이지 조건
            basketsList = typedQuery.getResultList();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if(entityManager!=null){
                entityManager.close();
            }
        }
        return basketsList;
    }

BasketsWithProduct 객체

@Getter
@Setter
public class BasketsWithProduct {
   private Baskets baskets;
   private Product product;
   public BasketsWithProduct(Baskets baskets, Product product) {
      this.baskets = baskets;
      this.product = product;
   }
}

Basket 테이블 

@Entity
@Table(name = "Baskets")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Baskets {

    @EmbeddedId
    private BasketId basketId;

    //status 장바구니 B, 삭제 D, 결재 C
    @NotEmpty
    @Column(name = "STATUS")
    private String status;

    //produtnumber + userid
    @NotNull
    @Column(name = "BIND_NUMBER")
    private String bindNumber;

    @ManyToOne
    @JoinColumn(name = "product_id", referencedColumnName = "ID")
    private Product product;

}

Product 테이블 

@Entity
@Getter
@Table(name = "PRODUCT")
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "PRODUCT_SEQ")
    @SequenceGenerator(name = "PRODUCT_SEQ", sequenceName = "PRODUCT_SEQ", allocationSize = 1)
    @Column(name = "ID")
    private Long id; //key
    
    @NotEmpty
    @Setter
    @Column(name = "PRODUCT_NAME")
    private String productName;

    @NotNull
    @Setter
    private BigDecimal price;

    @NotEmpty
    @Setter
    @Column(name = "IMAGE_URL")
    private String imageUrl;

    @NotEmpty
    @Setter
    @Column(name = "VIDEO_URL")
    private String videoUrl;

    @NotNull
    @Setter
    @Column(name = "DESCRIPTION")
    private String description;
}

 

일단 입력 하고 테이블을 불러오는데는 성공했습니다.

일단 baskets 테이블에서는 조회를 전부 하고있고요

이런식으로 product_id 기준으로 조인해서 데이터의 이름과 가격, 이미지 불러올겁니다 .

코드:

    //장바구니 리스트
    public List<Baskets> findAll(String userName,String status, int page, int size) {
        List<Baskets> basketsList = null;
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        try{
            CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
            CriteriaQuery<Baskets> criteriaQuery = criteriaBuilder.createQuery(Baskets.class);

            Root<Baskets> root = criteriaQuery.from(Baskets.class);
            //조인
            // Join<Baskets, Product> jointable = root.join("product", JoinType.INNER);
            // 복합키 검색 조건 생성
            Predicate compositeKeyPredicate = criteriaBuilder.equal(root.get("basketId").get("usersId"),userName );

            // 상태 검색 조건 생성
            Predicate statusPredicate = criteriaBuilder.equal(root.get("status"), status);

            // 검색 조건 결합
            Predicate finalPredicate = criteriaBuilder.and(compositeKeyPredicate, statusPredicate);
            
            //데이터 조회
            criteriaQuery.select(root);
            criteriaQuery.where(finalPredicate);
             
            //페이지 사이즈
            TypedQuery<Baskets> typedQuery = entityManager.createQuery(criteriaQuery);
            typedQuery.setFirstResult((page - 1) * size);
            typedQuery.setMaxResults(size);
            
            basketsList = typedQuery.getResultList();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if(entityManager!=null){
                entityManager.close();
            }
        }
        return basketsList;
    }

 

 

 

 

 

+ Recent posts