Search
Duplicate
✒️

프로젝트 회고록

프로젝트 회고록

NOTE
6주간 정말로 고생했고 열심히 했다고 생각은하지만.. 후회되는게 너무 많은거 같은 프로젝트다.
6주동안 참 힘들었지만 그래도 만족스러운 결과도 얻었고, 배운것도 있었다.
하지만 이번 프로젝트에서 후회되는 부분은 정말로 많았고 개선하고 싶었던 것도 너무 많았지만 시간은 내편이 아니었다.
이번 회고록에서 후회했던 부분을 정리해 다음 프로젝트에 참고하려고 한다.

프로젝트 소감

NOTE
까불지말고 하나의 분야라도 제대로 하자..
지금 생각하면 미친소리지만 나는 시작하기전에 모든 분야를 다 해보고싶었다. 디자인, 프론트, 백엔드, 데브옵스 등.. 물론 백엔드를 중점으로 할거지만 다른 분야도 건드리고 싶었다
하지만 이번 플젝에서 얕은지식은 별로 좋지못하다는걸 체감했다
얕은지식 → 설계상 문제생김 → 몰라서 이상하게 수정 → 이하 반복…
결국 내 시간을 더 쓰게되고, 그러면서 잠을 못자게 되고, 결국 프로젝트하면서 멘탈도 많이 흔들렸고 체력적으로 많이 힘들었다.
이번 프로젝트에서 많은걸 배웠다고 생각하지만, 그건 기술적인 부분은 아닌거같다 프로젝트를 하면서 팀원과 소통하는법과, 다른 팀원 코드 분석, 프로젝트 설계와 진행방식 같은걸 많이 경험해서 좋았다 거기다가 팀원들도 다 열심히하니 내 능력부족에 많은 아쉬움을 느끼기도 했다.
개인공부 시간을 짬내기도 힘들었다 하루 수면은 무조건 6시간이상으로 생각했을 때 취침을 12시 이전에 하고, 운동을 끝내고 돌아오면 9시 30분을 넘긴다. 그렇게되면 샤워시간을 제외하고 2시간 조금 모자란 정도로 여유가 있다는건데 너무 촉박하지 않은가? 그렇다고 버스에서 공부하는거는 너무 비효율적이다. 거기다가 이 경우도 내가 캠퍼스에서 내 할일을 모두 끝냈다고 가정한 경우다.
내가 발전하기 위해선 결국 어떻게든 실력을 늘려서 최대한 코드를 잘짜야한다고 공통때 느끼게 되었다 ㅋㅋ;

좋은 설계란 무엇일까?

NOTE
이번 프로젝트에서 절대적인 코드량은 적었다고 생각한다 문제는 기능구현 및 수정에서 생기는 버그모듈화 되지 않은 코드중복된 코드가 많아서 유지보수가 너무 어려웠던게 시간을 너무 많이 잡아먹은거 같다
처음 프로젝트를 시작할 때 목표는 강의에서 배웠던 스프링 개념들을 실제로 사용해보고, 설계를 깔금하게 만들어서 수정하기 쉽고 보기 좋을 코드를 만드는게 목표였다
하지만 백엔드에서는 스프링 데이터 JPA, 예외처리, 스프링 시큐리티, Junit코드 등에서 내가 생각한것보다 사용할 때 변수가 많았고 프론트엔드랑, 데브옵스쪽은 설계가 처음이라서 너무 많은 시행착오를 거쳐야했다 ㅠㅠ

백엔드

1. 스프링 데이터 JPA 사용

NOTE
1. 스프링 데이터 JPA를 사용하게되면, Repository가 인터페이스로 구현되는데 그러면 Service에 바로 로직을 작성해야 하는건가? 그리고 JPA에서 사용되는 중복되는 로직을 따로 뺄수는 없을까? 등 많은 고민을했다.
최종적인 코드구조, Repository에서 자주 쓰이는 코드는 common에서 관리했다.
public Community getCommunity(Long id) { return communityRepository.findStateOnById(id) .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ENTITY)); }
Java
복사
대표적인 커뮤니티 조회(커뮤니티 조회는, community 패키지 이외에도 많이 사용해서 분리시켰다.)
2.
Entity에서 데이터를 삭제할때 Delete가 아닌, 상태값의 변경에 따라 처리해주기로 했는데, 모든 Entity에서 기본적으로 0이 정상, 1이 삭제로 처리된다.
문제는 이게 모든 Entity마다 중복적으로 생기는 코드이고, 각 Entity마다 달아주기엔 너무 번거로워 지므로 다음번에는 공통 코드를 하나 만들려고 한다.
public void setCommunityStateOff() { this.communityState = 1; } public void setCommunityStateOn() { this.communityState = 0; }
Java
복사
대충 이런코드가 Entity마다 들어감;
3.
각 Entity별로 CRUD와 리스트조회는 어차피 Repository에서 같은 메서드로 진행될건데 이를 템플릿으로 만들 수 있지 않을까? 그러면 각 Entity별 특정로직을 제외하고는 상당한 중복코드를 줄일 수 있을거 같다.

2. 예외처리

NOTE
예외처리 부분에서 ControllerAdvice를 사용해서 전역으로 처리한다는 개념만 알았지, 에러코드를 커스텀해서 프론트에 전달하고 그 메세지를 프론트에서 받아서 처리하는 흐름의 개념이란걸 이번에 알게되었다
@Slf4j @RestControllerAdvice public class CommonExceptionHandler { @ExceptionHandler({ ObjectCrudException.class }) protected ResponseEntity<?> handleNotFoundDtoException(ObjectCrudException ex) { log.debug("에러코드: {}, Object: {}", ex.getErrorCode(), ex.getObject()); return new ResponseEntity(new ErrorDto(ex.getErrorCode().getStatus(), ex.getErrorCode().getMessage()), HttpStatus.valueOf(ex.getErrorCode().getStatus())); } @ExceptionHandler({CustomException.class}) protected ResponseEntity handleCustomException(CustomException ex) { ex.printStackTrace(); return new ResponseEntity(new ErrorDto(ex.getErrorCode().getStatus(), ex.getErrorCode().getMessage()), HttpStatus.valueOf(ex.getErrorCode().getStatus())); } @ExceptionHandler({ Exception.class }) protected ResponseEntity handleServerException(Exception ex) { ex.printStackTrace(); return new ResponseEntity(new ErrorDto(INTERNAL_SERVER_ERROR.getStatus(), INTERNAL_SERVER_ERROR.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR); } @ExceptionHandler({ MissingRequestHeaderException.class}) protected ResponseEntity handleHeaderException(Exception ex){ ex.printStackTrace(); return new ResponseEntity(new ErrorDto(NO_AUTHORITY.getStatus(), NO_AUTHORITY.getMessage()), HttpStatus.UNAUTHORIZED); } }
Java
복사
총 4가지의 에러를 받게 처리되어있는데, 나는 CustomException, MissingRequestHeaderException말고는 사용해본적이 없다 ㅋㅋ;
@AllArgsConstructor @Getter public class CustomException extends RuntimeException { private final ErrorCode errorCode; }
Java
복사
CustomException 코드
@AllArgsConstructor @Getter public enum ErrorCode { // 400 : 잘못된 요청 REQUEST_PARAMETER(400,"요청 파라미터 값이 올바르지 않습니다."), NO_DISH_NAME(400, "냥그릇 이름 정보가 없습니다."), // 401 : 접근 권한이 없음 NO_AUTHORITY(401, "접근 권한이 없습니다." ), // 404: 잘못된 리소스 접근 NOT_FOUND_ENTITY(404, "해당 객체를 찾지 못했습니다."), // 공통, // 404: 한번도 위치 정보를 발송하지 않은 기기를 등록 NOT_FOUND_LOCATION_INFO(404, "해당 냥그릇으로 부터 받은 위치 정보가 없습니다."), // 공통, //409 : 중복된 리소스 ALREADY_SAVED_DTO(409, "이미 저장된 객체입니다."), ALREADY_BLOCKED_COMMUNITY(409, "이미 삭제 처리된 게시글입니다."), ALREADY_UNBLOCKED_COMMUNITY(409, "블락 해제할 수 없는 글입니다."), // 500: INTERNAL SERVER ERROR, IMAGE_UPLOAD_FAIL(500, "파일 서버에 이미지 업로드 실패했습니다."), INTERNAL_SERVER_ERROR(500, "서버 내부 에러입니다."), NOT_CREATED_ERROR(500, "서버 관리자에게 문의해주세요."), NOT_RETRIEVE_ERROR(500, "서버 관리자에게 문의해주세요."), NOT_UPDATED_ERROR(500, "서버 관리자에게 문의해주세요."), NOT_DELETED_ERROR(500, "서버 관리자에게 문의해주세요."); private final int status; private final String message; }
Java
복사
공통 에러코드(각 에러마다 어떤 코드를 담을지 잘 생각해야 한다.)

3. 컨트롤러 헤더 예외처리 및 로그인여부 공통처리

NOTE
Controller부분에서 하나 기억나는 어려운점이 있었는데, 공통에러 로직처리를 할 때, token이 없는 유저는 401에러를 반환하는 로직을 구현하려 했는데 컨트롤러 인자값에서 token값 자체를 못받으니 500에러가 계속 나왔다.
아마 헤더값 자체를 받아서 값 여부를 확인하고 반환했으면 될거같았는데 .. 그러기엔 프로젝트 마감시간에 쫒기고있어 임시방편으로 헤더값이 없는 에러를 401로 수정해서 고쳤다.
이 부분에 대해서 Login여부 로직은 모든 응답에 필요한거 같다고 느낀거 같고, 이를 모듈로 관리하는법이 필요하다 느꼈다. (아마 AOP로 구현하는거 같은데 정확히는 모르겠다)
AOP를 공부하면서 로그인 여부, 로그등의 처리를 쉽게 하면 프로젝트 진행히 훨 수월할거라고 생각하고 있다.
@ApiOperation(value = "해당 냥그릇의 IoT 사진들 불러오기") @GetMapping("/dishes/{dishId}") public ResponseEntity<?> pictureList(@PathVariable("dishId") Long dishId, @RequestHeader(value = "Authorization", required = false) String token, Pageable pageable) { Slice<PictureDto> list = pictureService.findPictures(pageable, dishId, token); return ResponseEntity.status(HttpStatus.OK).body(list); }
Java
복사
Iot 사진들의 경우, 좋아요를 로그인한 유저만 할 수 있어야하므로 토큰을 받았다.(로그인 안해도 볼수는 있기떄문에 필수값은 아니였다)
@ExceptionHandler({ MissingRequestHeaderException.class}) protected ResponseEntity handleHeaderException(Exception ex){ ex.printStackTrace(); return new ResponseEntity(new ErrorDto(NO_AUTHORITY.getStatus(), NO_AUTHORITY.getMessage()), HttpStatus.UNAUTHORIZED); }
Java
복사
임시 땜빵코드 ㅋㅋ

4. DTO Converter

NOTE
Entity에서 빌더패턴을 사용했을때, Entity를 생성할때는 괜찮았지만 DTO변환시에 너무 코드가 길어지고 더러워졌던게 눈에 밟혔다. 그래서 DTO 컨버터를 작성하긴 했는데 특정 코드에만 적용되어 있어서 다음번에는 설계부터 들어갈까 한다.
public class DishConverter { public static DishDto dishConvertToDishInfoDto(Dish dish) { return DishDto.builder() .id(dish.getId()) .serialNumber(dish.getSerialNumber()) .adminGroup(dish.getAdminGroup()) .dishName(dish.getDishName()) .otherNote(dish.getOtherNote()) .loadAddress(dish.getLoadAddress()) .dishImg(dish.getDishImg()) .dishState(dish.getDishState()) .lat(dish.getLat()) .lon(dish.getLon()) .build(); } public static DishDto DishDto(Dish dish, DishFoodLog dishFoodLog){ String s = ""; return DishDto.builder() .id(dish.getId()) .serialNumber(dish.getSerialNumber()) .adminGroup(dish.getAdminGroup()) .dishName(dish.getDishName()) .otherNote(dish.getOtherNote()) .loadAddress(dish.getLoadAddress()) .dishImg(dish.getDishImg()) .dishState(dish.getDishState()) .lat(dish.getLat()) .lon(dish.getLon()) .food_weight(dishFoodLog.getFoodWeight()) .foodWeightChangeDate(s) .build(); } }
Java
복사

5. 테스트코드 작성

NOTE
초기 개발단계에서는 테스트코드를 작성했는데 이미지 업로드, 로그인 여부등 이게 정말 유효한 테스트 코드일까 의구심이 너무 많이 들었다.
결국 3일정도 작성하다가 다 폐기처분하고 프로젝트 막바지에 직접 웹에들어가서 하나하나 테스트하면서 진행했다 ㅠㅠ
테스트코드를 제대로 짜는법을 알았다면 시간을 더 절약할 수 있지 않았을까?

프론트

1. 구조 설계

NOTE
1.
프론트는 컨벤션을 작성하지 않아서 피를 많이본거 같다. 컴포넌트를 초기에 설계하지 않고 들어가서 css가 제각각이고, apis의 경우 처음에 파일로 따로 빼지않고 각 컴포넌트에 직접 사용해서 관리하기가 정말 힘들었다..
특히 api의 경우 인터셉터와 같은 공통로직이 들어가게되는데 컴포넌트에 직접쓴 axios를 공통코드로 다시 바꾼걸 생각하면 처음부터 잘 설계하는게 중요하다는걸 또 느낀다.
최종 프론트 구조
const authAPI = (options?: any) => { const token = localStorage.getItem("token"); const axiosInstance = axios.create({ baseURL, headers: { Authorization: token, }, ...options }); axiosInstance.interceptors.response.use( function (response) { return response; }, function (error) { commonUserErrorHandle(error); return Promise.reject(error); } ) return axiosInstance; }
JavaScript
복사
Axios 인스턴스 생성
const commonUserErrorHandle = (error: any) => { const { status, message } = error.response.data!; if (status === 401) { Swal.fire(message, '', 'error') } }
JavaScript
복사
예외상황 인터셉터 구현
2.
리덕스의 경우 타입스크립트를 사용하면서 느낀건데 조금 더 객체지향적으로 사용할 수 있지 않을까 많은 고민을 했다.
const communitySlice = createSlice({ name: 'community', initialState, reducers: { setIsGettingCommunityListOn(state) { state.isGettingCommunityList = true; }, setIsGettingCommunityListOff(state) { state.isGettingCommunityList = false; }, setIsGettingCommunityDetailOn(state) { state.isGettingCommunityDetail = true; }, setIsGettingCommunityDetailOff(state) { state.isGettingCommunityDetail = false; }, // 전체 게시글 getCommunityList(state, action) { state.communitySlice = action.payload; if(state.communitySlice.first){ state.communityList = state.communitySlice.content; }else{ state.communityList = state.communityList.concat(state.communitySlice.content); } }, // 게시글 좋아요 getCommunityListLikeOn(state, action) { let findCommunity = state.communityList.find((community: Community) => community.communityId === action.payload)!; findCommunity.isLike = true; findCommunity.likeCount += 1 }, getCommunityListLikeOff(state, action) { let findCommunity = state.communityList.find((community) => community.communityId === action.payload)!; findCommunity.isLike = false; findCommunity.likeCount -= 1 }, // 게시글 스크랩 getCommunityListScrapeOn(state, action) { let findCommunity = state.communityList.find((community) => community.communityId === action.payload)!; findCommunity.isScrap = true; }, getCommunityListScrapeOff(state, action) { let findCommunity = state.communityList.find((community) => community.communityId === action.payload)!; findCommunity.isScrap = false; }, //... }, });
JavaScript
복사
커뮤니티에서 이렇게 Action이 많은데, 커뮤니티 리스트 종류 역시 여러개다. 처음에는 다형성으로 어떻게 안되나 생각했는데, 걍 useState로 하는게 좋을듯
3.
컴포넌트는 꼭 상세하게 만들자!
type FlexDirection = "row" | "column" type FlexJustify = "flex-end" | "flex-start" | "center" | "space-between" interface Props { children: ReactNode; direction?: FlexDirection; justify?: FlexJustify; align?: FlexJustify; gap?: string; width?: string; height?: string; flexWrap?: string; }; export function LayoutFlex(props: Props) { return ( <LayoutInfo gap={props.gap ?? "0px"} direction={props.direction ?? "row"} justify={props.justify ?? "flex-start"} align={props.justify ?? "flex-start"} width={props.width ?? "auto"} flexWrap={props.flexWrap ?? ""}> {props.children} </LayoutInfo> ); }; const LayoutInfo = styled.div<Props>` display: flex; width: ${props => props.width}; gap: ${props => props.gap}; flex-direction: ${props => props.direction}; align-items: ${props => props.direction}; justify-content: ${props => props.justify}; flex-wrap: ${props => props.flexWrap}; `
JavaScript
복사
내가만든 flex div를 만들기위한 컴포넌트인데 솔직히 이거는 잘만든듯 ㅋㅋ

2. 타입스크립트 사용

NOTE
@ts-ignore
저 코드를 안쓰고 싶어도 쓸상황이 너무 많이 나온다.. 다음 프로젝트는 최대한 줄여봐야지

3. css 문제

NOTE
솔직히 이건 답이 없다.

데브옵스

1. AWS 문제

NOTE
이번에 서버에 익숙해지려고 일부러 내 돈내면서 직접 서버를 운영하고 도커로 이미지를 올렸다
초창기에는 무중단 배포나, 배포지식을 많이 배우고싶었는데 도커 맛보기만하고 만들기에 급급해서 아쉬웠다.
AWS를 만지면서도 여러문제가 많이 나왔는데 솔직히 깔끔하게 해결했던건 한번도 없는거 같다.
그냥 서버날리고 다시 이미지 뛰우고.. 이러다보니 내가 진짜 팀에게 민폐를 끼치는거 같아서 미안했다. 이런놈이 서버관리자..? 이래서 전문가가 필요하다는걸 뼈저리게 느낀다.
시간여유가 생기면 쿠버네티스나, 도커에대해서 좀 더 깊게 공부해보고 싶긴한데 스프링 백엔드 부터 제대로 구현하고 짬내서 배워야겠다
그리고 이번에는 AMI나 보안관련한거 env로 꼭처리하고 git ignore 처리해주자.

2. Nginx 문제

NOTE
해보고 싶었는데 팀원중 1명이 하고싶은 의지가 강해 내가 건드리지 않았다. 다음 프로젝트때 내가 서버를 만질지는 모르겠지만 하게된다면 한번은 봐야할거같다.

3. Jenkins

NOTE
이번에 도커에 젠킨스를 설치하고, 도커 인 도커 방식을 사용했는데 문제가 많은 방식으로 알고있다. 근데 그냥 돌아가니깐 냅두고 프로젝트 진행해서 2주차쯤 만들었던 코드가 그대로 있다 ㅋㅋ;
자동배포때 빌드버전, 테스트 코드 실행, 메세지 연동등 할 수 있는게 많아보였는데 이걸 내가 정말 해볼수 있을까 싶다

4. Docker

NOTE
1.
도커 로그 관리하기
2.
도커 파일 캐시 쌓이는거 주의하기
3.
도커 리소스 관리하기…
도커에 대한 기본적인 개념은 이해했다고 생각하고 있지만, 문제점이 생겼을때 해결능력이 너무 떨어졌던거 같다.