Mapstruct(DTO Mapper ๋ผ์ด๋ธ๋ฌ๋ฆฌ)
by JiwonDev
๐ญ DTO Mapper๊ฐ ๋ญ์ฃ ?
View(Controller)๋ ๋น์ฆ๋์ค ๋ก์ง, ๋๋ฉ์ธ๊ณผ ์๊ด์์ด ์ธ์ ๋ ์ง ๋ณ๊ฒฝ๋ ์ ์๋ค.
๋๋ฉ์ธ ์๋น์ค์์ ์ ์ถ๋ ฅ์ ๋ฐ๋ก ๋ฐ์ผ๋ฉด View์ ์์กด์ฑ์ด ์๊ธฐ๊ธฐ ๋๋ฌธ์ ์๋์ ๊ฐ์ด DTO๋ก ๋ณํํด์ ์ฌ์ฉํ๋ค.
// ์ด์ Controller(Dto ๋๋ Request)๊ฐ ๋ณํด๋, ๋๋ฉ์ธ์ ์ํฅ๋ฐ์ง ์๋๋ค. UserEntity toEntity(UserDto userDto) { return new userEntity(userDto.getId(), userDto.getPassword(), userDto.getName()); } RegisterUserCommand toCommand(RegisterUserRequest request){...} UserResponse toResponse(UserDto dto){...}
// View - Domain ์ฌ์ด ์ค๊ฐ ๊ณ์ธต์ด ์๋ค๋ฉด ๊ตณ์ด Dto๋ฅผ ๋ง๋ค์ง ์๊ณ ๊ฐ์ ํ๋์ฉ ๊บผ๋ด๋ ๋๋ค. void register(RegisterUserRequest request){ userService.register(request.getName(), request.getId(), request.getPassword()); }
์๋น์ค๋ฅผ ๋ง๋ค๋ค๋ณด๋ฉด ์๋์ ๊ฐ์ด ๋ง์ ํ๋๋ฅผ ์ฃผ๊ณ ๋ฐ์์ผ ํ๋ ์ผ์ด ๋ง๋ค.
์ด๊ฒ ๊ท์ฐฎ๊ธฐ๋ง ํ๋ฉด ๋คํ์ธ๋ฐ, ์์ฑ์๊ฐ ์๋ Builder๋ฅผ ์ฌ์ฉํ๋๊ฒฝ์ฐ ๋ณํ์ ๋นผ๋จน์์ ๋ ์ปดํ์ผ ํ์์ ์ฐพ๊ธฐ ์ด๋ ต๋ค.
@Getter @Setter @ToString public class RegisterOrderRequest { @NotNull private Long userId; @NotBlank private String payMethod; @NotBlank private String receiverName; @NotBlank private String receiverPhone; @NotBlank private String receiverZipcode; @NotBlank private String receiverAddress1; @NotBlank private String receiverAddress2; @NotBlank private String etcMessage; private List<RegisterOrderItem> orderItemList; }
// Dto, Request๊ฐ ๋ณ๊ฒฝ๋๋ค๋ฉด, ์๋ ๋งคํ ๋ฉ์๋( toCommand() )๋ ๋ค์ ์์ฑํด์ผํ๋ค. RegisterOrderCommand toCommand(RegisterOrderRequest request) { return RegisterOrderCommand.builder() .request.id() .request.payMethod() ... .build() // ์คํํ๊ธฐ ์ ๊น์ง ํ๋๊ฐ ๋๋ฝ๋์์์ ์๊ธฐ ์ด๋ ต๋ค. ); }
์ด๋ฐ ์ค์๋ฅผ ๋ง์์ฃผ๊ณ ๋งค๋ฒ ๋๊ฐ์ ๋ฐ๋ณต์์ ์ ์๋ํ ํด์ฃผ๋ ๋๊ตฌ, Dto Mapper ๊ฐ์๊ฑด ์์๊น?
โก ์ด๊ฒ ๋ฐ๋ก Mapstruct์ด๋ค.
MapStruct 1.5.0.Beta2 Reference Guide
If set to true, MapStruct in which MapStruct logs its major decisions. Note, at the moment of writing in Maven, also showWarnings needs to be added due to a problem in the maven-compiler-plugin configuration.
mapstruct.org
๐ ์๋ํํด์ฃผ๋ ModelMapper๋ ์๋ ๋ถํฐ ์์์ง์๋์?
ModelMapper ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ด๋ ๋๊ตฌ๋ค์ ์๋ ๋ถํฐ ์กด์ฌํ๋ค.
ํ์ง๋ง ๋๋ถ๋ถ ๋ฆฌํ๋ ์ ๊ณผ Setter๋ฅผ ์ฌ์ฉ์ ๊ฐ์ ํ๋ค๋ณด๋ ์ด๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋ฒ๊น ๋ ํ๋ค์ด์ง๊ณ , ๋ฐํ์ ์ฑ๋ฅ์ ์ํฅ์ ๋ผ์น๊ธฐ ๋๋ฌธ์ ์ฌ์ฉํ๊ธฐ์๋ ๋ง์ด ๋ง์ค์ฌ์ก์๋ค.
Mapstruct๋ฅผ ์ถ์ฒํ๋ ์ด์ ๋ ์๋์ ๊ฐ๋ค.
- ๋ฆฌํ๋ ์ ์ ์ฌ์ฉํ์ง ์๊ณ , ์ปดํ์ผ ํ์(์ด๋ ธํ ์ด์ ํ๋ก์ธ์)์ ์ฒ๋ฆฌํ๋ค. (= ์ฑ ์ฑ๋ฅ์ ์ํฅ ์์)
- ์์ฑ๋ ์ฝ๋๋ฅผ ์ง์ ํ์ธํ ์ ์์ด์ ๋งคํ ๋์ค์ ์ค๋ฅ๊ฐ ์๊ฒผ์ ๋ ๋๋ฒ๊น ์ด ์ฝ๋ค.
- ์ฒ๋ฆฌ์๋๊ฐ ๊ต์ฅํ ๋น ๋ฅธํธ์ด๋ผ์, ์ปดํ์ผ ์๊ฐ์๋ ๊ฑฐ์ ์ํฅ์ ๋ผ์น์ง ์๋๋ค.
- ์ด๋ฏธ ๋ง์ ์๋ฐ, ์คํ๋ง ๊ฐ๋ฐ์๋ค์ด ์ฌ์ฉํ๊ณ ์๋ ๊ฒ์ฆ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค.

๐ญ Mapstruct ์ถ๊ฐํ๊ธฐ
https://www.baeldung.com/mapstruct
mapstruct๋ ์ด๋ ธํ ์ด์ ํ๋ก์ธ์๋ฅผ ์ด์ฉํ๋ค. ์๋์ ์์กด์ฑ์ 2๊ฐ ์ถ๊ฐํด์ฃผ๋ฉด ๋๋ค.
implementation 'org.mapstruct:mapstruct:1.4.2.Final' annotationProcessor "org.mapstruct:mapstruct-processor:1.4.2.Final"
๋จ lombok์ ๊ฐ์ด ์ฌ์ฉํ๋ฉด ์ปดํ์ผ ๋์ค ์ถฉ๋ํ ์ ์๋ค. ๊ทธ๋์ ์๋์ ๊ฐ์ด lombok-mapstruct-binding์ ์ถ๊ฐํ๋ค.
* ๊ธฐ๋ณธ ์ฝ๋๋ฅผ ์์ฑํ๋ ๋กฌ๋ณต์ด ๋จผ์ ์คํ๋์ด์ผ ํ๊ธฐ ๋๋ฌธ์ ์์๋ฅผ ๋ง์ถฐ์ฃผ์ด์ผํ๋ค. (1. lombok -> 2. mapstruct)
์ฐธ๊ณ ๋ก ๋กฌ๋ณต ์ต์ ๋ฒ์ (1.18.16 ์ด์)์ ์ฌ์ฉํ๋ค๋ฉด, ๊ทธ๋ฅ ๋ฐ๋ก ์ถ๊ฐํ๋๋ผ๋ ์์์ ๋กฌ๋ณต์ด ๋จผ์ ์คํ๋๋ค.
annotationProcessor( 'org.projectlombok:lombok', // lombok 1.18.16 ์ด์์ ์ด๋ ๊ฒ ์์์ง์ ์ํด๋ ๋๋ค. 'org.projectlombok:lombok-mapstruct-binding' )
Maven์์ ์ถ๊ฐํ๋ ๋ฐฉ๋ฒ
<properties> <org.mapstruct.version>1.4.2.Final</org.mapstruct.version> </properties> <dependencies> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${org.mapstruct.version}</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok-mapstruct-binding</artifactId> <version>0.2.0</version> </dependency> </dependencies> ... <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> <configuration> <source>1.8</source> <target>1.8</target> <!-- mapstruct-์ด๋
ธํ
์ด์
ํ๋ก์ธ์ --> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </path> <!-- ๋ค๋ฅธ ์ด๋
ธํ
์ด์
ํ๋ก์ธ์ --> </annotationProcessorPaths> </configuration> </plugin> <plugins>
๐ญ Mapstruct ์ฌ์ฉํ๊ธฐ
์์ ์๋ฐ์์ ์ฌ์ฉํ๊ณ ์ถ๋ค๋ฉด ์ธํฐํ์ด์ค๋ฅผ ๋ง๋ค๊ณ , @Mapper ์ด๋
ธํ
์ด์
์ ๋ถ์ฌ์ฃผ๋ฉด ๋์ด๋ค.
ํ๋ ์ด๋ฆ์ ๊ธฐ๋ฐ์ผ๋ก ์์ฑ๋๋ฉฐ, ๋ง์ฝ ์ด๋ฆ์ด ๋ค๋ฅด๋ฉด @Mapping(source ="~" , target="~")์ผ๋ก ์ง์ ํด์ฃผ๋ฉด ๋๋ค.
import org.mapstruct.Mapper; @Mapper public interface UserMapper { UserEntity toEntity(UserRequest request); UserDto toDto(UserEntity entity); // ์ด๋ฆ์ด ๋ค๋ฅธ๊ฒฝ์ฐ ์ง์ ์ง์ ํด์ค๋ค. request.nick -> command.nickName @Mapping(source = "nickName", target = "nick") UserCommand toCommand(UserRequest request); // ๋ฐ๊พธ๊ณ ์ถ์๊ฒ ์ฌ๋ฌ ๊ฐ๋ผ๋ฉด ์๋์ ๊ฐ์ด ์ง์ ํด์ฃผ๋ฉด ๋๋ค. @Mappings({ @Mapping(source = "nickName", target = "nick"), @Mapping(source = "userId", target = "id")}) UserCommand toCommand(UserRequest request); }
๋น๋ํ๊ธฐ ์ ๊น์ง ์์ฑ๋ ์ฝ๋๋ฅผ ํ์ธํ ์ ์๋ค. ๊ทธ๋์ ์ธํฐํ์ด์ค๋ฅผ ํตํด ์ฃผ์
๋ฐ์ ์ฌ์ฉํ๋๊ฑธ ๊ถ์ฅํ๋ค.
์๋์ ๊ฐ์ด UserMapper mapper= Mappers.getMapper(UserMapper.class) ๋ก ์ฃผ์ ๋ฐ์์ ์ฌ์ฉํ๋ฉด ๋๋ค.
@Mapper public interface UserMapper { UserMapper INSTANCE = Mappers.getMapper(UserMapper.class); ... } public void main(){ UserMapper mapper = UserMapper.INSTANCE; mapper.toCommand(~) // ์ด๋ ๊ฒ ์ธํฐํ์ด์ค์ ์ฃผ์
๋ฐ์์ ์ฌ์ฉ }
๐ ๋ฆฌํ๋ ์ ์ด ์๋๋ฐ, ์ด๋ป๊ฒ ๋ณํํ๋๊ฑด๊ฐ์?
Mapstruct๋ ์๋์ ๊ฐ์ ๋ฐฉ๋ฒ์ ์ฌ์ฉํ๋ค. ์ํ๋ค๋ฉด ์ค์ ์ ํตํด ํน์ ๋ฐฉ๋ฒ์ ์ฌ์ฉํ์ง ์๋๋ก ๋ง๋ค ์ ์๋ค.
- public ํ๋์ ์ง์ ์ ๊ทผ
- Java Beans ์ด๋ฆ ๊ท์น์ ๋ฐ๋ฅธ Getter, Setter์ ์ด์ฉํ ์ ๊ทผ
- ๊ฐ์ฒด ๋ด์ ์กด์ฌํ๋ Builder (Builder๊ฐ ์ฌ๋ฌ๊ฐ๋ผ์ ๋ชป์ฐพ๊ฒ ๋ค๋ฉด ์ปดํ์ผ ์ค๋ฅ)
- ์์ฑ์
๐ ๋งคํ์ ์คํจํ๋ฉด ์ด๋ป๊ฒ ๋๋์?
๊ธฐ๋ณธ ์ค์ ์ null์ด ๋ค์ด๊ฐ๋๋ก ๋์ด์๋ค. ์ด๋ฅผ ๋ฐฉ์งํ๊ณ ์ถ๋ค๋ฉด ์๋์ ๊ฐ์ด ์ปดํ์ผ ์๋ฌ๊ฐ ๋ฐ์ํ๋๋ก ๋ณ๊ฒฝํด์ฃผ์.
@Mapper( unmappedTargetPolicy = ReportingPolicy.ERROR // ๋งคํ ์คํจ์ ์ปดํ์ผ ์๋ฌ ) public interface OrderDtoMapper { ... }
์ฐธ๊ณ ๋ก ์ด์ ๊ดํ ์ต์ ์ ์๋์ ๊ฐ๋ค. (๋ฌด์ํ๊ธฐ, ์ปดํ์ผ๊ฒฝ๊ณ , ์ปดํ์ผ์๋ฌ)
unmappedSourcePolicy | IGNORE(๊ธฐ๋ณธ), WARN, ERROR | Source์ ๋งคํ ๋ชปํ ํ๋๊ฐ ์์ ๋ |
unmmapedTargetPolicy | IGNORE, WARN(๊ธฐ๋ณธ), ERROR | Target์ ๋งคํ ๋ชปํ ํ๋๊ฐ ์์ ๋ |
typeConversionPolicy | IGNORE(๊ธฐ๋ณธ), WARN, ERROR | ๊ฐ์ด ์ ์ค๋ ์ ์๋ ํ์
๋ณํ์ผ ๋ (ex Long -> Int) |
nullValueMappingStrategy | RETURN_NULL(๊ธฐ๋ณธ), RETURN_DEFAULT | Source ์์ฒด๊ฐ null์ผ ๋ |
nullValuePropertyMappingStrategy | SET_TO_NULL(๊ธฐ๋ณธ), SET_TO_DEFAULT, IGNORE |
Source์ ํ๋ ๊ฐ์ด null์ผ ๋ (๋ฌด์ํ๋ฉด ํด๋น ๊ฐ์ ๋งคํํ์ง ์์) |
RETURN_DEFAULT๋ ํด๋น ์ธํฐํ์ด์ค์ ์๋ default ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ค. ๋ง์ฝ ์ฌ์ฉ๊ฐ๋ฅํ ๋ฉ์๋๊ฐ ์๋ค๋ฉด unmapped ์ ์ฑ ์ ๋ฐ๋ผ null์ ๋ฃ๊ฑฐ๋ ์ปดํ์ผ ์๋ฌ๋ฅผ ๋ฐ์์ํจ๋ค.
์ฐธ๊ณ ๋ก ReportingPolicy.ERROR๋ก ์ค์ ํ๋๋ฐ, ํน์ ๊ฐ์ ๋ฌด์ํ๊ณ null๋ก ๋งคํํ๊ณ ์ถ๋ค๋ฉด ์๋์ ๊ฐ์ด ํ๋ฉด ๋๋ค.
@Mapping(target = "createDate", ignore = true) // createDate๋ ๋งคํํ์ง์๊ณ null์ด ์
๋ ฅ๋๋ค. UserEntity userDTOToEntity(UserDTO userDTO);
๐ @Mapper(...) ์ค์ ๋๋ ค์ฐ๊ธฐ
@MapperConfig๋ฅผ ๋ง๋ค์ด์ ์ฌ์ฉํ ์ ์๋ค.
@MapperConfig(unmappedTargetPolicy = ReportingPolicy.ERROR, uses = GenericMapper.class) public interface MapStructMapperConfig { } // uses ์ค์ ์ ์ด์ฉํ๋ฉด GenericMapper๋ฅผ ๋ง๋ค์ด์ ์์ํ๋ฏ์ด ์ฌ์ฉํ ์ ์๋ค. @Mapper public interface GenericMapper { // objectMapper๋ฅผ ์ด์ฉํ์ฌ ๊ฐ์ฒด->๋ฌธ์์ด๋ก ๋ณํ ObjectMapper OBJECT_MAPPER = new ObjectMapper(); default String toString(Object obj) { ... } }
@Mapper(config = MapStructMapperConfig.class) public interface UserMapper { ... } @Mapper(config = MapStructMapperConfig.class) public interface MyMapper { ... }
๋ค๋ง ์ฌ๋ฌ ์ค์ ์ ์ํ์ฐธ์กฐ ( A -> B -> A )ํ๊ฒ ๋ง๋ค๋ฉด ๋งคํ๋์ค ์ปดํ์ผ ์๋ฌ๊ฐ ๋ฐ์ํ๋, ์ฃผ์ํด์ ์ฌ์ฉํ์.
๐ญ ์คํ๋ง์์ Mapstruct
์คํ๋ง์ ๊ฒฝ์ฐ๋ ๋ ๊ฐ๋จํ๋ค. ์ ๋ ธํ ์ด์ ์ ์์ฑ ๊ฐ์ ์ถ๊ฐํด์ฃผ๋ฉด ์คํ๋ง ๋น์ผ๋ก ์ถ๊ฐ๋๋ค.
์ฐธ๊ณ ๋ก ๋งคํ์ด ์คํจํ ๊ฒฝ์ฐ ์ฒ๋ฆฌ๋ฐฉ๋ฒ, ํ๋ ์ฃผ์ ๋ฐฉ๋ฒ๋ฑ ๋ค๋ฅธ ์ต์ ๋ ์๋์ ๊ฐ์ด ์ง์ ํด์ค ์ ์๋ค.
@Mapper( componentModel = "spring", // Spring Bean ์ผ๋ก ์์ฑ injectionStrategy = InjectionStrategy.CONSTRUCTOR, // ์์ฑ์ ์ฃผ์
unmappedTargetPolicy = ReportingPolicy.ERROR // ๋งคํ ์คํจ์ ์ปดํ์ผ ์๋ฌ ) public interface OrderDtoMapper { ... }
'์คํ๋ง ๋น์ผ๋ก ์ถ๊ฐ๋๋ค'๊ณ ํ๋๋ฐ, ๋ฆฌํ๋ ์ ์ ์ฌ์ฉํ๋ ๊ฑด ๋น์ฐํ ์๋๋ค. ๊ทธ๋ฅ @Component ๋ฅผ ๋ฌ์์ค๋ค.
์ค์ ์์ฑ๋ ์ฝ๋๋ ์๋์ ๊ฐ๋ค.
@Generated( value = "org.mapstruct.ap.MappingProcessor", date = "2022-01-16T08:19:05+0900", comments = "version: 1.4.2.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-7.1.1.jar, environment: Java 16.0.1 (Oracle Corporation)" ) @Component // ๊ทธ๋ฅ @Component ์ด๋
ธํ
์ด์
์ ๋ถ์ฌ์ค๋ค. public class OrderDtoMapperImpl implements OrderDtoMapper {...}
๐ ๊ฐ์ฒด๋ฅผ ๋งคํํ๊ฑฐ๋ ์ ์ถ๋ ฅ์ด ๋ค๋ฅธ ๊ฒฝ์ฐ
๊ฐ์ฒด๋ฅผ ๋งคํํ๊ฑฐ๋, ํ๋๊ฐ ์ฌ๋ฌ ๊ฐ์ผ๋๋ ์ด๋ฆ(userEntity.name)๊น์ง ํจ๊ป ์ ์ด์ฃผ๋ฉด ๋๋ค.
์ ์ฌ์ฉํ์ง ์์ง๋ง expression = "java(..code..)" ๋ก ๋ณํํ ๊ฐ์ ์ฝ๋๋ก ์กฐ์ํ ์๋ ์๋ค. (ํน์ ๋ฉ์๋๋ฅผ ํธ์ถํ๊ฑฐ๋)
@Mapping(source = "userEntity.nickName", target = "nick") @Mapping(source = "address", target = "add") // Address ๊ฐ์ฒด ๋งคํ @Mapping(target = "message", expression = "java(message + \".msg\")") // message + ".msg" UserCommand toCommand(UserEntity userEntity, String message, Address address);
public class UserDTO { private Long id; private String password; private String name; private String nick; private String message; private Address add; } public class Address { private String address; private int addressNum; }
๋ง์ฝ Dto์์ ๊ฐ์ฒด(Record, Account)๊ฐ ์๋ ๊ฒฝ์ฐ ๋ช ์์ ์ผ๋ก ์ง์ ํด์ฃผ์ง ์๋๋ค๋ฉด, ์๋์ผ๋ก ๋ณํ๋์ง ์๋๋ค.
Dto.name -> Entity.name ์ด๋ ๊ฒ ํ๋ํ๋ ์ ์ด์ค์ผํ๋๋ฐ, ์ด๋๋ ์๋์ ๊ฐ์ด target = "." ์ ์ฌ์ฉํ๋ฉด ๋๋ค.
@Mapper public interface CustomerMapper { // recode.name ์ฒ๋ผ ํ๋ํ๋ ์จ์ฃผ์ง ์๋๋ผ๋, "."์ ์ด์ฉํด์ ๊ฐ์ฒด๋ฅผ ํต์ผ๋ก ๋งคํํด์ค ์ ์๋ค. @Mapping( target = ".", source = "record" ) // Dto.record ๊ฐ์ฒด -> Entity.record ๊ฐ์ฒด @Mapping( target = ".", source = "account" ) // Dto.account ๊ฐ์ฒด -> Entity.account ๊ฐ์ฒด @Mapping( target = "id", source = "record.fileId" ) // ์ด๋ฆ์ด ๊ฒน์น๋ ๊ฒฝ์ฐ, ์ด๋ ๊ฒ ํด๊ฒฐํ๋ฉด ๋๋ค. Customer customerDtoToCustomer(CustomerDto customerDto); }
๋ฌผ๋ก ์ด๋ ๊ฒ ์ฌ์ฉํ ์ผ์ ๊ฑฐ์ ์๊ธดํ๋ค.
๐ ์ปฌ๋ ์ ์ธ ๊ฒฝ์ฐ (List, Set)
๊ทธ๋ฅ ์ฐ๋ฉด ๋๋ค. ๋ค๋ง ์ปฌ๋ ์ ์์ ๊ฐ์ฒด๊ฐ ๋ค์ด๊ฐ์๋ค๋ฉด, ์ด ๊ฐ์ฒด๋ฅผ ๋ณํํด์ค ๋ฉ์๋๊ฐ ํ์ํ๋ค.
์๋ ์ฝ๋๋ก ์๋ฅผ ๋ค๋ฉด CarDto method(Car car)ํํ์ ๋ฉ์๋๊ฐ ๋ฐ๋์ ์์ด์ผํ๋ค.
@Mapper public interface CarMapper { Set<String> integerSetToStringSet(Set<Integer> integers); List<CarDto> carsToCarDtos(List<Car> cars); CarDto carToCarDto(Car car); }
// ๋น๋ ํ ์์ฑ๋ ์ฝ๋ @Override public Set<String> integerSetToStringSet(Set<Integer> integers) { if ( integers == null ) { return null; } Set<String> set = new LinkedHashSet<String>(); for ( Integer integer : integers ) { set.add( String.valueOf( integer ) ); } return set; } @Override public List<CarDto> carsToCarDtos(List<Car> cars) { if ( cars == null ) { return null; } List<CarDto> list = new ArrayList<CarDto>(); for ( Car car : cars ) { list.add( carToCarDto( car ) ); } return list; }
๋ ์์ธํ ๋ด์ฉ์ ๊ณต์๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ์. ๊ทผ๋ฐ ๊ทธ๋ ๊ฒ ๊น๊ฒ ๊ณต๋ถํ ํ์๋ ์์ ๋ฏํ๋ค.

๋ธ๋ก๊ทธ์ ์ ๋ณด
JiwonDev
JiwonDev