본문 바로가기

Spring/Servlet

뉴렉처 서블릿4 - 공지사항 게시판 만들기 예제

코드의 분리

필요에 따라서, 작업 범위에 따라서 코드를 잘개 분리할수도 있고, 통합적으로 처리할 수 있다.

기본적으로 사용자의 요청을 처리하고 제어를 담당하는 Servlet(Controller), 문서를 출력하는 View(.jsp)가 존재한다.

혼자 개발하고 서비스할 때는 단순히 코드를 둘로 분리해도 좋지만,

여럿이서 협업하고 분업해야 할 때는 업무서비스(트랜잭션)을 사용자 입출력 Servlet과 분리하는 게 보편적이다.

예를 들어 계좌이체같은 요청이 오면 Servlet은 이를 업무서비스로 전달하고 결과를 받아 문서로 받아  view단으로 전달한다.

이를 통해 실수를 최소화하고 업무서비스의 재사용이 가능해진다.

Servlet의 경우 변경사항이 비교적 많기 때문에 이런 코드의 분리는 협업에서 더욱 효율을 높인다.

물론 재사용 없이 통째로 갈아엎는 게 나을 경우 합치는 게 낫다.

 

또한 업무를 처리할 때 주로 자바를 이용하는데, 데이터 소스나 데이터와 관련된 sql을 처리하는 계층을 하나 더 분리할 수 있다.

 데이터서비스(DAO, CRUD)를 분리할 수 있다.

 

정리하자면 

중간에 업무를 처리하는 단계를 세분화하여 기업형에서 보편적인 업무 단위는

사용자와의 상호작용을 담당하는 Servlet(Controller), 업무서비스(트랜잭션), 데이터서비스(DAO), view단(.jsp)으로 나뉜다.

- Servlet & 업무서비스 | DAO | view단으로 나누거나 Servlet | 업무서비스 &DAO | view단으로 나누기도 한다.

- sql문을 작성할 수 있는 사람이 DAO 처리해서 Entity(구조화된 자바 데이터 형태)로 업무서비스로 전달

- 업무처리해서 결과에 대한 데이터 model을 Servlet으로 전달

- Servlet에서 .jsp뷰단으로 model 전달 

 


공지사항 게시판 만들기 예제

 

1. 서비스 함수 찾아내기

    공지사항 결과물을 대략적으로 참고하여, 요청을 정리한 후 메소드로 변환한다 [NoticeService.class]

   

List<NoticeView>getNoticeList() int getNoticeCount() Notice getNotice(int id) Notice getNextNotice()
Notice getPrevNotice()
리스트 가져오기
1.기본 2.(int page)
개수 세기 (페이지생성)
1.기본
자세한 내용 가져오기 이전 글 가져오기
다음 글 가져오기
카테고리, 검색어 입력
3.(String field, String query, int page)
카테고리, 검색어 입력결과 수
2.(String field, String query)
   

오버로드한 메소드는 기능이 유사하다는 건데, 이는 하나를 구현하면 다른 것은 구현 메소드를 호출해 쓸수 있다는 걸 의미한다.

이때 가장 인자가 많은 메소드3를 구현해서 다른 메소드들1,2에서 없는 인자에 기본값을 넣어 호출한다.

 

2. SQL Developer에서 관련 SQL을 작성하고, 이클립스로 가져온다

해당 페이지에 부분적으로 출력할 결과물을 가져오는 방법

두 방법 모두 성능 면에서 비슷하다.

          getNoticeList

     ① rownum & 서브쿼리 사용

         - rownum을 쓰는 순간 실제 컬럼들을 *이라고 통칭할 수 없다. 테이블명[혹은 애칭].*로 써야한다.

            또한, 본쿼리에서도 rownum이 존재해 충돌하므로 조건절에서 rownum을 사용하기 위해 애칭을 붙인다.

         - 서브쿼리를 쓰는 이유

            rownum을 출력한 후 정렬이 되기 때문에 실제 정렬 결과에 맞지 않은 rownum이 출력된다.

            정렬을 사용하여 원하는 값만 출력하고 싶을 때는 서브쿼리를 한 번 거친 결과를  from절에 넣어 그 안에서 다시 출력한다.

           서브쿼리의 결과에 애칭을 붙일 수 있는데, 여기서는 N을 사용했다.

         - 서브쿼리를 두 번 쓰는 이유 

            rownum의 특성상 먼저 특정되기 때문에 between 1부터 시작하지 않는 조건절을 사용할 수 없다.

            따라서 이러한 조건절을 사용하기 위해 정렬을 적용한 서브쿼리 결과를 from절 안에 넣고 다시 한번 조건절을 적용해 출력한다.

 select * from
     (select rownum num, N.* //2.rownum 애칭을 준 후 일반적인 컬럼처럼 사용
     from (select * from notice_view order by regdate desc) N)      //1.정렬
     where num between 6 and 10; //3.조건 적용

    ② row_number() over & 서브쿼리 사용

select * from
    (select row_number() over (order by regdate desc) num,     //over절 안에 정렬
    notice2.* from notice2)
    where num between 6 and 10;

     ③ 페이지수 구하기

         - 첫페이지 1, 11, 21, 31 등차수열 an = a1 + (n-1)*10 →  1+(page-1)*10

         - 마지막페이지 10, 20, 30, 40 → page*10

         - ?에 꽂아넣기 st.setInt(2, 1+(page-1)*10); | st.setInt(3, page*10);

 

     ④ field like ?

         - 변수 field는 직접 쿼리문 중간에 넣고

         - 실제 검색어값 query는 setString으로 넣는다.  st.setString(1, "%"+query+"%")

 

    ⑤ 목록을 하나씩 꺼내 NoticeView에 넣고 list에 추가

          - Notice 클래스 추가 (기본생성자, 명시적생성자, getter&setter, toString 만들어둬야함)

          - Notice를 사용해도 좋으나, 댓글수를 가져오기 위해 NoticeView로 바꾼다.

             오라클에서 NoticeView 생성

 

          getNextNotice() [ getPrevNotice() ]

   ① 다음[이전] 글 id 알아내기

          select id from notice2 where regdate > (select regdate from notice2 where id=?)

   ② 출력문 where절에 꽂아넣기

 

 

그 외 쿼리문 

더보기

NOTICE_VIEW

     SELECT N.*(N.ID,  N.TITLE, N.WRITER_ID, N.REGDATE, N.HIT, N.FIELS)COUNT(C.ID) FROM NOTICE2 N                           LEFT JOIN "COMMENT' C ON N.ID = C.NOTICE_ID

     GROUB BY N.ID,  N.TITLE, N.WRITER_ID, N.REGDATE, N.HIT, N.FIELS

   ORDER BY N.REGDATE DESC;

 

getNoticeList

"SELECT * FROM (

    SELECT ROWNUM NUM, N.*

    FROM (SELECT * FROM NOTICE_VIEW WHERE" + FIELD + "LIKE ? ORDER BY REGDATE DESC) N

    ) WHERE NUM BETWEEN ? AND ?"       검색어        (변수  - 검색어)               최신순 정렬

 

getNotice

"SELECT * FROM NOTICE WHERE ID=?"

 

getNextNotice

"SELECT * FROM NOTICE2 
 WHERE ID = (
     SELECT ID FROM NOTICE2 
     WHERE REGDATE > (SELECT REGDATE FROM NOTICE2 WHERE ID=?) 
     AND ROWNUM =1)";


getPrevNotice

"SELECT ID FROM (SELECT * FROM NOTICE2 ORDER BY REGDATE DESC)
WHERE REGDATE < (SELECT REGDATE FROM NOTICE2 WHERE ID =?) AND ROWNUM=1"

getNoticeCount

"SELECT COUNT(ID) COUNT FROM             //id의 개수 집계함수 사용

        SELECT ROWNUM NUM, N.*

        FROM (SELECT * NOTICE2 WHERE" + FIELD + "LIKE ? ORDER BY RERDATE DESC) N)"

 

NOTICE_VIEW

     SELECT N.*(N.ID,  N.TITLE, N.WRITER_ID, N.REGDATE, N.HIT, N.FIELS)COUNT(C.ID) FROM NOTICE2 N                           LEFT JOIN "COMMENT' C ON N.ID = C.NOTICE_ID

     GROUB BY N.ID,  N.TITLE, N.WRITER_ID, N.REGDATE, N.HIT, N.FIELS

   ORDER BY N.REGDATE DESC;

 

getNoticeList

"SELECT * FROM (

    SELECT ROWNUM NUM, N.*

    FROM (SELECT * FROM NOTICE_VIEW WHERE" + FIELD + "LIKE ? ORDER BY REGDATE DESC) N

    ) WHERE NUM BETWEEN ? AND ?"       검색어        (변수  - 검색어)               최신순 정렬

 

getNotice

"SELECT * FROM NOTICE WHERE ID=?"

 

getNextNotice

"SELECT * FROM NOTICE2 
 WHERE ID = (
     SELECT ID FROM NOTICE2 
     WHERE REGDATE > (SELECT REGDATE FROM NOTICE2 WHERE ID=?) 
     AND ROWNUM =1)";


getPrevNotice

"SELECT ID FROM (SELECT * FROM NOTICE2 ORDER BY REGDATE DESC)
WHERE REGDATE < (SELECT REGDATE FROM NOTICE2 WHERE ID =?) AND ROWNUM=1"

getNoticeCount

"SELECT COUNT(ID) COUNT FROM             //id의 개수 집계함수 사용

        SELECT ROWNUM NUM, N.*

        FROM (SELECT * NOTICE2 WHERE" + FIELD + "LIKE ? ORDER BY RERDATE DESC) N)"

 

NOTICE_VIEW

     SELECT N.*(N.ID,  N.TITLE, N.WRITER_ID, N.REGDATE, N.HIT, N.FIELS)COUNT(C.ID) FROM NOTICE2 N                           LEFT JOIN "COMMENT' C ON N.ID = C.NOTICE_ID

     GROUB BY N.ID,  N.TITLE, N.WRITER_ID, N.REGDATE, N.HIT, N.FIELS

   ORDER BY N.REGDATE DESC;

 

getNoticeList

"SELECT * FROM (

    SELECT ROWNUM NUM, N.*

    FROM (SELECT * FROM NOTICE_VIEW WHERE" + FIELD + "LIKE ? ORDER BY REGDATE DESC) N

    ) WHERE NUM BETWEEN ? AND ?"       검색어        (변수  - 검색어)               최신순 정렬

 

getNotice

"SELECT * FROM NOTICE WHERE ID=?"

 

getNextNotice

"SELECT * FROM NOTICE2 
 WHERE ID = (
     SELECT ID FROM NOTICE2 
     WHERE REGDATE > (SELECT REGDATE FROM NOTICE2 WHERE ID=?) 
     AND ROWNUM =1)";


getPrevNotice

"SELECT ID FROM (SELECT * FROM NOTICE2 ORDER BY REGDATE DESC)
WHERE REGDATE < (SELECT REGDATE FROM NOTICE2 WHERE ID =?) AND ROWNUM=1"

getNoticeCount

"SELECT COUNT(ID) COUNT FROM             //id의 개수 집계함수 사용

        SELECT ROWNUM NUM, N.*

        FROM (SELECT * NOTICE2 WHERE" + FIELD + "LIKE ? ORDER BY RERDATE DESC) N)"

 

NOTICE_VIEW

     SELECT N.*(N.ID,  N.TITLE, N.WRITER_ID, N.REGDATE, N.HIT, N.FIELS)COUNT(C.ID) FROM NOTICE2 N                           LEFT JOIN "COMMENT' C ON N.ID = C.NOTICE_ID

     GROUB BY N.ID,  N.TITLE, N.WRITER_ID, N.REGDATE, N.HIT, N.FIELS

   ORDER BY N.REGDATE DESC;

 

getNoticeList

"SELECT * FROM (

    SELECT ROWNUM NUM, N.*

    FROM (SELECT * FROM NOTICE_VIEW WHERE" + FIELD + "LIKE ? ORDER BY REGDATE DESC) N

    ) WHERE NUM BETWEEN ? AND ?"       검색어        (변수  - 검색어)               최신순 정렬

 

getNotice

"SELECT * FROM NOTICE WHERE ID=?"

 

getNextNotice

"SELECT * FROM NOTICE2 
 WHERE ID = (
     SELECT ID FROM NOTICE2 
     WHERE REGDATE > (SELECT REGDATE FROM NOTICE2 WHERE ID=?) 
     AND ROWNUM =1)";


getPrevNotice

"SELECT ID FROM (SELECT * FROM NOTICE2 ORDER BY REGDATE DESC)
WHERE REGDATE < (SELECT REGDATE FROM NOTICE2 WHERE ID =?) AND ROWNUM=1"

getNoticeCount

"SELECT COUNT(ID) COUNT FROM             //id의 개수 집계함수 사용

        SELECT ROWNUM NUM, N.*

        FROM (SELECT * NOTICE2 WHERE" + FIELD + "LIKE ? ORDER BY RERDATE DESC) N)"

 


목록 페이지에서 검색 추가하기

쿼리스트링과 EL태그를 사용하여 검색을 추가하고, 새로고침된 페이지에서도 검색어와 필터를 그대로 적용해 보여줄 수 있다.

1. 뷰단에서 value값으로 f, q, p 넘겨주기

     f : field | q : query(검색어) | p : page

     삼항연산자 활용해서 페이지가 로드될 때 해당 값이 selected될 수 있도록 한다.

     카테고리의 value값을 실제 테이블 컬럼과 일치시키면 바로 쿼리값에 해당 field를 넣어줄 수 있기 때문에 용이하다.

 

 

2. Controller에서 값 받아서 서비스 함수 호출

    각각의 매개변수들에 기본값을 준 다음 사용자 입력값이 존재할 경우 변경

 

목록에서 페이징 구현

     - 현재 페이지 번호 출력

        EL empty 연산자와 삼항연산자 활용 - 빈문자열과 null일 때 모두 참

        EL을 통해 조건에 맞을때만 클래스를 추가할 수도 있음 (현재 페이지만 강조하기)

        태그의 큰따옴표 안에 EL이 있을 때 EL안에서는 작은 따옴표를 써야한다.

     - 마지막 페이지 번호 출력

      ① 전체 개수/한페이지당 출력수를 올림하기 위해 Math.ceil 정적함수 사용

      ② EL 태그안에서는 연산이 가능한데, 이때 나누기 연산은 실수를 반환한다. 정수로 만들기 위해 substringBefore 함수 사용

 

자세한 페이지 수정

서비스함수에서 데이터를 만들고 있기 때문에 컨트롤러에서는 사용자 입력과 출력만을 담당한다.

1. id값을 받아 서비스 객체 생성 후 getNotice(id)서비스 함수 호출

      Notice notice = service.getNotice(id);

2. n이라는 키워드에 리턴값 Notice객체 넣어 전달

     request.setAttribute("n",notice);

     request.getRequestDistpatcher(/경로).forward(request,response)

목록에 댓글 수 포함 - 쿼리 : view사용

1. notice와 comment 테이블 조인

     - "comment"는 예약어이기 때문에 테이블명으로 사용하거나 쿼리문에서 사용할 때 큰따옴표 안에 작성

     - 이너조인의 경우 자식테이블 기준으로 결과가 출력되기 때문에 outer조인을 사용

     - Inner Join에서 Inner 생략 가능,  Left Outer Join에서 outer 생략 가능

2. getNoticeList 함수 쿼리문 수정

     - 목록별 댓글 개수를 view로 만든다 [목륵 만들 때 많이 사용]

     CREATE VIEW NOTICE_VIEW

     AS

     SELECT N.*(N.ID,  N.TITLE, N.WRITER_ID, N.REGDATE, N.HIT, N.FIELS)COUNT(C.ID) CMT_COUNT FROM NOTICE2 N      

     LEFT JOIN "COMMENT' C ON N.ID = C.NOTICE_ID

     GROUB BY N.ID,  N.TITLE, N.WRITER_ID, N.REGDATE, N.HIT, N.FIELS;

   -- ORDER BY N.REGDATE DESC;

 

- GROUP BY 절은 ORDER BY절 위에 오며, N.*을 쓸 수 없다

- CONTENT의 경우 BLOB 대용량 데이터 형식을 가진다.

4000자를 제한해서 VARCHAR로 변환해서 쓰거나, 다른 방법 필요

- 데이터 필터 및 정렬은 뷰에 포함하지 않고

원본 데이터만 포함 시키는 게 좋다.

- 댓글 개수 컬럼을 추가하기 위해 GROUP BY절의 사용은 불가피

- 정렬은 본쿼리에서 한다.

 

 

 

 

 

 

 

 

 

 

3. NOTICE를 상속하는 NoticeView 클래스 생성


관리자 페이지

pubNoticeAll(ids) removeNoticeAll(ids) getNoticeNewestList()
일괄 공개 요청 일괄 삭제 요청 최신글목록 페이지 요청
insertNotice(notice) deleteNotice(id) updateNotice(notice)
공지 등록 요청 공지 삭제 요청 공지 수정 요청

 

 

관리자 Index페이지 (87강)

IndexController를 복붙해 경로 수정 WEB-INF/view/admin/index.jsp | 요청 url도 admin/index로 수정

한 프로젝트에 동일한 클래스 이름이 둘 이상인 경우에는 URL이 정상적으로 입력되지 못한다.따라서

IndexController가 두개가 되어 클래스와 관련된 url 매핑을 자동입력하지 못하고 클래스 주소를 그대로 노출(에러)

 

 

 

일괄 공개, 일괄삭제 (89~92강)

xController를 복붙해 경로 수정 WEB-INF/view/admin/index.jsp | 요청 url도 admin/index로 수정

한 프로젝트에 동일한 클래스 이름이 둘 이상인 경우에는 URL이 정상적으로 입력되지 못한다.

따라서 IndexController가 두개가 되어 클래스와 관련된 url 매핑을 자동입력하지 못하고 클래스 주소를 그대로 노출(에러)

 

//405 : url은 있는데 처리할 메소드가 없을 때 뜨는 오류  (메소드 오류)
//404: url이 없을 때 (url오류)
//403: url, 메소드 있는데 권한이 없다(보안오류)




① list.jsp
<form action="list" method="post">
checkboc 태그에 value값
체크한 value값만 전달됨
action을 설정하지 않아도 url(list)를 따라 현재 페이지를 요청하지만, 브라우저 호환성에 따라 안될 수 있기 때문에 설정 


② ListController.java
요청을 처리할 doPost() 메소드 선언
여기서
doGet은 단순 같은 페이지 출력한다면
doPost는 submit 요청처리

일괄삭제
삭제 처리는 service로직이 담당
deleteNoticeAll(ids) 호출

post(삭제)를 처리하고 나면
다시 페이지 호출 doGet
(=response.sendRedirect("list")

보통의 Controller 흐름
Post로 처리하고, 마지막에 sendRedirect로 처리 후의 페이지로 이동
③ Service로직
in(2, 4, 5) 같은 형태를 구현하기 위해
preparedStatement 사용 불가

대신 쿼리를 중간에 끊고,
반복문을 통해 2,4,5같은 형태를 만든다.



요청을 쉽게 하기 위한 꼼수
"/admin/board/notice/list"라고 매핑
list 클릭 후 Ctrl+F11 실행하면 자동적으로 url 입력됨

 

 

공지사항 등록 (94강)

ID에 대해 시퀀스 생성 후 해당 컬럼을 시퀀스로 등록해야 함

시퀀스 생성

캐시 : 일련번호를 뽑을때마다
그때그때 계산해서 뽑기에는 오래 걸리기 때문에, 미리 뽑아놓을 수 있음.
성능 향상

주기 : max값 이후로 순환하게 할 것인지

정렬 
: 캐시에 있는 데이터 쓸 경우 정렬을 보장할지 말지 여부
특정 컬럼 시퀀스 지정
테이블 편집 → ID열 → 시퀀스 넣기

 

 

파일업로드 (96강~103강)

ID에 대해 시퀀스 생성 후 해당 컬럼을 시퀀스로 등록해야 함

 

더보기

entype에 따른 전달 방식


설정하지 않았을 때(기본값)
 

요청에 대한 정보를 문자열로 매핑해서 전송
form 태그의 기본 enctype
쿼리스트링처럼 문자열 url처럼 매핑하고 있음. 
물론 post방식이라 url X, 파일명만 전송하지 파일 전송 하지 못함
multipart/form-data로 했을 때


파일은 파일로 옴
 

 

 

파일 업로드를 위해 서블릿을 설정해야 하는데, web.xml에 설정하거나 Annotation으로 설정한다.

여기서는 어노테이션을 이용한다.

 

MultipartConfig 설정 속성

location ="/tmp", 파일 용량이 일정 크기를 넘어설 때 저장할 절대경로
절대 경로는 서비스를 실행하는 리눅스와 윈도우즈에 차이가 있으므로
차라리 설정을 안하고 자바가 지정된 임시 디렉토리를 사용하도록 권장한다.

fileSizeThreshold=1024*1023 파일 용량이 일정 크기를 넘어설 때 디스크를 사용해서 임시에 저장하도록 함.
1MB를 넘어서면 메모리가 아닌 디스크 사용
maxFileSize=1024*1024*5 하나의 파일 최대 사이즈 제한 5MB
maxRequestSize=1024*1024*5*5 총 25MB 이하로만 수용, 디도스 공격방지

 

@MultipartConfig(
    fileSizeThreshold=1024*1023, 
    maxFileSize=1024*1024*50, 
    maxRequestSize=1024*1024*50*5 
)

 

 

input type="file"받기

 

1. Part filePart = request.getPart("file") 전달한 name값을 가지고 특정 part를 읽어옴

    바이너리 정보를 넘겨주는데, 이를 getInputStream을 통해 받는다

 

    저장 경로는 상대경로를 쓸 수 없고, 물리경로가 필요함      "/upload/" ->"c:\temp\upload"

    저장 경로를 어떻게 동적으로 알아낼 수 있을까?

2.  String realPath = request.getServletContext().getRealPath("/upload")

     루트로부터의 상대경로를 매개값으로 주면 실제 물리경로를 리턴한다.

      ※우리가 이클립스에서 보는 WebContent 안의 디렉토리들은 개발을 위한 워크스페이스에 불과하다.

           파일이 실제로 저장되는 upload폴더는 .metadate/~에 존재한다.

3. InputStream fis = filePart.getInputStream()

    리턴되는 객체(FileInputStream) 또한 인풋스트림의 일종이므로, 자료형을 InputStream 인터페이스로 지정

     cf.System.in.이 가지는 메소드와 fis.가 가지는 메소드가 거의 유사한데, 같은 InputStream을 구현하는 객체이기 때문dlek

 

4.   인풋스트림을 통해 데이터를 읽어온다 [여러 문자열이면 Scanner, 단순 파일명은 read 메소드로 충분]

      fis.read()는 1바이트값을 읽어오는데, 반환타입은 byte이 아닌 int

      스트림의 맨끝까지 읽어 더이상 읽어올 게 없을 때 -1 정수형 반환 (-를 확인하기 위해 int, 실제로 전달되는 건 byte)

      하지만 실제로 사용자가 전달하는 파일은 1바이트가 아니므로, 반복문을 통해 전부 읽어온다.

    int b;
    while((b=fis.read())!=-1) //담아온 값이 -1인지 확인

4-1. 읽어온 값을 저장한다.

     String fileName = filePart.getSubmittedFileName() //파일명 얻기

     String filePath = realPath + File.separator + fileName ; //구분자를 포함해 파일경로 만들기

여기서 \\를 직접 넣지 않는 까닭은 역슬래쉬는 윈도우즈 시스템에 국한되어 사용되기 때문에

 

4-2. 출력버퍼

        FileOutputStream fos = new FileOutputStream(filePath); //출력스트림 생성

        fos.write(buf, 0, size)

        read()함수가 아닌 read(byte[] b) 오버로드 함수를 통해 더 많은 값을 한번에 읽어오고

        마찬가지로 write(byte[], off, len) 오버로드 함수를 이용해서 파일을 출력한다.

    int b;
    while((b=fis.read())!=-1) //담아온 값이 -1인지 확인
    	fos.write(b);		//1바이트씩 읽어서 쓰려면 너무 오래걸림
-----------------------------------------------------------------
따라서 오버로드 함수 read(byte[] b)사용

byte[] buf = new byte[1024]; //byte는 정하기 나름 여기서는 1kb의 단위로 읽어오기로 함.
int size =0;				//초기값은 0, 여기서는 읽어온 데이터 byte의 개수를 반환한다. 
//1024byte 최대용량 중, 이번에 읽어온 용량이 320byte라면 그만큼만 저장되고 320 반환
// 1바이트 단위의 내용이 buf의 각 인덱스에 저장됨
while(size = fis.read(buf) !=-1) //더이상 읽어온 게 없다면 -1
	fos.write(buf,0, size); //size변수의 길이만큼 0인덱스부터 읽어서 write


//1024bytes씩 읽어오다가, 마지막 단위에서는 읽어온 만큼만 write하게 됨
//따라서 fos.write(buf)가 아닌 다른 오버로드함수 fos.write(buf, off, len)을 씀

read(), write() 복습

더보기

파일 업로드를 하면서 다시 FileInputStream, FileOutputStream, read()에 대해 접하는데 기억이 안나서 라이브러리를 찾아본다.

read()가 1바이트씩 읽고 더이상 읽을 바이트가 없을 때 -1을 리턴하기 때문에 속도가 느리다.

대체하여 read(b) 혹은 read(byte[] b, int off, int len)을 사용할 수 있는데,

read(b)의 경우에는 처음부터 끝까지 읽고 세번째 오버로드 메소드는 시작과 끝 인덱스를 정해 읽는다.

 

read(b)

- 인풋 스트림으로부터 bytes를 읽어 1바이트씩 byte 배열 b의 각 인덱스에 저장한다.

  [배열의 크기만큼 bytes단위로 한번에 읽어오고 저장하되 각 열에 1byte 단위로 저장]

- 실제로 읽은 수는 Integer 데이터타입을 가진다.

- 데이터가 유효할때까지, 파일 끝까지 읽을 때까지, 예외가 발생할때까진 블락된다. (이해가 안되네)

- b 배열의 길이가 0이고 읽어온 바이트가 없다면 0이 리턴된다.

- 읽는다면 최소 1바이트를 읽고, 모두 읽어서 스트림 끝에 달했을 때 -1을 리턴한다.

- 처음으로 읽은 바이트는 b[0]에, 이후 차례로 저장된다. 읽어진 바이트들의 총 수는 최대 배열의 길이와 같다. 

  한번에 읽혀진 바이트들의 개수가 k라고 가정하면 b[0]부터 b[k-1]까지 저장된다.

- read(b)는 매개변수 3개를 가지는 오버로드 함수 read(b, 0, b의길이)와 같음

 

write()

출력버퍼 OutputStream 객체의 메소드

 

다중파일업로드

1. Controller에서 collection으로 받기

   - getParts()

 사용하여

Collection<Part>

자료형으로 받은 뒤 반복문

2. StringBuilder

를 통해 여러 파일명을 하나의 문자열로 처리한다.

    append()

로 파일명 추가

, delete()

로 마지막 구분자 삭제

builder.delete(builder.length()-1,builder.length()); //마지막 character 삭제

3. 뷰단에서 파일명 클릭시 다운로드

      - <a download href="/경로/${파일명}">

 

4. 쓰고자하는 경로에 폴더가 없을때 만들어주기 [Controller]

    - File path = new File(path)에서 위에서 설정해준 경로를 매개값으로 가지는 파일 객체를 생성한 후

      exists 메소드를 통해 실제 해당 경로가 유효하지 않다면(특정 디렉토리 누락) mkdir[s] 메소드를 통해 폴더를 만든다.

5. 파일을 첨부하지 않았을 때 예외처리

 - getSize메소드로 part의 사이즈체크하여 0일때 반복문을 벗어난다.

   - 남은 것 :  동일 파일명 중복 방지 정책

 

 


사용자페이지 공개 게시물만 보도록 조건처리

1.list.jsp 공개 여부에 따라 체크박스 출력

    checkbox input태그 안에 checked옵션을 주되, pub=1인 게시글에만 선택적으로 조건처리한다.

<c:set var="open" value=""/> //기본값 빈문자열
	<c:if test="${n.pub}"> //n.pub값이 존재할때, 좀더 면밀하게 하면 n.pub-=1
		<c:set var="open" value="checked"/>
	</c:if>

 

 

2. 해당페이지에 출력되는 게시글들의 id목록과 체크된 게시글들의 id목록을 함께 전달해야 정확히 변경사항을 반영할 수 있다.

    이를 위해 hidden타입의 인풋태그를 사용

 

<c:set var="ids" value=""/> <!-- 기본값은 빈문자열 -->
	<c:forEach var="n" items="${list}">
		<c:set var="ids" value="${ids} ${n.id}"/> <!-- 사이뜨기로 구분된 아이디값 전송됨 -->
	</c:forEach>
<input type="hidden" name="ids" value="${ids}">

 

 

3. 공개여부에 따른 출력 처리는 ListController에서 한다

    - 해당 페이지 모든 게시글들의 id를 문자열 연산한 ids를 받아 ids_에 저장

    -  trim, split 메소드로 값들만 따로 뽑아 String 배열에 저장

    - 전체 리스트에서 공개해야 할 리스트를 제외하여 그 변경값을 update하는 로직이다.

      이를 위해 배열을 리스트로 변환하는 것이 메소드 활용에 유리하다.

      ① Arrays.asList(ids) : 배열을 리스트로 변환 (이때, 반환되는 리스트는 정적 길이의 리스트 객체이기 때문에 수정이 불가능하다.)

      ② new ArrayList( 반환 리스트 ) : 따라서 해당 내용을 가지는 새로운 객체 ArrayList를 선언한다.

      ③ cids.removeAll(oids) : 전체리스트에서 공개리스트를 뺀 결과, 즉 공개리스트를 손쉽게 만들 수 있다.

     - 공개, 비공개를 따로 처리할 수 있지만 Transaction 처리 측면에서 서비스함수는 하나로 만들어지는 것이 바람직하다.

                                                                             cf.업무적인(논리적인) 수행단위가 한번에 실행되어야 하기 때문에

      따라서 oids, cids를 모두 매개값으로 가지는 하나의 서비스 함수 pubNoticeAll을 호출한다.

 

 

 

4. 자료형식의 다변화 (서비스함수 pubNoticeAll 오버로드)

서비스 로직 처리시 유연하게 여러 개의 오버로드된 메소드를 제공하고, 가장 쉽게 사용할 수 있는 것을 선택할 수 있게 한다.

오버로드된 메소드들, 하나만 구현하고 다른 메소드들은 하나를 호출하게 하면 된다.

 

경우에 따라 선택해서 매개값을 전달할 수 있도록 여기서는 세 가지의 메소드가 선언된다.

가장 최초의 형태 배열리스트 SQL에 바로 사용할 수 있는 하나의 문자열

 

pubNoticeAll(int[] oids, int[] cids)

매개값이 배열일 때 String.valueOf(배열안의 값)을 통해 문자열로 변환, 최종적으로 리스트에 추가함으로써 리스트로 변환을 수행

 

int배열을 String 컬렉션으로 변환하기 위해 라이브러리를 쓸 수도 있지만, for문을 쓰는 것이 성능이 낫고 간단하다.

 

pubNoticeAll(List<String> oids, List<String> cids)

매개값이 리스트일 때 join(구분자, 리스트) 메소드를 통해 문자열로 변환

 

 

 

pubNoticeAll(String oids, String cids)

매개값이 문자열일 때 비로소 DB와 연결하여 UPDATE문을 실행한다

- in (1, 5, 8)같이 랜덤한 아이디 값들의 나열된 문자열을 preparedStatement로 처리할 수 없다.

- 쿼리문을 중간에 끊고 해당 문자열 변수를 삽입할 수 있지만, 가독성을 위해 String.format()이 사용되었다.