염산하

@[email protected] · 22 following · 15 followers

A software engineer in Seoul, and a father of a kid.

염산하

@[email protected] · Reply to 염산하's post

@purengom 여하간 사람들이 카톡을 어떻게 쓰는지 카톡만 몰랐던 것인가... 싶고요

염산하

@[email protected] · Reply to 푸른곰's post

@purengom 카톡은 AI 때문이라기엔... ㅎㅎ

푸른곰's avatar
푸른곰

@[email protected]

맥북 에어에서 제일 마음 드는 포트를 하나 꼽으라고 하면 3.5mm 헤드폰 포트 라고 대답할까봐요. 정말 괜찮네요.

염산하

@[email protected] · Reply to Woojin Kim's post

@me 사람이 어느 정도 간단히 상상할 수 있는 것들의 관계에서는 뉴턴 방식으로 생각하는 게 사실 더 직관적이긴 하죠 사과가 길을 따라가다니 뭔 소린가 싶고 ㅎㅎㅎ

염산하

@[email protected]

궤도가 이동진의 파이아키아 채널에 나와서 인터스텔라 영화에 대해 이야기하는 영상을 봤다. 거기서

광자는 질량이 없는 것으로 여겨진다. 그런데 블랙홀은 빛도 탈출할 수 없는 곳이다. 뉴턴의 법칙에 따르면 광자가 질량이 없다면 중력과 상호작용하지 않을 것이다. 그래서 블랙홀이 있다고 해도 빛에 영향을 주지 않을 것이다. 중력이 시공간의 형태에 의해서 나타나는 현상이라고 해야 블랙홀로 빛이 끌려가는 현상이 설명 가능하다.

라는 취지로 설명하는 것을 들었는데, 이런 식의 설명은 처음 들어봤다. 오호...

염산하

@[email protected] · Reply to Woojin Kim's post

@me 저도 이번에 깜짝 놀랐지 뭡니까...

염산하

@[email protected]

종립님이 인상적이라고 하신 리스크 테이커를 읽기 시작했다. 이 사람이 네이트 실버구나...

염산하

@[email protected] · Reply to 염산하's post

공부는 스스로, 하고 싶은 것을 배우고 익히는 것. 지식보다 사고력. 대학교육 자체는 통계적으로 유의미한 사고력 증진을 끌어내지 못했다.

염산하

@[email protected] · Reply to 염산하's post

공부의 재발견. 최근의 학습과학의 성과를 알리고 교육이 아닌 “공부”에 집중하고자 함, 이라고 서문에서 이야기하는 책. 마인드스케일 좋아하는데 요 책도 그래서 좋았다. 흠? 혹시 이 교수님도 유튜브 하시려나?

염산하

@[email protected] · Reply to Woojin Kim's post

@me 저 그거 알아요 F1 피트 스탑!

염산하

@[email protected] · Reply to Woojin Kim's post

@me 둘 다를 골라야 합니다 저는.

염산하

@[email protected]

아니 사람들이 이렇게나 카톡 프로필에 사진을 많이 올렸었나? 인스타랑 다를 바가 없었네...

염산하

@[email protected] · Reply to 염산하's post

연구 결과 학습 방법과 무관하게 질문을 목표로 삼은 집단이 더 많은 수의 좋은 질문을 만들어 냈습니다. 더 놀라운 점은 질문 집단이 이해 집단(내용 이해를 목표로 삼은 집단)보다 학습 내용을 더 잘 이해했다는 겁니다. 책을 읽거나 강의를 들을 때 질문하는 행위 자체에 초점을 두어 보세요. 그 과정에서 이해는 절로 따라올 겁니다.

염산하

@[email protected] · Reply to 염산하's post

글의 내용에 동의하는지, 글의 느낌은 어떤지, 글의 장점과 단점은 무엇인지 분석하는 것도 반성적 읽기의 일환입니다. 이를 통해 우리는 글을 다각적으로 이해하거나 겉으로는 드러나지 않던 새로운 의미를 찾아낼 수 있습니다. 반성적 읽기 능력은 배경지식을 많이 가지고 있다고 해서 저절로 늘지 않습니다. 글의 내용을 평가하고, 비판하고, 발전시키려는 의도적인 노력을 기울여야 조금씩 발전합니다.

염산하

@[email protected]

오, 애플 워치 시리즈 5 배터리 성능 수준 68% 라서 리퍼 받았는데 시리즈 6로 업글돼서 왔다! 두 세대 전 OS 쓰고 있었는데 최신 oS 설치가 되네!

Jaeyeol Lee's avatar
Jaeyeol Lee

@[email protected]

Disclaimer

이 글은 NestJS를 공부하면서 객체지향 프로그래밍 원칙과의 연결점을 스스로 정리한 내용입니다. Spring 같은 프레임워크를 이해할 때 객체지향 개념이 중요한 것처럼, NestJS 역시 객체지향 설계를 염두에 두면 훨씬 더 깊이 이해할 수 있을 것 같다는 관점에서 작성되었습니다.

소프트웨어는 시간이 지날수록 점점 복잡해지고, 원래 의도와 다르게 무너질 위험에 쉽게 노출됩니다. “한 클래스가 너무 많은 일을 한다”, “새로운 기능 하나 추가하려는데 기존 코드를 몽땅 뜯어고쳐야 한다”, “교체 가능한 구현체인데도 특정 코드에 딱 달라붙어버렸다”… 이런 상황을 겪어본 개발자는 많을 겁니다.

이러한 문제를 피하기 위해 정리된 다섯 가지 핵심 규칙이 바로 SOLID 원칙입니다. 이 개념은 소프트웨어 엔지니어 Robert C. Martin(일명 Uncle Bob) 이 다섯 가지 원칙을 하나의 묶음으로 제시하면서 널리 알려졌습니다. 이후로 사실상 “좋은 코드”를 판단하는 표준처럼 자리 잡았습니다.

하지만 SOLID는 객체지향 프로그래밍 전용 규칙이 아닙니다. 함수형 프로그래밍에도 적용할 수 있고, React 같은 UI 라이브러리, 오픈소스 프레임워크 내부 구조를 설계할 때도 그대로 통하는 일반적인 소프트웨어 설계 원칙 입니다. 이번 글에서는 NestJS를 예시로 삼아, 각 원칙이 어떤 문제를 해결하고 실제 코드에 어떻게 녹여낼 수 있는지 살펴보겠습니다.


SRP – 단일 책임 원칙

SRP는 “한 클래스는 오직 하나의 책임만 가져야 한다” 는 원칙으로, 여러 기능이 한곳에 얽히면 서로 다른 이유로 동시에 수정되어야 하므로 유지보수가 어려워집니다. NestJS가 Controller, Service, Repository를 나누는 구조를 제공하는 것도 사실 이 원칙을 실현하기 위함입니다.

❌ 위반 사례

@Controller('users')
export class UserController {
  constructor(private readonly repo: Repository<User>) {}

  @Get(':id')
  async getUser(@Param('id') id: string) {
    const user = await this.repo.findOneBy({ id });
    if (!user?.isActive) throw new Error('Inactive user');
    return { id: user.id, name: user.name };
  }
}

✅ 개선 사례

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get(':id')
  getUser(@Param('id') id: string) {
    return this.userService.findOne(id);
  }
}

@Injectable()
export class UserService {
  constructor(private readonly repo: Repository<User>) {}

  async findOne(id: string) {
    const user = await this.repo.findOneBy({ id });
    if (!user?.isActive) throw new Error('Inactive user');
    return user;
  }
}

개선 이유

Controller는 요청과 응답만 담당하고, Service는 비즈니스 로직만 다루며, Repository는 데이터베이스 접근에만 집중하게 나누면 각 계층이 독립적으로 바뀔 수 있고 코드의 응집도와 유지보수성이 높아집니다.

📊 다이어그램

G cluster_after After cluster_before Before R1 Request C1 Controller: 요청/응답 R1->C1 S1 Service: 비즈니스 로직 C1->S1 Res1 Response C1->Res1 S1->C1 Repo1 Repository S1->Repo1 Repo1->S1 DB1 Database Repo1->DB1 DB1->Repo1 R Request C UserController: 요청+로직+DB R->C DB Database C->DB Res Response C->Res DB->C

OCP – 개방-폐쇄 원칙

OCP는 “소프트웨어 개체는 확장에는 열려 있고, 변경에는 닫혀 있어야 한다” 는 원칙입니다. 즉, 새로운 기능을 넣더라도 기존 코드를 직접 수정하지 않고 확장 방식으로 처리할 수 있어야 한다는 뜻입니다.

❌ 위반 사례

@Injectable()
export class UserService {
  async findOne(id: string) {
    console.log(`[LOG] fetching user ${id}`);
    return { id };
  }
}

✅ 개선 사례

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    const req = context.switchToHttp().getRequest();
    console.log(`[LOG] ${req.method} ${req.url}`);
    return next.handle();
  }
}

@Module({
  providers: [{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }],
})
export class AppModule {}

개선 이유

UserService는 오직 사용자 로직만 담당하도록 두고, 로깅은 Interceptor로 분리하면 로깅 전략을 바꾸거나 새로운 로깅 방식을 추가할 때 기존 코드를 건드리지 않고 확장만으로 대응할 수 있습니다.

📊 다이어그램

G cluster_after After cluster_before Before R1 Request I1 LoggingInterceptor R1->I1 C1 Controller I1->C1 S1 Service: 로직만 C1->S1 I2 LoggingInterceptor C1->I2 S1->C1 DB1 Database S1->DB1 DB1->S1 Res1 Response I2->Res1 R Request C Controller R->C S Service: 로직 + 로깅 C->S Res Response C->Res S->C DB Database S->DB DB->S

LSP – 리스코프 치환 원칙

LSP는 “상위 타입을 사용하는 코드는 하위 타입으로 교체하더라도 정상적으로 동작해야 한다” 는 원칙입니다. MIT의 Barbara Liskov 교수가 1987년 발표한 개념으로, 인터페이스를 구현한 객체라면 언제든 안정적으로 대체 가능해야 한다는 점을 강조합니다.

❌ 위반 사례

export class StripeGateway {
  pay(amount: number): string {
    return `Paid ${amount} via Stripe`;
  }
}

export class TossGateway extends StripeGateway {
  pay(amount: number): string {
    throw new Error('Toss unavailable ❌');
  }
}

✅ 개선 사례

export interface PaymentGateway {
  pay(amount: number): string;
}

export class StripeGateway implements PaymentGateway {
  pay(amount: number) {
    return `Paid ${amount} via Stripe`;
  }
}

export class TossGateway implements PaymentGateway {
  pay(amount: number) {
    return `Paid ${amount} via Toss`;
  }
}

개선 이유

PaymentService는 인터페이스인 PaymentGateway에만 의존하고, 실제 구현체는 계약(pay 메서드)만 지키면 언제든 교체 가능하므로, 코드의 일관성과 신뢰성이 보장됩니다.

📊 다이어그램

G cluster_after After cluster_before Before R1 Request C1 Controller R1->C1 S1 PaymentService C1->S1 Res1 Response C1->Res1 S1->C1 G PaymentGateway 인터페이스 S1->G G->S1 Stripe1 StripeGateway G->Stripe1 Toss1 TossGateway G->Toss1 ExtAPI3 Stripe API Stripe1->ExtAPI3 ExtAPI4 Toss API Toss1->ExtAPI4 R Request C Controller R->C S PaymentService C->S Res Response C->Res S->C Stripe StripeGateway ✅ S->Stripe Toss TossGateway ❌: 계약 위반 S->Toss ExtAPI1 Stripe API Stripe->ExtAPI1 Toss->S 에러

ISP – 인터페이스 분리 원칙

ISP는 “클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다” 는 원칙으로, 거대한 인터페이스가 불필요한 의존을 강제하는 문제를 막고자 합니다. NestJS에서는 Guard, Pipe, Interceptor, Filter 등이 이 원칙을 잘 반영합니다.

❌ 위반 사례

class UglyRequestHandler {
  handle(req: any): any {
    try {
      this.authenticate(req);
      this.validate(req);
      this.log(req);
      const result = this.execute(req);
      return { statusCode: 201, message: '회원가입 성공', data: result };
    } catch (err: any) {
      return this.catchError(err);
    }
  }

  private authenticate(req: any) { /* ... */ }
  private validate(req: any) { /* ... */ }
  private log(req: any) { /* ... */ }
  private execute(req: any) { /* ... */ }
  private catchError(err: any) { /* ... */ }
}

✅ 개선 사례

@Controller('auth')
@UseFilters(HttpExceptionFilter)
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @UseGuards(GuestOnlyGuard)
  @UsePipes(new ValidationPipe())
  @UseInterceptors(LoggingInterceptor)
  @Post('signup')
  async signup(@Body() dto: CreateUserDto) {
    return this.authService.signup(dto);
  }
}

개선 이유

인증은 Guard, 검증은 Pipe, 로깅은 Interceptor, 에러 처리는 Filter가 담당하도록 분리하면 각 기능을 독립적으로 교체하거나 확장할 수 있고, 컨트롤러는 핵심 흐름만 관리할 수 있어 불필요한 의존이 사라집니다.

📊 다이어그램

G cluster_after After cluster_handler Handler (구성요소) cluster_before Before cluster_ugly UglyRequestHandler R1 Request M Middleware R1->M Res1 Response DB1 Database Repo Repository DB1->Repo G Guard 인증/인가 M->G I1 Interceptor (Before) G->I1 P Pipe 검증 I1->P C Controller P->C S Service C->S I2 Interceptor (After) C->I2 S->C S->Repo Repo->DB1 Repo->S F Exception Filter I2->F F->Res1 R Request U 인증 + 검증 + 로깅 + 실행 + 에러 R->U Res Response DB Database DB->U U->Res U->DB

DIP – 의존 역전 원칙

DIP는 “고수준 모듈은 저수준 모듈에 의존하지 않고, 추상화에 의존해야 한다” 는 원칙입니다. 구체 구현체에 묶여버리면 교체가 어려워지고, 시스템 전체가 쉽게 깨지기 때문에 등장한 개념입니다.

❌ 위반 사례

@Injectable()
export class UserService {
  async createUser(name: string) {
    console.log('User created:', name);
  }
}

✅ 개선 사례

export interface LoggerPort {
  log(message: string): void;
}

@Injectable()
export class ConsoleLogger implements LoggerPort {
  log(message: string) {
    console.log(message);
  }
}

@Injectable()
export class UserService {
  constructor(private readonly logger: LoggerPort) {}

  async createUser(name: string) {
    this.logger.log(`User created: ${name}`);
  }
}

@Module({
  providers: [{ provide: 'LOGGER', useClass: ConsoleLogger }],
})
export class AppModule {}

개선 이유

UserService는 LoggerPort라는 추상화에만 의존하고, 실제 구현은 DI 컨테이너에서 주입되므로 언제든 다른 로거(ConsoleLogger, WinstonLogger, LogtapeLogger, FileLogger 등)로 교체할 수 있어 코드가 훨씬 유연해집니다.

📊 다이어그램

G cluster_after After cluster_before Before R1 Request C1 Controller R1->C1 S1 UserService C1->S1 Res1 Response C1->Res1 S1->C1 L LoggerPort 인터페이스 S1->L L->S1 Impl1 ConsoleLogger L->Impl1 Impl2 WinstonLogger L->Impl2 Impl3 LogtapeLogger L->Impl3 R Request C Controller R->C S UserService: console.log 직접 호출 C->S Res Response C->Res S->C Console Console S->Console Console->S

마치며

SOLID는 오래 살아남는 코드를 위한 다섯 가지 약속입니다. 책임을 분리해 응집도를 높이고(SRP), 기존 코드를 건드리지 않고 확장할 수 있도록 만들며(OCP), 계약을 지켜 일관성을 유지하고(LSP), 불필요한 의존을 줄이고(ISP), 추상화를 통해 교체 가능성을 확보합니다(DIP).

세부적인 구현은 둘째치더라도 NestJS는 Guard, Pipe, Interceptor, DI Container 등 이미 SOLID를 녹여낼 수 있는 구조적 도구들을 제공합니다. SOLID는 특정 프레임워크의 패턴이 아니라, 현대 소프트웨어 전반에 알게 모르게 스며들어 있는 보편적 원칙이라고 할 수 있습니다.


글을 읽으시다가 사실과 다른 부분이 보이거나 설명이 모호해 보이는 지점, 혹은 보완하면 더 나아질 것 같은 아이디어가 떠오르신다면, 사소한 것이라도 편하게 지적해 주세요—빠르게 반영하며 글과 코드를 함께 다듬어 보겠습니다.

염산하

@[email protected]

TIL 세면대 뒤편이 좁은데 수전 교체를 해야한다면 전용 렌치를 사용한다. 별게 다 있네. https://link.coupang.com/a/cSfkZj

염산하

@[email protected]

TIL 세면대 뒤편이 좁은데 수전 교체를 해야한다면 전용 렌치를 사용한다. 별게 다 있네. https://link.coupang.com/a/cSfkZj

염산하

@[email protected]

TIL 세면대 뒤편이 좁은데 수전 교체를 해야한다면 전용 렌치를 사용한다. 별게 다 있네. https://link.coupang.com/a/cSfkZj

염산하

@[email protected] · Reply to Jaeyeol Lee's post

@kodingwarrior 윈도우에서 파워쉘을 패널 나눠서 쓰는 건가요?!!?

푸른곰's avatar
푸른곰

@[email protected]

커널형 완전 무선 이어폰을 안전하게 사용하기 위하여

부끄럽지만 저도 귓병이 생겼습니다. 커널형 완전 무선 이어폰의 전성시대라고 해도 과언이 아닙니다. 회사마다 거의 매년 한 두개 쯤의 플래그십 신제품을 내놓고 있고, 보급형까지 포함하면 이루 셀 수 없을 정도로 많은 제품이 나오고 있습니다. 부끄러운 얘기지만 저는 2009년부터 인이어형(소위 말하는 커널형) 이어폰을 사용해왔었는데 에어팟이 나오고 완전 무선 이어폰을 사용하기 시작한 이후에, 커널형으로 된 완전 무선 이어폰을 사용하게 된 이후로 왼쪽 귀에 경도의 외이도염을 앓고 있습니다.

purengom.com/2023/07/17/%ec%bb

Simon Willison's avatar
Simon Willison

@[email protected]

New release of my llm-openrouter plugin adds support for reasoning options and tool usage, which means you can now execute tools against 179 models made available via OpenRouter
simonwillison.net/2025/Sep/21/

Jaeyeol Lee's avatar
Jaeyeol Lee

@[email protected]

제가 죄송합니다



RE: https://hackers.pub/@2chanhaeng/01996496-49a8-7ba1-b1b9-cb126d1748cb

이찬행's avatar
이찬행

@[email protected]

오픈소스에 가장 많이 기여한 대학은?

경상국립대 GNU

엌ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

경상국립대학교의 영문 약자인 GNU를 기반으로 그려진 경상국립대의 로고 사진
ALT text details경상국립대학교의 영문 약자인 GNU를 기반으로 그려진 경상국립대의 로고 사진
염산하

@[email protected]

좋은 실수를 저지르기 위한 핵심 수법은 실수를 (특히, 스스로에게서) 감추지 않는 것이다. 실수를 저질렀을 때 부인하거나 외면하지 말고 자신의 실수가 마치 예술품인 양 머릿속에서 요모조모 뜯어보는 감정가가 되어야 한다. 어떻게 보면 예술품이 맞기도 하다. . 그러니 실수를 저질렀을 때는 숨을 깊이 들이마시고 이를 악물고 실수를 최대한 냉철하게 들여다보기 바란다. 쉬운 일은 아니다. 실수를 저질렀을 때 당황하고 화내는 것은 자연스러운 반응이다. 이런 정서적 반응을 극복하려면 무척 애써야 한다. 실수를 음미하고, 나를 헤매게 만들 별난 이상異常을 밝혀내는 데서 즐거움을 느끼는 기묘한 습관이 몸에 배도록 해야한다

공부의 재발견

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@[email protected]

〈내가 LLM과 함께 코딩하는 방식〉이라는 글을 써 봤습니다…만 이미 LLM 많이 활용하는 분들은 잘 알고 계실 내용들이긴 합니다.



RE: https://hackers.pub/@hongminhee/2025/how-i-code-with-llms

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@[email protected]


요즘 많은 사람들이 그러하듯이 나 역시 최근에는 LLM과 함께 코딩하는 일이 많아졌다. 내가 LLM과 함께 코딩한다고 말하면 의외라는 듯이 반응하는 사람들도 있고, 어떤 식으로 LLM을 활용하냐는 질문도 종종 받는다. 그래서 생각난 김에 내가 LLM을 코딩에 어떻게 활용하는지를 대략적으로 적어보고자 한다.

전제

당연하지만 내가 LLM을 활용하는 방식은 내가 주로 다루는 종류의 작업에 맞춰져 있다. 따라서 일반적인 다른 코딩에는 잘 맞지 않을 수도 있다. 내가 주로 다루는 작업이란 협업자들이나 소비자들과 주로 서면을 통해 비동기로 의사 소통을 하는 오픈 소스 프로젝트이며, 그것도 주로 애플리케이션 개발이 아닌 라이브러리 개발이다. 주로 사용하는 프로그래밍 언어는 TypeScript로 상당히 LLM이 잘 다루는 축에 속한다. 한편으로는 비교적 기존 지식의 도태가 빠르게 이뤄지는 생태계이기도 해서, 어떤 면에서는 불리한 점도 있다고 볼 수 있다.

어찌 되었든 내 LLM 활용 방식은 내가 주로 다루는 종류의 작업에 맞춰져 있으나, 그럼에도 이 글에서는 일반적인 코딩에 두루 적용할 수 있는 팁을 공유하려고 노력했다.

맥락이 왕이다

LLM 활용에 있어서 모델 자체의 성능 등 여러 고려 사항들이 있지만, 사람들이 LLM을 사용하는 것을 관찰했을 때 가장 흔히 놓치는 것이 충분한 맥락의 제공이다. 사람들은 자신이 무언가를 판단할 때 얼마나 많은 맥락에 의존하는지 의식하지 못한다. 머릿속 사소한 기억 조각부터 내가 접근 가능한 최신 문서들, 이슈에 담긴 캡처 이미지까지… 이들 대부분은 LLM이 기본적으로는 혼자서 접근할 수 없는 경우가 많다. 제아무리 LLM이 똑똑하다고 해도 필요한 맥락이 주어지지 않으면 엉뚱한 결과를 내놓기 마련이다. 주변에서 「LLM은 코딩을 너무 못한다」고 토로하는 경우를 보면 정말로 LLM이 풀기 어려운 문제를 주로 다루는 분들도 계셨지만, 대부분의 경우에는 LLM에게 충분히 맥락을 제공하지 못해서 그럴 때가 많았다.

아마도 이 글에서 다룰 대부분의 이야기는 결국 「어떻게 LLM에게 맥락을 잘 제공할까?」라는 고민에서 나온 팁들이라고 봐도 무방하다.

내가 쓰는 모델과 코딩 에이전트

2025년 9월 현재, 나는 거의 대부분의 작업에 Claude Code를 사용한다. 지난 몇 달 간 Claude Code를 주로 사용해 왔으며, 주기적으로 다른 도구들도 시도해 보고 있으나 여전히 Claude Code가 나에게 가장 맞다고 판단했다. 실은, 나는 대부분의 프로그래머에게 Claude Code가 가장 적합할 거라고 생각한다. 엄밀한 벤치마크에 근거한 것은 아니고, 체감일 따름이지만… 이유는 다음과 같다:

  • 다른 모델들은 도구 호출을 잘 못한다. 도구를 제공하면 필요한 순간에 도구를 활용할 수 있어야 한다. Claude 모델들은 이 부분에서 확실히 뛰어나다. 도구 호출은 풍부한 맥락을 제공하기 위해서는 반드시 필요하기 때문에, 도구 호출의 성능이 떨어지면 결과적으로 LLM을 동반한 코딩 자체의 성과가 떨어진다.

  • 다른 모델들은 질답이 길어질 때 성능이 떨어진다. 적어도 나는 LLM과 코딩할 때 한 방(one-shot)에 결과를 내놓는 방식, 즉 바이브 코딩(vibe coding)을 선호하지 않는다. 따라서 모델의 멀티턴(multi-turn) 성능이 중요한데, 다른 모델들은 질답이 길어짐에 따라 앞서 했던 이야기를 망각하는 경향이 있다.

  • 똑같이 Claude의 모델을 사용하더라도 Claude Code 자체의 성능이 다른 LLM 코딩 에이전트에 비해 뛰어나다. 이건 파인 튜닝이나 시스템 프롬프트 등에 비법 소스가 있기 때문이라고 여겨진다. 이 때문에 Claude Code를 Claude가 아닌 모델과 함께 쓸 수 있게 해주는 비공식 프록시 등도 존재한다.

물론, Claude 및 Claude Code에도 단점이 있다:

  • 상대적으로 문맥 윈도(context window)가 짧다. 그래서 토큰을 아껴야 한다. Claude Code의 대화 압축(conversation compaction)은 그럭저럭 잘 동작하는 편이긴 해도, 여전히 스트레스이긴 하다. (이를테면, 대화 압축은 영어로 이뤄지기 때문에 후속 세션에서는 갑자기 영어로 대답하기 시작한다. 아, 나는 프롬프트를 한국어로 적는다.)

  • 일부 LLM 코딩 에이전트들이 제공하는 LSP 지원이 아직 없다. 따라서 타입 오류나 린트 오류 등을 명령어 실행 등을 통해 따로 보여줄 수 있어야 한다. 대신 Claude Code에서는 (hooks) 기능이 제공되므로, 이를 잘 활용하면 어느 정도는 비슷한 효과를 볼 수 있다.

하지만 장점이 단점을 압도하기 때문에 앞으로도 상황에 큰 변화가 없다면 Claude Code를 주로 쓸 것 같다.

세부 지시는 서면으로

지시(prompt)는 세부적일 수록 좋다. 지시가 한 문장으로 끝난다면 좋은 지시가 아닐 가능성이 높다. 때로는 지시를 만드는 지시가 필요하기도 하다. 나의 지시 방식은 다음과 같다.

우선 대부분의 지시는 GitHub 이슈에 작성한다. 필요한 충분한 링크를 제공해야 하고, 가급적이면 캡처 이미지나 도표와 같은 시각적 정보에는 크게 의존하지 않아야 한다. 물론, 이슈는 기본적으로 LLM을 위한 것이 아니라 사람을 위한 것이므로 LLM에게만 필요한 정보는 이슈에 담고 싶지 않을 수 있다. 그런 건 이슈에 담지 않아도 된다.[1] 이미 남이 만든 이슈가 있다면 그 이슈를 활용해도 된다. 남이 만든 이슈에 맥락이 충분하지 않다고 여겨지면, 댓글로 맥락을 보충한다.

때로는 이슈 자체도 LLM으로 작성하기도 한다. 관련 문서나 상황을 충분히 공유하여 이슈를 작성해 달라고 하는 식이다. 예를 들면, 다음은 내가 만든 이메일 전송 라이브러리인 UpyoPlunk 트랜스포트를 추가하는 이슈 #11 Plunk transport를 작성하기 위해 사용했던 지시문이다:

Plunk라는 이메일 전송 프로바이더가 있습니다. Plunk의 트랜스포트를 Upyo에 추가하면 좋을 것 같은데요. 이슈 트래커에 일단 이슈를 먼저 만들고자 합니다. 이슈 제목과 내용을 영어로 작성해 주실 수 있을까요? 너무 문제 정의 및 해결책 제시가 구분되는 식의 형식적인 글 대신, 좀 더 사람이 쓴 것 같은 자연스러운 톤으로 부탁드립니다. 너무 길 필요도 없고요. 한두 문단 정도면 충분할 것 같습니다.

참고 링크:

단, 이 때 나는 Claude의 프로젝트 기능을 이용해서 Upyo의 기존 문서를 RAG로 제공한 상태에서 지시를 했다는 점을 밝힌다.

그 다음에 Claude Code의 계획 모드(plan mode)[2]에서 다음과 같이 지시한다.

https://github.com/dahlia/upyo/issues/11 이슈를 구현해야 합니다. 이슈 본문과 본문에서 링크된 관련 링크들을 모두 살펴본 뒤, Upyo 프로젝트에 Plunk 트랜스포트를 추가할 구현 계획을 세부적으로 세워주세요.

정확히는, 나는 이슈 링크를 제공하는 대신 이슈 번호만 제공하고 GitHub MCP를 이용해서 이슈를 직접 읽도록 하는 걸 선호한다. HTML이 아니라 Markdown 형식으로 이슈를 읽기 때문에 본문에 걸린 링크 등을 더 잘 따라가기 때문이다. 링크가 정말 중요한 경우 Claude Code의 지시에서 한 번 더 적기도 한다. 이슈에 미처 적지 못했던 LLM만을 위한 정보도 이 때 모두 적는다.

구현할 때 살펴봐야 할 소스 파일이 무엇인지 잘 알고 있다면, 그런 정보도 함께 제공하면 더 좋다. LLM이 코드베이스를 탐색하느라 삽질을 훨씬 덜 하고, 토큰도 덜 쓰기 때문이다.

나는 GitHub 이슈를 세부 지시를 적는 용도로 썼지만, PLAN.md 같은 문서 파일을 만들어서 거기다 적는 방법도 많이 쓰인다고 알고 있다.

설계는 사람이, 구현은 LLM이

내가 LLM을 코딩에 활용할 때의 기본적인 원칙은 큰 설계는 내 스스로 하고, 세부적인 구현은 LLM에게 맡긴다는 것이다. 지시할 때는 설계 의도를 정확히 제시하고, 구현 과정에서 실수할 수 있을 것 같은 우려점에 대해서 충분히 짚고 넘어간다. 특히, 나는 프로젝트를 처음 시작할 때 디렉터리나 패키지 구조를 여전히 직접 손으로 할 때가 많다. (하지만 이 부분은 내가 원체 LLM 시대 이전부터 쿠키커터 류의 프로젝트 템플릿도 좋아하지 않았기 때문일 수도 있다. 템플릿을 쓰든 LLM을 쓰든 내 마음에 들게 나오지 않기에.)

LLM은 자신이 가장 익숙한 기술로 문제를 해결하려는 경향이 있기 때문에, 기술 선택에 있어서도 명시적으로 지시를 하는 것이 좋다. 아직은 사소한 라이브러리 하나조차도 사람이 검토할 필요가 있다. LLM에게 모든 것을 맡기다 보면 보안 패치도 안 된 옛날 버전을 가져다 쓰거나 하는 일이 흔하기 때문이다. 나 같은 경우에는 후술할 AGENTS.md 문서에서 라이브러리를 설치하기 전에는 npm view 명령어를 통해 해당 패키지의 최신 버전이 무엇인지 먼저 확인하라는 지시를 포함시키기도 한다.

추상화를 할 때도, 적어도 API 설계는 여전히 내가 직접할 때가 많다. 어째서일까, LLM이 설계한 API는 아직은 별로일 때가 많다. 내가 받은 인상은, LLM은 구현해 나가며 필요할 때 API를 즉석(ad-hoc)에서 설계할 때가 잦다는 것이다. 물론, 사람이라도 이런 방식을 선호할 수 있는데, 그런 경우에는 LLM이 설계한 API에 불만이 없을지도 모른다. 하지만 적어도 내 경우에는 불만족스러울 때가 많다.

결국에는 LLM을 만능 노예가 아니라 똑똑하기도 하지만 미숙한 점도 많은 동료로 보고 LLM이 서툰 부분에는 최대한 사람이 도와서 일을 해낸다는 관점이 필요한 것 같다.

AGENTS.md

대부분의 LLM 코딩 에이전트들은 AGENTS.md 내지는 그에 준하는 기능을 제공하고 있다. 예를 들어 Claude Code는 CLAUDE.md 파일을 바라보며, Gemini CLIGEMINI.md를 바라보는 식인데, 점차 AGENTS.md 파일로 표준화되고 있는 추세이다. 나는 특정 벤더에서만 쓰는 파일들을 모조리 AGENTS.md로 심볼릭 링크를 건 다음 AGENTS.md 파일만을 정본으로 삼게 하고 있다. 이렇게 하면 나와 다른 LLM 코딩 에이전트를 사용하는 협업자들과 같은 지침을 공유할 수 있다.

아무튼, AGENTS.md 문서의 역할은 간단하다. 이 프로젝트에 대한 지침, 즉 시스템 프롬프트이다. 대부분의 LLM 코딩 에이전트들은 이 파일을 자동으로 생성하는 기능을 제공하는데, 안 쓸 이유는 없다. 자동으로 생성하게 한 후, 잘못된 부분만 고쳐서 써도 된다. 그보다 더 중요한 건 시간이 흐르면서 AGENTS.md 문서가 낡게 되는 것을 피하는 것이다. AGENTS.md 문서는 꾸준히 갈고 닦아야 한다. 특히, 대규모 리팩터링이 있거나 한 뒤에는 반드시 AGENTS.md 문서를 갱신해야 한다. 이 지시 자체도 AGENTS.md 문서에 넣어두는 게 좋다.

그러면 AGENTS.md 문서에는 어떤 내용을 넣을까? 나의 경우에는 다음과 같은 내용을 넣는다:

  • 프로젝트의 목표와 개요. 저장소 URL을 적어두는 것도 의외로 GitHub MCP 등을 활용할 때 쓸모가 있다.
  • 프로젝트가 사용하는 개발 도구. 예를 들어 npm은 절대 쓰지 않으며 pnpm만 쓴다는 식의 지시를 포함한다.
  • 디렉터리 구조와 각 디렉터리의 역할.
  • 빌드 및 테스트 방법. 특히, 그 프로젝트만의 특수한 절차가 있다면 반드시 기술한다. 예를 들어, 빌드나 테스트 전에 반드시 코드 생성을 해야 한다면, 이에 관해 적어야 한다—물론, 가장 좋은 것은 빌드 스크립트로 그러한 절차를 자동화하는 것이다. 그 편이 사람에게도 좋고, 토큰을 아끼는 데에도 좋다.
  • 코딩 스타일이나 문서 스타일. 포매터를 쓰는 방법을 적는 것도 좋다.
  • 개발 방법론. 이를테면 버그를 고칠 때는 회귀 테스트를 먼저 작성하고, 테스트가 실패하는 것으로 버그가 재현되는 것을 확인한 다음에 버그 수정을 하라는 지침 같은 것들.

일단은 위와 같이 시작하고, LLM 코딩 에이전트를 활용하면서 눈에 밟히는 실수들을 LLM이 할 때마다 지침을 추가하는 것을 권한다. 예를 들어, 나는 TypeScript 프로젝트에서 any 타입이나 as 키워드를 피하라는 지침을 추가하는 편이다.

다음은 내가 관리하는 프로젝트들의 AGENTS.md 문서들이다:

문서 제공

LLM에 지식 컷오프가 있다는 건 잘 알려져 있다. 방금 갓 나온 모델이 아닌 한, 내가 쓰는 라이브러리나 런타임 등의 API, CLI 도구 등에 대해 다소 낡은 지식을 가지고 있을 가능성이 높다는 뜻이다. 게다가 만약 비주류 프로그래밍 언어나 프레임워크 등을 쓰고 있다면 이 문제는 더욱 커진다.

따라서 내가 사용하는 프로그래밍 언어나 프레임워크 등에 대한 지식을 제공해야 하는데, 가장 쉽고 효율적인 방법은 Context7을 MCP로 붙이는 것이다. Context7은 다양한 기술 문서를 비교적 최신판으로 유지하면서 벡터 데이터베이스에 색인하고, LLM이 요청할 경우 관련된 문서 조각을 제공하는 RAG 서비스이다. 새로운 문서를 추가하는 것도 쉬워서, 만약 내가 필요한 문서가 등록되어 있지 않다면 얼마든지 새로 추가해서 사용할 수도 있다. 다만, 특별히 지시하지 않는 한 Context7을 따로 활용하지 않을 때도 있기 때문에, 「Context7 MCP를 통해 관련 문서를 확인해 보라」는 식의 지시가 필요할 수 있다.

RFC 같은 기술 명세 문서를 제공할 때는 평문(plain text) 형식이 제공되므로 평문 문서의 링크를 제공하는 게 좋다. 나의 경우 연합우주(fediverse) 관련 개발을 많이 하다 보니 FEP 문서를 제공해야 할 일이 많은데, 이 때도 HTML으로 렌더링 된 웹 페이지가 아닌 Markdown 소스 파일의 링크를 직접 제공하는 식으로 사용하고 있다.

이 외에도 웹사이트에서 /llms.txt/llms-full.txt 파일을 제공하는 관행이 퍼지고 있으므로, 이를 활용하는 것도 좋다. (내가 만든 소프트웨어 라이브러리들의 경우 프로젝트 웹사이트에서 모두 /llms.txt/llms-full.txt 파일을 제공하고 있다.)

다만 아무리 LLM에게 친화적인 평문이라고 해도 기술 문서 전체를 다 제공하는 건 아무래도 토큰 낭비가 심하기 때문에, Context7을 쓸 수 있다면 Context7을 쓰는 것을 추천한다.

계획 모드의 활용

Claude Code를 포함해 최근의 많은 LLM 코딩 에이전트는 계획 모드를 제공한다. 냅다 구현하는 것을 방지하고, 구현 계획을 LLM 스스로 세우게 한 뒤 사람이 먼저 검토할 수 있게 하는 것이다. 특히, 나는 계획 모드에서는 비싼 Claude Opus 4.1을 사용하고 실제 구현에서는 비교적 저렴한 Claude Sonnet 4를 사용하는 “Opus Plan Mode”를 사용하고 있다.[3]

나는 계획을 면밀히 검토하고 조금이라도 내 성에 차지 않으면 얼마든지 계획 수정을 요구한다. 작업에 따라 다르지만, 어떤 작업이든 적어도 서너 번 이상은 수정을 요구하는 것 같다. 반대로 얘기하면, 이 정도로 계획을 다듬지 않으면 내가 원하는 방향으로 구현하지 않을 가능성이 높다. LLM은 나와는 다른 전제를 품을 때가 많아서, 여러 세부 계획에서 나와는 동상이몽을 하고 있을 가능성이 높다. 그런 것들을 사전에 최대한 제거하여 내 의도에 일치시켜야 한다.

알아서 피드백 루프를 돌게

LLM 코딩 에이전트로 개발을 할 때 가장 중요하다고 여겨지는 부분은 바로, 스스로 웬만큼 방향을 조정할 수 있도록 여건을 마련하는 일이다. LLM의 각종 구현 실수를 하나하나 내가 지적하는 게 아니라, 각종 자동화된 테스트와 정적 분석을 통해 스스로 깨닫고 구현을 고칠 수 있도록 해주는 것이다.

예를 들어, CSS 버그를 고칠 때를 생각해 보자. LLM이 CSS 코드를 고치게 한 뒤, 내가 웹 브라우저를 확인하는 방식은 너무 번거롭다. 대신, Playwright MCP를 붙여서 스스로 화면을 볼 수 있게 하는 게 낫다. 요는, LLM의 작업 결과가 요구 사항을 충족하는지를 스스로 판단할 수 있게 하여, 요구 사항이 충족될 때까지 작업을 계속하게 만드는 것이다.

비슷한 이유에서, 구현하기에 앞서 테스트 코드를 먼저 작성하도록 지시하는 것이 여러모로 편하다. 테스트 코드를 작성하는 과정까지만 사람이 지켜보면 되고, 그 뒤는 상대적으로 신경을 덜 써도 되기 때문이다. 실은, 나는 LLM 코딩 에이전트를 활용할 때도 가끔은 테스트를 직접 짜기도 한다. 프롬프트로 요구 사항을 정확하게 검증하는 테스트 코드를 짜게 하는 것보다 내가 직접 테스트 코드를 짜는 게 빠르겠다고 느낄 때 그렇다.

이런 작업 흐름을 선호하다 보니, 좀 더 엄밀한 타입 시스템을 갖춘 프로그래밍 언어, 좀 더 엄격한 린트 규칙 등이 LLM 코딩 에이전트를 활용할 때 훨씬 유리하다고 생각하게 되었다. LLM 시대 이전에도 생각은 비슷하긴 했지만 말이다.

가끔은 손 코딩

하지만 여전히 LLM에 많은 한계점이 있기 때문에, 나는 아직도 가끔은 손 코딩을 한다. API를 설계할 때도 그렇고, 엄밀한 테스트 코드를 짜고 싶을 때도 그렇다. (LLM은 테스트 코드를 좀 대충 짜는 경향이 있다.) 그리고 무엇보다, 재밌을 것 같은 코딩은 내가 한다!

바이브 코딩에 깊게 심취했다가 코딩의 재미가 사라졌다는 소프트웨어 프로그래머들의 얘기를 종종 듣는다. 내 생각에는, 재미있는 부분은 LLM에게 시키지 않는 게 좋다. 결과의 품질 때문이 아니라, 소프트웨어 프로그래머로서 모티베이션을 유지하기 위해서 그렇다. 재미 없고 지루한 부분, 그러니까 코딩하기 싫어지게 하는 작업에서 최대한 LLM을 활용하는 것이 LLM과 공존하는 좋은 전략이 아닐까 생각한다. 뭐, 적어도 내게는 이 방식이 잘 먹히는 것 같다.


  1. 이건 한 가지 팁인데, LLM에게만 필요한 정보를 <!-- … --> 주석 안에 적는 방법도 있다. ↩︎

  2. Shift + Tab을 두 번 누르면 계획 모드에 진입할 수 있다. ↩︎

  3. Claude Code에서 /model 명령어를 통해 고를 수 있다. ↩︎

이루왕's avatar
이루왕

@[email protected]

이건 뭐 주기적으로 통신사도 바꾸고 주기적으로 카드도 재발급해야하는건가 이럴거면 주기적으로 주민번호도 바꿔줘 다 털릴거면

염산하

@[email protected]

홀로... 이상태로 천년만년 업데이트 안하고 문제없이 잘 돌았으면 좋겠... (아직 0.5.x)

염산하

@[email protected]

왜 내가 쓰는 클로드는 바보 같지... 지피티5 씽킹이 일단 최고시다. 아 딥리서치도.

염산하

@[email protected]

공부의 재발견

염산하

@[email protected]

iOS 26 RC, 쓸 데없어 보이는 글라스 효과 때문에 UI 가 무거워졌을 거라고 생각했는데, 희한하게 오히려 반응성은 더 경쾌해졌다. 탭 했을 때 메뉴 뜨는거나 메뉴 뜨는 반응 등에서. 아이폰 14 프로.

← Newer
Older →