오늘 정리할 내용부터 적어보자.
- 게시글 파일 올리기
- AOP
- Hash 알고리즘
게시글에 파일을 올리기 위해서는
첨부파일 : <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는 파일저장 경로와 중복검사를 할 때 사용할 것 이다.
// 파일 목록을 저장할 리스트를 생성
ArrayList<FileVO> fileList = new ArrayList<FileVO>();
FileVo 객체 타입으로 이루어진 배열을 선언해줬다.
// MultipartFile 배열을 첨부파일의 갯수만큼 길이가 생성(단, 첨부파일없어도 길이는 무조건 1)
// 첨부파일이 없는 경우는 배열의 첫번째 파일이 비어있는지 체크하는 방식
if (upfile[0].isEmpty()) {
// 첨부파일이 없는경우 수행할 로직이 없음
}
MultipartFile은 스프링에서 제공하는 인터페이스다. HTTP multipart 요청을 처리하는데, MultipartFile 요청은 큰 파일을 청크 단위로 쪼개서 효율적으로 파일 업로드 할 수 있게 해준다.
MutipartFile 배열은 첨부파일의 갯수만큼 길이가 생성되는데, 첨부파일이 없는 경우에도 1의 길이를 갖는다. 그래서 배열의 첫번째 요소가 비어있는지 확인하는 것으로 판별한다.
만약 첫번째 요소가 비어있다면 그냥 Board값만 올리고 if문을 종료한다.
else {
// 첨부파일이 있는경우 파일업로드 작업 진행
// 파일업로드 경로 설정(HttpServletRequest 객체를 이용해서 경로 구해옴)
// request.getSession().getServletContext().getRealPath() -> /webapp/폴더경로
String savePath = request.getSession().getServletContext().getRealPath("/resources/upload/board/");
보내줄 파일이 있을 때 파일을 업로드할 경로를 savePath에 지정해준다.
(/webapp/폴더경로)로 경로를 지정해준다.
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++;
}
이번에는 코드가 기니까 하나하나 정리해보자.
위의 과정을 거쳐서 저장할 파일의 이름이 중복되지 않도록 만든다.
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를 추가해줬다.
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문이니 특별히 설명 없이 넘어가겠다.
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는 관점지햘 프로그래밍의 약자로 일반적으로 사용하는 클래스(Service, DAO)에서 중복되는 공통 코드 부분을 별도의 영역으로 분리해내고, 코드가 실행되기 전이나 이후의 시점에 해당 코드를 붙여 넣음으로써 소스코드의 중복을 줄이고, 필요한 때마다 가져다 쓸 수 있게 객체화 하는 기술을 말한다.
- Joinpoint : 클라이언트가 호출하는 모든 비즈니스 메소드를 뜻한다. 일반적으로 Service의 모든 클래스를 지칭한다.
- Pointcut : 필터링된 Joinpoint이다. 공통 기능을 적용하려고 선택된 메소드들이다.
- Advice : Pointcut에 적용할 공통 기능의 코드이다.
- Aspect or Advisor : Pointcut과 Advice를 합친 의미다 Pointcut + Advice = Aspect
어떤 Pointcut에 어떤 Advice를 적용할지 결정한다.
Advisor는 Aspect와 같지만 몇몇 특수한 경우에 사용한다.(트랜잭션 처리)
그럼 바로 설정을 적용해보자.
<!-- aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
<scope>runtime</scope>
</dependency>
를 pom.xml에 추가해준다. 라이브러리 추가다.
<!-- 클래스 객체 생성 -->
<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 알고리즘은 암호화를 할때 주로 사용한다고 한다.
필자는 알고리즘에 자신이 없어서 여기까지만 설명하고 따로 더 공부하겠다...ㅠㅠ
@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();
}
}
}
차근차근 잡아보자.
이러면 암호화가 끝난다.
현재 대부분의 사이트들이 이런식으로 비밀번호를 암호화해서 받기 때문에 관리자도 암호를 줄 수 없다고 한다. 본인은 개발 공부하면서 처음알았다.
오늘은 여기까지하고 마치도록하겠다.
빠잉~