[๐Ÿ”ฅTroubleShooting - TicToc๐Ÿ”ฅ] Service ๊ณ„์ธต ์ด๊ฑฐ.. ๋„ˆ๋ฌด ๋ฌด๊ฑฐ์šด๋ฐ..โ“โ“ ๐Ÿ‹๐Ÿ‹

._mungยท2025๋…„ 3์›” 7์ผ
0

TicToc

๋ชฉ๋ก ๋ณด๊ธฐ
2/6

๐Ÿ“Œ ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ

ํŒ€์› : ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ
๊ธฐ๊ฐ„ : 2025.01 ~ ์ง„ํ–‰ ์ค‘
๋งํฌ : https://github.com/M-ung/TicToc_Server
์„œ๋น„์Šค ๋‚ด์šฉ : ๋‹น์‹ ์˜ ์‹œ๊ฐ„์— ๊ฐ€์น˜๋ฅผ ๋งค๊ธฐ๋‹ค, ์‹œ๊ฐ„ ๊ฑฐ๋ž˜ ๊ฒฝ๋งค ํ”Œ๋žซํผ


๐Ÿ”ฅTroubleShooting๐Ÿ”ฅ

Problems

๊ฒฝ๋งค ๊ด€๋ จ ๋น„์ง€๋‹ˆ์Šค ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ๊ณ ๋ฏผ์ด ์ƒ๊ฒผ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด "๊ฒฝ๋งค ์ˆ˜์ •" ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด ๋ณด์ž. ๊ทธ๋Ÿผ "๊ฒฝ๋งค ์ˆ˜์ •" ์ „์— ์‚ฌ์šฉ์ž์˜ userId์™€ ๊ฒฝ๋งค์˜ userId๊ฐ€ ๋™์ผํ•œ ์ง€ ํ™•์ธํ•ด์•ผ ํ•˜๋Š” ๋กœ์ง์ด ํ•„์š”ํ–ˆ๋‹ค. ์ด ์™ธ์—๋„ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ, ๊ฐ์ฒด ์ˆ˜์ •๊ณผ ๊ฐ™์€ ์—ฌ๋Ÿฌ ๊ธฐ๋Šฅ๋“ค์ด ํ•„์š”ํ•  ๋•Œ Service ๊ณ„์ธต์— ๋ฉ”์„œ๋“œ๋ฅผ ๊ณ„์† ๋งŒ๋“ค์–ด์™”๋‹ค.
๊ทธ๋ ‡๊ฒŒ ๋˜๋ฉด Service ์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค. (์•„๋ž˜ ์ฝ”๋“œ๋Š” ๋ฉ€ํ‹ฐ๋ชจ๋“ˆ + ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ ์ ์šฉ ์ „ ์ฝ”๋“œ์ด๋‹ค.)

@Service
@Transactional
@RequiredArgsConstructor
public class AuctionCommandServiceImpl implements AuctionCommandService {
    private final AuctionRepository auctionRepository;
    private final AuctionHistoryRepository auctionHistoryRepository;

    @Override
    public void register(final Long userId, AuctionRequestDTO.Register requestDTO) {
        checkAuctionTimeRange(userId, requestDTO.sellStartTime(), requestDTO.sellEndTime());
        auctionRepository.save(Auction.of(userId, requestDTO));
    }

    @Override
    public void update(final Long userId, final Long auctionId, AuctionRequestDTO.Update requestDTO) {
        validateAuctionAccess(userId, auctionId);
        checkAuctionTimeRange(userId, requestDTO.sellStartTime(), requestDTO.sellEndTime());
        try {
            findAuctionById(auctionId).update(requestDTO);
        } catch (OptimisticLockingFailureException e) {
            throw new ConflictAuctionUpdateException(CONFLICT_AUCTION_UPDATE);
        }
    }

    @Override
    public void delete(final Long userId, final Long auctionId) {
        validateAuctionAccess(userId, auctionId);
        try {
            findAuctionById(auctionId).deactivate();
        } catch (OptimisticLockingFailureException e) {
            throw new ConflictAuctionDeleteException(CONFLICT_AUCTION_DELETE);
        }
    }

    private Auction findAuctionById(final Long auctionId) {
        return auctionRepository.findById(auctionId)
                .orElseThrow(() -> new AuctionNotFoundException(AUCTION_NOT_FOUND));
    }

    private void validateAuctionAccess(final Long userId, final Long auctioneerId) {
        if(!userId.equals(auctioneerId)) {
            throw new AuctionNoAccessException(AUCTION_NO_ACCESS);
        }
    }

    private void checkAuctionTimeRange(Long userId, LocalDateTime sellStartTime, LocalDateTime sellEndTime) {
        if(auctionRepository.existsAuctionInTimeRange(userId, sellStartTime, sellEndTime)) {
            throw new DuplicateAuctionDateException(DUPLICATE_AUCTION_DATE);
        }
    }
}

์œ„ ์ฝ”๋“œ๋Š” ๊ฐœ๋ฐœ์ด ๋๋‚œ ์ƒํƒœ๊ฐ€ ์•„๋‹Œ, ๊ฐœ๋ฐœ ์ค‘์ธ ์ƒํƒœ์ด๋‹ค. ๊ทธ๋Ÿผ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  validateAuctionAccess ์™€ ๊ฐ™์€ ๋ฉ”์„œ๋“œ๊ฐ€ ๋ณต์žก์„ฑ์„ ์ฆ๊ฐ€์‹œํ‚ค๊ณ  ์žˆ์œผ๋ฉฐ ํ•ด๋‹น ๋ฉ”์„œ๋“œ๋Š” ๋‹ค๋ฅธ Service ๊ณ„์ธต์—์„œ๋„ ํ•„์š”ํ•˜๊ธฐ์— ์ฝ”๋“œ ์ค‘๋ณต์„ฑ ๋˜ํ•œ ์ผ์œผ์ผฐ๋‹ค.

๋ฌธ์ œ๋ฅผ ์ •๋ฆฌํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

1. Service ๊ณ„์ธต์˜ ์ฑ…์ž„ ์ฆ๊ฐ€
2. ์ฝ”๋“œ ๋ณต์žก์„ฑ ์ฆ๊ฐ€
3. ์ฝ”๋“œ ์ค‘๋ณต์„ฑ ์ฆ๊ฐ€


How

์œ„ ๋ฌธ์ œ๋“ค์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ findAuctionById ์™€ ๊ฐ™์ด DB๋ฅผ ์ ‘๊ทผํ•ด์•ผ ํ•˜๋Š” ๋ฉ”์„œ๋“œ๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด ๋„๋ฉ”์ธ์— ๋ฉ”์„œ๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” "์บก์Šํ™”" ๋ฐฉ์‹์„ ํƒํ–ˆ๋‹ค.

1. ๋„๋ฉ”์ธ ์บก์Šํ™” ๋ฉ”์„œ๋“œ ์ ์šฉ
2. Service ๊ณ„์ธต์„ ๋‹จ์ˆœํ™”


Process

1. ๋„๋ฉ”์ธ ์บก์Šํ™” ๋ฉ”์„œ๋“œ ์ ์šฉ
DB ์ ‘๊ทผ์ด ํ•„์š”ํ•˜์ง€ ์•Š์€ ๋„๋ฉ”์ธ์˜ ๋น„์ง€๋‹ˆ์Šค ๋กœ์ง์€ ๋„๋ฉ”์ธ ๊ณ„์ธต์— ์บก์Šํ™”ํ•˜์—ฌ ๋ฉ”์„œ๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ๋‹ค.
์ด์ „์—๋Š” Service ๊ณ„์ธต์—์„œ ์ง์ ‘ ๊ฒ€์ฆ ๋กœ์ง๊ณผ ์ƒํƒœ ๋ณ€๊ฒฝ์„ ์ˆ˜ํ–‰ํ–ˆ์ง€๋งŒ, ๊ฐ์ฒด ์ง€ํ–ฅ์ ์ธ ์„ค๊ณ„ํ•˜์—ฌ ๋ณต์žก์„ฑ๊ณผ ์ค‘๋ณต์„ฑ์„ ์ค„์ด๊ธฐ ์œ„ํ•ด ๋„๋ฉ”์ธ ๊ฐ์ฒด๊ฐ€ ์Šค์Šค๋กœ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ๋‹ค.

@Getter
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "auction")
public class Auction extends BaseTimeEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long auctioneerId;
    private String title;
    @Column(nullable = false, columnDefinition = "TEXT")
    private String content;
    private Integer startPrice;
    private Integer currentPrice;
    private Integer finalPrice;
    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
    private LocalDateTime sellStartTime; // ํŒ๋งคํ•˜๊ณ  ์‹ถ์€ ์‹œ๊ฐ„(์‹œ์ž‘)
    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
    private LocalDateTime sellEndTime; // ํŒ๋งคํ•˜๊ณ  ์‹ถ์€ ์‹œ๊ฐ„(์ข…๋ฃŒ)
    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
    private LocalDateTime auctionOpenTime; // ๊ฒฝ๋งค ์‹œ์ž‘ ์‹œ๊ฐ„
    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
    private LocalDateTime auctionCloseTime; // ๊ฒฝ๋งค ์ข…๋ฃŒ ์‹œ๊ฐ„
    @Enumerated(EnumType.STRING)
    private AuctionProgress progress;
    @Enumerated(EnumType.STRING)
    private AuctionType type;
    @Enumerated(EnumType.STRING)
    private TicTocStatus status;

    public static Auction of(final Long userId, AuctionUseCaseReqDTO.Register requestDTO) {
        return Auction.builder()
                .auctioneerId(userId)
                .title(requestDTO.title())
                .content(requestDTO.content())
                .startPrice(requestDTO.startPrice())
                .currentPrice(requestDTO.startPrice())
                .finalPrice(requestDTO.startPrice())
                .sellStartTime(requestDTO.sellStartTime())
                .sellEndTime(requestDTO.sellEndTime())
                .auctionOpenTime(LocalDateTime.now())
                .auctionCloseTime(requestDTO.auctionCloseTime())
                .progress(NOT_STARTED)
                .type(requestDTO.type())
                .status(TicTocStatus.ACTIVE)
                .build();
    }

    public void update(AuctionUseCaseReqDTO.Update requestDTO) {
        validateAuctionAlreadyStarted();
        this.title = requestDTO.title();
        this.content = requestDTO.content();
        this.startPrice = requestDTO.startPrice();
        this.currentPrice = requestDTO.startPrice();
        this.finalPrice = requestDTO.startPrice();
        this.sellStartTime = requestDTO.sellStartTime();
        this.sellEndTime = requestDTO.sellEndTime();
        this.auctionCloseTime = requestDTO.auctionCloseTime();
        this.type = requestDTO.type();
    }

    public void deactivate(final Long userId) {
        validateAuctionAccess(userId);
        validateAuctionAlreadyStarted();
        this.status = TicTocStatus.DISACTIVE;
    }

    public void validateAuctionAccess(final Long userId) {
        if(!userId.equals(this.auctioneerId)) {
            throw new BidNoAccessException(BID_NO_ACCESS);
        }
    }

    private void validateAuctionAlreadyStarted() {
        if(!this.getProgress().equals(NOT_STARTED)) {
            throw new AuctionAlreadyStartedException(AUCTION_ALREADY_STARTED);
        }
    }
}

2. Service ๊ณ„์ธต์„ ๋‹จ์ˆœํ™”
Service ๊ณ„์ธต์—์„œ๋Š” DB๋ฅผ ์ ‘๊ทผํ•˜๋Š” ๋กœ์ง๋งŒ ์ฒ˜๋ฆฌํ•˜๊ณ , ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ ๋„๋ฉ”์ธ ๊ฐ์ฒด์—์„œ ์ˆ˜ํ–‰ํ•˜๋„๋ก ๋ณ€๊ฒฝํ•˜์—ฌ ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์™€ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ์—ญํ• ๋งŒ ๋‹ด๋‹นํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ๋‹ค.

@Service
@Transactional
@RequiredArgsConstructor
public class AuctionCommandService implements AuctionCommandUseCase {
    private final LocationCommandUseCase locationCommandUseCase;
    private final AuctionRepositoryPort auctionRepositoryPort;
    private final CloseAuctionUseCase closeAuctionUseCase;

    @Override
    public void register(final Long userId, AuctionUseCaseReqDTO.Register requestDTO) {
        auctionRepositoryPort.validateAuctionTimeRangeForSave(userId, requestDTO.sellStartTime(), requestDTO.sellEndTime());
        var auction = auctionRepositoryPort.saveAuction(Auction.of(userId, requestDTO));
        var auctionId = auction.getId();
        if (!requestDTO.type().equals(AuctionType.ONLINE)) {
            locationCommandUseCase.saveAuctionLocations(auctionId, requestDTO.locations());
        }
        closeAuctionUseCase.save(auctionId, auction.getAuctionCloseTime());
    }

    @Override
    public void update(final Long userId, final Long auctionId, AuctionUseCaseReqDTO.Update requestDTO) {
        var findAuction = auctionRepositoryPort.findAuctionByIdForUpdate(auctionId);
        findAuction.validateAuctionAccess(userId);
        auctionRepositoryPort.validateAuctionTimeRangeForUpdate(userId, auctionId, findAuction.getSellStartTime(), findAuction.getSellEndTime());
        try {
            findAuction.update(requestDTO);
            if(!findAuction.getType().equals(AuctionType.ONLINE)) {
                locationCommandUseCase.deleteAuctionLocations(auctionId);
            }
            if (!requestDTO.type().equals(AuctionType.ONLINE)) {
                locationCommandUseCase.saveAuctionLocations(auctionId, requestDTO.locations());
            }
            closeAuctionUseCase.delete(auctionId);
            closeAuctionUseCase.save(auctionId, findAuction.getAuctionCloseTime());
        } catch (OptimisticLockingFailureException e) {
            throw new ConflictAuctionUpdateException(CONFLICT_AUCTION_UPDATE);
        }
    }

    @Override
    public void delete(final Long userId, final Long auctionId) {
        var findAuction = auctionRepositoryPort.findAuctionByIdForUpdate(auctionId);
        try {
            findAuction.deactivate(userId);
            closeAuctionUseCase.delete(auctionId);
        } catch (OptimisticLockingFailureException e) {
            throw new ConflictAuctionDeleteException(CONFLICT_AUCTION_DELETE);
        }
    }
}

Result

์ด ๊ฒฐ๊ณผ Auction์˜ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ AuctionCommandService ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ UserCommandService, BidCommandService ์—์„œ ํ•„์š”ํ•  ๋•Œ validateAuctionAccess ๋ฉ”์„œ๋“œ๋ฅผ ์ค‘๋ณต์ ์œผ๋กœ ๊ตฌํ˜„ํ•  ํ•„์š” ์—†์ด ๋„๋ฉ”์ธ์—์„œ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•ด์กŒ๋‹ค.


Thoughts

ํ•ญ์ƒ ํ”„๋กœ์ ํŠธ๋ฅผ ๊ฒฝํ—˜ํ•  ๋•Œ๋งˆ๋‹ค ๊ฐ–๊ฒŒ ๋˜๋Š” ํฐ ๊ณ ๋ฏผ์ด ์ฝ”๋“œ์˜ ์ค‘๋ณต์„ฑ์„ ์–ด๋–ป๊ฒŒ ํ•˜๋ฉด ์ค„์ผ ์ˆ˜ ์žˆ์„๊นŒ์˜€๋‹ค. ํ•˜์ง€๋งŒ ์ด๋ฒˆ ๊ธฐํšŒ์— ๋„๋ฉ”์ธ์— ์บก์Šํ™”ํ•ด์„œ ๋ฉ”์„œ๋“œ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์„ ์ ์šฉํ•˜์—ฌ ์ฝ”๋“œ์˜ ์ค‘๋ณต์„ฑ๊ณผ ๋ณต์žก์„ฑ์„ ์ค„์ผ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์œ„ ๋ฐฉ๋ฒ•์œผ๋กœ Service ๊ณ„์ธต์˜ ๋ถ€๋‹ด์€ ์ค„์—ˆ์ง€๋งŒ, ๋„๋ฉ”์ธ์˜ ๋ถ€๋‹ด์ด ๋ฐ˜๋Œ€๋กœ ์ปค์ง€๊ฒŒ ๋˜์—ˆ๋‹ค. ์ด ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ๋Š” ๊พธ์ค€ํžˆ ํ”„๋กœ์ ํŠธ ํ•˜๋ฉด์„œ ๊ณ ๋ฏผํ•˜๋ฉฐ ๊ฐœ์„ ํ•ด ๋‚˜๊ฐ€์•ผ ํ•  ๋ถ€๋ถ„์ด๋‹ค.


profile
๐Ÿ’ป ๐Ÿ’ป ๐Ÿ’ป

0๊ฐœ์˜ ๋Œ“๊ธ€

๊ด€๋ จ ์ฑ„์šฉ ์ •๋ณด