2024. 5. 11. 15:06ㆍProject/데브툰
데브툰은 팀 프로젝트로, Git을 사용하면서 협업하고 있습니다. 기능 구현 시 PR을 올리는 것을 기본으로 하고 지속적인 리팩토링은 이슈를 발생시켜 처리하고 있습니다. 설계/로직 개선이나 성능 최적화와 같은 큰 주제는 별도의 포스팅으로 다룰 예정입니다. 이외 리팩토링 작업은 여러 단위를 하나로 묶어서 발행하려고 합니다.(해당 포스팅) 앞으로 글의 가독성을 위해 말을 편하게 할 예정입니다. 양해 부탁드립니다. 🍎🍎🍎
목차
fix: 프로모션 조회 에러 수정
refactor: 비즈니스 로직의 return값을 도메인으로 변경
refactor: 문자열 하드코딩 부분을 상수로 분리
refactor: 계산 로직 Calculator 클래스로 분리
회고
만족한 점
아쉬운 점
다음에는 이렇게
이슈 및 PR은 Git Flow 전략을 따릅니다. 기본적으로 이슈 확인 → 목표 설정 → 문제 상황 및 원인 분석 → 리팩토링 → (선택) 추가적인 리팩토링, 코드리뷰, 코드리뷰에 대한 리팩토링...ㅎㅎ 이러한 순서로 진행됩니다.
fix: 프로모션 조회 에러 수정
이슈 확인
목표
유효한 프로모션이 없더라도 에러반환이 아닌 빈 리스트를 반환하도록 수정
문제 상황 및 원인
조회하는 메서드인데 요청 바디를 파라미터로 넘겨서 500 에러가 발생했다.
반환된 프로모션을 리스트로 반환하는 로직은 컨트롤러가 아닌 서비스 레이어에서 작성해야 한다. 컨트롤러에서는 HTTP 요청을 받고, 요청을 처리할 서비스 메서드를 호출하며, HTTP 응답을 반환하는 역할만 수행한다.
/**
* 현재 적용 가능한 프로모션 전체 조회
*/
@GetMapping("/now")
public ResponseEntity<ApiReponse<Page<RetrieveActivePromotionsResponse>>> retrieveActivePromotions(
@RequestBody final RetrieveActivePromotionsRequest request,
Pageable pageable
) {
Page<RetrieveActivePromotionsResponse> activePromotions = promotionService.retrieveActivePromotions(request, pageable);
if (activePromotions.hasContent()) {
activePromotions.getContent().forEach(promotion -> log.info("프로모션 상세 조회: {}", promotion));
} else {
log.info("조회된 프로모션 없음.");
}
return ResponseEntity.ok(ApiReponse.success(activePromotions));
}
리팩토링
컨트롤러 코드
/**
* 현재 적용 가능한 프로모션 전체 조회
* : 조회 할 프로모션이 없을 경우 빈 리스트 반환
*/
@GetMapping("/now")
public ResponseEntity<ApiReponse<Page<RetrieveActivePromotionsResponse>>> retrieveActivePromotions(
Pageable pageable
) {
Page<RetrieveActivePromotionsResponse> activePromotions = promotionService.retrieveActivePromotions(pageable);
return ResponseEntity.ok(ApiReponse.success(activePromotions));
}
서비스 코드
/**
* 현재 활성화된 프로모션 전체 조회
* : 현재 활성화된 프로모션이 없는 경우 빈 페이지를 반환합니다.
*/
@Transactional(readOnly = true)
public Page<RetrieveActivePromotionsResponse> retrieveActivePromotions(
Pageable pageable
) {
// 활성화된 프로모션 목록 조회
Page<PromotionEntity> activePromotions = validateActivePromotionExists(pageable);
// 각 프로모션 엔티티를 프로모션 속성과 함께 응답 객체로 변환
return activePromotions.map(promotionEntity -> {
PromotionAttributeEntity promotionAttribute =
validatePromotionAttributeExists(promotionEntity);
return RetrieveActivePromotionsResponse.from(promotionEntity, promotionAttribute);
});
}
private Page<PromotionEntity> validateActivePromotionExists(Pageable pageable) {
LocalDateTime currentTime = LocalDateTime.now();
Page<PromotionEntity> promotions = promotionRepository.findActivePromotions(
currentTime, pageable
);
if (promotions.isEmpty()) {
return new PageImpl<>(Collections.emptyList(), pageable, 0);
}
return promotions;
}
private PromotionAttributeEntity validatePromotionAttributeExists(PromotionEntity promotionEntity) {
PromotionAttributeEntity promotionAttribute =
promotionAttributeRepository.findByPromotionEntityId(promotionEntity.getId())
.orElseThrow(() -> new DevtoonException(ErrorCode.NOT_FOUND,
ErrorMessage.getResourceNotFound(
"PromotionEntity",
promotionEntity.getId()
))
);
return promotionAttribute;
}
기존 테스트 보완
· 기존 조회 테스트를 조금 더 가독성 좋게 수정
· 현재 시간 기준 유효한 프로모션 조회, 등록된 프로모션이 현재 시간에는 유효하지 않을 경우 빈 페이지 반환 테스트 추가
추가 작업 - 이슈 확인
refactor: 부정형 조건문 제거
리팩토링
현재 시간 기준 활성화된 프로모션이 없을 경우 예외 처리가 아닌 빈 페이지 반환
private Page<PromotionEntity> validateActivePromotionExists(Pageable pageable) {
LocalDateTime currentTime = LocalDateTime.now();
Page<PromotionEntity> promotions = promotionRepository.findActivePromotions(
currentTime, pageable
);
if (promotions.isEmpty()) {
return new PageImpl<>(Collections.emptyList(), pageable, 0);
}
return promotions;
}
refactor: 비즈니스 로직의 return값을 도메인으로 변경
이슈 확인
목표
비즈니스 계층에서 도메인 객체를 반환하도록 설정함으로써 코드의 재사용성을 향상할 것
왜?
다른 서비스에서 특정 서비스의 메서드를 호출할 때, 도메인 객체를 반환받으면 후속 작업이 편리하다. 현재처럼 응답 객체를 반환받을 경우, 서비스 단에서 이를 활용하는 데에 제약이 있음.
그럼 어떻게 수정?
서비스단에서 도메인 객체를 반환하고 → 컨트롤러단에서 응답 객체 내 from() 메서드를 통해 도메인을 응답 객체로 변환
리팩토링 전/후 비교
1️⃣ CookiePaymentService
전
/**
* 특정 회원 쿠키 결제 내역 조회
*/
public CookiePaymentDetailDto retrieve(final Long webtoonViewerId) {
CookiePaymentEntity cookiePayment = cookiePaymentRepository.findByWebtoonViewerId(webtoonViewerId)
.orElseThrow(() -> new DevtoonException(ErrorCode.NOT_FOUND, ErrorMessage.getResourceNotFound("특정 회원 CookiePayment 내역", webtoonViewerId)));
Price totalPrice = cookiePayment.calculateTotalPrice();
Price paymentPrice = cookiePayment.calculatePaymentPrice();
return new CookiePaymentDetailDto(
cookiePayment,
totalPrice,
paymentPrice
);
}
후
@Slf4j
@RequiredArgsConstructor
@Service
public class CookiePaymentService {
private final CookiePaymentRepository cookiePaymentRepository;
private final WebtoonViewerRepository webtoonViewerRepository;
private final CookiePolicyRepository cookiePolicyRepository;
private final PromotionRepository promotionRepository;
private final CookieWalletService cookieWalletService;
private final CookieWalletRepository cookieWalletRepository;
/**
* 특정 회원 쿠키 결제 내역 조회
*/
public CookiePaymentEntity retrieve(final Long webtoonViewerId) {
return cookiePaymentRepository.findByWebtoonViewerId(webtoonViewerId)
.orElseThrow(() -> new DevtoonException(ErrorCode.NOT_FOUND,
ErrorMessage.getResourceNotFound(
ResourceType.COOKIE_PAYMENT,
webtoonViewerId
)));
}
}
😀 초기화 방법 변경: 수동 초기화 → 생성자 초기화
수동 초기화
객체를 생성한 후 각 필드를 개별적으로 설정(set)하는 방식이라는 점에서 일종의 수동 초기화 방법
문제점:
· 객체가 생성된 후 필드가 설정되므로, 객체가 완전한 상태로 존재하지 않을 가능성 존재
· 가독성 및 유지보수가 어려움
🔽 코드
@Getter
public class CookiePaymentRetrieveResponse {
private Long webtoonViewerNo;
private Integer quantity;
private BigDecimal totalPrice; // 총 금액
private BigDecimal totalDiscountRate; // 총 할인율
private BigDecimal paymentPrice; // 결제 금액 = totalPrice * (1-totalDiscountRate)
private LocalDateTime createdAt;
public static CookiePaymentRetrieveResponse from(
final CookiePaymentDetailDto cookiePaymentDetailDto
) {
CookiePaymentRetrieveResponse response = new CookiePaymentRetrieveResponse();
response.webtoonViewerNo = cookiePaymentDetailDto.getCookiePayment().getWebtoonViewerId();
response.quantity = cookiePaymentDetailDto.getCookiePayment().getQuantity();
response.totalPrice = cookiePaymentDetailDto.getTotalPrice().getAmount();
response.totalDiscountRate = cookiePaymentDetailDto.getCookiePayment().getTotalDiscountRate();
response.paymentPrice = cookiePaymentDetailDto.getPaymentPrice().getAmount();
response.createdAt = cookiePaymentDetailDto.getCookiePayment().getCreatedAt();
return response;
}
}
생성자를 통한 초기화
장점:
· 객체의 완전성 보장
생성자를 통해 모든 필드를 초기화하면, 객체가 생성되는 시점에 모든 필드가 초기화되므로 객체가 불완전한 상태로 존재할 가능성이 없다. 이는 객체의 상태가 항상 완전하게 초기화된 상태로 존재하도록 보장
· 생성자를 통해 필드를 초기화하면 필수 필드들이 누락되지 않도록 보장 가능
· 코드 가독성 및 유지보수성 향상
🔽 코드
@Getter
public class CookiePaymentRetrieveResponse {
private Long webtoonViewerNo;
private Integer quantity;
private BigDecimal totalPrice;
private BigDecimal totalDiscountRate;
private BigDecimal paymentPrice;
// 모든 필드를 초기화하는 생성자
public CookiePaymentRetrieveResponse(
Long webtoonViewerNo,
Integer quantity,
BigDecimal totalPrice,
BigDecimal totalDiscountRate,
BigDecimal paymentPrice,
LocalDateTime createdAt
) {
this.webtoonViewerNo = webtoonViewerNo;
this.quantity = quantity;
this.totalPrice = totalPrice;
this.totalDiscountRate = totalDiscountRate;
this.paymentPrice = paymentPrice;
this.createdAt = createdAt;
}
// CookiePaymentDetailDto로부터 객체를 생성하는 정적 팩토리 메서드
public static CookiePaymentRetrieveResponse from(
final CookiePaymentDetailDto cookiePaymentDetailDto
) {
return new CookiePaymentRetrieveResponse(
cookiePaymentDetailDto.getCookiePayment().getWebtoonViewerId(),
cookiePaymentDetailDto.getCookiePayment().getQuantity(),
cookiePaymentDetailDto.getTotalPrice().getAmount(),
cookiePaymentDetailDto.getCookiePayment().getTotalDiscountRate(),
cookiePaymentDetailDto.getPaymentPrice().getAmount(),
cookiePaymentDetailDto.getCookiePayment().getCreatedAt()
);
}
}
2️⃣ PromotionService
프로모션 삭제 메서드
전
/**
* 프로모션 소프트 삭제
*/
@Transactional
public PromotionSoftDeleteResponse softDelete(final Long id) {
PromotionEntity promotion = promotionRepository.findById(id)
.orElseThrow(() -> new DevtoonException(ErrorCode.NOT_FOUND, ErrorMessage.getResourceNotFound("Promotion", id)));
promotion.recordDeletion(LocalDateTime.now());
PromotionEntity softDeletedPromotion = promotionRepository.save(promotion);
return PromotionSoftDeleteResponse.from(softDeletedPromotion);
}
후
@Slf4j
@RequiredArgsConstructor
@Service
public class PromotionService {
private final PromotionRepository promotionRepository;
private final PromotionAttributeRepository promotionAttributeRepository;
/**
* 프로모션 삭제
* : 삭제 시간을 통해 로직상에서 삭제 처리를 구분합니다.
*/
@Transactional
public PromotionEntity delete(final Long id) {
PromotionEntity promotion = promotionRepository.findById(id)
.orElseThrow(() -> new DevtoonException(
ErrorCode.NOT_FOUND,
ErrorMessage.getResourceNotFound(ResourceType.PROMOTION, id)
));
promotion.recordDeletion(LocalDateTime.now());
return promotionRepository.save(promotion);
}
}
현재 활성화된 프로모션 전체 조회 메서드
전
/**
* 현재 활성화된 프로모션 전체 조회
* : 현재 활성화된 프로모션이 없는 경우 빈 페이지를 반환합니다.
*/
@Transactional(readOnly = true)
public Page<RetrieveActivePromotionsResponse> retrieveActivePromotions(
Pageable pageable
) {
// 활성화된 프로모션 목록 조회
Page<PromotionEntity> activePromotions = validateActivePromotionExists(pageable);
// 각 프로모션 엔티티를 프로모션 속성과 함께 응답 객체로 변환
return activePromotions.map(promotionEntity -> {
PromotionAttributeEntity promotionAttribute =
validatePromotionAttributeExists(promotionEntity);
return RetrieveActivePromotionsResponse.from(promotionEntity, promotionAttribute);
});
}
후
· 접근제어자를 private에서 public으로 수정하여 서비스 메서드 사용 가능하도록 변경
@Slf4j
@RequiredArgsConstructor
@Service
public class PromotionService {
private final PromotionRepository promotionRepository;
private final PromotionAttributeRepository promotionAttributeRepository;
/**
* 현재 활성화된 프로모션 전체 조회
* : 현재 활성화된 프로모션이 없는 경우 빈 페이지를 반환합니다.
*/
@Transactional(readOnly = true)
public Page<PromotionEntity> retrieveActivePromotions(final Pageable pageable) {
Page<PromotionEntity> activePromotions = validateActivePromotionExists(pageable);
return activePromotions;
}
private Page<PromotionEntity> validateActivePromotionExists(final Pageable pageable) {
LocalDateTime currentTime = LocalDateTime.now();
Page<PromotionEntity> promotions = promotionRepository.findActivePromotions(
currentTime, pageable
);
if (promotions.isEmpty()) {
return new PageImpl<>(Collections.emptyList(), pageable, 0);
}
return promotions;
}
// 접근 제어자 수정: private -> public
public PromotionAttributeEntity validatePromotionAttributeExists(
final PromotionEntity promotionEntity
) {
PromotionAttributeEntity promotionAttribute =
promotionAttributeRepository.findByPromotionEntityId(promotionEntity.getId())
.orElseThrow(() -> new DevtoonException(
ErrorCode.NOT_FOUND,
ErrorMessage.getResourceNotFound(
ResourceType.PROMOTION,
promotionEntity.getId()
))
);
return promotionAttribute;
}
}
refactor: 문자열 하드코딩 부분을 상수로 분리
이슈 확인
하드코딩 지양 → 상수 처리로 인한 이점
- 오타로 인한 에러 방지
- 유지보수성 향상
- 코드 의미 파악 쉬워짐
에러 메시지에서 문자열 하드 코딩한 부분을 상수로 처리하기 전, 고려한 부분
ErrorMessage : 에러 메시지를 관리하기 위한 상수 클래스
· 에러 메시지 문자열은 private static final 필드로 관리
· 정적 메서드, 호출 시점(에러 상황 발생)에 필요한 정보를 인자로 받아 메시지를 동적으로 포맷팅 하여 반환
프로젝트에서 두 가지 버전의 ErrorMessage 상수 클래스 가 있다.
ver1. 특정 에러 메시지 제공
특정 콘텍스트에 맞는 에러 메시지를 제공한다.
- 장점:
- 에러 메시지가 명확해 이해하기 쉽다.
- 개발자가 미리 에러 케이스를 정의하여 런터임 오류 가능성을 줄일 수 있다.
- 단점:
- 각각의 에러 유형에 대해 메서드를 정의해야해서 코드 중복 가능성이 있다.
- 새로운 유형의 에러 메시지가 필요할 때 마다 해당 클래스르 수정해야해서 유연성이 떨어진다.
// comment
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class ErrorMessage {
private static final String ID_NOT_FOUND = "id : '%d' 를 찾을 수 없습니다.";
public static String getCommentNotFound(final Long id) {
return String.format(ID_NOT_FOUND, id);
}
}
// webtoon
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class ErrorMessage {
private static final String ID_NOT_FOUND = "id : '%d' 를 찾을 수 없습니다.";
private static final String TITLE_CONFLICT = "title : '%s' 가 존재합니다.";
public static String getWebtoonNotFound(final Long id) {
return String.format(ID_NOT_FOUND, id);
}
public static String getTitleConflict(final String title) {
return String.format(TITLE_CONFLICT, title);
}
}
// webtoon_viewer
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class ErrorMessage {
private static final String ID_NOT_FOUND = "id : '%d' 를 찾을 수 없습니다.";
private static final String MEMBERSHIP_STATUS_NOT_FOUND = "membership status : '%s' 를 찾을 수 없습니다.";
private static final String EMAIL_CONFLICT = "email : '%s' 가 존재합니다.";
public static String getWebtoonViewerNotFound(final Long id) {
return String.format(ID_NOT_FOUND, id);
}
public static String getMembershipStatusNotFound(final String status) {
return String.format(MEMBERSHIP_STATUS_NOT_FOUND, status);
}
public static String getEmailConflict(final String email) {
return String.format(EMAIL_CONFLICT, email);
}
}
ver2. 유연한 메세지 포맷 제공
- 정적 메서드에, 제네릭 <T>를 사용
- 해당 메서드가 어떠한 타입의 식별자도 받을 수 있어 다양한 상황에서 재사용 할 수 있음.
- 타입 안정성: 호출 시점에 타입 체크가 가능해 런타임에 발생할 수 있는 ClassCastException을 방지할 수 있음.
- 장점:
- 예외 메시지 포맷을 일관되게 유지하여 확장성을 가질 수 있고 코드 중복을 줄일 수 있다.
- 다양한 엔티티, 식별자에 대해 유연하게 메세지를 처리할 수 있다.
- 단점:
- 서비스 레이어에서 매번 해당 entity부분을 상단에 상수로 정의해야 하는 번거로움 존재.
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ErrorMessage {
private static final String RESOURCE_NOT_FOUND = "%s : '%s' 을/를 찾을 수 없습니다.";
public static <T> String getResourceNotFound(String entity, T identifier) {
return String.format(RESOURCE_NOT_FOUND, entity, identifier);
}
}
⇒ 결론: ver2. 유연한 메세지 포맷 제공 선택
만약 프로젝트의 규모가 크지 않고 예상되는 예외 상황이 많지 않다고 판단이 된다면, 매번 예외 메시지를 작성해 관리할 수 있다. 이 방식은 메서드 이름을 통해 발생한 예외 상황을 쉽게 파악할 수 있다는 장점이 존재한다.
그러나 이번 프로젝트는 유연하고 확장 가능한 설계를 목표로 하고 있다. 물론 각 서비스에서 상수를 정의해야 하는 번거로움이 있지만, 예외 메시지 포맷을 일관되게 유지하는 방법이 가진 장점이 더 크기에 선택했다.
그리고 기존에 사용된 'entity'라는 용어는 주로 데이터베이스 엔티티를 지칭하는 데 한정될 수 있다. 프로젝트가 다양한 형태의 리소스(예: 이미지 등)를 포괄할 수 있도록 'resourceName'으로 변경했다.
약간 수정해서 가져가기로 결정
• final키워드 추가
• 불변성 보장
• 멀티 스레드 환경에서 추가적인 동기화 없이 안정하게 공유할 수 있음
• 제네릭 → Object사용
• 타입과 무관하게 메서드 적용 가능, 코드 간결
• 제네릭을 사용할 때 발생가능한 타입 관련 복잡한 문제를 피할 수 있음
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ErrorMessage {
private static final String RESOURCE_NOT_FOUND = "%s : '%s' 을/를 찾을 수 없습니다.";
public static String getResourceNotFound(final String resourceName, final Object identifier) {
return String.format(RESOURCE_NOT_FOUND, resourceName, identifier);
}
}
적용 코드
@Slf4j
@RequiredArgsConstructor
@Service
public class WebtoonPaymentService {
private static final String WEBTOON_VIEWER = "WebtoonViewer";
private static final String WEBTOON = "Webtoon";
private static final String WEBTOON_PAYMENT = "WebtoonPayment";
...
private Long getWebtoonViewerIdOrThrow(final Long webtoonViewerId) {
return webtoonViewerRepository.findById(webtoonViewerId)
.orElseThrow(() -> new DevtoonException(
ErrorCode.NOT_FOUND, ErrorMessage.getResourceNotFound(WEBTOON_VIEWER,
webtoonViewerId))
).getId();
}
private Long getWebtoonIdOrThrow(final Long webtoonId) {
return webtoonRepository.findById(webtoonId)
.orElseThrow(() -> new DevtoonException(
ErrorCode.NOT_FOUND, ErrorMessage.getResourceNotFound(WEBTOON, webtoonId))
).getId();
}
/**
* 특정 회원 웹툰 결제 내역 단건 조회
*/
public WebtoonPaymentEntity retrieve(final Long webtoonViewerId) {
return webtoonPaymentRepository.findByWebtoonViewerId(webtoonViewerId)
.orElseThrow(() -> new DevtoonException(ErrorCode.NOT_FOUND,
ErrorMessage.getResourceNotFound(WEBTOON_PAYMENT, webtoonViewerId)));
}
}
적용 부분에 대한 코드리뷰
에러 메시지에서 사용하는 자원 이름을 Enum 클래스로 관리하는 것으로 수정
package yjh.devtoon.common.utils;
import lombok.Getter;
@Getter
public enum ResourceType {
BAD_WORDS_WARNING_COUNT("BadWordsWarningCount"),
COMMENT("Comment"),
COOKIE_WALLET("CookieWallet"),
COOKIE_PAYMENT("CookiePayment"),
WEBTOON_PAYMENT("WebtoonPayment"),
POLICY("Policy"),
PROMOTION("Promotion"),
WEBTOON("Webtoon"),
WEBTOON_VIEWER("WebtoonViewer");
private final String resourceName;
ResourceType(String resourceName) {
this.resourceName = resourceName;
}
public String getResourceName() {
return resourceName;
}
}
에러 메세지
package yjh.devtoon.payment.constant;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import yjh.devtoon.common.utils.ResourceType;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ErrorMessage {
private static final String RESOURCE_NOT_FOUND = "%s : '%s' 을/를 찾을 수 없습니다.";
public static String getResourceNotFound(
final ResourceType resourceType,
final Object identifier
) {
return String.format(RESOURCE_NOT_FOUND, resourceType, identifier);
}
}
적용
/**
* 특정 회원 쿠키 결제 내역 조회
*/
public CookiePaymentDetailDto retrieve(final Long webtoonViewerId) {
CookiePaymentEntity cookiePayment =
cookiePaymentRepository.findByWebtoonViewerId(webtoonViewerId)
.orElseThrow(() -> new DevtoonException(ErrorCode.NOT_FOUND,
// ========================================================
ErrorMessage.getResourceNotFound(ResourceType.COOKIE_PAYMENT,
webtoonViewerId)));
Price totalPrice = cookiePayment.calculateTotalPrice();
Price paymentPrice = cookiePayment.calculatePaymentPrice();
return new CookiePaymentDetailDto(
cookiePayment,
totalPrice,
paymentPrice
);
}
refactor: 계산 로직 Calculator 클래스로 분리
이슈 확인
문제 확인
Price도메인 현재 상황 및 관련 코드리뷰
@Embeddable
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Price {
@Column(name = "cookie_price", precision = 8, scale = 2)
private BigDecimal amount;
public static Price of(final BigDecimal amount) {
return new Price(amount);
}
public static Price of(final int amount) {
return new Price(BigDecimal.valueOf(amount));
}
private Price(final BigDecimal amount) {
Objects.requireNonNull(amount);
this.amount = toBigDecimal(amount);
}
private BigDecimal toBigDecimal(BigDecimal amount) {
Objects.requireNonNull(amount);
return new BigDecimal(amount.toString());
}
public Price add(final Price price) {
return new Price(add(price.amount));
}
private BigDecimal add(final BigDecimal amount) {
return this.amount.add(amount);
}
public Price subtract(final Price price) {
return new Price(subtract(price.amount));
}
private BigDecimal subtract(final BigDecimal amount) {
return this.amount.subtract(amount);
}
public Price multiply(final Price price) {
return new Price(multiply(price.amount));
}
private BigDecimal multiply(final BigDecimal amount) {
return this.amount.multiply(amount);
}
public Price divide(final Price price) {
return new Price(divide(price.amount));
}
private BigDecimal divide(final BigDecimal amount) {
return this.amount.divide(amount, 3, RoundingMode.HALF_UP);
}
public Price calculateTotalPrice(Integer quantity) {
BigDecimal quantityAsBigDecimal = BigDecimal.valueOf(quantity);
BigDecimal result = quantityAsBigDecimal.multiply(this.amount);
return new Price(result);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Price price = (Price) o;
return Objects.equals(amount, price.amount);
}
@Override
public int hashCode() {
return Objects.hashCode(amount);
}
@Override
public String toString() {
return "Price{" +
"amount=" + amount +
'}';
}
}
해결
계산 로직 Calculator enum 클래스로 분리
Price 도메인
package yjh.devtoon.payment.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.Objects;
@Embeddable
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Price {
@Column(name = "cookie_price", precision = 8, scale = 2)
private BigDecimal amount;
...
public Price plus(final Price price) {
BigDecimal result = Calculator.PLUS.calculate(this.amount, price.getAmount());
return new Price(result);
}
public Price minus(final Price price) {
BigDecimal result = Calculator.MINUS.calculate(this.amount, price.getAmount());
return new Price(result);
}
public Price multiply(final Price price) {
BigDecimal result = Calculator.MULTIPLY.calculate(this.amount, price.getAmount());
return new Price(result);
}
public Price divide(final Price price) {
BigDecimal result = Calculator.DIVIDE.calculate(this.amount, price.getAmount());
return new Price(result);
}
...
}
Calculator enum 클래스
import java.util.function.BiFunction;
public enum Calculator {
PLUS("더하기", BigDecimal::add),
MINUS("빼기", BigDecimal::subtract),
MULTIPLY("곱하기", BigDecimal::multiply),
DIVIDE("나누기", (a, b) -> a.divide(b, 3, RoundingMode.HALF_UP));
private final String name;
private final BiFunction<BigDecimal, BigDecimal, BigDecimal> biFunction;
Calculator(String name, BiFunction<BigDecimal, BigDecimal, BigDecimal> biFunction) {
this.name = name;
this.biFunction = biFunction;
}
public BigDecimal calculate(BigDecimal a, BigDecimal b) {
return this.biFunction.apply(a, b);
}
}
관련해서는 다음에 발행할 리팩토링: 쿠키 결제 로직 4단계로 개선하기 (feat. 원시값 포장) 포스팅에서 더 자세히 다룰 예정입니다.
회고
만족한 점
프로젝트 시작부터 리팩토링의 중요성을 인식하고, 적극적으로 리팩토링을 진행한 점을 꼽을 수 있다.
기획 단계에서 정한 API를 우선적으로 모두 개발한 후, 다시 확인하며 리팩터링 할 부분을 점검했다. 각 리팩토링 부분은 하나씩 이슈로 등록하여 관리했으며, 이를 통해 프로젝트 히스토리도 체계적으로 관리할 수 있었다. 리팩토링은 작은 오타 수정, 코드 컨벤션 통일부터 성능 최적화 등 다양한 부분에 적절히 배분한 점도 만족스러웠다.
아쉬운 점
기능 개발 및 리팩토링에 집중하느라 코드 리뷰에 충분한 시간을 할애하지 못한 점이 아쉬웠다.
다음에는 이렇게
다음에는 코드 리뷰 시간을 충분히 확보하고, 코드 리뷰를 단순한 과제가 아닌 학습의 기회로 활용해야겠다. 타인의 코드를 보는 것은 자신의 코드를 돌아보는 기회라는 것을 인지하고 있기에, 다음에는 코드 리뷰를 보다 철저하게 진행할 계획이다. 또한, 리팩토링을 시작하기 전에 팀원들과 회의를 통해 프로젝트를 어느 정도까지 발전시킬지 논의하고, 방향성을 정한 후 진행하는 것 또한 좋을 것 같다.
'Project > 데브툰' 카테고리의 다른 글
[데브툰] 리팩토링: 프로모션 조회 설계 및 성능 개선 도전하기 - 성능 편 (0) | 2024.05.20 |
---|---|
[데브툰] 리팩토링: 프로모션 조회 설계 및 성능 개선 도전하기 - 설계 편 (0) | 2024.05.14 |
[데브툰] 이제 너만 믿는다, 테스트 코드 작성하기 (0) | 2024.05.08 |
[데브툰] 다양한 정책을 쉽게 등록하고 삭제하기 (0) | 2024.05.07 |
[데브툰] Git 활용하여 자신있게 프로젝트 협업하기 (0) | 2024.05.01 |