스프링 | Mybatis 블로그 만들기 (feat. REST서버)

Salt·2023년 6월 27일
post-thumbnail

MyBatis로 대형 블로그 서비스 만들기

  1. 회원가입 기능
  2. 블로그 포스팅
  3. 회원 이름별로 블로그 포스팅 테이블이 있다

→ 받아야할 것들

  1. 아이디, 비번, 닉네임, 회원번호
  2. 닉네임, 제목, 본문, 글번호, 쓴날짜, 수정날짜, 조회수

+application-db.properties 에 이거 추가하자(디비랑연동)

url=jdbc:mysql://127.0.0.1:3306/dynamic_blog_dev
username_=root
password=mysql
driver-class-name=com.mysql.cj.jdbc.Driver

Blog 엔티티

**@Getter @Setter @ToString
@AllArgsConstructor @NoArgsConstructor
@Builder**   // 빌더 패턴 생성자를 쓸 수 있게 해줌
public class Blog {

    private long blogId;
    private String writer;
    private String blogTitle;
    private String blogContent;
    private Date publishedAt;
    private Date updatedAt;
    private long blogCount;

@Getter @Setter @toString @AllArgsConstructor @NoArgsConstructor @Builder

DB 컬럼 하나하나를 멤버변수로 치환한다.

대형 서비스는 int보다는 long 자료형을 쓰는 게 낫다.

BlogMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- 자바 인터페이스를 연동 -->
<mapper namespace="com.spring.blog.repository.BlogRepository">

    <!-- 전체조회 -->
    <select id="findAll" resultType="com.spring.blog.entity.Blog">
        SELECT
            blog_id as blogId,
            writer,
            blog_title as blogTitle,
            blog_content as blogContent,
            published_at as publisedAt,
            updated_at as updatedAt,
            blog_count as blogCount
        FROM
            blog
    </select>

		<!-- 개별조회 -->
    <select id="findByid" resultType="com.spring.blog.entity.Blog">
        SELECT
            blog_id as blogId,
            writer,
            blog_title as blogTitle,
            blog_content as blogContent,
            published_at as publisedAt,
            updated_at as updatedAt,
            blog_count as blogCount
        FROM
            blog
        WHERE
            blog_id = #{blogId}
    </select>

		<!-- 저장 -->
    <insert id="save" parameterType="com.spring.blog.entity.Blog">
        INSERT INTO
            blog (writer, blog_title, blog_content)
            VALUES (#{writer}, #{blogTitle}, #{blogContent})
    </insert>

		<!-- 삭제 -->
    <delete id="deleteByid" parameterType="long">
        DELETE FROM blog
        WHERE blog_id = #{blogId}
    </delete>

		<!-- 수정 -->
    <update id="update" parameterType="com.spring.blog.entity.Blog">
        UPDATE blog
        SET blog_title = #{blogTitle}, blog_content = #{blogContent}, updated_at = now()
        WHERE blog_id = #{blogId}
    </update>

    <!-- 테스트용 -->
    <update id="createBlogTable">
        CREATE TABLE IF NOT EXISTS blog(
        blog_id int auto_increment primary key,
        writer varchar(16) not null,
        blog_title varchar(200) not null,
        blog_content varchar(4000) not null,
        published_at datetime default now(),
        updated_at datetime default now(),
        blog_count int default 0
        )
    </update>
    <update id="dropBlogTable">
        DROP TABLE blog
    </update>

    <insert id="insertTestData">
        INSERT INTO blog VALUES
        (null, '1번유저', '1번제목', '1번본문', now(), now(), null),
        (null, '2번유저', '2번제목', '2번본문', now(), now(), null),
        (null, '3번유저', '3번제목', '3번본문', now(), now(), null)
    </insert>
</mapper>

현업에서는 SELECT * FROM 이거 절대 안씀.

작업자가 데이터 적재 상황을 모르고 넘어갈 수 있기 때문이라 컬럼 다 쓴다.

"com.spring.blog.repository.BlogRepository" 자바 인터페이스와 연동해준다.

BlogRepository interface

**@Mapper**
public interface BlogRepository {

    // BeforeEach, AfterEach 테스트용 코드
    void createBlogTable();
    void insertTestData();
    void dropBlogTable();

		// 전체 데이터 조회
		List<Blog> findAll();

****		// 단일조회
		Blog findByid(long blogId);

		// 저장
		void save(Blog blog);

		// 삭제
		void deleteByid(long blogId);

		// 수정
		void update(Blog blog);

우선 mapper.xml파일과 repository.인터페이스 연동 @Mapper

  1. BeforeEach, AfterEach 테스트 메서드
  2. 기능 메서드 5개 나열

BlogRepositoryTest

**@SpringBootTest**
**@TestInstance**(TestInstance.Lifecycle.PER_METHOD)  // DROP TABLE시 필요한 어노테이션
public class BlogRepositoryTest {

    **@Autowired**
    BlogRepository blogRepository;

    **@BeforeEach**    
    public void setBlogTable() {
        blogRepository.createBlogTable();
        blogRepository.insertTestData();
    }

    @Test
    @DisplayName("전체 행을 얻어오고 그중 1번 인덱스 행만 추출해 확인")
    public void findAllTest() {
        // given
        int blogId = 1;     // 인덱스는 0번부터라서 사람이 보기에 편하도록 blogId라는 변수명에 1을 넣어 밑에서 불러오자.

        // when
        List<Blog> blogList = blogRepository.findAll();

        // then
        assertEquals(3, blogList.size());
        assertEquals(2, blogList.get(blogId).getBlogId());    // 사람 기준 2번객체 id 2번
    }

    @Test
    public void findByidTest() {
        // given
        long id = 2;

        // when
        Blog blog = blogRepository.findByid(id);

        // then
        assertEquals("2번유저",blog.getWriter());
        assertEquals("2번제목",blog.getBlogTitle());
        assertEquals(2, blog.getBlogId());
    }

    @Test
    public void saveTest() {
        // given
        String writer = "작가명";
        String blogTitle = "타이틀";
        String blogContent = "본문";

        // 빌더 패턴으로 객체 생성하기. ( = setter) 대신 장점은 파라미터 순서 상관없이 막 쓸 수 있고 가독성 좋음
        Blog blog = Blog.builder()
                .writer(writer)
                .blogTitle(blogTitle)
                .blogContent(blogContent)
                .build();

        int blogId  = 3;

        // when
        blogRepository.save(blog);
        List<Blog> blogList = blogRepository.findAll();

        // then
        assertEquals(4, blogList.size());
        assertEquals(writer, blogList.get(blogId).getWriter());
        assertEquals(blogTitle, blogList.get(blogId).getBlogTitle());
        assertEquals(blogContent, blogList.get(blogId).getBlogContent());
    }

    @Test
    public void deleteByidTest() {
        // given
        long blogId = 3;

        // when
        blogRepository.deleteByid(blogId);

        // then
        assertEquals(2, blogRepository.findAll().size());
        assertNull(blogRepository.findByid(blogId));
    }

		@Test
    public void updateTest() {
        // given
        long blogId = 2;
        String blogTitle = "수정한제목";
        String blogContent = "수정한본문";

        Blog blog = blogRepository.findByid(blogId);
        blog.setBlogTitle(blogTitle);
        blog.setBlogContent(blogContent);

        // when
        blogRepository.update(blog);

        // then
        assertEquals(blogTitle, blogRepository.findByid(blogId).getBlogTitle());
        assertEquals(blogContent, blogRepository.findByid(blogId).getBlogContent());
    }

    **@AfterEach** 
    public void dropBlogTable() {
        blogRepository.dropBlogTable();
    }
}

@SpringBootTest @TestInstance

@BeforeEach @Test @AfterEach

@BeforeEach

CREATE TABLE 로 테이블을 만듬

더미데이터를 몇 개 입력해준다

@Test

@AfterEach

DROP TABLE 로 테이블을 없앰 (PK까지 초기화)

  • 이걸 하는 이유가 멀까 ??? 💡 @BeforeEach 초기 세팅을 잡아줌으로써, 격리된 환경에서 테스트를 실행할 때 테스트 간의 상호작용이나 의존성으로 인해 에러가 발생할 가능성을 없앤다. @Test 실제 테스트 검증 구간 @AfterEach 테이블을 깨끗하게 삭제함으로써, 다음 테스트를 할 때 영향을 배제함. **모든 테스트 코드 순서를 랜덤하게 바꿔놔도 서로 영향을 절대 미치면 안된다!!!** 그래서 다 깨끗하고 같은 상황에서 돌려야 한다 ex. save() 데이터가 추가된 이후에, findAll()를 실행하면 데이터 개수가 바뀜, 근데 순서를 바꾸면 또 데이터가 그 전 3개로 고정되서 결과가 같음

BlogService

public interface BlogService {
    // 비즈니스 로직 정의

    // 전체 조회
    List<Blog> findAll();
    // 개별 조회
    Blog findByid(long blogId);
    // 삭제
    void deleteByid(long blogId);
    // 사용자추가
    void save(Blog blog);
    // 수정
    void update(Blog blog);
  
}

BlogServiceImpl

// Service 구현체는 blogRepository를 멤버변수로 가지고 있고, 호출한다.
// Controller -> Service -> blogRepository -> DB
@Service
public class BlogServiceImpl implements BlogService {
    BlogRepository blogRepository;

    @Autowired     // 생성자 주입
    public BlogServiceImpl(BlogRepository blogRepository) {
        this.blogRepository = blogRepository;
    }
    @Override
    public List<Blog> findAll() {
        return blogRepository.findAll();
    }
    @Override
    public Blog findByid(long blogId) {
        return blogRepository.findByid(blogId);
    }
    @Override
    public void deleteByid(long blogId) {
        blogRepository.deleteByid(blogId);
    }
    @Override
    public void save(Blog blog) {
        blogRepository.save(blog);
    }

    @Override
    public void update(Blog blog) {
        blogRepository.update(blog);
    }
}

BlogServiceTest

@SpringBootTest
public class BlogServiceTest {

    // blogService변수 안에 BlogService객체 넣어서 저장
    @Autowired
    BlogService blogService;

    @Test
    @Transactional
    public void findAllTest() {
        // given
        // when
        List<Blog> blogList = blogService.findAll();
        // then
        assertEquals(3, blogList.size());
    }
    @Test
    @Transactional
    public void findByidTest() {
        // given
        // when
        Blog blog = blogService.findByid(2);
        // then
        assertEquals(2, blog.getBlogId());
        assertEquals("2번제목", blog.getBlogTitle());
        assertEquals("2번본문", blog.getBlogContent());
    }
    @Test
    @Transactional
    public void deleteByidTest() {
        // given
        // when
        blogService.deleteByid(2);
        // then
        assertNull(blogService.findByid(2));
    }
    @Test
    @Transactional
    public void saveTest() {
        // given
        int blogId = 4;
        String writer = "작가";
        String blogTitle = "제목";
        String blogContent = "본문";
        int lastBlogIndex = 3;

        Blog blog = Blog.builder()
                .blogId(blogId)
                .writer(writer)
                .blogTitle(blogTitle)
                .blogContent(blogContent)
                .build();

        // when
        blogService.save(blog);

        // then
        assertEquals(4, blogService.findAll().size());
        assertEquals(writer, blogService.findAll().get(lastBlogIndex).getWriter());
        assertEquals(blogTitle, blogService.findAll().get(lastBlogIndex).getBlogTitle());
        assertEquals(blogContent, blogService.findAll().get(lastBlogIndex).getBlogContent());
    }
    @Test
    @Transactional
    public void updateTest() {
        // given
        long blogId = 2;

        String blogTitle = "수정제목";
        String blogContent = "수정콘텐츠";

        Blog blog = blogService.findByid(blogId);
        blog.setBlogTitle(blogTitle);
        blog.setBlogContent(blogContent);

        // when
        blogService.update(blog);
        // then
        assertEquals(blogTitle, blogService.findByid(blogId).getBlogTitle());
        assertEquals(blogContent, blogService.findByid(blogId).getBlogContent());
    }

}

BlogController

@Controller
@RequestMapping("/blog")
public class BlogController {

    **private BlogService blogservice;

    @Autowired
    public BlogController(BlogService blogService) {
        this.blogservice = blogService;
    }**

    @RequestMapping(value="/list", method=RequestMethod.GET)
    public String list(Model model) {
        List<Blog> blogList = blogservice.findAll();
        model.addAttribute("blogList", blogList);

        // /WEB-INF/views/ 이후에 올 경로 .jsp
        return "blog/list";
    }

		@RequestMapping(value = "/detail/{blogId}")
    public String detail(@PathVariable long blogId, Model model) {
        Blog blog = blogService.findByid(blogId);
        if(blog == null) {
            try {
                throw new NotFoundBlogIdException("없는 아이디로 조회했습니다.");
            } catch(NotFoundBlogIdException e) {       // 현업에서는 e 안씀
                return "blog/NotFoundBlogIdExceptionResultPage";
            }
        }
        model.addAttribute("blog", blog);
        // 리팩토링 -> model.addAttribute("blog", blogService.findByid(blogId));

        return "blog/detail";
    }

    @RequestMapping(value = "/insert", method = RequestMethod.GET)
    public String insert() {
        return "blog/form";
    }

    @RequestMapping(value = "/insert", method = RequestMethod.POST)
    public String insert(Blog blog) {
        blogService.save(blog);
        return "redirect:/blog/list";
    }

    @RequestMapping(value = "/delete", method = RequestMethod.POST)
    public String delete(long blogId) {
        blogService.deleteByid(blogId);
        return "redirect:/blog/list";
    }

    @RequestMapping(value = "/update-form", method = RequestMethod.POST)
    public String update(long blogId, Model model) {
        Blog blog = blogService.findByid(blogId);
        model.addAttribute("blog", blog);
        return "blog/update-form";
    }

		@RequestMapping(value = "/update", method = RequestMethod.POST)
    public String update(Blog blog) {
        blogService_practice.update(blog);

        return "redirect:/blog/list";
    }

}

BlogController에서 BlogService 사용하기 위해 생성자 주입을 해주자.

일단 BlogService타입이라는 변수 blogservice 선언하고,

생성자를 만들어서 blogservice에 실제 blogService 객체를 주입한다.

왜 insert(save)는 두 메서드 value가 같은데,

update는 두 메서드의 value가 다를까?

save는 그냥 작성 폼을 불러와서, 저장하는 기능 insert경로에서 한번에 하면 되지만

두개다 같은 /insert 에서 폼보여주는동시에(GET), 저장(POST) 요청함 그치만 HTTP 요청타입이 다르기 때문에 두개로 구분해준거고

update는 다른 경로에서 저장되어있는 작성 폼을 뷰를 거쳐서 불러오고, 수정하는 기능을 한다.

/update-form 이라는 다른 경로에서 **저장된작성폼불러오기(POST), /update** 에서 수정후list로리턴하기(POST)

둘다 HTTP 요청타입이 같지만 기능이 달라서??? 그런거라고 이해하자

BlogUpdateDTO

@Getter @Setter @Builder @ToString
@AllArgsConstructor @NoArgsConstructor

**public class BlogUpdateDTO {**
    private long blogId;
    private String writer;
    private String blogTitle;
    private String blogContent;

**public BlogUpdateDTO(Blog blog){**
    this.blogId = blog.getBlogId();
    this.writer = blog.getWriter();
    this.blogTitle = blog.getBlogTitle();
    this.blogContent = blog.getBlogContent();
    }
}

Entity(PK필수)DB에 대응하는 자바 클래스이고, 데이터 전달이 아닌 단순 정의만 해줌

따라서 실질적으로 역직렬화나 직렬화를 위한 로직(레이어간 전송)에는

DTO(PK없어도됨)(Data Transfer Object)라는 클래스를 만들고

활용할 쿼리문에 맞춰서 멤버변수를 정의해서 사용한다.

보통 SELECT, INSERT 기능 별로 DTO를 적당히 나누는게 좋다.

  1. Update시에 넘겨주는 데이터만 DTO에 정의하고,
  2. 그 데이터를 DTO로 변환해주기 위해 생성자를 쓴다.

엔터티에는 데이터의 무결성(데이터 못바꿈)을 위해 Setter를 안 쓰는 게 좋다.

DTO가 엔터티의 하위개념이므로, DTO는 엔터티의 내부구조를 알아야 작동할 수 있지만

엔터티는 DTO의 구조와 상관없이 작동해야 하므로, 엔터티는 DTO의 내부를 몰라도 된다.

Entity와 DTO로 분리하면 유지보수성이 좋아지는데, 데이터 전송과 비즈니스 로직이 분리돼서

수정해야 할 때 둘 다 건들일 필요가 없고 한 쪽만 선택해서 수정 및 확장을 하기 때문에

유지보수하기 편하다고 하는 것이다. 또한 테스트를 할 때도 각각을 독립적으로 테스트

할 수 있다.

🌟깃허브에서 더 자세히보기

0개의 댓글