| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- 로그백
- OOP
- string
- 단위테스트
- springsecurity
- JPA
- DDD
- 문제선택근거
- MVC
- 디스크i/o
- AOP
- TDD
- JDBC
- 이벤트핸들러등록
- model
- 설정세팅
- SpringLegacy
- 클린아키텍처
- Ajax
- SEQUENCE
- MVC요청플로우
- 전전긍긍
- 냄새라도
- 공통기술
- Transaction
- 이벤트핸들러this
- 공통규약
- 사면초가
- 커뮤니티서버
- index
- Today
- Total
개발이군고구마
[MSA] 공통으로 사용하는 컴포넌트의 API 설계 방법 본문
각 모듈별 개발이 얼추 완료가 된 이후 놓치고 있었던 부분이 떠올랐다. 바로 마이페이지.
해당 컴포넌트를 생각하다보니
겹쳐지는 모듈 (여기서는 Point) 과 각각의 컨텍스트에 대한 조회를 어떻게 마이페이지 api 와 연결할 것인가 고민이 되었다.
1. 많은 컨텍스트가 침범하는 컨텍스트
우선 아래는 많은 모듈들이 함께 사용하고 있는 "포인트" 컨텍스트이다.
다행이 해당 컨텍스트는 각각의 컨텍스트와 분리되어 운용하여 독립적으로 사용이 가능하다.

그렇다면 각 컨텍스트는 트랜잭션이 있을 때 포인트 컨텍스트 트랜잭션 처리를 해줘야한다.
이때 중요한 것이 각 컨텍스트의 침범 없이 느슨하게 결합도를 최대한 줄여야 한다는 것인데, 어떻게 설계하면 좋을까?
느슨한 조합
interface

이렇게 설계를 잡게 되면
- 실제 Point 를 사용해야하는 컨텍스트에서 (Order)
- Client를 인터페이스를 주입 받고 (인터페이스 느슨한 결합) outport? 해당 클라이언트를 호출하게 되면
메인 컨텍스트와 경계가 겹쳐진 컨텍스트 사이가 느슨하게 조합되게 된다.
✅ 즉 챌린지/주문은 포인트에서 무슨 일이 일어나는지 모름!
private final PointSpendClient pointSpendClient;
private void processSideEffect(SingleOrderCommand command, Order savedOrder) {
pointProductService.decreaseSingleItemStock(command.orderItemId(), command.quantity());
String itemName = savedOrder.getOrderItems().getFirst().getItemSnapshot().getItemName();
pointSpendClient.spendPoints(
new PointSpendRequest(command.memberId(), savedOrder.getTotalPrice(), savedOrder.getId(), itemName + "교환")
);
}
[본격 비교 - 트랜잭션 스크립트를 사용하고 있는 현 사이트]

예를 들어 주문이 들어간다고 해보자.
주문 서비스 로직
- 상품 재고 조회 -> 감소
- 회원에 대한 포인트 조회 -> 포인트 추가
단순 명료, 빠르게 기능 구현이 가능함.
💥 이게 왜 문제가 되나? 높은 결합도
- DIP 는 지켜지긴 하지만, 서비스 인터페이스의 범위가 넓어지면 컨텍스트 경계가 느슨해짐
- 모든 클라이언트가 동일한 서비스 인터페이스를 이용하기 때문에 하나의 도메인에 다양한 요구사항이 섞임
2. 많은 컨텍스트가 모이는 마이페이지 컴포넌트 설계
1. 단일 API - facade 패턴
ㄴ 모놀리식 또는 단일 마이크로 서비스 환경
1) 방법
- 하나의 백엔드 애플리케이션 안에 “조합(혹은 Facade) 서비스” 계층을 두고, Point/Challenge/Member 모듈을 내부에서 직접 호출
- 프론트엔드 → /api/user/mypage 단일 엔드포인트
public class MypageController {
private final MypageService mypageService;
마이페이지 메인 조회 API
@GetMapping("/api/user/mypage")
public ResponseEntity<MypageMainDto> getMypageMain() {
MypageMainDto mypageMainDto = mypageService.getMypageMain();
return ResponseEntity.ok(mypageMainDto);
}
}
@Service
@RequiredArgsConstructor
public class MypageService {
private final ChallengeService challengeService;
private final PointService pointService;
public MypageMainDto getMypageMain() {
List<ChallengeDto> challengeMypageResponseDto = challengeService.getChallendgesByUserIdForMypage(user.getId());
List<PointDto> pointMypageResponseDto = pointService.getPointByUserIdForMypage(user.getId());
MypageMainDto mypageMainDto = new MypageMainDto(challengeMypageResponseDto, pointMypageResponseDto);
return new MypageMainDto(); // 임시 반환
}
}
조합 (다만 서비스가 직접 의존되어 있음)
2. 얇은 (모듈별) API 프리젠테이션 레이어 - BFF 패턴
ㄴ 다중 마이크로 서비스 환경

1) 방법
- 도메인별 서비스는 독립 마이크로서비스(또는 모듈)로 두고, 별도의 BFF 레이어
- 클라이언트 → BFF 서비스 (도메인 API호출 인터페이스 = 프리젠테이션 레이어) → 도메인 서비스(Point/Challenge/Member)
@RestController
@RequestMapping("/api/mypage")
public class MypageBffController {
private final MypageBffService svc;
public MypageBffController(MypageBffService svc) {
this.svc = svc;
}
@GetMapping
public ResponseEntity<MypageResponseDto> get(@AuthenticationPrincipal User user) {
return ResponseEntity.ok(svc.fetchForUser(user.getId()));
}
}
@Service
public class MypageBffService {
private final PointClient pointClient;
private final ChallengeClient challengeClient;
private final MemberClient memberClient;
public MypageBffService(PointClient p, ChallengeClient c, MemberClient m) {
this.pointClient = p;
this.challengeClient = c;
this.memberClient = m;
}
public MypageResponseDto fetchForUser(Long memberId) {
// (1) 비동기 병렬 호출 고려 가능
PointSummaryDto point = pointClient.getSummary(memberId);
List<ChallengeDto> challenges = challengeClient.getParticipations(memberId);
MemberInfoDto info = memberClient.getInfo(memberId);
// (2) 클라이언트별 가공 로직—예: 모바일은 포인트 요약만, 웹은 세부내역까지
return new MypageResponseDto(point, challenges, info);
}
}
@FeignClient(name = "point-service", url = "${services.point.url}")
public interface PointClient {
@GetMapping("/api/points/{memberId}/summary")
PointSummaryDto getSummary(@PathVariable("memberId") Long memberId);
}
@FeignClient(name = "challenge-service", url = "${services.challenge.url}")
public interface ChallengeClient {
@GetMapping("/api/challenges/{memberId}/participations")
List<ChallengeDto> getParticipations(@PathVariable("memberId") Long memberId);
}
@FeignClient(name = "member-service", url = "${services.member.url}")
public interface MemberClient {
@GetMapping("/api/members/{memberId}")
MemberInfoDto getInfo(@PathVariable("memberId") Long memberId);
}
인터페이스에 삽입된 api 를 각각 모듈별로 호출하게 함
3. 단일 API + (api 호출 없는) BFF 패턴

1) WHAT (고려한 사항)
- Point/Challenge/Member 각각은 엄격한 바운디드 컨텍스트로 유지하고 싶다. (즉, 서비스 객체의 침범도 하지 않게)
- MyPage는 애플리케이션 레이어의 Use‐Case(조합)로 구현
- 프론트엔드엔 단일 API(/api/user/mypage)만 제공
- 그러나 도메인 별로 독립 모듈로 둘 정도로 서비스의 규모가 크지 않다. 현재 모놀리식 아키텍처 따르고 있음
2) WHAT & WHY (왜 문제인가)
서비스를 구체 클래스로 만들어서 쓰고 있는 컨텍스트가 많다.
3) HOW (어떻게 해결할 것인가)
- BFF 패턴 인터페이스를 만들되 (마이페이지용) API는 연결하지 않는다
- 마이페이지는 조합facade 계층이지만 서비스를 의존받지 않는다. 대신 인터페이스를 의존 받고 거기서 서비스를 호출한다.
- 그 외 모듈별 상세 화면은 컨텍스트 api를 호출한다. 즉, 마이페이지는 모듈의 기능을 "조합" 하는 것에만 집중한다.
4) HOW & WHY (이게 최선인가)
✅ PointService를 직접 주입하는 것과 비교하여
- DIP 원칙 준수
- 고수준 모듈 ( 무엇을 할지 - 챌린지/상품) -> 저수준 모듈(어떻게 할지 - 포인트 비즈니스 로직) 의존이 아니라 둘 다 "추상(인터페이스)" 에 의존하고, 구현체는 런타임에 주입하는 방식
- 외부/내부 의 경계가 명확하게 유지됨
- 컨텍스트 경계가 명확
- 챌린지와 상품은 포인트의 세부를 전혀 몰라도 됨
- PointEarnClient, PointSpendClient 등 역할별 인터페이스로 책임을 분리 -> SRP
- 교체 확장 용이
- 나중에 포인트 적립 정책이 바뀌면 adaptor만 수정하면 됨 -> OCP
- 테스트 편의성
- mock 인터페이스를 주입하게 되면 챌린지/상품 비즈니스 로직 단위 테스트 깔끔해짐
❎ BFF MSA 구조와 비교하여
- 완전히 API를 분리하여 마이페이지는 단순히 마이페이지에 필요한 API만 모아놓은 Facade는 아니긴 함
- 결국 인터페이스도 -> 서비스를 구현하기 때문 (완전한 web 레이어가 아니라)
'SERVER > Architecture' 카테고리의 다른 글
| Strangler Fig Pattern : 테스트 하지 않는 환경에서 TDD 흐름 만들기 (0) | 2026.01.02 |
|---|---|
| [TDD] 단위 테스트에서 만난 오류와 원리들 (0) | 2025.09.03 |
| [냄새라도 TDD/OOP] 2-2. 단위 테스트 (컨트롤러) (0) | 2025.07.04 |
| [냄새라도 TDD/OOP] 3. 통합테스트 (서비스) (0) | 2025.06.27 |
| [냄새라도 TDD/OOP] 2-1. 단위 테스트 (도메인단) (0) | 2025.06.27 |