Skip to content

architecture Architecture

Kimgyuilli edited this page Jan 15, 2026 · 4 revisions

아키텍처 가이드

이 문서는 Cherrish 프로젝트의 전체 아키텍처와 레이어별 책임을 설명합니다.

목차


패키지 구조

프로젝트는 DDD (Domain-Driven Design) 기반의 레이어드 아키텍처를 따릅니다.

src/
├── main/
│   ├── java/
│   │   └── com.sopt.cherrish/
│   │       ├── global/                      # 공통 설정
│   │       │   ├── config/                    # 전역 설정 (DB, Security, Swagger 등)
│   │       │   ├── exception/                 # 전역 예외 처리 (GlobalExceptionHandler)
│   │       │   ├── response/                  # 공통 응답 형식
│   │       │   │   ├── error/
│   │       │   │   └── success/
│   │       │   ├── entity/                    # 공통 엔티티 (BaseEntity 등)
│   │       │   ├── annotation/                # 커스텀 어노테이션
│   │       │   └── swagger/                   # Swagger 설정
│   │       │
│   │       ├── domain/                      # 도메인 영역
│   │       │   ├── user/                    # User 도메인
│   │       │   │   ├── presentation/          # Presentation Layer (표현 계층)
│   │       │   │   │   ├── UserController.java
│   │       │   │   │   └── dto/
│   │       │   │   │       ├── request/
│   │       │   │   │       └── response/
│   │       │   │   │
│   │       │   │   ├── application/           # Application Layer (응용 계층)
│   │       │   │   │   ├── service/            # 비즈니스 로직
│   │       │   │   │   └── facade/             # 여러 도메인 서비스 조합 (필요시)
│   │       │   │   │
│   │       │   │   ├── domain/                # Domain Layer (도메인 계층)
│   │       │   │   │   ├── model/              # 도메인 모델/엔티티
│   │       │   │   │   └── repository/         # 리포지토리
│   │       │   │   │
│   │       │   │   ├── exception/              # 도메인별 예외
│   │       │   │   │   └── UserErrorCode.java
│   │       │   │   │
│   │       │   │   └── infrastructure/         # Infrastructure Layer (외부 연동)
│   │       │   │       └── client/             # 외부 API 클라이언트 (필요시)
│   │       │   │
│   │       │   ├── userprocedure/           # UserProcedure 도메인 (QueryDSL 사용 예시)
│   │       │   │   ├── presentation/
│   │       │   │   │   └── dto/
│   │       │   │   ├── application/
│   │       │   │   │   └── service/
│   │       │   │   ├── domain/
│   │       │   │   │   ├── model/
│   │       │   │   │   ├── repository/         # Repository + QueryDSL
│   │       │   │   │   │   ├── UserProcedureRepository.java
│   │       │   │   │   │   ├── UserProcedureRepositoryCustom.java    # QueryDSL 커스텀 인터페이스
│   │       │   │   │   │   └── UserProcedureRepositoryImpl.java      # QueryDSL 구현체
│   │       │   │   │   └── vo/                 # Value Object
│   │       │   │   └── exception/
│   │       │   │
│   │       │   ├── challenge/               # Challenge 도메인 (복잡한 하위 구조 예시)
│   │       │   │   ├── core/                  # 핵심 챌린지 기능
│   │       │   │   │   ├── presentation/
│   │       │   │   │   ├── application/
│   │       │   │   │   │   ├── service/
│   │       │   │   │   │   ├── facade/
│   │       │   │   │   │   └── scheduler/
│   │       │   │   │   ├── domain/
│   │       │   │   │   └── exception/
│   │       │   │   ├── recommendation/        # 챌린지 추천 기능
│   │       │   │   │   └── infrastructure/    # 외부 API 연동
│   │       │   │   │       └── openai/
│   │       │   │   └── demo/                  # 기타 하위 도메인
│   │       │   │
│   │       │   └── ...                      # 기타 도메인 (procedure, calendar, maindashboard 등)
│   │       │
│   │       └── CherrishApplication.java
│   │
│   └── resources/
│       ├── application.yml
│       └── application-{profile}.yml
└── test/

레이어별 책임

Presentation Layer (Controller)

책임:

  • HTTP 요청/응답 처리
  • 입력 검증 (@Valid)
  • 비즈니스 로직 포함 금지

✅ 해야 할 일:

  • HTTP 상태 코드 및 응답 형식 결정
  • 요청 데이터 검증 (@Valid, @PathVariable, @RequestParam)
  • Service 계층 호출 및 결과 반환
  • API 문서화 (@Operation, @Tag)

❌ 하지 말아야 할 일:

  • 비즈니스 로직 작성 (Service로)
  • 데이터베이스 직접 접근 (Repository는 Service에서만)
  • 트랜잭션 관리 (Service에서 관리)
  • 예외 직접 처리 (GlobalExceptionHandler가 처리)

예제:

@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
@Tag(name = "User", description = "회원 관련 API")
public class UserController {

  private final UserService userService;

  // 데이터가 있는 성공 응답
  @Operation(summary = "회원 조회")
  @GetMapping("/{id}")
  public CommonApiResponse<UserResponseDto> getUser(@PathVariable Long id) {
    UserResponseDto user = userService.getUser(id);
    return CommonApiResponse.success(SuccessCode.SUCCESS, user);
  }

  // 데이터가 없는 성공 응답 (생성, 삭제 등)
  @Operation(summary = "회원 생성")
  @PostMapping
  public CommonApiResponse<Void> createUser(@Valid @RequestBody UserRequestDto request) {
    userService.createUser(request);
    return CommonApiResponse.success(SuccessCode.SUCCESS);
  }
}

Application Layer (Service)

책임:

  • 비즈니스 로직 처리
  • 트랜잭션 관리 (@Transactional)
  • 여러 Repository 조합

✅ 해야 할 일:

  • 핵심 비즈니스 로직 구현
  • 트랜잭션 경계 설정 (@Transactional)
  • 도메인 객체 간 협업 조율
  • 유효성 검증 (비즈니스 규칙)
  • 여러 Repository 조합하여 복잡한 작업 수행

❌ 하지 말아야 할 일:

  • HTTP 관련 코드 작성 (Controller에서만)
  • SQL 쿼리 직접 작성 (Repository에서)
  • Entity를 직접 반환 (DTO 변환 필수)

주의사항:

  • 클래스 레벨에 @Transactional(readOnly = true) 선언 (기본값)
  • 쓰기 작업 메서드에만 @Transactional 재선언
  • 트랜잭션 안에서 외부 API 호출 지양

예제:

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {

    private final UserRepository userRepository;

    // 조회는 readOnly
    public UserResponseDto getUser(Long id) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new BaseException(UserErrorCode.USER_NOT_FOUND));
        return UserResponseDto.from(user);
    }

    // 쓰기 작업은 @Transactional 재선언
    @Transactional
    public UserResponseDto createUser(UserRequestDto request) {
        // 1. 비즈니스 검증
        validateDuplicateEmail(request.getEmail());

        // 2. Request DTO → Entity 변환
        User user = request.toEntity();

        // 3. Entity 저장
        User savedUser = userRepository.save(user);

        // 4. Entity → Response DTO 변환
        return UserResponseDto.from(savedUser);
    }

    private void validateDuplicateEmail(String email) {
        if (userRepository.existsByEmail(email)) {
            throw new BaseException(UserErrorCode.DUPLICATE_EMAIL);
        }
    }
}

Domain Layer (Repository)

책임:

  • 데이터 접근만 담당
  • JPA 메서드 네이밍 규칙 준수
  • 복잡한 쿼리는 @Query 또는 QueryDSL 사용

✅ 해야 할 일:

  • 데이터베이스 CRUD 작업
  • JPA 네이밍 규칙을 따르는 메서드 정의 (findBy, existsBy, deleteBy 등)
  • 복잡한 쿼리는 @Query 어노테이션 사용
  • 성능이 중요한 경우 QueryDSL 사용

❌ 하지 말아야 할 일:

  • 비즈니스 로직 작성 (Service에서)
  • DTO 변환 작업 (Service에서)
  • 트랜잭션 관리 (Service에서)

예제:

public interface UserRepository extends JpaRepository<User, Long> {

    // JPA 메서드 네이밍 규칙
    Optional<User> findByEmail(String email);

    boolean existsByEmail(String email);

    List<User> findByAgeGreaterThan(int age);

    // 복잡한 쿼리는 @Query 사용
    @Query("SELECT u FROM User u WHERE u.name LIKE %:keyword% AND u.isActive = true")
    List<User> searchActiveUsers(@Param("keyword") String keyword);

    // Native Query (필요한 경우만)
    @Query(value = "SELECT * FROM users WHERE created_at > :date", nativeQuery = true)
    List<User> findRecentUsers(@Param("date") LocalDateTime date);
}

JPA 메서드 네이밍 규칙:

  • findBy...: 조회
  • existsBy...: 존재 여부 확인
  • countBy...: 개수 세기
  • deleteBy...: 삭제

커스텀 구현 (QueryDSL 사용):

// 1. 커스텀 인터페이스
public interface UserProcedureRepositoryCustom {
    Map<Integer, Long> findMonthlyProcedureCounts(Long userId, int year, int month);
    List<UserProcedure> findDailyProcedures(Long userId, LocalDate date);
}

// 2. 구현체 ({Repository이름}Impl 형식)
@RequiredArgsConstructor
public class UserProcedureRepositoryImpl implements UserProcedureRepositoryCustom {
    private final JPAQueryFactory queryFactory;

    @Override
    public List<UserProcedure> findDailyProcedures(Long userId, LocalDate date) {
        return queryFactory
            .selectFrom(userProcedure)
            .join(userProcedure.procedure).fetchJoin()  // N+1 방지
            .where(
                userProcedure.user.id.eq(userId),
                userProcedure.scheduledAt.between(date.atStartOfDay(), date.plusDays(1).atStartOfDay())
            )
            .fetch();
    }
}

// 3. 기존 Repository에 상속
public interface UserProcedureRepository
    extends JpaRepository<UserProcedure, Long>, UserProcedureRepositoryCustom {
    Optional<UserProcedure> findByIdAndUserId(Long id, Long userId);
}

Infrastructure Layer

책임:

  • 외부 시스템 연동
  • 외부 API 클라이언트
  • 메시징/이벤트 처리

위치:

  • domain/{domain}/infrastructure/

사용 예시:

  • domain/ai/infrastructure/client/: AI API 클라이언트
  • domain/challenge/recommendation/infrastructure/openai/: OpenAI 연동
  • domain/challenge/recommendation/infrastructure/prompt/: 프롬프트 관리

주의사항:

  • Infrastructure는 외부 시스템 연동에만 사용
  • Repository는 domain/repository에 위치 (Infrastructure가 아님)
  • QueryDSL 구현체(RepositoryImpl)도 domain/repository에 위치

레이어 간 의존성 규칙

Presentation Layer (Controller)
        ↓
Application Layer (Service / Facade)
        ↓
Domain Layer (Model / Repository / VO)
        ↓
Infrastructure Layer (External API / Messaging)

원칙:

  • 상위 레이어는 하위 레이어에 의존할 수 있음
  • 하위 레이어는 상위 레이어에 의존하면 안 됨
  • 각 레이어는 자신의 책임만 수행

Facade Pattern

사용 시기:

  • 여러 도메인의 Service를 조합해야 할 때
  • 복잡한 비즈니스 워크플로우를 단순화할 때

위치:

  • domain/{domain}/application/facade/

예제:

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MainDashboardFacade {
    private final UserService userService;
    private final ChallengeService challengeService;
    private final UserProcedureService userProcedureService;

    public MainDashboardResponseDto getMainDashboard(Long userId) {
        UserSummaryDto user = userService.getUserSummary(userId);
        ChallengeSummaryDto challenge = challengeService.getCurrentChallenge(userId);
        ProcedureSummaryDto procedure = userProcedureService.getRecentProcedures(userId);
        return MainDashboardResponseDto.of(user, challenge, procedure);
    }
}

예외 처리

각 도메인의 exception/ 패키지에 {Domain}ErrorCode.java 작성

@Getter
@RequiredArgsConstructor
public enum UserErrorCode implements ErrorCode {
    USER_NOT_FOUND(NOT_FOUND, "사용자를 찾을 수 없습니다."),
    DUPLICATE_EMAIL(CONFLICT, "이미 사용 중인 이메일입니다.");

    private final HttpStatus httpStatus;
    private final String message;
}

// 사용
throw new BaseException(UserErrorCode.USER_NOT_FOUND);

DTO 작성 규칙

DTO는 record로 작성

// Request DTO
public record UserCreateRequestDto(
    @NotBlank String name,
    @Min(1) Integer age
) {
    public User toEntity() {
        return User.builder().name(name).age(age).build();
    }
}

// Response DTO
public record UserResponseDto(Long id, String name, Integer age) {
    public static UserResponseDto from(User user) {
        return new UserResponseDto(user.getId(), user.getName(), user.getAge());
    }
}

Value Object (VO)

위치: domain/{domain}/domain/vo/

사용 시기: 도메인 개념을 표현하는 불변 객체, 비즈니스 규칙이 포함된 값 객체

@Embeddable
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ScheduledDate {
    private LocalDateTime scheduledAt;

    public ScheduledDate(LocalDateTime scheduledAt) {
        validate(scheduledAt);
        this.scheduledAt = scheduledAt;
    }

    private void validate(LocalDateTime scheduledAt) {
        if (scheduledAt == null || scheduledAt.isBefore(LocalDateTime.now())) {
            throw new BaseException(ErrorCode.INVALID_SCHEDULED_DATE);
        }
    }
}

도메인별 하위 구조 (Challenge 예시)

복잡한 도메인의 경우 하위 도메인으로 분리:

domain/challenge/
├── core/                    # 핵심 챌린지 기능
│   ├── presentation/
│   ├── application/
│   │   ├── service/
│   │   ├── facade/
│   │   └── scheduler/      # 스케줄러
│   ├── domain/
│   └── exception/
│
├── recommendation/         # 챌린지 추천 기능
│   ├── presentation/
│   ├── application/
│   └── infrastructure/
│       └── openai/        # OpenAI 연동
│
└── ...                    # demo, homecare 등 기타 하위 도메인

원칙:

  • 각 하위 도메인은 독립적인 레이어 구조를 가짐
  • 하위 도메인 간 직접 참조는 지양
  • 필요시 상위 레벨의 Facade에서 조율

Clone this wiki locally