
큐시즘에 들어오고 나서 한 첫 프로젝트가 끝이 났다!
사실 개발이 끝난 건 3/10이었지만, 최종 발표는 3/19에 끝났기에 이제야 한 번 기록해보려고 한다.
이번 프로젝트에서 함께 하게 된 기업은 위플로 라는 이름의 드론 관련 업체였다!
핵심적인 기술은 버티핏으로, 드론이 이착륙 패드에 접촉을 하게 되면 고장여부 및 노후화 정도를 점수를 통해 확인할 수 있는 것이었다.
처음에는 저런 어려운 기술을 우리가 다루어야 하는건가..? 라는 생각이 들었지만 전혀 그런 것은 아니었고, 테스트 이후의 비즈니스 모델이 없다는 점이 문제점이었다.
또 다른 기업들은 코바코, 코드잇 등으로 네임밸류가 상당히 높아서 끌리기는 했지만 결국 위플로를 선택하고 팀 매칭이 되었다.
선택했던 이유는, 미팅에서 보였던 대표님의 태도가 솔직하고 진정성이 느껴져서 좋았고 무언가 실제로 비즈니스 모델을 만들어 보기에 좋을 것이라는 생각이 들었기 때문이다.
이미 기술력은 충분히 좋아서, 딱 그 이후만 만들면 되는 느낌? 이라 도전해보고 싶었다.
그렇게 팀 매칭이 되고, 설레는 첫 프로젝트가 시작되었다.
팀 빌딩 자체는 랜덤으로 진행된 것 같았다.
원래 알던 사람은 없었던 것 같구.. 기획 3 디자인 2 프론트 2 백엔드 2 이렇게 구성됐다.
항상 프로젝트를 할 때 개발 파트끼리만 협업하거나, 해도 5명 정도로 했던 터라
이정도로 다인원과 함께 협업을 진행해보는 것은 처음이었다.
무언가 일을 함께 하기 전에 미리 친해지고 나서 시작하는 걸 선호하는 타입인데,
하필 1주차 2주차를 소마 코테를 보느라 연속으로 빠지게 되어서 그럴 수가 없었다.
그래도 2주차 때는 코테 보고 나서 어떻게든 가서 참여를 하기는 했고, 회식까지 같이 가기는 했는데 무언가 이 어색한 분위기..
어색한 분위기를 정말 못견뎌서 원래 좀 나서서 푸는 편인데 아침부터 2차 코테때문에 진이 빠졌던 터라 텐션이 오르지가 않아서 힘들었다 ㅜㅜ
그래서 팀원들이랑 많이 친해지지 못한 것 같아서 아쉬운 마음이 들었다 😢
그러고 나서 백엔드를 함께 하는 친구랑 카페에 가서 ERD 설계를 진행했는데, 말도 좀 잘 통하고 생각보다 재밌게 작업을 할 수 있어서 오히려 즐거웠던 순간이었다.
이런 감정을 느끼는 걸 보니, 나는 그래도 백엔드를 잘 선택한 것 같다는 생각이 든다 😮💨
개발은 기업 프로젝트 종료 약 일주일 전부터 본격적으로 시작을 했던 것 같다.
그 이전까지는 함께 백엔드 파트인 현수가 CI/CD 및 배포를 다 해주었고, 나는 API 명세서랑 DTO 설계 등을 진행 해둔 상태였다.
이렇게 하다보니, 개발을 시작해서는 비즈니스 로직만 잘 짜면 되는 거라서 사실 큰 어려움은 없었던 것 같다.
그리고 이번에는 로그인 기능도 하지 않았고, 딱히 CRUD랄 것도 없는 기능들이어서 에러가 날 일도 거의 없었던 것 같다.
그래서 사실 백엔드 직무로서 크게 배울 게 없었을 수도 있는 프로젝트였지만, 그럼에도 개인적으로는 사소한 것부터 큰 것들 까지 많이 배울 수 있었던 것 같다.
좀 더 자세하게 말하면 yml 파일을 깃허브에 올리지 않고, 환경 변수만 서로 공유하면서 로컬에서 설정하는 것이다.
어떻게 보면 정말 당연한거고 기본적인 것인데, 첫 프로젝트에서도 혼자 백엔드였기도 하고 두 번째에서도 기간이 짧은 해커톤이었기에 이런 부분에서 모르는 것들이 많았던 것 같다.
그래서 개발을 시작하기 전에 오히려 잘 모르는 부분이 많아 식은땀을 자주 흘렸었는데, 다행히 함께 하는 현수가 친절하게 잘 알려줘서 따라갈 수 있었던 것 같다.
역시 본인이 모르는 것을 창피해하지 않고 인정하며, 적극적으로 물어보는 사람들이 빠르게 성장하는 이유가 있는 듯하다. 물론 쉽지는 않다 😅
나도 정확~히 이해한 것은 아니지만,
아마 글로벌에서는 security와 같은 각종 설정, 상수, 전역 에러 처리 등의 역할을 하는 것 같고
도메인에서는 말 그대로 도메인 별로 패키지를 하나씩 더 두어서 그 하위에 컨트롤러, 서비스, DTO 등을 두는 것인 듯하다.
처음에는 이런식으로 구조를 잡고 시작했는데, 하다보니 생각보다 백엔드측에서 개발할 것이 별로 없어서 그냥 컨트롤러, 서비스 패키지를 만들어서 나누어서 했었다.
이번에는 개발 기간이 길지 않았고, 클래스도 많지 않아서 괜찮았다만, 더 긴 프로젝트를 하는 경우에는 초반에 패키지 구조를 더 잘잡고 시작해야겠다는 생각이 든다.
Integer 과 int가 다른 타입인 것은 알고 있을 것이고..
Integer Long Boolean 같은 타입들을 보통 Wrapper 클래스 라고 부른다.
Queue, ArrayList와 같은 자료구조의 타입을 지정할 때 주로 사용을 했었던 것 같다.
여느때와 다름 없이 DTO 설계를 진행하던 중, 새로운 에러가 발견했다.
int 타입에서는 null이 들어갈 수 없다는 것이었는데, 아래 코드가 문제였다.
@Data
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ProductInfoDto {
private String productImage;
private LocalDate estimateDate;
private String category;
private String name;
private int price;
private int salePrice;
private int totalPrice;
private int amount;
}
상품의 정보를 담는 DTO이고,
필요한 정보만 보내주기 위해 @Builder를 사용,
그리고 null은 반환하지 않게 하도록 @JsonInclude(JsonInclude.Include.NON_NULL)를 사용했다.
즉, 빌더 객체를 만들고 값을 넣어주지 않으면 null이 되어 Json으로 보내줄 때 자동으로 빠지게 되는 것인데, 여기서 int형은 null이 될 수 없기 때문에 에러가 뜬 것이었다.
그렇기에 아래와 같이 수정을 진행해서 해결했었다.
@Data
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ProductInfoDto {
private String productImage;
private LocalDate estimateDate;
private String category;
private String name;
private Integer price;
private Integer salePrice;
private Integer totalPrice;
private Integer amount;
}
이러한 점을 깨닫고 난 후, 만약 null을 사용할 DTO라면 Wrapper 클래스를 사용해서 만들어주었다.
사소하지만 모를 수도 있는 부분이기에, 좋은 배움이었던 것 같다!
요즘 코드를 잘 때 보통 getter setter를 많이 사용하곤 한다.
그래서 자연스럽게 .get~ 으로 값을 가져와서 할당하고는 하는데, boolean형에 대해서는 그게 되지 않았다.
기존에는 아래처럼 boolean으로 코드를 작성했었다.
// 유저의 보험 가입 여부
@Column(name = "is_join")
private boolean isJoin;
그리고 아래처럼 자연스럽게 코드를 작성했으나, .isJoin(user.getIsJoin()) 이 부분에서 계속 빨간줄이 그어졌다.
@Override
public InsuranceResponse getInsuranceDetails(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new NotFoundException("User not found"));
return InsuranceResponse.builder()
// 이 부분
.isJoin(user.getIsJoin())
.joinDate(user.getJoinDate())
.updateDate(user.getUpdateDate())
.insuranceRate(user.getInsuranceRate())
.build();
}
그래서 검색을 해 보니, boolean 형은 getter에서 .get~이 아니라 .is~로 지원해준다는 사실을 알게 되었다.
만약 동일하게 .get~을 사용하고 싶다면, boolean의 Wrapper 클래스인 Boolean을 사용하면 된다고 한다.
그렇기에 결론적으로 boolean형을 그대로 사용하고 싶다면 .isJoin(user.isJoin())으로 바꾸면 되고, 통일을 하고 싶다면 타입을 바꿔주면 되는 것이었다.
나는 통일성을 위해서 아래처럼 타입을 변경해주면서 해결했었다.
// 유저의 보험 가입 여부
@Column(name = "is_join")
private Boolean isJoin;
타입으로도 여러 문제가 발생할 수 있다는 사실을 이번 프로젝트에서 깨달았고, 역시 기본기가 중요하다는 사실을 다시금 느낄 수 있었다!
이전에 프로젝트를 했을 때는, 아래처럼 응답 객체를 new ResponseEntity<>로 만들어 바로 반환했었다.
// 저장 및 응답 객체 반환
return new ResponseEntity<>(myPageService.addMyPage(request, userEmail), HttpStatus.CREATED);
이번에는 현수가 알려준 좀 더 깔끔한 방법을 사용했는데, ApiResponse라는 클래스를 하나 만들어서 응답 객체의 틀을 미리 짜두는 것이었다.
@Getter
@RequiredArgsConstructor
public class ApiResponse<T> {
@JsonProperty("isSuccess")
private final Boolean isSuccess;
private final String code;
private final String message;
@JsonInclude(JsonInclude.Include.NON_NULL)
private final T data;
//data만 들어가는 응답 포맷
public static <T> ApiResponse<T> onSuccess(T data) {
return new ApiResponse<>(true, SuccessStatus._OK.getCode(), SuccessStatus._OK.getMessage(), data);
}
//message까지 같이 넣을 수 있는 응답 포맷
public static <T> ApiResponse<T> onSuccess(String message, T data) {
return new ApiResponse<>(true, SuccessStatus._OK.getCode(), message, data);
}
...
오버로딩을 사용하여 여러 메소드를 만들었기에 데이터만 넣어줄 수도 있고, 필요하다면 메세지도 지정해서 따로 넣어줄 수도 있다.
@Getter
@AllArgsConstructor
public enum SuccessStatus implements BaseCode {
_OK(HttpStatus.OK, "200", "성공 입니다."),
_CREATED(HttpStatus.CREATED, "201", "성공적으로 생성되었습니다.");
private final HttpStatus httpStatus;
private final String code;
private final String message;
@Override
public ReasonDto getReason() {
return ReasonDto.builder()
.isSuccess(true)
.code(code)
.message(message)
.build();
}
@Override
public ReasonDto getReasonHttpStatus() {
return ReasonDto.builder()
.isSuccess(true)
.httpStatus(httpStatus)
.code(code)
.message(message)
.build();
}
}
위 SuccessStatus 같은 클래스나, Message 클래스에 메세지 등을 추가로 만들어서 응답 객체를 더욱 구체화할 수 있었다.
다만 여기서 getReason(), getReasonHttpStatus() 은 어떻게 쓰이는지 잘 모르겠기에.. 나중에 더 학습이 필요한 부분이다.
@GetMapping("/{user_id}")
public ApiResponse<InsuranceResponse> getInsuranceDetails(@PathVariable("user_id") Long userId) {
InsuranceResponse insuranceResponse = insuranceService.getInsuranceDetails(userId);
return ApiResponse.onSuccess(Message._GET_INSURANCE_MESSAGE.getMessage(), insuranceResponse);
}
그래서 컨트롤러에서 이런식으로 Resonse 데이터를 만들고, 적절한 메세지와 데이터를 넣어주어 반환함으로써 더욱 깔끔한 구현이 가능했다.
{
"code" : "200",
"message" : "유저의 보험 페이지를 조회합니다.",
"data" :
{
"isJoin" : true
"joinDate" : "2024-03-05",
"updateDate" : "2027-03-05",
"insuranceRate" : 10
},
"isSuccess" : true
}
위처럼 code message data isSuccess 필드로 나누어져 Json이 보내지는 것을 볼 수 있다.
찾아보니 방법은 다양한 것 같다. 다음 프로젝트 시에는 조금 더 적합한 형태를 갖추어서 진행해보아야겠다는 생각이 든다!
.get~을 붙여야 함사실 당연한거기는 한데, 막상 사용할 때 기억이 안나서 왜 이러지..? 했었기에 한 번 더 적어두려고 한다.
Message._GET_INSURANCE_MESSAGE.getMessage();
위의 경우에는 클래스명이 Message인 것으로, 뒤에도 getMessage()를 붙여주어 사용할 수 있다.
기존에 나는 그냥 service 패키지에 ~Service 클래스를 만들어서 바로 사용을 했었다.
이번에는 그러지 않고, ~Service는 Interface로, ~ServiceImpl은 실제로 메서드를 구현하는 방식으로 진행했다.
public interface InsuranceService {
InsuranceResponse getInsuranceDetails(Long userId);
InsuranceResponse joinInsurance(Long userId);
}
이렇게 인터페이스를 만들어서, 사용할 메서드들을 미리 지정해두고
@Service
@RequiredArgsConstructor
public class InsuranceServiceImpl implements InsuranceService {
private final UserRepository userRepository;
@Override
public InsuranceResponse getInsuranceDetails(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new NotFoundException("User not found"));
return InsuranceResponse.builder()
.isJoin(user.getIsJoin())
.joinDate(user.getJoinDate())
.updateDate(user.getUpdateDate())
.insuranceRate(user.getInsuranceRate())
.build();
}
...
이렇게 오버라이딩을 통해서 메서드를 구현해주었다.
분리하는 이유는 여러 가지가 있는데, 여기서 적는 것보다는 글을 하나 제대로 적어보는 게 좋을 것 같아 추후 작성해보려고 한다!
사실 내가 한 부분은 아니기는 하지만.. 그래도 느낀 점은 있었다!
프로젝트 시작부터 AWS EC2, RDS, S3, Docker, Github Actions를 이용해서 파이프라인을 구축해두니 굉장히 편리했다.
로컬에서 개발을 하고 pr을 올리면, 자동으로 테스트, 빌드, 도커 허브에 푸쉬, 배포까지 진행이 되니 좋았다.
테스트를 하면서 실제로 몇 번 에러를 미리 찾아내 수정을 했었는데, 이런 부분도 좋았던 것 같다.
기존에 프로젝트하면서 힘들고 불편했던 점들이 한 번에 해결된 듯한 느낌이었다.
그래서 한 번 혼자서 테스트 및 학습을 해본 뒤에 다음 프로젝트부터는 한 번 직접 적용해보고 싶다는 생각이 들었다!
물론 아쉬웠던 점들도 있었다.
개발에만 국한되는 것이 아니라, 프로젝트 전반적으로 얘기해보고자 한다.
기업 프로젝트는 팀 빌딩이 되고 마감까지 약 3주 정도의 기간이 있었다.
동아리에 들어와서 거의 바로 진행되는 거라 친해진 상태도 아니었고, 다들 정신도 없음을 감안하면은 긴 기간은 아니기는 하다.
그래도 다들 열심히 한다면 어느정도 유의미한 결과물은 만들 수 있는 기간이라고 생각을 한다.
하지만 내가 생각했을 때의 문제점은 기획 단계가 너무 길었다는 것이다.
1주 안에 기획을 어느정도 끝낸 후에, 얘기를 해서 수정을 하거나 전달을 하고
디자인 작업에 들어갔으면 좋았을 것 같은데,
대략 개발 마감 10일 전쯤?에 기획이 마무리가 되어서, 디자인이랑 프론트가 급하게 시작을 했었던 것 같다.
그래서 사실상 개발 기간은 약 일주일이 되지 않았었다.
물론 기획에서 시간을 헛되이 쓴 것은 절대 아니었다.
서칭도 많이 했고, 여러가지 기능들 및 솔루션도 생각을 해서 전달해주었지만
문제는 딱 하나 시간이 얼마 없었다는 것이었다.
한 일주일 정도만 더 있었다면 어떻게든 다 구현했겠지만, 프론트에서는 그럴 수 없다고 판단하여 결국 약 1/3 정도로만 MVP를 만들어 진행했었다.
백엔드야 어느 정도 미리 만들어둘 수 있기 때문에 가능했을 것 같은데, 프론트는 디자인이 나온 후에 개발을 진행하고 백엔드에서 API를 만들어주어야 연동도 할 수 있어서 그런 판단을 한 것 같다.
그래서 프론트라는 파트가 생각보다 머리도 아프고 중간에서 피곤할 것 같다는 생각도 들었다.
어쨌든 간 결국 MVP는 완성을 해냈지만, 기능이 많지는 않아서 조금 비어보이는 점이 아쉬웠다.
위에서 이어지는 말인데, 개발자의 입장에서는 어느 정도
이 기간 동안 이걸 어떻게 해? 라는 생각이 들 수도 있는 기획이었다.
하지만 과연 여기서 책임이 기획 파트에만 있을까?
그렇지는 않다고 생각을 한다.
개발 파트도 조금 더 관심을 갖고 물어보고, 미리 전달을 받고 싶다는 의사를 표방했다면 조금은 더 나은 일정관리가 되었을 수 있다.
나 또한 그때 당시에 소마 코테를 준비하느라 초반에 제대로 집중하지 못했기에, 할 말은 없는 듯하다.
그래서 다음 프로젝트에는 기획 단계부터 관심을 가지며 최대한 적극적으로 참여하는 것이 중요하겠다라는 생각이 들었다.
그래야만 결국 나도 일정에서 오는 스트레스를 줄일 수 있고, 미리 어떻게 개발할 지 구상해볼 수 있기 때문이다.
이것 또한 내가 초반에 나가지 못해서 그랬던 것도 있지만, 초반에 제대로 회식을 하면서 친해지는 시간이 있었더라면 더욱 좋았지 않았을까 하는 생각도 든다.
지금은 모두 친하고 편하게 지내고는 있지만, 개발이 거의 끝나기 전까지만해도 다들 어색한 상태였기 때문에 그런 점이 아쉬웠다.
왜냐하면 친근감이 있어야 의견도 적극적으로 낼 수 있고, 여러 말을 전달하는 데 다른 부분에서 시간이나 감정을 쓰지 않을 수 있기 때문이다.
적어도 나는 그렇게 생각한다.
그래서 프로젝트를 하려고 모였다면 바로 개발만 하는 것이 아니라,
초반에는 여러 번 만나며 친해지기도 하고 서로의 스타일도 알아가면서, 다르다면 이를 맞추려는 과정이 꼭 필요하다고 생각을 한다.
원래도 모두를 챙기고 싶어하는 성향이기도 해서, 이를 보면 기획 혹은 PM에도 적합하지 않나라는 생각이 들기는 하지만.. 섣불리 도전해보기는 쉽지 않은 것 같다.
그래서 백엔드를 계속 맡으며 기술적으로 실력을 키우며 전문성을 높이다가, 나중에 경력이 쌓이면 PM을 맡아보는 것도 좋겠다라는 생각이 든다.
여기까지 이번 큐시즘 29기 기업 프로젝트를 하면서의 회고를 작성해 보았다.
물론 아쉬운 점도 많지만, 그래도 실질적으로 중요한 것들을 배웠고 앞으로 어떤 걸 더 공부해야할지 알 수 있었던 귀중한 시간이었다!
이번에 함께했던 팀원들 및 모든 큐시즘 29기 사람들에게 고생했다고 말해주고 싶다 👍🏻
이제 다음 프로젝트를 위해 달려봐야겠다 🔥
상호님 멋져요 같이하신 팀원분도 멋지네요