백엔드 코드 아키텍처¶
코드/시스템 3문서 — 백엔드 코드 아키텍처(이 문서) · 프론트엔드 코드 아키텍처 · 시스템·인프라 아키텍처(전체 지도 + 서버·배포·운영).
Spring Boot 3.4.4 / Java 21 / Gradle. base 패키지 com.example.demo. 이 문서는 실제 코드(mvp-back/src/main/java/com/example/demo)를 정독해 작성했다 — 클래스명·패턴은 코드에서 확인한 것이다. 도메인 디테일은 회원·스터디룸 등 도메인 페이지로, 결정의 배경·트레이드오프는 engineering-principles·tech-decisions로 빠진다.
1. 레이어 — 포트·어댑터(헥사고날) + 계층형 혼합¶
presentation/ controller · dto · advice · aspect · response · util (요청·응답 표면)
application/ service · dto (유스케이스·트랜잭션 경계)
domain/ 순수 도메인 객체 + Repository 포트(인터페이스)
infrastructure/ persistence/{entity,repository} · security · logging (어댑터·기술)
external/ ai · s3 · sens (외부 연동)
common/ annotation · exception · swagger · util
config/ Bean·프로퍼티 설정
- 도메인별로
domain/<name>,application/service/<name>,presentation/{controller,dto}/<name>,infrastructure/persistence/{entity,repository}/<name>가 대칭으로 놓인다. - 포트·어댑터가 실제로 동작: 예)
domain/member/MemberRepository는 JPA를 모르는 순수 인터페이스(포트),infrastructure/persistence/repository/member/MemberRepositoryAdapter가implements MemberRepository하며SpringDataJpaMemberRepository를 주입해 구현한다(어댑터). 단 포트 위치는 도메인마다 일관되지 않다 — member처럼domain/에 둔 곳도, homework·review·teachingnote·studyroom 등처럼 포트 인터페이스를infrastructure/persistence/repository/에 둔 곳도 있다(서비스가 후자를 import하는 건 위반이 아님). - 도메인 ↔ 엔티티 분리: 도메인 객체와 JPA 엔티티는 서로를 모른다. 변환은 엔티티 클래스 안의
MemberEntity.from(member)/entity.toDomain()메서드가 담당한다(별도 Mapper 클래스는 없다 — 변환 책임이 엔티티에 있음). - 일탈 지점(정직하게): 순수 헥사고날이 100%는 아니다. 예)
MediaAssetService는 도메인 포트를 거치지 않고infrastructure/.../mediaasset/MediaAssetRepository를 직접 import한다. 조회 위주 흐름은 계층형(Controller → Service → Repository)으로 가는 곳도 많다 — 명령형은 포트·어댑터, 조회형은 계층형이라는 분리 원칙은 engineering-principles.
2. JPA 조회 — 쿼리 작성 × 결과 매핑¶
두 축을 독립적으로 고른다: ①쿼리 작성(Derived / JPQL @Query / Native / QueryDSL) × ②결과 매핑(Entity / Interface Projection / Class Projection).
- QueryDSL 실사용:
config/QueryDslConfig가JPAQueryFactoryBean을 등록하고,TeacherProfileQueryDslRepository·StudyRoomQueryDslRepository·HomeworkQueryDslRepository·ConnectionQueryDslRepository등에서Projections.constructor(XxxResponse.class, …)(Class Projection)로 응답 DTO를 직접 만든다. - Interface Projection도 혼용: 예)
open_challenge/challengeattempt/MyCompletedChallengeProjection(interface)을 Spring Data JPA가 프록시로 반환. - 매핑 선택 기준: Entity=영속성 컨텍스트 관리(쓰기 적합), Interface Projection=런타임 프록시(Closed/Open/Nested), Class Projection=
record/생성자(불변·테스트 용이). 결정 트리·근거는 engineering-principles.
3. 예외 처리 — 단일 예외 + ErrorCode enum¶
새 예외 클래스를 만들지 않고, 하나의 BusinessException + 도메인별 ErrorCode enum으로 모든 비즈니스 예외를 관리한다.
common/exception/BusinessException—RuntimeException상속,ErrorCode필드 1개.common/exception/code/ErrorCode—interface { int getStatus(); String getMessage(); }. 구현 enum이 약 26종:CommonErrorCode·AuthErrorCode·MemberErrorCode·StudyRoomErrorCode·TeachingNoteErrorCode·HomeworkErrorCode·QnaErrorCode·ReviewErrorCode·MediaAssetErrorCode·SecurityErrorCode… +open_challenge/하위 6종.common/exception/response/ErrorResponse—extends ApiResponse,code(enum name) 필드 추가.of(ErrorCode)/of(BusinessException)팩토리.presentation/advice/ErrorAdvice—@RestControllerAdvice.BusinessException·MethodArgumentNotValidException두 가지를 일관 JSON으로 변환.- 필터 단 예외: Spring MVC advice는 시큐리티 필터에서 난 예외를 못 잡으므로,
infrastructure/security/ExceptionHandlerFilter가 필터 체인의BusinessException을 직접ErrorResponse로 write한다. - 새 에러 = enum 한 줄. 발생 =
throw new BusinessException(AuthErrorCode.INVALID_CREDENTIALS).
4. 소프트 딜리트 — @SQLDelete + @SQLRestriction¶
⚠️ 과거 문서가 적었던
SoftDeleteOrchestrator/SoftDeletePolicy<T>같은 클래스는 코드에 존재하지 않는다. 실제 구현은 아래와 같다(코드 확인).
- soft delete 대상 엔티티마다
@SQLDelete(sql="UPDATE … SET deleted_at = NOW() WHERE id = ?")+@SQLRestriction("deleted_at IS NULL")를 직접 선언한다. 적용 엔티티 19종 확인(MemberEntity·ConnectionEntity·StudyRoomEntity·StudyRoomStudentEntity·TeachingNoteEntity·TeachingNoteGroupEntity·HomeworkEntity·QnaContextEntity·ReviewEntity·StudyNoteEntity·ColumnArticleEntity등). SoftDeleteBaseEntity(@MappedSuperclass) —BaseEntity를 상속해LocalDateTime deletedAt한 필드 추가.- 감사(audit):
BaseEntity가@PrePersist/@PreUpdate로reg_date/mod_date를 직접 채운다(AuditingEntityListener미사용 —@EnableJpaAuditing은JpaAuditingConfig에 있으나 실제 리스너는LikeEntity만 예외적으로 사용). - 삭제 연쇄 판단은 코드(도메인/서비스)에: FK는 실제로 걸되, soft delete는 DB
ON DELETE CASCADE를 못 타므로 연쇄 삭제 순서·범위는 애플리케이션이 오케스트레이션한다(engineering-principles·DDL은 infra §2-6).
5. 인증 — JWT를 쿠키로 (헤더 아님)¶
⚠️ JWT는 Authorization 헤더/localStorage가 아니라 HttpCookie로 주고받는다(코드 확인).
- 발급:
AuthController(소셜은OAuthController.setAuthCookies())가 로그인 성공 시ResponseCookie.from("Authorization", accessToken)+ResponseCookie.from("refresh-token", refreshToken)을Set-Cookie로 내려준다. - 수신:
JwtAuthenticationFilter.getAccessToken()이request.getCookies()를 순회해 이름Authorization쿠키 값을 읽는다(헤더 미사용). 없으면BusinessException(AuthErrorCode.REQUIRED_ACCESS_TOKEN). - Refresh: refresh token은 in-memory
ConcurrentHashMap(RefreshTokenRepositoryImpl—Map<Long,String>)에 저장하고GET /api/auth/refresh에서 교차검증한다. ⚠️ 서버 재기동 시 소실·인스턴스 간 비공유(현 단일 인스턴스라 동작, Redis 이관은 후속). Redis는 알림·AI 레이트리밋·이메일 등 다른 용도에 쓴다. - 인가(SecurityConfig):
SecurityFilterChain3개 —@Order(1)Swagger,@Order(2)public,@Order(3)private.hasRole()/hasAnyRole()+ RoleHierarchy(ADMIN > TEACHER, STUDENT, PARENT, MEMBER). 쿠키/CORS·SameSite 등 전송 계층은 infra §2-4.
6. API 명세·응답 래퍼 + role prefix¶
- 응답 envelope:
presentation/response/ApiResponse({status, message}) →SuccessResponse<T>(+ data,onSuccess(data)={200, "성공입니다.", data}) /ErrorResponse(+ code). - 경로 role prefix:
/api/public/**(permitAll) ·/api/teacher/**·/api/student/**(역할별) ·/api/common/**(TEACHER·STUDENT·PARENT, ADMIN은 RoleHierarchy로 포함) ·/api/notification. 컨트롤러는presentation/controller/<도메인>에 도메인 대칭으로 둔다. - Swagger 에러 명세 자동화:
common/swagger/ErrorResponseAnnotation(메타 어노테이션)을 단Api{Domain}ErrorResponses어노테이션들을,ApiErrorResponsesCustomizer implements OperationCustomizer가 리플렉션으로 읽어 HTTP 상태별로 그룹화해 OpenAPI ApiResponse+Example을 생성한다. 새 도메인은 어노테이션 1개만 추가. - 사이트맵 API(SEO):
GET /api/public/sitemap/{columns|profiles|previews}— 인증 불필요·페이지네이션 없음.
6.1 공통 Enums / 상수¶
| 분류 | 값 |
|---|---|
| 역할(Role) | ROLE_ADMIN · ROLE_TEACHER · ROLE_STUDENT · ROLE_PARENT · ROLE_MEMBER(임시) |
| 정렬 — 룸/선생 | LATEST · OLDEST · ALPHABETICAL |
| 정렬 — 질문/노트/과제 | LATEST_EDITED · OLDEST_EDITED · TITLE_ASC · TAUGHT_AT_ASC · DEADLINE_IMMINENT · DEADLINE_RECENT |
| 상태(공통) | PENDING · COMPLETED · APPROVED · REJECTED · TERMINATED |
| 제출(과제) | NOT_SUBMIT · SUBMIT · LATE_SUBMIT |
| 공개여부(visibility) | PRIVATE · PUBLIC · TEACHER_ONLY · STUDENT_ONLY · STUDENT_AND_PARENT · SPECIFIC_STUDENTS_ONLY · STUDY_ROOM_STUDENTS_ONLY 등 |
7. 미디어 (Presigned URL / S3) — external/s3¶
첨부는 presigned URL 직접 업로드, 본문에는 media://{mediaId} 참조만 심고 조회 시 백엔드가 실제 URL로 치환한다.
| 단계 | 호출 / 클래스 | 골격 |
|---|---|---|
| 1) URL 발급(배치) | MediaAssetService.createUploadUrl() → POST /common/media/presign-batch |
req {mediaAssetList:[{fileName, contentType, sizeBytes}]} (※ targetType은 요청 바디가 아니라 이후 attachMediaAssets()에서 전달) → res [{fileName, mediaId, uploadUrl, headers}] |
| 2) 직접 업로드 | 클라가 S3로 PUT {uploadUrl} |
Content-Type 헤더 필수, presigned 만료 |
| 3) 저장 | 리소스 POST |
content 안에 media://{mediaId} + mediaIds 동봉 |
| 4) 조회 치환 | MediaAssetContentResolver.resolveMediaInContent() |
JSON 트리 DFS로 ^media://[\w-]+$를 presigned GET URL(기본 TTL 15분, s3.view-ttl-minutes)로 치환 |
- S3 키:
{keyPrefix}/uploader/{10자리 zero-pad id}/{image|file}/{mediaId}.{ext}(buildKey()). TargetTypeenum 17종(TEACHING_NOTE·HOMEWORK·QNA·REVIEW·COLUMN_ARTICLE·STUDY_ROOM_THUMBNAIL·PROFILE_IMAGE·CHALLENGE_QUESTION…)으로 공개/비공개 S3 키 prefix를 나눈다. 프론트(Tiptap) 직렬화는 frontend-architecture §에디터·미디어.
8. 외부 연동 (external/)¶
external/ai— AI 코치.AiCoachingClient(interface) + 구현 3종AnthropicCoachingClient·OpenAiCoachingClient·StubAiCoachingClient(프로바이더 전환),AiCoachingProperties. 오픈챌린지 사고력 코칭이 여기를 탄다(오픈챌린지).external/s3—S3Service(presigned 발급·조회, §7).external/sens— 네이버 클라우드 SENS 알림톡 클라이언트(카카오 알림톡 발송, 카카오 알림 (알림 고도화)).
9. 데이터 모델 (상속 전략 · ERD)¶
- Member 상속은 단일 테이블이 아니다: 도메인 계층은 Java 상속(
Teacher extends Member)이지만, DB는 조인 전략 —TeacherEntity/StudentEntity가@OneToOne(MemberEntity) + @MapsId로member본 테이블과 PK를 공유하는 별도 테이블(teacher·student).MemberEntity에DTYPE없음. - 전체 모델은 theEdu (erdcloud), 운영 DDL·테이블↔도메인·FK 매핑은 infra §2-6.
10. Config¶
config/: CorsProperties · JpaAuditingConfig(@EnableJpaAuditing) · QueryDslConfig(JPAQueryFactory) · RedisConfig · SwaggerConfig · WebClientConfig · WebConfig. config/properties/FrontendOriginProperties(@ConfigurationProperties("auth.frontend") — baseUrl·allowedOrigins 등, 프리뷰 CORS는 infra §2-4).
관련¶
frontend-architecture · infra · version-control · tech-decisions · engineering-principles · 회원 도메인 (Member)