
→ 받아야할 것들
+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
**@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 자료형을 쓰는 게 낫다.
<?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" 자바 인터페이스와 연동해준다.
**@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
**@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까지 초기화)
public interface BlogService {
// 비즈니스 로직 정의
// 전체 조회
List<Blog> findAll();
// 개별 조회
Blog findByid(long blogId);
// 삭제
void deleteByid(long blogId);
// 사용자추가
void save(Blog blog);
// 수정
void update(Blog blog);
}
// 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);
}
}
@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());
}
}
@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 요청타입이 같지만 기능이 달라서??? 그런거라고 이해하자
@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를 적당히 나누는게 좋다.
엔터티에는 데이터의 무결성(데이터 못바꿈)을 위해 Setter를 안 쓰는 게 좋다.
DTO가 엔터티의 하위개념이므로, DTO는 엔터티의 내부구조를 알아야 작동할 수 있지만
엔터티는 DTO의 구조와 상관없이 작동해야 하므로, 엔터티는 DTO의 내부를 몰라도 된다.
Entity와 DTO로 분리하면 유지보수성이 좋아지는데, 데이터 전송과 비즈니스 로직이 분리돼서
수정해야 할 때 둘 다 건들일 필요가 없고 한 쪽만 선택해서 수정 및 확장을 하기 때문에
유지보수하기 편하다고 하는 것이다. 또한 테스트를 할 때도 각각을 독립적으로 테스트
할 수 있다.