SMAIVNN
article thumbnail

스마일게이트 개발 캠프를 성공적으로 마친 기수들은 커뮤니티 활동을 이어갑니다!

커뮤니티에서는 이전 기수들과 새로운 기수들이 함께하며 다양한 스터디 활동, 세미나 활동이 이루어지는데요. 그렇게 캠프의 인연이 계속해서 이어집니다.

 

저 또한 이번에 스마일게이트 커뮤니티에서 주관하는 스터디인 테크 하이킹_협업을 위한 클린 코드와 아키텍처를 5주간 진행하는데요. 취준생인 저를 제외하고 현업을 뛰시는 N년차의 뛰어나신 분들과 함께할 수 있게 되었습니다. 이 글은 해당 스터디의 첫 번째 글입니다.

 

시작

 

스터디는 로버트 C 마틴의 클린 코드라는 책을 읽고 해당 책의 내용에 대해 다양한 토론사례를 확인하는 것으로 계획되어 있습니다. 구글에 클린 코드를 쳐보면 "클린 코드 비판"이 가장 먼저 나오는 만큼 다양한 의견이 있는 책입니다. 하지만,개발자로서 협업을 위해 어떻게 하면 더 좋은 코드를 작성할 수 있을까? 좋은 코드란 뭘까?라는 고민을 모두 해보았을 텐데, 이것에 대한 각자의 답을 찾고자 시작되었습니다. 

 

저 또한 이 스터디 이후, 저만의 좋은 코드를 정의하고 더 나아가 좋은 아키텍처란 뭘까 생각하며 앞으로 취업 이후에도, 또 협업 과정에서도 적용할 있는 습관을 갖는 것이 목표입니다.

 

W1, 첫 주차의 주제는 나쁜 코드가 부르는 결과에 대해서 이야기 해보았습니다. 1장 깨끗한 코드, 2장 의미 있는 이름을 읽고 각자가 있었던 사례 혹은 현재 문제가 있는 코드를 함께 보며 토론 형식의 피드백을 진행했습니다.

 

클린 코드란

책에서는 이런 의문이 들도록 내용이 전개됩니다. 

"클린 코드란 뭘까?"

 

책에서는 다양한 사례를 들어 설명해주지만, 저는 의도한 대로 동작하는 예상 가능한 코드라고 스스로 정의해보았습니다. 

 

스터디원들의 한줄 생각은 아래와 같습니다.

"내가 예상하는대로 동작하는 코드"

"작성자가 이미 모든 사항을 고려했으므로, 고칠 궁리를 하다보면 언제나 제자리로 돌아온다. 튜닝의 끝은 순정."

"깨끗한 코드는 결코 설계자의 의도를 숨기지 않는다. 오히려 명쾌한 추상화와 단순한 제어문으로 가득하다."

"효율적인 코드, 중복 x 가독성 o"

"아름다운 코드: 가독성의 측면에서 어느 것 하나 빼거나 더할 필요가 없는 것"

 

다양하고 재밌는데요, 이제 위에 내용과 더불어 제가 개인적으로 생각해볼 수 있는 것이 무엇일까 고민한 흔적을 기록하려고 합니다.

 

클린 코드? 클린 아키?

책에서는 자신이 짠 클래스 이름과 메서드 이름을 모두 암기하지 못한다고 합니다. 지난 프로젝트를 진행하며 딱 같은 상황이 있었는데요. 아래 처럼, 로컬 함수를 만들어놓고 이 함수를 까먹어서 새로운 유틸 함수를 만듭니다. 심지어 둘의 동작은 같은데 작성한 코드도 다릅니다.

// 로컬함수
private extractImageUrls(content: string): string[] {
    const imgTags = content.match(/<img[^>]+src="([^">]+)"/g);
    if (!imgTags) {
      return [];
    }
    return imgTags
      .map((tag) => {
        const match = tag.match(/src="([^">]+)"/);
        return match ? match[1] : '';
      })
      .filter((src) => src);
}

//유틸 함수
export function extractImageUrlsFromHtml(html: string): string[] {
  const imageUrls = [];
  const regex = /<img[^>]+src=["']([^"']+)["']/g;
  let match;
  while ((match = regex.exec(html)) !== null) {
    imageUrls.push(match[1]);
  }
  return imageUrls;
}

 

사실 작성한 코드의 이름을 까먹기도 했고, 슥 봤을때도 뭐였더라 싶어 넘어갔습니다. 이처럼 클린 코드, 좋은 이름은 참 중요합니다.

 

그렇다면 클린 코드란 뭘까요?

 

개인적으로는 의존성이 적은 코드, 한 가지를 잘 하는 코드가 좋은 코드일 수 있다라고 생각합니다. 우리가 개발을 진행하며 다양한 서비스의 함수를 참고하고, 특정 코드에 의존하는 상황은 빈번합니다. 그렇기 때문에 이를 느슨하게 하기 위해서 의존성 주입을 진행하고 모듈화를 하고 다양한 디자인 패턴을 사용합니다.

 

저는 NestJS를 현재는 주로 사용하고 있습니다. NestJS에서는 이미 모듈기반 아키텍처를 사용하고 의존성 주입(DI: Dependency Injection)을 사용하고 있습니다. 그렇다면 어떻게 의존성을 더 줄일수 있을까요?

 

인터페이스를 사용하고, 팩토리 패턴을 사용하며 필요할 때 인스턴스를 생성하는 동적 구조로 결정할 수 있을겁니다.

함수형 프로그래밍을 사용할 수 도 있을 것이고, SOLID원칙을 적용한 설계 패턴을 적용할 수 있을 것입니다.

 

이에 대해 고민하고 조사하며 이런 유튜브를 봤는데요

https://www.youtube.com/watch?v=vE74gnv4VlY&ab_channel=CoderOne

 NestJS와 TypeScript에서 올바른 클린 코드를 작성하는 방법-SOLID라는 꽤나 자극적인 제목입니다. 그만큼 댓글에서도 다양한 의견이 있습니다.

 

대표적으로 하나만 말하자면 단일 책임 원칙, SRP(Single Responsibility Principle)에 대한 것입니다.

영상의 예제를 갖고 설명을 해봅시다.

@Injectable()
export class OrdersService {
  constructor(
    private prisma: PrismaService,
    private emailsService: EmailsService,
  ) {}

  async submitOrder(data: Prisma.OrderCreateInput): Promise<Order> {
    const createdOrder = await this.prisma.order.create({ data });
    //❌ Bad
    //Here we'are sending Order email inside the OrdersService which should
    //Have one responsibility (Taking care of Orders)
    this.emailsService.sendOrderEmail(createdOrder.orderId);

    return createdOrder;
  }
}

 

영상 속 유튜버는 위 코드처럼 SubmitOrder함수가 주문에 관한 함수라는 이름을 무시하고 이메일 서비스의 주문 이메일을 실행하므로서 SRP를 위배하는 나쁜 코드라고 말하였습니다. 그리고 이것을 해결하기 위해서는 아래와 같이 컨트롤러에서 작성을 해야한다고 합니다.

class SubmitOrderDto {
  @IsNumber()
  productId: number;
}

@Controller('/orders')
export class OrdersController {
  constructor(
    private ordersService: OrdersService,
    private emailsService: EmailsService,
  ) {}

  @Post()
  public async submitOrder(@Body() submitOrderDto: SubmitOrderDto) {
    const createdOrder = await this.ordersService.submitOrder({
      products: { connect: [{ productId: submitOrderDto.productId }] },
    });

    //✅ Good
    //Services should allow us to share code between modules easily and effortlessly
    //Each Service method should follow a SRP
    await this.emailsService.sendOrderEmail(createdOrder.orderId);

    return {
      message: 'Thanks for you order!',
      orderNumber: createdOrder.orderId,
    };
  }
}

 

서비스 레이어는 SRP에 맞게 각자에 맞는 함수를 작성하고, 컨트롤러에서 다양한 서비스를 가져와 비즈니스 코드를 작성합니다!

 

- 어라? 하지만 우리가 주로 사용하는 MVC패턴, Controller, Service, Repositoy패턴에서는 Controller는 Service에게 전달하는 역할만 하고 Service레이어에서 비즈니스 코드를 작성한다고 하는데요? (저도 이 입장입니다.)

 

- 어라? 그럼 SRP를 위배하잖아요 !!!

 

(실제로 댓글에서 위와 같이 싸웁니다.)

 

그리고 스터디원들 또한 위와 같은 고민이 있었습니다.

 

정확히는 서비스를 작성하다보면 코드의 중복이 생기기도 하고 SRP 를 위배하기도 한다. 어떻게 해야할까?입니다.

 

SRP

결론적으로 말하자면, 로직에 따라 생각해야한다입니다.

 

가독성에 기반한 코드를 먼저 생각해봅시다. 우리는 위에서부터 쭉 내려오는 비즈니스 로직을 선호합니다. 그 편이 가독성이 훨씬 훨씬 좋기 때문이죠.

@Injectable()
export class OrdersService {
  async submitOrder(data: Prisma.OrderCreateInput): Promise<Order> {
    const createdOrder = await this.prisma.order.create({ data });
    // save created order
    // anothor code
    // and other code
    this.emailsService.sendOrderEmail(createdOrder.orderId);
    // and other code again
    return createdOrder;
  }
}

 

이 코드에서 원칙을 지키기 위해 유튜버가 말한 것처럼 다 분리해야할까요? 아니요. 결국 어디선가 합쳐질 코드입니다.

그렇다면 SRP를 좁게 보지 않고 상황에 맞게 봐야한다가 더 적당할 듯 싶습니다.

 

submitOrder는 이름 그대로 주문을 하는 책임을 갖습니다.

또, sendOrderEmail함수는 주문 이메일을 발송하는 책임만 갖습니다.

@Injectable()
export class EmailsService {
  public async sendOrderEmail(orderId: number) {
    console.log(`Sending order email for order ${orderId}`);
    //TODO: Send Email logic here...
  }
}

 

결국 책에서 말하는 의미있는 이름을 짓는 것이 함수의 방향성을 나타내고, 해당하는 역할을 수행한다면 SRP에 위배되지 않는다고 생각합니다. 무조건의 분리보다 더욱 합당하다고 생각하지 않으신가요?

 

그렇다면 중복 코드는 어떻게 해야할까요?

 

중복 코드

그럼 클린 코드를 위해 중복 코드는 어떻게 해야할까요? 코드 중복은 비효율적이니까 반드시 하나로 빼야할까요?

 

개발을 하다보면 여러 함수에서 공통적으로 사용되는 로직이 있을 때 이를 별도의 함수로 분리하는 것이 좋다는 이야기가 흔합니다. 하지만 모든 중복 코드를 무조건 함수로 분리하는 것이 항상 좋은 방법은 아닙니다.

 

예를 들어, logicA, logicB, logicC라는 함수에서 공통적으로 D라는 로직이 들어간다고 가정해봅시다. 이 D가 단순히 중복된다고 해서 반드시 함수로 분리해야 하는 것은 아닙니다.

 

이유는 각 함수에서 D를 다르게 변환하거나, 특정 맥락에서만 사용될 수 있는 특성이 있을 때, 굳이 함수로 빼면 오히려 코드 가독성과 유지 보수성이 떨어질 수 있기 때문입니다. 다음은 이를 코드로 풀어 설명한 예시입니다.

@Injectable()
export class Service {
  // logicA에서는 D가 그대로 사용됨
  public async logicA(Id: number) {
    const resultA = this.commonLogic(Id); // D를 수행하는 로직을 함수로 뺌
    console.log(resultA);
  }

  // logicB에서는 D가 조금 다르게 변환됨
  public async logicB(Id: number) {
    const resultB = this.commonLogic(Id) + ' is modified'; // 변형된 D 로직
    console.log(resultB);
  }

  // logicC에서는 D가 완전히 다른 용도로 사용됨
  public async logicC(Id: number) {
    const resultC = `ID_${this.commonLogic(Id)}`; // 완전히 다른 형식으로 사용
    console.log(resultC);
  }

  // D 로직을 별도의 함수로 분리
  private commonLogic(Id: number): string {
    return `User ID is ${Id}`; // 단순한 예시의 D 로직
  }
}

 

위 코드에서 보듯이, commonLogic이라는 함수를 만들어 logicA, logicB, logicC에서 공통적으로 사용하지만, 각 함수는 이 로직을 다르게 변환하거나 사용합니다. 만약 commonLogic을 각 함수에 직접 작성하는 방식으로 코드를 바꾼다면, 코드는 더 단순하고 각 함수에 맞춘 맞춤형 구현을 할 수 있습니다.

// 중복을 없애지 않는 방식
@Injectable()
export class Service {
  public async logicA(Id: number) {
    const resultA = `User ID is ${Id}`; // 공통적인 D 로직 그대로 사용
    console.log(resultA);
  }

  public async logicB(Id: number) {
    const resultB = `User ID is ${Id} is modified`; // 변형된 D 로직
    console.log(resultB);
  }

  public async logicC(Id: number) {
    const resultC = `ID_User_${Id}`; // 완전히 다른 형식의 로직 사용
    console.log(resultC);
  }
}

 

이처럼, 단순히 중복된다고 해서 모든 코드를 함수로 분리하는 것이 꼭 좋은 방법은 아닙니다. 특히 각 함수가 고유한 맥락에서 로직을 처리하고, 중복된 코드가 다르게 변형될 가능성이 있다면, 그대로 두는 것이 더 나을 수 있습니다.

 

만약 문자열을 반환하는 것이 아닌, 사소하게 다른 if문, for문.. 등등 다른 로직으로 생각해 보아도 그렇습니다.

따라서, 누구라도 쓸 만한 함수는 유틸 함수로 분리하고, 그렇지 않은 함수는 분리를 고려합니다.

 

마무리

스터디원들과 함께 이야기하며 느낀 것은, 결국 실무와 이상의 차이가 크다는 것입니다. 우리는 클린 코드의 중요성도 알고, 잘 작성하고 싶어 하지만 여러가지 요인에 의해서 결국 지켜지지 않을 때가 많습니다. 또 무조건 적으로 책을 맹신하며 작성하는 것 또한 옳지 않은 방법이라고 생각합니다. 팀바팀의 그라운드 룰, 그리고 책에서 말하는 것 중 필요한 부분을 내 습관으로 들이는 것. 이 부분이 가장 중요하다고 생각합니다.

 

오늘 작성한 글의 내용은 스터디를 통해 나온 다양한 의견들, 스터디를 진행 이후 내용들에 대해서 정리하고 스스로 정의를 세우려고 해보았습니다. 그러니 많은 댓글을 통해 의견 주시면 감사하겠습니다.

 

또, 스터디 마지막에는 우리가 이야기 해볼만한 다양한 질문이 나왔는데요, 비록 시간이 없어 진행되지는 않았지만 내용이 흥미로워 공유합니다.

 

- 현업에서 내가 아닌 남들의 클린 코드, 어떻게 해결해야할까?

- GPT에게 부탁하는 변수명 괜찮을까?

- 예외처리에 대한 클린코드

- 기능이 커질수록 비슷한 생기는 비슷한 이름 짓기, 어떻게 해결할까?

 

그럼 댓글로 다양한 의견 부탁드립니다. 감사합니다.


스터디 내용 중 메모한 내용

- 로그는 모두 로깅한다고 하지만, 일단 내가 중요하다고 생각하는 핵심 기능 위주로 혹은 궁금한 부분 위주로 진행한다.

- 구체적인 변수 명은 주석의 역할도 한다. 그러니 대충 짓지 말자.

 

profile

SMAIVNN

@SMAIVNN

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!