[Spring Boot] Rest AOP Interceptor / File

KDH59·2024년 5월 24일

[SPRING BOOT]

목록 보기
3/7

AOP

□ spect Oriented Programming : 관점 지향 프로그래밍
□ 대표적인 기능 Interceptor (배우진 않음)
□ Interceptor 를 활용 하면 Controller 도착 전 특정 작업을 수행 하거나
□ Controller 를 지나 client 에 도착 전 특정 작업을 수행 할 수 있다.

Interceptor (프로젝트 간 빠질수 없는 내용)

  • 우선 로그인 체크 관련

1. 로그인 체크 로직 ( controller 접근전/후 )

□ kr.co.gudi.config

  • application.properties 설정 외 추가할 것들

□ kr.co.gudi.util

  • 날짜계산/ 정규편식 / 업로드,다운로드 /로그인체크 등

□ 객체화 / 의존성주입(어노테이션)
@Component : service가 아닌 페키지들 밖에서 애매한것들에 사용 그럼 쓸 수 있음

preHandle [ 로그인 세션 체크 포함 ]

□ 컨트롤러를 거치기전에 이곳을 들른다.

□ 반환값이 false 면 컨트롤러에 접근 할 수 없다. (하얀화면을 보여줌)

□ 그래서 false 로 컨트롤러에 가지 못하게 한 다음 response 를 이용해서 다른곳으로 보내는게 일반 적이다.

@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler)
		throws Exception {
	boolean pass = true;
	logger.info("=== preHandle ===");
	
	HttpSession session = req.getSession();
	
	if(session.getAttribute("loginId")==null) {
		pass = false;
		resp.sendRedirect("/"); //context 경로가 있다면 같이 넣어줘야 하낟. /.15_TotalBoard/
	}
	
	return pass;
}

postHandle [ 로그인 세션 체크 포함 ]

□ 컨트롤러를 접근한 후 뷰에 보내지기 전 들른다.

□ 뷰에 보내고 싶은 내용이 있다면 ModelAndView 에 넣어주면 된다.

@Override
public void postHandle(HttpServletRequest req, HttpServletResponse resp, Object handler,
		ModelAndView mav) throws Exception {
	logger.info("=== postHandle ===");
	
	HttpSession session = req.getSession();
	String loginId = (String) session.getAttribute("loginId");
	mav.addObject("loginId",loginId);
	
}

2. 체크 기능을 인터셉터에 등록(인터셉터에서 잡아낼 내용/예외로 보내줄 내용) [★핵심]

kr.co.gudi.config -> InterCeptorConfig.java

  • 만드는 이유 ▼ □ 순서 ( 핵심 : 잘못 등록할 경우 무한루프 돌아감 )
  1. 인터셉터가 예외 둘 URL 패턴을 만든다
  2. 인터셉터에 등록할 로직 추가
  3. 인터셉터가 가로챌 URL 패턴을 등록
  4. 인터셉터가 예외로 둘 URL 패턴 등록

□ 모든 요청에 대해서 인터셉터를 걸면 안된다 왜냐하면 서버 죽어버림
□ css, js, image 요청에 대해서도 예외를 꼭 넣어줘야한다 당연히
□ 예외를 둘 요청이 엄청 많기 때문에 리스트로 입력해주는것이 권장된다.

    @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 요청에 대해서도 예외를 꼭 넣어줘야한다 당연히
          □ 예외를 둘 요청이 엄청 많기 때문에 리스트로 입력해주는것이 권장된다
      }
  }

□ application.properties에서 static패턴을 resources/** 로 패턴변경

<link rel="stylesheet" href="resources/css/detail.css" type="text/css">

.jsp 에서 link 태그에 hreg = 'resources/' 추가

kr.co.gudi.util -> LoginChecker.java

@Autowired LoginChecker checker; // 인터셉터에 등록 할 클래스를 가져온다.
@Configuration // 이 어노테이션이 있어야 설정 클래스로 인식된다.

// 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();
        String loginId = (String) session.getAttribute("loginId");
        mav.addObject("loginId", loginId);

    }

Aspect-J

  • Aspect-J 는 point cut 표현식을 사용해서 특정 메서드 사용 전 후에 처리를 도와주는 라이브러리 이다.
  • Spring 에서는 이 기능을 활용하기 위해 최소 3가지 라이브러리가 필요 하지만
  • Boot 에서는 하나의 라이브러리만 설정 하면 된다.
  • 이후 별도의 설정없이 Aspect-J 의 기능을 사용 할 수 있다.

□ service 딴에서 특정 패키지, 클래스, 매소드 등에 한정하여 매소드가 실행 하기 전과 후 뭔가 실행하고 싶으면 사용

File Upload / Download

□ File Upload 를 위해서느 commons-fileupload 와 commons-io 라는 라이브러리가 필요 하다.
□ 하지만 Boot 에서는 기본으로 위 라이브러리를 가지고 있다.
□ Application.properties 에서 아래 설정만 추가 해 주자

17_FileService

□ 1개 파일의 용량 제한 ( 파일 단위를 MB를 사용 가능 )
spring.servlet.multipart.max-file-size=50MB

□ 여러 개 파일을 올릴 경우 총 용량 제한
spring.servlet.multipart.max-request-size=500MB

□ 저장될 파일 위치
spring.servlet.multipart.location=C:/upload

단일 파일 저장

@PostMapping(value="/upload")
public String upload(MultipartFile uploadFile) {
	logger.info(" upload File : "+uploadFile.getOriginalFilename());
	service.upload(uploadFile);
	
	return"redirect:/fileList";
}
  • @Value("${spring.servlet.multipart.location}")

    □ Spring의 환경 설정 파일 (예: application.properties 또는 application.yml)에 정의된 값을 자바 클래스의 필드에 주입하기 위해 사용됩니다. 여기서 ${spring.servlet.multipart.location}는 파일 업로드 시 임시 파일을 저장할 위치를 지정하는 프로퍼티를 나타냅니다.

       @Value("${spring.servlet.multipart.location}")
    	private String 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();
    		}
    	}

    멀티 파일 저장

    @PostMapping(value="/multiUpload")
    	public String MultiUpload(MultipartFile[] files) {
    		logger.info("files length : "+files.length);
    		service.multiUpload(files);
    		return"redirect:/fileList";
    	}

□ 파일 업로드는 단일 업로드 할 때 사용한 upload service 이용하면 됨

public void multiUpload(MultipartFile[] files) {
	for (MultipartFile file : files) {
		try {
			upload(file);
			Thread.sleep(1); // 1ms 단위로 업로드 하도록 지연시킴
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

이미지 불러오기

□ 브라우저에 이미지 불러올 수 있도록

  @RequestMapping(value="/photo/{fileName}")
  public ResponseEntity<Resource> imgView(@PathVariable String fileName) {
      logger.info("fileName : "+fileName);
      return service.imgView(fileName);

  }

□ service 에서 처리 할 수 있도록 생성한다.

  public ResponseEntity<Resource> imgView(String fileName) {

      // 1. 특정 경로에서 파일을 읽어와 Resource 로 만든다.
      Resource resource = new FileSystemResource(root+"/"+fileName);
      // 2. 보내질 파일의 형태를 지정해 준다.(헤더에)
      HttpHeaders header = new HttpHeaders();
      // 예 : image/gif, image/png, image/jpg. image/jpeg

      try {
          String type = Files.probeContentType(Paths.get(root+"/"+fileName)); // 경로를 주면 해당 파일의 mime=type을 알아낸다.
          logger.info("mime-type : "+ type);
          header.add("content-type", type);
      } catch (IOException e) {
          e.printStackTrace();
      }

      // 보낼 내용 헤더, 상태(200 또는 OK 는 정상이라는 뜻)
      return new ResponseEntity<Resource>(resource, header, HttpStatus.OK);
  }

□ colsole 에 mime=type타입을 알아 내서 찍히는 내용
// 예 : image/gif, image/png, image/jpg. image/jpeg

   2024-05-24 11:42:56.012  INFO 15124 --- [p-nio-80-exec-8] kr.co.gudi.controller.FileController     : fileName : 1711528559390.jpg
  2024-05-24 11:42:56.012  INFO 15124 --- [p-nio-80-exec-8] kr.co.gudi.service.FileService           : mime-type : image/jpeg
  2024-05-24 11:42:56.017  INFO 15124 --- [p-nio-80-exec-9] kr.co.gudi.controller.FileController     : fileName : 1716515217545.png
  2024-05-24 11:42:56.017  INFO 15124 --- [p-nio-80-exec-9] kr.co.gudi.service.FileService           : mime-type : image/png
  2024-05-24 11:42:56.017  INFO 15124 --- [-nio-80-exec-10] kr.co.gudi.controller.FileController     : fileName : 1711528559385.jpg
  2024-05-24 11:42:56.018  INFO 15124 --- [-nio-80-exec-10] kr.co.gudi.service.FileService           : mime-type : image/jpeg
  2024-05-24 11:42:56.020  INFO 15124 --- [p-nio-80-exec-5] kr.co.gudi.controller.FileController     : fileName : 1716516883910.png
  2024-05-24 11:42:56.021  INFO 15124 --- [p-nio-80-exec-5] kr.co.gudi.service.FileService           : mime-type : image/png
  2024-05-24 11:42:56.024  INFO 15124 --- [p-nio-80-exec-1] kr.co.gudi.controller.FileController     : fileName : 1716516883941.png
  2024-05-24 11:42:56.024  INFO 15124 --- [p-nio-80-exec-1] kr.co.gudi.service.FileService           : mime-type : image/png
  2024-05-24 11:46:34.101  INFO 15124 --- [on(2)-127.0.0.1] inMXBeanRegistrar$SpringApplicationAdmin : Application shutdown requested.
  2024-05-24 11:46:34.223  INFO 15124 --- [on(2)-127.0.0.1] o.apache.catalina.core.StandardService   : Stopping service [Tomcat]
  2024-05-24 11:46:34.226  INFO 15124 --- [on(2)-127.0.0.1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Destroying Spring FrameworkServlet 'dispatcherServlet'
  
  

이미지 다운로드

□ controller 부분

@RequestMapping(value="/download/{fileName}")
public ResponseEntity<Resource> download(@PathVariable String fileName) {
	logger.info("download Name : "+fileName);
	return service.download(fileName);
}

□ service 부분

public ResponseEntity<Resource> download(String fileName) {
	
	// 1. 특정 경로에서 파일을 읽어와 Resource 로 만든다.
	Resource resource = new FileSystemResource(root+"/"+fileName);
	// 2. 보내질 파일의 형태를 지정해 준다.(헤더에)
	HttpHeaders header = new HttpHeaders();
	
	
	try {
		// image/... 는 이미지 text/... 는 문자열, application/octet-stream 는 바이러리를 의미함
		header.add("content-type","application/octet-stream"); //content=type
		// 보낼 파일 이름
		// content=Disposition 는 내다 보내려는 컨텐트의 형태를 의미한다. inline 이면 문자열, attachment 는 다운로드 파일을 의미
		// attachment;filename="fileName.jpg"
		// 이 떄 파일명이 한글일 경우 깨져서 다운로드 된다. 그래서 안전하게 인코딩 해준다
		String oriFile = URLEncoder.encode("이미지_"+fileName, "UTF-8");
		header.add("content=Disposition", "attachment;filename=\""+fileName+"\"");
	} catch (Exception e) {
		e.printStackTrace();
	}
	
	// 보낼 내용 헤더, 상태(200 또는 OK 는 정상이라는 뜻)
	return new ResponseEntity<Resource>(resource, header, HttpStatus.OK);
}

Spring Boot Scheduler

  • 일정관리,
    □ Spring Boot Scheduler 는 별도의 라이브러리나 설정없이 사용 가능 하다. (쉬움)
    □ 사용할 클래스에 @EnabledScheduling 을 추가 하고
    □ 사용할 메서드에 @Scheduled 메서드를 사용하여 동작 할 수 있도록 한다.

    ★★ cron : 리눅스에 특정한 쉘을 특정 시간마다 돌려주는거 ([자동으로] 리눅스에서 채용)

fixedDelay / fixedRate

@EnableScheduling
@Component
public class ScheduleUtil {
    // 유의점 : 스케쥴러는 프로그램과 생명주기를 함계 한다. -> 켜질때 같이 켜지고 꺼질떄 같이 꺼진다.

Logger logger = LoggerFactory.getLogger(getClass());

/*@Scheduled(fixedDelay = 1000)
public void fixedDelay() {
	logger.info("이전 작업이 끝나고 1초 후 실행 "); // 이전작업이 끝나면 거기서 부터 1초
	try {
		Thread.sleep(500);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
}*/

@Scheduled(fixedRate = 1000)
public void fixedRate() {
	logger.info("무조건 1초 마다 실행 "); // 무조건 1초
	try {
		Thread.sleep(500);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
}

Cron

profile
[JAVA]

0개의 댓글