[Java] SpringBoot 프로젝트 : 프로필 사진 기능 구현(6) - 이미지 Ajax 전송 데이터 처리(@ResponseBody, @ModelAttribute)
목적
- 회원제 웹사이트 개발시 거의 필수적으로 사용되는 프로필 사진 등록 및 수정 기능 구현
- 프로필 이미지 등록 관련 DB, Java, JS 처리 총 정리
- 일부 받아서 쓴 코드 리뷰
지난 글에서는 JS에서 FormData 객체를 통해 Ajax 방식으로 데이터를 컨트롤러에 전송하는 방법을 알아봤다.
이번 글은 마지막 과정인 컨트롤러에서 전달 받은 데이터를 처리하고 DB에 저장하는 코드를 보자.
데이터 흐름도는 다음과 같다.
[데이터 흐름도]
이전 글에서 Ajax를 이용해 컨트롤러에 이미지 데이터를 넘겨줬다.
[JavaScript - Ajax처리]
상기를 위해 코드를 다시 보면 다음과 같다.
//파일 전송
let form_data = new FormData();
form_data.append('upload',my_photo);//회원이 업로드한 이미지
$.ajax({
url:'../mypage/updateProfileImg.do',
data:form_data,
type:'post', //전송 방식
dataType:'json', //전달 받을 데이터 형식
contentType:false, //전달 하는 데이터 형식
//enctype:'multipart/form-data',
processData:false, //data String으로 변환여부
success:function(param){
if(param.result == 'logout'){
alert('로그인 후 변경 가능합니다.');
}else if(param.result == 'success'){
alert('프로필 사진이 수정되었습니다.');
$('#upload').val(''); //input 태그 value 초기화
$('#profile-change').modal('hide'); //모달창 닫기
}else{
alert('파일 전송 중 오류가 발생했습니다.');
}
},
error:function(){
alert('네트워크 오류가 발생했습니다.');
}
});
url을 보면 updateProfileImg.do라는 경로로 지정되어 있다.
해당 매핑이 된 컨트롤러 코드를 보자.
[Controller - updateProfileImg]
@RequestMapping("/mypage/updateProfileImg.do")
@ResponseBody
public Map<String, String> updateProfile(MemberVO member, HttpSession session){
Map<String, String> mapAjax = new HashMap<String, String>();
MemberVO user = (MemberVO)session.getAttribute("user");
if(user == null) {
mapAjax.put("result", "logout");
}else {
member.setMem_num(user.getMem_num());
mypageService.updateProfile(member);
mapAjax.put("result", "success");
}
return mapAjax;
}
코드의 흐름은 단순하다.
- key/value 리턴을 위해 Map 객체 생성
- session에서 로그인 한 회원 정보 가져와서 로그인 여부 체크
- 로그인 안한 경우: key: result / value: logout 담기
- 로그인 한 경우: memberVO에 로그인한 회원번호 담아주고 서비스 호출, key: result / value: success 담기
여기서 두 가지 의문점이 생긴다.
- 컨트롤러는 view를 리턴한다고 했는데 왜 map 객체를 리턴하는가..
- Ajax로 전송한 이미지 파일은 어디있는가..
[@ResponseBody]
먼저 어떻게 map객체를 리턴하는지부터 살펴보자.
위에 Ajax 파라미터를 정의하면서 dataType을 'json'으로 설정했다.
즉, 서버에 데이터를 보내면 그에 대한 리턴을 json형식으로 받겠다는 말이다.
따라서 서버에서는 어떤 처리를 거친 후 Ajax에 대한 응답으로 json형식의 데이터를 전달해줘야 한다.
근데 컨트롤러는 마지막에 view를 리턴해야 한다고 하지 않았던가..(기본적으로 리턴한 이름을 갖는 view를 viewresolver가 찾기 때문)
이럴 때 @ResponseBody 어노테이션을 쓴다.
view가 아니라 데이터를 HttpResponse의 body에 실어 보내고 싶을 때, 즉 문자열이나 json 형식, 또는 객체 등의 데이터를 그대로 리턴하고 싶을 때 사용하면 된다.
추가로 @ResponseBody가 붙어 있으면 리턴되는 타입에 맞게 HttpMessageConverter라는게 동작해서 페이지에서 Java의 객체를 json, xml, String 등의 타입으로 변환해준다고 한다(map객체 → json).
좀 더 자세한 설명을 해둔 포스트가 있어 참고를 위해 달아 놓겠다.
참고 : https://wildeveloperetrain.tistory.com/144
[@ModelAttribute]
다음은 사라진 업로드 이미지를 찾아보자.
위 코드를 다시 보면 updateProfile의 첫 파라미터가 MemberVO member라고 되어있다.
이게 사실 앞에 어노테이션이 생략된 것인데 쓰면 다음과 같다.
@RequestMapping("/mypage/updateProfileImg.do")
@ResponseBody
public Map<String, String> updateProfile(@ModelAttritbute MemberVO member, HttpSession session)
클라이언트에서 뭔가 요청을 보내올 때 데이터를 같이 보내오는 경우가 많은데, 이 데이터를 선언된 객체(여기선 memberVO)에 묶어주는 역할을 한다고 보면 된다.
비슷한 어노테이션으로 @RequestParam이 있는데 이건 어떤 데이터를 받아서 1대1로 매핑할 때 사용한다.
한마디로, 둘 다 클라이언트에서 HttpRequest에 담겨오는 값들을 컨트롤러의 파라미터에 전달 받을 때 사용한다.
근데 이 @ModelAttribute를 쓸 때 중요한 점이 있다.
클라이언트에서 넘어온 데이터가 위에서처럼 어떤 객체로 매핑이 되려면 setter가 있어야 한다는 것이다.
MemberVO를 다시 보자.
[MemberVO - setter]
public class MemberVO {
private int mem_num;
private byte[] mem_photo;
private String mem_photo_name;
//회원번호
public int getMem_num() {
return mem_num;
}
public void setMem_num(int mem_num) {
this.mem_num = mem_num;
}
//이미지 바이트 배열
public byte[] getMem_photo() {
return mem_photo;
}
public void setMem_photo(byte[] mem_photo) {
this.mem_photo = mem_photo;
}
//이미지 파일명
public String getMem_photo_name() {
return mem_photo_name;
}
public void setMem_photo_name(String mem_photo_name) {
this.mem_photo_name = mem_photo_name;
}
//이미지 업로드시
public void setUpload(MultipartFile upload) throws IOException{
//MultipartFile -> byte[]
setMem_photo(upload.getBytes());
//파일 이름
setMem_photo_name(upload.getOriginalFilename());
}
}
보면 private으로 선언된 변수들의 값을 넣거나 꺼내기 위해 getter/setter 메소드가 선언되어 있다.
근데 맨 아래에 upload라고 선언된 변수는 없는데 setter만 있는 것을 볼 수 있다.
이는 JS에서 FormData 객체를 이용해 데이터를 담아 Ajax 전송을 할 때 'upload'라는 name으로 데이터를 보냈기 때문에 이를 받아 추가 작업을 하기 위해 선언해 둔 것이다.
let form_data = new FormData();
form_data.append('upload',my_photo);//회원이 업로드한 이미지
페이지에서 이렇게 form으로 데이터를 전송하면 해당 name의 첫 글자를 대문자로하고 앞에 set을 붙인 메소드를 찾아 실행한다(upload → setUpload).
만약 다음과 같은 input 태그가 있다면
<input type = "text" name = "nick_name" value = "홍길동">
memberVO에는 아래와 같은 setter가 있어야 매핑이 된다.
public class MemberVO {
private String mem_name;
public void setNick_name(String mem_name) {
this.mem_name = mem_name;
}
}
따라서 위와 같은 방식으로 @ModelAttribute 어노테이션을 이용해 어떤 객체 안에 업로드한 파일을 담기 위해서는 form에서 데이터를 보낼 때 name 값을 setter의 이름과 동일하게 설정해줘야 한다.
이제 setUpload 내부를 보자.
public void setUpload(MultipartFile upload) throws IOException{
//MultipartFile -> byte[]
setMem_photo(upload.getBytes());
//파일 이름
setMem_photo_name(upload.getOriginalFilename());
}
upload라는 이름으로 form에서 데이터를 보내오면 setUpload가 실행된다.
MultipartFile 타입으로 데이터를 받고 있는데, 설명은 다음과 같다(참고).
multipart request로 받은 업로드된 파일을 표현하는 것이라는데, 쉽게 Spring에서 업로드 파일을 다룰 때 사용된다고 생각하면 된다.
이걸 이용하면 파일의 바이트 배열, 타입, 이름, 크기 등도 쉽게 구할 수 있다.
암튼 이렇게 받은 파일의 바이트 배열을 mem_photo에, 파일의 원본이름을 mem_photo_name에 할당해줬다.
이 과정까지 하면 현재 memberVO에는 회원번호, 회원이 업로드한 이미지 바이트 배열, 업로드한 파일 이름이 담겨있게 된다.
마지막으로 주입 받은 서비스의 흐름을 보고 마무리 하도록 하겠다.
[Mapper]
@Mapper
public interface MemberMapper{
@Update("UPDATE member_detail SET mem_photo=#{mem_photo}, mem_photo_name = #{mem_photo_name} WHERE mem_num=#{mem_num}")
public void updateProfile(MemberVO member);
}
[Service]
public interface MypageService {
public void updateProfile(MemberVO member);
}
[ServiceImpl]
@Service
@Transactional
public class MypageServiceImpl implements MypageService{
@Autowired
private MypageMapper mypageMapper;
@Override
public void updateProfile(MemberVO member) {
mypageMapper.updateProfile(member);
}
}
[Controller]
@Controller
public class MypageController {
@Autowired
private MypageService mypageService;
}
[결과 화면]
이렇게 해서 SpringBoot 프로젝트 프로필 사진 기능을 구현하며 사용한 기술들을 모두 정리해봤다.
서버에서 가져온 프로필 이미지를 화면에 띄우는 과정에서 I/O Stream, VO, Mybatis, Tiles, AbastractView, Base64 encoding 등의 개념과 프로필 이미지 등록 및 수정 과정에서 input type file, FileReader, FormData, Ajax parameter, Spring @Responsbody, Spring @ModelAttribute, setter 등의 개념도 정리했다.
배웠던 내용들 중 기억하고 있던 것들과 책이나 구글링을 통해 찾아본 정보들을 열심히 적어봤는데, 아무래도 하나하나 깊은 이해를 통해 작성한 것은 아니기에 틀린 내용도 다분히 있을 것이라 생각한다.
혹시 잘못된 정보나 오류가 있으면 알려주길 바라며.. 프로필 사진 기능 구현 정리를 마치도록 하겠다.