πŸ“— JPA

MapStruct! JPA Entity λ§€ν•‘ κ°„ μ£Όμ˜ν•΄μ„œ μ‚¬μš©ν•˜μž

GroovyArea 2024. 3. 24. 16:04

졜근 비상 결제 λͺ¨λ“œλ₯Ό 개발 및 λ°°ν¬ν•˜λ©°, 
참으둜 어이 μ—†λŠ” μž‘μ€ μ½”λ”© κ΄€λ ¨ 이슈둜 인해 데이터에 이상이 μžˆμ—ˆλ‹€.
λ°”λ‘œ, MapStruct μ–˜ λ•Œλ¬ΈμΈλ°..
ν•œλ²ˆ λ‚˜μ—΄ν•΄λ³΄κ² λ‹€.
(항상 μƒκ°ν•˜λŠ” κ±°μ§€λ§Œ, μ½”λ“œ 단 ν•œμ€„μ˜ νŒŒκΈ‰νš¨κ³Όκ°€ μ—„μ²­ λ‚˜λ‹€.)

 

MapStruct λž€?

  • Java Bean μœ ν˜• κ°„ λ§€ν•‘ κ΅¬ν˜„μ„ λ„μ™€μ£ΌλŠ” μ½”λ“œ 생성기
  • 컴파일 νƒ€μž„μ— μ½”λ“œ 생성 및 λŸ°νƒ€μž„μ—μ„œ μ•ˆμ •μ„± 보μž₯
  • 순수 Java code λ₯Ό ν˜ΈμΆœν•˜λ―€λ‘œ λ‹€λ₯Έ λ§€ν•‘ λΌμ΄λΈŒλŸ¬λ¦¬λ³΄λ‹€ 속도가 λΉ λ₯΄λ‹€. (Reflection 을 μ‚¬μš©ν•˜μ§€ μ•ŠκΈ° λ•Œλ¬Έ)
  • Annotation Processor λ₯Ό μ΄μš©ν•˜μ—¬ λ§€ν•‘ 방식에 νŽΈλ¦¬ν•¨μ„ 제곡

 

μ‚¬μš© μ˜ˆμ‹œ

JAVA

@Mapper
public interface UserMapper {

		UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
		
		UserResponseDTO convertToUserResponseDTO(User user);
}

 
Kotlin

@Entity
@Table(name = "users")
class User(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    val uid: String,
    val name: String,
    val address: String,
    val phoneNumber: String,
    val createdAt: LocalDateTime = LocalDateTime.now(),
    @LastModifiedDate
    val updatedAt: LocalDateTime = LocalDateTime.now(),
)

data class UserResponseDTO(
    val uid: String,
    val name: String,
    val address: String,
)

@Mapper(
    componentModel = ComponentModel.SPRING,
    nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE,
    nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT,
    unmappedTargetPolicy = ReportingPolicy.IGNORE,
    unmappedSourcePolicy = ReportingPolicy.IGNORE
)
interface UserMapper {

		companion object {
				val INSTANCE = Mappers.getMapper(UserMapper::class.java)
		}
		
		fun convertToResponseDTO(user: User): UserResponseDTO
}

 
μœ„μ™€ 같이 μ˜ˆμ‹œ μ½”λ“œκ°€ μžˆλ‹€.
gradle 의 classes task λ₯Ό μ‹€ν–‰ν•˜μ—¬, build ν•˜κ²Œ 되면 μ•„λž˜μ™€ 같이 MapStruct κ°€ UserMapper λ₯Ό κ΅¬ν˜„ν•œ, UserMapperImpl 클래슀λ₯Ό μƒμ„±ν•΄μ£ΌλŠ” 것을 확인할 수 μžˆλ‹€.
 

MapStruct μ‚¬μš© 쀑 이슈 λ°œμƒ

졜근 MapStruct μ‚¬μš© 쀑 μ΄μŠˆκ°€ λ°œμƒν–ˆλ‹€.
 

이슈 μ„€λͺ…

  • Jpa Entity λ§€ν•‘ 쀑, id (PK) κΉŒμ§€ λ§€ν•‘ λ˜μ–΄ 버린 이슈
  • Jpa Entity λ₯Ό μƒμ„±ν•˜μ—¬ Id ν•„λ“œ 값이 0 μ΄κ±°λ‚˜, null 일 경우, mysql 의 auto_increment 속성에 μ˜ν•΄ μ„ ν˜•μ μœΌλ‘œ μ¦κ°€ν•œ λ°μ΄ν„°λ‘œ μ €μž₯ν•  수 μžˆλ‹€.
    • ν•˜μ§€λ§Œ, id ν•„λ“œ 값을 ν• λ‹Ήν•œ ν›„ save λ₯Ό ν•˜κ²Œ 되면, JPA λŠ” ν•΄λ‹Ή 식별 ν‚€λ₯Ό κ°€μ§„ DB Record κ°€ μ‘΄μž¬ν•  경우, Update 쿼리λ₯Ό, μ‘΄μž¬ν•˜μ§€ μ•Šμ„ 경우 Insert 쿼리λ₯Ό μƒμ„±ν•˜μ—¬ Flush ν•œλ‹€.
    • 이 λΆ€λΆ„μ—μ„œ κ°œλ°œκ³„, μš΄μ˜κ³„ DB 데이터가 μƒμ΄ν•˜μ—¬ 인지 λ˜μ§€ λͺ»ν•œ 채 문제λ₯Ό μΌμœΌμΌ°λ‹€.

이슈 νŠΈλž˜ν‚Ή κ³Όμ •

    @Mapping(
        source = "dto.orderItemStatus",
        target = "status"
    )
    @Mapping(
        source = "order",
        target = "order"
    )
    @Mapping(
        target = "oiid",
        expression = "java(OrderItem.generateOIID(hashedId))"
    )
    fun convertToOrderItem(
        dto: OrderSyncRequestDTO.OrderItem,
        order: Order,
        sellerId: Long,
        hashedId: String,
    ): OrderItem
  • λ¬Έμ œκ°€ λ˜μ—ˆλ˜ ν•¨μˆ˜
  • Order JPA Entity λ₯Ό 인자둜 λ„˜κΈ΄ 상황
@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2024-03-21T14:37:36+0900",
    comments = "version: 1.5.4.Final, compiler: IncrementalProcessingEnvironment from kotlin-annotation-processing-gradle-1.7.22.jar, environment: Java 17.0.7 (Azul Systems, Inc.)"
)
@Component
public class OrderItemMapperImpl implements OrderItemMapper {

    @Override
    public OrderItem convertToOrderItem(OrderSyncRequestDTO.OrderItem dto, Order order, long sellerId, String hashedId) {

        OrderItemStatus status = null;
        String productName = null;
        BigDecimal productPrice = null;
        int quantity = 0;
        BigDecimal vat = null;
        BigDecimal vos = null;
        BigDecimal discountedPrice = null;
        BigDecimal originalPrice = null;
        String categoryName = null;
        Integer originalQuantity = null;
        if ( dto != null ) {
            isVat = dto.getVatted();
            status = dto.getOrderItemStatus();
            productName = dto.getProductName();
            productPrice = dto.getProductPrice();
            quantity = dto.getQuantity();
            totalPrice = dto.getTotalPrice();
            vat = dto.getVat();
            vos = dto.getVos();
            discountedPrice = dto.getDiscountedPrice();
            originalPrice = dto.getOriginalPrice();
            priceType = dto.getPriceType();
            originalQuantity = dto.getOriginalQuantity();
        }
        Order order1 = null;
        long sellerId1 = 0L;
        if ( order != null ) {
            order1 = order;
            sellerId1 = order.getSellerId();
        }

        String oiid = OrderItem.generateOIID(hashedId);
        LocalDateTime paidAt = null;
        LocalDateTime refundedAt = null;
        Long productId = null;

        OrderItem orderItem = new OrderItem( oiid, productName, productPrice, quantity, options, paidAt, refundedAt, productId, sellerId1, totalPrice, isVat, vat, vos, originalPrice, status, order1 );

				// 이 뢀뢄이 문제
        if ( order != null ) {
            orderItem.setId( order.getId() );
            orderItem.setCreatedAt( order.getCreatedAt() );
            orderItem.setUpdatedAt( order.getUpdatedAt() );
        }

        return orderItem;
    }
}

 
OrderItemMapperImpl ν΄λž˜μŠ€μ—μ„œ ν•΄λ‹Ή 좔상 λ©”μ„œλ“œλ₯Ό κ΅¬ν˜„ν•œ μ½”λ“œλŠ” 같은 μ΄λ¦„μ˜ ν•„λ“œλ₯Ό λ§€ν•‘ν•˜κΈ° μœ„ν•΄ order 의 id, createdAt, updatedAt λ₯Ό getter λ₯Ό 톡해 호좜, μƒˆλ‘­κ²Œ μƒμ„±λœ orderItem 객체에 속성해 setter λ₯Ό 톡해 λΆ€μ—¬ν•˜κ³  μžˆλŠ” 뢀뢄을 확인할 수 μžˆλ‹€.
 

κ°œλ°œκ³„

κ°œλ°œκ³„μ—μ„œ order_items ν…Œμ΄λΈ” 쀑 쀑간에 λΉ„μ–΄ μžˆλŠ” λ ˆμ½”λ“œκ°€ λ‹€μˆ˜ μžˆμ—ˆκ³ , auto_increment λ˜λŠ” order 의 id 에 ν•΄λ‹Ή λ˜λŠ” λ ˆμ½”λ“œκ°€ μ—†μ—ˆμœΌλ―€λ‘œ Insert Query κ°€ λ°œμƒν–ˆλ‹€.

 

κ°œλ°œκ³„ 디버그 ν…ŒμŠ€νŠΈ

μš”κ΅¬ 사항

  • order 및 order_items λ₯Ό μƒμ„±ν•˜κ³  μ €μž₯

디버그 κ³Όμ •

  • μƒˆλ‘œμ΄, auto_increment 될 order 의 id = 9098875
  • order_items id κ°€ 9098875 인 λ ˆμ½”λ“œλŠ” μ—†λŠ” 상황
  • order_items jpa entity λŠ” 2κ°œκ°€ μƒκ²Όμ§€λ§Œ, id (order entity 의 id 와 동일) λŠ” λ™μΌν•œ 것을 확인할 수 μžˆλ‹€..
  • κ·Έ κ²°κ³Ό insert λŠ” λ˜μ§€λ§Œ, order_item λ ˆμ½”λ“œλŠ” 1개만 μŒ“μΈλ‹€.
  • λ§Œμ•½ μžˆμ—ˆλ‹€λ©΄, κΈ°μ‘΄ λ°μ΄ν„°λŠ” update λœλ‹€.

β‡’ λ™μΌν•œ μš”μ²­μ„ 톡해 update κ°€ λ˜μ–΄λ²„λ¦° order_item 데이터 확인 κ°€λŠ₯
 

ν…ŒμŠ€νŠΈκ³„

  • μš΄μ˜κ³„μ—μ„œ order_items ν…Œμ΄λΈ”μ€ λΉ„μ–΄ μžˆλŠ” 데이터가 μ—†μ—ˆμœΌλ―€λ‘œ, μƒˆλ‘­κ²Œ auto_increment λ˜λŠ” order id κ°’κ³Ό λ™μΌν•œ order_item 의 id 값은 이미 μ‘΄μž¬ν•˜λ―€λ‘œ, update Query κ°€ λ°œμƒλ˜μ—ˆλ‹€.

β‡’ order_items κ°€ 2개 인 μƒν™©μ—μ„œ update 쿼리만 λ°œμƒ λ˜λŠ” 것을 확인할 수 μžˆμ—ˆλ‹€. (rollback 을 톡해, DB λ°˜μ˜μ€ μ•ˆ μ‹œμΌ°λ‹€.)
β‡’ μ‹€μ œλ‘œ 2개 λ‘œμš°κ°€ μƒˆλ‘­κ²Œ μ μž¬λ˜μ–΄μ•Ό ν•˜λŠ” order_item 은 λ™μΌν•œ order_id id 값을 ν• λ‹Ή 받은 ν›„ 기쑴에 μ €μž₯ λ˜μ–΄ μžˆλŠ” order_item λ ˆμ½”λ“œμ˜ 데이터λ₯Ό update ν•˜λŠ” 결과둜 μ΄μ–΄μ‘Œλ‹€.
 

ν•΄κ²°

  • hotfix μƒν™©μ΄λ―€λ‘œ, κ°„λž΅νžˆ MapStruct μ• λ…Έν…Œμ΄μ…˜ Mapping 의 속성 쀑 ignore 속성을 톡해 id, createdAt, updatedAt ν•„λ“œλ₯Ό true 둜 μ„€μ •ν–ˆλ‹€.
    @Mapping(
        source = "dto.orderItemStatus",
        target = "status"
    )
    @Mapping(
        source = "order",
        target = "order"
    )
    @Mapping(
        target = "oiid",
        expression = "java(OrderItem.generateOIID(hashedId))"
    )
    @Mapping( // μΆ”κ°€
        target = "id",
        ignore = true
    )
    @Mapping( // μΆ”κ°€
        target = "createdAt",
        ignore = true
    )
    @Mapping( // μΆ”κ°€
        target = "updatedAt",
        ignore = true
    )
    fun convertToOrderItem(
        dto: OrderSyncRequestDTO.OrderItem,
        order: Order,
        sellerId: Long,
        hashedId: String,
    ): OrderItem
  • ignore 속성을 μ΄μš©ν•œ λͺ¨μŠ΅μ„ 확인 κ°€λŠ₯ν•˜λ‹€.
  • 이 μƒνƒœλ‘œ λ‹€μ‹œ λΉŒλ“œλ₯Ό μ§„ν–‰ν•˜λ©΄, MapStruct λŠ” Target 의 ν•΄λ‹Ή ν•„λ“œ 맀핑을 λ¬΄μ‹œν•œλ‹€.

 

κ²°λ‘ 

  • Object κ°„ 맀핑에 νŽΈμ˜μ„±μ„ λ”ν•˜κΈ° μœ„ν•΄ μ‚¬μš©ν•˜λŠ” MapStruct!
  • JPA Entity κ°„ 맀핑에 μœ μ˜ν•˜μž..
  • JPA Entity 맀핑에 ν•„μš”ν•œ 인자둜 JPA Entity λ₯Ό μ΄μš©μ„ μ§€μ–‘ν•˜μž..
λ°˜μ‘ν˜•