본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
7장 : 마이크로서비스 경계와 통신

모듈형 모놀리스에서 서비스 경계 찾기

6장까지는 Study Log API의 REST와 GraphQL 진입점을 하나의 애플리케이션 안에서 완성했습니다. 기능이 늘었다고 곧바로 프로세스와 DB를 나누면 네트워크 실패와 배포 비용만 먼저 생깁니다.

먼저 하나의 배포 단위를 유지한 채 학습 세션 원본과 보고서 집계 책임을 패키지와 포트로 분리합니다. 코드 경계를 지키지 못하는 상태에서 배포만 나누어도 서비스 경계는 생기지 않습니다.


서비스 경계는 업무 책임과 데이터에서 나온다

Study 쪽은 세션의 생성·완료와 원본 데이터를 소유하고, Report 쪽은 과목별 횟수와 분량처럼 조회에 맞춘 결과를 책임집니다. 양쪽이 JPA Entity를 공유하지 않고 ReportPort.SubjectReport라는 작은 값 계약만 넘기면 나중에 구현을 로컬 집계나 원격 호출로 바꿀 수 있습니다.


내부 포트로 보고서 기능을 분리한다

첫 구현은 네트워크를 쓰지 않는 local adapter입니다. 아직 집계 저장소를 연결하지 않았으므로 0을 반환하는 학습용 stub이며, 실제 보고서가 완성됐다고 해석하면 안 됩니다. study-log.report.mode=local이거나 설정을 생략했을 때만 이 bean을 등록합니다.

src/main/java/com/andongmin/studylog/report/ReportPort.java
package com.andongmin.studylog.report;

public interface ReportPort {
    SubjectReport findBySubject(String subject);

    record SubjectReport(String subject, long sessions, long minutes) {
    }
}
src/main/java/com/andongmin/studylog/report/LocalReportAdapter.java
package com.andongmin.studylog.report;

import org.springframework.stereotype.Component;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;

@Component
@ConditionalOnProperty(name = "study-log.report.mode",
        havingValue = "local", matchIfMissing = true)
public class LocalReportAdapter implements ReportPort {
    @Override
    public SubjectReport findBySubject(String subject) {
        return new SubjectReport(subject, 0, 0);
    }
}
src/main/java/com/andongmin/studylog/report/StudyReportService.java
package com.andongmin.studylog.report;

import org.springframework.stereotype.Service;

@Service
public class StudyReportService {
    private final ReportPort reports;

    public StudyReportService(ReportPort reports) {
        this.reports = reports;
    }

    public ReportPort.SubjectReport report(String subject) {
        if (subject == null || subject.isBlank()) {
            throw new IllegalArgumentException("subject must not be blank");
        }
        return reports.findBySubject(subject);
    }
}

금지된 패키지 의존을 구조 테스트로 찾는다

컴파일 성공은 패키지 경계를 보장하지 않습니다. 아래 검사는 report 패키지가 session 구현을 import하면 실패하도록 명령의 성공·실패 방향을 명시합니다. 장기적으로는 ArchUnit 같은 구조 테스트로 같은 규칙을 자동화할 수 있습니다.

./mvnw -q -DskipTests compile
if rg "com.andongmin.studylog.session" \
    src/main/java/com/andongmin/studylog/report; then
  echo "report must not import session implementation" >&2
  exit 1
fi
Port 확인 결과
report 패키지가 session 구현 타입을 직접 import하지 않고 `ReportPort` 계약만 사용하면 명령이 성공합니다. 이 결과는 런타임 집계값의 정확성까지 검증하지는 않습니다.

내부 포트가 생겼으므로 보고서가 별도 서비스로 이동하는 경우에도 StudyReportService는 그대로 두고 adapter만 HTTP 구현으로 교체할 수 있습니다.