Java 21 & Spring Boot 3.4 ์ฝ๋ ์์ฑ ์คํ์ผ ๊ฐ์ด๋์ ๋๋ค.
| ๊ธฐ์ | ์ ์ฉ | ์ด์ |
|---|---|---|
| record DTO | โ | ๋ถ๋ณ์ฑ ๋ณด์ฅ, ๋ณด์ผ๋ฌํ๋ ์ดํธ ์ ๊ฑฐ |
| Virtual Threads | โ | ๋์์ฑ ์ฒ๋ฆฌ ์ฑ๋ฅ ํฅ์ |
| FixtureMonkey | โ | ํ ์คํธ ๋ฐ์ดํฐ ์๋ ์์ฑ |
# application.yml
spring:
threads:
virtual:
enabled: true| ๊ตฌ๋ถ | ๊ถ์ฅ | ์ด์ |
|---|---|---|
| Request/Response DTO | record |
๋ถ๋ณ, ๊ฐ๊ฒฐ, ์์ Java |
| Entity | class + Lombok |
JPA ๊ธฐ๋ณธ ์์ฑ์ ์๊ตฌ |
| Service/Controller | Lombok |
@RequiredArgsConstructor |
| Config | Lombok |
@RequiredArgsConstructor |
ํ๋ฒ ์์ฑ๋๋ฉด ๊ฐ์ ๋ฐ๊ฟ ์ ์๋ค๋ ์๋ฏธ์ ๋๋ค.
// record๋ setter๊ฐ ์์
MemberResponse response = new MemberResponse(1L, "ํ๊ธธ๋");
response.setName("๊น์ฒ ์"); // โ ์ปดํ์ผ ์๋ฌ - ์ด๋ฐ ๋ฉ์๋ ์์ฒด๊ฐ ์์
// ๊ฐ์ ๋ฐ๊พธ๊ณ ์ถ์ผ๋ฉด ์ ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ผ ํจ
MemberResponse updated = new MemberResponse(1L, "๊น์ฒ ์");// Entity - ๊ฐ ๋ณ๊ฒฝ ๊ฐ๋ฅํด์ผ ํจ (๋น์ฆ๋์ค ๋ก์ง)
member.updateNickname("์๋๋ค์"); // โ
ํ์ํจ
// DTO - ๊ฐ ๋ณ๊ฒฝ ๋ถํ์ (๋ฐ์ดํฐ ์ด๋ฐ์ฉ)
// API ์์ฒญ ๋ฐ๊ณ โ ์๋ต ๋ณด๋ด๋ฉด ๋. ์ค๊ฐ์ ๊ฐ ๋ฐ๊ฟ ์ผ ์์// ํ๋๊ฐ 5๊ฐ ์ด์์ด๊ฑฐ๋ ์ ํ์ ํ๋๊ฐ ๋ง์ ๋
@Builder
public record MemberDetailResponse(
Long id,
String nickname,
String email,
String phone,
String profileImage,
LocalDateTime createdAt
) {
public static MemberDetailResponse from(Member member) {
return MemberDetailResponse.builder()
.id(member.getId())
.nickname(member.getNickname())
// ...
.build();
}
}
// ํ๋๊ฐ 3๊ฐ ์ดํ๋ฉด ๊ทธ๋ฅ ์์ฑ์ ์ฌ์ฉ
public record MemberSummary(Long id, String nickname) {
public static MemberSummary from(Member member) {
return new MemberSummary(member.getId(), member.getNickname());
}
}// Entity - ํ์ (JPA๊ฐ ๊ธฐ๋ณธ ์์ฑ์ ์๊ตฌ)
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member { }
// Service, Controller - @RequiredArgsConstructor
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository; // ์๋ ์ฃผ์
}
// Config ํด๋์ค
@Configuration
@RequiredArgsConstructor
public class SecurityConfig { }๊ฒฐ๋ก : Lombok์ ํ๋ก์ ํธ์์ ๊ณ์ ์ฌ์ฉํฉ๋๋ค. DTO์์๋ง record๋ก ๋์ฒดํ๋ ๊ฒ์ ๋๋ค.
๋ชจ๋ ๋๋ฉ์ธ์์ ๋์ผํ ์ด๋ ธํ ์ด์ ์์์ ํจํด์ ์ฌ์ฉํฉ๋๋ค.
1. ์คํ
๋ ์คํ์
(@Entity, @Service, @RestController ๋ฑ)
2. ์ค์ (@Table, @RequestMapping ๋ฑ)
3. Lombok (@Getter, @Builder, @RequiredArgsConstructor ๋ฑ)
@Entity
@Table(name = "member")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 20)
private String nickname;
@Column(nullable = false, unique = true)
private String email;
@Enumerated(EnumType.STRING) // STRING ํ์
@Column(nullable = false)
private MemberStatus status;
@ManyToOne(fetch = FetchType.LAZY) // LAZY ํ์
@JoinColumn(name = "store_id")
private Store store;
// @Builder๋ private ์์ฑ์์๋ง
@Builder
private Member(String nickname, String email) {
this.nickname = nickname;
this.email = email;
this.status = MemberStatus.ACTIVE;
}
// ๋น์ฆ๋์ค ๋ฉ์๋
public void updateNickname(String nickname) {
this.nickname = nickname;
}
}| ์ด๋ ธํ ์ด์ | ํ์ | ๋น๊ณ |
|---|---|---|
@Entity, @Table |
O | |
@Getter |
O | @Setter ๊ธ์ง |
@NoArgsConstructor(access = PROTECTED) |
O | |
@Builder |
O | private ์์ฑ์์๋ง |
@Enumerated(EnumType.STRING) |
O | ORDINAL ๊ธ์ง |
@ManyToOne(fetch = LAZY) |
O | EAGER ๊ธ์ง |
public class MemberRequest {
public record Create(
@NotBlank(message = "๋๋ค์์ ํ์์
๋๋ค.")
@Size(min = 2, max = 20, message = "๋๋ค์์ 2~20์ ์ฌ์ด์ฌ์ผ ํฉ๋๋ค.")
String nickname,
@NotBlank(message = "์ด๋ฉ์ผ์ ํ์์
๋๋ค.")
@Email(message = "์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ํ์์ด ์๋๋๋ค.")
String email
) {}
public record Update(
@Size(min = 2, max = 20)
String nickname
) {}
}public class MemberResponse {
@Builder
public record Detail(
Long id,
String nickname,
String email,
LocalDateTime createdAt
) {
public static Detail from(Member member) {
return Detail.builder()
.id(member.getId())
.nickname(member.getNickname())
.email(member.getEmail())
.createdAt(member.getCreatedAt())
.build();
}
}
// ํ๋ 3๊ฐ ์ดํ โ ์์ฑ์ ์ง์ ์ฌ์ฉ
public record Summary(
Long id,
String nickname
) {
public static Summary from(Member member) {
return new Summary(member.getId(), member.getNickname());
}
}
}| ๊ตฌ๋ถ | ํจํด | ๋น๊ณ |
|---|---|---|
| Request | record + Validation |
๋ถ๋ณ, Jackson ์๋ ์ง์ |
| Response | @Builder + record |
from() ํฉํ ๋ฆฌ ๋ฉ์๋ |
record ์ฅ์ : ๋ถ๋ณ์ฑ ๋ณด์ฅ,
equals/hashCode/toString์๋ ์์ฑ, ๋ฉํฐ์ค๋ ๋ ์์
ํ๋ก์ ํธ์์๋ ๋ณ๋์ Converter ํด๋์ค๋ฅผ ๋ง๋ค์ง ์๊ณ , DTO ๋ด๋ถ์ from() ์ ์ ํฉํ ๋ฆฌ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ Entity๋ฅผ DTO๋ก ๋ณํํฉ๋๋ค.
public class PaymentResponse {
@Builder
public record Detail(
Long id,
String paymentId,
String orderName,
Integer amount,
PaymentStatus status
// ... ๊ธฐํ ํ๋
) {
// โ
์ด๊ฒ์ด ์ปจ๋ฒํฐ ์ญํ ์ ํฉ๋๋ค
public static Detail from(Payment payment) {
return Detail.builder()
.id(payment.getId())
.paymentId(payment.getPaymentId())
.orderName(payment.getOrderName())
.amount(payment.getAmount())
.status(payment.getStatus())
.build();
}
}
public record Summary(
Long id,
String paymentId,
String orderName
) {
// ํ๋๊ฐ ์ ์ผ๋ฉด Builder ์์ด ์์ฑ์ ์ฌ์ฉ
public static Summary from(Payment payment) {
return new Summary(
payment.getId(),
payment.getPaymentId(),
payment.getOrderName()
);
}
}
}// Service์์ ์ฌ์ฉ
@Override
public PaymentResponse.Detail getPayment(Long memberId, String paymentId) {
Payment payment = paymentRepository.findByPaymentId(paymentId)
.orElseThrow(() -> new CustomException(ErrorCode.PAYMENT_NOT_FOUND));
// โ
๊ฐ๊ฒฐํ ๋ณํ
return PaymentResponse.Detail.from(payment);
}
// ๋ฆฌ์คํธ ๋ณํ
@Override
public List<PaymentResponse.Summary> getMyPayments(Long memberId) {
return paymentRepository.findByMemberIdOrderByCreatedAtDesc(memberId)
.stream()
.map(PaymentResponse.Summary::from) // โ
๋ฉ์๋ ๋ ํผ๋ฐ์ค ์ฌ์ฉ
.toList();
}| ์ฅ์ | ์ค๋ช |
|---|---|
| ์บก์ํ | ๋ณํ ๋ก์ง์ด DTO ๋ด๋ถ์ ์์ง๋จ |
| ํ์ ์์ ์ฑ | ์ปดํ์ผ ํ์์ ํ์ ์ฒดํฌ |
| ๊ฐ๊ฒฐ์ฑ | Service ์ฝ๋๊ฐ ๊น๋ํด์ง |
| ์ผ๊ด์ฑ | ํ๋ก์ ํธ ์ ์ฒด์์ ๋์ผํ ํจํด ์ฌ์ฉ |
| ํ ์คํธ ์ฉ์ด์ฑ | DTO ๋จ์๋ก ๋ณํ ๋ก์ง ํ ์คํธ ๊ฐ๋ฅ |
// โ ์ด๋ฐ ํด๋์ค๋ฅผ ๋ง๋ค์ง ์์ต๋๋ค
@Component
public class PaymentConverter {
public PaymentResponse.Detail toDetail(Payment payment) {
return PaymentResponse.Detail.builder()
.id(payment.getId())
// ...
.build();
}
}์ด์ :
- ๋ถํ์ํ ์ถ์ํ: DTO ๋ณํ์ ๋จ์ํ ๋งคํ ์์
- ํ์ผ ์ฆ๊ฐ: ๋๋ฉ์ธ๋น Converter ํด๋์ค ์ถ๊ฐ ์ ํ์ผ ์ 2๋ฐฐ ์ฆ๊ฐ
- ์์กด์ฑ ์ฃผ์ ๋ถํ์: ์ ์ ๋ฉ์๋๋ก ์ถฉ๋ถ
- ์์ง๋ ์ ํ: ๋ณํ ๋ก์ง์ด DTO์์ ๋ถ๋ฆฌ๋๋ฉด ๊ด๋ฆฌ ํฌ์ธํธ ์ฆ๊ฐ
ํ์ฌ 18๊ฐ ๋๋ฉ์ธ, 42๊ฐ์ from() ๋ฉ์๋๊ฐ ์ด ํจํด์ ๋ฐ๋ฅด๊ณ ์์ต๋๋ค:
- โ payment (Detail, Summary)
- โ community (PostResponse, CommentResponse)
- โ photo (PhotoResponse, RestorationResponse)
- โ store (PhotoLabResponse)
- โ inquiry (InquiryResponse)
- โ member (MemberResponse)
- โ auth (AuthResponse)
- โ reservation (ReservationResponse)
์ธ๋ถ ๋ฐ์ดํฐ๋ ์ถ๊ฐ ๋ก์ง์ด ํ์ํ ๊ฒฝ์ฐ, ํ๋ผ๋ฏธํฐ๋ฅผ ์ถ๊ฐํฉ๋๋ค:
public record PostDetailResDTO(
Long id,
String content,
boolean isLiked,
boolean isMine,
String profileImageUrl,
List<PostImageResDTO> images
) {
// โ
์ถ๊ฐ ์ ๋ณด๋ฅผ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ์
public static PostDetailResDTO from(
Post post,
boolean isLiked,
boolean isMine,
String profileImageUrl,
List<PostImageResDTO> images
) {
return new PostDetailResDTO(
post.getId(),
post.getContent(),
isLiked,
isMine,
profileImageUrl,
images
);
}
}Controller๋ Query/Command Service๋ฅผ ๋ชจ๋ ์ฃผ์ ๋ฐ์ ์ฌ์ฉํฉ๋๋ค.
@Tag(name = "Member", description = "ํ์ API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {
private final MemberQueryService memberQueryService;
private final MemberCommandService memberCommandService;
@Operation(summary = "ํ์ ์กฐํ")
@GetMapping("/{memberId}")
public ApiResponse<MemberResponse.Detail> getMember(
@PathVariable Long memberId
) {
return ApiResponse.success(
SuccessCode.MEMBER_FOUND,
memberQueryService.getMember(memberId)
);
}
@Operation(summary = "ํ์ ์์ฑ")
@PostMapping
public ApiResponse<Long> createMember(
@Valid @RequestBody MemberRequest.Create request
) {
return ApiResponse.success(
SuccessCode.MEMBER_CREATED,
memberCommandService.createMember(request)
);
}
@Operation(summary = "ํ์ ์ ๋ณด ์์ ")
@PatchMapping("/{memberId}")
public ApiResponse<Void> updateMember(
@PathVariable Long memberId,
@Valid @RequestBody MemberRequest.Update request
) {
memberCommandService.updateMember(memberId, request);
return ApiResponse.success(SuccessCode.MEMBER_UPDATED, null);
}
}| ์ด๋ ธํ ์ด์ | ์์ | ๋น๊ณ |
|---|---|---|
@Tag |
1 | Swagger ๋ฌธ์ํ |
@RestController |
2 | |
@RequiredArgsConstructor |
3 | |
@RequestMapping |
4 | |
@Operation |
๋ฉ์๋ 1 | summary ํ์ |
@GetMapping ๋ฑ |
๋ฉ์๋ 2 |
CQRS (Command Query Responsibility Segregation): ์กฐํ(Query)์ ๋ช ๋ น(Command)์ ๋ถ๋ฆฌํ์ฌ ์ฑ ์์ ๋ช ํํ ํฉ๋๋ค.
service/
โโโ command/
โ โโโ {Domain}CommandService.java (interface)
โ โโโ {Domain}CommandServiceImpl.java (๊ตฌํ์ฒด)
โโโ query/
โโโ {Domain}QueryService.java (interface)
โโโ {Domain}QueryServiceImpl.java (๊ตฌํ์ฒด)
์ฅ์ :
| ์ธก๋ฉด | ์ค๋ช |
|---|---|
| ํ ์คํธ ์ฉ์ด์ฑ | Interface๋ฅผ Mock์ผ๋ก ์ฃผ์ ํ์ฌ ๋จ์ ํ ์คํธ ๊ฒฉ๋ฆฌ |
| ํ์ฅ์ฑ | ์บ์ฑ, ๋น๋๊ธฐ ๋ฑ ๋ค๋ฅธ ๊ตฌํ์ฒด ์ถ๊ฐ ๊ฐ๋ฅ |
| ๋ช ํํ ๊ณ์ฝ | Interface๋ก ์๋น์ค ์ฑ ์ ๋ช ์ |
| ์ผ๊ด์ฑ | ํ๋ก์ ํธ ์ ์ฒด 35๊ฐ Service๊ฐ ๋์ผ ํจํด |
๋จ์ :
- ํ์ผ ์ ์ฆ๊ฐ (Service๋น 2๊ฐ ํ์ผ)
- ๊ตฌํ์ฒด๊ฐ 1๊ฐ๋ฟ์ด๋ฉด ๊ณผ๋ํ ์ถ์ํ๋ก ๋๊ปด์ง ์ ์์
๊ฒฐ๋ก :
- ํ๋ก์ ํธ ์ ์ฒด๊ฐ ์ด๋ฏธ ์ด ํจํด์ ๋ฐ๋ฅด๊ณ ์์
- Command/Query ๋ถ๋ฆฌ์ ์ ๋ง์
- ํ ์คํธ ์์ฑ ์ ์ค์ง์ ์ธ ์ด์ ์กด์ฌ
- ํ์ฌ ๊ตฌ์กฐ ์ ์ง ๊ถ์ฅ
// โ ์ด๋ ๊ฒ ๋ณ๊ฒฝํ์ง ์์ต๋๋ค
service/
โโโ command/
โ โโโ PaymentCommandService.java (interface ์์ด ๋จ์ผ ํด๋์ค)
โโโ query/
โโโ PaymentQueryService.java (interface ์์ด ๋จ์ผ ํด๋์ค)์ด์ :
- ์ผ๊ด์ฑ ์ฐ์ : 35๊ฐ Service๋ฅผ ๋ชจ๋ ๋ณ๊ฒฝํ๋ ๋น์ฉ์ด ํผ
- ํ ์คํธ ๋ณต์ก๋: Mockito๋ก ๊ตฌ์ฒด ํด๋์ค Mock ์์ฑ ํ์
- ํ์ฅ ๊ฐ๋ฅ์ฑ: ํฅํ ๋ค๋ฅธ ๊ตฌํ์ฒด ์ถ๊ฐ ์ ๋ฆฌํฉํ ๋ง ๋น์ฉ ๋ฐ์
์ฐธ๊ณ :
- ์๊ท๋ชจ ํ๋ก์ ํธ์์๋ ๋จ์ผ ํด๋์ค๊ฐ ๋ ์ค์ฉ์ ์ผ ์ ์์
- ํ์ง๋ง ํ์ฌ ํ๋ก์ ํธ ๊ท๋ชจ์ ํ ์ปจ๋ฒค์ ์ ๊ณ ๋ คํ์ฌ ํ์ฌ ๊ตฌ์กฐ ์ ์ง
// Interface
public interface MemberQueryService {
MemberResponse.Detail getMember(Long memberId);
List<MemberResponse.Summary> getMembers();
}
// Implementation
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MemberQueryServiceImpl implements MemberQueryService {
private final MemberRepository memberRepository;
private final MemberQueryRepository memberQueryRepository;
@Override
public MemberResponse.Detail getMember(Long memberId) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));
return MemberResponse.Detail.from(member);
}
@Override
public List<MemberResponse.Summary> getMembers() {
return memberQueryRepository.findAllSummary();
}
}// Interface
public interface MemberCommandService {
Long createMember(MemberRequest.Create request);
void updateMember(Long memberId, MemberRequest.Update request);
void deleteMember(Long memberId);
}
// Implementation
@Service
@RequiredArgsConstructor
@Transactional
public class MemberCommandServiceImpl implements MemberCommandService {
private final MemberRepository memberRepository;
@Override
public Long createMember(MemberRequest.Create request) {
if (memberRepository.existsByEmail(request.email())) {
throw new CustomException(ErrorCode.DUPLICATE_EMAIL);
}
Member member = Member.builder()
.nickname(request.nickname())
.email(request.email())
.build();
return memberRepository.save(member).getId();
}
@Override
public void updateMember(Long memberId, MemberRequest.Update request) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));
member.updateNickname(request.nickname());
}
@Override
public void deleteMember(Long memberId) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));
member.softDelete();
}
}| ๊ตฌ๋ถ | ํด๋์ค ๋ ๋ฒจ ํธ๋์ญ์ | ์ฉ๋ |
|---|---|---|
| QueryServiceImpl | @Transactional(readOnly = true) |
์กฐํ ์ ์ฉ (Read) |
| CommandServiceImpl | @Transactional |
์์ฑ/์์ /์ญ์ (CUD) |
| ์ด๋ ธํ ์ด์ | ์์น | ๋น๊ณ |
|---|---|---|
@Service |
๊ตฌํ์ฒด ํด๋์ค | ํ์ |
@RequiredArgsConstructor |
๊ตฌํ์ฒด ํด๋์ค | ํ์ |
@Transactional(readOnly = true) |
QueryServiceImpl ํด๋์ค | ์กฐํ ์ต์ ํ |
@Transactional |
CommandServiceImpl ํด๋์ค | CUD ์์ |
์ฐธ๊ณ : ๊ธฐ์กด ๋๋ฉ์ธ(member, auth, store ๋ฑ)์ ์ ์ง์ ์ผ๋ก CQRS ํจํด์ผ๋ก ๋ง์ด๊ทธ๋ ์ด์ ์์ ์ด๋ฉฐ, ์ ๊ท ๋๋ฉ์ธ์ ์ ํจํด์ ๋ฐ๋ฆ ๋๋ค.
JPA Repository๋
interface๋ก, QueryDSL Repository๋class๋ก ์์ฑํฉ๋๋ค.
| ๊ตฌ๋ถ | ํ์ | ๋ค์ด๋ฐ | ์ฉ๋ |
|---|---|---|---|
| JPA Repository | interface |
{Domain}Repository |
๊ธฐ๋ณธ CRUD, ๋จ์ ์ฟผ๋ฆฌ |
| QueryDSL Repository | class |
{Domain}QueryRepository |
๋ณต์กํ ๋์ ์ฟผ๋ฆฌ |
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String email);
boolean existsByEmail(String email);
@Query("SELECT m FROM Member m JOIN FETCH m.store WHERE m.id = :id")
Optional<Member> findByIdWithStore(@Param("id") Long id);
@EntityGraph(attributePaths = {"store"})
List<Member> findAllByStatus(MemberStatus status);
}@Repository
@RequiredArgsConstructor
public class MemberQueryRepository {
private final JPAQueryFactory queryFactory;
public List<Member> searchMembers(MemberSearchCondition condition) {
return queryFactory
.selectFrom(member)
.where(
nicknameContains(condition.nickname()),
statusEq(condition.status())
)
.orderBy(member.createdAt.desc())
.fetch();
}
private BooleanExpression nicknameContains(String nickname) {
return hasText(nickname) ? member.nickname.contains(nickname) : null;
}
private BooleanExpression statusEq(MemberStatus status) {
return status != null ? member.status.eq(status) : null;
}
}// โ ํด๋์ค ๋ ๋ฒจ @Builder
@Entity
@Builder
@AllArgsConstructor
public class Member { }
// โ @Setter, @Data
@Setter
@Data
public class Member { }
// โ EAGER (๊ธฐ๋ณธ๊ฐ)
@ManyToOne
private Store store;
// โ ORDINAL (๊ธฐ๋ณธ๊ฐ)
@Enumerated
private MemberStatus status;
// โ Controller์์ ์์ธ ์ฒ๋ฆฌ
@GetMapping("/{id}")
public ApiResponse<?> get(@PathVariable Long id) {
try { ... } catch (Exception e) { ... }
}| ๊ฒ์ฆ ์ ํ | ์์น | ์์ |
|---|---|---|
| ํ์ ๊ฒ์ฆ | DTO | @NotBlank, @Email, @Size |
| ๋น์ฆ๋์ค ๊ฒ์ฆ | Service | ์ค๋ณต ์ฒดํฌ, ๊ถํ ๊ฒ์ฌ |
// Service์์ ๋น์ฆ๋์ค ๊ฒ์ฆ
if (memberRepository.existsByEmail(request.email())) {
throw new CustomException(ErrorCode.DUPLICATE_EMAIL);
}// โ
๋จ๊ฑด: orElseThrow ์ฆ์ ํธ์ถ
Member member = memberRepository.findById(id)
.orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));
// โ
๋ชฉ๋ก: ๋น ๋ฆฌ์คํธ ๋ฐํ
public List<Member> getMembers() {
return memberRepository.findAll();
}
// โ Optional ํ๋ผ๋ฏธํฐ/ํ๋ ๊ธ์ง
void doSomething(Optional<String> name);
private Optional<String> nickname;class MemberServiceTest { }
@Test
void ํ์_์กฐํ_์ฑ๊ณต() { }
@Test
void ์กด์ฌํ์ง_์๋_ํ์_์กฐํ์_์์ธ๋ฐ์() { }@ExtendWith(MockitoExtension.class)
class MemberServiceTest {
private static final FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
.objectIntrospector(ConstructorPropertiesArbitraryIntrospector.INSTANCE)
.build();
@Test
void ํ์_์กฐํ_์ฑ๊ณต() {
// given
Member member = fixtureMonkey.giveMeOne(Member.class);
given(memberRepository.findById(anyLong()))
.willReturn(Optional.of(member));
// when
MemberResponse.Detail result = memberService.getMember(1L);
// then
assertThat(result.id()).isEqualTo(member.getId());
}
}FixtureMonkey ์ฅ์ : ๋๋ค ํ ์คํธ ๋ฐ์ดํฐ ์๋ ์์ฑ, ์ฃ์ง ์ผ์ด์ค ๋ฐ๊ฒฌ ์ฉ์ด
@Test
void ํ์_์์ฑ_์ฑ๊ณต() {
// given - ํ
์คํธ ๋ฐ์ดํฐ ์ค๋น
MemberRequest.Create request = new MemberRequest.Create("๋๋ค์", "test@test.com");
// when - ํ
์คํธ ๋์ ์คํ
MemberResponse.Detail result = memberService.createMember(request);
// then - ๊ฒฐ๊ณผ ๊ฒ์ฆ
assertThat(result.nickname()).isEqualTo("๋๋ค์");
}// โ
LocalDateTime ์ฌ์ฉ
private LocalDateTime createdAt;
// โ Date, Timestamp ๊ธ์ง
private Date createdAt;// ISO 8601 (JacksonConfig ์ ์ญ ์ค์ )
"createdAt": "2025-01-15T14:30:00"
// ํน์ ํฌ๋งท ํ์์
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;// โ ๋งค์ง ๋๋ฒ
if (retryCount > 3) { }
// โ
์์ ๋๋ Enum
private static final int MAX_RETRY_COUNT = 3;
if (retryCount > MAX_RETRY_COUNT) { }log.debug() // ๊ฐ๋ฐ ๋๋ฒ๊น
์ฉ
log.info() // ๋น์ฆ๋์ค ๋ก์ง ํ๋ฆ
log.warn() // ์์๋ ์์ธ
log.error() // ์์คํ
์๋ฌlog.info("[MemberService.getMember] memberId: {}", memberId);// โ ๊ธ์ง
log.info("ํ์: {}", member); // ์ ์ฒด ๊ฐ์ฒด
log.info("๋น๋ฐ๋ฒํธ: {}", password);
log.info("ํ ํฐ: {}", accessToken);// โ ํ๋ผ๋ฏธํฐ 3๊ฐ ์ด๊ณผ
void create(Long a, Long b, LocalDateTime c, String d, int e);
// โ
Command ๊ฐ์ฒด๋ก ๋ฌถ๊ธฐ
void create(ReservationCommand command);
public record ReservationCommand(
Long memberId,
Long storeId,
LocalDateTime date,
String memo
) {}