클린 아키텍쳐에 대하여 깊게 고민하고 생각해보며 공부하였습니다.
이해하기에는 쉽지 않았으나, 공부한 내용을 글로 작성합니다.
글의 내용이 길다고 생각하여 마지막 부분에 쉬운 이해를 위해  내용을 간단하게 정리하였습니다.

 

클린 아키텍쳐란 무엇인가?

소프트웨어 프로젝트를 진행하다 보면 프로젝트의 크기가 커져 코드가 점점 복잡해집니다. 처음에는 깔끔했던 코드가 기능이 추가될수록 서로 얽혀 나중에는 작은 기능 하나를 수정해도 예상치 못한 곳에서 버그가 터져 나올 수 있습니다. 이런 상태를 '소프트웨어가 부패했다' 또는 '아키텍처가 무너졌다'고 표현합니다.

클린 아키텍처는 이러한 문제에 대한 해결책으로 로버트 C. 마틴(Uncle Bob)이 제안한 설계 사상입니다. 아래의 내용은 클린 아키텍쳐의 핵심 철학입니다.

 "좋은 아키텍처는 시스템을 유연하고, 테스트하기 쉽고, 유지보수하기 좋게 만든다. 이를 위해 시스템의 핵심 비즈니스 로직과 외부 기술(세부 사항)을 철저히 분리해야 한다."

여기서 '세부 사항(Details)'이란 프레임워크(Next.js), 데이터베이스(PostgreSQL), UI, 외부 라이브러리 등을 의미합니다. 이것들은 언젠가 바뀔 수 있는 부차적인 요소입니다. 반면, '핵심 비즈니스 로직'은 이 서비스가 존재하는 이유(서비스의 규칙)이며 쉽게 변하지 않습니다.

클린 아키텍처는 이 변하지 않는 핵심을 변하기 쉬운 세부 사항들로부터 보호하는 '방어벽'을 만드는 설계 방법론입니다.


클린 아키텍쳐의 시스템

클린 아키텍처는 시스템을 크게 네 개의 계층으로 나누어 설명합니다. 가장 안쪽부터 바깥쪽으로 이동합니다. 

 

아래는 각 계층의 역할과 특징입니다.

Entities (엔티티)

  • 역할 : 시스템의 가장 핵심적인 비즈니스 규칙을 담는 영역입니다. 이 규칙은 특정 애플리케이션에 종속되지 않습니다.

  • 예시 : 쇼핑몰 시스템의 Product(상품)나 Order(주문) 객체. 이 객체들은 상품의 가격 계산 로직이나 주문의 상태 변경 규칙 같은 고유한 비즈니스 로직을 포함할 수 있습니다.

  • 특징
    엔티티는 변하지 않고 안정적입니다. 외부의 어떤 계층이 바뀌더라도 엔티티는 영향을 받지 않습니다. 예를 들어, 애플리케이션이 웹에서 모바일로 바뀌거나 데이터베이스 종류가 바뀌어도 엔티티는 그대로 유지되어야 합니다.
// 예시 코드

// /src/core/entities/reservation.entity.ts
export class Reservation {
  public readonly rsv_id: string;
  public rsv_user: string;
  public rsv_room: string;
  public rsv_start: Date;
  public rsv_end: Date;

  constructor(props: Omit<Reservation, 'isWithinBusinessHours'>) {
    Object.assign(this, props);
  }

  // 엔티티 스스로가 가질 수 있는 핵심 비즈니스 규칙
  public isWithinBusinessHours(): boolean {
    const startHour = this.rsv_start.getHours();
    const endHour = this.rsv_end.getHours();
    return startHour >= 9 && endHour <= 18;
  }
}

 Use Cases (유즈케이스)

  • 역할 : 애플리케이션에 특화된 비즈니스 규칙을 구현합니다. 이 계층은 엔티티를 사용하여 시스템이 사용자에게 제공하는 특정 기능(유스케이스)을 완성시킵니다.

  • 예시: RegisterUserUseCase는 사용자 등록에 필요한 모든 단계를 조율하지만, 실제 데이터베이스에 사용자 정보를 저장하는 코드는 직접 작성하지 않고, 저장소 인터페이스를 호출할 뿐입니다.

  • 특징
    "사용자를 등록한다", "상품을 장바구니에 담는다"와 같은 구체적인 기능를 코드로 표현합니다.
    이 계층은 데이터가 어떻게, 어디에 저장되는지 전혀 알지 못합니다. 대신 "사용자를 등록해줘" 또는 "상품 데이터를 가져와줘"라는 추상적인 규칙(인터페이스)에만 의존합니다.
// 예시 코드
// /src/core/repositories/reservation.repository.ts (인터페이스 정의)
export interface IReservationRepository {
  save(reservation: Reservation): Promise<void>;
  findById(id: string): Promise<Reservation | null>;
}

// /src/core/use-cases/create-reservation.use-case.ts
import { Reservation } from '../entities/reservation.entity';
import { IReservationRepository } from '../repositories/reservation.repository';

export class CreateReservationUseCase {
  // 실제 구현이 아닌, 인터페이스에 의존함
  constructor(private reservationRepo: IReservationRepository) {}

  async execute(input: { userId: string, roomId: string, start: Date, end: Date }) {
    const newReservation = new Reservation({ ... });

    if (!newReservation.isWithinBusinessHours()) {
      throw new Error("예약은 업무 시간 내에만 가능합니다.");
    }
    // ... 다른 비즈니스 규칙 검사

    await this.reservationRepo.save(newReservation);
  }
}

Interface Adapters (인터페이스 어댑터)

  • 역할 : 안쪽의 유스케이스/엔티티 계층과 바깥쪽의 프레임워크/드라이버 계층 사이에서 데이터를 변환하고 전달하는 '번역가' 역할을 합니다.

  • 예시
    Controller : 웹 요청(HTTP Request)을 받아 유스케이스가 이해할 수 있는 데이터 형식으로 변환한 뒤, 해당 유스케이스를 호출합니다.
    Repository 구현체 : 유스케이스 계층에 정의된 "데이터 저장 규칙(인터페이스)"을 실제 데이터베이스 기술(예: SQL)을 사용하여 구현합니다.

  • 특징
    이 계층의 코드는 안쪽의 핵심 로직과 바깥쪽의 외부 기술 모두를 알고 있습니다.
    Controllers, Presenters, Gateways(Repositories) 등이 여기에 속합니다.
// 예시 코드
// /src/infra/repositories/supabase-reservation.repository.ts
import { IReservationRepository } from '@/core/repositories/reservation.repository';
import { Reservation } from '@/core/entities/reservation.entity';
import { supabase } from '@/infra/db/supabase'; // Supabase 클라이언트

export class SupabaseReservationRepository implements IReservationRepository {
  async save(reservation: Reservation): Promise<void> {
    const { error } = await supabase.from('reservations').insert([
      { ...reservation } // 엔티티를 DB 스키마에 맞게 변환
    ]);
    if (error) throw new Error(error.message);
  }
  //... findById 등 구현
}

Frameworks & Drivers (프레임워크 & 드라이버)

  • 역할 : 시스템의 가장 바깥쪽 경계로, 우리가 사용하는 모든 외부 도구와 기술이 이 계층에 해당합니다.

  • 특징
    웹 프레임워크, 데이터베이스, UI 프레임워크, 외부 라이브러리 등 구체적인 기술들입니다.
    이 계층의 코드는 주로 다른 계층들을 연결하고 초기화하는 '접착제' 역할을 합니다.

  • 예시 : Next.js 프레임워크, PostgreSQL 데이터베이스, React UI 라이브러리, 웹 서버 등.

위와 같은 구조로 가장 중요한 엔티티유스케이스는 가장 불안정한 프레임워크데이터베이스 기술로부터 완벽하게 보호받게 됩니다.

중요한 것 (핵심 비즈니스 로직)
이 서비스가 존재하는 이유 그 자체입니다. 예를 들어, 프로젝트에서는 "회의실을 특정 규칙에 따라 예약한다"는 것이 핵심 로직입니다. 이 로직은 웹사이트로 만들든, 모바일 앱으로 만들든 변하지 않습니다.

중요하지 않은 것 (세부 사항)
 핵심 로직을 구현하기 위해 사용하는 외부 도구들입니다.

  • 웹 프레임워크: Next.js, Express 등
  • 데이터베이스: PostgreSQL, MySQL, Supabase 등
  • UI: React, Angular 등

 


의존성 규칙 (The Dependency Rule)

클린 아키텍쳐를 이해할 때, 가장 중요한 규칙입니다.

"소스 코드의 의존성(참조 관계)은 오직 안쪽을 향해야 한다."


시스템을 여러 개의 구조로 생각했을 때, 안쪽 원은 바깥쪽 원에 대해 아무것도 모릅니다.

  • 안쪽 원 (핵심 로직): 데이터베이스가 무엇인지, 웹 프레임워크가 무엇인지 알 필요가 없습니다.
  • 바깥쪽 원 (세부 사항): 안쪽 원의 규칙을 알고 있으며, 그 규칙에 따라 움직입니다.

이 규칙으로 인해 데이터베이스를 바꾸거나 웹 프레임워크를 교체하는 큰 변화에도 핵심 비즈니스 로직 코드는 단 한 줄도 수정할 필요가 없게 됩니다. 이것이 클린 아키텍처가 추구하는 유연성입니다.


클린 아키텍쳐를 사용하는 이유

  • 유연성 : 데이터베이스나 웹 프레임워크를 바꿔야 할 때, 핵심 비즈니스 로직은 건드리지 않고 해당 부분만 교체하면 됩니다.
  • 테스트 용이성 : 핵심 로직을 외부 기술과 분리하여 독립적으로 쉽게 테스트할 수 있습니다.
  • 유지보수성 : 코드가 역할별로 명확히 분리되어 있어 이해하고 수정하기 쉽습니다.

정리

 

클린 아키텍쳐를 공부하며 많은 시간을 쓰고, 깊은 고민을 했습니다. 이후, 이해하게 된 내용은 상당히 간단하게 요약할 수 있었습니다.

시스템의 중요한 부분은 (핵심 비즈니스 로직)과 중요하지 않은 부분(외부 기술, 세부사항)을 분리하는 것입니다.

시스템은 안쪽으로 갈수록 중요해지며 안쪽은 Entity, Use Case 등이 위치하며 바깥쪽에는 Framework, Data Base 등이 있습니다.

이 계층의 사이에는 내부의 핵심 로직과 외부 세계(웹, DB 등) 사이에서 데이터를 변환하고 전달하는 중간 계층인 Adapter가 존재합니다.

 

Entity는 비즈니스 규칙과 데이터 구조 등이 위치합니다.

Use Case는 애플리케이션 고유의 로직이며 엔티티를 조율하여 실제 기능을 완성합니다.
Adapter는 Controller, Repository 구현체 등이 속합니다.
Frameworks & Drivers는 웹 프레임워크(Next.js), 데이터베이스(PostgreSQL), UI 등 우리가 사용하는 모든 외부 기술입니다. 이들은 '세부사항'이며, 언제든 교체될 수 있습니다.

 

 

 

출처 : https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

728x90

'CS' 카테고리의 다른 글

세션(Session)이란?  (1) 2025.08.03
Prisma ORM에 대하여  (0) 2025.08.02
객체 지향이란?  (0) 2025.06.29
데이터 정규화(Normalization)란?  (0) 2025.06.26
동기화와 비동기화  (0) 2025.06.20

채용 사이트 '로켓펀치'에서 지원 내역을 확인 중, 신경 쓰이는 UI 문제를 발견했습니다.

작성하는 글은 문제를 분석하고 해결하는 방법에 관한 내용이며, 문제를 분석하고 단순히 눈에 보이는 현상을 넘어 더 구조적인 해결책을 고민하고 적용한 글 입니다.


문제 : 스크롤과 함께 사라지는 핵심 기능들

로켓펀치의 '지원 내역' 모달은 지원 현황을 한눈에 볼 수 있어 편리하였으나, 지원 내역이 많아지면 아래와 같은 문제가 발생합니다.

개인정보를 위해 지원 내용은 삭제하였습니다.

모달이 상단의 필터 버튼과 하단의 메뉴 버튼이 화면 밖으로 사라집니다.


위와 같은 상황은 사용자 관점에서 강력한 불편함으로 이어집니다.

  • 모달 위쪽의 기능을 확인할 수 없으며, 모달을 닫을 수 없습니다.
  • 아래의 버튼은 가려져서 알아보기 어렵습니다.

문제 해결을 위해 개발자 도구로 styles의 css 부분 하나하나 확인하였고, 다음과 같은 문제를 찾았습니다.

이 외에도 모달을 감싸는 가장 바깥쪽 배경(Overlay) 요소에 height: 100% 스타일이 적용되어 있었습니다. 이는 모달의 높이를 현재 보이는 브라우저 화면 높이에 완전히 고정시킵니다. 그러나, 모달 내부에 표시될 '지원 내역' 콘텐츠의 양이 많아지면서, 콘텐츠의 실제 높이가 고정된 100%를 초과하게 되었습니다.

이후, 내용물이 틀을 넘쳤을 때 어떻게 처리할지에 대한 overflow 속성이 정의되어 있지 않았습니다. overflow의 기본값은 visible이므로 콘텐츠는 틀 밖으로 빠져나가 화면에 잘립니다.


해결 방법

간단한 해결책은 모달 자체의 높이 제한 입니다. 개발자 도구를 열어 모달의 최상위 컨테이너에 height 속성을 적용 했습니다.

화면 높이를 90%로 제한

 

    height: 90vh; // 화면 높이의 90%로 제한

이 한 줄의 입력으로 모달이 화면 밖으로 벗어나는 문제는 해결 가능합니다.


추가 설명 : overflow 속성

height: 90vh로 모달 컨테이너의 크기를 강제로 제한하면 그보다 내용물이 많은 모달 콘텐츠는 overflow의 기본값인 visible에 따라 컨테이너 밖으로 넘치게 됩니다.

이 콘텐츠가 흘러넘치게 되면 모달이 아닌 페이지 전체의 높이가 길어지는 문제가 생길 수 있습니다. 따라서, 이 방법은 모달에 스크롤을 만든 것이 아닌 페이지 전체에 스크롤이 생기도록 만든 것 입니다.

이러한 경우, 배경 스크롤이라는 또 다른 UX 문제가 생깁니다. 모달 위에서 스크롤할 때 그 뒤의 페이지 본문까지 함께 움직이는 현상은 사용자에게 큰 불편을 줄 수 있습니다. 따라서,  눈앞의 문제만 고치는 것이 아닌 명확하게 제어되는 UI를 만들어야 합니다.


근본적인 해결 : Flexbox를 이용한 레이아웃 재설계

가장 이상적인 UI는 "헤더와 푸터는 고정, 콘텐츠 영역만 스크롤" 되는 것입니다. 이를 위해 CSS Flexbox를 도입하여 모달의 구조를 재설계 해야합니다.

HTML 구조 개선

평평한 모달 내부 구조를 header, main, footer의 의미 있는 구조로 개선해야 하며, 견고한 HTML은 안정적인 CSS에 있어서 중요합니다.

// 기존 내용
<div class="modal-box">
  <button>헤더 버튼</button>
  <div>콘텐츠 카드</div>
  <button>푸터 버튼</button>
</div>

// Header, Body, Footer 분리
<div class="modal-box">
  <header class="modal-header">
    <button>헤더 버튼</button>
  </header>
  <main class="modal-content">
    <div>콘텐츠 카드</div>
  </main>
  <footer class="modal-footer">
    <button>푸터 버튼</button>
  </footer>
</div>

CSS Flexbox 적용

개선된 HTML 구조에 각 영역의 역할을 명확히 정의하는 Flexbox 스타일을 적용해야 합니다.

// 모달 전체를 감싸는 Flex 컨테이너
.modal-box {
  display: flex;
  flex-direction: column; // 아이템을 세로로 배치
  height: 90vh; // 전체 높이 제한
  overflow: hidden; // 자식 요소의 둥근 모서리 유지를 위함
}

// 고정될 헤더와 푸터
.modal-header,
.modal-footer {
  flex-shrink: 0; /* 공간이 부족해도 절대 줄어들지 않도록 고정 */
}

// 스크롤될 콘텐츠 영역
.modal-content {
  overflow-y: auto;   // 내용이 넘칠 경우, 해당 영역만 스크롤
}
  • .modal-box : flex 컨테이너로 되어 내부 아이템들의 배치 기준을 세웁니다.
  • .modal-header, .modal-footer : flex-shrink: 0을 이용해 자신의 크기를 유지하며 뷰포트 내에 고정됩니다.
  • .modal-content : overflow-y: auto를 이용하여 내용이 많을 때 내부에서 흘러넘치지 않게 합니다.

결과

버그가 고쳐진 모달

헤더와 푸터는 화면에 고정되어 중앙의 콘텐츠 목록만 부드럽게 스크롤됩니다.

사용자는 긴 지원 내역을 스크롤 하면서도 언제든지 상단의 필터와 하단의 메뉴 버튼에 즉시 접근할 수 있습니다. 따라서, 사용자 경험이 향상하는 UI가 됩니다.


정리

문제를 해결하며 느낀 점을 적어보았습니다.


눈앞에만 보이는 것을 넘어서 그 더 깊게 원리를 파고들면 견고한 코드를 만들 수 있습니다.
논리적이고 의미 있는 HTML 구조는 CSS를 단순하고 예측 가능하게 만드는 가장 확실한 방법입니다.
개발자의 목표는 단순히 버그를 수정하는 것이 아니라, 더 나은 사용자 경험을 만드는 것입니다. '작동한다'에서 멈추지 않고 '어떻게 하면 더 편리할까?'를 깊게 고민하는 것이 문제 해결에 중요하다는 것을 느꼈습니다.

작은 불편함에서 시작했으나 문제를 넘기지 않고 깊게 파고들어 더 나은 해결책을 찾아가는 과정이 좋은 경험이 되었습니다. 이러한 경험이 사용자의 입장에서 생각하고 문제를 해결하는 것에 큰 귀감이 되었습니다.

728x90

'Study' 카테고리의 다른 글

검색 엔진 최적화(SEO)란?  (0) 2025.07.06
웹 개발과 개발 용어에 대한 이해(2)  (0) 2024.10.17
웹 개발과 개발 용어에 대한 이해(1)  (1) 2024.09.05

프로젝트 리팩토링


1. 백엔드 아키텍처 재정비: API와 비즈니스 로직의 분리

프로젝트의 규모가 커지면 코드의 위치와 역할에 대한 고민을 해야합니다.
Next.js의
api 폴더에 너무 많은 책임이 있었습니다.

문제점

  • 핵심 비즈니스 로직이 API 라우트 핸들러와 뒤섞여 있었습니다.
  • /api 폴더의 역할이 모호해지면서 네임스페이스 충돌이나 코드 파악에 어려움이 있었습니다.

개선 방향

핵심 백엔드 로직 분리 : 기존 /api 폴더에 있던 주요 백엔드 코드를 별도의 독립적인 백엔드 폴더(예: /src/backend 또는 /src/services)로 이동하기로 했습니다. 지금부터 /api 폴더는 원격 사용자와의 통신을 담당하는 '어댑터(Adaptor)' 계층의 역할만 수행합니다.

Next.js의 App Router API Route 핸들러만 남겨두고, 실제 데이터 처리 및 비즈니스 로직은 새로 만든 백엔드 폴더에서 처리하도록 구조를 변경할 예정입니다.

/api 폴더의 새 역할 : /api 폴더에는 이제 라우팅과 직접적으로 관련되거나, 업무 보조를 위한 간단한 스크립트 등 상대적으로 중요도가 낮은 코드만 남겨두기로 했습니다. 각 폴더의 책임과 역할이 명확해졌습니다.

결론 : /api는 클라이언트 요청만 받으며, /backend는 업무를 처리하는 것으로 역할을 분리하여 유지보수성과 확장성을 높였습니다.

 RESTful API 컨벤션 바로 세우기

API 경로명의 컨벤션이 일관되지 않아 개선해야 합니다. 

API의 엔드포인트는 자원(Resource)을 표현해야 합니다. 자원은 '물건'이나 '개념'이므로, '무엇을 한다'는 동사형이 아닌 '무엇'이라는 명사형으로 작성하는 것이 원칙입니다. 가장 쉬운 방법은 복수형 명사를 사용하는 것입니다.

예외적으로 단수형/동사형을 사용하는 경우
모든 경로가 복수형 명사일 필요는 없으며, 예외 규칙이 있습니다.

  1. 역할자(Actor) 기반 경로: admin, user와 같이 특정 역할자의 고유한 자원을 가리킬 때는 단수형을 사용할 수 있습니다. (예: /admin/dashboard, /user/profile)
  2. 구조를 위한 폴더명: 단순히 경로를 묶어주기 위한 폴더명은 자원이 아니므로 복수형으로 만들지 않습니다. (예: /api/v1/users에서 api, v1은 자원이 아닙니다.)
  3. 행위명 사용: POST, GET, PUT, DELETE와 같은 HTTP Method로 표현하기 어려운 특정 '행위'가 필요할 때 예외적으로 동사형을 사용합니다. (예: /posts/{id}/publish)

페이지 경로와의 구분
프론트엔드 페이지 경로와 API 엔드포인트는 명확히 구분해야 합니다.

  • 게시물 생성 페이지: .../posts/create (페이지)
  • 게시물 생성 API: POST /api/posts (API)
  • 게시물 수정 페이지: .../posts/{id}/edit (페이지)
  • 게시물 수정 API: PUT /api/posts/{id} (API)

    결론: 명확하고 일관된 API 컨벤션을 통해 팀원이 API의 역할을 쉽게 예측하고 사용할 수 있도록 개선해야 합니다.

DTO, 인증, 보안

데이터의 흐름과 사용자 인증 방식에 대해서 생각해야 합니다.

 

DTO (Data Transfer Object)
API는 용도에 따라 상세하게 나누어야 합니다. 예를 들어, 게시물 목록을 불러오는 API와 게시물 상세 정보를 불러오는 API는 서로 다른 정보를 필요로 합니다. 따라서 주가 되는 API(상세 정보)와 부가 되는 API(요약 정보)를 구분하고, 각 용도에 맞는 DTO를 설계하여 필요한 데이터만 효율적으로 주고받도록 개선할 예정입니다.

로그인 토큰 저장 및 관리
로그인 성공 후 발급되는 토큰의 저장과 관리방식에 대하여 생각해야 합니다.

클라이언트 저장소
 토큰은 전역 상태 관리 라이브러리(Zustand, Redux 등)나 로컬 스토리지에 저장할 수 있습니다.

서버 주도 쿠키 방식
ID/PW와 같은 민감 정보를 로컬 스토리지에 저장하는 것은 피해야 합니다.
더 안전한 방법은 서버에서 HttpOnly 옵션을 포함한 쿠키에 토큰을 담아 클라이언트로 보내는 것 입니다.
위와 같은 방법을 이용 하면 클라이언트의 JavaScript 코드가 쿠키에 직접 접근할 수 없으므로 토큰을 안전하게 보호할 수 있습니다. 클라이언트는 이 쿠키를 읽기만 가능하며, 수정은 불가능합니다.
Next.js 환경에서 서버 컴포넌트나 API Route가 다른 API 서버로 요청을 보낼 때, fetch 함수의 credentials: 'include' 옵션을 사용하면 브라우저의 쿠키를 함께 전송할 수 있습니다.

인증 및 접근 제어
사용자 인증 정보는 커스텀 훅(예: useUser)을 만들어 최상위 Layout 컴포넌트에 적용하면 좋습니다. 이를 통해 모든 하위 페이지와 컴포넌트에서 일관되게 사용자 로그인 상태를 확인하고, 역할에 따른 접근 권한을 효과적으로 제어할 수 있습니다.

 


정리

 프로젝트의 뼈대를 더 튼튼하게 만들기 위하여 고민해야 합니다.  좋은 아키텍처와 컨벤션이 당장의 개발 속도보다 장기적인 프로젝트의 건강과 확장성에 중요합니다. 

728x90

 

멘토님과 상담 후의 글을 작성합니다.

 

AI 시대에 맞춰 전략을 준비

 

"단순 구현 능력의 가치는 점차 하락할 것이다. 기업은 더 넓은 시야와 깊이 있는 문제 해결 능력을 가진 개발자를 원한다."

AI가 반복적인 코딩을 자동화하면서, 개발자의 역할은 다음과 같이 확장되고 있습니다.

  • 더 넓은 역할: 기획, 디자인(UX/UI), 데이터 분석 등 다른 직군과의 경계가 허물어지며, 제품 전체를 이해하고 기여하는 능력이 중요해졌습니다.
  • 더 높은 수준의 문제 해결: "어떻게 만들까?"를 넘어 " 이렇게 만들어야 하는가?", "이 기술이 우리 비즈니스 문제에 최적의 해결책인가?"를 답할 수 있어야 합니다.
  • 지식 공유와 커뮤니케이션: 내가 아는 것을 팀에 명확히 공유하고, 동료의 작업을 이해하며 함께 성장하는 협업 능력이 핵심 역량이 되었습니다.

AI는 우리의 대체재가 아닌 우리가 더 중요한 문제에 집중할 수 있도록 돕는 조수로 사용 가능합니다.

완성도와 기술적 깊이

1. 기술 경험 중심으로 차별화하기

단순히 어떤 기술을 '사용했다'에서 그치지 않고, 기술적 도전을 녹여내는 것이 중요합니다.

  • 권한 관리: 특정 사용자만 아이템을 수정하거나 볼 수 있는 역할 기반 권한 시스템 도입 (Supabase RLS 심화)
  • 실시간 협업 기능: 여러 사용자가 동시에 캔버스에 접속해 작업하는 모습을 실시간으로 공유 (Supabase Realtime, WebSockets)
  • 아키텍처 개선: 클린 아키텍처, DDD(도메인 주도 설계) 등을 적용하여 유지보수성과 확장성이 높은 코드 구조로 리팩토링하기
  • UX/UI 고도화: 드래그 시 스냅 기능, 되돌리기/다시 실행(Undo/Redo), 객체 그룹화 등 사용자 경험을 극적으로 개선하는 기능 추가

2. '진짜 실무'처럼 보이기

개인 프로젝트라도 협업의 흔적을 남겨 프로페셔널함을 어필할 수 있습니다.

  • GitHub 관리: 기능 개발 단위로 PR(Pull Request)을 작성하고, 스스로 리뷰하며 고민의 흔적을 남기기. 이슈(Issue)를 등록하여 해야 할 일을 체계적으로 관리하기.
  • 문서화: 프로젝트의 구조, 사용된 기술, 그리고 왜 그 기술을 선택했는지에 대한 이유를 README.md나 별도의 Docs에 상세히 기록하기.

기업은 당신의 'Why'와 'How'를 궁금해한다

면접관은 포트폴리오의 화려한 결과물보다 그 과정에 담긴 지원자의 생각의 깊이를 보고 싶어 합니다.

"interact.js 대신 왜 Konva.js를 선택했나요?"
"Supabase의 실시간 기능을 사용하면서 겪었던 가장 큰 기술적 어려움은 무엇이었고, 그것을 어떻게 해결했나요?"

이런 질문에 대비해야 합니다. 단순히 "Konva.js가 크기 조절 기능이 있어서요"라고 답하는 대신, "초기에는 DOM 기반의 interact.js를 고려했으나, 향후 이미지 필터 적용, 객체 그룹화 등 복합적인 그래픽 편집 기능의 확장성을 고려했을 때, Scene Graph 기반의 객체 모델을 제공하는 Konva.js가 더 적합하다고 판단했습니다."와 같이 기술적 트레이드오프(Trade-off)를 고민한 흔적을 보여주는 것이 중요합니다.

앞으로의 스터디 방향: 기록하고, 파고들고, 공유하기

  1. 경험 기록하기: 프로젝트를 진행하며 마주치는 모든 문제와 해결 과정을 그냥 넘기지 말고 기록으로 남기기. (Notion, 블로그, GitHub 이슈 등)
  2. 깊게 파고들기: "왜 이 코드는 이렇게 동작하지?"라는 의문이 생길 때, 라이브러리 공식 문서나 소스 코드를 들여다보는 습관 들이기.
  3. 공유하기: 내가 배운 것을 블로그나 GitHub에 공유하기. 공유는 지식을 내 것으로 만드는 최고의 방법이며, 나의 성장 과정을 보여주는 훌륭한 증거가 됩니다.

이번 멘토링은 기술적인 조언을 넘어, 개발자로서 어떻게 성장해야 할지에 대한 방향성을 재정립하는 계기가 되었습니다. AI 시대에서 살아남기 위해, 나만의 '대체 불가능성'을 만들어가야 합니다. 그러기 위해서 깊이 있는 기술적 이해, 논리적인 문제 해결 능력, 그리고 동료와 함께 성장하는 협업의 자세가 필요합니다.

728x90

 

Next.js 환경에서 클린 아키텍처를 어떻게 적용했는지, 그리고 그 과정에서 무엇을 얻었는지 작성하는 글입니다.

1. Presentation 계층: API Route 핸들러

      // app/api/spaces/route.ts

import { NextRequest, NextResponse } from 'next/server';
import { SbSpaceRepository } from '../../infrastructure/repositories/SbSpaceRepository';
import { GetSpaceUsecase } from '../application/usecases/GetSpaceUsecase';
import { createClient } from '../../infrastructure/supabase/server';
import { SbRoomRepository } from '../../infrastructure/repositories/SbRoomRepository';

export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url);
    const spaceId = Number(searchParams.get('spaceId'));

    // --- 의존성 주입 (Dependency Injection) ---
    const supabase = await createClient();
    const spaceRepository = new SbSpaceRepository(supabase);
    const roomRepository = new SbRoomRepository(supabase);
    const getSpaceUsecase = new GetSpaceUsecase(
      spaceRepository,
      roomRepository
    );
    // -----------------------------------------

    const spaces = await getSpaceUsecase.execute(spaceId);
    return NextResponse.json(spaces);

  } catch (error) {
    console.error('Error fetching spaces:', error);
    return NextResponse.json(
      { error: 'Failed to fetch spaces' },
      { status: 500 }
    );
  }
}
    

getSpaceUsecase가 어떻게 데이터를 가져오는지, DB가 Supabase인지 모릅니다. 오직 Usecase를 실행합니다.

2. Application 계층: Usecase

Usecase는 "공간 정보를 가져온다"는 애플리케이션의 특정 동작을 책임집니다. 어떤 Repository를 통해 데이터를 가져올지 알고 있지만, 그 Repository가 어떻게 구현되었는지는 모르며 오직 인터페이스에만 의존합니다.

      // application/usecases/GetSpaceUsecase.ts

import { SpaceRepository } from '@/app/api/domain/repository/SpaceRepository';
import { RoomRepository } from '@/app/api/domain/repository/RoomRepository';
import { GetSpaceDto } from '../dtos/GetSpaceDto';

export class GetSpaceUsecase {
  // 구현체(SbSpaceRepository)가 아닌 추상화(SpaceRepository)에 의존
  constructor(
    private spaceRepository: SpaceRepository,
    private roomRepository: RoomRepository
  ) {}

  async execute(id: number): Promise<GetSpaceDto> {
    // 1. Space 정보를 가져온다.
    const space = await this.spaceRepository.findById(id);
    // 2. Room 정보를 가져온다.
    const rooms = await this.roomRepository.findBySpaceId(id);

    // 3. 데이터를 DTO(Data Transfer Object)로 가공하여 반환한다.
    const roomInfos = rooms.map((room) => ({
        id: room.id,
        name: room.name,
        // ...
    }));
      
    return new GetSpaceDto(space.id, space.name, roomInfos);
  }
}
    

Usecase는 여러 비즈니스 로직을 수행하고, 결과를 외부로 전달할 DTO 형태로 가공합니다.

3. Infrastructure 계층: Repository 구현체

여기가 Supabase와 직접 통신하는부분입니다. Domain 계층에 정의된 SpaceRepository 인터페이스를 실제로 구현합니다.

      // infrastructure/repositories/SbSpaceRepository.ts

import { Space } from '../../domain/entities/Space';
import { SpaceRepository } from '../../domain/repository/SpaceRepository';
import { supabaseAdmin } from '../supabase/client';

export class SbSpaceRepository implements SpaceRepository { // 인터페이스 구현
  constructor(private supabase: SupabaseClient) {}

  async findById(id: number): Promise<Space> {
    const { data, error } = await this.supabase
      .from('space')
      .select('*') // JOIN을 사용한다면 여기에 복잡한 쿼리가 들어갈 수 있음
      .eq('id', id)
      .single();

    if (error) {
      throw new Error(`공간 조회 중 오류 발생: ${error.message}`);
    }

    return data;
  }
}
    

나중에 데이터베이스를 Supabase에서 다른 것으로 바꾸고 싶을 때, 이 파일과 createClient 부분만 수정하면 됩니다. Usecase나 API 핸들러는 건드릴 필요가 없습니다.

 

클린 아키텍처는 모든 프로젝트에 적용해야 하는 것은 아니라고 생각했으며 작은 규모의 프로젝트에서는 오버 엔지니어링이 될 수 있습니다.  그러나, 프로젝트가 복잡해질 가능성이 있는 경우에는 초기에 이 구조를 도입하는 것이 장기적으로 생산성과 안정성을 얻을 수 있습니다.

단순히 코드를 작성하는 것이 아닌 견고하고 유연한 소프트웨어를 설계하는 경험으로 한 단계 더 성장하는 것을 느꼈습니다.

 

728x90