Post

⚓ DATT 04 - 패키지 구조 설계

⚓ DATT 04 - 패키지 구조 설계

DATT v2 패키지 구조 설계

설계 목표

DATT v2는 단순 CRUD 프로젝트가 아니라 다음 기능을 포함하는 위치 기반 기록 플랫폼이다.

  • 회원가입 및 JWT 인증
  • 공공데이터 기반 장소 관리
  • 장소 그룹화
  • Anchor 기록
  • 이미지 업로드
  • 리뷰 및 평점
  • 장소 컬렉션
  • 공유
  • 게임화
  • 배치 처리

따라서 패키지 구조는 단순히 controller, service, repository를 전역으로 나누는 방식보다 도메인 기준으로 분리한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
나쁜 구조:
    controller
    service
    repository
    entity

좋은 구조:
    member
    auth
    place
    anchor
    review
    collection
    gamification
    batch
    global

목표는 다음과 같다.

  • 도메인별 응집도 강화
  • 기능 변경 시 영향 범위 축소
  • 협업과 유지보수성 향상
  • 패키지만 보고 서비스 구조 파악 가능
  • 실무형 백엔드 구조로 설명 가능

전체 패키지 구조

DATT v2의 기본 패키지 구조는 다음과 같다.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
com.datt
    ├── DattApplication.java
    ├── global
    │   ├── config
    │   ├── error
    │   ├── response
    │   ├── logging
    │   ├── security
    │   └── util
    ├── auth
    │   ├── controller
    │   ├── service
    │   ├── dto
    │   └── infrastructure
    ├── member
    │   ├── controller
    │   ├── service
    │   ├── domain
    │   ├── repository
    │   └── dto
    ├── place
    │   ├── controller
    │   ├── service
    │   ├── domain
    │   ├── repository
    │   ├── dto
    │   └── batch
    ├── anchor
    │   ├── controller
    │   ├── service
    │   ├── domain
    │   ├── repository
    │   └── dto
    ├── review
    │   ├── controller
    │   ├── service
    │   ├── domain
    │   ├── repository
    │   └── dto
    ├── collection
    │   ├── controller
    │   ├── service
    │   ├── domain
    │   ├── repository
    │   └── dto
    ├── gamification
    │   ├── service
    │   ├── domain
    │   ├── repository
    │   └── dto
    ├── file
    │   ├── controller
    │   ├── service
    │   ├── domain
    │   ├── repository
    │   └── infrastructure
    └── batch
        ├── config
        ├── job
        └── scheduler

패키지 설계 기준

1. global 패키지

global 패키지는 특정 도메인에 속하지 않는 공통 코드를 관리한다.

1
2
3
4
5
6
7
com.datt.global
    ├── config
    ├── error
    ├── response
    ├── logging
    ├── security
    └── util

global.config

Spring 설정 클래스를 둔다.

예시:

1
2
3
4
5
6
7
com.datt.global.config
    ├── WebConfig.java
    ├── JpaConfig.java
    ├── QuerydslConfig.java
    ├── RedisConfig.java
    ├── SwaggerConfig.java
    └── CorsConfig.java

역할:

  • Web MVC 설정
  • CORS 설정
  • JPA Auditing 설정
  • Querydsl 설정
  • Redis 설정
  • Swagger/OpenAPI 설정

global.error

공통 예외 처리를 담당한다.

1
2
3
4
5
6
7
com.datt.global.error
    ├── ErrorCode.java
    ├── BusinessException.java
    ├── GlobalExceptionHandler.java
    ├── ErrorResponse.java
    ├── ValidationErrorResponse.java
    └── FieldErrorResponse.java

역할:

  • 도메인별 에러 코드 관리
  • 비즈니스 예외 정의
  • 전역 예외 처리
  • Validation 예외 응답 처리

global.response

공통 API 응답 wrapper를 둔다.

1
2
com.datt.global.response
    └── ApiResponse.java

예시 응답:

1
2
3
4
5
{
    "success": true,
    "data": {},
    "error": null
}

global.logging

요청/응답 로깅, traceId 관리 등을 담당한다.

1
2
3
4
com.datt.global.logging
    ├── RequestLoggingFilter.java
    ├── TraceIdFilter.java
    └── MDCUtil.java

역할:

  • request URI 기록
  • HTTP method 기록
  • 응답 시간 기록
  • traceId 발급
  • MDC 기반 로그 추적

global.security

Spring Security와 JWT 공통 인프라를 둔다.

1
2
3
4
5
6
7
com.datt.global.security
    ├── SecurityConfig.java
    ├── JwtAuthenticationFilter.java
    ├── JwtTokenProvider.java
    ├── JwtAuthenticationEntryPoint.java
    ├── JwtAccessDeniedHandler.java
    └── CustomUserDetailsService.java

역할:

  • JWT 인증 필터
  • 토큰 검증
  • 인증 실패 처리
  • 인가 실패 처리
  • SecurityFilterChain 설정

주의할 점은 auth 패키지와 역할을 분리하는 것이다.

1
2
3
4
5
global.security:
    JWT 검증, Security Filter, 인증 인프라

auth:
    로그인, 회원가입, 토큰 재발급 등 사용자 인증 유스케이스

global.util

범용 유틸리티를 둔다.

1
2
3
4
com.datt.global.util
    ├── DateTimeUtils.java
    ├── TokenGenerator.java
    └── GeoUtils.java

주의:

util 패키지는 쉽게 비대해질 수 있으므로 정말 여러 도메인에서 공통으로 사용하는 코드만 둔다.


2. auth 패키지

auth 패키지는 인증 유스케이스를 담당한다.

1
2
3
4
5
com.datt.auth
    ├── controller
    ├── service
    ├── dto
    └── infrastructure

auth.controller

1
AuthController.java

담당 API:

  • 회원가입
  • 로그인
  • 로그아웃
  • 토큰 재발급
  • 아이디 찾기
  • 비밀번호 재설정

auth.service

1
2
3
AuthService.java
TokenService.java
PasswordResetService.java

역할:

  • 회원가입 처리
  • 로그인 검증
  • Access Token 발급
  • Refresh Token 관리
  • 비밀번호 재설정 처리

auth.dto

1
2
3
4
5
6
7
SignUpRequest.java
SignUpResponse.java
LoginRequest.java
LoginResponse.java
TokenRefreshRequest.java
TokenRefreshResponse.java
PasswordResetRequest.java

auth.infrastructure

인증과 관련된 외부 인프라 또는 저장소 성격의 구현을 둔다.

1
2
RefreshTokenRepository.java
EmailVerificationClient.java

초기에는 Redis 없이 DB 기반 Refresh Token으로 시작할 수 있다.

추후 Redis를 도입하면 이 계층에서 교체 가능하게 설계한다.


3. member 패키지

member 패키지는 사용자 계정과 프로필을 관리한다.

1
2
3
4
5
6
com.datt.member
    ├── controller
    ├── service
    ├── domain
    ├── repository
    └── dto

member.domain

1
2
3
Member.java
Role.java
MemberStatus.java

Member Entity 예시:

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
@Entity
@Table(name = "member")
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100, unique = true)
    private String email;

    @Column(nullable = false, length = 255)
    private String password;

    @Column(nullable = false, length = 30, unique = true)
    private String nickname;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 20)
    private Role role;

    @Column(nullable = false)
    private int level;

    @Column(nullable = false)
    private int exp;
}

member.repository

1
MemberRepository.java

주요 메서드:

1
2
3
4
5
Optional<Member> findByEmail(String email);

boolean existsByEmail(String email);

boolean existsByNickname(String nickname);

member.service

1
2
MemberService.java
MemberProfileService.java

역할:

  • 회원 조회
  • 프로필 조회
  • 닉네임 변경
  • 회원 탈퇴
  • 경험치/레벨 반영

member.controller

1
MemberController.java

담당 API:

  • 내 정보 조회
  • 프로필 조회
  • 닉네임 변경
  • 회원 탈퇴

4. place 패키지

place 패키지는 공공데이터 기반 장소와 장소 그룹을 관리한다.

1
2
3
4
5
6
7
com.datt.place
    ├── controller
    ├── service
    ├── domain
    ├── repository
    ├── dto
    └── batch

place.domain

1
2
3
4
5
Place.java
PlaceGroup.java
PlaceGroupMapping.java
PlaceCategory.java
MatchType.java

핵심 구분:

1
2
3
4
5
6
7
8
Place:
    공공데이터 원본 장소

PlaceGroup:
    사용자에게 노출되는 대표 장소

PlaceGroupMapping:
    원본 장소와 대표 장소 연결

place.repository

1
2
3
PlaceRepository.java
PlaceGroupRepository.java
PlaceGroupMappingRepository.java

주요 조회:

  • 키워드 검색
  • 카테고리 필터
  • 지도 bounds 조회
  • 인기순 조회
  • PlaceGroup 상세 조회

place.service

1
2
3
4
PlaceSearchService.java
PlaceGroupService.java
PlaceScoreService.java
PlaceMatchingService.java

역할:

  • 장소 검색
  • 장소 그룹화
  • 인기 점수 계산
  • 공공데이터 원본 매칭

place.controller

1
2
PlaceController.java
PlaceGroupController.java

담당 API:

  • 장소 검색
  • 장소 상세 조회
  • 지도 bounds 조회
  • 인기 장소 조회

place.dto

1
2
3
4
5
PlaceSearchRequest.java
PlaceSearchResponse.java
PlaceGroupResponse.java
PlaceMapResponse.java
PlaceDetailResponse.java

place.batch

place 도메인 내부 배치 로직을 둔다.

1
2
3
PublicPlaceImportJob.java
PlaceGroupRefreshJob.java
PlaceScoreRefreshJob.java

역할:

  • 공공데이터 적재
  • 장소 그룹 갱신
  • 인기 점수 갱신

5. anchor 패키지

anchor 패키지는 사용자가 장소에 남기는 기록을 담당한다.

1
2
3
4
5
6
com.datt.anchor
    ├── controller
    ├── service
    ├── domain
    ├── repository
    └── dto

anchor.domain

1
2
3
Anchor.java
AnchorImage.java
AnchorVisibility.java

Anchor는 DATT의 핵심 도메인이다.

1
2
장소는 공공데이터에서 오지만,
장소의 가치는 사용자의 Anchor에서 만들어진다.

anchor.service

1
2
AnchorService.java
AnchorImageService.java

역할:

  • Anchor 생성
  • Anchor 수정
  • Anchor 삭제
  • 이미지 연결
  • 공개 범위 검증

anchor.repository

1
2
AnchorRepository.java
AnchorImageRepository.java

주요 조회:

  • 장소별 Anchor 목록
  • 사용자별 Anchor 목록
  • 최근 Anchor 목록

anchor.controller

1
AnchorController.java

담당 API:

  • Anchor 작성
  • Anchor 수정
  • Anchor 삭제
  • Anchor 상세 조회
  • 장소별 Anchor 조회

6. review 패키지

review 패키지는 DATT 내부 리뷰와 평점을 관리한다.

1
2
3
4
5
6
com.datt.review
    ├── controller
    ├── service
    ├── domain
    ├── repository
    └── dto

review.domain

1
2
Review.java
Rating.java

리뷰는 외부 데이터가 아니라 DATT 내부 데이터다.

1
2
3
외부 평점 없음
외부 리뷰 없음
내부 리뷰만 사용

review.service

1
ReviewService.java

역할:

  • 리뷰 작성
  • 리뷰 수정
  • 리뷰 삭제
  • 중복 리뷰 검증
  • 장소 평균 평점 갱신

review.repository

1
ReviewRepository.java

주요 메서드:

1
2
3
boolean existsByMemberIdAndPlaceGroupId(Long memberId, Long placeGroupId);

List<Review> findByPlaceGroupId(Long placeGroupId);

review.controller

1
ReviewController.java

담당 API:

  • 리뷰 작성
  • 리뷰 수정
  • 리뷰 삭제
  • 장소별 리뷰 조회

7. collection 패키지

collection 패키지는 장소 저장, 장소 묶음, 공유를 담당한다.

1
2
3
4
5
6
com.datt.collection
    ├── controller
    ├── service
    ├── domain
    ├── repository
    └── dto

collection.domain

1
2
3
PlaceCollection.java
PlaceCollectionItem.java
CollectionVisibility.java

컬렉션 예시:

  • 성수 작업 카페 모음
  • 서울 심야 산책 장소
  • 부산 여행 후보

collection.service

1
2
CollectionService.java
CollectionShareService.java

역할:

  • 컬렉션 생성
  • 컬렉션 수정
  • 컬렉션 삭제
  • 장소 추가
  • 장소 제거
  • 공유 링크 생성

collection.repository

1
2
PlaceCollectionRepository.java
PlaceCollectionItemRepository.java

collection.controller

1
2
CollectionController.java
CollectionShareController.java

담당 API:

  • 내 컬렉션 목록
  • 컬렉션 상세
  • 컬렉션 공유 조회
  • 컬렉션에 장소 추가

8. gamification 패키지

gamification 패키지는 경험치, 레벨, 칭호, 배지를 관리한다.

1
2
3
4
5
com.datt.gamification
    ├── service
    ├── domain
    ├── repository
    └── dto

gamification.domain

1
2
3
4
GamificationEvent.java
Badge.java
MemberBadge.java
GamificationEventType.java

이벤트 예시:

이벤트경험치
Anchor 작성+10
이미지 업로드+15
리뷰 작성+10
컬렉션 생성+5
공유 링크 생성+5

gamification.service

1
2
3
GamificationService.java
BadgeService.java
LevelPolicy.java

역할:

  • 경험치 지급
  • 중복 경험치 지급 방지
  • 레벨 계산
  • 칭호 지급
  • 활동 이벤트 기록

gamification.repository

1
2
3
GamificationEventRepository.java
BadgeRepository.java
MemberBadgeRepository.java

9. file 패키지

file 패키지는 이미지 업로드와 저장소 연동을 담당한다.

1
2
3
4
5
6
com.datt.file
    ├── controller
    ├── service
    ├── domain
    ├── repository
    └── infrastructure

file.service

1
2
3
FileService.java
ImageService.java
ThumbnailService.java

역할:

  • 이미지 업로드
  • 확장자 검증
  • 용량 검증
  • 썸네일 생성
  • 저장소 업로드

file.infrastructure

1
2
3
LocalFileStorage.java
S3FileStorage.java
FileStorage.java

초기에는 LocalFileStorage로 시작하고, 추후 S3FileStorage로 교체 가능하게 인터페이스를 둔다.

1
2
3
4
5
6
public interface FileStorage {

    String upload(MultipartFile file);

    void delete(String fileUrl);
}

10. batch 패키지

batch 패키지는 전체 배치 설정과 스케줄링을 관리한다.

1
2
3
4
com.datt.batch
    ├── config
    ├── job
    └── scheduler

단, place 관련 배치 로직 자체는 place.batch에 둔다.

1
2
3
4
5
batch:
    스케줄링 실행, 공통 배치 설정

place.batch:
    공공데이터 적재, 장소 그룹 갱신, 점수 갱신 로직

의존성 방향

패키지 간 의존성은 다음 방향을 지킨다.

1
2
3
4
5
controller
    ↓
service
    ↓
domain / repository

도메인 간 의존은 최소화한다.

예를 들어 Anchor 생성 시 PlaceGroup이 필요하더라도 Anchor 도메인이 Place 내부 구현에 깊게 의존하지 않도록 한다.

1
2
3
AnchorService
    → PlaceGroupReader
    → PlaceGroupRepository

이런 식으로 조회 전용 컴포넌트를 둘 수 있다.


Reader / Writer 분리 전략

복잡도가 올라가면 Service 하나가 비대해질 수 있다.

따라서 필요 시 Reader / Writer를 분리한다.

예시:

1
2
3
4
5
6
7
8
PlaceGroupReader.java
PlaceGroupWriter.java

AnchorReader.java
AnchorWriter.java

MemberReader.java
MemberWriter.java

역할:

컴포넌트역할
Reader조회 전용
Writer생성/수정/삭제
Service유스케이스 조합

초기에는 Service 중심으로 시작하되, 클래스가 커지면 Reader/Writer로 분리한다.


DTO 위치 기준

DTO는 각 도메인 내부에 둔다.

1
2
3
place.dto
anchor.dto
review.dto

DTO를 global에 두지 않는 이유는 다음과 같다.

  • 요청/응답은 도메인별로 의미가 다르다
  • 전역 DTO 패키지는 금방 비대해진다
  • 도메인 단위 응집도가 떨어진다

Entity 위치 기준

Entity는 각 도메인의 domain 패키지에 둔다.

예시:

1
2
3
member.domain.Member
place.domain.PlaceGroup
anchor.domain.Anchor

Entity를 전역 entity 패키지에 두지 않는다.

이유는 다음과 같다.

1
2
Entity는 단순 DB 테이블 클래스가 아니라
도메인 규칙을 표현하는 객체이기 때문이다.

Repository 위치 기준

Repository는 각 도메인 내부에 둔다.

예시:

1
2
3
member.repository.MemberRepository
place.repository.PlaceGroupRepository
anchor.repository.AnchorRepository

도메인별 repository를 분리하면 해당 도메인의 데이터 접근 책임이 명확해진다.


Controller 위치 기준

Controller도 각 도메인 내부에 둔다.

예시:

1
2
3
auth.controller.AuthController
place.controller.PlaceController
anchor.controller.AnchorController

전역 controller 패키지를 만들지 않는다.

이유는 다음과 같다.

  • API가 많아질수록 controller 패키지가 비대해진다
  • 어떤 API가 어떤 도메인에 속하는지 파악하기 어렵다
  • 도메인 기준 변경 범위가 흐려진다

공통 코드와 도메인 코드 분리 기준

global에 넣어도 되는 것:

  • 공통 응답
  • 공통 예외
  • 공통 로그
  • Security 설정
  • Web 설정
  • 범용 util

global에 넣으면 안 되는 것:

  • 특정 도메인의 비즈니스 로직
  • 특정 도메인 전용 DTO
  • 특정 도메인 전용 검증 로직
  • 특정 도메인 전용 상수

예를 들어 PlaceCategory는 global이 아니라 place.domain에 둔다.


예상 패키지별 책임 요약

패키지책임
global공통 설정, 예외, 응답, 보안, 로그
auth회원가입, 로그인, 토큰 재발급
member사용자 프로필, 레벨, 계정 정보
place공공데이터 장소, 장소 그룹, 지도 조회
anchor장소 기록, Anchor 이미지
review내부 리뷰, 평점
collection장소 저장, 장소 묶음, 공유
gamification경험치, 레벨, 칭호
file이미지 업로드, 저장소 연동
batch스케줄링, 배치 실행

Trade-off

장점

장점
도메인별 응집도가 높다
기능 변경 시 영향 범위가 명확하다
패키지만 봐도 서비스 구조를 이해하기 쉽다
포트폴리오 설명에 유리하다
규모가 커져도 controller/service/repository 전역 패키지보다 관리가 쉽다

단점

단점
초기 파일 수가 많아진다
작은 기능도 패키지 구조를 고민해야 한다
도메인 경계가 애매한 경우 결정 비용이 생긴다
global 패키지가 비대해질 위험이 있다

운영 관점 고려 사항

패키지 구조는 운영과도 연결된다.

예를 들어 장애가 발생했을 때 다음처럼 파악 가능해야 한다.

1
2
3
4
5
auth 문제인가?
place 검색 문제인가?
anchor 저장 문제인가?
file 업로드 문제인가?
gamification 이벤트 문제인가?

도메인 기준으로 패키지가 분리되어 있으면 로그, 에러 코드, 장애 분석도 도메인 단위로 정리하기 쉽다.


면접에서 설명할 포인트

이 패키지 구조에서 설명할 수 있어야 하는 질문은 다음과 같다.

  1. 왜 controller/service/repository 전역 패키지를 사용하지 않았는가?
  2. 왜 도메인 기준 패키지 구조를 선택했는가?
  3. global 패키지가 비대해지는 문제는 어떻게 막을 것인가?
  4. auth와 security를 왜 분리했는가?
  5. place와 anchor 사이의 의존성은 어떻게 관리할 것인가?
  6. DTO를 도메인 내부에 둔 이유는 무엇인가?
  7. Entity를 domain 패키지에 둔 이유는 무엇인가?
  8. 파일 저장소를 infrastructure로 분리한 이유는 무엇인가?

최종 결정

DATT v2는 다음 구조를 기본으로 한다.

1
2
3
4
5
6
7
8
9
10
11
com.datt
    ├── global
    ├── auth
    ├── member
    ├── place
    ├── anchor
    ├── review
    ├── collection
    ├── gamification
    ├── file
    └── batch

이 구조는 DATT의 핵심 방향인:

1
2
3
4
5
공공데이터 기반 장소
사용자 Anchor 기록
내부 리뷰/평점
장소 컬렉션
게임화

를 도메인 단위로 명확하게 분리하기 위한 설계다.

앞으로 구현 과정에서 클래스가 비대해지면 Reader/Writer, Policy, Validator, Processor 등으로 세분화한다.

중요한 것은 처음부터 과도하게 복잡하게 만드는 것이 아니라, 변경 가능성이 높은 지점을 도메인 기준으로 분리해두는 것이다.

This post is licensed under CC BY 4.0 by the author.