본문 바로가기

Programming/Framework

DAY 160. Spring MVC 게시판 페이징

 

01. BoardController.java

 

- 사용자로부터 넘어오는 값을 매개 값으로 받을 것이다.

 

- @RequestParam 어노테이션이 붙어 있으면 필수로 넘어와야 하는 파라미터가 된다. (만약, 존재하지 않는 파라미터이면 Requried request parameter 'page' for method parameter type int is not present 오류가 발생

 

- 넘겨주는 값이 없어도 에러가 안나게 하는 것은 @RequestParam(required = false)

 

- BUT! @RequestParam(required = false) 로 되어 있어도 int page는 스프링 내부적으로 문자열 형태로 들어오고, 내부적으로 정수형으로 형변환을 시켜준다.

 

- 그런데 page는 존재 하지 않으니까 스프링이 문자열 형태인 "page"에 NULL을 넣게 되고, NULL을 정수 값으로 바꾸려고 해서 NullPointerException 발생 - 500 에러가 발생함

 

- 즉, 이 상태는 "필수로 넘어오는 값이 아니여도 상관없지만 정수로 변환하는 과정에서 NULL을 정수로 바꾸려는 것이기 때문에 에러가 발생"한다.

 

  • 매개변수를 int로 설정한 경우 null이 들어갈 수 없기때문에 null이 오면 바인딩 에러가 난다.
  • 이럴때는 Integer로 변경하던가 defaultValue 속성을 지정해준다.
  • defaultValue 속성을 지정해주면 required 속성이 필요없어진다.

 

 

 

@GetMapping("/board/list")
public String list(@RequestParam(required = false) int page) {

    log.info("{}", page);

    return "board/list";

}

 

 

따라서 기본 값이 설정되어있으면, 그 값을 넣어주기 때문에 defaultValue="1" 로 넣어준다.

 

이렇게 되면 required 속성이 필요 없어짐

 

@GetMapping("/board/list")
public String list(@RequestParam(defaultValue = "1") int page) {

    log.info("{}", page);

    return "board/list";

}

 

▼ 주소 창에 이렇게 page의 번호를 직접 넘겨보면 콘솔에 다음처럼 출력이 잘 된다.

http://localhost:8088/mvc/board/list?page=2

 

 

 

더보기
더보기

※ 만약 주소 창에, 정수값이 아닌 문자라든지 잘못된 URL을 입력했을 경우는 어떻게 처리할까?

 

- 파라미터를 일단 String으로 받아서 내부에서 문자열이 맞는지 확인해보고 아니면 1 넣기

 


 

 

PageInfo를 제대로 완성하기 위해서 서비스를 통해서 BoardCount랑 BoardList를 받아와야 한다.

 

▼ 이 시점까지 완료된 컨트롤러

@GetMapping("/board/list")
public ModelAndView list(
        ModelAndView model,
        @RequestParam(defaultValue = "1") int page) {

    int listCount = 0;
    PageInfo pageInfo = null;
    List<Board> list = null;

    log.info("page number : {}", page);

    listCount = service.getBoardCount();

    pageInfo = new PageInfo(page, 10, listCount, 10);

    list = service.getBoardList(pageInfo);

    model.addObject("pageInfo", pageInfo);
    model.addObject("list", list);
    model.setViewName("board/list");

    return model;
}

 

 

아직 서비스도 다오도 아무것도 없다.

 

서비스를 만들어보자. 서비스도 Bean으로 등록해서 사용할 것이다. 그래서 위에 필드로 선언

private BoardService service;

 

 

02. BoardService.java

- com > kh > mvc > board > model > service에 인터페이스로 만든다.

- 여기에 getBoardCount()getBoardList() 추상 메소드 만들기

 

여기까지만 하면 BoardService에는 초기화된 것도, 주입된 것도 없다. 그래서 JVM에 의해서 null로 값이 들어가게 되고, 서비스가 null인데 .get~~ 메소드 쓰고 있기 때문에 NullPointerException 발생한다.

 

[에러 발생 이유]

1) 구현체를 만들지 않았다. -> 구현체를 만들어야 함

2) 값이 주입되지 않았기 때문에 JVM에 의해서 NULL로 초기화 됨 -> NULL이 아니도록 값을 주입해야 함

 

 

03. BoardServiceImpl.java

- 서비스 구현체인 이 클래스를 만들기

 

- BoardServiceImpl을 빈으로 만들어야 한다. 따라서 class에 @Service 빈으로 등록 (애플리케이션이 실행되면서 해당하는 객체를 애플리케이션 컨텍스트에서 보관할 것이고)

 

- 그리고 controller에서 BoardService 빈이 필요한 시점에 주입받을 수 있도록 @Autowired 를 붙인다.

 

 

더보기
더보기

spring에서 mvc는 model, view, controller 사이 의존 관계를 DI 컨테이너를 통해서 관리하고 있다.

 

Bean 객체를 만들어서 그와 그것의 관계들을 애플리케이션 컨텍스트 안에 저장하고 관리하면서 필요할 때 주입받기도 한다.

 

mvc 요청 처리 과정 그림을 보면 웹 mvc와 관련된 객체들이 여러개가 있는데,

 

모두 애플리케이션 컨텍스트 안에 저장해놓고 필요할 때 주입받는다.

 

그래서 @autowired 가 안 붙어있으면 해당 객체는 JVM에서 null로 값을 주입해버린다.

 

컨트롤러 역시 @Controller 어노테이션이 붙어있어서 구동되는 순간 애플리케이션 컨텍스트에서 빈(객체)로 만든다. 필드에 값이 없으면 NULL이 들어가고 나중에 사용자로부터 요청이 왔을 때 nullpointerexception이 발생할 수 있다.

 

따라서 @Autowired 를 붙여서 빈으로 주입받을 수 있도록 하고,

 

실제 그 구현체에는 @Service 어노테이션을 붙여서 애플리케이션 컨텍스트 안에 Bean으로 관리되도록 해야 한다.

그럼 연관 관계도 만들어지게 된다.

 

BoardServiceImpl 역시 BoardService타입에 해당하는 빈으로 구현체는 관리되고 있을 것이다.

 

제대로 주입이 되었다면, NULL 예외 에러는 발생하지 않는다.

 


 

 

먼저, BoardCount() 메소드 부터 디테일하게 구현해보자. 데이터베이스에서 게시글의 갯수를 알아올 때 사용할 메소드임.

 

쿼리를 수행할 수 있는 객체인 board-mapper.xml에 쿼리문를 만들어보자.

 

그리고 다음과 같이 작성

 

@Autowired
private BoardMapper mapper;

@Override
public int getBoardCount() {

    return mapper.getBoardCount();
}

 

 

04. BoardMapper.java

- 인터페이스로 생성

 

- com > kh > mvc > board > model >  dao 에 생성

 

- 추상 메소드 생성 int getBoardCount();

 

- 여기까지만 하면 NullPointerException 발생! (위와 마찬가지의 이유)

 

[에러 발생 이유]

1) 구현체를 만들지 않았다. -> 구현체를 만들어야 함

2) 값이 주입되지 않았기 때문에 JVM에 의해서 NULL로 초기화 됨 -> NULL이 아니도록 값을 주입해야 함

 

- but 우리는 구현체를 직접 만들지는 않고, mybatis-context.xml 에 mapperScanner가 만들어 주게 하도록 

BoardMapper.java 인터페이스에 @Mapper 어노테이션을 붙인다.

 

- board-mapper.xml에 만들어놓은 쿼리를 연결하는 방식을 사용할 것이기 때문에

 

 

[필수 작업]

1) board-mapper.xml 에 mapper의 namespace를 바꿔야 한다. HOW? 풀패키지명.인터페이스명

2) 추상메소드와 쿼리문 ID를 동일하게 맞춘다.

 

 


 

이제 게시글 목록을 가져와보자.

 

05. BoardServiceImpl.java

@Override
public List<Board> getBoardList(PageInfo pageInfo) {

    int offset = (pageInfo.getCurrentPage() - 1 ) * pageInfo.getListLimit();
    int limit = pageInfo.getListLimit();
    RowBounds rowBounds = new RowBounds(offset, limit);

    return mapper.findAll(rowBounds);
}

 

- mybatis에서 페이징하기 위해 RowBounds 객체를 만들었던 것을 활용해보자.

 

- 지금은 다오를 직접 구현하지 않기 때문에 서비스에서 만들어서 mapper한테 전달하면 된다.

 

- board-mapper.xml에 findAll 쿼리 준비

 

 


 

06. BoardController.java

 

/** *
* @param currentPage 현재 페이지
* @param pageLimit 한 페이지에 보이는 페이지의 수
* @param listCount 전체 리스트의 수
* @param listLimit 한 페이지에 표시될 리스트의 수
*/

 

- 게시글 listLimit을 사용자가 원하는대로 조절(사용자로부터 받아와보자)

 

 

@Slf4j
@Controller
public class BoardController {
	
	@Autowired
	private BoardService service;

	// ▼ 페이징
	@GetMapping("/board/list")
	public ModelAndView list(
			ModelAndView model,
			@RequestParam(defaultValue = "1") int page,
            // pageInfo 객체의 마지막 매개값으로 쓸 것임(사용자가 넘겨주지 않으면 기본으로 10개씩 보임)
			@RequestParam(defaultValue = "10") int count) {
		
//		int listCount = 0;
		PageInfo pageInfo = null;
		List<Board> list = null;
		
		log.info("page number : {}", page);
        
		// pageInfo 객체의 3번째 매개값으로 코드 정리
//		listCount = service.getBoardCount();
		
		pageInfo = new PageInfo(page, 10, service.getBoardCount(), count);
		
		list = service.getBoardList(pageInfo);
		
		model.addObject("pageInfo", pageInfo);
		model.addObject("list", list);
		model.setViewName("board/list");
		
		return model;
		
	}
	

	
}

 

localhost:8088/mvc/board/list?count=5

localhost:8088/mvc/board/list?page=17&count=5

 

- 이런식으로 page를 주지 않고 count만 주면, 한 페이지에 5개의 글씩 보이게 된다.

 

- 지금 이 상태에서는 url 창에 count5로 해도, 다른 페이지를 선택하면 다시 원래대로 10개씩 글이 보인다. (유지되지는 않음)

 

- 이것도 유지하게 하려면 어떻게 하면 될까?

 

 

07. list.jsp

 

<!--  10개 페이지 목록 -->
<c:forEach begin="${ pageInfo.startPage }" end="${ pageInfo.endPage }" varStatus="status">
    <c:if test="${ status.current == pageInfo.currentPage }">				
        <button disabled>${ status.current }</button>
    </c:if>

    <c:if test="${ status.current != pageInfo.currentPage }">				
    	// 여기부분에 &count=${pageInfo.listLimit} 추가하였음
        <button onclick="location.href='${ path }/board/list?page=${ status.current }&count=${ pageInfo.listLimit }'">${ status.current }</button>
    </c:if>
</c:forEach>

 

- 개수가 유지될 수 있도록 pageInfo.listLimit 의 값도 같이 넘겨버리면 된다. 여기에서 넘기는 값은 BoardController.java에 @RequestParam(defaultValue = "10") int count 에서 받는다.

 

 


 

 

※ 드롭다운으로 10개씩 보기 20개씩 보기 만들어보세요!

※ 검색 기능 URL에 같이 태워 보세요. mapper에 sql문도 좀 달라야 할 것

 

 

검색 기능 추가한다면 페이지 이동할 때 검색결과가 유지되게 하려면 url에 같이 넘겨버리면 된다.

 

- 화면에 검색 기능 만들기

- 몇개씩 보기 select로 만들기 select 박스 하나 입력받는 텍스트 창 하나 선택하느냐에 따라서 select 값에선 type으로 content, writer, title인지 넘겨주고 키워드도 같이 넘겨줘야함

 

- 쿼리문에서 type 값이 wrtier인지 title인지 content인지 확인해서 쿼리문 태우기

#{키워드} 넘겨주기

- 검색버튼 만들기 요청하는거 만들기 location.href='${path}' ~~

 

<select id="findAll" parameterType="map" resultMap="boardListResultMap">
    <include refid="boardListSql">
    <choose>
    	<when test="type = writer">
        	AND M.ID LIKE '%' || #{keyword}  || '%'
        </when>
        <when test="type = title">
        	AND M.ID LIKE '%' || #{keyword}  || '%'
        </when>
        <when test="type = contentr">
        	AND M.ID LIKE '%' || #{keyword}  || '%'
        </when>
    </choose>
</select>