[생각정리] Common Layer에 대한 생각

jeyong·2025년 9월 19일
0

공부 / 생각 정리  

목록 보기
105/105

지금까지 여느 글과 마찬가지로, 이번 글도 내가 개발하면서 새롭게 깨달은 부분들을 정리해두려는 기록이다. 이번 주제는 Common Layer이다.

앞서 작성했던 패키지 구조 글에서 이어지는 이야기인데, 계층 기반 아키텍처를 적용해 나가면서 자연스럽게 떠오른 고민들을 정리해보고 싶다.

들어가며

지난번 패캐지 구조 글에서

덕분에 exception, dto처럼 계층 간에 공유가 필요한 클래스들을 어디에 둘지에 대한 실제적인 고민이 생겼고, 그 고민 자체가 꽤 신선한 경험이었다. 예전에는 별다른 기준 없이 “공통이니까 common에 넣자”는 식의 습관적인 선택이 많았다면, 이번에는 클래스의 책임이 어느 계층에 더 가까운지, 어디까지 사용될지를 명확히 따져본 뒤 위치를 결정했다.

라고 언급한 적이 있다. 이번 글에서는 그 부분을 더 깊게 파고들어 기록해 두려고 한다.

인사이트

기존에 exception은 우리 프로젝트에서 아래와 같이 정의되어 있었다.

com.ahmadda
├── application
│   └── exception
│       ├── BusinessFlowViolatedException.java
│       ├── AccessDeniedException.java
│       ├── UnauthorizedException.java
│       ├── NotFoundException.java
│       └── ...
│
├── domain
│   └── exception
│       ├── BusinessRuleViolatedException.java
│       ├── UnauthorizedOperationException.java
│       ├── BlankPropertyException.java
│       ├── NullPropertyException.java
│       └── ...
│
├── infra
│   └── exception
│       ├── InvalidJwtException.java
│       ├── InvalidOauthTokenException.java
│       └── ...
│
├── presentation
│   ├── EventController.java
│   └── ...

책임 분리 관점에서는 굉장히 명확하다. 하지만 실제 개발 과정에서는 몇 가지 문제에 부딪혔다.

  • BusinessRuleViolatedException이 application 계층에서 쓰이고 있다는 리뷰가 반복적으로 발생했다.
  • RestControllerAdvice에서 해당 예외가 잡혔는지, Swagger 문서화 대상인지 매번 헷갈렸다.
  • 결국 예외의 책임과 전달 경로를 검증하는 데 지나치게 많은 비용이 들었다.

이러한 문제를 해결하기 위해 리펙토링 데이를 진행했다.

리팩토링 데이는 우아한테크코스 lv3이 끝난 뒤 주어진 방학 기간 동안 하루를 정해 리팩토링에만 몰두하는 행사였다. 내가 주최했는데, 다시 lv4가 시작되면 사용자 가치 제공이 우선순위가 될 것이고 이런 리팩토링은 뒤로 밀릴 것 같았기 때문이다. 다행히도 백엔드 팀원 모두 흔쾌히 동의해주었고, 덕분에 file changed가 183개에 달하는 큰 변화를 시도할 수 있었다. 개인적으로도 낭만적인 경험이었다. 이름만 들어도 설레는 “리팩토링 데이”라는 날에, 팀원들과 함께 설계를 새롭게 정리하는 시간이라니.

리팩토링의 결과, 우리는 common 패키지를 도입했다.

com.ahmadda
├── common
│   └── exception
│       ├── ForbiddenException.java
│       ├── NotFoundException.java
│       ├── UnauthorizedException.java
│       ├── UnprocessableEntityException.java
│       └── ...

여기서 눈에 띄는 부분은 예외 클래스명이 HTTP 상태 코드와 일치한다는 점이다. 이는 기존의 계층 기반 예외 정의와는 완전히 다른 접근법이다.

  • 기존 방식: 책임 분리 중심. 각 계층별로 세분화된 예외 정의.
  • 새로운 방식: 실용주의 중심. 공통 계층에서 HTTP 상태 코드 기반 예외 정의.

즉, common에 있는 예외를 사용한다는 것은 RestControllerAdvice에서 잡혀 클라이언트에 전달될 것을 전제로 한다. 말하자면, “이 예외는 클라이언트에게 의미 있는 메시지로 포장되어야 한다”라는 의도를 코드 구조로 드러낸 셈이다.

반대로, 단순히 내부 제약을 표현하거나 로그를 남기면 충분한 경우라면 각 계층에서 RuntimeException을 직접 던지거나 별도의 예외를 정의해도 된다. 결국 common 예외는 실용주의 관점에서 “API 응답으로 전환하기 적합한 예외”라는 신호다.

물론 문제도 있다. 이렇게 되면 사실상 exception이 웹에 의존적인 성격을 띠게 된다. 그리고 이는 domain, infra, application에서 common 예외를 직접 던진다는 것은 웹이라는 환경이 코드 전반에 침투한 것과도 같다.

팀 내부에서는 이 부분을 논의한 끝에, 필요성이 명확해질 때 다시 점검하기로 했다. ForbiddenException이나 NotFoundException처럼 이름이 웹에 종속적으로 보이긴 하지만, 사실 웹이 아닌 환경에서도 큰 문제는 없다. 의미 자체는 충분히 전달되기 때문이다.

그리고 실제 코드도 웹 의존성을 강하게 품고 있지는 않다.

package com.ahmadda.common.exception;

public class UnprocessableEntityException extends RuntimeException {

    public UnprocessableEntityException(final String message) {
        super(message);
    }
}

HTTP 상태 코드에 의존하거나 메시지를 강제하지 않는다. 이름이 웹 친화적일 뿐, 실질적으로는 단순한 RuntimeException이다. 그렇기에 배치나 다른 환경에서도 의미적으로 크게 어색하지는 않을 거라 본다. 결국 “우리는 웹 개발자다”라는 맥락이 있기에 가능한 합리화일지도 모르겠다.

번외

현재 common 패키지에는 exception 외에는 아무것도 없다. 나는 앞으로도 이 상태를 유지하고 싶다. 흔히 말하는 “common의 저주” 때문이다.
common 패키지는 편리하다는 이유로 무언가를 무분별하게 쌓아두는 창고가 되기 쉽다. 그렇게 되면 구조적 균형은 무너지고, 코드가 여기저기 흩어져 응집력이 약해지며, 중복까지 늘어나 프로젝트는 점점 더 복잡해진다. 처음에는 빠르게 해결하려고 넣은 선택이 시간이 지나면서 발목을 잡는 구조적 부채가 되는 셈이다.

그래서 우리 팀은 앞으로도, exception처럼 모두가 납득할 만한 합리적 이유가 있을 때만 common을 채우기로 했다.

물론 앞으로도 이 구조가 언제까지 유효할지는 알 수 없다. 필요하다면 다시 해체하거나 다른 방식으로 바꿀 수도 있다. 하지만 지금 이 순간만큼은, common 패키지가 덕분에 우리는 덜 헷갈리고, 더 명확하게 문서화하며, 리뷰 과정에서도 한결 빠르게 합의할 수 있게 되었다.

0개의 댓글