ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 81일차 - 인터셉터, 파일 업로드, 다운로드, 스케쥴러
    백엔드(웹 서버, WAS)/Spring Boot 2024. 5. 24. 17:22

    1. 로그인 체크 로직(컨트롤러 접근전, 접근후)

    2. 체크 기능을 인터셉터에 (인터셉터에서 잡아낼 내용/예외로 보내줄 내용)

    config 패키지

    application.properties 에서 말고 다른 설정들을 넣어두는 패키지

    util 패키지

    프로젝트 팀장이 미리 유용한 기능 캘린더, 파일 업로드 다운로드 등 기능들을 만들어서 넣어두는 패키지

     

    @Component

    컨트롤러도 아니고 서비스도 아닌 애매한 애들을 컨포넌트 어노테이션을 붙인다

    서비스로 붙여도 상관은 없지만 혼동이 올 수 있어 권장하지 않는다

    Default Method 

    강제 오버라이딩되지 않고 사용자가 원하는 기능만 추가할 수 있다

    HandlerInterceptor 사용 방법

    Alt+Shift+S 를 눌러 오버라이딩 메소드를 선택해준다

    그 후 postHandler, preHandler 를 선택한 후 생성해준다

    핸들러가 잘 작동하는지 확인하기 위해 로거를 찍어본다

    @Configuration // 이 어노테이션이 있어야 설정 클래스가 인식된다
    public class InterCeptorConfig implements WebMvcConfigurer {
    	
    	// 인터셉터에 등록할 클래스를 가져온다
    	@Autowired LoginChecker checker;
    
    	@Override
    	public void addInterceptors(InterceptorRegistry registry) {
    		// 0. 인터셉터가 예외 둘 URL 패턴을 만든다
    		List<String> excludeList = new ArrayList<String>();
    		excludeList.add("/"); // 메인 페이지 요청은 로그인 체크하면 안된다
    		excludeList.add("/join*"); // join 뒤에 뭐가 오든지
    		excludeList.add("/login.*"); // login. 뒤에 뭐가 오든지
    		excludeList.add("/logout*"); // logout 뒤에 뭐가 오든지        
    		excludeList.add("/resources/**"); // img, css, js 예외처리, /resources/ 뒤에 뭐가 오든지
    		
    		// 1. 인터셉터에 등록할 로직 추가
    		registry.addInterceptor(checker)
    		.addPathPatterns("/**") // 2. 인터셉터가 가로챌 URL 패턴을 등록
    		.excludePathPatterns(excludeList); // 3. 인터셉터가 예외로 둘 URL 패턴 등록
    		// 모든 요청에 대해서 인터셉터를 걸면 안된다 왜냐하면 서버 죽어버림
    		// css, js, image 요청에 대해서도 예외를 꼭 넣어줘야한다 당연히
    		// 예외를 둘 요청이 엄청 많기 때문에 리스트로 입력해주는것이 권장된다
    		
    		
    	}
    	
    	
    }

    InterCeptorConfig 

    // Autowired 는 해야겠는데  Service 는 아니므로..
    @Component
    public class LoginChecker implements HandlerInterceptor {
    	
    	Logger logger = LoggerFactory.getLogger(getClass());
    
    	// 컨트롤러를 거치기전에 이곳을 들른다
    	// 반환값이 false 면 컨트롤러에 접근할 수 없다(하얀화면을 보여준다)
    	// 그래서 false 로 컨트롤러에 가지 못하게 한 다음 response 를 이용해서 다른곳으로 보내는게 일반적이다
    	@Override
    	public boolean preHandle(HttpServletRequest req, 
    			HttpServletResponse resp, Object handler)
    			throws Exception {
    		boolean pass = true;
    		logger.info("=== PRE HANDLER ===");
    		HttpSession session = req.getSession();
    		if (session.getAttribute("loginId") == null) {
    			pass = false;
    			resp.sendRedirect("/"); // context 경로가 있다면 같이 넣어줘야 한다 /15_TotalBoard/
    		}
    		return pass;
    	}
    
    	// 컨트롤러에 접근한 후 뷰에 보내지기 전 들른다
    	// 뷰에 보내고 싶은 내용이 있다면 ModelAndView 에 넣어주면 된다
    	@Override
    	public void postHandle(HttpServletRequest req, 
    			HttpServletResponse resp, Object handler,
    			ModelAndView mav) throws Exception {
    		logger.info("=== POST HANDLER ===");
    		HttpSession session = req.getSession();
    		// 만약에 정상적으로 로그인된 상태라면 loginId 가 모델을 통해서 보내지고
    		// 아니라면 로그인 페이지로 툭 튕긴다
    		String loginId = (String) session.getAttribute("loginId");
    		mav.addObject("loginId", loginId);
    	}	
    	
    }

    # static -> resources
    spring.mvc.static-path-pattern=/resources/**

    정리

    로그인 체크나, 알림 기능을 구현할때에 필수적인 스킬이다

    꼭 알아두자 

    Aspect-J

    인터셉터는 컨트롤러 기준으로 감지해서 실행되었다면 에스펙트-제이는 서비스 기준으로 실행된다

    어떤 클래스에 실행하기 전후에 감지하여 작업을 실행한다

    @Before("execution(* kr.co.gudi.service.HomeService.before())")

    설정하기 귀찮아서 힘듬

    내장 톰캣은 어떤 요청에 대해서 경로를 지정해주는 설정을 해줘야한다

    나중에 외장 톰캣을 바꾸었을때 옛날에 사용했던 설정 해주면 된다

    나중에 AWS 올릴때에는 소스에서 변경하지 않고 여기서 한번에 바꾸어서 올리면 된다

    server.xml 를 수정할 수 없을 경우에는 어떻게 해야할까?

    Controller

    @Controller
    public class FileController {
    	Logger logger = LoggerFactory.getLogger(getClass());
    	
    	private final FileService service;
    
    	public FileController(FileService service) {
    		this.service = service;
    	}
    	
    	@GetMapping(value="/")
    	public String home() {
    		return "home";
    	}
    	
    	@PostMapping(value="/upload")
    	public String upload(MultipartFile uploadFile) {
    		logger.info("upload file : {}", uploadFile);
    		service.upload(uploadFile);
    		return "redirect:/fileList";
    	}
    
    	@PostMapping(value="/multiUpload")
    	public String multiUpload(MultipartFile[] uploadFiles) {
    		logger.info("upload files : {}", uploadFiles.length);
    		service.multiUpload(uploadFiles);
    		return "redirect:/fileList";
    	}
    	
    	@GetMapping(value="/fileList")
    	public String fileList(Model model) {
    		List<String> list = service.fileList();
    		logger.info("list : {}", list);
    		model.addAttribute("list", list);
    		return "result";
    	}
    
    	@GetMapping(value="/delete")
    	public String imgDelete(String fileName) {
    		logger.info("fileName : {}", fileName);
    		service.delete(fileName);
    		return "redirect:/fileList";
    	}
    
    	@GetMapping(value="/photo/{fileName}")
    	public ResponseEntity<Resource> imgView(@PathVariable String fileName) {
    		logger.info("fileName : {}", fileName);
    		return service.imgView(fileName);
    	}
    
    	@GetMapping(value="/download/{fileName}")
    	public ResponseEntity<Resource> imgDownload(@PathVariable String fileName) {
    		logger.info("fileName : {}", fileName);
    		return service.imgDownload(fileName);
    	}	
    	
    }

    톰캣의 일부 수행하면서 이미지를 보여줘야한다

    1. photo/{path} 요청이 들어오면 '이미지 형태' 로 보내줘야한다 파일이 아닌

    경로 설정에 주의하자 org.springframework.core.io 이다

    .

    body : 우리가 보낼 데이터

    headers : 파일의 메타데이터 

    rawstatus : 응답 코드 200, 정상

    Service

    @Service
    public class FileService {
    	Logger logger = LoggerFactory.getLogger(getClass());
    
    	@Value("${spring.servlet.multipart.location}")
    	private String root; // 파일 경로를 누가 실수로 건드리면 안되기 때문에 캡슐화한다
    
    	public String getRoot() {
    		return root;
    	}
    
    	public void upload(MultipartFile file) {
    		// 1. 파일명 추출
    		String fileName = file.getOriginalFilename();
    
    		// 2. 새 파일명 생성
    		String ext = fileName.substring(fileName.lastIndexOf("."));
    		String newFileName = System.currentTimeMillis() + ext;
    		logger.info(fileName + "->" + newFileName);
    
    		// 3. 파일 저장
    		try {
    			byte[] bytes = file.getBytes();
    			Path path = Paths.get(root + "/" + newFileName);
    			Files.write(path, bytes);
    		} catch (IOException e) {
    			e.printStackTrace();
    		}
    	}
    
    	public void multiUpload(MultipartFile[] uploadFiles) {
    		for (MultipartFile file : uploadFiles) {
    			upload(file);
    			try {				
    				Thread.sleep(1);
    			} catch (InterruptedException e) {
    				e.getStackTrace();
    			}
    		}
    	}
    
    	public List<String> fileList() {
    		String[] list = new File(root).list();
    		return Arrays.asList(list);
    	}
    
    	public ResponseEntity<Resource> imgView(String fileName) {
    		// 1. 특정 경로에서 파일을 읽어와 Resource 로 만든다
    		Resource resource = new FileSystemResource(root+"/"+fileName);
    		
    		// 2. 보내질 파일의 형태를 지정해준다	
    		// 헤더에 보내질 파일의 형태를 지정해준다		
    		HttpHeaders header = new HttpHeaders();
    
    		// 2-1. 예를 들면 image/gif, image/png, image/jpg, image/jpeg
    		// 컴퓨터는 gif, png 가 이미지인지 모른다 이것을 해결해주는것이 따로 있음
    		// 이 확장자가 image 인지 아닌지 알 수 없다 반복문과 if 문으로 복잡하게 확인해야하는
    		// 과정이 있었는데 Spring 에서 이 과정을 대신 해주는 메서드를 제공한다
    		try {
    			String type = Files.probeContentType(Paths.get(root+"/"+fileName)); // 경로를 주면 해당 파일의 mime-type 을 알아낸다			
    			header.add("content-type", type);
    			logger.info("type : {}", type);
    		} catch (IOException e) {
    			e.getStackTrace();
    		}
    		// 클라이언트에게 보낼 내용(데이터), 헤더, 상태(200 또는 HttpStatus.OK 는 정상이라는 의미이다)
    		return new ResponseEntity<Resource>(resource,header,HttpStatus.OK);
    	}
    
    	public void delete(String fileName) {
    		File file = new File(root+"/"+fileName);
    		if (file.exists()) {
    			file.delete();
    		}
    	}
    
    	public ResponseEntity<Resource> imgDownload(String fileName) {
    		// 1. 특정 경로에서 파일을 읽어와 Resource 로 만든다
    		Resource resource = new FileSystemResource(root+"/"+fileName);
    		
    		// 2. 보내질 파일의 형태를 지정해준다	
    		// 헤더에 보내질 파일의 형태를 지정해준다		
    		HttpHeaders header = new HttpHeaders();
    		try {
    			String type = Files.probeContentType(Paths.get(root+"/"+fileName)); // 경로를 주면 해당 파일의 mime-type 을 알아낸다			
    			// application/octet-stream 는 바이너리를 의미한다
    			header.add("content-type", "application/octet-stream");
    
    			// content-Disposition 는 내가 보내려는 컨텐트의 형태를 의미한다
    			// inline 이면 문자열, attachment 는 다운로드 파일을 의미한다
    			// 이때 파일의 이름이 한글인 경우에는 깨져서 다운로드 된다 
    			// 그래서 안전하게 인코딩 해준다
    			String oriFile = URLEncoder.encode("이미지_" + fileName, "UTF-8");
    			header.add("content-Disposition", "attachment;filename=\""+ oriFile +"\"");
    			logger.info("type : {}", type);
    		} catch (IOException e) {
    			e.getStackTrace();
    		}
    		// 클라이언트에게 보낼 내용(데이터), 헤더, 상태(200 또는 HttpStatus.OK 는 정상이라는 의미이다)
    		return new ResponseEntity<Resource>(resource,header,HttpStatus.OK);
    	}
    }

    shceduler 

    1. 매일매일 9시마다 출근하는 사람 체크해줘

    2. 매일 5시 50분마다 퇴실 안찍은 사람 체크해줘

    3. 서버가 시작 후 한시간마다 인원수를 확인해줘

     

    cron : 리눅스에서 특정한 쉘을 일정 시간마다 실행되는 명령어에서 차용해왔다

    그래서 cron 명령어를 알면 스케줄러를 자유롭게 사용할 수 있다

     

    fiexedDelay

    대현이가 물 받은 다음에 1초 이후에 준모가 받아

    하지만 대현이가 물 받는 시간이 예상보다 길어지면 시간이 들쭉날쭉할 수 있다

    fixedRate

    대현이가 물 받고 물 마시고 있는데 1초 후 준모가 갑자기 물컵을 가져와서 마신다

    그래서 두개 이상의 프로세스가 충돌이 날 수 있다

    ★ cron

    일정 초, 분, 시간, 일 마다 작업을 수행한다

     

Designed by Tistory.