그 서비스, 진짜 일하고 있나요? (싱크홀 안티패턴)

석현·2025년 6월 4일
0

Insight

목록 보기
40/43

요즘 팀원들과 코드 리뷰를 하다 보면, 단순히 레이어를 나눴다는 이유로 안심하고 넘어가는 경우가 꽤 많습니다. 그런데 정말 제대로 나눈 걸까요? 이번 글에서는 우리가 흔히 사용하는 레이어드 아키텍처(Layered Architecture) 구조에 대해 다시 생각해보고, 실무에서 종종 마주치는 싱크홀 안티패턴(Sinkhole Anti-pattern)에 대해서도 정리해보려고 합니다. 모두가 사용하는 구조이기에, 더 잘 쓰는 게 중요하다고 생각해요.


✅ 레이어드 아키텍처란?

레이어드 아키텍처는 말 그대로 소프트웨어를 '층'으로 나눠 관심사를 분리한 구조입니다. 관심사라는 건 같은 목적이나 책임을 가진 로직들을 하나의 묶음으로 본다는 개념입니다. 대표적으로 아래와 같은 3계층 구조가 가장 많이 사용되죠:

  • Presentation Layer (표현 계층): 사용자 요청을 받아들이고 응답을 반환합니다. 주로 Controller가 위치합니다.
  • Domain Layer (도메인 계층): 핵심 비즈니스 로직이 들어갑니다. Service나 UseCase가 주로 이 계층에 속합니다.
  • Data Source Layer (데이터 소스 계층): 데이터베이스, 외부 API 등 실제 IO 작업이 일어나는 영역입니다. Repository, DAO 등이 여기에 해당합니다.

이렇게 계층을 나누면 어떤 장점이 있을까요?

  • 변경이 한 곳에만 영향을 줌 → 유지보수에 강함
  • 각 계층을 따로 테스트 가능 → 테스트 전략 수립이 쉬움
  • 역할 분리가 명확 → 가독성과 협업 효율이 높아짐

하지만, '나눈다'는 건 결국 추상화의 문제입니다. 추상화는 잘하면 명확한 구조가 되지만, 잘못하면 '불필요한 중간 과정'만 늘어나는 결과가 됩니다.


⚠️ 싱크홀 안티패턴이란?

싱크홀(Sinkhole) 안티패턴은 이름처럼 요청이 중간 계층에서 그냥 '빠져버리는' 현상이에요. 즉, 중간 계층(주로 Service나 UseCase)이 아무 일도 안 하면서 하위 계층으로 요청을 단순 전달만 하는 구조입니다.

예를 들어:

@Service
public class OrderService {
    private final OrderDao orderDao;

    public OrderService(OrderDao orderDao) {
        this.orderDao = orderDao;
    }

    // 그냥 DAO 호출만 하는 경우
    public Order getOrder(Long id) {
        return orderDao.getById(id);
    }
}

겉으로 보면 문제 없어 보이지만, 실은 이 Service 계층은 존재 의미가 없습니다. 이건 추상화가 아니라 불필요한 중계일 뿐이에요.

CPU와 메모리는 결국 이 "아무 일도 안 하는 코드"를 실행하느라 낭비되고, 코드 구조는 더 복잡해지고, 팀은 이 계층이 무슨 역할을 하는지 혼란스러워합니다.


그럼 이건 무조건 없애야 할까?

꼭 그렇지도 않습니다. 오히려 서비스 계층이 비어 있어도 괜찮은 경우도 있습니다:

  • 향후 확장될 여지가 있거나
  • 팀의 아키텍처 원칙상 일관성을 유지하기 위해서라면

중요한 건 "일관성 없는 예외"를 만드는 것이지, "초기에는 비어 있더라도 명확한 역할이 부여된 계층"은 오히려 팀에게 방향성을 줍니다.


그럼 어떻게 할까? (내 생각)

저는 팀원들에게 아래와 같이 리뷰를 남기곤 합니다:

"이 Service는 지금 역할이 없습니다. 그대로 두어도 되지만, 꼭 필요한 계층인지 한 번 다시 고민해보면 좋겠습니다."

"지금은 단순 전달만 하지만, 추후 Validation이나 Logging 로직이 붙는다면 이 위치가 적절할 수 있겠네요."

아래는 싱크홀에서 벗어난 Service 예시입니다:

@Service
public class OrderService {
    private final OrderDao orderDao;
    private final PaymentValidator paymentValidator;

    public OrderService(OrderDao orderDao, PaymentValidator validator) {
        this.orderDao = orderDao;
        this.paymentValidator = validator;
    }

    public Order getValidatedOrder(Long id) {
        Order order = orderDao.getById(id);
        if (!paymentValidator.isValid(order)) {
            throw new IllegalArgumentException("유효하지 않은 결제 정보입니다.");
        }
        return order;
    }
}

또는 다음과 같은 형태도 많죠:

@Service
public class EmailSenderService {
    private final MailProviderClient client;

    public void sendWelcomeEmail(User user) {
        String body = "Welcome, " + user.getName();
        client.sendEmail(user.getEmail(), body);
    }
}

이런 코드처럼 비즈니스 중심의 책임이 명확하게 포함되면, 해당 계층은 더 이상 단순 전달이 아니라고 생각합니다.


정리하면...

싱크홀 안티패턴은 단순히 'Service가 비어있다'가 아니라, 의미 없는 계층이 남용되는 현상을 말합니다. 이를 방지하기 위해선 다음 기준을 명확히 가져가야 합니다:

  • 이 계층이 하는 일은 무엇인가?
  • 향후 확장성은 어떻게 되는가?
  • 정말 이 로직이 여기에 있어야만 하는가?

결국 구조는 팀을 위한 약속입니다. 단순히 패턴을 따라가는 것이 아니라, 팀이 이해하고 유지할 수 있는 구조를 설계하는 것이 진짜 실무 아키텍처라고 생각해요.


profile
Learner

0개의 댓글