Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BSVR-44] 영속성 계층 (JPA) 모듈 추가 #6

Merged
merged 8 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions application/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
dependencies {
implementation(project(":domain"))
implementation(project(":usecase"))
implementation(project(":infrastructure:jpa"))

// spring
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package org.depromeet.spot.application.config;

import org.depromeet.spot.jpa.config.JpaConfig;
import org.depromeet.spot.usecase.config.UsecaseConfig;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@ComponentScan(basePackages = {"org.depromeet.spot.application"})
@Configuration
@Import({JpaConfig.class})
@Import(value = {UsecaseConfig.class, JpaConfig.class})
public class SpotApplicationConfig {}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package org.depromeet.spot.application.member.controller;

import org.depromeet.spot.application.member.controller.request.MemberRequest;
import org.depromeet.spot.application.member.controller.response.MemberResponse;
import org.depromeet.spot.application.member.service.port.MemberService;
import org.depromeet.spot.application.member.dto.request.MemberRequest;
import org.depromeet.spot.application.member.dto.response.MemberResponse;
import org.depromeet.spot.usecase.port.in.MemberUsecase;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -22,13 +22,13 @@
@RequestMapping("/api/members")
public class MemberController {

private final MemberService memberService;
private final MemberUsecase memberUsecase;

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@Operation(summary = "Member 생성 API")
public MemberResponse create(@RequestBody MemberRequest request) {
val member = memberService.create(request.name());
val member = memberUsecase.create(request.name());
return MemberResponse.from(member);
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UseCase에서도 Dto를 만들어서 한 번 더 검증을 수행하지 않은 건 우리 코드 복잡성이 아직 크지 않아서인거 맞아?
컨트롤러에서 MemberRequest로 HTTP요청에 대한 유효성을 검사하잖아(필수 필드가 왔는지, 유효한 형식인지) 등등, 그러면 Service로 넘길 때 UseCase에 대한 MemberDto를 사용하면 비지니스 로직에 대한 유효성을 검사할 수 있을 것 같아.

분리했을 때의 장점은 만약 우리가 API request에 대한 형식을 바꾸거나 확장하면서 업데이트 할 일이 생긴다면 비지니스 로직에 대한 데이터 검증은 UseCase에 역할을 덜어놨으니 MemberRequest dto에서만 수정을 하면 되고!

그래서 만약 UseCase Dto를 만든다면 수정방향은
Application 내의 controller

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@Operation(summary = "Member 생성 API")
public MemberResponse create(@RequestBody MemberRequest request) {
    MemberDto memberDto = new MemberDto(null, request.name()); // MemberDto는 usecase내에 있음!
    MemberDto createdMember = memberUsecase.create(memberDto);
    return MemberResponse.from(createdMember);
}

UseCase내의 MemberService.java

@Service
@RequiredArgsConstructor
public class MemberService implements MemberUsecase {
    private final MemberRepository memberRepository;

    @Override
    public MemberDto create(final MemberDto memberDto) {
        Member member = new Member(null, memberDto.getName());
        Member savedMember = memberRepository.save(member);
        return new MemberDto(savedMember.getId(), savedMember.getName());
    }
}

그리고 UseCase내에 MemberDto 추가..

Copy link
Collaborator Author

@EunjiShin EunjiShin Jul 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

궁금한 점이 있어! usecase로 넘어갈 때 Dto를 한 번 더 사용하는 것의 이점이 뭘까?

UseCase에 대한 MemberDto를 사용하면 비지니스 로직에 대한 유효성을 검사할 수 있을 것 같아.

라고 해줬는데, 어차피 로직에 참여하는 주체는 도메인이니까, 유효성 검사는 도메인에서 처리해야 하지 않을까?

  • 외부 통신체 (request, response 등)의 유효성은 controller의 dto에서 처리
    • ex. 길이 체크, not null 체크 등.. 클라이언트가 보낸 요청이 우리 api의 포맷에 부합하는가 확인
  • 비즈니스 로직 관련 유효성 검사는 도메인에서 처리
    • ex. 우리 서비스는 성인만 이용할 수 있다던지.. 도메인 로직 (요구사항)에 부합하는가 확인

DTO는 말 그대로 계층간 이동을 위해 존재하는 객체라서(data transfer), 여기에서 비즈니스 로직을 작성하는건 성격에 맞지 않는 것 같아.

우리가 API request에 대한 형식을 바꾸거나 확장하면서 업데이트 할 일이 생긴다면 비지니스 로직에 대한 데이터 검증은 UseCase에 역할을 덜어놨으니 MemberRequest dto에서만 수정을 하면 되고!

이건 requestDto를 바로 도메인으로 바꾸는 지금과 동일한 것 같고.

DTO는 계층 이동에 사용되는 객체이고, usecase와 domain은 같은 계층이라는 생각이 들었어.
infrastructure는 영속성, application은 http로, 이후 충분히 변동 가능성이 있는 계층이지만, usecase와 domain은 모두 외부에 의존하는 값이 아니라 순수 Java로 동작할 계층이니까!

(ex. rdb는 nosql로, http 어플리케이션은 grpc로 바뀔 수 있지만, usecase와 domain은 항상 자바 로직일 것)

결과적으로 usecase dto는 도메인과 request/response의 맵퍼 정도의 역할만 수행해주는 것 같아. 혹시 내가 의도를 잘못 이해했다면 알려주어~

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree!! LGTM~

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.depromeet.spot.application.member.dto.request;

public record MemberRequest(String name) {}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.depromeet.spot.application.member.controller.response;
package org.depromeet.spot.application.member.dto.response;

import org.depromeet.spot.domain.member.Member;

Expand Down

This file was deleted.

1 change: 1 addition & 0 deletions infrastructure/jpa/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
dependencies {
implementation(project(":domain"))
implementation(project(":usecase"))

// spring
implementation("org.springframework.boot:spring-boot-starter-data-jpa:_")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import org.depromeet.spot.domain.member.Member;
import org.depromeet.spot.jpa.member.entity.MemberEntity;
import org.depromeet.spot.jpa.member.repository.port.MemberRepository;
import org.depromeet.spot.usecase.port.out.MemberRepository;
import org.springframework.stereotype.Repository;

import lombok.RequiredArgsConstructor;
Expand Down
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ include("application")
include("infrastructure")
include("infrastructure:jpa")
findProject(":infrastructure:jpa")?.name = "jpa"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jpa로 이름 재정의하는거구나. 간결하게 파악하려고 하는 거 외에 다른 이유가 있어?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요거는 IntelliJ에서 모듈 생성하면 자동으로 작성되는 코드더라고!

include("usecase")
42 changes: 42 additions & 0 deletions usecase/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/

### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/

### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

### VS Code ###
.vscode/

### Mac OS ###
.DS_Store
10 changes: 10 additions & 0 deletions usecase/build.gradle.kts
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
dependencies {
implementation(project(":domain"))

// spring
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.springframework.boot:spring-boot-starter-web")
}

tasks.bootJar { enabled = false }
tasks.jar { enabled = true }
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.depromeet.spot.usecase.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(
basePackages = {
"org.depromeet.spot.usecase.port",
"org.depromeet.spot.usecase.service",
})
public class UsecaseConfig {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.depromeet.spot.usecase.port.in;

import org.depromeet.spot.domain.member.Member;

public interface MemberUsecase {

Member create(String name);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.depromeet.spot.jpa.member.repository.port;
package org.depromeet.spot.usecase.port.out;

import org.depromeet.spot.domain.member.Member;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package org.depromeet.spot.application.member.service;
package org.depromeet.spot.usecase.service;

import org.depromeet.spot.application.member.service.port.MemberService;
import org.depromeet.spot.domain.member.Member;
import org.depromeet.spot.jpa.member.repository.port.MemberRepository;
import org.depromeet.spot.usecase.port.in.MemberUsecase;
import org.depromeet.spot.usecase.port.out.MemberRepository;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import lombok.val;

@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {
public class MemberService implements MemberUsecase {

private final MemberRepository memberRepository;

Expand Down