MapStruct: 객체 간 매핑 프레임워크 🗺️
Java 개발자라면 객체 간 매핑 작업이 얼마나 번거롭고 시간 소모적인지 잘 알고 계실 겁니다. 특히 대규모 프로젝트에서는 이러한 작업이 더욱 복잡해지죠. 하지만 걱정 마세요! MapStruct라는 강력한 도구가 여러분의 구원자가 될 겁니다. 🦸♂️
MapStruct는 Java 기반의 코드 생성 도구로, 객체 간 매핑을 쉽고 효율적으로 처리할 수 있게 해줍니다. 이 프레임워크를 사용하면 반복적이고 오류가 발생하기 쉬운 매핑 코드를 자동으로 생성할 수 있어, 개발 시간을 크게 단축시킬 수 있습니다.
오늘은 MapStruct의 세계로 여러분을 안내하겠습니다. 이 글을 통해 MapStruct의 기본 개념부터 고급 기능까지 상세히 알아보고, 실제 프로젝트에 어떻게 적용할 수 있는지 살펴보겠습니다. 마치 재능넷에서 다양한 재능을 거래하듯이, MapStruct를 통해 여러분의 코딩 재능을 한층 업그레이드할 수 있을 거예요! 🚀
1. MapStruct 소개 📚
MapStruct는 Java 애노테이션 프로세서를 기반으로 하는 코드 생성기입니다. 이 도구의 주요 목적은 bean 클래스 간의 매핑을 쉽게 구현하는 것입니다. 다른 매핑 프레임워크와 비교했을 때, MapStruct는 다음과 같은 특징을 가지고 있습니다:
- 타입 안정성: 컴파일 시점에 오류를 잡아낼 수 있어 런타임 오류를 방지합니다.
- 성능: 생성된 매핑 코드는 순수한 메서드 호출로 이루어져 있어 매우 빠릅니다.
- 가독성: 생성된 코드는 손으로 작성한 것처럼 깔끔하고 이해하기 쉽습니다.
- 유연성: 복잡한 매핑 로직도 쉽게 구현할 수 있습니다.
MapStruct를 사용하면 개발자는 매핑 인터페이스만 정의하면 됩니다. 그러면 MapStruct가 자동으로 해당 인터페이스의 구현체를 생성해줍니다. 이는 마치 재능넷에서 필요한 재능을 찾아 바로 활용할 수 있는 것과 비슷하죠. 필요한 매핑을 정의하면, MapStruct가 그에 맞는 '재능'을 제공해주는 셈입니다. 😊
🔍 MapStruct vs 수동 매핑
전통적인 방식으로 객체 간 매핑을 구현한다면 다음과 같은 코드를 작성해야 할 것입니다:
public class UserMapper {
public UserDto userToUserDto(User user) {
UserDto dto = new UserDto();
dto.setId(user.getId());
dto.setName(user.getName());
dto.setEmail(user.getEmail());
// ... 더 많은 필드들 ...
return dto;
}
}
하지만 MapStruct를 사용하면 다음과 같이 간단하게 정의할 수 있습니다:
@Mapper
public interface UserMapper {
UserDto userToUserDto(User user);
}
이렇게 정의만 해두면, MapStruct가 자동으로 구현체를 생성해줍니다. 놀랍지 않나요? 😮
이러한 MapStruct의 강력한 기능은 특히 대규모 프로젝트에서 그 진가를 발휘합니다. 수많은 DTO(Data Transfer Object)와 엔티티 간의 변환이 필요한 경우, MapStruct는 개발자의 시간과 노력을 크게 절약해줄 수 있습니다.
위 다이어그램은 MapStruct의 기본적인 작동 원리를 보여줍니다. 소스 객체에서 대상 객체로의 매핑을 MapStruct가 자동으로 처리해주는 것을 볼 수 있습니다. 이는 마치 재능넷에서 의뢰인의 요구사항(소스 객체)을 전문가의 결과물(대상 객체)로 변환하는 과정과 유사하다고 볼 수 있겠네요. 🎨
다음 섹션에서는 MapStruct를 실제로 어떻게 사용하는지, 그리고 어떤 고급 기능들이 있는지 자세히 알아보겠습니다. MapStruct를 마스터하면, 여러분의 Java 개발 능력은 한 단계 더 도약할 수 있을 것입니다! 💪
2. MapStruct 시작하기 🚀
MapStruct를 프로젝트에 도입하는 것은 생각보다 간단합니다. 마치 재능넷에서 원하는 서비스를 쉽게 찾아 이용하듯이, MapStruct도 몇 가지 간단한 단계만 거치면 바로 사용할 수 있습니다. 그럼 지금부터 MapStruct를 프로젝트에 설정하고 사용하는 방법을 상세히 알아보겠습니다.
2.1 의존성 추가 📦
먼저, 프로젝트의 빌드 파일에 MapStruct 의존성을 추가해야 합니다. Maven을 사용하는 경우, pom.xml
파일에 다음과 같은 의존성을 추가합니다:
<properties>
<org.mapstruct.version>1.5.3.Final</org.mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source> <!-- 또는 더 높은 Java 버전 -->
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Gradle을 사용하는 경우, build.gradle
파일에 다음과 같이 추가합니다:
plugins {
id 'java'
id 'net.ltgt.apt' version '0.21'
}
dependencies {
implementation 'org.mapstruct:mapstruct:1.5.3.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
}
이렇게 의존성을 추가하면, MapStruct를 사용할 준비가 완료됩니다. 마치 재능넷에서 원하는 서비스를 찾아 계약을 맺는 것처럼, 이제 MapStruct와 '계약'을 맺은 셈이죠! 😄
2.2 첫 번째 매퍼 만들기 🛠️
이제 실제로 매퍼를 만들어 보겠습니다. 예를 들어, User
엔티티를 UserDto
로 변환하는 매퍼를 만들어 봅시다.
먼저, 엔티티와 DTO 클래스를 정의합니다:
public class User {
private Long id;
private String name;
private String email;
private LocalDate birthDate;
// getters and setters
}
public class UserDto {
private Long id;
private String name;
private String email;
private int age;
// getters and setters
}
이제 이 두 클래스 간의 매핑을 처리할 매퍼 인터페이스를 만듭니다:
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper
public interface UserMapper {
@Mapping(target = "age", expression = "java(calculateAge(user.getBirthDate()))")
UserDto userToUserDto(User user);
default int calculateAge(LocalDate birthDate) {
return Period.between(birthDate, LocalDate.now()).getYears();
}
}
이 코드에서 주목할 점은 다음과 같습니다:
@Mapper
어노테이션: 이 인터페이스가 MapStruct 매퍼임을 나타냅니다.@Mapping
어노테이션: 특별한 매핑 로직이 필요한 필드에 사용됩니다. 여기서는birthDate
를age
로 변환하는 로직을 지정했습니다.calculateAge
메서드: 커스텀 매핑 로직을 구현한 디폴트 메서드입니다.
이렇게 정의만 해두면, MapStruct가 컴파일 시점에 자동으로 구현체를 생성합니다. 마치 재능넷에서 의뢰를 올리면 알맞은 전문가가 매칭되는 것처럼, MapStruct가 여러분이 정의한 매핑을 자동으로 구현해주는 것이죠! 🎩✨
2.3 매퍼 사용하기 🖥️
이제 생성된 매퍼를 사용해봅시다. 다음과 같이 매퍼를 주입받아 사용할 수 있습니다:
@Service
public class UserService {
private final UserMapper userMapper;
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
public UserDto getUserDto(User user) {
return userMapper.userToUserDto(user);
}
}
이렇게 간단하게 MapStruct를 사용하여 객체 간 매핑을 처리할 수 있습니다. 복잡한 매핑 로직을 일일이 작성할 필요 없이, MapStruct가 자동으로 처리해주니 개발자는 비즈니스 로직에 더 집중할 수 있게 되죠.
위 다이어그램은 MapStruct를 사용한 매핑 프로세스를 시각적으로 보여줍니다. User
객체가 입력되면, UserMapper
를 통해 자동으로 UserDto
로 변환되는 과정을 볼 수 있습니다. 이는 마치 재능넷에서 의뢰인의 요구사항이 전문가의 손을 거쳐 최종 결과물로 탄생하는 과정과 유사하다고 볼 수 있겠네요. 🎨✨
지금까지 MapStruct의 기본적인 사용법을 알아보았습니다. 다음 섹션에서는 더 복잡한 매핑 시나리오와 MapStruct의 고급 기능들을 살펴보겠습니다. MapStruct의 강력한 기능들을 마스터하면, 여러분의 코드는 더욱 깔끔하고 유지보수가 쉬워질 것입니다. 계속해서 MapStruct의 세계를 탐험해볼까요? 🚀
3. MapStruct 고급 기능 🔧
MapStruct의 기본 사용법을 익혔다면, 이제 더 복잡한 시나리오를 다룰 차례입니다. MapStruct는 단순한 필드 매핑 외에도 다양한 고급 기능을 제공합니다. 이러한 기능들을 활용하면, 마치 재능넷에서 고급 전문가의 서비스를 받는 것처럼, 복잡한 매핑 요구사항도 쉽게 해결할 수 있습니다. 😎
3.1 중첩된 객체 매핑 🎭
실제 프로젝트에서는 객체 안에 다른 객체가 포함된 경우가 많습니다. MapStruct는 이러한 중첩된 객체도 쉽게 매핑할 수 있습니다.
예를 들어, User
클래스 안에 Address
객체가 포함되어 있다고 가정해봅시다:
public class User {
private Long id;
private String name;
private Address address;
// getters and setters
}
public class Address {
private String street;
private String city;
private String zipCode;
// getters and setters
}
public class UserDto {
private Long id;
private String name;
private String fullAddress;
// getters and setters
}
이제 User
를 UserDto
로 변환하면서, Address
정보를 하나의 문자열로 합치고 싶다면 다음과 같이 매퍼를 작성할 수 있습니다:
@Mapper
public interface UserMapper {
@Mapping(target = "fullAddress", expression = "java(addressToString(user.getAddress()))")
UserDto userToUserDto(User user);
default String addressToString(Address address) {
if (address == null) {
return null;
}
return String.format("%s, %s, %s", address.getStreet(), address.getCity(), address.getZipCode());
}
}
이 예제에서 addressToString
메서드는 Address
객체를 받아 문자열로 변환합니다. MapStruct는 이 메서드를 자동으로 호출하여 fullAddress
필드를 채웁니다.
3.2 컬렉션 매핑 📚
MapStruct는 리스트나 맵과 같은 컬렉션의 매핑도 자동으로 처리할 수 있습니다. 예를 들어, User
객체가 여러 개의 Order
객체를 가지고 있다고 가정해봅시다:
public class User {
private Long id;
private String name;
private List<Order> orders;
// getters and setters
}
public class Order {
private Long id;
private String productName;
private BigDecimal price;
// getters and setters
}
public class UserDto {
private Long id;
private String name;
private List<OrderDto> orders;
// getters and setters
}
public class OrderDto {
private Long id;
private String productInfo;
// getters and setters
}
이 경우, 다음과 같이 매퍼를 작성할 수 있습니다:
@Mapper
public interface UserMapper {
UserDto userToUserDto(User user);
@Mapping(target = "productInfo", expression = "java(order.getProductName() + \" - $\" + order.getPrice())")
OrderDto orderToOrderDto(Order order);
}
MapStruct는 User
객체의 orders
리스트를 자동으로 순회하면서 각 Order
객체를 OrderDto
로 변환합니다. 이는 마치 재능넷에서 여러 전문가들이 협업하여 하나의 프로젝트를 완성하는 것과 같은 원리입니다! 🤝
3.3 조건부 매핑 🔀
때로는 특정 조건에 따라 다르게 매핑해야 할 때가 있습니다. MapStruct는 이러한 상황을 위한 조건부 매핑 기능을 제공합니다.
@Mapper
public interface UserMapper {
@Mapping(target = "status", expression = "java(user.getAge() >= 18 ? \"ADULT\" : \"MINOR\")")
UserDto userToUserDto(User user);
}
이 예제에서는 사용자의 나이에 따라 status
필드를 다르게 설정합니다.
3.4 다중 소스 매핑 🔗
때로는 여러 소스 객체의 정보를 조합하여 하나의 대상 객체를 생성해야 할 때가 있습니다. MapStruct는 이러한 시나리오도 지원합니다.
@Mapper
public interface OrderMapper {
@Mapping(source = "order.id", target = "orderId")
@Mapping(source = "user.name", target = "userName")
@Mapping(source = "payment.amount", target = "totalAmount")
OrderSummaryDto orderToSummary(Order order, User user, Payment payment);
}
이 예제에서는 Order
, User
, Payment
세 개의 객체에서 정보를 가져와 OrderSummaryDto
를 생성합니다.
위 다이어그램은 MapStruct의 고급 매핑 기능을 시각화한 것입니다. 여러 소스 객체(User, Order, Payment)의 정보가 OrderMapper를 통해 하나의 OrderSummaryDto로 통합되는 과정을 보여줍니다. 이는 마치 재능넷에서 여러 전문가의 재능이 하나의 프로젝트로 융합되는 것과 같은 원리입니다! 🎨✨
3.5 사용자 정의 메서드 사용 🛠️
때로는 MapStruct의 기본 매핑으로는 해결할 수 없는 복잡한 변환 로직이 필요할 수 있습니다. 이런 경우, 사용자 정의 메서드를 작성하여 사용할 수 있습니다.
@Mapper
public interface UserMapper {
@Mapping(target = "fullName", expression = "java(formatFullName(user))")
UserDto userToUserDto(User user);
default String formatFullName(User user) {
return user.getFirstName() + " " + user.getLastName().toUpperCase();
}
}
이 예제에서 formatFullName
메서드는 사용자의 이름을 특정 형식으로 변환합니다. MapStruct는 이 메서드를 자동으로 호출하여 fullName
필드를 채웁니다.
3.6 매핑 전략 설정 ⚙️
MapStruct는 다양한 매핑 전략을 제공합니다. 예를 들어, 소스 객체의 null 값을 어떻게 처리할지, 매핑되지 않은 필드를 어떻게 다룰지 등을 설정할 수 있습니다.
@Mapper(nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL,
unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface UserMapper {
UserDto userToUserDto(User user);
}
이 설정에서는 소스 필드가 null일 경우 대상 필드도 null로 설정하고, 매핑되지 않은 대상 필드는 무시하도록 지정했습니다.
3.7 매핑 후처리 🔄
때로는 매핑이 완료된 후 추가적인 처리가 필요할 수 있습니다. MapStruct는 이를 위해 @AfterMapping
어노테이션을 제공합니다.
@Mapper
public interface UserMapper {
UserDto userToUserDto(User user);
@AfterMapping
default void setAdditionalInfo(User user, @MappingTarget UserDto userDto) {
userDto.setLastUpdated(LocalDateTime.now());
userDto.setVersion(user.getVersion() + 1);
}
}
이 예제에서는 매핑이 완료된 후 lastUpdated
필드를 현재 시간으로 설정하고, version
필드를 증가시킵니다.
🔍 MapStruct 사용 시 주의사항
- 순환 참조 주의: 객체 간 순환 참조가 있는 경우, 무한 루프에 빠질 수 있으므로 주의해야 합니다.
- 성능 고려: 매우 큰 객체나 복잡한 매핑의 경우, 성능에 영향을 줄 수 있으므로 벤치마킹이 필요할 수 있습니다.
- 테스트 중요성: 자동 생성된 코드라도 반드시 테스트를 통해 정확성을 검증해야 합니다.
- 버전 호환성: MapStruct 버전 업그레이드 시 기존 매핑에 영향이 없는지 확인이 필요합니다.
이러한 고급 기능들을 활용하면, MapStruct를 통해 거의 모든 복잡한 매핑 시나리오를 우아하게 처리할 수 있습니다. 마치 재능넷에서 고급 전문가들이 복잡한 프로젝트를 능숙하게 해결하는 것처럼 말이죠! 🚀
MapStruct의 이러한 강력한 기능들은 개발자의 생산성을 크게 향상시키고, 코드의 가독성과 유지보수성을 높여줍니다. 복잡한 매핑 로직을 일일이 수동으로 작성하는 대신, MapStruct가 제공하는 선언적 방식의 매핑을 통해 깔끔하고 효율적인 코드를 작성할 수 있습니다.
다음 섹션에서는 실제 프로젝트에서 MapStruct를 어떻게 효과적으로 활용할 수 있는지, 그리고 다른 프레임워크들과 어떻게 통합하여 사용할 수 있는지 알아보겠습니다. MapStruct의 세계는 더욱 깊고 넓답니다. 함께 더 탐험해볼까요? 💪😊
4. MapStruct 실전 활용 및 통합 🌟
지금까지 MapStruct의 기본 사용법부터 고급 기능까지 살펴보았습니다. 이제 실제 프로젝트에서 MapStruct를 어떻게 효과적으로 활용할 수 있는지, 그리고 다른 프레임워크들과 어떻게 통합하여 사용할 수 있는지 알아보겠습니다. 마치 재능넷에서 여러 전문가들의 재능을 조합하여 하나의 완벽한 프로젝트를 만들어내는 것처럼, MapStruct도 다른 도구들과 잘 어우러져 더 큰 가치를 만들어낼 수 있습니다. 🎭
4.1 Spring Framework와의 통합 🍃
Spring Framework는 Java 생태계에서 가장 널리 사용되는 프레임워크 중 하나입니다. MapStruct는 Spring과 매우 잘 통합됩니다.
@Mapper(componentModel = "spring")
public interface UserMapper {
UserDto userToUserDto(User user);
}
componentModel = "spring"
설정을 통해 MapStruct는 Spring의 컴포넌트 스캔 메커니즘에 의해 자동으로 감지되고 빈으로 등록될 수 있는 구현체를 생성합니다.
이제 다음과 같이 Spring의 의존성 주입을 통해 매퍼를 사용할 수 있습니다:
@Service
public class UserService {
private final UserMapper userMapper;
@Autowired
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
public UserDto getUserDto(User user) {
return userMapper.userToUserDto(user);
}
}
4.2 JPA 엔티티와 DTO 매핑 🗄️
JPA 엔티티와 DTO 간의 매핑은 매우 일반적인 사용 사례입니다. MapStruct를 사용하면 이러한 매핑을 쉽게 처리할 수 있습니다.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "user")
private List<Order> orders;
// getters and setters
}
public class UserDto {
private Long id;
private String name;
private int orderCount;
// getters and setters
}
@Mapper
public interface UserMapper {
@Mapping(target = "orderCount", expression = "java(user.getOrders().size())")
UserDto userToUserDto(User user);
}
이 예제에서는 JPA 엔티티인 User
를 UserDto
로 변환하면서, 주문 목록의 크기를 orderCount
필드에 매핑합니다.
4.3 MapStruct와 Lombok 함께 사용하기 🐘
Lombok은 Java의 상용구 코드를 줄여주는 인기 있는 라이브러리입니다. MapStruct와 Lombok을 함께 사용할 때는 약간의 추가 설정이 필요합니다.
Maven pom.xml
설정:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
이렇게 설정하면 Lombok과 MapStruct가 함께 잘 작동합니다.
4.4 테스트 작성하기 🧪
MapStruct로 생성된 매퍼도 반드시 테스트해야 합니다. 다음은 JUnit을 사용한 간단한 테스트 예제입니다:
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
public void testUserToUserDto() {
User user = new User();
user.setId(1L);
user.setName("John Doe");
user.setOrders(Arrays.asList(new Order(), new Order()));
UserDto userDto = userMapper.userToUserDto(user);
assertEquals(user.getId(), userDto.getId());
assertEquals(user.getName(), userDto.getName());
assertEquals(2, userDto.getOrderCount());
}
}
4.5 성능 최적화 🚀
MapStruct는 일반적으로 매우 효율적이지만, 대량의 데이터를 처리할 때는 성능 최적화가 필요할 수 있습니다.
- 배치 처리: 대량의 객체를 매핑할 때는 배치 처리를 고려하세요.
- 지연 로딩 주의: JPA 엔티티를 매핑할 때 지연 로딩된 관계를 조심스럽게 다루세요.
- 캐싱 활용: 자주 사용되는 매핑 결과는 캐싱을 고려해보세요.
4.6 버전 관리와 마이그레이션 📊
프로젝트가 발전함에 따라 DTO나 엔티티의 구조가 변경될 수 있습니다. 이런 경우 매핑 로직도 함께 업데이트해야 합니다.
@Mapper
public interface UserMapper {
@Mapping(target = "fullName", source = "name") // 이전 버전
@Mapping(target = "fullName", expression = "java(user.getFirstName() + \" \" + user.getLastName())") // 새 버전
UserDto userToUserDto(User user);
}
이런 방식으로 버전 간 호환성을 유지하면서 점진적으로 매핑 로직을 업데이트할 수 있습니다.
위 다이어그램은 MapStruct가 Java 생태계의 다른 주요 도구들과 어떻게 통합되는지를 보여줍니다. Spring, JPA, Lombok, JUnit 등과 조화롭게 작동하여 개발 프로세스를 더욱 효율적으로 만들어줍니다. 이는 마치 재능넷에서 다양한 분야의 전문가들이 협업하여 하나의 완벽한 프로젝트를 완성하는 것과 같습니다! 🌈
MapStruct를 실제 프로젝트에 적용하면서, 여러분은 점점 더 강력하고 유연한 매핑 솔루션을 구축할 수 있을 것입니다. 복잡한 비즈니스 로직, 다양한 데이터 모델, 그리고 끊임없이 변화하는 요구사항 속에서도 MapStruct는 여러분의 든든한 동반자가 될 것입니다.
MapStruct의 세계는 여기서 끝나지 않습니다. 새로운 버전이 출시될 때마다 더욱 강력한 기능들이 추가되고 있으며, 커뮤니티의 활발한 참여로 계속해서 발전하고 있습니다. 여러분도 이 여정에 동참해보는 건 어떨까요? MapStruct를 사용하면서 발견한 팁이나 트릭을 공유하고, 오픈 소스 프로젝트에 기여하면서 함께 성장해 나갈 수 있습니다.
자, 이제 여러분은 MapStruct의 기본부터 고급 기능, 그리고 실제 활용 방법까지 모두 알게 되었습니다. 이 강력한 도구를 활용하여 여러분의 프로젝트를 한 단계 더 발전시켜 보세요. 복잡한 매핑 작업은 MapStruct에게 맡기고, 여러분은 더 중요한 비즈니스 로직에 집중할 수 있을 것입니다. 마치 재능넷에서 전문가의 도움을 받아 프로젝트를 성공적으로 완수하는 것처럼 말이죠! 🎉
MapStruct와 함께하는 개발 여정이 즐겁고 생산적이기를 바랍니다. 행운을 빕니다! 🍀