콘텐츠로 이동

백엔드 아키텍처 (헥사고날 / 어댑터)

Spring Boot 3.4.4 / Java 21 / Gradle. 도메인을 인프라(JPA)로부터 격리하는 포트-어댑터(헥사고날) 구조.

1. 레이어 구조

presentation/   컨트롤러 + DTO (요청·응답 표면)
application/     서비스 (유스케이스, 트랜잭션 경계)
domain/          순수 도메인 객체 + Repository 포트(인터페이스)
infrastructure/  JPA 엔티티 + 어댑터(Repository 구현) + security 등

패키지 base: com.example.demo. 도메인별로 domain/<name>, application/service/<name>, presentation/{controller,dto}/<name>, infrastructure/persistence/repository/<name> 가 대칭으로 존재.

2. 핵심 원칙: 도메인 ↔ 엔티티 분리

  • 도메인 객체와 JPA 엔티티는 서로를 몰라야 한다. 둘은 서로 다른 레이어에 종속.
  • 타입 변환(toDomain / fromDomain)의 책임은 엔티티/도메인이 아니라 어댑터(또는 매퍼) 에 둔다 (SRP). 편의 메서드를 엔티티/도메인에 넣지 않는다.
  • 도메인 Repository는 포트(인터페이스), infrastructure의 어댑터가 Spring Data JPA를 감싸 구현.

3. 영속 엔티티 안전 조립 (어댑터 패턴의 핵심)

연관 엔티티를 from()으로 매번 새로 만들면 비영속 엔티티 문제 발생 — id가 같아도 이미 영속된 데이터 대신 다른 데이터가 저장될 수 있음. 해결: 어댑터에서 연관 대상을 findById영속 상태로 조회한 뒤 조립.

@Override
public StudyRoom save(StudyRoom studyRoom) {
    TeacherEntity teacherEntity = (TeacherEntity) memberRepository
        .findById(studyRoom.getTeacher().getId()).orElseThrow();
    StudyRoomEntity entity = StudyRoomEntity.builder()
        .teacher(teacherEntity)          // 영속 엔티티 주입
        .name(studyRoom.getName())
        .build();
    return studyRoomRepository.save(entity).toDomain();
}

안티패턴: 연관관계 대신 Long teacherId 만 필드로 두기 → 저장은 되지만 FK 제약이 안 생겨 무결성 위험. 정석(연관 엔티티 + 영속 조회)으로 리팩토링할 것.

4. 프론트 대칭 원칙 (참고)

FE도 의존성 방향을 infrastructure → core로 유지. domain.ts가 infra의 dto를 import하면 방향 역전 → 금지. 변환 책임은 repository. 자세히는 teams/engineering/guides/frontend-conventions.

5. 관련

6. 열린 질문

  • 변환 메서드를 어댑터마다 둘지, 공용 유틸 클래스로 모을지 (코드 중복 vs 응집)
  • DDD 리팩토링: soft_delete 모듈 등을 core/ 최상위로 승격 예정