나는 '문제를 해결'하는 개발자

송인재·2024년 10월 3일

회고

목록 보기
7/15
post-thumbnail

개요

이번 스프린트에서부터 사내에 기능을 본격적으로 개발하기 시작했다.

지난 스프린트까지는 Front-End기능까지만 구현을 담당했었는데,
회사 백엔드 채택 언어인 NestJS에 대한 기본적인 공부를 마쳤고

(지금은 DB 지식이 모자란 것 앝아, 해당 책으로 먼저 공부 중이다.)

본격적으로 기능을 구현하는데 무리가 없을 것이라 생각했다.
이후 판단은 CTO님께서 하실 것이라고 생각하고,
앞의 두 기능의 구현을 단순구현과 더불어 어떻게 더 개선할 수 있을지 고민하며,
시험을 본다는 생각으로 임했다.

앞의 두 기능을 성공적(?)으로 개발을 마쳤고,
CTO님께서 회사 핵심 기능 개발 목록 중 하나인 "환불" 기능을 맡겨주셨다.

기능 개발에 있어, 어떤 기능이 주어져도 다 개발할 자신은 있었지만,
"돈"과 관련된 개발에 있어서 만큼은
약간의 두려운 마음에 나중으로 미루고 싶은 생각이 들었었다.

하지만 이전에 CTO님께서 이런 말씀을 해주셨다.

개발이 서비스의 발목을 잡아서는 안된다.
우리는 어떠한 것이든 개발할 수 있어야한다.

단순히 두려움 때문에 연차 핑계를 대며, 기능 개발을 미룰 수 없다고 생각했다.
인턴이든 입사한지 한달이 됐든 나는 서비스의 일원이고 해내야했다.

본격적인 업무 전에, 결제에 관련한 회사 코드들과 DB들을 분석하고 업무에 들어갔다.
(이때까지는 몰랐다. 환불 구현을 위해 수많은 고난의 길을 걸을 줄은...)

2차 스프린트 기간: 24.07.22(월) ~ 24.08.02(금)


업무

공지사항 기능 추가

기존 언어인 Meteor에서 React로 마이그레이션을 진행을 하며,
몇가지 사라진 기능들 중 하나가 "공지사항" 기능이었다.

작업 이전까지는 DB에 남아있는 공지사항들을 불러와 보여주고 있었는데,
공지사항들을 새로 추가할 일들이 여럿 생기면서,
해당 기능의 필요성이 대두되어 기능 개발에 들어갔다.

이전 공지사항들과의 호환성에 문제가 없어야 해서,
어찌 보면 약간 까다롭기도 하다가도,
어찌보면 어느정도의 틀이 정해져있어 조금은 간단하기도 했다.

단순 공지사항 CRUD에 더불어,
어떠한 공지사항들은 팝업에 띄울 수 있어야 하고,
어떠한 공지사항들은 상단에 고정시킬 수 있어야해 조금 더 즐거웠다.

그 일례로 공지사항 상단 고정에 있어,
단순하게 상단 고정 여부를 필드를 추가하여 boolean값으로 두고자 했으나,

상단 고정 여부끼리는 순서를 어떻게 구분할지 질문이 들어와,
updateAt으로 상단 고정이 필요한 것은 수정을 걸쳐
상단에 올리는 방식으로 답했으나,
말하고도 의문이 들었던 점이 수정이 필요없는 것들을 수정을 걸친다는 점이었고,
상단 고정 여부를 number로 바꿔 이를 해결했다.

터득한 Point.

  • 하나의 문제에 대한 다양한 해결책과, 상황에 맞는 다른 해결책이 존재함

데이터 파싱 오류 해결

클라이언트에서 서버로 데이터를 보낼 때,
몽고DB의 ObjectId나, Date 값들이 string으로 바뀌어 들어와,
원시 값이 아닌 값들에 대한 파싱 작업이 필요했는데

해당 파싱이 어떠한 로직에서는 컨트롤러,
어떠한 로직은 서비스 로직에서 수행하고 있었다.

이것을 파싱하는 것을 컨트롤러 단에 둘까 서비스단에 둘까 고민을 하는데,
둘 다 썩 좋은 판단은 아닌 것 같았다.
분명 다른 사람들도 이와 같은 고민을 하고 있을 것이라 생각했고,
공식문서에서 class-transfomer 라이브러리를 이용한 것을 떠올렸다.

때문에 해당 방식을 이용하여 해결하고,
다른 곳들을 모두 적용하려면 문제가 나는 곳에 해당하는 DTO를 모두 만들어
이를 수행해야했기 때문에, 이미 존재하는 코드에 영향이 있을 것 같아,
다른 코드에는 이를 수행하지 않고 PR에 코멘트로 남겼다.

하지만 전혀 다른 곳에서 문제가 발생했는데,
해당 라이브러리가 3년전이 마지막 업데이트 된 것을 말해주시면서
해당 라이브러리의 안정성에 대한 의문을 남겨주셨다.

해결방법을 생각하다가 Pipe를 통해 데이터를 파싱하는 것이 떠올랐다.
이를 응용하여 ObjectId도 파싱이 가능하지 않을까 생각하여,
공식문서와 NestJS 깃허브에 들어가 Pipe 관련 코드들을 뜯어봤다.

기존 Pipe들은 하나의 depth에 대해서만 파싱했지만,
우리의 DB에는 도큐먼트 안에 서브도큐먼트들도 많고,
한 body에 여러 값들을 파싱해줘야 했기에 여러 depth까지도 파싱이 가능해야했다.
(사실 데이터를 넘길 때 여러 depth가 있다면 그것은 매우 아쉬운 구조지만... 쩔수...)

그래서 아래와 같이 여러 depth여도 파싱이 가능하도록 Pipe를 구현했다.

import { Injectable, PipeTransform, BadRequestException } from '@nestjs/common';
import { Types } from 'mongoose';

@Injectable()
export class ParseObjectIdPipe<T> implements PipeTransform<T, Types.ObjectId | T> {
  constructor(private readonly paths?: string[]) {}

  transform(value: T): Types.ObjectId | T {
    if (typeof value === 'string') {
      return this.convertToObjectId(value);
    } else if (this.paths && typeof value === 'object' && value !== null) {
      const newValue = { ...value };
      this.iterateObject(newValue);
      return newValue;
    }
    return value;
  }

  private iterateObject(obj: any, path?: string): void {
    for (const [key, val] of Object.entries(obj)) {
      const currentPath = path ? `${path}.${key}` : key;

      if (this.paths.includes(currentPath) && typeof val === 'string') {
        obj[key] = this.convertToObjectId(val);
      } else if (typeof val === 'object' && val !== null) {
        this.iterateObject(val, currentPath);
      }
    }
  }

  private convertToObjectId(value: string): Types.ObjectId {
    try {
      return new Types.ObjectId(value);
    } catch (error) {
      throw new BadRequestException(`Invalid ObjectId: ${value}`);
    }
  }
}

다만 위와 같은 코드를 사용하다보니 개선할 여지가 느껴졌다.
왜냐하면 모든 object들의 depth를 확인하기 때문에,
도큐먼트들 안에 많은 필드들과 서브 도큐먼트들이 있다면
불필요한 곳까지 탐색을 하고 있기 때문이었다.

그래서 필요한 path들에 한해서만 depth를 들어가도록 아래와 같이 개선했다.

import { Injectable, PipeTransform, BadRequestException } from '@nestjs/common';
import { Types } from 'mongoose';

@Injectable()
export class ParseObjectIdPipe<T> implements PipeTransform<T, Types.ObjectId | T> {
  constructor(private readonly paths?: string[]) {}

  transform(value: T): Types.ObjectId | T {
    if (typeof value === 'string') {
      return this.convertToObjectId(value);
    } else if (this.paths && typeof value === 'object' && value !== null) {
      const newValue = { ...value };
      this.paths.forEach((path) => this.convertByPath(newValue, path));
      return newValue;
    }
    return value;
  }

  private convertByPath(obj: object, path: string) {
    const keys = path.split('.');
    let currentObj = obj;
    let prevObj = {};

    for (const key of keys) {
      if (!(key in currentObj)) return;
      prevObj = currentObj;
      currentObj = currentObj[key];
    }

    const finalKey = keys[keys.length - 1];
    if (typeof prevObj[finalKey] === 'string') {
      prevObj[finalKey] = this.convertToObjectId(prevObj[finalKey]);
    }
  }

  private convertToObjectId(value: string): Types.ObjectId {
    try {
      return new Types.ObjectId(value);
    } catch (error) {
      throw new BadRequestException(`Invalid ObjectId: ${value}`);
    }
  }
}

NestJS를 사용한지는 이제 한달이 안되가지만,
알면 알수록 재밌는 언어인 것 같다.
처음에 입사했을 때는 생각지도 못한 백엔드를 공부해야한다는 사실이 막막했지만,

"개발자"란 단순히 코딩하는 사람을 넘어 문제를 해결하는 사람이고,
문제를 해결하는 사람이, 단순히 모른다는 이유로
문제를 해결하지 못하면 안된다고 생각한다.

내가 개발자를 택한 이유도,
내가 기술을 통해 세상을 조금 더 나은 방향으로 발전시키고 싶었기에
이제는 스스로를 "프론트엔드 개발자"라고 묶어두지 않아야겠다.
나는 문제를 해결하는 개발자가 되어야겠다.

터득한 Point.

  • 나는 문제를 해결하는 "개발자" 엔지니어이다.

환불 기능 추가

현재 환불 기능은 고객과 파트너가 전화나 채팅으로 상의를 거친 후,
고객 혹은 파트너가 사내 고객센터에 전화하여 환불을 신청하는 방식이다.
이후, 사내 환불 담당자가 어드민 페이지에서 환불 처리를 진행한다.

사실 환불 담당자가 환불처리만 하는 것이 아니다.
스타트업이 대부분 그렇겠지만 한 사람이 많은 일을 한다.
때문에, 한 사람의 리소스는 중요하다.

스타트업에 입사하기 이전에는, 성장할 수 있는 방법이 있다면 다 하면 된다고 생각했다.
하지만 현실은 녹록지 않았다.

그것을 앎에도 더 할 수 있는 리소스가 나오지 않는다.
개발팀에 역할은 서비스의 기능을 구현하고 유지보수하는 것 이외에,
다른 사람의 리소스를 개발로서 줄여주는 역할도 포함이 된다.

이번 환불 기능은 유저들의 UX뿐만이 아니라,
우리의 리소스도 줄이고 성장을 위한 하나의 발판이기도 했다.
때문에, 오류가 없도록 한 번 만들 때, "잘" 구현하고자 했다.

그에 대한 일환으로 기존에 있던 환불 코드도 개선하고자 했다.
(나중에서야 깨닫는다. 이 발상이 위험한 발상이었다는 것을...)

결제 과정과 환불 과정에 있어 모든 과정들에 로그를 쌓는다.
하지만 해당 로그를 쌓는 과정에서 불필요한 데이터들을 많이 넘겨주고 있다 생각했다.
그래서 이를 위해 필요한 데이터만 넘겨줘 업데이트를 할 수 있도록,
repository 파일안에 파이프라인을 거쳐 업데이트하는 새로운 메소드를 만들었다.

하지만 해당 메소드를 쓰는 곳은 당장에 하나였는데,
하나만을 위한 메소드를 만드는 것이 맞을까, 필요없는 데이터들을 넘기는 것이 맞는지
정확하게 감이 오지 않아 관련하여 CTO님께 여쭤봤다.

CTO님과의 대화를 통해 스스로에게 2가지의 문제점이 있었다.

지금까지의 문제점
1. DB 쿼리를 통해 문제를 해결하려 한다.
2. 재사용성이 좋고, 짧고 이쁜 코드를 지향하고 있다.

먼저 첫번째의 문제점은, DB 쿼리로 모든 것을 해결하려고 했던 점이다.

DB 쿼리로 모든 것을 해결하려고 한다는 것은,
쿼리를 그만큼 복잡하게 짜고 있다는 것이고,
복잡하게 짜고 있다는 것은 DB의 의존도와 비용이 증가한다는 소리다.
(현재도 최대한 복잡한 쿼리를 지양하고 있음에도 DB에서 가장 많은 비용이 나가고 있다.)

그리고 복잡한 쿼리는 DB의 의존도를 높이게 되는데,
데이터베이스의 한계를 늘리는데는 한계가 존재하기 때문이다.
때문에, 브라우저나 서버에 어느정도 위임을 하되,
매개변수로 쿼리를 넘기는 것과 같이 단일책임원칙을 위반하는 일은 없도록 짜야한다.

DB에 대해 공부했던 것을 써먹을 수 있다 생각하여 좋았는데,
DB에 더 많이 알게 되었다고, 그것을 사용할 수 있다고 사용하는 것이 아니라,

DB에 대해 많이 알되, 모르고 사용하지 않은 것과, 알고 사용하지 않은 것에는
차이가 있고, 해당 지식을 적재적소에 사용해야한다는 것을 다시 깨달았다.

두번째 문제점은, 재사용성이 좋도록 짧고 이쁘게 짜려고 하는 것이다.

재사용성이 많아진다는 것에는 장점만 있는 것이 아니다.
서로의 의존성이 강해진다는 소리다.

때문에, 코드에 깊게 공을 들이는 순간 물렁한(?) 코드가 되지 못한다.
어떠한 기능을 만들어 달라는 요구사항이 왔을때,
코드가 이렇게 짜여져있어서 안된다는 말이 나오면 안된다.

비즈니스는 움직여야한다. 코드 때문에 멈추는 일이 벌어져서는 안된다.
좋은 코드는 클린 코드가 아니라, 비즈니스에 발맞춰 움직일 수 있는 코드이다.

코드의 가독성을 떨어뜨리는 것은 최적화 말고는 없어야한다.
그리고 미래를 생각해서, 미리 재사용성이 가능하도록 구현해서는 안된다.

백엔드뿐만이 아니라, 프론트에서도 계속해서 코드에 공을 들이고,
재사용성을 중요시하여 코드를 짜려고 하다 CTO님께서 좋은 말씀을 해주셔서,
습관이 굳어지기 전에 계속 나아지고 있다.
(사실 CTO님이 지켜보시다가 이전과 비슷한 질문을 해서 말씀해주셨다...😅)

하지만 늘 CTO님의 끝말에 이런 말씀을 해주신다.

개발에는 정답이 없고 상황에 맞는 해결책만 있을 뿐이다.
계속해서 많은 경험을 쌓아야한다.

API 명세서가 굳어있다면 그것은 종이쪼가리에 불과하다는 말처럼,
고정관념을 갖지 않고, 가지고 있는 지식이 틀릴 수도 있다를 명심하며,
늘 유연한 자세를 갖도록 해야겠다.

(다음 스프린트에서는 꼭 완성하리...)

터득한 Point.

  • DB의 의존성을 많이 갖도록 짜지말자.
  • 좋은 코드란 비즈니스와 함께 움직일 수 있는 코드이다.
  • 개발에는 정답이 없다. 상황에 맞는 해결책이 각기 다르다.
profile
꿈을 꾸고 도전하기🌟

0개의 댓글