
⭐️데이터 비동기로 조회하고 →

조회수가 가장 높고, 최신글 순서로
예쁘게 꾸며도 된다
✔️project(board(controller)) → BoardController.java
/**
* 자동완성
*/
@PostMapping(value = "/searchAutoComplete", produces = "application/json; charset=UTF-8")
@ResponseBody
public List<Board> searchAutoComplete(@RequestBody String query) {
List<Board> boardList = service.searchAutoComplete(query);
System.out.println(query);
System.out.println(boardList);
return boardList;
}
✔️project(board(model(service))) → BoardService.java
/**
* 자동완성
* @param query
* @return
*/
List<Board> searchAutoComplete(String query);
✔️project(board(model(service))) → BoardServiceImpl.java
/**
* 자동완성
*/
@Override
public List<Board> searchAutoComplete(String query) {
return dao.autoComplete(query);
}
✔️project(board(model(dao))) → BoardDAO.java
/**
* 자동완성
* @param query
* @return
*/
public List<Board> autoComplete(String query) {
RowBounds rowBounds = new RowBounds(0, 5);
return sqlSession.selectList("boardMapper.searchAutoComplete", query, rowBounds);
}
✔️resources(mappers) → board-mapper.xml
<!-- 자동완성 -->
<select id="searchAutoComplete" resultMap="board_rm">
SELECT BOARD_NO, BOARD_TITLE, BOARD_CODE, BOARD_NAME
FROM BOARD
JOIN BOARD_TYPE USING(BOARD_CODE)
WHERE BOARD_DEL_FL = 'N'
AND BOARD_TITLE LIKE '%${query}%'
ORDER BY BOARD_NO DESC
</select>
✔️webapp(resources(js(board))) → header.js((jsp안에 scirpt로 줌)
)
const autoComplete = document.getElementById("autoComplete");
const input = document.getElementById("query");
input.addEventListener("input", () => {
fetch("/board/searchAutoComplete", {
method: "POST",
headers: { "Content-Type": "application/text" },
body: input.value // JS 객체 -> JSON 파싱
})
.then(response => response.json())
.then(data => {
autoComplete.innerHTML = "";
autoComplete.style.display = 'block';
autoComplete.hidden = false;
console.log(data)
if (data.length > 0) {
data.forEach(item => {
const li = document.createElement("li");
li.classList.add("autocomplete-item");
const a = document.createElement("a");
a.innerText = item.boardName + " - " + item.boardTitle;
a.setAttribute("href", `/board/\${item.boardCode}/\${item.boardNo}`);
li.append(a);
autoComplete.append(li);
});
} else {
const div = document.createElement("div");
div.classList.add("autocomplete-item");
div.innerText = "검색 결과가 없습니다.";
autoComplete.appendChild(div);
}
})
.catch(e => {
console.log("예외 발생");
console.log(e);
})
});

@Scheduled
- Spring에서 제공하는 스케줄러 - 스케줄러 : 시간에 따른 특정 작업(Job)의 순서를 지정하는 방법.
설정 방법
1) servlet-context.xml → Namespaces 탭 → task 체크 후 저장
2) servlet-context.xml → Source 탭 → task:annotation-driven/ 추가
@Scheduled 속성
- fixedDelay : 이전 작업이 끝난 시점으로 부터 고정된 시간(ms)을 설정
@Scheduled(fixedDelay = 10000) // 이전 작업이 끝난 후 10초 뒤에 실행
- fixedRate : 이전 작업이 수행되기 시작한 시점으로 부터 고정된 시간(ms)을 설정
@Scheduled(fixedRate = 10000) // 이전 작업이 시작된 후 10초 뒤에 실행
cron 속성 : UNIX계열 잡 스케쥴러 표현식으로 작성
- cron="초 분 시 일 월 요일 [년도]" - 요일 : 1(SUN) ~ 7(SAT)
ex) 2019년 9월 16일 월요일 10시 30분 20초 cron="20 30 10 16 9 2 " // 연도 생략 가능
- 특수문자 * : 모든 수.
- : 두 수 사이의 값. ex) 10-15 -> 10이상 15이하
, : 특정 값 지정. ex) 3,4,7 -> 3,4,7 지정
/ : 값의 증가. ex) 0/5 -> 0부터 시작하여 5마다
? : 특별한 값이 없음. (월, 요일만 해당)
L : 마지막. (월, 요일만 해당)
@Scheduled(cron="0 * * * * *") // 매 분마다 실행
- 주의사항 - @Scheduled 어노테이션은 매개변수가 없는 메소드에만 적용 가능
✔️project(common(scheduling)) → ImageDeleteScheduling.java
// 스프링이 일정 시간마다 해당 객체를 이용해서 코드를 수행
// == 스프링이 해당 클래스를 객체로 만들어서 관리를 해야 된다.
// == Bean 등록
@Component // @Controller, @Service, @Repository의 부모 어노테이션
// Bean 등록을 하겠다고 명시하는 어노테이션
public class ImageDeleteScheduling {
@Autowired
private ServletContext servletContext;
@Autowired
private BoardService service;
//@Scheduled(fixedDelay = 10000) // ms 단위
// 5초 동안 작업 -> 10초 대기 -> 작업(5초) -> 10초 대기
//@Scheduled(fixedRate = 10000)
// 일(5초)
// 대기(10초)
// cron="초 분 시 일 월 요일 [년도]"
//@Scheduled(cron = "0,30 * * * * *")
@Scheduled(cron = "0 0 * * * *") // 매 정시(*시 0분 0초)
public void test() {
// System.out.println("스케쥴러가 일정 시간마다 자동으로 출력");
System.out.println("----------- 게시판 DB, 서버 불일치 파일 제거 -----------");
// 서버에 저장된 파일 목록을 조회해서
// DB에 저장된 파일 목록과 비교하여
// 매칭되지 않는 서버 파일 제거
// 1) 서버에 저장된 파일 목록 조회
// -> application 객체를 이용해서
// /resources/images/board의 실제 서버 경로를 얻어옴
String filePath = servletContext.getRealPath("/resources/images/board");
// C:\workspace\6_Framework\boardProject\src\main\webapp\resources\images\board
// 2) filePath에 저장된 모든 파일 목록 읽어오기
File path = new File(filePath);
File[] imageArr = path.listFiles();
// 배열 -> List로 변환
List<File> serverImageList = Arrays.asList(imageArr);
// 확인 (임시)
// for(File f : serverImageList) {
// System.out.println(f.toString());
// }
// 3) DB 파일 목록 조회
List<String> dbImageList = service.selectImageList();
// 확인 (임시)
// for(String s : dbImageList) {
// System.out.println(s);
// }
// 4) 서버에 파일 목록이 있을 경우에 비교
if(!serverImageList.isEmpty()) {
// 5) 서버 파일 목록 순차 접근
for(File server : serverImageList) {
// 6) 서버에 존재하는 파일이
// DB(dbImageList)에 없다면 삭제
// String[] temp = server.toString().split("\\");
// String s = temp[temp.length-1];
// System.out.println(server.getName());
// List.indexOf(객체) = 객체가 List에 있으면 해당 인덱스 반환
// 없으면 -1 반환
if(dbImageList.indexOf(server.getName()) == -1) {
// db 파일 목록 서버 파일 이름
System.out.println(server.getName() + "삭제");
server.delete(); // File.delete() : 파일 삭제
}
} // for문 종료
System.out.println("----------- 이미지 파일 삭제 스케쥴러 종료 -----------");
}
}
}
백엔드에서 일정시간마다 시작되는 것
getName() 사용하기
✔️project(board(model(service))) → BoardService.java
...
/** DB 이미지(파일) 목록 조회
* @return list
*/
List<String> selectImageList();
✔️project(board(model(service))) → BoardServiceImpl.java
...
// DB 이미지(파일) 목록 조회
@Override
public List<String> selectImageList() {
return dao.selectImageList();
}
✔️project(board(model(dao))) → BoardDAO.java
...
/** DB 이미지(파일) 목록 조회
* @return list
*/
public List<String> selectImageList() {
return sqlSession.selectList("boardMapper.selectImageListAll");
}
✔️resources(mappers) → board-mapper.xml
...
<!-- DB 이미지(파일) 목록 조회 -->
<select id="selectImageListAll" resultType="string">
SELECT IMG_RENAME FROM BOARD_IMG
</select>
// 3) DB 파일 목록 조회 List<String> dbImageList = service.selectImageList(); // 확인 (임시) // for(String s : dbImageList) { // System.out.println(s); // }
필요한 것은 다 가지고 옴
db랑 서버랑 일치하면 다 지워주는 작업만 하면 된다
효율적인 관리가 가능하다
Syso는 효율 저하가 있어서 사용 지양
💡 로그의 장점 VS 단점
장점
: 프로그램의 문제 파악에 용이
빠르고 효율적인 디버깅 가능
수행내역 파악이 쉬움
로그 이력을 파일, DB 등으로 남길 수 있음
단점 : 로그에 대한 디바이스(파일) 입출력으로 인해 런타임 오버헤드 발생
로깅을 위한 추가 코드로 인해 전체 코드 사이즈 증가
심하게 생성되는 로그는 혼란을 야기하거나 어플리케이션 성능에 영향을 미침
개발 중간에 로깅 코드를 추가하기 어려움
db에는 남길 수 있는데 잘 남기지 않음
전체 코드 증가 : syso가 코드 사이 사이에 들어가 있다고 생각하면 됨
심하게 생성되는 로그 : 이것도 너무 많이 사용하면 문제가 있음
logging framework : log4j, logback, slf4j
logback가 최신
log4j 에서 logback으로 변경해야 하고 이걸 변경해주는 slf4j가 있다
log4j.xml (로그 관련 설정 파일) 구조




<Appenders>
<appender ..
: 로그 출력할 때, 어떤 모양으로 화면에 붙일까?
console이라고 적혀있음
console에 찍겠다(빨간줄)
이걸 의미함
패턴들을 의미함

logger
level에는 6가지가 있음


- warn : 경고가 있지만 실행은 되는 단계
error : 프로그램 꺼짐
이걸로 변경함
C:log로 하면 c드라이브 안의 log폴더
/로 시작하면 실행하고 있는 최상위 폴더로 시작
debug로 변경
원래는 info로 되어있음
그리고 appender-ref 넣어주기
edu.kh.project가 logger가 되면 dailyRollingFile이 실행되게 만들어준다
지정한 패키지 외 다른 것
기본적으로
이걸 warn → debug로 변경
⇒ console에서 debug이상, 파일에서도 debug이상으로 셋팅해줌
✔️pom.xml
...
<!-- Log4j (Logging) -->
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.17.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${org.slf4j-version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${org.slf4j-version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${org.slf4j-version}</version>
<scope>runtime</scope>
</dependency>
...
✔️log4j.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM
"http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
<!-- Appenders -->
<appender name="console" class="org.apache.log4j.ConsoleAppender">
<param name="Target" value="System.out" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} [%p] %m%n" />
</layout>
</appender>
<!-- //날짜별 로그 파일 생성 하기 -->
<appender name="dailyRollingFile" class="org.apache.log4j.DailyRollingFileAppender">
<param name="File" value="/logs/runtime.log" />
<param name="Append" value="true" />
<param name="encoding" value="UTF-8" />
<param name="DatePattern" value="'.'yyyy-MM-dd" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} [%p] %m%n" />
</layout>
</appender>
<!-- log level
trace -> debug -> info -> warn -> error -> fatal
-->
<!-- Application Loggers -->
<logger name="edu.kh.project">
<level value="debug" />
<appender-ref ref="dailyRollingFile" />
</logger>
<!-- 3rdparty Loggers -->
<logger name="org.springframework.core">
<level value="info" />
</logger>
<logger name="org.springframework.beans">
<level value="info" />
</logger>
<logger name="org.springframework.context">
<level value="info" />
</logger>
<logger name="org.springframework.web">
<level value="info" />
</logger>
<!-- Root Logger -->
<root>
<priority value="debug" />
<appender-ref ref="console" />
</root>
</log4j:configuration>
✔️project(member(model(service))) → MemberServiceImpl.java
package edu.kh.project.member.model.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import edu.kh.project.member.model.dao.MemberDAO;
import edu.kh.project.member.model.dto.Member;
@Service
public class MemberServiceImpl implements MemberService {
// org.slf4j.Logger : 로그를 작성할 수 있는 객체
// org.slf4j.LoggerFactory
private Logger logger = LoggerFactory.getLogger(MemberServiceImpl.class);
@Autowired
private MemberDAO dao;
@Autowired // bean으로 등록된 객체 중 타입이 일치하는 객체를 DI (의존성 주입)
private BCryptPasswordEncoder bcrypt;
@Override
public Member login(Member inputMember) {
// 로그 출력
logger.info("MemberService.login() 실행"); // 정보 출력
logger.debug("memberEmail : " + inputMember.getMemberEmail());
logger.warn("이건 경고 용도");
logger.error("이건 오류 발생 시");
Member loginMember = dao.login(inputMember);
if(loginMember != null) { // 아이디가 일치하는 회원이 조회된 경우
// 입력한 pw, 암호화된 pw 같은지 확인
// 같을 경우
if(bcrypt.matches(inputMember.getMemberPw(),
loginMember.getMemberPw())) {
// 비밀번호를 유지하지 않기 위해서 로그인 정보에서 제거
loginMember.setMemberPw(null);
} else { // 다를 경우
loginMember = null; // 로그인 실패처럼 만듦
}
}
return loginMember;
}
...
logger라는 factory 안에서 logger를 가져온다
로그인 시도를 하니깐
어노테이션을 사용할 수 있음
근데 클릭해서 들어가면 변수명이 log로 되어있음
즉, 아까 쓴 logger말고 log로 변경해줘야 한다
실행하고 로그인 시도하면
콘솔에는 jdbc 등도 있는데 메모장에는 없음
⇒ 사용한 것만 찍히기에 나오지 않는 것