μ΄ νμΌμ Claudeκ° Azit νλ‘μ νΈμμ μ½λλ₯Ό μμ±νκ±°λ 리뷰ν λ νμ μ€μν΄μΌ νλ κ·μΉμ μ μν©λλ€.
λλ©μΈ μ€μ¬ μ€κ³(DDD)λ₯Ό κΈ°λ°μΌλ‘ νλ©°, μμ‘΄μ±μ λ°λμ Adapter β Application β Domain λ°©ν₯μΌλ‘λ§ νλ¦ λλ€.
com.youthexpedition.azit
βββ infrastructure/ # μ μ μ€μ (Security, JWT, Redis, GlobalExceptionHandler)
βββ modules/
βββ {module}/ # auth, member, crew, store, location, image ...
βββ domain/
β βββ model/ # μμ λΉμ¦λμ€ λ‘μ§ λ° λλ©μΈ λͺ¨λΈ (μΈλΆ μμ‘΄μ± κΈμ§)
β βββ enums/ # λλ©μΈ μλ¬ μ½λ, μνκ° Enum
βββ application/
β βββ port/
β β βββ in/ # UseCase μΈν°νμ΄μ€, Command/Query DTO
β β βββ out/ # μμμ±/μΈλΆ API μ°λ Port μΈν°νμ΄μ€
β βββ service/ # UseCase ꡬν체
βββ adapter/
βββ in/web/ # REST Controller (CommonResponse κ·κ²© μ€μ)
βββ out/
βββ persistence/ # PersistenceAdapter
β βββ entity/ # JPA Entity (*Entity λ€μ΄λ°)
βββ mapper/ # Domain β Entity λ§€ν
- μμ POJOμ¬μΌ νλ©°, Spring / JPA / Jakarta λ± μΈλΆ νλ μμν¬ μμ‘΄μ±μ κ°μ Έμ μ λ©λλ€.
@Entity,@Table,@NotBlankλ±μ μ΄λ Έν μ΄μ μ λλ©μΈ λͺ¨λΈμ μ§μ μ¬μ© κΈμ§μ λλ€.- JPA λ§€νμ
adapter/out/persistence/entity/μλ*Entityν΄λμ€μμλ§ ν©λλ€. - Lombok νμ© (μ€μ©μ μμΈ):
@Getter,@Builder,@NoArgsConstructor,@AllArgsConstructorλ± μ»΄νμΌ νμ μ΄λ Έν μ΄μ μ νμ©ν©λλ€. - BusinessException νμ© (μ€μ©μ μμΈ): λλ©μΈ λ΄λΆμμ λΉμ¦λμ€ κ·μΉ μλ°μ ννν λ
BusinessExceptionμ μ§μ λμ§λ κ²μ νμ©ν©λλ€.
// β
μ¬λ°λ₯Έ λλ©μΈ λͺ¨λΈ
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member {
private Long id;
private String nickname;
private MemberStatus status;
public void withdraw() {
if (this.status == MemberStatus.WITHDRAWN) {
throw new BusinessException(MemberErrorCode.MEMBER_ALREADY_WITHDRAWN);
}
this.status = MemberStatus.WITHDRAWN;
}
}- Inbound Port: UseCase μΈν°νμ΄μ€λ₯Ό μ μν©λλ€.
- Outbound Port: μμμ±/μΈλΆ API μ°λμ μν μΈν°νμ΄μ€λ₯Ό μ μν©λλ€.
- λλ©μΈ λͺ¨λΈμ Controller λ± μΈλΆ κ³μΈ΅μ μ§μ λ ΈμΆνμ§ μμ΅λλ€. λ°λμ Command/Query DTOλ‘ λ³νν©λλ€.
// Inbound Port
public interface MemberUseCase {
void withdraw(Long memberId, String accessToken);
}
// Outbound Port
public interface SaveMemberPort {
void save(Member member);
}- Web Adapter: μμ² DTO β Command λ³ν ν UseCase νΈμΆ, μλ΅μ
CommonResponseκ·κ²© μ¬μ©. - Persistence Adapter: JPA Entity β Domain Model λ§€ν μ± μμ κ°μ§λλ€.
- νΉμ λλ©μΈμ μ’ μλμ§ μλ μ μ κΈ°μ κΈ°λ° μμλ§ μμΉν©λλ€.
- κ΅¬μ± μμ μμ:
SecurityConfig,JwtProvider,RedisConfig,GlobalExceptionHandler
| μ½μ΄ | μ¬μ© μ |
|---|---|
DTO |
UserSignUpDTO |
VO |
MoneyVO |
Impl |
UserServiceImpl |
API |
PaymentAPI |
ID |
UserId |
| β μ§μ | β μ¬μ© |
|---|---|
req |
Request |
res |
Response |
cnt |
Count |
svc |
Service |
mgr |
Manager |
// β
public CommonResponse<Void> signup(UserSignupReq req) { ... }
// β
public CommonResponse<Void> signup(UserSignupRequest request) { ... }- Java 21: Switch Expressions, Record, Virtual Threads λ± μ΅μ λ¬Έλ² μ κ·Ή νμ©
- Spring Boot 3.4.1: Security Configλ λλ€ μ€νμΌ(
.authorizeHttpRequests(auth -> auth...)) μ¬μ© - Jakarta:
javax.*ν¨ν€μ§ λμjakarta.*ν¨ν€μ§ μ¬μ©
// β
import javax.persistence.Entity;
// β
import jakarta.persistence.Entity;λͺ¨λ API μλ΅μ CommonResponse<T>λ₯Ό μ¬μ©νλ©°, λ°λμ CommonSuccessCodeλ₯Ό ν¨κ» μ λ¬ν©λλ€.
// λ°μ΄ν° μλ μ±κ³΅ μλ΅
return CommonResponse.of(CommonSuccessCode.SUCCESS, response);
// λ°μ΄ν° μλ μ±κ³΅ μλ΅
return CommonResponse.of(CommonSuccessCode.SUCCESS);λΉμ¦λμ€ μμΈλ λ°λμ BusinessExceptionκ³Ό λλ©μΈλ³ μλ¬ μ½λλ₯Ό μ¬μ©ν©λλ€.
// β
μ¬λ°λ₯Έ μμΈ μ²λ¦¬
throw new BusinessException(MemberErrorCode.MEMBER_NOT_FOUND);
throw new BusinessException(AuthErrorCode.TOKEN_REUSE_DETECTED);
// β μ§μ (λ©μμ§ νλμ½λ©)
throw new RuntimeException("User not found");μλ¬ μ½λλ BaseErrorCodeλ₯Ό ꡬνν λλ©μΈλ³ EnumμΌλ‘ κ΄λ¦¬ν©λλ€.
νλ μμ: code β message β status (HttpStatus)
@Getter
@AllArgsConstructor
public enum MemberErrorCode implements BaseErrorCode {
MEMBER_NOT_FOUND("MEMBER_NOT_FOUND", "μ‘΄μ¬νμ§ μλ μ¬μ©μμ
λλ€.", HttpStatus.NOT_FOUND),
MEMBER_ALREADY_WITHDRAWN("MEMBER_ALREADY_WITHDRAWN", "μ΄λ―Έ νν΄ν μ¬μ©μμ
λλ€.", HttpStatus.BAD_REQUEST);
private final String code;
private final String message;
private final HttpStatus status;
}- λͺ¨λ ν΅μ¬ λΉμ¦λμ€ λ‘μ§(Domain, Application κ³μΈ΅)μλ λ°λμ λ¨μ ν μ€νΈλ₯Ό ν¬ν¨ν©λλ€.
- ν μ€νΈλ μΈλΆ μμ‘΄μ±μ Mockitoλ‘ κ²©λ¦¬νμ¬ λΉ λ₯΄κ³ λ 립μ μΌλ‘ μ€ν κ°λ₯ν΄μΌ ν©λλ€.
- ν
μ€νΈ λ©μλλͺ
μ μμ΄ snake_case λ‘ μμ±νλ©°,
λ©μλλͺ _μν©_κΈ°λ결과ꡬ쑰λ₯Ό λ°λ¦ λλ€.
@Test
void signup_fail_duplicateEmail() { ... }
@Test
void signup_success() { ... }
@Test
void findById_throwsException_whenMemberNotFound() { ... }@ExtendWith(MockitoExtension.class)
class MemberServiceTest {
@InjectMocks
private MemberService memberService;
@Mock
private LoadMemberPort loadMemberPort;
@Mock
private SaveMemberPort saveMemberPort;
@Test
void withdraw_throwsException_whenMemberAlreadyWithdrawn() {
// given
Member member = Member.builder()
.id(1L)
.status(MemberStatus.WITHDRAWN)
.build();
given(loadMemberPort.findById(1L)).willReturn(Optional.of(member));
// when & then
assertThatThrownBy(() -> memberService.withdraw(1L, "accessToken"))
.isInstanceOf(BusinessException.class)
.hasFieldOrPropertyWithValue("errorCode", MemberErrorCode.MEMBER_ALREADY_WITHDRAWN);
}
}| κ³μΈ΅ | ν μ€νΈ μ’ λ₯ | μ¬μ© λꡬ |
|---|---|---|
| Domain | μμ λ¨μ ν μ€νΈ | JUnit 5 (Mock λΆνμ) |
| Application (UseCase) | λ¨μ ν μ€νΈ + Mock | JUnit 5 + Mockito |
| Web Adapter (Controller) | μ¬λΌμ΄μ€ ν μ€νΈ | @WebMvcTest + MockMvc |
| Persistence Adapter | μ¬λΌμ΄μ€ ν μ€νΈ | @DataJpaTest |
class MemberTest {
@Test
void withdraw_success() {
// given
Member member = Member.builder()
.status(MemberStatus.ACTIVE)
.build();
// when
member.withdraw();
// then
assertThat(member.getStatus()).isEqualTo(MemberStatus.WITHDRAWN);
}
@Test
void withdraw_throwsException_whenAlreadyWithdrawn() {
// given
Member member = Member.builder()
.status(MemberStatus.WITHDRAWN)
.build();
// when & then
assertThatThrownBy(member::withdraw)
.isInstanceOf(BusinessException.class)
.hasFieldOrPropertyWithValue("errorCode", MemberErrorCode.MEMBER_ALREADY_WITHDRAWN);
}
}λ°λ³΅ μ¬μ©λλ ν μ€νΈ λ°μ΄ν°λ λ³λμ Fixture ν΄λμ€λ‘ λΆλ¦¬ν©λλ€.
public class MemberFixture {
public static Member activeMember() {
return Member.builder()
.id(1L)
.nickname("testUser")
.status(MemberStatus.ACTIVE)
.build();
}
}μ½λ μμ± λλ PR 리뷰 μ μλ νλͺ©μ νμΈν©λλ€.
- Domain κ³μΈ΅μ
@Entity,@NotBlankλ± μΈλΆ μ΄λ Έν μ΄μ μ΄ μλκ°? - JPA Entityλ
adapter/out/persistence/entity/μλ*Entityν΄λμ€λ‘ λΆλ¦¬λμ΄ μλκ°? - λλ©μΈ λͺ¨λΈμ΄ Controller λ μ΄μ΄κΉμ§ μ§μ λ ΈμΆλμ§ μλκ°?
-
javax.*λμjakarta.*λ₯Ό μ¬μ©νκ³ μλκ°? -
req,res,svcλ± λͺ¨νΈν μ€μλ§μ μ¬μ©νμ§ μμλκ°? - λΉμ¦λμ€ μμΈλ
BusinessException+ λλ©μΈ μλ¬ μ½λλ‘ μ²λ¦¬νλκ°? - λͺ¨λ API μλ΅μ΄
CommonResponse.of(CommonSuccessCode.SUCCESS, ...)κ·κ²©μ λ°λ₯΄λκ°? - ErrorCode Enum νλ μμκ°
code β message β statusμμΈκ°? - ν΅μ¬ λ‘μ§μ λ¨μ ν μ€νΈκ° μμ±λμ΄ μλκ°?
- ν
μ€νΈ λ©μλλͺ
μ΄ μμ΄
λ©μλλͺ _μν©_κΈ°λ결과ꡬ쑰λ₯Ό λ°λ₯΄λκ°?