๐Ÿ’ญ [Dataracy] ํ—ฅ์‚ฌ๊ณ ๋‚  + DDD ๊ตฌ์กฐ ์„ค๊ณ„ ์ ์šฉ๊ธฐ

๋ฐ•์ค€ํ˜•ยท2025๋…„ 8์›” 16์ผ

์Šคํ”„๋ง ๊ฐœ๋ฐœ

๋ชฉ๋ก ๋ณด๊ธฐ
11/20
post-thumbnail

๊นƒํ—ˆ๋ธŒ์˜ ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋“ค์ด ์ž‘์„ฑํ•œ ์ฝ”๋“œ๋“ค์„ ๋ณด๋ฉด ๋ฐ”๋กœ ์•Œ ์ˆ˜ ์žˆ๋“ฏ์ด, ์‚ฌ๋žŒ๋งˆ๋‹ค ๋ชจ๋‘ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„ ๋ฐฉ๋ฒ•๊ณผ, ์ฝ”๋“œ ๊ตฌ์กฐ ์„ค๊ณ„๋Š” ๋ชจ๋‘ ๋‹ค๋ฅด๋‹ค. ํ˜„์žฌ Dataracy๋ผ๋Š” ํ”Œ๋žซํผ ๊ฐœ๋ฐœ์—์„œ ๋‚˜๋„ ์–ด๋–ป๊ฒŒ ์„ค๊ณ„๋ฅผ ํ•ด์•ผํ•  ์ง€ ๋งŽ์€ ๊ณ ๋ฏผ์„ ํ•˜๋ฉฐ ๊ณ„์†ํ•ด์„œ ๋ฐ”๊พธ์–ด ๋‚˜๊ฐ€๊ณ  ์žˆ๋‹ค.

์ฒ˜์Œ์—๋Š” Controller, Service, Domain, Repository ์ด๋ ‡๊ฒŒ ๊ณ„์ธต๋ณ„๋กœ ์„ค๊ณ„ํ•˜์˜€๊ณ , ๋ฐ”๋กœ ์ด์ „ ํ”„๋กœ์ ํŠธ์ธ HitZone์—์„œ๋Š” DDD ๊ตฌ์กฐ์— ๋Œ€ํ•ด์„œ ๊ณ ๋ฏผ์„ ํ•ด๋ณด์•˜๋Š”๋ฐ ์ด๋ฒˆ ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•˜๋ฉฐ DDD์™€ ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜์— ๋Œ€ํ•ด์„œ ์ ‘ํ•˜๊ฒŒ ๋˜์–ด ์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์— ๋‚˜์—๊ฒŒ ์ปค์Šคํ…€ํ•˜์—ฌ ์ ์šฉํ•ด๋ณด์•˜๋‹ค.



๐Ÿค” ์™œ ๋ ˆ์ด์–ด๋“œ์—์„œ ํฌํŠธ/์–ด๋Œ‘ํ„ฐ๋กœ ์ „ํ™˜ํ–ˆ๋Š”๊ฐ€

โŒ ๊ธฐ์กด ๋ ˆ์ด์–ด๋“œ ์•„ํ‚คํ…์ฒ˜์˜ ํ•œ๊ณ„

  • ๊ทœ์น™ ํฌ์„ & ๊ฒฐํ•ฉ๋„ ์ฆ๊ฐ€
    Service๊ฐ€ DTO ์กฐ๋ฆฝยท์ฟผ๋ฆฌยท์™ธ๋ถ€ ํ˜ธ์ถœ๊นŒ์ง€ ๋ชจ๋‘ ๋– ์•ˆ์Œ
    โ†’ ์‹œ๊ฐ„์ด ์ง€๋‚ ์ˆ˜๋ก ๋„๋ฉ”์ธ ๊ทœ์น™์ด I/O์— ์ž ์‹๋  ์ˆ˜ ์žˆ๋‹ค.
  • ๋ณ€๊ฒฝ ๋‚ด์„ฑ ๋ถ€์กฑ
    ์ €์žฅ์†Œ/๊ฒ€์ƒ‰/์บ์‹œ ์ „๋žต์„ ๋ฐ”๊พธ๋ฉด ControllerยทService๊นŒ์ง€ ์—ฐ์‡„ ์ˆ˜์ • ํ•„์š”
  • ์ฝ๊ธฐ/์“ฐ๊ธฐ ๊ด€์‹ฌ์‚ฌ ์ถฉ๋Œ
    ์ฝ๊ธฐ(์กฐ์ธ/ํ”„๋กœ์ ์…˜/์บ์‹œ)์™€ ์“ฐ๊ธฐ(ํŠธ๋žœ์žญ์…˜)๊ฐ€ ๊ฐ™์€ ๋ ˆ์ด์–ด์—์„œ ์„ž์ž„
  • ๋А๋ฆฌ๊ณ  ๋ถˆ์•ˆ์ •ํ•œ ํ…Œ์ŠคํŠธ
    Service ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธํ™”๋˜์–ด ๋А๋ฆผ

๐ŸŽฏ ์ „ํ™˜ ๋ชฉํ‘œ

  • ์ฝ”์–ด(๋„๋ฉ”์ธ/์œ ์Šค์ผ€์ด์Šค)๋ฅผ ๊ตฌํ˜„ ๊ธฐ์ˆ ๋กœ๋ถ€ํ„ฐ ๊ฒฉ๋ฆฌ : ๋„๋ฉ”์ธ/์œ ์Šค์ผ€์ด์Šค์—์„œ ํ”„๋ ˆ์ž„์›ŒํฌยทI/O ์ œ๊ฑฐ
  • ๋‹จ๋ฐฉํ–ฅ ์˜์กด : Adapters โ†’ Application โ†’ Domain
  • ๊ฒฝ๊ณ„-ํ•œ์ • ๋งคํ•‘ : ๋ณ€ํ™˜์€ Webโ†”App, Appโ†”Domain(๋ช…๋ น/์‘๋‹ต), Domainโ†”Persistence(JPA/Query), Appโ†”Index/Cache(Redis/ES) ๊ฒฝ๊ณ„์—์„œ๋งŒ ์ˆ˜ํ–‰ํ•œ๋‹ค.

๐Ÿ‘‰ ํ•œ ์ค„ ์š”์•ฝ:
โ€œ๊ธฐ์ˆ ์ด ๋ฐ”๋€Œ์–ด๋„ ๊ทœ์น™์€ ์•ˆ ๋ฐ”๋€Œ๊ฒŒโ€ โ†’ ๊ทœ์น™์€ ๋„๋ฉ”์ธ/์œ ์Šค์ผ€์ด์Šค, ๊ธฐ์ˆ ์€ ์–ด๋Œ‘ํ„ฐ.
๋‚˜์ค‘์— ๊ตฌํ˜„ํ•  ๊ธฐ์ˆ ์ด ๋ฐ”๋€Œ๊ฑฐ๋‚˜ ์ถ”๊ฐ€๋˜์–ด๋„ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ ์˜ํ–ฅ์„ ๋ฐ›์ง€ ์•Š๋Š”๋‹ค.



๐Ÿงฉ ํ•ต์‹ฌ ๊ฐœ๋… ์ •๋ฆฌ: DDD & ํ—ฅ์‚ฌ๊ณ ๋‚ 

๐Ÿ“˜ DDD (Domain-Driven Design)

  • Ubiquitous Language : ํŒ€์ด ์“ฐ๋Š” ์šฉ์–ด๋ฅผ ์ฝ”๋“œ์— ๊ทธ๋Œ€๋กœ ํˆฌ์˜
  • ๋ชจ๋ธ ์‘์ง‘ : ์—”ํ‹ฐํ‹ฐ/VO/๋„๋ฉ”์ธ ์„œ๋น„์Šค/๋ถˆ๋ณ€์‹ ๋“ฑ์œผ๋กœ ๊ทœ์น™์„ ์ฝ”๋“œ์— ๊ณ ์ •
  • Bounded Context : ์˜๋ฏธ ์ถฉ๋Œ ์—†๋Š” ๊ฒฝ๊ณ„ ๋‹จ์œ„๋กœ ๋‚˜๋ˆ„์–ด ๋…๋ฆฝ ์ง„ํ™”

๐Ÿ›  ํ—ฅ์‚ฌ๊ณ ๋‚  (Ports & Adapters)

  • Port(๊ณ„์•ฝ) : ์ฝ”์–ด๊ฐ€ ์™ธ๋ถ€์™€ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ์ถ”์ƒ ์ธํ„ฐํŽ˜์ด์Šค
  • Adapter(๊ตฌํ˜„) : Port๋ฅผ ๊ตฌํ˜„ํ•ด UI/DB/์บ์‹œ/๊ฒ€์ƒ‰ ๊ฐ™์€ ๊ธฐ์ˆ ์„ ์—ฐ๊ฒฐ
  • Driving Adapter (์ž…๋ ฅ/์ƒ์œ„) : Controller ๊ฐ™์€ UI ๊ณ„์ธต โ†’ ์ฝ”์–ด ํ˜ธ์ถœ
  • Driven Adapter (์ถœ๋ ฅ/ํ•˜์œ„) : JPA/Query/Redis/Index โ†’ ์ฝ”์–ด์˜ ์š”์ฒญ ์ˆ˜ํ–‰

๐Ÿ‘‰ ์™œ Controller๋„ ์–ด๋Œ‘ํ„ฐ์ธ๊ฐ€?
HTTP ์š”์ฒญ์„ ๋ฐ›์•„ Port In(UseCase)์„ ํ˜ธ์ถœํ•˜๋Š” "๊ตฌํ˜„์ฒด"์ด๊ธฐ ๋•Œ๋ฌธ.
์ฆ‰, Controller = ์ฝ”์–ด๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ์ž…๋ ฅ ์–ด๋Œ‘ํ„ฐ.



๐Ÿ“‘ ๊ฐ ๊ณ„์ธต์— ๋ฌด์—‡์ด ์žˆ์–ด์•ผ ํ•˜๋Š”๊ฐ€ (๊ฐ€์ด๋“œ๋ผ์ธ)

๐Ÿ› Domain

  • ๋„๋ฉ”์ธ ๋ชจ๋ธ, VO, ENUM, ๋„๋ฉ”์ธ ์„œ๋น„์Šค,
  • ๋ถˆ๋ณ€์‹, ๋„๋ฉ”์ธ ์˜ˆ์™ธ
    โžก๏ธ ํ”„๋ ˆ์ž„์›Œํฌ ๋ถˆ๊ฐ€

โš™๏ธ Application

  • Port In (UseCase)
  • Application Service
    • ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ : ๋„๋ฉ”์ธ ๊ทœ์น™์„ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์–ด๋–ค ์ˆœ์„œ๋กœ, ์–ด๋–ค ํฌํŠธ๋ฅผ ๋ถˆ๋Ÿฌ์•ผ ํ•˜๋Š”์ง€ ์กฐํ•ฉํ•˜๋Š” ๊ฒƒ์ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋น„์Šค์˜ ์ฑ…์ž„
  • Port Out (์™ธ๋ถ€ I/O ๊ณ„์•ฝ)
  • App DTO, App Mapper
    โžก๏ธ ์™ธ๋ถ€ I/O ๊ตฌํ˜„ ๋ฐฉ๋ฒ•์€ ๋ชจ๋ฆ„

๐ŸŒ Adapters

  • Web: Controller, Web DTO, Web Mapper
  • Query: QueryDSL Adapter (์กฐ๊ฑด/์กฐ์ธ/ํ”„๋กœ์ ์…˜/์ •๋ ฌ/ํŽ˜์ด์ง•)
  • JPA: Entity, JpaRepository, Impl, Entity Mapper (์˜์† โ†” ๋„๋ฉ”์ธ)
  • Redis: ์ธ์ฆ/๋ณด์•ˆ ๊ด€๋ จ ํ† ํฐ ๊ด€๋ฆฌ (์˜ˆ: Refresh Token ์ €์žฅยท์กฐํšŒ, ๋งŒ๋ฃŒ TTL ์ ์šฉ), ์กฐํšŒ์ˆ˜/์ค‘๋ณต๋ฐฉ์ง€ TTL
  • Index/Search: ๊ฒ€์ƒ‰ ์ธ๋ฑ์‹ฑ/์—…๋ฐ์ดํŠธ, ์กฐํšŒ ์ตœ์ ํ™” (ES/Painless Script ๋“ฑ)


๐Ÿ”„ ์„ค๊ณ„ ํšŒ๋กœ: ๊ตฌ์กฐ๋„ & ์‹œํ€€์Šค

๐Ÿ— ๊ตฌ์กฐ๋„ (Mermaid)

๐Ÿ— ์‹œํ€€์Šค Diagram

ํ”„๋กœ์ ํŠธ ์„ธ๋ถ€ ์ •๋ณด ์กฐํšŒ API๋ฅผ ๊ฐ€์ •
ํ”„๋กœ์ ํŠธ ์กฐํšŒ ์‹œ ์„ธ๋ถ€ ์ •๋ณด ๋ฐ˜ํ™˜ ๋ฐ ํ”„๋กœ์ ํŠธ ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ์ˆ˜ ์ฆ๊ฐ€ ๋กœ์ง

๐Ÿ‘ค ์‚ฌ์šฉ์ž ->> ๐ŸŒ ์›น ์ปจํŠธ๋กค๋Ÿฌ: GET /api/v1/projects/{id}

๐ŸŒ ์›น ์ปจํŠธ๋กค๋Ÿฌ ->> ๐ŸŽฏ ์ž…๋ ฅํฌํŠธ(์œ ์Šค์ผ€์ด์Šค): getProjectDetail(id, userId, viewerId) ํ˜ธ์ถœ
๐ŸŽฏ ์ž…๋ ฅํฌํŠธ(์œ ์Šค์ผ€์ด์Šค) ->> โš™๏ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋น„์Šค: ProjectReadService ์‹คํ–‰
โš™๏ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„œ๋น„์Šค ->> ๐Ÿงฉ ๋„๋ฉ”์ธ๊ทœ์น™: ๋ถˆ๋ณ€์‹/๊ทœ์น™ ํ™•์ธ
โš™๏ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋น„์Šค ->> ๐Ÿ› ๏ธ ์กฐํšŒ ์ถœ๋ ฅํฌํŠธ: findProjectWithDataById(id) ์š”์ฒญ

๐Ÿ› ๏ธ ์กฐํšŒ ์ถœ๋ ฅํฌํŠธ ->> ๐Ÿ“Š ์ฟผ๋ฆฌ ์–ด๋Œ‘ํ„ฐ: ์กฐ๊ฑดยท์กฐ์ธยทํ”„๋กœ์ ์…˜ยท์ •๋ ฌยทํŽ˜์ด์ง• ์ฒ˜๋ฆฌ
๐Ÿ“Š ์ฟผ๋ฆฌ ์–ด๋Œ‘ํ„ฐ -->> ๐Ÿ› ๏ธ ์กฐํšŒ ์ถœ๋ ฅํฌํŠธ: Project + DataIds ๋ฐ˜ํ™˜
โš™๏ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋น„์Šค ->> ๐Ÿ› ๏ธ ๋ ˆ๋””์Šค(์บ์‹œ) ์ถœ๋ ฅํฌํŠธ: increaseViewCount(id, viewerId, "PROJECT") ์š”์ฒญ
๐Ÿ› ๏ธ ๋ ˆ๋””์Šค(์บ์‹œ) ์ถœ๋ ฅํฌํŠธ ->> โšก ๋ ˆ๋””์Šค์–ด๋Œ‘ํ„ฐ: โž• setIfAbsent + increment ์‹คํ–‰

โš™๏ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„œ๋น„์Šค -->> ๐ŸŒ ์›น์ปจํŠธ๋กค๋Ÿฌ: Application DTO ๋ฐ˜ํ™˜
๐ŸŒ ์›น์ปจํŠธ๋กค๋Ÿฌ -->> ๐Ÿ‘ค ์‚ฌ์šฉ์ž: Web DTO ๋ณ€ํ™˜ ํ›„ 200 OK ์‘๋‹ต



๐Ÿ“– ๊ตฌํ˜„ ์˜ˆ์‹œ: ํ”„๋กœ์ ํŠธ ์ƒ์„ธ ์กฐํšŒ (์กฐํšŒ์ˆ˜ยท์ข‹์•„์š” ํฌํ•จ)

๐ŸŒ Web Layer (Driving Adapter)

๐Ÿ“Œ ์—ญํ• 

  • HTTP ์ง„์ž…์ (Controller).
  • ์š”์ฒญ ํ—ค๋”/ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํŒŒ์‹ฑํ•˜๊ณ , Port In(UseCase) ์„ ํ˜ธ์ถœํ•œ๋‹ค.
  • Web DTO โ†” App DTO ๋ณ€ํ™˜์„ ์ฑ…์ž„์ง„๋‹ค.
  • ์ฆ‰, Web Layer๋Š” UI ๊ณ„์•ฝ๋งŒ ๋‹ด๋‹นํ•œ๋‹ค.

๐Ÿ’ป ํ•ต์‹ฌ ์ฝ”๋“œ

@RestController
@RequiredArgsConstructor
public class ProjectReadController implements ProjectReadApi {
    private final GetProjectDetailUseCase getProjectDetailUseCase; // Port In

    @Override
    public ResponseEntity<SuccessResponse<ProjectDetailWebResponse>> getProjectDetail(
            HttpServletRequest request, HttpServletResponse response, Long projectId) {

        Long userId   = extractHeaderUtil.extractAuthenticatedUserIdFromRequest(request);
        String viewer = extractHeaderUtil.extractViewerIdFromRequest(request, response);

        ProjectDetailResponse appDto =
            getProjectDetailUseCase.getProjectDetail(projectId, userId, viewer);

        ProjectDetailWebResponse webDto = projectReadWebMapper.toWebDto(appDto);
        return ResponseEntity.ok(SuccessResponse.of(ProjectSuccessStatus.GET_PROJECT_DETAIL, webDto));
    }
}

๐Ÿ“ ์„ค๋ช…

  • Web Layer๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ์ „ํ˜€ ์—†๋‹ค.
  • ์˜ค์ง ์š”์ฒญ/์‘๋‹ต ๋ณ€ํ™˜๊ณผ ํฌํŠธ ํ˜ธ์ถœ, ์ฟ ํ‚ค์™€ ํ—ค๋” ์„ค์ •์—๋งŒ ์ง‘์ค‘ํ•œ๋‹ค.
  • ๋”ฐ๋ผ์„œ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์‰ฝ๊ณ , UI ๊ณ„์•ฝ(API ์ŠคํŽ™) ๋ณ€๊ฒฝ์—๋„ Application์„ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค.

โš™๏ธ Application Layer (Port + Service + Tx/Orchestration)

๐Ÿ”Œ Ports (๊ณ„์•ฝ)

๐Ÿ“Œ ์—ญํ• 

  • Service์™€ Adapter ๊ฐ„์˜ ์ถ”์ƒ ๊ณ„์•ฝ.
  • Adapter ๊ต์ฒด ๊ฐ€๋Šฅ์„ฑ์„ ํ™•๋ณดํ•œ๋‹ค. (์˜ˆ: JPA โ†’ MyBatis๋กœ ๋ณ€๊ฒฝํ•ด๋„ Service๋Š” ๊ทธ๋Œ€๋กœ)

๐Ÿ’ป ํ•ต์‹ฌ ์ฝ”๋“œ

public interface FindProjectPort {
    Optional<ProjectWithDataIdsResponse> findProjectWithDataById(Long projectId);
}

public interface CacheProjectViewCountPort {
    void increaseViewCount(Long targetId, String viewerId, String targetType);
}

๐Ÿ“ ์„ค๋ช…

  • Service๋Š” Port๋งŒ ๋ฐ”๋ผ๋ณธ๋‹ค.
  • ๊ตฌํ˜„์ฒด๋Š” Adapter์—์„œ ์ œ๊ณตํ•œ๋‹ค.
  • Port = โ€œApplication์˜ ์˜์กด์„ฑ ์—ญ์ „โ€์„ ๊ตฌํ˜„ํ•˜๋Š” ํ•ต์‹ฌ.

๐Ÿ” Service

๐Ÿ“Œ ์—ญํ• 

  • UseCase ๊ตฌํ˜„์ฒด์ด์ž ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„.
  • ์—ฌ๋Ÿฌ Port๋ฅผ ์กฐํ•ฉํ•˜์—ฌ ๋น„์ฆˆ๋‹ˆ์Šค ์š”๊ตฌ์‚ฌํ•ญ์„ ์ถฉ์กฑํ•œ๋‹ค.
  • ๋„๋ฉ”์ธ ๋ถˆ๋ณ€์‹ ๊ฒ€์‚ฌ, ๋ฉฑ๋“ฑ ์ฒ˜๋ฆฌ ํŠธ๋ฆฌ๊ฑฐ ๋“ฑ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ ์ฑ…์ž„๋งŒ ๊ฐ€์ง„๋‹ค.

๐Ÿ’ป ํ•ต์‹ฌ ์ฝ”๋“œ

@Service
@RequiredArgsConstructor
public class ProjectReadService implements GetProjectDetailUseCase {

    private final FindProjectPort findProjectPort;         
    private final ValidateTargetLikeUseCase likeUseCase;   
    private final CacheProjectViewCountPort viewCountPort; 

    @Override @Transactional(readOnly = true)
    public ProjectDetailResponse getProjectDetail(Long projectId, Long userId, String viewerId) {
        var res = findProjectPort.findProjectWithDataById(projectId)
            .orElseThrow(() -> new ProjectException(ProjectErrorStatus.NOT_FOUND_PROJECT));

        boolean isLiked = (userId != null) &&
            likeUseCase.hasUserLikedTarget(userId, projectId, TargetType.PROJECT);

        viewCountPort.increaseViewCount(projectId, viewerId, "PROJECT"); // TTLยท๋ฉฑ๋“ฑ

        return projectDetailDtoMapper.toResponseDto(res.project(), /* ... */ isLiked, /* ... */);
    }
}

๐Ÿ“ ์„ค๋ช…

  • Service๋Š” โ€œ์ด ์ž‘์—…์„ ์œ„ํ•ด ์–ด๋–ค Port๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•˜๋Š”๊ฐ€?โ€๋ฅผ ๊ณ ๋ฏผํ•œ๋‹ค.
  • ๋ชจ๋“  I/O๋Š” Port ๋’ค๋กœ ์ˆจ๊ธฐ๋ฏ€๋กœ, Infra ์„ธ๋ถ€์‚ฌํ•ญ(DB, Redis, ES ๋“ฑ) ์€ Application์ด ๋ชจ๋ฅธ๋‹ค.
  • @Transactional์„ ๋ถ™์—ฌ ํŠธ๋žœ์žญ์…˜ ๋‹จ์œ„(์ฝ๊ธฐ/์“ฐ๊ธฐ)๋ฅผ ๋ณด์žฅํ•œ๋‹ค.

๐Ÿ“Š Adapter - Query (์ฝ๊ธฐ ์ตœ์ ํ™”)

๐Ÿ“Œ ์—ญํ• 

  • ์‹ค์ œ QueryDSL๋กœ DB์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•œ๋‹ค. (Jpa๋Š” Persistence Adapter)
  • ์กฐ๊ฑด/์กฐ์ธ/ํ”„๋กœ์ ์…˜/ํŽ˜์ด์ง•์€ ์ „๋ถ€ Adapter ์•ˆ์—์„œ ๋๋‚ธ๋‹ค.
  • Application์€ โ€œ์ฟผ๋ฆฌ ์„ธ๋ถ€โ€๋ฅผ ๋ชจ๋ฅธ ์ฑ„ ๋‹จ์ˆœ DTO๋‚˜ ์—”ํ‹ฐํ‹ฐ์—์„œ ๋ณ€ํ™˜๋œ ๋„๋ฉ”์ธ ๋ชจ๋ธ๋งŒ ๋ฐ›๋Š”๋‹ค.

๐Ÿ’ป ํ•ต์‹ฌ ์ฝ”๋“œ

@Repository
@RequiredArgsConstructor
public class ReadProjectQueryDslAdapter implements FindProjectPort {
    private final JPAQueryFactory q;

    @Override
    public Optional<ProjectWithDataIdsResponse> findProjectWithDataById(Long id) {
        ProjectEntity e = q.selectFrom(QProjectEntity.projectEntity)
            .where(QProjectEntity.projectEntity.id.eq(id),
                   QProjectEntity.projectEntity.isDeleted.isFalse())
            .fetchOne();
        if (e == null) return Optional.empty();

        return Optional.of(new ProjectWithDataIdsResponse(ProjectEntityMapper.toMinimal(e), List.of()));
    }
}

๐Ÿ“ ์„ค๋ช…

  • DB ์ฟผ๋ฆฌ ์ตœ์ ํ™”๋Š” Adapter์—์„œ๋งŒ ๊ณ ๋ฏผํ•œ๋‹ค.
  • Application์€ โ€œfindProjectWithDataByIdโ€๋ผ๋Š” ๊ณ„์•ฝ๋งŒ ์•Œ๋ฉด ๋œ๋‹ค.
  • ์œ ์ง€๋ณด์ˆ˜์„ฑ์ด ๋†’์•„์ง€๊ณ , DB ๊ต์ฒด(MySQL โ†’ PostgreSQL) ์‹œ Application์€ ๋ณ€๊ฒฝ์ด ์—†๋‹ค.

๐Ÿ”„ Adapter - Redis (์กฐํšŒ์ˆ˜ ๋ฉฑ๋“ฑ)

๐Ÿ“Œ ์—ญํ• 

  • TTL(5๋ถ„) ๊ธฐ๋ฐ˜์œผ๋กœ ์กฐํšŒ์ˆ˜ ์ค‘๋ณต ๋ฐฉ์ง€.
  • ๊ฐ™์€ viewer๊ฐ€ 5๋ถ„ ๋‚ด ์—ฌ๋Ÿฌ ๋ฒˆ ์กฐํšŒํ•ด๋„ 1ํšŒ๋กœ๋งŒ ์นด์šดํŠธ.
  • ์ƒˆ๋กœ๊ณ ์นจ/๋ด‡ ์ŠคํŒŒ์ดํฌ๋ฅผ ์™„์ถฉ.

๐Ÿ’ป ํ•ต์‹ฌ ์ฝ”๋“œ

@Component
@RequiredArgsConstructor
public class ProjectViewCountRedisAdapter implements CacheProjectViewCountPort {
    private final StringRedisTemplate redis;

    @Override
    public void increaseViewCount(Long projectId, String viewerId, String targetType) {
        if (viewerId == null) return;
        String dedupKey = "viewDedup:%s:%s:%s".formatted(targetType, projectId, viewerId);

        Boolean first = redis.opsForValue().setIfAbsent(dedupKey, "1", Duration.ofMinutes(5));
        if (Boolean.TRUE.equals(first)) {
            String countKey = "viewCount:%s:%s".formatted(targetType, projectId);
            redis.opsForValue().increment(countKey);
        }
    }
}

๐Ÿ“ ์„ค๋ช…

  • Redis๋Š” ์งง์€ TTL ๋ฉฑ๋“ฑ ๋ณด์žฅ์— ํŠนํ™”๋œ ์ €์žฅ์†Œ.
  • ๋ณธ ์˜ˆ์‹œ์—์„œ๋Š” โ€œ์กฐํšŒ์ˆ˜ ์ฆ๊ฐ€โ€๋ฅผ ์•ˆ์ •์ ์œผ๋กœ ๋‹ค๋ฃจ๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ–ˆ๋‹ค.
  • ๋‚˜์ค‘์— RDB์™€ ๋™๊ธฐํ™” ์‹œ์—๋„ ์•ˆ์ •์ ์ด๋‹ค. (Scheduler Or Batch)

๐Ÿ—„๏ธ JPA Entity & Mapper

๐Ÿ“Œ ์—ญํ• 

  • DB ํ…Œ์ด๋ธ” ๋งคํ•‘ ์ „์šฉ.
  • ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์—†์Œ.
  • Domain ๊ฐ์ฒด์™€ ๋ถ„๋ฆฌํ•˜์—ฌ DB์™€ ๊ฒฐํ•ฉํ•œ Adapter ์ „์šฉ์œผ๋กœ ์‚ฌ์šฉ.

๐Ÿ’ป ํ•ต์‹ฌ ์ฝ”๋“œ

@Entity @Table(name = "project")
@Where(clause = "is_deleted = false")
public class ProjectEntity {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private Long commentCount = 0L;
    private Long likeCount    = 0L;
    private Long viewCount    = 0L;
    ...
}

๐Ÿ“ ์„ค๋ช…

  • Entity๋Š” ์˜ค์ง DB์™€ ๊ฒฐํ•ฉ๋œ ORM์œผ๋กœ DB์—์„œ ์ƒ์„ฑ, ์ˆ˜์ •, ์กฐํšŒ, ์‚ญ์ œ ๋“ฑ์˜ ์ž‘์—…์— ์‚ฌ์šฉ๋œ๋‹ค.
  • Mapper๊ฐ€ Entity <-> Domain ๋ณ€ํ™˜์„ ๋‹ด๋‹นํ•˜์—ฌ ๊ด€์‹ฌ์‚ฌ๋ฅผ ๋ถ„๋ฆฌํ•œ๋‹ค.

ํ•ต์‹ฌ: ์˜์† ํ‘œํ˜„(JPA) โ†” ๋„๋ฉ”์ธ ๋ณ€ํ™˜์€ Entity Mapper์—์„œ๋งŒ. ๋ชจ๋ธ ์นจ์‹ ๋ฐฉ์ง€.



๐Ÿš€ ์ •๋ฆฌ

  • Web Layer: ์š”์ฒญ/์‘๋‹ต ๊ณ„์•ฝ, Port In ํ˜ธ์ถœ
  • Application: Txยท์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜, Port Out ํ˜ธ์ถœ
  • Ports: ๊ณ„์•ฝ ์ •์˜, Adapter ๊ต์ฒด ๊ฐ€๋Šฅ์„ฑ ๋ณด์žฅ
  • Adapter: Web, Db, Persistence, Query, Redis ๋“ฑ ์‹ค์ œ ๊ตฌํ˜„์„ ๋‹ด๋‹น

๐Ÿ‘‰ ์ด ๊ตฌ์กฐ ๋•๋ถ„์—

  • UI/API ๋ณ€๊ฒฝ ์‹œ Application์€ ์˜ํ–ฅ ์—†์Œ
  • Infra ๊ต์ฒด(JPA โ†’ MyBatis, Redis โ†’ Memcached)์—๋„ Service๋Š” ๊ทธ๋Œ€๋กœ
  • ๊ฐ ๊ณ„์ธต์€ ์ž๊ธฐ ์ฑ…์ž„๋งŒ ์ˆ˜ํ–‰


โœจ ํ•ต์‹ฌ ์š”์•ฝ

โœ… ์–ป๋Š” ์ด์ 

  • ๐Ÿ”’ ๋ณ€๊ฒฝ ๋‚ด์„ฑ: ์ €์žฅ์†Œยท๊ฒ€์ƒ‰ยท์บ์‹œ ๊ต์ฒด ์‹œ ์–ด๋Œ‘ํ„ฐ๋งŒ ์ˆ˜์ •, ์ฝ”์–ด(์œ ์Šค์ผ€์ด์Šคยท๋„๋ฉ”์ธ)๋Š” ์•ˆ์ •์ 
  • ๐Ÿ‘€ ๊ฐ€๋…์„ฑยท์œ ์ง€๋ณด์ˆ˜์„ฑ: ๊ณ„์ธต ์—ญํ• ์ด ๋ถ„๋ฆฌ๋ผ ์ฝ”๋“œ ์˜๋„๊ฐ€ ์„ ๋ช…ํ•˜๊ณ  ์˜จ๋ณด๋”ฉ ์šฉ์ด
  • โšก ์„ฑ๋Šฅยท์šด์˜ ์ตœ์ ํ™”: Query Adapter์— ์„ฑ๋Šฅ ํŠœ๋‹ ์ง‘์ค‘, Redis TTL๋กœ ์ค‘๋ณต ์š”์ฒญ ๋ฐฉ์–ด

โš–๏ธ ๊ณ ๋ ค์‚ฌํ•ญ

  • ์ดˆ๊ธฐ์— PortยทMapper๊ฐ€ ๋งŽ์•„๋ณด์—ฌ ์ง„์ž… ์žฅ๋ฒฝ์ด ์žˆ์Œ
  • CQRS-lite๋Š” ํ•„์š”ํ•œ ๊ณณ๋งŒ ์ ์šฉํ•ด์•ผ ํ•˜๋ฉฐ, ๊ณผ๋„ํ•œ ๋ถ„๋ฆฌ๋Š” ์˜คํžˆ๋ ค ๋ณต์žก๋„โ†‘
  • ์˜์กด ๋ฐฉํ–ฅ: Adapters โ†’ Application โ†’ Domain
  • Controller๋Š” Port In๋งŒ ํ˜ธ์ถœ (Repo ์ง์ ‘ ์ ‘๊ทผ ๊ธˆ์ง€)
  • Service๋Š” Tx/์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ ์ „์šฉ, ๋ชจ๋“  I/O๋Š” Port Out ๊ฒฝ์œ 
  • Query/์—…์„œํŠธ/TTL/๋ฉฑ๋“ฑ์€ Adapter ๋‚ด๋ถ€ ์ „์šฉ
  • ๊ฒฝ์Ÿ ํ•„๋“œ(์กฐํšŒ์ˆ˜ยท์ข‹์•„์š”)๋Š” ๋ฝยท์›์ž ์—ฐ์‚ฐยทTTLยท๋ฉฑ๋“ฑ์œผ๋กœ ๋ฐฉ์–ด


๐ŸŽฏ ๋งˆ๋ฌด๋ฆฌ

์™œ ์–ด๋Œ‘ํ„ฐ์ธ๊ฐ€?

โ†’ ๊ตฌํ˜„์„ ์ฝ”์–ด ๋ฐ–์œผ๋กœ ๋ฐ€์–ด๋‚ด ์ฝ”์–ด ๊ทœ์น™ ๋ณด์กด

์™œ Controller๋„ ์–ด๋Œ‘ํ„ฐ์ธ๊ฐ€?

โ†’ ํ—ฅ์‚ฌ๊ณ ๋‚ ์—์„œ UI๋Š” Driving Adapter(Port In ํ˜ธ์ถœ์ž)

๊ณ„์ธต ์—ญํ• 

  • Domain โ†’ ๋„๋ฉ”์ธ ๋ชจ๋ธ, ๊ทœ์น™, ๋ถˆ๋ณ€์‹
  • Application โ†’ ํฌํŠธ, Tx, ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜
  • Adapters โ†’ I/O ๊ตฌํ˜„

๐Ÿ‘‰ ๊ฐ€๋…์„ฑยท๋ณ€๊ฒฝ ๋‚ด์„ฑยท์šด์˜ ์•ˆ์ •์„ฑ์„ ๋™์‹œ์— ํ™•๋ณดํ•  ์ˆ˜ ์žˆ๋‹ค.
์ด๋ ‡๊ฒŒ ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์—์„œ ์ด ์„ค๊ณ„ ๊ตฌ์กฐ๋ฅผ ํ† ๋Œ€๋กœ ๊ฐœ๋ฐœ์„ ์ง„ํ–‰ํ•˜๊ณ  ์žˆ๋Š”๋ฐ ์„ค๊ณ„ ๊ตฌ์กฐ๋Š” ์ •๋‹ต์ด ์—†๊ธฐ์— ๊ณ„์†ํ•ด์„œ ์„ค๊ณ„์— ๋Œ€ํ•ด์„œ ๊ณ ๋ฏผํ•ด๋ณด๋Š” ๊ณผ์ •์„ ๊ฑฐ์น˜๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค!!

profile
๋งค์ผ ๋งค์ผ ์„ฑ์žฅํ•˜๊ธฐ

5๊ฐœ์˜ ๋Œ“๊ธ€

comment-user-thumbnail
2025๋…„ 8์›” 17์ผ

์˜ค์˜ค์˜ค ๊ธ€์ด ๋„ˆ๋ฌด ์ข‹๋‹ค. ๊ทผ๋ฐ ํ•˜๋‚˜ ์ƒ๊ฐํ•  ๋ถ€๋ถ„์ด ์žˆ์„ ๊ฒƒ ๊ฐ™์•„.

"CacheProjectViewCountPort" ์ด๋ ‡๊ฒŒ ์บ์‹œ๋ผ๋Š” ๋งฅ๋ฝ์ด ์ธํ„ฐํŽ˜์ด์Šค๋กœ ๋‚˜์˜จ๋‹ค๋ฉด ์‚ฌ์šฉํ•˜๋Š” ๊ธฐ์ˆ ์ด ์™ธ๋ถ€๋กœ ๋…ธ์ถœ๋˜๋Š”๊ฑฐ ์•„๋‹๊นŒ, "ModifyProjectViewCountPort"๋กœํ•˜์ง€ ์•Š์€ ์ด์œ ๊ฐ€ ์žˆ์„๊นŒ?

์™œ๋ƒํ•˜๋ฉด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ ๊ตฌํ˜„ํ•˜๋”๋ผ๋„ ์ด์ƒํ•˜์ง€ ์•Š์€ ์ธํ„ฐํŽ˜์ด์Šค๋ผ๊ณ  ์ƒ๊ฐ์ด ๋“ค์–ด์„œ

3๊ฐœ์˜ ๋‹ต๊ธ€