Skip to main content

API 애플리케이션의 예외 설계 — Server/Client, ErrorCode, Resolver 기반 구조

· 16 min read
Ryukato
BackEnd Software Developer

API 애플리케이션을 개발하다 보면, 예외 설계는 항상 뒤로 미루기 쉬운 주제다.
하지만 한 번 잘못 설계되기 시작하면, 이후 기능이 늘어날수록 예외 구조는 걷잡을 수 없게 복잡해지고,
결국 API의 응답 구조 · 로깅 · 모니터링까지 모두 영향을 받게 된다.

이 글에서는 먼저 _자주 범하는 예외 설계 패턴_을 살펴보고,
그 다음에 Server / Client 축과 ErrorCode enum, Resolver, Factory Method를 활용한
좀 더 견고한 예외 설계 방식을 정리해본다.


자주 범하는 예외 구조

실제 코드베이스에서 자주 볼 수 있는 구조는 대략 이런 모습이다.

// 모든 API 예외의 상위라고 주장하는 추상 클래스
public class ApiException extends RuntimeException {
public ApiException(String message) {
super(message);
}
}

// Auth 관련 예외
public class AuthException extends RuntimeException {
public AuthException(String message) {
super(message);
}
}

// User 관련 예외
public class UserException extends RuntimeException {
public UserException(String message) {
super(message);
}
}

처음에는 나쁘지 않아 보인다.
하지만 실제 구현이 진행되면 이런 식으로 세분화되기 쉽다.

public class InvalidLoginAccountException extends AuthException {
public InvalidLoginAccountException() {
super("invalid login account");
}
}

public class DeactivatedUserException extends AuthException {
public DeactivatedUserException() {
super("deactivated user");
}
}

public class NoUserException extends UserException {
public NoUserException(Long userId) {
super("no user. id=" + userId);
}
}

1) 예외가 도메인별로만 분기되어 있다

AuthException, UserException, OrderException처럼
“어느 도메인에서 발생했는지”에 따라 예외를 나누는 구조는 한눈에 보기에는 이해하기 쉽다.

하지만 HTTP 응답 관점에서는 곧 문제가 드러난다.

  • 어떤 AuthException은 401(Unauthorized)로 내려가야 하고
  • 어떤 AuthException은 403(Forbidden)일 수도 있고
  • UserException 중 어떤 것은 400(Bad Request)이고
  • 어떤 것은 404(Not Found)일 수 있다.

즉, 도메인별 예외 구조만으로는 HTTP Status를 일관성 있게 매핑하기 어렵다.
글로벌 예외 핸들러에는 instanceofif/else가 쌓이기 시작한다.

2) 예외가 너무 세분화되어 관리가 어렵다

상황별로 클래스를 나누다 보면:

  • InvalidLoginAccountException
  • DeactivatedUserException
  • InvalidPhoneNumberException
  • UserNotActiveException
  • UserAlreadyRegisteredException
  • ...

예외 클래스 개수가 금방 수십~수백 개가 되어버린다.

문제는:

  • 전반적인 일관성 상실 (메시지/필드/로깅 정책이 제각각)
  • 실제 어떤 예외가 있는지 한눈에 파악하기 어려움
  • 신규 팀원이 진입했을 때 학습 비용 증가
  • 중복/의미가 비슷한 예외가 생기기 시작함

3) 반대로 예외가 지나치게 추상적이다

다른 극단으로, 모든 예외를 다음과 같이 처리하는 경우도 있다.

public class ApiException extends RuntimeException {

private final int httpStatus;

public ApiException(String message, int httpStatus) {
super(message);
this.httpStatus = httpStatus;
}

public int getHttpStatus() {
return httpStatus;
}
}

여기에 모든 예외를 몰아 넣으면:

  • 이게 클라이언트의 잘못인지, 서버의 잘못인지 구분하기 어렵고
  • ErrorCode의 체계도 잡기 어려우며
  • 도메인 레이어에서 httpStatus를 직접 알고 사용하는 구조가 된다.

결국 **프레젠테이션 계층(HTTP)**에 대한 지식이 도메인 레이어로 침범하는 문제가 발생한다.

4) 도메인 레이어가 HTTP를 직접 알게 되는 구조

위의 ApiException처럼 도메인에서 HTTP 상태 코드를 직접 들고 있게 되면,

  • 나중에 gRPC, 메시지 큐, CLI 등 다른 인터페이스를 붙이기 어렵고
  • Hexagonal Architecture / Clean Architecture에서 말하는 레이어 분리가 깨진다.

정리하자면, 자주 보이는 예외 구조들은

  • 도메인별로만 쪼개져 있거나
  • 지나치게 세분화되어 있거나
  • 너무 추상적이거나
  • 도메인과 HTTP가 뒤섞여 있거나

하는 문제를 가진 경우가 많다.


제안 구조: Server / Client 중심 + ErrorCode 기반 설계

위의 문제를 해결하기 위한 핵심 방향은 다음과 같다.

  1. 예외의 큰 축은 Client / Server 두 가지로만 나눈다.
  2. 발생 가능한 예외는 ErrorCode enum으로 모두 나열한다.
  3. 도메인 레이어는 HTTP를 전혀 모르고, 프레젠테이션 레이어에서만 HTTP를 매핑한다.
  4. 예외 생성은 Factory Method로 일관성 있게 만든다.

아래는 이를 Java 코드로 정리한 예시들이다.


1) 도메인 레이어: ErrorKind, ErrorCode, DomainException

먼저 예외의 종류를 크게 나누는 ErrorKind
실제 에러 코드를 정의하는 ErrorCode enum을 만든다.

package com.example.domain.error;

public enum ErrorKind {
CLIENT, // 요청자(클라이언트) 잘못 - validation, 권한, not found 등
SERVER // 서버 내부 또는 외부 시스템 문제
}
package com.example.domain.error;

public enum ErrorCode {

// 공통/기본
INVALID_ARGUMENT,
VALIDATION_FAILED,

// 인증/인가
UNAUTHORIZED,
FORBIDDEN,

// 유저 도메인
USER_NOT_FOUND,

// 시스템/외부 의존성
EXTERNAL_API_FAILURE,
DB_FAILURE;

// 필요하다면 별도의 code 문자열 필드를 둘 수도 있다.
}

DomainException은 도메인 레이어에서 사용할 공통 예외 베이스 클래스다.

package com.example.domain.error;

import java.util.Collections;
import java.util.Map;

public class DomainException extends RuntimeException {

private final ErrorCode code;
private final ErrorKind kind;
private final Map<String, Object> metadata;

public DomainException(
ErrorCode code,
ErrorKind kind,
String message
) {
this(code, kind, message, Collections.emptyMap(), null);
}

public DomainException(
ErrorCode code,
ErrorKind kind,
String message,
Map<String, Object> metadata
) {
this(code, kind, message, metadata, null);
}

public DomainException(
ErrorCode code,
ErrorKind kind,
String message,
Map<String, Object> metadata,
Throwable cause
) {
super(message, cause);
this.code = code;
this.kind = kind;
this.metadata = metadata != null ? metadata : Collections.emptyMap();
}

public ErrorCode getCode() {
return code;
}

public ErrorKind getKind() {
return kind;
}

public Map<String, Object> getMetadata() {
return metadata;
}
}

여기서 주목해야 할 점은:

  • HTTP 상태 코드가 전혀 등장하지 않는다는 것
  • 도메인은 오직 error의 _종류(kind)_와 _코드(code)_만 표현한다는 것

2) ClientException / ServerException 두 개만 둔다

이제 DomainException을 상속하는 두 종류의 예외만 둔다.

package com.example.domain.error;

import java.util.Map;

public class ClientException extends DomainException {

public ClientException(ErrorCode code, String message) {
super(code, ErrorKind.CLIENT, message);
}

public ClientException(ErrorCode code, String message, Map<String, Object> metadata) {
super(code, ErrorKind.CLIENT, message, metadata);
}
}
package com.example.domain.error;

import java.util.Map;

public class ServerException extends DomainException {

public ServerException(ErrorCode code, String message) {
super(code, ErrorKind.SERVER, message);
}

public ServerException(ErrorCode code, String message, Map<String, Object> metadata) {
super(code, ErrorKind.SERVER, message, metadata);
}

public ServerException(ErrorCode code, String message, Map<String, Object> metadata, Throwable cause) {
super(code, ErrorKind.SERVER, message, metadata, cause);
}
}

예외 클래스를 “종류(성격)” 기준으로 두 개만 두고,
실제 의미는 ErrorCode로 풀어내는 방식이다.


3) ErrorCode + Factory Method 패턴

실제 도메인 코드에서는 직접 new ClientException(...)을 호출하기보다는,
도메인별 Factory 클래스를 두고 그 안에서만 예외를 생성하는 것을 추천한다.

3-1. User 도메인용 팩토리: UserErrors

package com.example.domain.user;

import com.example.domain.error.ClientException;
import com.example.domain.error.ErrorCode;

import java.util.HashMap;
import java.util.Map;

public final class UserErrors {

private UserErrors() {
}

public static ClientException userNotFound(long userId) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("userId", userId);

return new ClientException(
ErrorCode.USER_NOT_FOUND,
"user not found: id=" + userId,
metadata
);
}

public static ClientException invalidAgeRange(int min, int max) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("min", min);
metadata.put("max", max);

return new ClientException(
ErrorCode.INVALID_ARGUMENT,
"invalid age range: [" + min + ", " + max + "]",
metadata
);
}
}

사용 예시는 다음과 같다.

User user = userRepository.findById(userId)
.orElseThrow(() -> UserErrors.userNotFound(userId));

if (age < 0 || age > 150) {
throw UserErrors.invalidAgeRange(0, 150);
}

3-2. 공통 시스템 예외용 팩토리: DomainExceptions

package com.example.domain.error;

import java.util.Collections;
import java.util.Map;

public final class DomainExceptions {

private DomainExceptions() {
}

public static ClientException invalidArgument(String message) {
return new ClientException(
ErrorCode.INVALID_ARGUMENT,
message,
Collections.emptyMap()
);
}

public static ServerException dbFailure(String message, Throwable cause) {
return new ServerException(
ErrorCode.DB_FAILURE,
message,
Collections.emptyMap(),
cause
);
}

public static ServerException externalApiFailure(String provider, Throwable cause) {
Map<String, Object> metadata = Map.of("provider", provider);

return new ServerException(
ErrorCode.EXTERNAL_API_FAILURE,
"external api failure: provider=" + provider,
metadata,
cause
);
}
}

사용 예시는 다음과 같다.

if (!isValid(request)) {
throw DomainExceptions.invalidArgument("request is invalid");
}

try {
externalClient.call(...);
} catch (Exception e) {
throw DomainExceptions.externalApiFailure("PAYMENT_GATEWAY", e);
}

이렇게 하면:

  • ErrorCode–메시지–metadata의 조합이 한 곳에 모이고
  • 서비스 코드에서는 의도만 읽으면 된다.

4) Presentation 레이어: ErrorResponse와 HttpErrorResponseResolver

이제 도메인 예외를 HTTP 응답으로 바꾸는 레이어를 살펴보자.
이 부분은 Spring MVC 기준으로 설명하지만, WebFlux/gRPC 등에서도 동일한 개념으로 적용 가능하다.

4-1. ErrorResponse DTO

package com.example.api.error;

import java.util.Map;

public class ErrorResponse {

private final String code;
private final int httpStatus;
private final String description;
private final String resourceUri;

private final String traceId;
private final String timestamp;
private final Map<String, Object> details;
private final Boolean retryable;

public ErrorResponse(
String code,
int httpStatus,
String description,
String resourceUri,
String traceId,
String timestamp,
Map<String, Object> details,
Boolean retryable
) {
this.code = code;
this.httpStatus = httpStatus;
this.description = description;
this.resourceUri = resourceUri;
this.traceId = traceId;
this.timestamp = timestamp;
this.details = details;
this.retryable = retryable;
}

public String getCode() {
return code;
}

public int getHttpStatus() {
return httpStatus;
}

public String getDescription() {
return description;
}

public String getResourceUri() {
return resourceUri;
}

public String getTraceId() {
return traceId;
}

public String getTimestamp() {
return timestamp;
}

public Map<String, Object> getDetails() {
return details;
}

public Boolean getRetryable() {
return retryable;
}
}
  • code : ErrorCode.name (또는 별도의 문자열 코드)
  • httpStatus : HTTP 상태 코드 숫자
  • description : 클라이언트에게 보여줄 메시지
  • resourceUri : 요청 URI (/api/users/123 등)
  • traceId : 로그/트레이싱 연동용 식별자
  • timestamp : 에러 발생 시각
  • details : validation error 등 상세 정보
  • retryable : 재시도 여부 (서버/외부 시스템 에러 등에 유용)

4-2. HttpErrorResponseResolver

package com.example.api.error;

import com.example.domain.error.DomainException;
import com.example.domain.error.ErrorCode;
import com.example.domain.error.ErrorKind;
import org.springframework.http.HttpStatus;

import java.time.Instant;
import java.util.Map;

public final class HttpErrorResponseResolver {

private HttpErrorResponseResolver() {
}

public static ErrorResponse resolve(
DomainException ex,
String resourceUri,
String traceId
) {
HttpStatus status = resolveStatus(ex);
String description = resolveDescription(ex);
Map<String, Object> details = buildDetails(ex);
Boolean retryable = resolveRetryable(ex);

String timestamp = Instant.now().toString();

return new ErrorResponse(
ex.getCode().name(), // 필요하다면 별도 코드 문자열로 매핑 가능
status.value(),
description,
resourceUri,
traceId,
timestamp,
details,
retryable
);
}

private static HttpStatus resolveStatus(DomainException ex) {
ErrorKind kind = ex.getKind();
ErrorCode code = ex.getCode();

if (kind == ErrorKind.CLIENT) {
return resolveClientStatus(code);
} else {
return resolveServerStatus(code);
}
}

private static HttpStatus resolveClientStatus(ErrorCode code) {
switch (code) {
case USER_NOT_FOUND:
return HttpStatus.NOT_FOUND;
case INVALID_ARGUMENT:
case VALIDATION_FAILED:
return HttpStatus.BAD_REQUEST;
case UNAUTHORIZED:
return HttpStatus.UNAUTHORIZED;
case FORBIDDEN:
return HttpStatus.FORBIDDEN;
default:
return HttpStatus.BAD_REQUEST;
}
}

private static HttpStatus resolveServerStatus(ErrorCode code) {
switch (code) {
case EXTERNAL_API_FAILURE:
return HttpStatus.BAD_GATEWAY;
case DB_FAILURE:
return HttpStatus.SERVICE_UNAVAILABLE;
default:
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}

private static String resolveDescription(DomainException ex) {
// 기본은 ex.getMessage(), 필요하다면 i18n 리소스를 사용할 수도 있다.
return ex.getMessage();
}

@SuppressWarnings("unchecked")
private static Map<String, Object> buildDetails(DomainException ex) {
// 예: INVALID_ARGUMENT일 때 metadata에 fieldErrors가 있으면 details로 노출
if (ex.getCode() == ErrorCode.INVALID_ARGUMENT) {
Object fieldErrors = ex.getMetadata().get("fieldErrors");
if (fieldErrors instanceof Map) {
return (Map<String, Object>) fieldErrors;
}
}
return null;
}

private static Boolean resolveRetryable(DomainException ex) {
switch (ex.getCode()) {
case EXTERNAL_API_FAILURE:
case DB_FAILURE:
return Boolean.TRUE;
default:
return null;
}
}
}

5) Spring 글로벌 예외 핸들러

마지막으로, Spring의 @RestControllerAdvice에서
위의 Resolver를 사용해 ErrorResponse를 만들어 반환한다.

package com.example.api.error;

import com.example.domain.error.DomainException;
import com.example.domain.error.ErrorKind;
import org.slf4j.MDC;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import jakarta.servlet.http.HttpServletRequest;

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(DomainException.class)
public ResponseEntity<ErrorResponse> handleDomainException(
DomainException ex,
HttpServletRequest request
) {
String resourceUri = request.getRequestURI();
String traceId = MDC.get("traceId"); // 로깅 필터에서 넣어두었다고 가정

ErrorResponse errorResponse = HttpErrorResponseResolver.resolve(
ex,
resourceUri,
traceId
);

// kind에 따라 로그 레벨을 분리할 수도 있다.
if (ex.getKind() == ErrorKind.CLIENT) {
// logger.warn("client error: {} - {}", ex.getCode(), ex.getMessage());
} else {
// logger.error("server error: {} - {}", ex.getCode(), ex.getMessage(), ex);
}

return ResponseEntity
.status(errorResponse.getHttpStatus())
.body(errorResponse);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpected(
Exception ex,
HttpServletRequest request
) {
String resourceUri = request.getRequestURI();
String traceId = MDC.get("traceId");

ErrorResponse errorResponse = new ErrorResponse(
"UNEXPECTED_ERROR",
500,
"internal server error",
resourceUri,
traceId,
java.time.Instant.now().toString(),
null,
null
);

// logger.error("unexpected error", ex);

return ResponseEntity
.status(errorResponse.getHttpStatus())
.body(errorResponse);
}
}

전체 예외 처리 흐름 요약

위 구조를 한 번에 정리하면 다음과 같다.

  1. 도메인 레이어

    • DomainException + ClientException / ServerException
    • ErrorCode enum으로 발생 가능한 모든 예외 유형 정의
    • HTTP, gRPC 등 프로토콜은 전혀 모른다.
  2. Factory Method (UserErrors, DomainExceptions 등)

    • ErrorCode, 메시지, metadata, kind를 한 곳에서 캡슐화
    • 서비스 코드에서는 throw UserErrors.userNotFound(id);처럼 의도만 표현
  3. Presentation 레이어

    • HttpErrorResponseResolverDomainExceptionErrorResponse로 변환
    • HTTP Status, description, traceId, timestamp, details, retryable 등을 결정
  4. 글로벌 예외 핸들러

    • @RestControllerAdvice에서 DomainException과 일반 Exception을 처리
    • 모든 API에서 일관된 JSON 에러 응답을 반환
  5. 클라이언트

    • 항상 동일한 구조의 ErrorResponse를 받고
    • code / httpStatus / description / details / retryable를 기준으로 UI, 재시도 정책, 로깅 등을 구현

정리

이 글에서 제안한 예외 설계의 핵심은 다음과 같다.

  • 도메인은 Client/Server + ErrorCode만 알고, HTTP를 모른다.
  • 예외 클래스는 ClientException / ServerException 두 개로 최소화한다.
  • ErrorCode enum + Factory Method로 예외 생성 규칙을 일관되게 유지한다.
  • Presentation 레이어는 Resolver를 통해 ErrorResponse를 만들고, HTTP Status를 결정한다.

이 구조는 단순하지만 확장성이 높고,
서비스가 커질수록 “예외 설계의 빚”을 줄여주는 효과가 크다.

이미 운영 중인 시스템이라도,
먼저 새로운 모듈이나 신규 API부터 이 패턴을 적용해 보고
점진적으로 나머지 코드베이스로 확장해 나가면
예외 처리의 일관성과 유지보수성이 크게 개선될 것이다.

개발자는 글을 못 쓴다고요? 서평

· 5 min read
Ryukato
BackEnd Software Developer

“LINE Blockchain Developer”(이하 LBD) 서비스를 개발할 때 많은 도움을 받았던 테크니컬 라이터분께서 책을 내셨다. 평소 그 분의 성격이나 인품에 비춰봤을 때, “다소 제목이 공격적인데”라는 생각이 들었으나, 제목은 일종의 미끼 역활을 톡톡히 해낸 것으로 생각한다. 지금은 제목과 같이 글쓰기에 자신이 없거나 자신이 쓴 글이 좋지 않다라는 생각을 할 수 있겠지만, 이 책을 읽고 난 후에는, 이 책의 내용을 자신의 것으로 만든 다면 글쓰기에 있어 분명 달라진 자신을 발견할 수 있을 것이라고 믿는다.

저자 분과 함께 재직 시절, 어느날 점심을 사주시겠다고 대신 인터뷰를 할 수 있느냐고 물어 오셨다. 당연히 흔쾌히 수락을 했지만 어떤 책을 쓰시려고 하는 건지 몹시 궁금했었다. 점심을 먹으며 나의 궁금증에 대한 즉답을 해주시진 않았던 것으로 기억하는데, 대신 개발자들이 글을 쓰는데 어려워 하는 부분, 그리고 문서나 글을 잘 쓰려면 어떤 방법이 있을 것 같은지에 대해 물어봐주셨고, 나름 성실히 답을 해드렸다. 그리고 이런 질문과 답이 오가는 과정에서 어떤 책을 쓰시려고 하는지 예상이 되었고, 그 예상이 결국 맞았다.

아무튼 이런 저런 사연은 이제 그만하고 책의 내용이나 내가 느낀 점에 대해 말을 해보겠다. 아직 대략 반 정도를 읽은 상태이긴 하나 책의 구성이나 책의 내용, 그리고 무엇보다 각 챕터마다 주제가 되는 내용을 서술해 가는 과정이 견고하고 이해가 쉽도록 되어 있다. 또한 내용이 정말 이해하기 쉽게 서술되어 있어서 읽으면서도 아 맞아 내가 이 부분은 부족했는데, 이렇게 해보면 되겠다는 생각을 바로 바로 떠올릴 수 있었다. 예를 들면 OAS와 Swagger을 사용할 때, 사용할 수 있는 어노테이션(Annotation)에 대한 설명 그리고 도구를 전적으로 믿고 놓칠 수 있는 부분 모두를 간결하면서도 정확하게 전달해주는 부분이 많은 도움이 되었다.

남은 부분들을 읽어 나가면서 내가 부족한 부분이나 전혀 생각하지 못했던 부분들을 발견하고 이 책의 도움을 받아 어떻게 해결할 수 있을지에 대해 기대가 된다. 그리고 이 책을 준비하시면서 얼마나 많은 노력을 하셨을지 가늠이 되지 않는다. 정말 많은 감사를 드릴 뿐이다. 많은 분들이 읽었으면 하고 비단 개발자뿐만 아니라 다른 직군의 분들에게도 많은 도움이 될 수 있는 책이라고 생각한다.

아 한가지 빠뜨린 부분이 있는데, 아마 저자를 아시는 분들은 느끼셨을 것 같다. 책의 중간 중간 주석과 같은 형태로 남겨 놓으신 짧은 문장에서 그 부분의 유머가 느껴진다. :)

구매 링크

CTO로서의 첫 2주 기록

· 7 min read
Ryukato
BackEnd Software Developer

들어가며

현재 회에 합류한 뒤 지난 2주 동안은 조직·프로세스·기술 구조를 빠르게 이해하고 기반을 정비하는 일에 집중했습니다.
CTO로서 ‘지금 무엇을 파악해야 하고, 무엇부터 정비해야 하는가’라는 기준으로 접근했고, 그 과정에서 느낀 점과 실제로 진행한 작업들을 정리해 보았습니다.


프롬프트 체이닝은 항상 정답일까?

· 10 min read
Ryukato
BackEnd Software Developer

LLM을 활용하여 텍스트에서 키워드를 추출하거나 정보를 구조화할 때, 막연히 **프롬프트 체이닝(Prompt Chaining)**이 더 좋은 결과를 만들어줄 것이라 기대보다는 테스트를 통해 단일 프롬프트(single prompt) 방식과 비교 분석하여 어떤 방법이 자신의 상황에 더 적합한지 선택하는 것이 좋을 것으로 생각됩니다.


쿠팡 일용직 퇴직금 미지급 사태를 보며

· 16 min read
Ryukato
BackEnd Software Developer

쿠팡 풀필먼트서비스(CFS)에서 불거진 퇴직금 미지급 사태는 표면적으로는 “퇴직금을 주지 않았다”는 문제로 보일 수 있다.
하지만 조금만 더 들여다보면, 단순히 돈을 떼먹은 사건이 아니라 취업규칙을 악의적으로 개정하고, 그 사실을 근로자들에게 알리지도 않은 구조적 문제라는 점에서 심각성이 훨씬 크다.


RAG Embedding server 구현해보기

· 7 min read
Ryukato
BackEnd Software Developer

이 포스팅에서는 직접 구현한 FastAPI 기반 RAG Embedding Server의 구조, 동작 방식, 설계 의도 등을 설명합니다.
해당 서버는 Qdrant를 벡터 스토어로 사용하며, 테스트 데이터를 기반으로 한 임베딩을 비동기 방식으로 처리합니다.

전체 코드는 rag_embedding_server 에서 확인 가능합니다.

Airflow에서의 heavy data 처리에 대한 단게적 개선

· 8 min read
Ryukato
BackEnd Software Developer

외부 데이터 소스로부터 가져온 raw-data들에 대해 중복 데이터 제거 및 데이터 셋간의 관계 설정 및 데이터 클랜징 처리등의 것들을 하면서 대용량 데이터의 처리에 대해 단계적으로 개선한 내용을 간략히 정리하여 공유 합니다. 본 글의 내용은 성능 병목을 개선하기 위한 단계별 전략을 일반적인 케이스로 정리한 가이드입니다. 각 단계는 실제로 성능 향상에 효과적인 접근법을 순차적으로 나열한 것입니다.

모 기업의 스톡옵션 가치 무효화에 대한 단상

· 8 min read
Ryukato
BackEnd Software Developer

최근 업계에서 주목받는 한 기업이, 직원들에게 부여했던 스톡옵션의 가치를 ‘0원’으로 평가해 논란이 되고 있다.해당 기업은 차액보상형 스톡옵션(* 행사시점 주가와 행사가격의 차이만큼 현금으로 보상하는 방식)을 운영하고 있었던 것으로 보이며, 내부적으로 시가를 0원으로 산정해 실질 보상이 없는 구조가 되어버렸다. 그러나 해당 기업의 스톡옵션에 대한 평가 방식이 정말 합당한 것인지 의문이 든다. 이슈의 내용을 정리한 기사에 따르면 해당 기업은 시리즈 B-1 투자 유치를 하여, 기업 가치 5,000억원정도로 평가 받고, 이에 따라 주당 가격 또한 15만 208원정도로 평가 받았다고 한다. 해당 기업은 다음과 같은 사유로 ‘0원’의 시가 평가를 정당화했다고 주장한다:

  • 특정인 간 일회성 거래
  • 전체 발행주식 총수의 2%에 불과한 소규모 거래
  • 최신 감사보고서 기준, 상속세 및 증여법 상 평가액이 0원이라는 법률 자문

정말 합당한 것일까?

Entity vs DTO 논쟁의 본질: 순수성인가, 실용성인가?

· 12 min read
Ryukato
BackEnd Software Developer

소프트웨어 설계에서 자주 반복되는 논쟁 중 하나는 바로 Entity를 Presentation Layer에서 사용하는 것이 적절한가? 하는 질문이다. 이 논쟁은 단순히 기술적인 분리가 옳으냐를 넘어서, 실용성과 유지보수 비용, 협업의 복잡성, 그리고 조직적 일관성이라는 주제를 포함하고 있다.

이 글에서는 실용주의 관점에서 Entity와 DTO 사이의 경계를 바라보고, 그에 따른 trade-off를 분석하고자 한다.


About Sharding and MSA

· 13 min read
Ryukato
BackEnd Software Developer

🧭 서문

많은 팀과 조직이 데이터베이스의 성능 병목이나 대용량 처리 이슈에 직면했을 때, 가장 먼저 떠올리는 해결책 중 하나가 바로 **샤딩(Sharding)**입니다.
샤딩은 분명히 강력한 수평 확장 전략이지만, 그만큼 도입과 운영에 따르는 복잡도와 위험성도 큽니다.

특히, 샤딩은 단순한 기술적 기능이 아니라 전체 시스템 아키텍처에 영향을 주는 구조적 결정이기 때문에, 도입을 서두르기보다는 샤딩 없이 해결할 수 있는 방안들을 먼저 고려하고, 샤딩이 필요한 시점과 범위를 명확히 판단한 후에 적용하는 것이 바람직합니다.

이 글에서는 샤딩 도입 전 고려할 수 있는 단계, 샤딩의 적용 원칙, MSA 및 vertical slicing과의 관계, 그리고 궁극적으로 나노 서비스 아키텍처와 BFF 구조로의 확장까지 폭넓게 다뤄봅니다.


주 52시간 근무 시간 제도에대한 생각

· 15 min read
Ryukato
BackEnd Software Developer

최근 링크드인 등 SNS에서는 주 52시간 근무제에 대해 부정적인 의견이나, 근무 시간 제도의 자율화를 주장하는 글들을 자주 접하게 된다. 이들 중 상당수는 “지인과 대화해보니 한국 제도는 너무 경직돼 있다”, “회사를 차리면 더 유연한 근로를 허용하겠다”, “젊을 때는 바짝 일하고 나중에 워라밸을 챙기면 된다”는 식의 경험적 혹은 이상적인 주장을 바탕으로 하고 있다.

그러나 이러한 담론에서는 주 52시간 제도가 만들어진 배경이나, 제도의 존재 목적, 사회적 완화 시 발생 가능한 구조적 비용 등에 대한 깊이 있는 고민은 거의 보이지 않는다. 더 나아가, 제도 때문에 기업 성장이 저해된다는 주장조차도 구체적인 데이터나 논리적 근거 없이 반복되곤 한다.

그렇다면 이러한 주장들을 단순한 일부의 개인 의견으로 치부해야만 할까? 솔직히 이 지점에서 나 또한 회의감과 함께 고민이 생긴다. 이 글은 그 고민에서 출발한다.

Domain & Infra model separation

· 7 min read
Ryukato
BackEnd Software Developer

이 문서는 TestDomainItem을 예시로 하여, 도메인 모델과 MongoDB용 인프라 모델을 분리하는 과정을 단계별로 정리한 것입니다.
각 단계에서는 선택된 구조의 장단점 및 고려할 트레이드오프도 함께 포함되어 있습니다.


About SAGA Pattern

· 9 min read
Ryukato
BackEnd Software Developer

✅ Saga 패턴이란?

마이크로서비스 아키텍처(MSA) 환경에서 분산된 서비스 간의 트랜잭션 정합성을 보장하기 위해
각 서비스의 로컬 트랜잭션 + 보상 트랜잭션을 결합하여
전체 트랜잭션의 일관성을 유지하는 패턴이다.