백엔드 아키텍처 (헥사고날 / 어댑터)¶
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. 관련¶
- teams/engineering/guides/jpa-query — 조회 쿼리/매핑 선택
- teams/engineering/guides/soft-delete — Cascade 소프트 딜리트
- teams/engineering/guides/exception-handling — 예외 시스템
- 헥사고날/어댑터 채택 — raw
엔티티-리팩토링-가이드·DDD-리팩토링근거 (추후 ADR 신규 시 링크)
6. 열린 질문¶
- 변환 메서드를 어댑터마다 둘지, 공용 유틸 클래스로 모을지 (코드 중복 vs 응집)
- DDD 리팩토링: soft_delete 모듈 등을
core/최상위로 승격 예정