Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: MapStruct 적용 #38

Merged
merged 10 commits into from
Oct 4, 2024
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation "io.jsonwebtoken:jjwt-api:${jjwt_version}"
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${swagger_version}"
implementation 'org.mapstruct:mapstruct:1.6.2'

annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.2'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
// testImplementation 'org.springframework.security:spring-security-test'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,53 @@
package com.ordertogether.team14_be.spot.controller;

import com.ordertogether.team14_be.spot.dto.SpotDto;
import com.ordertogether.team14_be.spot.dto.controllerdto.SpotCreationRequest;
import com.ordertogether.team14_be.spot.dto.controllerdto.SpotCreationResponse;
import com.ordertogether.team14_be.spot.dto.controllerdto.SpotDetailResponse;
import com.ordertogether.team14_be.spot.dto.controllerdto.SpotViewedResponse;
import com.ordertogether.team14_be.spot.mapper.SpotMapper;
import com.ordertogether.team14_be.spot.service.SpotService;
import java.math.BigDecimal;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
public class SpotController {

private final SpotService spotService;

@Autowired
public SpotController(SpotService spotService) {
this.spotService = spotService;
}

// Spot 전체 조회하기
@GetMapping("/api/v1/spot/{lat}/{lng}")
public ResponseEntity<List<SpotDto>> getSpot(
public ResponseEntity<List<SpotViewedResponse>> getSpot(
@PathVariable BigDecimal lat, @PathVariable BigDecimal lng) {
return ResponseEntity.ok(spotService.getSpot(lat, lng));
}

// Spot 생성하기
@PostMapping("/api/v1/spot")
public ResponseEntity<SpotDto> createSpot(@RequestBody SpotDto spotDto) {
return ResponseEntity.ok(spotService.createSpot(spotDto));
public ResponseEntity<SpotCreationResponse> createSpot(
@RequestBody SpotCreationRequest spotCreationRequest) {
return new ResponseEntity<>(
spotService.createSpot(SpotMapper.INSTANCE.toSpotDto(spotCreationRequest)),
HttpStatus.CREATED);
}

// Spot 상세 조회하기
@GetMapping("/api/v1/spot/{id}")
public ResponseEntity<SpotDto> getSpot(@PathVariable Long id) {
public ResponseEntity<SpotDetailResponse> getSpotDetail(@PathVariable Long id) {
return ResponseEntity.ok(spotService.getSpot(id));
}

// Spot 수정하기
@PutMapping("/api/v1/spot")
public ResponseEntity<SpotDto> updateSpot(@RequestBody SpotDto spotDto) {
return ResponseEntity.ok(spotService.updateSpot(spotDto));
public ResponseEntity<SpotCreationResponse> updateSpot(
@RequestBody SpotCreationRequest spotCreationRequest) {
return ResponseEntity.ok(
SpotMapper.INSTANCE.toSpotCreationResponse(
spotService.updateSpot(SpotMapper.INSTANCE.toSpotDto(spotCreationRequest))));
}

// Spot 삭제하기
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.ordertogether.team14_be.spot.dto.controllerdto;

import java.math.BigDecimal;

public record SpotCreationRequest(
Long id,
BigDecimal lat,
BigDecimal lng,
String storeName,
Integer minimumOrderAmount,
String togetherOrderLink,
String pickUpLocation) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ordertogether.team14_be.spot.dto.controllerdto;

import com.ordertogether.team14_be.spot.enums.Category;

public record SpotCreationResponse(
Long id,
Category category,
String storeName,
Integer minimumOrderAmount,
String pickUpLocation) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ordertogether.team14_be.spot.dto.controllerdto;

import com.ordertogether.team14_be.spot.enums.Category;

public record SpotDetailResponse(
Category category,
String storeName,
Integer minimumOrderAmount,
String pickUpLocation,
String deliveryStatus) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.ordertogether.team14_be.spot.dto.controllerdto;

public record SpotModifyRequest(
Long id, String storeName, Integer minimumOrderAmount, String pickUpLocation) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.ordertogether.team14_be.spot.dto.controllerdto;

import com.ordertogether.team14_be.spot.enums.Category;

public record SpotViewedResponse(
Category category, String storeName, Integer minimumOrderAmount, String pickUpLocation) {}
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package com.ordertogether.team14_be.spot.dto;
package com.ordertogether.team14_be.spot.dto.servicedto;

import com.ordertogether.team14_be.spot.entity.Spot;
import com.ordertogether.team14_be.spot.enums.Category;
import jakarta.persistence.Column;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import lombok.*;

@Builder
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class SpotDto {
private Long id;
Expand All @@ -31,23 +30,4 @@ public class SpotDto {
private LocalDateTime modifiedAt;
private Long createdBy;
private Long modifiedBy;

public Spot toEntity() {
return Spot.builder()
.id(id)
.lat(lat)
.lng(lng)
.category(category)
.storeName(storeName)
.minimumOrderAmount(minimumOrderAmount)
.togetherOrderLink(togetherOrderLink)
.pickUpLocation(pickUpLocation)
.deliveryStatus(deliveryStatus)
.isDeleted(isDeleted)
.createdAt(createdAt)
.modifiedAt(modifiedAt)
.createdBy(createdBy)
.modifiedBy(modifiedBy)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.ordertogether.team14_be.spot.entity;

import com.ordertogether.team14_be.common.persistence.entity.BaseEntity;
import com.ordertogether.team14_be.member.persistence.entity.Member;
import com.ordertogether.team14_be.spot.enums.Category;
import jakarta.persistence.*;
import java.math.BigDecimal;
Expand All @@ -21,6 +22,10 @@ public class Spot extends BaseEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne
@JoinColumn(name = "master_id") // PK 참조해서 master_id 속성 추가
private Member member;

@Column(precision = 10, scale = 8)
private BigDecimal lat;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.ordertogether.team14_be.spot.mapper;

import com.ordertogether.team14_be.spot.dto.controllerdto.SpotCreationRequest;
import com.ordertogether.team14_be.spot.dto.controllerdto.SpotCreationResponse;
import com.ordertogether.team14_be.spot.dto.controllerdto.SpotDetailResponse;
import com.ordertogether.team14_be.spot.dto.controllerdto.SpotViewedResponse;
import com.ordertogether.team14_be.spot.dto.servicedto.SpotDto;
import com.ordertogether.team14_be.spot.entity.Spot;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.ReportingPolicy;
import org.mapstruct.factory.Mappers;

@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.IGNORE) // Spring Bean으로 등록
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MappercomponentModel, unmappedTargetPolicy 각 옵션들은 무슨 역할을 수행하나요?

그리고 MapStruct 를 사용한 이유가 궁금합니다!

Copy link
Contributor Author

@rbm0524 rbm0524 Oct 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

componentModel = "spring"을 통해서 Spring bean으로 관리합니다. unmappedTargetPolicy = ReportingPolicy.IGNORE은 매핑되지 않는 필드에 대해 null처리를 하거나 무시합니다. createdAt이나 id 필드는 dto에서 값을 받아오는 것이 아니기 때문에 Controller dto에서 Service dto로 매핑할 때 null 처리를 해놓고 Spot 엔티티를 새로 생성하거나 업데이트할 때 null이 아닌 dto의 값만 활용할 수 있게 한 것입니다.
MapStruct는 Mapper 클래스나 Mapping을 직접 코딩할 필요 없이 자동으로 매핑해주기 때문에 확장에 용이하다고 생각해서 적용해봤습니다!

public interface SpotMapper {
SpotMapper INSTANCE = Mappers.getMapper(SpotMapper.class); // 객체 생성해서 INSTANCE에 할당

SpotDto toDto(Spot spot);

SpotDto toSpotDto(SpotCreationRequest spotCreationRequest);

Spot toEntity(SpotDto spotDto);

Spot toEntity(SpotDto spotDto, @MappingTarget Spot spot); // 생성 또는 수정할 때 사용

SpotCreationResponse toSpotCreationResponse(SpotDto spotDto);

SpotDetailResponse toSpotDetailResponse(SpotDto spotDto);

SpotViewedResponse toSpotViewedResponse(SpotDto spotDto);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.ordertogether.team14_be.spot.repository;

import com.ordertogether.team14_be.spot.entity.Spot;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface SimpleSpotRepository extends JpaRepository<Spot, Long> {
List<Spot> findByLatAndLngAndIsDeletedFalse(BigDecimal lat, BigDecimal lng);

Optional<Spot> findByIdAndIsDeletedFalse(Long id);
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,42 @@
package com.ordertogether.team14_be.spot.repository;

import com.ordertogether.team14_be.spot.dto.servicedto.SpotDto;
import com.ordertogether.team14_be.spot.entity.Spot;
import com.ordertogether.team14_be.spot.mapper.SpotMapper;
import jakarta.persistence.EntityNotFoundException;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@Repository
public interface SpotRepository extends JpaRepository<Spot, Long> {
List<Spot> findByLatAndLngAndIsDeletedFalse(BigDecimal lat, BigDecimal lng);
@RequiredArgsConstructor
public class SpotRepository {

Optional<Spot> findByIdAndIsDeletedFalse(Long id);
private final SimpleSpotRepository simpleSpotRepository;

public SpotDto save(Spot spot) {
return SpotMapper.INSTANCE.toDto(simpleSpotRepository.save(spot));
}

public SpotDto findByIdAndIsDeletedFalse(Long id) {
return SpotMapper.INSTANCE.toDto(
simpleSpotRepository
.findByIdAndIsDeletedFalse(id)
.orElseThrow(() -> new EntityNotFoundException(id + "에 해당하는 Spot을 찾을 수 없습니다.")));
}

public List<SpotDto> findByLatAndLngAndIsDeletedFalse(BigDecimal lat, BigDecimal lng) {
return simpleSpotRepository.findByLatAndLngAndIsDeletedFalse(lat, lng).stream()
.map(SpotMapper.INSTANCE::toDto)
.toList();
}

public void delete(Long id) {
Spot spot =
simpleSpotRepository
.findByIdAndIsDeletedFalse(id)
.orElseThrow(() -> new EntityNotFoundException(id + "에 해당하는 Spot을 찾을 수 없습니다."));
spot.delete();
}
}
Original file line number Diff line number Diff line change
@@ -1,79 +1,55 @@
package com.ordertogether.team14_be.spot.service;

import com.ordertogether.team14_be.spot.dto.SpotDto;
import com.ordertogether.team14_be.spot.dto.controllerdto.SpotCreationResponse;
import com.ordertogether.team14_be.spot.dto.controllerdto.SpotDetailResponse;
import com.ordertogether.team14_be.spot.dto.controllerdto.SpotViewedResponse;
import com.ordertogether.team14_be.spot.dto.servicedto.SpotDto;
import com.ordertogether.team14_be.spot.entity.Spot;
import com.ordertogether.team14_be.spot.mapper.SpotMapper;
import com.ordertogether.team14_be.spot.repository.SpotRepository;
import jakarta.persistence.EntityNotFoundException;
import jakarta.transaction.Transactional;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class SpotService {

private final SpotRepository spotRepository;

@Autowired
public SpotService(SpotRepository spotRepository) {
this.spotRepository = spotRepository;
}

// Spot 전체 조회하기
public List<SpotDto> getSpot(BigDecimal lat, BigDecimal lng) {
public List<SpotViewedResponse> getSpot(BigDecimal lat, BigDecimal lng) {
return spotRepository.findByLatAndLngAndIsDeletedFalse(lat, lng).stream()
.map(this::toDto)
.collect(Collectors.toList());
.map(SpotMapper.INSTANCE::toSpotViewedResponse)
.toList();
}

@Transactional
public SpotDto createSpot(SpotDto spotDto) {
Spot spot = spotDto.toEntity();
return toDto(spotRepository.save(spot));
public SpotCreationResponse createSpot(SpotDto spotDto) {
Spot spot = SpotMapper.INSTANCE.toEntity(spotDto, new Spot());
return SpotMapper.INSTANCE.toSpotCreationResponse(spotRepository.save(spot));
}

// Spot 상세 조회하기
public SpotDto getSpot(Long id) {
Spot spot =
spotRepository
.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Spot을 찾을 수 없습니다."));
return toDto(spot);
public SpotDetailResponse getSpot(Long id) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전체적으로 조회만 수행하는 메서드에 대해서 트랜잭션 어노테이션을 명시하지 않은 이유가 있을까요?
readOnly 옵션을 활성화하여 트랜잭션 어노테이션을 붙이면 어떤 장점이 생길까요?

공부하고 댓글로 알려주면 유익할 것 같습니다

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Transactional이 조회에서 사용될 필요가 있을까? 생각해서 붙이지 않았습니다. 생성이나 수정이 아니라서 새로고침하면 된다고 생각했었습니다. @Transactional(readOnly=true)를 보니 트래픽 분산으로 속도가 향상되고 예상치 못한 수정 방지, dirty checking을 위한 Snapshot을 보관하지 않아서 메모리도 절약할 수 있네요. 추가하겠습니다!

SpotDto spotDto = spotRepository.findByIdAndIsDeletedFalse(id);
return SpotMapper.INSTANCE.toSpotDetailResponse(spotDto);
}

@Transactional
public SpotDto updateSpot(SpotDto spotDto) {
Spot spot = spotRepository.save(spotDto.toEntity());
return toDto(spot);
Spot spot =
SpotMapper.INSTANCE.toEntity(
spotDto,
SpotMapper.INSTANCE.toEntity(
spotRepository.findByIdAndIsDeletedFalse(spotDto.getId())));
return SpotMapper.INSTANCE.toDto(spot);
}

@Transactional
public void deleteSpot(Long id) {
Optional<Spot> spotToDelete = spotRepository.findByIdAndIsDeletedFalse(id);
spotToDelete.ifPresent(Spot::delete);
}

// Service Layer에서 toDto만들어서 매핑시키기
public SpotDto toDto(Spot spotInStream) {
Spot spot =
spotRepository
.findById(spotInStream.getId())
.orElseThrow(() -> new EntityNotFoundException("Spot을 찾을 수 없습니다."));

return SpotDto.builder()
.id(spot.getId())
.lat(spot.getLat())
.lng(spot.getLng())
.category(spot.getCategory())
.storeName(spot.getStoreName())
.minimumOrderAmount(spot.getMinimumOrderAmount())
.togetherOrderLink(spot.getTogetherOrderLink())
.pickUpLocation(spot.getPickUpLocation())
.deliveryStatus(spot.getDeliveryStatus())
.isDeleted(spot.getIsDeleted())
.build();
spotRepository.delete(id);
}
}