본문 바로가기
won2dev-log
HomeArchiveTagsCategoriesAboutProjects
HomeArchiveTagsCategoriesAboutProjects
won2dev-logwon2dev-logwon2dev-log

비전공 개발자의 로그 | won2dev-log

Navigation
  • Home
  • Archive
  • About
  • Projects
Categories
  • Docs
  • TIL
  • Project
  • Automation
  • Git · GitHub
더보기
Tags
  • TIL
  • Java
  • Spring
  • Backend
  • n8n
더보기
About

기록을 거름 삼아 공유는 성장을 만든다.

LicensePrivacy
© won2dev 2026. All rights reserved.
Home›TIL›Common은 어디까지 책임져야 할까
TIL

Common은 어디까지 책임져야 할까

won2dev·2026년 07월 31일
#TIL

자세한 프로젝트 내용은 포트폴리오 — 모니 - 초보 투자자를 위한 AI 모의투자 플랫폼 에서 확인할 수 있습니다.

Common을 만들 때는 그냥 "겹치는 코드 모아두는 폴더" 정도로 생각했다. 근데 서비스를 하나씩 붙여보니, 그 폴더 안에 뭘 넣을지 정하는 게 생각보다 어려운 결정이었다.

처음엔 그냥 다 넣으면 되는 줄 알았다

User, Stock, Trade, Portfolio, Payment, AI — 서비스를 이렇게 나누고 나니까 곧바로 반복되는 코드가 보이기 시작했다. 예외 처리, 응답 포맷, Gateway 인증 필터, BaseEntity 같은 것들.

이런 걸 서비스마다 복붙해서 쓰면 나중에 반드시 문제가 생긴다. 공통 응답 형식 하나 바꾸려고 해도 서비스 다섯 개를 돌아다니면서 똑같이 고쳐야 하니까.

그래서 Common Module로 뺐다. 여기까진 별생각 없었다. "공통으로 쓰는 거니까 Common에 넣는다" — 이 판단 자체는 쉬웠다.

문제는 그다음이었다.


"공통으로 쓰는 코드"가 다 같은 게 아니었다

Common을 만들고 나서 뭔가 이상하다는 느낌이 들었던 지점이 있다. ErrorCode를 넣으려고 하는데, User Service에서 나는 에러랑 Stock Service에서 나는 에러가 당연히 다르다.

근데 이걸 Common에서 다 구현해버리면? Common이 User Service의 에러 종류를 알아야 하고, Stock Service가 에러 코드 하나 추가할 때마다 Common을 건드려야 한다. 이러면 Common을 나눈 의미가 없어진다. 오히려 서비스 간 결합만 하나 더 생기는 꼴이다.

그래서 여기서는 인터페이스만 뒀다.

java
public interface ErrorCode {

    String getCode();

    String getMessage();

    HttpStatus getStatus();
}
public interface ErrorCode {

    String getCode();

    String getMessage();

    HttpStatus getStatus();
}

Common은 User Service에 어떤 에러가 있는지 알 필요가 없다. 필요한 형태(인터페이스)만 정의하고, 실제 구현은 각 서비스가 알아서 하게 뒀다.

반대로 GatewayHeaderAuthenticationFilter, GlobalResponse, ExceptionHandlerSupport, BaseEntity 같은 건 서비스마다 달라질 이유가 딱히 없었다. 이런 것까지 인터페이스로 쪼개서 각 서비스가 다시 구현하게 하면, 그것도 이상한 일이다. 그래서 이런 건 구현체 자체를 Common에서 그대로 제공했다.

plain text
Common Module

├── GatewayHeaderAuthenticationFilter
├── GlobalResponse
├── ExceptionHandlerSupport
├── BaseEntity
└── 기타 공통 구현체
Common Module

├── GatewayHeaderAuthenticationFilter
├── GlobalResponse
├── ExceptionHandlerSupport
├── BaseEntity
└── 기타 공통 구현체

정리하면 기준은 하나였다. 서비스마다 구현이 달라져야 하면 인터페이스로 분리하고, 공통으로 동일하게 사용할 수 있으면 구현체를 그대로 제공한다.


Security에서 이 기준이 특히 중요했다

Gateway 헤더를 검증하는 필터는 어느 서비스에서 붙여도 똑같이 동작해야 한다. 그래서 필터 자체는 Common에서 구현체로 제공했다.

근데 여기서 하나 선을 그은 게 있다. Common이 필터는 제공하지만, 그 필터를 SecurityConfig 어디에 어떻게 등록할지는 각 서비스가 결정하게 뒀다.

사실 이걸 더 편하게 만들 방법도 있었다. Common에서 Security 설정을 통째로 완성해두고 이렇게 가져다 쓰는 것도 가능하다.

java
@Import(CommonSecurityConfig.class)
@SpringBootApplication
public class UserApplication {
}
@Import(CommonSecurityConfig.class)
@SpringBootApplication
public class UserApplication {
}

@Import 자체가 잘못된 방법은 아니다. Spring에서 외부 설정을 가져오는 정상적인 방법이고, 상황에 따라 쓸 수도 있다. 근데 이번엔 안 썼다. 이유는 @Import 그 자체가 아니라, 이걸 쓰는 순간 Common이 각 서비스의 애플리케이션 구성 방식까지 결정하게 된다는 게 걸렸기 때문이다.

Component Scan도 같은 이유로 피했다.

java
@ComponentScan({
    "com.moni.common",
    "com.moni.user"
})
@ComponentScan({
    "com.moni.common",
    "com.moni.user"
})

이렇게 하면 Common이 User Service 패키지 구조를 알고 있어야 한다. 서비스 구조가 바뀌거나 새 서비스가 추가될 때마다 Common의 Scan 설정도 같이 봐야 한다는 뜻이다. MSA에서는 서비스가 독립적으로 변경될 수 있어야 하는데, Common이 서비스의 패키지 구조를 알고 있으면 Common과 각 서비스 사이에 불필요한 결합이 생긴다.

사실 이 얘기를 팀 안에서 먼저 꺼낸 것도 나였다. 이전 프로젝트에서 딱 이런 식으로 Scan 범위를 넓혀서 Import하는 방식을 쓴 적이 있는데, 그때 겪은 게 있어서였다. 거의 모든 서비스가 Common에 이런 식으로 얽혀있다 보니, 나중에 구조를 나누려고 뜯어보면 어디서부터 손대야 할지부터 막막했다. 의존이 사방에 퍼져있어서 정리하는 데만 시간이 너무 많이 들었다.

그래서 이번엔 처음부터 초점을 다르게 잡았다. "멀티레포로 갈 거냐 말 거냐"를 지금 결정하자는 게 아니라, Common을 처음부터 독립적으로 라이브러리화할 수 있는 구조로 만들어두자는 거였다. 그렇게만 해두면 나중에 실제로 멀티레포 전환이 필요해지는 시점이 와도, 그건 그냥 Common을 라이브러리로 패키징해서 배포하는 문제로 끝난다. 서비스마다 얽힌 의존을 하나하나 풀어야 하는 일이 안 생긴다는 뜻이다. @Import나 Component Scan으로 서비스를 끌어들이는 방식을 피한 것도 결국 이 기준에서 나온 판단이다.

그래서 Common은 딱 여기까지만 하기로 했다. 공통 기능을 제공하는 것까지. 그걸 어떻게 조합하고 어디에 등록할지는 서비스 몫으로 남겼다.

JPA도 똑같은 기준으로 나눴다. BaseEntity처럼 다 같이 쓰는 건 Common에서 제공하고, 각 서비스의 Entity, Repository, DB 구성은 건드리지 않았다.


그렇다고 다 인터페이스로 만든 것도 아니다

처음엔 "공통 모듈이니까 다 추상화해야 하는 거 아닌가" 하는 생각도 잠깐 했다. 근데 그렇게 하면 오히려 코드가 복잡해진다.

GlobalResponse나 ExceptionHandlerSupport처럼 애초에 서비스마다 달라질 이유가 없는 것까지 인터페이스로 만들고 각 서비스에서 구현하게 하면, 그건 그냥 일을 늘리는 거다. 반대로 ErrorCode처럼 서비스마다 다를 수밖에 없는 걸 Common에서 특정 구현체로 박아버리면 그건 결합이 생긴다.

그래서 기준을 "공통으로 쓰느냐"가 아니라 "이 구현이 서비스마다 달라질 가능성이 있는가"로 잡았다. 말로 하니 간단해 보이는데, 막상 하나하나 넣으면서 이 코드는 어느 쪽인지 판단하는 데 생각보다 시간이 걸렸다.


나중에 멀티레포로 가도 이 구조면 괜찮을 것 같다

앞에서 얘기한 것처럼 애초에 초점을 멀티레포 전환 자체가 아니라 Common의 라이브러리화에 뒀던 거라, 지금은 멀티모듈로 관리하고 있어도 나중에 서비스를 멀티레포로 쪼갠다면 Common도 별도 라이브러리로 가볍게 뺄 수 있을 것 같다.

공통 구현체는 라이브러리로 패키징하고 각 서비스가 의존성으로 받으면 되고, ErrorCode처럼 서비스마다 다른 부분은 지금처럼 각자 구현하면 된다. 아직 실제로 멀티레포로 옮겨본 건 아니라서 이게 진짜 매끄럽게 될지는 장담 못 하겠다. 처음부터 공통/서비스별을 나눠뒀으니 수정 범위가 크진 않을 거라고 예상만 하는 중이다.

다만, 사실 멀티레포가 좋은지 모노레포가 좋은지는 아직도 헷갈린다. 요즘은 또 모노레포가 추세인 것 같기도 하고, 잘 모르겠다. 구글 같은 경우엔 코드 대부분을 하나의 모노레포에서 관리한다고 한다. 그래서 "언젠가는 멀티레포로 가야 한다"는 전제 자체가 맞는 건지도 사실 확신은 없다. 다만 지금 구조는 멀티레포로 가도 괜찮고 계속 모노레포로 가도 크게 손해 볼 게 없는 방향이라, 어느 쪽이든 아직 유효한 선택이라고 생각하고 있다. 결국 정답이 정해져 있다기보다는, 우리 상황이나 프로젝트 규모에 따라 그때그때 적용을 고민해보는 게 맞는 것 같다.


정리

처음 시작할 때는 Common을 그냥 "여러 서비스가 같이 쓰는 코드 모아두는 곳" 정도로 생각했다. 근데 만들면서 보니 그렇게 단순하지 않았다.

서비스마다 달라지는 부분까지 Common이 가져가면 결합이 생기고, 반대로 다 인터페이스로 쪼개면 필요 없는 복잡도가 생긴다. 공통 기능을 제공하는 것과 서비스의 애플리케이션 구성을 대신 해주는 것도 다른 문제였다.

그래서 이번엔 이렇게 정리했다.

Common은 공통 기능을 제공하고, 서비스는 자신의 상황에 맞게 그것을 조립한다.

결국 중요한 건 Common에 코드를 얼마나 넣었느냐가 아니라, 뭘 공통화하고 뭘 각 서비스 책임으로 남길지를 정하는 기준이었다. 이 기준을 세우는 데 시간이 좀 걸렸는데, 지금 와서 보면 그 시간이 아깝진 않았던 것 같다.

참고 자료

  • Spring 공식 문서 — @Import (Spring Framework 6.2)
  • Spring 공식 문서 — @ComponentScan (Spring Framework 6.2)
  • microservices.io — Essential characteristics of the microservice architecture: independently deployable
  • Potvin, R., Levenberg, J. — Why Google Stores Billions of Lines of Code in a Single Repository, Communications of the ACM, 2016

공유하기
이전 글Docker는 알겠는데, Kubernetes는 왜 필요한가다음 글 Refresh Token 재발급에서 Race Condition을 마주쳤다

목차

  • 처음엔 그냥 다 넣으면 되는 줄 알았다
  • "공통으로 쓰는 코드"가 다 같은 게 아니었다
  • Security에서 이 기준이 특히 중요했다
  • 그렇다고 다 인터페이스로 만든 것도 아니다
  • 나중에 멀티레포로 가도 이 구조면 괜찮을 것 같다
  • 정리

카테고리

TIL

태그

#TIL

최근 글

어드민은 왜 따로 분리했나 — Okta OIDC와 Thymeleaf로 어드민 서비스 구성한 이유레이어드와 헥사고날, 하나를 고르지 않아도 된다Terraform을 처음 써보고 인프라가 달라 보이기 시작했다머지는 늦게, 배포는 먼저 — stage 브랜치 CI/CD 설계Docker는 알겠는데, Kubernetes는 왜 필요한가