⚓ 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 이벤트 문제인가?
도메인 기준으로 패키지가 분리되어 있으면 로그, 에러 코드, 장애 분석도 도메인 단위로 정리하기 쉽다.
면접에서 설명할 포인트
이 패키지 구조에서 설명할 수 있어야 하는 질문은 다음과 같다.
- 왜 controller/service/repository 전역 패키지를 사용하지 않았는가?
- 왜 도메인 기준 패키지 구조를 선택했는가?
- global 패키지가 비대해지는 문제는 어떻게 막을 것인가?
- auth와 security를 왜 분리했는가?
- place와 anchor 사이의 의존성은 어떻게 관리할 것인가?
- DTO를 도메인 내부에 둔 이유는 무엇인가?
- Entity를 domain 패키지에 둔 이유는 무엇인가?
- 파일 저장소를 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 등으로 세분화한다.
중요한 것은 처음부터 과도하게 복잡하게 만드는 것이 아니라, 변경 가능성이 높은 지점을 도메인 기준으로 분리해두는 것이다.