본문 바로가기

스프링 Spring

21. 04. 06.

목차

    9. 비즈니스 계층

    • 고객의 요구사항을 반영하는 계층.
    • 프레젠테이션 계층과 영속 계층의 중간 다리 역할
    • 로직을 기준으로 처리.
    • ex) 쇼핑몰에서 상품을 구매 : Business- 구매 서비스 / Persistence- 상품 처리 객체, 회원 처리 객체 (포인트 적립)

     

    9.1 비즈니스 계층의 설정

     

    인터페이스 BoardService.java

    package site.levinni.service;
    
    import java.util.List;
    import site.levinni.domain.BoardVO;
    
    public interface BoardService {
        void register (BoardVO boardVO); 
    
        BoardVO get(Long bno); // 상세 조회
    
        boolean modify(BoardVO boardVO);
    
        boolean remove(Long bno);
    
        List<BoardVO> getList();
    }

     

    🔹 각 계증 간의 연결은 인터페이스를 이용해서 느슨한 연결을 함.
    🔹 명백하게 반환해야 할 데이터가 있는 'select'를 해야 하는 메서드는 리턴 타입을 지정할 수 있음.
    🔹 인터페이스 앞에만 접근제한자 public 쓰면 되고 인터페이스 블럭 안에는 public을 쓰지 않아도 자동으로 public으로 인식함.

     

     

    BoardServiceImpl.java

    package site.levinni.service;
    
    import java.util.List;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import lombok.AllArgsConstructor;
    import lombok.extern.log4j.Log4j;
    import site.levinni.domain.BoardVO;
    import site.levinni.mapper.BoardMapper;
    
    @Service
    @Log4j
    @AllArgsConstructor
    public class BoardServiceImpl implements BoardService{
    
        private BoardMapper mapper;
    
        @Override
        public void register(BoardVO boardVO) {
            log.info("register...");
            mapper.insertSelectKey(boardVO);
        }
    
        @Override
        public BoardVO get(Long bno) {
            // TODO Auto-generated method stub
            return mapper.read(bno);
        }
    
        @Override
        public boolean modify(BoardVO boardVO) {
            // TODO Auto-generated method stub
            return mapper.update(boardVO) > 0;
        }
    
        @Override
        public boolean remove(Long bno) {
            // TODO Auto-generated method stub
            return mapper.delete(bno) > 0;
        }
    
        @Override
        public List<BoardVO> getList() {
            // TODO Auto-generated method stub
            log.info("getList....");
            return mapper.getList();
        }
    
    }

     

    🔹 @Service : 계층 구조상 비즈니스 영역을 담당하는 객체임을 표시.
    🔹 BoardServiceImpl이 동작하기 위해선 BoardMapper 객체가 필요하다.
    🔹 @AutoWired를 쓸 수도 있지만, 스프링 4.3부터 단일 파라미터의 생성자가 있으면 자동으로 주입할 수 있으므로 @AllArgsConstructor로 대체

     

     

    🎈 스프링의 서비스 객체 설정(root-context.xml)

    <context:component-scan base-package="site.levinni.service"/>


     

    9.2 비즈니스 계층의 구현과 테스트

     

    BoardServiceTests.java

    package site.levinni.service;
    
    import static org.junit.Assert.assertNotNull;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    import lombok.extern.log4j.Log4j;
    import site.levinni.domain.BoardVO;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
    @Log4j
    public class BoardServiceTests {
    
        @Autowired
        private BoardService service;
    
        @Test
        public void testExist() {
            assertNotNull(service);
            log.info(service);
        }
    
        @Test
        public void testRegister() {
            BoardVO boardVO = new BoardVO();
            boardVO.setTitle("단위 테스트 작성 제목 in service");
            boardVO.setContent("단위 테스트 작성 내용 in service");
            boardVO.setWriter("newbie");
    
            service.register(boardVO);
            log.info(boardVO);
        }
    
        @Test
        public void testGetList() {
            service.getList().forEach(log::info);
        }
    
        @Test
        public void testModify() {
            BoardVO boardVO = new BoardVO();
            boardVO.setTitle("수정 단위 테스트 작성 in service");
            boardVO.setContent("수정 단위 테스트 작성 in service");
            boardVO.setWriter("newbie");
            boardVO.setBno(6L);
    
            log.info(service.modify(boardVO));
        }
    
        @Test
        public void testGet() {
            log.info(service.get(1L));
        }
    
        @Test
        public void testRemove() {
            log.info(service.remove(85L));
        }
    }

     

    🔹 테스트를 하기 전 먼저 BoardService가 존재하는지 부터 데스트 해봐야 함.
    🔹 assertNotnull임!

    🔹 글번호 타입이 Long이었으니깐 숫자 뒤에 L 붙여 주자.

     


     

    10. 프레젠테이션(웹) 계층의 CRUD 구현

    10.1 Controller의 작성

    • 하나의 클래스 내에서 여러 메서드를 작성하고, @RequestMapping 등을 이용해 URL을 분기하는 구조로 작성할 수 있기 때문에 하나의 클래스에서 필요한 만큼 메서드의 분기를 이용하는 구조로 작성함.
    • 이전에는 Tomcat(WAS)를 실행해서 웹 화면에서 결과를 확인했었는데 이 방법은 시간도 오래 걸리고, 테스트를 자동화 하기에 어려움.

     

    🎈 BoardController의 분석

    작성하기 전 원하는 기능을 호출하는 방식에 대해 정리하는 것이 좋음.

    Task - URL - Method - Parameter - form - URL 이동
    전체 목록 - /board/list - GET
    등록 처리 - /board/register - POST - 모든 항목 - 입력화면 필요 - 이동
    조회 - /board/read - GET - bno=123
    삭제 처리 - /board/remove - bno - 입력화면 필요 - 이동
    수정 처리 - /board/modify - 모든 항목 - 입력화면 필요 - 이동


     

    10.2 BoardController의 작성

     

    BoardController.java

    package site.levinni.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.servlet.mvc.support.RedirectAttributes;
    
    import lombok.AllArgsConstructor;
    import lombok.extern.log4j.Log4j;
    import site.levinni.domain.BoardVO;
    import site.levinni.service.BoardService;
    
    @Controller
    @Log4j
    @RequestMapping("/board/*")
    @AllArgsConstructor
    public class BoardController {
    
        private BoardService service;
        
        @GetMapping("/list")
        public void list(Model model) {
            log.info("list.....");
            model.addAttribute("list", service.getList());
        }
    
        @GetMapping("get")
        public void get(@RequestParam Long bno, Model model) {
            log.info("get.....");
            model.addAttribute("board", service.get(bno));
        }
        @GetMapping("register") //jsp 찾으려 함.
        public void register() {
    
        }
    
        @PostMapping("register")
        public String register(BoardVO boardVO, RedirectAttributes rttr) {
            log.info("register...." + boardVO);
            service.register(boardVO);
            rttr.addFlashAttribute("result", boardVO.getBno());
    
            return "redirect:/board/list";
    
        }
    
        @PostMapping("modify")
        public String modify(BoardVO boardVO, RedirectAttributes rttr) {
            log.info("modify....");
            if(service.modify(boardVO)) {
                rttr.addFlashAttribute("result", "success");
            }
            return "redirect:/board/list";
    
        }
    
        @PostMapping("remove")
        public String remove(@RequestParam Long bno, RedirectAttributes rttr) {
            log.info("remove....");
            if(service.remove(bno)) {
                rttr.addFlashAttribute("result", "success");
            }
            return "redirect:/board/list";
        }
    }
    

     

    🔹 @Controller를 추가해서 스프링의 빈으로 이신할 수 있게 함.
    🔹 @RequestMapping: '/board'로 시작하는 모든 처리를 BoardController가 하도록 지정.
    🔹 BoardService 타입 객체와 같이 연동해야 하므로 의존성 처리도 해야 함. (@AllArgsConstructor 이용)
    🔹 list()는 게시물의 목록을 전달해야 하므로 Model을 파라미터로 지정하고 이를 통해 BoardServiceImpl 객체의 getList() 결과를 담아 전달.
    🔹 register(): 리턴 타입 String, 파라미터 RedirectAttributes를 통해 글 등록 작업이 끝난 후 목록 화면으로 다시 이동하고 새로운 게시물의 번호를 같이 전달. 리턴 시에는 redirect: 접두어 사용.

    서블릿으로 했던 게시판 작업을 스프링으로 바꿀 때, 로그인 후 세션 처리는 세션 객체를 파라미터로 담으면 됨.

     

     

    BoardControllerTests.java

    package site.levinni.controller;
    
    import org.junit.Before;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    import org.springframework.test.context.web.WebAppConfiguration;
    import org.springframework.test.web.servlet.MockMvc;
    import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
    import org.springframework.test.web.servlet.setup.MockMvcBuilders;
    import org.springframework.web.context.WebApplicationContext;
    
    import lombok.extern.log4j.Log4j;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @WebAppConfiguration
    @ContextConfiguration({"file:src/main/webapp/WEB-INF/spring/root-context.xml",
            "file:src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml"})
    @Log4j
    public class BoardControllerTests {
        @Autowired
        private WebApplicationContext ctx;
        private MockMvc mvc;
    
        @Before
        public void setup() {
            mvc = MockMvcBuilders.webAppContextSetup(ctx).build();
        }
    
        @Test
        public void testList() throws Exception {
            log.info(mvc.perform(MockMvcRequestBuilders.get("/board/list"))
                    .andReturn()
                    .getModelAndView()
                    .getModelMap()
                    );
        }
    
        @Test
        public void testGet() throws Exception {
            log.info(mvc.perform(MockMvcRequestBuilders
                    .get("/board/get")
                    .param("bno", "88"))
    
                .andReturn()
                .getModelAndView()
                .getModelMap());
        }
    
        @Test
        public void testRegister() throws Exception {
            log.info(mvc.perform(MockMvcRequestBuilders.post("/board/register")
                    .param("title", "컨트롤러 테스트 제목")
                    .param("content", "컨트롤러 테스트 내용")
                    .param("writer", "user00")
                    )
                    .andReturn()
                    .getModelAndView()
                    .getViewName());
        }
        @Test
        public void testModify() throws Exception {
            log.info(mvc.perform(MockMvcRequestBuilders.post("/board/modify")
                    .param("title", "컨트롤러 테스트 제목 수정")
                    .param("content", "컨트롤러 테스트 내용 수정")
                    .param("writer", "user00")
                    .param("bno", "88")
                    )
                    .andReturn()
                    .getModelAndView()
                    .getViewName());
        }
        @Test
        public void testRemove() throws Exception {
            log.info(mvc.perform(MockMvcRequestBuilders
                    .post("/board/remove")
                    .param("bno", "86"))
                .andReturn()
                .getModelAndView()
                .getViewName());
        }
    }
    

     

    🔹 @Before가 적용된 메서드는 모든 테스트 전에 매번 실행됨. (필요할 때마다 객체를 만들겠다는 뜻)
    🔹 MockMvc: 가짜로 URL과 파라미터 등을, 브라우저에서 사용하는 것처럼 만들어서 Controller를 실행해 봄.
    🔹 testList()는 MockMvcRequestBuilders를 통해 GET 방식의 호출을 함.

    🔹 반환된 결과를 이용해 Model에 어떤 데이터들이 담겨 있는지 확인함.

     


    🔹 {}: 맵의 투스트링 부분
    🔹 로그가 이전보다 긴 이유- 서블릿컨텍스트도 포함하기 때문에. (이전엔 root-context만 이용)

     


    🔹testRegister() 로그 기록인데 register()의 리턴 값인 redirect:/board/list문자열을 확인 할 수 있다.


     

    11. 화면 처리 ✨

    1.1 목록 페이지 작업과 includes

    less 스크립트 기반
    sass 루비 기반 - 더 빠름

    w3school 부트스트랩 참고하기!

    다운받은 부트스트랩에서 .html을 sts로 가져온 후 내용 복사해서 실행 브라우저에서 f12로 경로 하나하나 수정.

    (5교시)

    🧩(참고) 스프링 타일즈

    헤더, 푸터, 사이드 바 등 페이지를 include 방식으로 나누는 기존 구조를 쉽게 적용하기 위한 템플릿 프레임워크.
    장점: include 디자인을 변경하면 페이지를 전체적으로 수정해야 하는 번거로움을 없애고 일괄적인 페이지 관리를 가능하도록 함.
    include 방식은 jsp가 jsp를 가져 오는 것인데, 타일즈를 사용하면 스프링에 있는 타일즈가 jsp를 관리하는 것.

     

    텍스트 -> 이미지화 ex캡챠

    갠플
    부트스트랩 테마 + 약간의 양념
    카카오로그인 ? 텍스트 에디터? 그 안에 파일첨부 ? 회원 간의 쪽지?

    텍스트 마이닝, 워드 클라우드 (파이썬 자연어 처리)


     

    ✅ 재전송(redirect) 처리

    post 방식에서 재전송 처리를 하지 않으면 사용자가 브라우저에서 '새로고침'을 통해 동일한 내용을 계속 서버에 등록할 수 있기 때문에 문제가 된다.
    따라서, 등록·수정·삭제 작업은 처리가 완료된 후 다시 같은 내용을 전송할 수 없도록 아예 브라우저의 URL을 이동하는 방식을 이용한다. 이 때, 경고창이나 모달창을 이용해 브라우저에서 등록·수정·삭제의 결과를 알 수 있게 피드백을 줘야 한다.
    addFlashAttribute()를 이용하면 일회성으로만 데이터를 사용할 수 있으므로 이를 이용해 경고창이나 모달창 등을 보여주는 방식으로 처리할 수 있다.

    '스프링 Spring' 카테고리의 다른 글

    21. 04. 12.  (0) 2021.04.13
    21. 04. 07.  (0) 2021.04.08
    Part3. 기본적인 웹 게시물 관리 21. 04. 05.  (0) 2021.04.06
    21. 04. 02.  (0) 2021.04.03
    21. 04. 01.  (0) 2021.04.01