Skip to content

Latest commit

Β 

History

History
313 lines (237 loc) Β· 9.85 KB

File metadata and controls

313 lines (237 loc) Β· 9.85 KB

Azit ν”„λ‘œμ νŠΈ μ½”λ“œ 생성 κ°€μ΄λ“œ

이 νŒŒμΌμ€ Claudeκ°€ Azit ν”„λ‘œμ νŠΈμ—μ„œ μ½”λ“œλ₯Ό μƒμ„±ν•˜κ±°λ‚˜ 리뷰할 λ•Œ 항상 μ€€μˆ˜ν•΄μ•Ό ν•˜λŠ” κ·œμΉ™μ„ μ •μ˜ν•©λ‹ˆλ‹€.


1. μ•„ν‚€ν…μ²˜: ν—₯사고날 μ•„ν‚€ν…μ²˜ (Hexagonal Architecture)

도메인 쀑심 섀계(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 λ§€ν•‘

Domain 계측 κ·œμΉ™

  • 순수 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;
    }
}

Application 계측 κ·œμΉ™

  • 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);
}

Adapter 계측 κ·œμΉ™

  • Web Adapter: μš”μ²­ DTO β†’ Command λ³€ν™˜ ν›„ UseCase 호좜, 응닡은 CommonResponse 규격 μ‚¬μš©.
  • Persistence Adapter: JPA Entity ↔ Domain Model λ§€ν•‘ μ±…μž„μ„ κ°€μ§‘λ‹ˆλ‹€.

Infrastructure 계측 κ·œμΉ™

  • νŠΉμ • 도메인에 μ’…μ†λ˜μ§€ μ•ŠλŠ” μ „μ—­ 기술 기반 μš”μ†Œλ§Œ μœ„μΉ˜ν•©λ‹ˆλ‹€.
  • ꡬ성 μš”μ†Œ μ˜ˆμ‹œ: SecurityConfig, JwtProvider, RedisConfig, GlobalExceptionHandler

2. λͺ…λͺ… κ·œμΉ™ (Naming Convention)

ν—ˆμš©ν•˜λŠ” ν‘œμ€€ μ•½μ–΄

μ•½μ–΄ μ‚¬μš© 예
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) { ... }

3. 기술 μŠ€νƒ μ€€μˆ˜

  • 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;

4. 곡톡 응닡 및 μ˜ˆμ™Έ 처리

API 응닡 규격

λͺ¨λ“  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;
}

5. λ‹¨μœ„ ν…ŒμŠ€νŠΈ μ½”λ“œ μž‘μ„± κ·œμΉ™

κΈ°λ³Έ 원칙

  • λͺ¨λ“  핡심 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직(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

도메인 ν…ŒμŠ€νŠΈ μ˜ˆμ‹œ (Mock λΆˆν•„μš”)

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();
    }
}

6. λΉ λ₯Έ 체크리슀트

μ½”λ“œ 생성 λ˜λŠ” 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 μˆœμΈκ°€?
  • 핡심 λ‘œμ§μ— λ‹¨μœ„ ν…ŒμŠ€νŠΈκ°€ μž‘μ„±λ˜μ–΄ μžˆλŠ”κ°€?
  • ν…ŒμŠ€νŠΈ λ©”μ„œλ“œλͺ…이 μ˜μ–΄ λ©”μ„œλ“œλͺ…_상황_κΈ°λŒ€κ²°κ³Ό ꡬ쑰λ₯Ό λ”°λ₯΄λŠ”κ°€?