코드 분석 url
https://github.com/woowacourse-teams/2023-3-ddang
GitHub - woowacourse-teams/2023-3-ddang: 중고 경매 거래 플랫폼
중고 경매 거래 플랫폼. Contribute to woowacourse-teams/2023-3-ddang development by creating an account on GitHub.
github.com
https://github.com/woowacourse-teams/2023-3-ddang/pull/37
경매 엔티티 및 레포지토리(등록) 추가 by JJ503 · Pull Request #37 · woowacourse-teams/2023-3-ddang
📄 작업 내용 요약 경매 엔티티 및 레포지토리(등록) 추가 🙋🏻 리뷰 시 주의 깊게 확인해야 하는 코드 📎 Issue 번호 closed #26
github.com
package com.ddang.ddang.actuion.infrastructure.persistence;
import com.ddang.ddang.actuion.domain.Auction;
import com.ddang.ddang.actuion.domain.Price;
import com.ddang.ddang.configuration.JpaConfiguration;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;
import java.time.LocalDateTime;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@SuppressWarnings("NonAsciiCharacters")
@Import(JpaConfiguration.class)
class JpaAuctionRepositoryTest {
@PersistenceContext
EntityManager em;
@Autowired
JpaAuctionRepository auctionRepository;
@Test
void 경매를_저장한다() {
// given
final Auction auction = Auction.builder()
.title("경매 상품 1")
.description("이것은 경매 상품 1 입니다.")
.startBidPrice(new Price(1000))
.closingTime(LocalDateTime.now())
.build();
// when
auctionRepository.save(auction);
// then
em.flush();
em.clear();
assertThat(auction.getId()).isPositive();
}
}
@Import(JpaConfiguration.class)
왜 JapConfiguration을 import 해올까?
JpaConfiguration
package com.ddang.ddang.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@Configuration
@EnableJpaAuditing
public class JpaConfiguration {
}
JpaConfiguration에는 데이터를 생성, 수정시 자동으로 감시 기능을 해주기 위한 @EnableJpaAuditing 애노테이션이 존재한다.
즉, 테스트 환경에서도 Audting(감시)기능을 적용하기 위해서이다!!
https://github.com/woowacourse-teams/2023-3-ddang/pull/37
경매 엔티티 및 레포지토리(등록) 추가 by JJ503 · Pull Request #37 · woowacourse-teams/2023-3-ddang
📄 작업 내용 요약 경매 엔티티 및 레포지토리(등록) 추가 🙋🏻 리뷰 시 주의 깊게 확인해야 하는 코드 📎 Issue 번호 closed #26
github.com
package com.ddang.ddang.auction.application.dto;
import com.ddang.ddang.auction.domain.BidUnit;
import com.ddang.ddang.auction.domain.Auction;
import com.ddang.ddang.auction.domain.Price;
import com.ddang.ddang.auction.presentation.dto.CreateAuctionRequest;
import java.time.LocalDateTime;
public record CreateAuctionDto(
String title,
String description,
int bidUnit,
int startBidPrice,
LocalDateTime closingTime
) {
public static CreateAuctionDto from(final CreateAuctionRequest request) {
return new CreateAuctionDto(
request.title(),
request.description(),
request.bidUnit(),
request.startBidPrice(),
request.closingTime()
);
}
public Auction toEntity() {
return Auction.builder()
.title(title)
.description(description)
.bidUnit(new BidUnit(bidUnit))
.startBidPrice(new Price(startBidPrice))
.closingTime(closingTime)
.build();
}
}
Dto를 왜 Class를 사용하지 않고 Record를 사용할까?
- DTO는 계층간에 데이터를 전달하는 용도로 많이 쓰이며, 단순히 전달해줄 데이터값만 포함하고 있어야 한다. DTO에는 특정한 로직이 포함된 메소드를 갖지 않는 것이 좋으며, 데이터가 도중에 변하지 않도록 설정해줄 필요가 있다.
- record는 일반 클래스처럼 사용할 수 있지만, 필드에 final을 자동으로 추가해주는 특징을 가지고 있으며 getter와 equals/hashCode, toString 메소드를 기본적으로 지원해주므로 개발자에게 편리한 개발 환경을 제공해준다.
- DTO를 record로 구현한다면, DTO가 가져야 할 특징을 쉽게 구현할 수 있다.
그러므로, 온전히 데이터 불변성을 지키는 DTO 클래스를 생성할 필요가 있다면, record를 고려해봐도 좋을 것 같다.
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@@ -46,4 +50,22 @@ public ResponseEntity<ExceptionResponse> handleRegionNotFoundException(final Reg
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ExceptionResponse(ex.getMessage()));
}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
final MethodArgumentNotValidException ex,
final HttpHeaders headers,
final HttpStatusCode status,
final WebRequest request
) {
logger.info(String.format(EXCEPTION_FORMAT, MethodArgumentNotValidException.class), ex);
final String message = ex.getFieldErrors()
.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(System.lineSeparator()));
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ExceptionResponse(message));
}
}
왜 ResponseEntityExceptionHandler를 상속받을까?
스프링 프레임워크에서 제공하는 기본적인 예외 처리 기능을 확장하고 커스터마이징하기 위해서이다.
Spring은 예외를 미리 처리해 둔 ResponseEntityExceptionHandler를 추상 클래스로 제공하고 있다. ResponseEntityExceptionHandler는 스프링 예외에 대한 ExceptionHandler가 모두 구현되어 있으므로 ControllerAdvice 클래스가 이를 상속받는다.
import com.ddang.ddang.region.infrastructure.persistence.JpaRegionRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class RegionService {
왜 @Transactional(readOnly = true)로 할까?
readOnly = true를 설정하게 되면 스프링 프레임워크는 JPA의 세션 플러시 모드를 MANUAL로 설정하게 된다.
MANUAL이란? 트랜잭션 내에서 사용자가 수동으로 flush를 호출하지 않으면 flush가 자동으로 수행되지 않는 모드
트랜잭션 내에서 강제로 flush()를 호출하지 않는 한, 수정 내역에 대해 DB에 적용되지 않는다.
트랜잭션 Commit시 영속성 컨텍스트가 자동으로 flush되지 않으므로 조회용으로 가져온 Entity의 예상치 못한 수정을 방지할 수 있다.
JPA는 해당 트랜잭션 내에서 조회하는 Entity는 조회용임을 인식하고 변경 감지를 위한 Snapshot을 따로 보관하지 않으므로 메모리가 절약되는 성능상 이점 역시 존재한다.
class AuctionServiceTest {
@Autowired
AuctionService auctionService;
@Test
void 경매를_등록한다() {
// given
final CreateAuctionDto createAuctionDto = new CreateAuctionDto(
"경매 상품 1",
"이것은 경매 상품 1 입니다.",
1_000,
1_000,
LocalDateTime.now()
);
// when
final Long actual = auctionService.create(createAuctionDto);
// then
assertThat(actual).isPositive();
}
}
isPositive() 메서드
주어진 값이 양수인지 확인한다.
양수가 아닌 경우 테스트는 실패하고, 적절한 오류 메시지가 출력된다.
@Test
void 가능한_범위_내의_가격을_받는_경우_정상적으로_생성된다() {
// when & then
assertThatCode(() -> new BidUnit(1_000)).doesNotThrowAnyException();
}
assertThatCode()
코드 블록이 예외를 던지지 않는지 검증하는 데 사용됩니다.
@Test
void 경매를_등록한다() throws Exception {
// given
final CreateAuctionRequest request = new CreateAuctionRequest(
"경매 상품 1",
"이것은 경매 상품 1 입니다.",
1_000,
1_000,
LocalDateTime.now()
.plusDays(3L)
);
given(auctionService.create(any(CreateAuctionDto.class))).willReturn(1L);
// when & then
mockMvc.perform(post("/auctions").contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
)
.andExpectAll(
status().isCreated(),
header().string(HttpHeaders.LOCATION, is("/auctions/1")),
jsonPath("$.id", is(1L), Long.class)
);
}
}
상태 코드 검증
status().isCreated() : 응답 상태 코드가 201 Created인지 확인
헤더 검증
header().string(HttpHeaders.LOCATION, is("/auction/1") : Location 필드가 /auction/1로 설정되었는지 확인
package com.ddang.ddang.auction.presentation.dto;
import com.ddang.ddang.auction.application.dto.ReadAuctionDto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.time.LocalDateTime;
@JsonInclude(JsonInclude.Include.NON_NULL)
public record AuctionResponse(
Long id,
String images,
String title,
String category,
String description,
int startBidPrice,
Integer lastBidPrice,
String status,
int bidUnit,
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul")
LocalDateTime registerTime,
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul")
LocalDateTime closingTime,
String directRegions,
int auctioneerCount,
boolean deleted
) {
public static AuctionResponse from(final ReadAuctionDto dto) {
return new AuctionResponse(
dto.id(),
"https://img.danawa.com/prod_img/500000/139/918/img/19918139_1.jpg?_v=20230605093237",
dto.title(),
"전자기기",
dto.description(),
dto.startBidPrice(),
dto.lastBidPrice(),
"unbidden",
dto.bidUnit(),
dto.registerTime(),
dto.closingTime(),
"서울특별시 송파구 가락1동",
0,
dto.deleted()
);
}
}
@JsonInclude(JsonInclude.Include.NON_NULL) : JSON 직렬화 시 null 값을 포함하지 않도록 설정
@JsonFormat: 필드의 JSON 포맷을 지정한다. 여기서는 registerTime과 closingTime 필드를 특정 형식과 시간대("Asia/Seoul")로 포맷합니다.
@Test
void 지정한_아이디에_해당하는_경매를_조회한다() {
// given
final CreateAuctionDto createAuctionDto = new CreateAuctionDto(
"경매 상품 1",
"이것은 경매 상품 1 입니다.",
1_000,
1_000,
LocalDateTime.now()
);
final Long saveAuctionId = auctionService.create(createAuctionDto);
// when
final ReadAuctionDto actual = auctionService.readByAuctionId(saveAuctionId);
// then
SoftAssertions.assertSoftly(softAssertions -> {
softAssertions.assertThat(actual.id()).isPositive();
softAssertions.assertThat(actual.title()).isEqualTo(createAuctionDto.title());
softAssertions.assertThat(actual.description()).isEqualTo(createAuctionDto.description());
softAssertions.assertThat(actual.bidUnit()).isEqualTo(createAuctionDto.bidUnit());
softAssertions.assertThat(actual.startBidPrice()).isEqualTo(createAuctionDto.startBidPrice());
softAssertions.assertThat(actual.lastBidPrice()).isNull();
softAssertions.assertThat(actual.winningBidPrice()).isNull();
softAssertions.assertThat(actual.deleted()).isFalse();
softAssertions.assertThat(actual.closingTime()).isEqualTo(createAuctionDto.closingTime());
});
}
SoftAssertions
보통 테스트를 진행할 때 앞의 검증이 실패하면 해당 테스트는 전부 중지됩니다.
Soft Assertions을 이용하면 이전의 실패와 상관없이 모든 asserThat를 실행한 뒤 실패 내역을 전부 확인할 수 있습니다.
경매 도메인 엔티티 삭제 비즈니스 로직 추가
https://github.com/woowacourse-teams/2023-3-ddang/pull/45/files
경매 도메인 엔티티 삭제 비즈니스 로직 추가 by JJ503 · Pull Request #45 · woowacourse-teams/2023-3-ddang
📄 작업 내용 요약 경매 도메인 엔티티 삭제 비즈니스 로직 추가 🙋🏻 리뷰 시 주의 깊게 확인해야 하는 코드 📎 Issue 번호 closed #29
github.com
@EqualsAndHashCode(callSuper = false, of = {"id"})
callSuper 속성을 통해 equals와 hashCode 메소드 자동 생성 시 부모 클래스의 필드까지 감안할지 안 할지에 대해서 설정할 수 있습니다. callSuper = true로 설정하면 부모 클래스 필드 값들도 동일한지 체크하며, callSuper = false로 설정(기본값)하면 자신 클래스의 필드 값들만 고려합니다.
@Test
void 지정한_아이디에_해당하는_경매를_삭제한다() throws Exception {
// given
willDoNothing().given(auctionService).deleteByAuctionId(anyLong());
// when & then
mockMvc.perform(delete("/auctions/{auctionId}", 1L).contentType(MediaType.APPLICATION_JSON))
.andExpectAll(status().isNoContent());
}
willDoNothing() : 특정 메서드를 호출했을 때 아무런 동작도 하지 않도록 설정하는 것
'Backend > Code Analysis' 카테고리의 다른 글
[프로젝트 분석] F-lab 당근마켓 (0) | 2024.05.31 |
---|