Architecture & Design Pattern

(30개)

엔티티를 컨트롤러까지 올려서 filter -> remove?

결론부터 말씀드리면, 엔티티(Entity)를 컨트롤러(Controller)까지 올려서 비즈니스 로직(filter -> remove)을 처리하는 것은 절대로 피해야 할 설계 방식입니다.

이 질문의 핵심은 "왜 우리는 아키텍처를 계층(Layer)으로 나누는가?" 에 대한 이해와 맞닿아 있습니다.

이 개념을 고급 레스토랑에 비유해서 설명해 드리겠습니다.

  • 컨트롤러 (Controller): 손님의 주문을 받고, 완성된 요리를 손님에게 전달하는 웨이터

  • 서비스 (Service): 주문을 받아 요리법에 따라 조리 과정을 총괄하는 주방장(Head Chef)

  • 리포지토리 (Repository): 냉장고에서 신선한 재료를 꺼내오는 주방 보조

  • 엔티티 (Entity): 요리에 사용되는 날것의 식재료 (예: 생고기, 생선, 채소)

사용자께서 질문하신 상황은 "웨이터(컨트롤러)가 주방에서 날고기(엔티티)를 직접 들고 나와서, 손님 테이블 앞에서 어떤 부위를 쓸지 자르고(filter) 어떤 부위를 버릴지(remove) 결정하는 것" 과 같습니다. 상상만 해도 이상하고 위험한 상황이죠.

왜 이것이 잘못되었는지, 기술적인 이유를 4가지로 나누어 설명해 드리겠습니다.


  1. 역할과 책임의 위반 (Separation of Concerns, SoC)

계층형 아키텍처의 가장 기본 원칙은 각 계층이 자신의 역할과 책임에만 집중하는 것입니다.

  • 컨트롤러의 책임: HTTP 요청을 받고, 요청 데이터를 적절한 형식(DTO)으로 변환하여 서비스 계층에 전달하고, 서비스의 처리 결과를 받아 HTTP 응답으로 변환하여 클라이언트에게 전달하는 '관문(Gateway)' 역할입니다.

  • 서비스의 책임: 애플리케이션의 핵심 비즈니스 로직을 수행합니다. 여러 데이터를 조합하거나, 복잡한 계산을 하거나, 트랜잭션을 관리하는 등 '실질적인 일'을 하는 곳입니다.

  • 리포지토리의 책임: 데이터베이스에 접근하여 데이터를 저장하고 조회하는 일만 책임집니다.

filter -> remove와 같은 로직은 명백한 비즈니스 로직이므로, 서비스 계층에서 수행되어야 합니다. 컨트롤러가 이 로직을 수행하는 것은 웨이터가 주방장의 일을 하는 것과 같으며, 시스템의 구조를 무너뜨리는 행위입니다.

  1. 트랜잭션(Transaction) 문제

JPA를 사용하는 환경에서 데이터의 변경(CUD - Create, Update, Delete) 작업은 반드시 트랜잭션(@Transactional) 내에서 이루어져야 합니다.

  • 트랜잭션의 범위: 트랜잭션은 보통 서비스 계층의 메서드에 선언됩니다. 서비스 메서드가 시작될 때 트랜잭션이 시작되고, 메서드가 끝날 때 커밋(Commit) 또는 롤백(Rollback)됩니다.

  • 엔티티의 상태: 서비스 계층을 벗어난 엔티티는 더 이상 영속성 컨텍스트(Persistence Context)가 관리하지 않는 준영속(Detached) 상태가 됩니다.

  • 문제 발생: 컨트롤러에 있는 준영속 상태의 엔티티에 대해 remove를 시도하면, 트랜잭션 범위 밖이므로 아무런 효과가 없거나 예외가 발생합니다. 즉, 데이터베이스에 변경 사항이 전혀 반영되지 않습니다.

  1. 지연 로딩(Lazy Loading)과 LazyInitializationException

엔티티들은 보통 다른 엔티티와 연관관계를 맺고 있습니다 (예: Order 엔티티는 List을 가짐). 성능을 위해 이 연관관계는 대부분 지연 로딩(FetchType.LAZY)으로 설정됩니다.

  • 문제 상황: 서비스 계층에서 Order 엔티티만 조회해서 컨트롤러로 반환했다고 가정해봅시다. 이때 order.getOrderItems()는 실제 데이터가 아닌, 프록시(Proxy) 객체입니다.

  • 예외 발생: 트랜잭션이 끝난 컨트롤러에서 order.getOrderItems().stream().filter(...) 와 같이 실제 데이터에 접근하려고 시도하는 순간, 데이터베이스 세션이 이미 닫혀있기 때문에 LazyInitializationException 이라는 무서운 예외를 마주하게 됩니다.

  1. API 스펙의 오염과 DTO의 필요성

엔티티는 데이터베이스 테이블 구조와 1:1로 대응하는, 시스템의 가장 내밀한 데이터 모델입니다.

  • 엔티티 노출의 위험: 만약 엔티티를 컨트롤러에서 다루고 심지어 API 응답으로 그대로 반환한다면, DB 스키마가 변경될 때마다 API 스펙이 변경됩니다. 또한, 비밀번호나 내부 관리용 플래그 같은 민감한 정보가 외부에 노출될 위험도 있습니다.

  • DTO의 역할: DTO(Data Transfer Object)는 계층 간, 특히 컨트롤러와 클라이언트 간의 데이터 전송을 위해 사용하는 객체입니다. DTO를 사용하면 API 스펙을 내부 데이터 모델(엔티티)과 분리하여, 내부 로직이 변경되어도 API의 안정성을 유지할 수 있습니다.


올바른 설계 방식

  1. Controller: 클라이언트로부터 요청을 받습니다. (예: "주문 ID 123번에서 '반품된' 상품을 모두 삭제해주세요.")

  2. Controller -> Service: 컨트롤러는 이 요청을 해석하여, orderService.removeReturnedItems(123L); 와 같이 서비스 계층의 메서드를 호출합니다.

  3. Service:

    • @Transactional 어노테이션이 붙은 removeReturnedItems 메서드가 실행됩니다.

    • 리포지토리를 통해 Order 엔티티를 조회합니다. (orderRepository.findById(123L))

    • 조회한 Order 엔티티 내에서 OrderItem 리스트를 가져와, 비즈니스 로직(filter -> remove)을 수행합니다.

    • order.getOrderItems().removeIf(item -> item.getStatus() == OrderStatus.RETURNED);

    • 메서드가 종료되면, 변경된 내용(Dirty Checking)이 데이터베이스에 자동으로 반영(커밋)됩니다.

  4. Service -> Controller: 서비스는 처리 결과를 반환하고, 컨트롤러는 이를 바탕으로 성공/실패 응답을 클라이언트에게 보냅니다.

결론:

엔티티는 서비스 계층 안에서만 다루어져야 합니다. 컨트롤러는 엔티티의 존재 자체를 몰라야 하며, 오직 DTO를 통해 서비스 계층과 소통하는 것이 현대적인 웹 애플리케이션의 가장 기본적이고 중요한 설계 원칙입니다.

  1. 모놀리식 아키텍처(Monolithic Architecture)와 마이크로서비스 아키텍처(MSA)의 장단점을 비교 설명해주세요.

  2. MSA 환경에서 분산 트랜잭션을 어떻게 처리할 수 있을까요? (예: Two-Phase Commit, Saga 패턴)

  3. CAP 이론이란 무엇이며, 각 요소를 포기했을 때 어떤 시스템 특성을 갖게 되는지 설명해주세요.

  4. CQRS(Command Query Responsibility Segregation) 패턴은 왜 필요한가요?

  5. 이벤트 기반 아키텍처(Event-Driven Architecture)란 무엇이며, 어떤 장점이 있나요?

  6. 객체지향 설계 5원칙(SOLID)에 대해 각각 설명해주세요.

  7. GoF 디자인 패턴 중 자주 사용하는 패턴 3가지를 설명하고, 어떤 상황에서 사용했는지(또는 사용할 수 있는지) 설명해주세요.

  8. 빌더(Builder) 패턴이 생성자나 정적 팩토리 메서드에 비해 갖는 장점은 무엇인가요?

  9. 어댑터(Adapter) 패턴과 프록시(Proxy) 패턴의 차이점은 무엇인가요?

  10. API 게이트웨이(API Gateway)의 역할은 무엇인가요?

  11. 서버리스(Serverless) 아키텍처의 장단점에 대해 설명해주세요.

  12. 메시지 큐(Message Queue)를 사용하는 이유는 무엇이며, 어떤 문제를 해결할 수 있나요?

  13. 멱등성(Idempotency)이란 무엇이며, 왜 API 설계에서 중요한가요?

  14. 옵저버(Observer) 패턴과 발행/구독(Pub/Sub) 패턴의 차이점은 무엇인가요?

  15. 의존성 주입(DI)을 직접 구현한다면 어떻게 설계할 것인가요?

  16. MSA 환경에서 서비스 간 통신 방법(동기 vs 비동기)을 비교하고, 각각 어떤 경우에 적합한지 설명해주세요.

  17. 서킷 브레이커(Circuit Breaker) 패턴이란 무엇이며, 왜 필요한가요?

  18. API 설계 시, 페이징(Paging) 처리 방법(커서 기반 vs 오프셋 기반)의 장단점을 비교 설명해주세요.

  19. 좋은 로그(Log)를 남기기 위한 원칙은 무엇이라고 생각하시나요?

  20. 클린 아키텍처(Clean Architecture)의 의존성 규칙(Dependency Rule)이란 무엇이며, 왜 중요한가요?

  21. 전략(Strategy) 패턴과 템플릿 메서드(Template Method) 패턴의 차이점은 무엇인가요?

  22. 데코레이터(Decorator) 패턴과 프록시(Proxy) 패턴의 공통점과 차이점은 무엇인가요?

  23. 싱글톤(Singleton) 패턴을 구현하는 여러 가지 방법을 설명하고, 각각의 장단점을 비교해주세요. (특히 스레드 안전성 관점에서)

  24. 도메인 주도 설계(DDD)의 핵심 개념인 애그리거트(Aggregate)와 바운디드 컨텍스트(Bounded Context)에 대해 설명해주세요.

  25. 이벤트 소싱(Event Sourcing) 패턴이란 무엇이며, 어떤 장단점이 있나요?

  26. 웹 서비스의 확장성(Scalability)을 높이는 방법에는 어떤 것들이 있나요? (Scale-up vs Scale-out)

  27. 장애 허용 시스템(Fault Tolerant System)을 만들기 위한 설계 원칙은 무엇이 있을까요?

  28. 상태를 가지는(Stateful) 서비스와 상태가 없는(Stateless) 서비스의 차이점은 무엇이며, 왜 Stateless 아키텍처가 권장되나요?

  29. 팩토리 메서드(Factory Method) 패턴과 추상 팩토리(Abstract Factory) 패턴의 차이점은 무엇인가요?

  30. 컴포지트(Composite) 패턴에 대해 설명하고, 어떤 경우에 유용하게 사용할 수 있나요?

Last updated