끝을 보는 용기

Day 118 - 게임 아이템 거래소 프로젝트 25%, 코드 한 줄 쓸 때마다 '지금이 최선일까?' 고민하다

writingforever162 2025. 2. 1. 23:40

1. 프로젝트 진행 상황 및 계획

🥇 경매장에 아이템을 등록하는 C 구현하기 (완료) 
🥈 경매장에서 아이템을 입찰하는 U (Patch) 구현하기 (완료)

🥉 경매장에 등록한 아이템을 취소하는 D 구현하기 (완료)
4️⃣ 동시성 제어 관련 공부하기 (진행 전, 25.02.02 완료 목표)

5️⃣ 경매장 CUD에 각각 예외 처리하기 (완료)

 

2. 코드 한 줄 쓸 때마다 '지금이 최선일까?' 고민했다.

더보기
package no.gunbang.market.domain.auction.service;

import lombok.RequiredArgsConstructor;
import no.gunbang.market.common.Item;
import no.gunbang.market.common.ItemRepository;
import no.gunbang.market.common.exception.CustomException;
import no.gunbang.market.common.exception.ErrorCode;
import no.gunbang.market.domain.auction.dto.request.CreateAuctionRequestDto;
import no.gunbang.market.domain.auction.dto.response.CreateAuctionResponseDto;
import no.gunbang.market.domain.auction.entity.Auction;
import no.gunbang.market.domain.auction.repository.AuctionRepository;
import no.gunbang.market.domain.user.entity.User;
import no.gunbang.market.domain.user.repository.UserRepository;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class AuctionService {

    private final AuctionRepository auctionRepository;
    private final ItemRepository itemRepository;
    private final UserRepository userRepository;

    public CreateAuctionResponseDto saveAuction(
        CreateAuctionRequestDto requestDto,
        Long userId
    ) {
        User foundUser = findUserById(userId);

        Long itemId = requestDto.getItemId();

        Item foundItem = findItemByItem(itemId);

        // 입력된 경매 희망 기간을 검증하는 구간 시작
        boolean isAuctionDaysOutOfRange = requestDto.getAuctionDays() < 3
            || 7 < requestDto.getAuctionDays();

        if (isAuctionDaysOutOfRange) {
            throw new CustomException(ErrorCode.AUCTION_DAYS_OUT_OF_RANGE);
        }
        // 입력된 경매 희망 기간을 검증하는 구간 종료

        Auction auctionToSave = Auction.of(
            foundUser,
            foundItem,
            requestDto.getStartingPrice(),
            requestDto.getAuctionDays()
        );

        Auction savedAuction = auctionRepository.save(auctionToSave);

        return CreateAuctionResponseDto.toDto(savedAuction);
    }

    private User findUserById(Long userId) {
        return userRepository.findById(userId)
            .orElseThrow(
                () -> new CustomException(ErrorCode.USER_NOT_FOUND)
            );
    }

    private Item findItemByItem(Long itemId) {
        return itemRepository.findById(itemId)
            .orElseThrow(
                () -> new CustomException(ErrorCode.ITEM_NOT_FOUND)
            );
    }
}

▲ 처음에는 사용자가 며칠 동안 경매를 진행할지 자유롭게 정할 수 있도록 하되, 3일 미만이거나 7일 넘게 입력하면 예외가 발생하도록 코드를 작성했다. 이때 문득, 경매 희망 기간뿐만 아니라 '입찰 가격이 경매 시작 가격보다 낮은지', 또는 '보유한 금액보다 더 높게 입찰하려는지' 등등 검증하는 구간이 늘어날 때마다 서비스 레이어(Service Layer)에 추가하면 비즈니스 로직(business logic)이 한눈에 보이지 않을 듯했다. 이 문제는 단순히 검증 로직을 프라이빗 메서드(private method)로 추출해서는 제대로 해결되지 않을 듯싶었다.

더보기
package no.gunbang.market.domain.auction.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.NoArgsConstructor;
import no.gunbang.market.common.BaseEntity;
import no.gunbang.market.common.Item;
import no.gunbang.market.common.Status;
import no.gunbang.market.common.exception.CustomException;
import no.gunbang.market.common.exception.ErrorCode;
import no.gunbang.market.domain.user.entity.User;
import org.hibernate.annotations.Comment;

@Entity
@Table(name = "auction")
@Getter
@NoArgsConstructor
public class Auction extends BaseEntity {

    @Comment("경매장 식별자")
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(columnDefinition = "BIGINT")
    private Long id;

    @Comment("경매 시작가")
    private long startingPrice;

    @Comment("경매 마감 기한")
    private LocalDateTime dueDate;

    @Enumerated(EnumType.STRING)
    @Comment("경매 진행 상태")
    private Status status;

    @Comment("사용자 외래키")
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    @Comment("아이템 외래키")
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "item_id")
    private Item item;

    public static Auction of(
        User user,
        Item item,
        long startingPrice,
        int auctionDays
    ) {
        // private method 호출 
        validateAuctionDays(auctionDays);

        Auction auction = new Auction();
        auction.user = user;
        auction.item = item;
        auction.startingPrice = startingPrice;
        auction.status = Status.ON_SALE;
        auction.dueDate = auction.toDueDate(auctionDays);
        return auction;
    }

    private LocalDateTime toDueDate(int auctionDays) {
        return LocalDateTime.now().plusDays(auctionDays);
    }

    // 경매 희망 기간이 3일 미만이거나 7일 초과인지 검증하는 메서드
    private static void validateAuctionDays(int auctionDays) {
        if (auctionDays < 3 || auctionDays > 7) {
            throw new CustomException(ErrorCode.AUCTION_DAYS_OUT_OF_RANGE);
        }
    }
}

▲ 이런 이유로 검증 로직을 엔티티(Entity) 안에 넣어서 검증 외에 입찰이나 경매 취소 같은 비즈니스 로직을 간결하게 유지하려고 했다. 사용자가 경매 희망 기간을 3일보다 짧거나 7일보다 길게 입력했는지 검증하는 로직은 경매(Auction) 엔티티 클래스(class) 내부에서만 쓸 예정이었으므로 private로 설정했다.

package no.gunbang.market.domain.auction.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.NoArgsConstructor;
import no.gunbang.market.common.BaseEntity;
import no.gunbang.market.common.Item;
import no.gunbang.market.common.Status;
import no.gunbang.market.common.exception.CustomException;
import no.gunbang.market.common.exception.ErrorCode;
import no.gunbang.market.domain.user.entity.User;
import org.hibernate.annotations.Comment;

@Entity
@Table(name = "auction")
@Getter
@NoArgsConstructor
public class Auction extends BaseEntity {

    @Comment("경매장 식별자")
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(columnDefinition = "BIGINT")
    private Long id;

    @Comment("경매 시작가")
    private long startingPrice;

    @Comment("경매 마감 기한")
    private LocalDateTime dueDate;

    @Enumerated(EnumType.STRING)
    @Comment("경매 진행 상태")
    private Status status;

    @Comment("사용자 외래키")
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    @Comment("아이템 외래키")
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "item_id")
    private Item item;

    // 최소 경매 희망 기간을 정적 변수로 선언 및 초기화
    private static final int minAuctionDays = 3;
    
    // 최대 경매 희망 기간을 정적 변수로 선언 및 초기화
    private static final int maxAuctionDays = 7;

    public static Auction of(
        User user,
        Item item,
        long startingPrice,
        int auctionDays
    ) {
        // private method 호출 
        validateAuctionDays(auctionDays);

        Auction auction = new Auction();
        auction.user = user;
        auction.item = item;
        auction.startingPrice = startingPrice;
        auction.status = Status.ON_SALE;
        auction.dueDate = auction.toDueDate(auctionDays);
        return auction;
    }

    private LocalDateTime toDueDate(int auctionDays) {
        return LocalDateTime.now().plusDays(auctionDays);
    }

    // 경매 희망 기간이 3일 미만이거나 7일 초과힌지 검증하는 메서드
    private static void validateAuctionDays(int auctionDays) {
        if (auctionDays < minAuctionDays || auctionDays > maxAuctionDays) {
            throw new CustomException(ErrorCode.AUCTION_DAYS_OUT_OF_RANGE);
        }
    }
}

▲ 그러다가 3과 7이 정확히 무엇을 의미하는지 정확하게 보여줘야, 나중에 팀에서 코드를 리팩토링할 때 헷갈리지 않을 듯해서 3은 '최소 경매 희망 기간'을 의미하는 'minAuctionDays'로, 7은 '최대 경매 희망 기간'을 뜻하는 'maxAuctionDays' 변수로 선언 및 초기화해서 사용했다. 

package no.gunbang.market.domain.auction.service;

import lombok.RequiredArgsConstructor;
import no.gunbang.market.common.Item;
import no.gunbang.market.common.ItemRepository;
import no.gunbang.market.common.exception.CustomException;
import no.gunbang.market.common.exception.ErrorCode;
import no.gunbang.market.domain.auction.dto.request.CreateAuctionRequestDto;
import no.gunbang.market.domain.auction.dto.response.CreateAuctionResponseDto;
import no.gunbang.market.domain.auction.entity.Auction;
import no.gunbang.market.domain.auction.repository.AuctionRepository;
import no.gunbang.market.domain.user.entity.User;
import no.gunbang.market.domain.user.repository.UserRepository;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class AuctionService {

    private final AuctionRepository auctionRepository;
    private final ItemRepository itemRepository;
    private final UserRepository userRepository;

    public CreateAuctionResponseDto saveAuction(
        CreateAuctionRequestDto requestDto,
        Long userId
    ) {
        User foundUser = findUserById(userId);

        Long itemId = requestDto.getItemId();

        Item foundItem = findItemByItem(itemId);

        Auction auctionToSave = Auction.of(
            foundUser,
            foundItem,
            requestDto.getStartingPrice(),
            requestDto.getAuctionDays()
        );

        Auction savedAuction = auctionRepository.save(auctionToSave);

        return CreateAuctionResponseDto.toDto(savedAuction);
    }
    
    private User findUserById(Long userId) {
        return userRepository.findById(userId)
            .orElseThrow(
                () -> new CustomException(ErrorCode.USER_NOT_FOUND)
            );
    }

    private Item findItemByItem(Long itemId) {
        return itemRepository.findById(itemId)
            .orElseThrow(
                () -> new CustomException(ErrorCode.ITEM_NOT_FOUND)
            );
    }
}

▲ 고민한 부분을 반영한 결과, 경매를 등록하는 비즈니스 로직이 한눈에 들어왔다. 

 

팀 프로젝트를 진행할 때면 '팀에 민폐가 되지는 않을까?'라든지 '한 사람 몫을 제대로 할 수 있을까?'와 같은 걱정이 늘 따라다니지만, 그래도 팀 프로젝트를 할 때마다 내가 우물 안 개구리가 되지 않도록 도와주는 팀원들이 있어서 기대감이 들곤 한다. 3과 7이란 숫자를 바로 입력하는 대신, 정적 변수를 써서 다른 사람이 코드를 읽었을 때도 바로 이해할 수 있도록 하면 좋겠다는 피드백을 팀원들이 해주었으니까. 특히 이번 팀 프로젝트를 하는 동안에는 이런 피드백을 더 많이 얻을 수 있도록 부지런히 공부하고 노력해야겠다.