2022-04-28

GGAE99·2022년 4월 28일
0

진도

목록 보기
41/43

오늘 정리할 내용부터 적어보자.

오늘 정리할거

  1. 게시글 파일 올리기
  2. AOP
  3. Hash 알고리즘

게시글 파일 올리기

게시글 작성 jsp

게시글에 파일을 올리기 위해서는

첨부파일 : <input type="file" name="upfile" multiple><br>

위 처럼 file타입의 input값을 넣어줘야한다.
지금 이 input값을 보내주는 태그를 form태그로 해놨는데,

<form action="/boardWrite.do" method="post" enctype="multipart/form-data">

이렇게 뒤에 enctype을 붙여서 보내줄 데이터의 타입을 정의해줘야 데이터가 정상적으로 넘어간다.

컨트롤러

컨트롤러로 가보자.

public String boardWrite(Board b, MultipartFile[] upfile,HttpServletRequest request) {

먼저 받아올 매개변수를 MultipartFile[] upfile로 지정해 파일들을 받아줬다.
Board는 게시글 객체로 다른 정보들을 받아온 것 이니 지금은 신경쓰지말자.
HttpServletRequest 객체 request는 파일저장 경로와 중복검사를 할 때 사용할 것 이다.

  1. 먼저 파일을 받아올 리스트를 생성한다.
// 파일 목록을 저장할 리스트를 생성
ArrayList<FileVO> fileList = new ArrayList<FileVO>();

FileVo 객체 타입으로 이루어진 배열을 선언해줬다.

  1. 보내준 파일이 있는지 확인한다.
// MultipartFile 배열을 첨부파일의 갯수만큼 길이가 생성(단, 첨부파일없어도 길이는 무조건 1)
// 첨부파일이 없는 경우는 배열의 첫번째 파일이 비어있는지 체크하는 방식
if (upfile[0].isEmpty()) {
	// 첨부파일이 없는경우 수행할 로직이 없음
}

MultipartFile은 스프링에서 제공하는 인터페이스다. HTTP multipart 요청을 처리하는데, MultipartFile 요청은 큰 파일을 청크 단위로 쪼개서 효율적으로 파일 업로드 할 수 있게 해준다.
MutipartFile 배열은 첨부파일의 갯수만큼 길이가 생성되는데, 첨부파일이 없는 경우에도 1의 길이를 갖는다. 그래서 배열의 첫번째 요소가 비어있는지 확인하는 것으로 판별한다.
만약 첫번째 요소가 비어있다면 그냥 Board값만 올리고 if문을 종료한다.

  1. 보내줄 파일이 있을 때 (로직 시작)
else {
	// 첨부파일이 있는경우 파일업로드 작업 진행
	// 파일업로드 경로 설정(HttpServletRequest 객체를 이용해서 경로 구해옴)
	// request.getSession().getServletContext().getRealPath() -> /webapp/폴더경로
	String savePath = request.getSession().getServletContext().getRealPath("/resources/upload/board/");

보내줄 파일이 있을 때 파일을 업로드할 경로를 savePath에 지정해준다.
(/webapp/폴더경로)로 경로를 지정해준다.

  1. 파일 중복검사
for (MultipartFile file : upfile) {
		// 파일명이 기존 파일과 겹치는경우 기존파일을 삭제하고 새파일만 남는 현상이 생김(덮어쓰기)
		// 파일명 중복처리
		// 사용자가 업로드한 파일 이름
		String filename = file.getOriginalFilename();
		// test.txt -> test_1.txt 겹치는 파일명이 있으면 _n 을 붙여주는 규칙을 사용하자.
		// test.txt -> test_2.txt
		// 업로드한 파일명이 text.txt인 경우 -> "text" 와 ".txt" 두 부분으로 분리
		String onlyFilename = filename.substring(0, filename.lastIndexOf("."));
		String extention = filename.substring(filename.lastIndexOf("."));// .txt
		// 실제 업로드할 파일명을 저장할 변수
		String filepath = null;
		// 파일명 중복 시 뒤에 붙일 숫자 변수
		int count = 0;
		while (true) {
			if (count == 0) {
				// 반복 첫번째 회차에는 원본파일명을 그대로 적용
				filepath = onlyFilename + extention;// test.txt
			} else {
				filepath = onlyFilename + "_" + count + extention;// test_n.txt
			}
			File checkFile = new File(savePath + filepath); 
            //savePate = 폴더 경로 / filepath = 파일이름 -> 2개를 합친 이 이름이 이미 그 경로에 존재하는지 확인
			if (!checkFile.exists()) {
				break;
			}
			count++;
		}

이번에는 코드가 기니까 하나하나 정리해보자.

  • 받아온 파일(upfile)의 파일 이름을 MutipartFile에 다 넣을 때 까지 반복해서 실행한다.
  • 반복 때 마다 반복 회차에해당하는 파일의 이름을 filename에 넣어준다.
  • 업로드한 파일명과 파일형식을 나누어서 분리해준다.
    subSting메소드를 사용해 파일 전체 이름의 시작부분부터 "."이 나오기 전까지를 onlyFilename으로
    "."부터 마지막까지를 extention으로 저장한다.
  • 파일을 저장할 이름을 filepath 변수로 선언한다.
  • 파일이 중복되면 뒤에 숫자를 붙여서 파일을 구분해주기 위해 숫자 변수 couint를 선언한다.
  • while문으로 파일 반복검사를 실행한다.
    첫번째 반복에서는 filepath 를 원본 파일명으로 그대로 적용한다.
  • checkFile에 (파일경로 + 전체 파일이름) 을 저장해준다.
  • checkFile.exists()메소드로 이미 이 파일이 존재하는지 확인해준다.
    파일이 존재하는 경우 count를 1 늘리고 while문을 다시 시작한다.
    파일이 존재하지 않는경우 while문을 빠져나온다.
  • 파일이 존재하는 경우에는 filepath를(파일이름+"_"+count+파일형식)으로 초기화한다.
  • 같은 파일이 존재하지 않을때까지 반복분을 반복한다.

위의 과정을 거쳐서 저장할 파일의 이름이 중복되지 않도록 만든다.

  1. 중복처리가 끝난 파일 업로드
		try {
			// 중복처리가 끝난 파일명(filepath)으로 파일을 업로드할 FileOutputStream객체 생성
			FileOutputStream fos = new FileOutputStream(new File(savePath + filepath));
			// 업로드 속도증가를 위한 보조스트림 생상
			BufferedOutputStream bos = new BufferedOutputStream(fos);
			// 파일업로드
			byte[] bytes;
			bytes = file.getBytes();
			bos.write(bytes);
			bos.close();
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		// 서버파일 업로드 작업 끝(파일1개단위)
		FileVO fileVo = new FileVO();
		fileVo.setFilename(filename);
		fileVo.setFilepath(filepath);
		fileList.add(fileVo);
        }
    }
        
 int result = service.boardWrite(b,fileList);

중복처리가 끝난 파일(filepath)를 업로드할 객체를 생성하고 파일을 올려준다.
보조스트림을 사용하여 더 빠르게 올려주도록 코드를 짰다.
FileVO 타입의 fileVO에 원본 파일이름과 저장해줄 이름을 넣어줬다.
마지막으로 보내줄 fileList에 fileVo를 추가해줬다.

  1. 데이터베이스에 저장
public int boardWrite(Board b, ArrayList<FileVO> fileList) {
	int result1 = dao.boardWrite(b);
	int result = 0;
	if(result1 > 0) {
		for(FileVO file : fileList) {
			file.setBoardNo(b.getBoardNo());
			result += dao.insertFile(file);
		}
	}else {
		result = -1;
	}
	return result;
}

받아온 Board와 FileVO 배열을 각각 데이터베이스에 넣어줬다.
Board를 넣어주고 결과값이 있으면 b.getBoardNo으로 게시물 번호를 가져와서 게시물 번호에 해당하는 파일이라고 알려주면서 데이터베이스에 저장하는 것 이다.
이 부분은 그냥 insert문이니 특별히 설명 없이 넘어가겠다.

  1. 사실 7번은 아니고 조회해오는 방법 정도?
public Board boardView(int boardNo) {		
	Board b = dao.boardView(boardNo);
	return b;
}

컨트롤러에서 boardNo를 받아와서 게시물 내용과 게시물에 포함된 파일을 불러올 예정이다.
사실 정리하고 싶은건 이부분이 아니라 sql문이다.

  <select id="boardView" parameterType="int" resultMap="getBoard">
  	select * from board where board_no =#{boardNo}
  </select>
  
  <select id="selectFileList" parameterType="int" resultType="f">
  	select 
  				file_no as fileNo,
  				board_no as boardNo,
  				filename,
  				filepath
  	from file_tbl where board_no=#{boardNo}
  </select>  
  
  <!-- 이와 같은 방식은 불러올 파일이 많으면 사용하면 안된다. -->
  <!-- join방식으로 하기때문에 중복된 데이터를 가져온다. -->
  <resultMap type="b" id="getBoard">
  	<result column="board_no" property="boardNo"/>
  	<result column="board_title" property="boardTitle"/>
  	<result column="board_content" property="boardContent"/>
  	<result column="board_writer" property="boardWriter"/>
  	<result column="board_date" property="boardDate"/>
  	<collection property="fileList"
  				column="board_no"
  				javaType="java.util.ArrayList"
  				ofType="f"
  				select="selectFileList"
  	/>
  </resultMap>

이 부분을 정리하고 싶었는데, 게시물 내용은 그냥 게시물 번호에 해당하는 것을 조회해오면된다.
그런데 여기서 파일을 가져오는 방식을 join을 사용한 방법으로 가져온다.
select에서 resultMap = getBoard라고 선언했다. (연결할 resultMap의 ID를 뜻한다.)
그리고 getBoard를 아이디로 갖는 resultMap을 선언해줬다.
column에서 받아올 값들을 각각 property로 선언했다.
그리고 collerction의 property인 fileList는 뒤에 적어준 select = "쿼리아이디" 의 쿼리문에서 받아온 값을 넣어주려고 한다.
위의 방법을 사용하면 select를 두번 할 필요 없이 한번에 Board객체만 내보내서 게시글을 받아올 수 있다.

AOP

AOP는 관점지햘 프로그래밍의 약자로 일반적으로 사용하는 클래스(Service, DAO)에서 중복되는 공통 코드 부분을 별도의 영역으로 분리해내고, 코드가 실행되기 전이나 이후의 시점에 해당 코드를 붙여 넣음으로써 소스코드의 중복을 줄이고, 필요한 때마다 가져다 쓸 수 있게 객체화 하는 기술을 말한다.

  • Joinpoint : 클라이언트가 호출하는 모든 비즈니스 메소드를 뜻한다. 일반적으로 Service의 모든 클래스를 지칭한다.
  • Pointcut : 필터링된 Joinpoint이다. 공통 기능을 적용하려고 선택된 메소드들이다.
  • Advice : Pointcut에 적용할 공통 기능의 코드이다.
  • Aspect or Advisor : Pointcut과 Advice를 합친 의미다 Pointcut + Advice = Aspect
    어떤 Pointcut에 어떤 Advice를 적용할지 결정한다.
    Advisor는 Aspect와 같지만 몇몇 특수한 경우에 사용한다.(트랜잭션 처리)

그럼 바로 설정을 적용해보자.

  1. 라이브러리를 추가해준다.
		<!-- aspectjweaver -->
		<dependency>
		    <groupId>org.aspectj</groupId>
		    <artifactId>aspectjweaver</artifactId>
		    <version>1.9.6</version>
		    <scope>runtime</scope>
		</dependency>

를 pom.xml에 추가해준다. 라이브러리 추가다.

  1. sevlet-context.xml에 Namespaces에서 aop를 체크해준다.
  2. AOP 설정을 적용한다.
<!-- 클래스 객체 생성 -->
<bean id="클래스를 지칭할 이름" class="가져올 클래스 이름(경로로 가져와야한다. xx.or.common.BeforeAdvice)"/>
<!-- AOP 설정 -->
<aop:config>
<aop:aspect id="객체 아이디" ref="객체에서 사용할 메소드">
<aop:pointcut expression="(메소드를 사용할 서비스들을 지정)execution(* kr.or.member.model.service.MemberService.*(..))" id="pointcut1"/>
<aop:aspect ref="가져올 클래스 객체 아이디">
 	<aop:around method="클래스 객체의 메소드" pointcut-ref="(적용할 메소드를 적용할 곳 지정)pointcut1"/>
</aop:aspect>

위의 방식으로 진행된다.

이렇게 하는 방법도 있지만, 어노테이션으로 하는 방법도 있다.
그 방법은 비밀번호 암호화와 함께 설명해보겠다.

Hash알고리즘

Hash 알고리즘은 암호화를 할때 주로 사용한다고 한다.
필자는 알고리즘에 자신이 없어서 여기까지만 설명하고 따로 더 공부하겠다...ㅠㅠ

@Component
public class SHA256Enc {
	
	public String endData(String data) throws Exception{
		//Spring Security의 MessageDigest 객체를 통한 암호화
		MessageDigest mDigest = MessageDigest.getInstance("SHA-256");//암호화 알고리즘 중 SHA-256 알고리즘 사용
		//매개변수로 받은 암호화 전 비밀번호를 byte배열로 변환
		byte[] beforePw = data.getBytes();
		//byte배열로 변환된 암호화전 비밀번호를 SHA-256으로 암호화
		mDigest.update(beforePw);
		//암호화된 pw를 byte배열로 추출
		byte[] encStr = mDigest.digest();
		//byte -> 1byte로 정수 표현 -> 2진수 8자리 -> 표현할 수 있는 숫자 갯수 2^8 -> 256개 -> -128 ~ 127
		//0 ~ 255로 변환하여 사용(16진수 -> 0 ~ f로 표현)
		StringBuffer sb = new StringBuffer();
		for(int i=0;i<encStr.length;i++) {
			byte tmp = encStr[i];
			String tmpText = Integer.toString((tmp & 0xff)+0x100,16).substring(1);
			sb.append(tmpText);
		}
		return sb.toString();
	}

이것은 SHA256이라는 해시 알고리즘을 사용한 암호화 코드이다.
받아온 데이터를 암호화해서 String값을 내보내는 코드이다.

@Component
@Aspect
public class PasswordEncAdvice {
	@Autowired
	private SHA256Enc enc;
	
	@Pointcut(value="execution(* xx.or.member.model.service.MemberService.*Member(xx.or.member.model.vo.Member))")
	public void encPointcut() {}
	
	@Before(value="encPointcut()")
	public void encPassword(JoinPoint jp) {
		Object[] args = jp.getArgs();
		Member m = (Member)args[0];
		String beforePw = m.getMemberPw();
		try {
			String encPw = enc.endData(beforePw);
			m.setMemberPw(encPw);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

차근차근 잡아보자.

  • @Component를 사용해 PasswordEncAdvice를 객체화해줬다. 그 후 AOP 설정을 사용하기 위해 @Aspect라는 설정도 넣어줬다.
  • @Autiwired라는 설정을 사용해 아까 만들어뒀던 SHA256Enc 클래스를 객체화했다.
  • @Pointcut을 사용해 해당 메소드를 적용할 서비스들을 설정해줬다.
    (위는 MemberService에서 이름이 Member로 끝나는 서비스들 중 매개변수가 Member타입인 서비스들만 적용되도록 코드를 짜뒀다.)
  • @Before을 사용해 서비스가 데이터베이스에 접속하기 전에 먼저 데이터를 받아서 메소드를 실행하도록 만들었다.
  • Object[] 타입으로 데이터를 받아줬지만 이미 매개변수가 Member인 것만 받았기 때문에 받아올 데이터는 Member타입으로 정해져있다. 그리고 한개로 정해져있는데 이 부분은 설명을 생략한다.
  • Member m = (Member)args[0]으로 첫번쨰 오브젝트를 멤버타입으로 받아준다.
  • Member타입으로 받은 m에서 getMemberPw()메소드를 사용해 받아온 비밀번호를 받아준다.
  • beforePw에 받아온 비밀번호 값을 저장해준다.
  • encPw 문자열에 enc.endData(beforePw)값을 저장해준다.(암호화한 비밀번호)
  • m.setMemberPw(encPw)로 암호화한 비밀번호를 Member객체의 암호화로 초기화한다.

이러면 암호화가 끝난다.
현재 대부분의 사이트들이 이런식으로 비밀번호를 암호화해서 받기 때문에 관리자도 암호를 줄 수 없다고 한다. 본인은 개발 공부하면서 처음알았다.

오늘은 여기까지하고 마치도록하겠다.
빠잉~

0개의 댓글