Lazy Loading이란?

Lazy Loading은 연관된 엔터티(Entity)를 즉시 불러오지 않고, 해당 엔터티에 실제로 접근될 때까지 로딩을 지연시키는 방법입니다. 예를 들어, 게시글(Notice)과 댓글(Comment)이 있고, 게시글에서 댓글을 Lazy Loading으로 설정했다고 가정해봅시다. 이 경우, 게시글을 불러올 때 연관된 댓글들은 즉시 로딩되지 않습니다. 댓글에 실제로 접근(예: notice.getComments())할 때 데이터베이스에서 댓글 정보를 불러오게 됩니다.

' > Spring' 카테고리의 다른 글

AOP에 대하여  (0) 2023.10.21
FetchType.EAGER vs FetchType.LAZY  (0) 2023.10.09
간단한 테스트 코드 함수 정리  (0) 2023.10.02
JpaReporitory vs EntityManager  (0) 2023.09.24
RedirectAttributes?  (0) 2023.09.17

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('게시판', '수정 오류');
      }

 

https://junit.org/junit5/docs/current/user-guide/

JUnit 5 User Guide

Although the JUnit Jupiter programming model and extension model do not support JUnit 4 features such as Rules and Runners natively, it is not expected that source code maintainers will need to update all of their existing tests, test extensions, and custo

junit.org

(자주 사용되는듯 해서 작성해봤습니다.)
@Mock: 이 어노테이션은 Mockito 라이브러리와 함께 사용됩니다. @Mock 어노테이션은 가짜(mock) 객체를 생성합니다. 해당 객체는 실제 객체처럼 동작하도록 설정할 수 있습니다.

@InjectMocks: Mockito에서 해당 어노테이션은 mock 객체를 대상 객체에 주입합니다. 주로 테스트하려는 클래스의 인스턴스에서 사용됩니다.

@Test: @Test 어노테이션은 해당 메소드가 단위 테스트 메소드임을 나타냅니다.

when() 및 thenReturn: 이것들도 Mockito의 메서드들입니다. when()은 특정 메소드 호출을 가로채서 그에 대한 동작을 정의하는 데 사용되며, thenReturn은 가로채진 메소드 호출의 반환값을 지정하는 데 사용됩니다.

mockMvc: Spring Test에서 제공하는 MockMvc는 웹 서버 없이 스프링 MVC의 동작을 테스트하기 위해 사용됩니다.

andExpect: MockMvc의 perform() 메소드 후에 연결되어 특정 조건을 검사합니다. 예를 들면, HTTP 응답 상태 코드나 응답 본문의 내용 등을 검사하는 데 사용됩니다.

jsonPath: andExpect 메소드와 함께 사용되며, JSON 응답 본문의 특정 부분을 검사하는 데 사용됩니다.

contentType 및 content: 이들은 MockMvc의 andExpect 메소드와 함께 사용되며, 응답의 Content-Type이나 본문의 내용을 검사하는 데 사용됩니다.

perform: MockMvc의 perform() 메소드는 HTTP 요청을 수행하며 그 결과를 검사하는 데 사용됩니다.

' > Spring' 카테고리의 다른 글

FetchType.EAGER vs FetchType.LAZY  (0) 2023.10.09
Lazy Loading이란?  (0) 2023.10.09
JpaReporitory vs EntityManager  (0) 2023.09.24
RedirectAttributes?  (0) 2023.09.17
url 맵핑에 관하여  (0) 2023.09.17

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 부분들 완성하고 세션 추가해서 운영서버에 올려 보도록 하겠습니다.

가장 큰 차이점 

JpaRepository는 빠른 개발과 표준 CRUD 연산에 초점을 맞춘 반면, EntityManager는 보다 세밀한 데이터베이스 연산과 엔터티 관리에 사용됩니다. 실제 프로젝트에서는 종종 두 인터페이스가 함께 사용되기도 합니다.

  1. 정의와 목적:
    • JpaRepository: Spring Data JPA의 일부로 제공되는 인터페이스로, 주요 CRUD 연산을 위한 여러 메서드를 미리 정의해둔 것입니다. 사용자가 별도의 구현 없이 빠르게 데이터베이스 연산을 수행할 수 있게 해줍니다.
    • EntityManager: JPA의 핵심 인터페이스로, 엔터티의 생명주기를 관리하며 데이터베이스와의 상호작용을 위한 API를 제공합니다. 보다 세밀한 데이터베이스 연산이 필요한 경우에 사용됩니다.
  2. 기능:
    • JpaRepository: save, findAll, delete, findById 등의 기본 CRUD 연산을 위한 메서드가 미리 정의되어 있습니다. 또한, 쿼리 메서드의 이름 규칙을 따라 메서드를 정의함으로써 직접 쿼리를 작성하지 않고도 데이터베이스 연산을 수행할 수 있습니다.
    • EntityManager: persist, merge, remove, find, createQuery 등의 메서드를 제공하여 직접 엔터티 객체와 데이터베이스 간의 상호작용을 수행할 수 있습니다. 사용자는 필요에 따라 JPQL (Java Persistence Query Language)나 Criteria API를 사용하여 복잡한 쿼리를 작성할 수 있습니다.
  3. 사용 시기:
    • JpaRepository: 대부분의 표준적인 CRUD 연산이나 간단한 쿼리에 대한 처리를 할 때 사용됩니다. 빠른 개발 속도와 쉬운 유지보수를 위해 많이 활용됩니다.
    • EntityManager: 특정 엔터티의 생명주기를 세밀하게 관리하거나, 복잡한 쿼리와 데이터베이스 연산을 수행할 필요가 있을 때 사용됩니다.
  4. 확장성:
    • JpaRepository: 기본적인 메서드 외에도 사용자 정의 쿼리를 추가할 수 있습니다. 그러나 복잡한 연산을 수행하기 위해서는 종종 EntityManager를 사용해야 할 수도 있습니다.
    • EntityManager: 사용자가 필요에 따라 완전히 맞춤화된 데이터베이스 연산을 수행할 수 있게 해줍니다.

제 코드가 JpaReporitory로 해결할수 있는 부분들까지  EntityManager로 작성했다는게 큰 미스였습니다. 

그래서 세밀하게 db를 변경 시키는 작업이 아니면 JpaReporitory로 변경하도록 하려고 합니다. 

 

 

 

' > Spring' 카테고리의 다른 글

Lazy Loading이란?  (0) 2023.10.09
간단한 테스트 코드 함수 정리  (0) 2023.10.02
RedirectAttributes?  (0) 2023.09.17
url 맵핑에 관하여  (0) 2023.09.17
http 메시지 컨버트 위치?  (0) 2023.09.03

앞서 말씀 드리자면 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가 훨씬 편하고 생산성을 높일수 있다고 생각이 듭니다. 천천히 일단 할거고요. 일단 게시판이 완성되면 운영서버에 올리도록 하겠습니다. 관리자 페이지 쪽도 변경을 해야해서 좀 변경할게 많네요. ;;; ㅎㅎ 

+ Recent posts