반려냥이의 프로필, 어떻게 관리하고 계신가요?
Petpular는 반려냥이의 정보를 한눈에 보고 관리할 수 있도록 돕는 집사들을 위한 플래폼입니다.
커뮤니티로 다른 집사들과 소통하세요!
유기동물 목록을 조회해보고 사지 말고 입양하세요!
첫 개인프로젝트 주제는 나와 가장 관련있으면서 재미있게 개발할 수 있는 주제로 선정하고자 했습니다. 여러가지 관심사 중 우리집 최고 권위자인 고양이 '빵이'와 관련된 서비스가 생각이 났고, 최근 주위에 반려고양이를 키우기 시작하는 사람들이 늘어나고 있기에 모두가 함께 사용할 수 있는 서비스인 <반려동물 커뮤니티>를 이번 프로젝트의 주제로 선정하게 되었습니다.
기획 / 일정표(WBS) 제작 -> DB 설계 -> URL / API 설계 -> 기능 구현 -> 테스트 -> 배포
2022-03-10
~ 2022-04-12
- Back end
- Front end
- 버전 및 이슈 관리
- Chrome
- 반려동물 등록 : 반려냥이의 프로필을 등록하고 수정할 수 있습니다.
- 반려동물 추가정보 등록 : 반려냥이의 사료 / 모래 구매일과 수량, 용량을 등록하면 다음 구매일을 추천해줍니다.
- 반려동물 프로필 : 등록한 반려냥이의 프로필과 사료 / 모래의 다음 구매일을 확인할 수 있습니다.
- 유기동물 조회 : 시 / 도 / 군(구)별 현재 보호중인 유기동물 현황을 볼 수 있습니다.
- 그 외에도 유저의 프로필 수정 및 게시글 / 반려동물 목록 / 유기동물 찜 목록 보기 기능을 제공합니다.
- 회원 삭제 기능 추가
- 소셜 로그인 도입
- API 호출 성능 개선
클라이언트에서 반려고양이 정보를 넘겨주고 서버에서는 @ModelAttribute
어노테이션으로 파라미터를 받아오게 코드를 짰으나 모든 parameter가 null로 넘어오고 있었습니다.
해결 과정
클라이언트와 DB는 snake_case를 사용하고 있고, 서버는 camelCase를 사용하고 있기 때문에 클라이언트 - 서버 간 통신에서 변수명이 일치하지 않아 데이터를 받아오지 못한다는 생각이 들었습니다.
따라서 클라이언트를 camelCase로 바꿔보았으나 여전히 에러는 고쳐지지 않았습니다.
알고보니 @ModelAttribute
는 HTTP 파라미터 데이터를 JAVA 객체에 매핑하기 때문에 객체에 생성자나 setter 메소드를 붙여주어야 한다는 사실을 알게되었습니다. 또, Query string이나 Form형태의 데이터만 받을 수 있다는 사실을 알게 되었습니다.
하지만 현재 프로젝트에서는 Lombok의 @Data
어노테이션을 사용하고 있으므로 생성자와 setter가 모두 있고 Form 형식으로 보내주고 있었기에 에러 원인을 알 수 없었습니다.
결과적으로 문제는 date형태의 객체였습니다.. 받으려는 객체를 LocalDate
형태로 받고 있기 때문에 바인딩 에러가 뜨고 있었습니다. 따라서 LocalDate
타입의 필드에 @DateTimeForma
t 어노테이션을 붙여줌으로 해결하였습니다.
문제 상황 - #5
사진을 업로드할 때 파일명이 한글로 되어있으면 업로드 되지 않는 문제가 있었습니다.
해결 과정
파일명이 한글일 때 처리하는 다양한 방식 중 파일명을 UUID로 난수화하여 업로드하는 방식을 선택하였습니다.
@Component
public class FileManagerService {
public final static String FILE_UPLOAD_PATH = "/Users/jin-yujin/Desktop/yujin/Petpular/workspace/images/";
public String savaFile(String userLoginId, MultipartFile file) {
String directoryName = userLoginId + "_" + System.currentTimeMillis() + "/";
String filePath = FILE_UPLOAD_PATH + directoryName;
// 디렉토리 생성
File directory = new File(filePath);
if (directory.mkdir() == false) {
return null; // 디렉토리 생성 시 실패하면 null을 리턴
}
// 파일 업로드: byte 단위로 업로드한다.
try {
byte[] bytes = file.getBytes();
//파일명 암호화
String origName = new String(file.getOriginalFilename().getBytes("8859_1"),"UTF-8");
String ext = origName.substring(origName.lastIndexOf(".")); // 확장자
String saveFileName = getUuid() + ext;
Path path = Paths.get(filePath + saveFileName);
Files.write(path, bytes);
return "/images/" + directoryName + saveFileName;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
// 파일명 난수화
public static String getUuid() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
문제 상황 - #14
pet name과 id를 gnb에 뿌려주었는데 pet 정보가 업데이트 되었을 때 새로고침을 시켜도 gnb는 새로고침이 되지 않아 실시간으로 데이터가 바뀌지 않는 문제가 있었습니다.
해결 과정
세션은 값이 변경되면 바로 업데이트가 되기 때문에 pet 데이터를 로그인을 하는 시점에 세션에 담아서 뿌려주려고 시도했습니다. 하지만 session은 1 대 1 매핑이므로 여러 개의 데이터를 넣을 수 없었습니다.
고민 끝에 pet name과 id를 string으로 리스트로 만들어서 세션에 담아 보내준 다음 클라이언트 단에서 리스트로 넘어온 데이터를 가공하는 방식을 사용하였습니다.
또, 로그인을 할 때 뿐만 아니라 pet 데이터를 수정했을 때에도 세션 정보가 업데이트 되어야하므로 pet update 메소드에 세션을 업데이트 하는 코드를 추가해주었습니다.
jsp에서 리스트로 넘어온 데이터를 가공하는 코드
<c:set var="petIdArr" value="${fn:split(petIdArr, ',')}" />
<c:set var="petNameArr" value="${fn:split(petNameArr, ',')}" />
<c:forEach items="${petIdArr}" var="petId" varStatus="status">
<c:set var="petName" value="${petNameArr[status.index]}" />
<li class="mypet-nav">
<a class="pet-nav d-block" href="/mypet/${petId}">
> ${petName}
</a>
</li>
</c:forEach>
문제 상황 - #29
회원가입 후 로그인을 하면 메인페이지에 접근 시 바인딩 에러가 뜨는 문제가 있었습니다.
- 문제 코드
List<Pet> petList = petBO.getPetByUserId(userId);
if(userId != null) {
String petNameArr = petList.get(0).getName();
String petIdArr = Integer.toString(petList.get(0).getId());
if (petList.size() > 0) {
for (int i = 1; i < petList.size(); i++) {
petIdArr = petIdArr + "," + petList.get(i).getId();
petNameArr = petNameArr + "," + petList.get(i).getName();
}
session.setAttribute("petIdArr", petIdArr);
session.setAttribute("petNameArr", petNameArr);
}
해결 과정
디버깅을 해보니 pet name과 id를 세션에 담아주는 코드에 문제가 있었습니다.
if (petList.size() > 0) {
for (int i = 1; i < petList.size(); i++) {
문제가 발생하는 이유는 user가 회원가입 후 로그인을 했을 때에는 petList가 존재하지 않는데 petList.size()
메소드를 실행시키려고 하기 때문에 발생하는 에러였습니다.
이를 해결하기 위해 다음과 같이 petList가 존재할 때에만 데이터를 가공하여 리스트에 담도록 코드를 수정하였습니다.
List<Pet> petList = petBO.getPetByUserId(userId);
if(userId != null && petList.size() != 0) {
String petNameArr = petList.get(0).getName();
String petIdArr = Integer.toString(petList.get(0).getId());
if (petList.size() > 0) {
for (int i = 1; i < petList.size(); i++) {
petIdArr = petIdArr + "," + petList.get(i).getId();
petNameArr = petNameArr + "," + petList.get(i).getName();
}
session.setAttribute("petIdArr", petIdArr);
session.setAttribute("petNameArr", petNameArr);
}
}
문제상황 - #49
이미지 삭제 시 로컬에 저장되어있는 이미지가 삭제되지 않는 문제가 있었습니다.
- 문제코드
Path path = Paths.get(FILE_UPLOAD_PATH + imgaePath.replace("/image/", "").split("/")[0];
if (Files.exists(path)) {
// 이미지 파일이 있으면 삭제
try {
Files.delete(path);
} catch (IOException e) {
e.printStackTrace();
}
}
해결과정
기존에는 이미지 파일이 있으면 디렉토리를 바로 삭제하려고 시도하는데, File.delete() 메소드는 디렉토리에 파일이 존재하지 않을 때 즉, 빈 디렉토리만 삭제가 가능하기때문에 발생하는 문제였습니다.
이를 해결하기 위해서, 우선 디렉토리 안에 있는 파일을 삭제한 다음 디렉토리를 삭제하는 과정을 거치도록 코드를 수정하였습니다.
Path path = Paths.get(FILE_UPLOAD_PATH + imagePath.replace("/images/", ""));
if (Files.exists(path)) {
try {
Files.delete(path);
} catch (IOException e) {
e.printStackTrace();
}
}
path = path.getParent();
if (Files.exists(path)) {
try {
Files.delete(path);
} catch (IOException e) {
e.printStackTrace();
}
}
한 달이라는 짧은 기간 안에 프로젝트를 완성해야한다는 부담감이 크게 다가와서 시작부터 조급한 마음을 갖고 있었습니다. 하지만 프로젝트를 하면서 하루에 시간을 충분히 갖고 차근차근 개발을 하다보니 일정을 며칠이나 앞서나가게 되는 경험을 하게 되었습니다.
이번 프로젝트를 통해 우선순위를 정해 정해진 기간 내에 프로젝트를 완성할 수 있도록 일정을 관리하는 방법을 깨달았습니다.
혼자 프로젝트를 진행하다보니 적용하고자 하는 기술들을 스스로 찾고 공부해야해서 어려움이 많았습니다. 예를 들어 API를 호출하기 위해 WebClient를 적용시키고자 공부를 해야했는데, 기초적인 CRUD가 아닌 ‘기술’을 적용시키는 건 처음이었기에 많은 시간과 노력이 들었습니다.
프로젝트를 진행하면서 스스로 수 많은 자료를 찾아 새로운 기술을 적용하는 과정은 쉽지 않았지만 한단계씩 성장하고 있다는 생각이 드니 힘듦보다는 즐거움이 더 크게 다가오는 경험이었습니다.
기획부터 DB설계, URL설계, 개발까지 스스로 공부하고 개발해본 첫 프로젝트를 진행하였습니다.
기획 단계에서 하고싶은 기능을 무작정 다 넣긴 했지만 ‘정말 내가 할 수 있을까?’라는 기대 반 두려움 반으로 시작했는데, 에러를 마주하고 스스로 해결하면서 ‘내가 진짜 할 수 있구나'라는 생각과 함께 큰 자신감을 얻게 된 프로젝트였습니다.