๐ŸŒผ Spring ์‹ฌํ™” โ‘  HttpMessageConverter, Converter, Formatter / TIL - day 33

ํ•˜๋ฆฌ๋น„ยท2025๋…„ 4์›” 17์ผ
1

๐ŸŒผ Spring

๋ชฉ๋ก ๋ณด๊ธฐ
6/11
post-thumbnail

๐Ÿ“ฆ HttpMessageConverter


๊ธฐ์กด ๊ตฌ์กฐ์˜ view resolver ๊ฐ€ ํ™”๋ฉด์„ ๋ณด์—ฌ์ฃผ๋Š” ๋Œ€์‹ , http ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด์—ฌ์คŒ


๐Ÿ“– HttpMessageConverter์˜ ๊ตฌ์กฐ

  • ์š”์ฒญ์‹œ โ‡ข ArgumentResolver ์‚ฌ์šฉ
    : @RequestBody, HttpEntity<>, RequestEntity<>

  • ์‘๋‹ต์‹œ โ‡ข ReturnValueHandler ์‚ฌ์šฉ
    : @ResponseBody, HttpEntity<>, ResponseEntity<>

    ๐Ÿ’ก ๊ตฌ์กฐ์ƒ ์–‘๋ฐฉํ–ฅ์œผ๋กœ (์š”์ฒญ, ์‘๋‹ต) ์ „๋ถ€ ์ž‘๋™ํ•œ๋‹ค


๐Ÿ“Œ ์šฐ์„ ์ˆœ์œ„

โ–ถ๏ธ Byte โ‡ข String โ‡ข JSON

  • ์š”์ฒญ์ด๋‚˜ ์‘๋‹ต์„ ๋ณ€ํ™˜ํ• ๋•Œ ์–ด๋–ค Media Type์ธ์ง€ ํ™•์ธ
  • ์ˆœ์„œ๋Œ€๋กœ ๊ฒ€์‚ฌํ•ด์„œ ๊ฐ€๋Šฅํ•œ ๊ฒŒ ์žˆ์œผ๋ฉด ๊ทธ๊ฑธ๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ  ๋‚˜๋จธ์ง€๋Š” ๋ฌด์‹œํ•จ

๐Ÿ“Œ ๋™์ž‘ ํ๋ฆ„

์š”์ฒญ

  • 1๏ธโƒฃ RequestBody๋‚˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ”์ธ๋”ฉ
  • 2๏ธโƒฃ canRead()๋กœ ๋ฐ์ดํ„ฐ ํƒ€์ž…์„ ์กฐํšŒ, Content-Type ํ—ค๋”์˜ ๋ฏธ๋””์–ดํƒ€์ž… ์ง€์›์—ฌ๋ถ€ ํ™•์ธ
  • 3๏ธโƒฃ read()๋กœ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜

์‘๋‹ต

  • 1๏ธโƒฃ ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ @ResponseBody๋‚˜ HttpEntity<> ๋ฆฌํ„ดํ•จ
  • 2๏ธโƒฃ canWrite()๋กœ ๋ฐ์ดํ„ฐ ํƒ€์ž…์„ ์กฐํšŒ, Accept ํ—ค๋”์˜ ๋ฏธ๋””์–ดํƒ€์ž… ์ง€์›์—ฌ๋ถ€ ํ™•์ธ
  • 3๏ธโƒฃ write() ๋กœ ์‘๋‹ต๋ฉ”์„ธ์ง€ ๋ฐ”๋””์— ์ง์ ‘ ๋ฐ์ดํ„ฐ ์ž…๋ ฅ

๐Ÿ“ฅ ArgumentResolver (์š”์ฒญ)

๐ŸŒŸ ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ž๋™์œผ๋กœ ๋ฐ”์ธ๋”ฉํ•ด์ฃผ๋Š” ์ธํ„ฐํŽ˜์ด์Šค

  • Spring MVC ๊ตฌ์กฐ์—์„œ ์š”์ฒญ์ด ์ปจํŠธ๋กค๋Ÿฌ๋กœ ๊ฐ€๊ธฐ ์ „ RequestMappingHandlerAdapter๊ฐ€ ํ˜ธ์ถœํ•จ
  • Controller๊ฐ€ ํ•„์š”ํ•œ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ’์„ ์ƒ์„ฑํ•จ
  • ๊ฐ’์ด ์ค€๋น„๋˜๋ฉด ์‹ค์ œ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค

๐Ÿ“ ์‹ค์ œ ํด๋ž˜์Šค ์ด๋ฆ„ : HandlerMethodArgumentResolver

โœ”๏ธ ArgumentResolver์˜ ์ข…๋ฅ˜


๐Ÿ“ค ReturnValueHandler (์‘๋‹ต)

๐ŸŒŸ ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฐ’์„ HTTP ์‘๋‹ต์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค

  • ModelAndView, @ResponseBody, HttpEntity<> ๋“ฑ
  • ์˜ˆ๋ฅผ ๋“ค์–ด, ์ปจํŠธ๋กค๋Ÿฌ์—์„œ String ๋ฆฌํ„ดํ•ด๋„ ๋ฌธ์ž ๊ทธ๋Œ€๋กœ๊ฐ€ ์•„๋‹ˆ๋ผ View ์ด๋ฆ„์ž„์„ ์ธ์‹ํ•จ

๐Ÿ“ ์‹ค์ œ ํด๋ž˜์Šค ์ด๋ฆ„ : HandlerMethodReturnValueHandler

โœ”๏ธ ReturnValueHandler์˜ ์ข…๋ฅ˜

  • ์ธํ„ฐํŽ˜์ด์Šค์ด๋ฏ€๋กœ ํ™•์žฅ๊ฐ€๋Šฅ, ๊ตฌํ˜„์ฒด ๋งŽ์Œ

๐Ÿ’ก ์š”์ฒญ๊ณผ ์‘๋‹ต ๋ชจ๋‘์—์„œ ์‚ฌ์šฉ๋˜๋Š” ํด๋ž˜์Šค

โ–ถ๏ธArgumentResolver + ReturnValueHandler๋ฅผ ๋‘˜๋‹ค ๊ตฌํ˜„ํ•œ ํด๋ž˜์Šค

ํด๋ž˜์Šค๋ช…์ฒ˜๋ฆฌ ๋Œ€์ƒ
RequestResponseBodyMethodProcessor@RequestBody, @ResponseBody
HttpEntityMethodProcessorHttpEntity<>, RequestEntity<>, ResponseEntity<>

โ†’ ๋‚ด๋ถ€์—์„œ HttpMessageConverter๋ฅผ ํ˜ธ์ถœํ•ด ๊ฐ์ฒด ์ƒ์„ฑ ๋˜๋Š” ์‘๋‹ต ๋ณ€ํ™˜์„ ์ฒ˜๋ฆฌํ•จ



๐Ÿ“ฆ Converter, Fomatter


โš™๏ธ WebMvcConfigurer

์„ค์ •์„ ํ†ตํ•ด Converter, Formatter๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค

  • ์ปค์Šคํ…€ Converter, Formatter, HttpMessageConverter ๋“ฑ์„ ๋“ฑ๋กํ•  ๋•Œ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•ด์„œ ์‚ฌ์šฉ
  • ์†์— @Configuration ๋„ ์žˆ์Œ โ‡ข @Component๋ฅผ ํฌํ•จ โ‡ข Spring Bean์œผ๋กœ ๋“ฑ๋ก

๐Ÿ“– Converter

๊ฐ์ฒด์˜ ํƒ€์ž…์„ ์„œ๋กœ ๋ณ€ํ™˜ํ• ๋•Œ

  • HttpServletRequest
    ๊ธฐ๋ณธ์ ์œผ๋กœ ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ๋ฌธ์ž์—ด๋กœ ์ฒ˜๋ฆฌ๊ฐ€ ๋˜๊ธฐ ๋•Œ๋ฌธ์—
    ์ˆซ์ž๋กœ ๋ณ€ํ™˜ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, ํ•˜๋‚˜์”ฉ ์ง์ ‘๋ณ€ํ™˜ํ•ด์ค˜์•ผํ•œ๋‹ค

    @Slf4j
     @RestController
     public class TypeConverterController {
    
         @GetMapping("/param")
         public void param(HttpServletRequest request) {
    
             // ์กฐํšŒ์‹œ : String
             String stringExample = request.getParameter("example");
     
             // โœ…Integer๋กœ Type ๋ณ€ํ™˜
             Integer integerExample = Integer.valueOf(stringExample);
             log.info("integerExample = {}", integerExample);
    
         }
       }
  • @RequestParam
    ๋ณ€ํ™˜ ์•ˆ๊ฑฐ์ณ๋„ ์•Œ์•„์„œ ๋‚˜์˜จ๋‹ค~~ (๋ฐ”์ธ๋”ฉ๋  ํƒ€์ž…์„ ์ •ํ•ด๋†จ์Œ)
    ์š”๊ฑธ ๋ˆ„๊ตฐ๊ฐ€ ์•Œ์•„์„œ ํƒ€์ž…์„ ์ž๋™์œผ๋กœ ๋ณ€ํ™˜ํ•ด์ค€๋‹ค! ์ด๊ฒŒ ํƒ€์ž… ์ปจ๋ฒ„ํ„ฐ??
    @GetMapping("/v2/param")
    public void paramV2(@RequestParam Integer example) {
    		// Integer ํƒ€์ž…์œผ๋กœ ๋ฐ”์ธ๋”ฉ
       log.info("example = {}", example);
    }

๐Ÿ“Œ Converter Interface

ํŠน์ •ํƒ€์ž…์„ ๋‹ค๋ฅธ ํƒ€์ž…์œผ๋กœ ๋ณ€ํ™˜ํ• ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค

  • ์ƒˆ๋กœ์šด ํƒ€์ž…์˜ ๋ณ€ํ™˜
    • localhost:8080/type-converter?person=wonuk:120
    • ์ž…๋ ฅ๋ฐ›์€ ๋ฌธ์ž์—ด ๋ฐ์ดํ„ฐ๋ฅผ Person ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜
    public class Person {
    		private String name;
    		private String age;
    }
    • wonuk:120 โ†’ TypeConverter โ†’ name: wonuk , age : 10 (๊ฐœ์›”์ˆ˜๋ฅผ ๋‚˜์ด๋กœ)
    • ์ด๋Ÿด๋•Œ ์ปจ๋ฒ„ํ„ฐ ์ธํ„ฐํŽ˜์ด์Šค ์‚ฌ์šฉ!
  • ์Šคํ”„๋ง์ด ์ œ๊ณตํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ด๋ฏ€๋กœ implementsํ•ด์„œ ์ปจ๋ฒ„ํ„ฐ๋กœ ๋“ฑ๋กํ•˜๋ฉด ๋œ๋‹ค
  • ๋ณ€ํ™˜ํ•˜๊ณ ์ž ํ•˜๋Š” ํƒ€์ž…์— ๋งž์ถฐ์„œ Type Converter๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  ๋“ฑ๋กํ•˜๋ฉด ๋œ๋‹ค

๐Ÿ“Œ Converter ์‚ฌ์šฉํ•˜๊ธฐ

โš ๏ธ ์ฃผ์˜ : ๊ฐ™์€ ์ด๋ฆ„์„ ๊ฐ€์ง„ interface๊ฐ€ ๋งŽ์œผ๋‹ˆ ์ฃผ์˜
org.springframework.core.convert.converter

  • 1๏ธโƒฃ Converter implements ํ•˜๊ธฐ
  • 2๏ธโƒฃ Converter๋ฅผ ์—ด๊ณ 
    • S : ๋ณ€ํ™˜ํ•  ์†Œ์Šค
    • T : ๋ณ€ํ™˜ํ•  ํƒ€์ž…

โœ”๏ธ ์˜ˆ์‹œ


โถ String โ†’ Integer

@Slf4j
public class StringToIntegerConverter implements Converter<String, Integer> {
	
	@Override
	public Integer convert(String source) {
		log.info("source = {}", source);
		// ๊ฒ€์ฆ
		return Integer.valueOf(source);
	}
	
 }

โท Integer โ†’ String

@Slf4j
public class IntegerToStringConverter implements Converter<Integer, String> {
	
	@Override
	public String convert(Integer source) {
		log.info("source = {}", source);
		return String.valueOf(source);
	}
}

โธ String โ†’ Person

(์š”์ฒญ์˜ˆ์‹œ : localhost:8080/type-converter?person=wonuk:1200)

@Getter
public class Person {
	
		// ์ด๋ฆ„
		private String name;
		// ๋‚˜์ด
		private int age;
	
		public Person(String name, int age) {
			this.name = name;
			this.age = age;
		}
	
}

------------------

public class StringToPersonConverter implements Converter<String, Person> {
		// source = "wonuk:1200"
		@Override
		public Person convert(String source) {
			// ':' ๋ฅผ ๊ตฌ๋ถ„์ž๋กœ ๋‚˜๋ˆ„์–ด ๋ฐฐ์—ด๋กœ ๋งŒ๋“ ๋‹ค.
			String[] parts = source.split(":");
	
			// ์ฒซ๋ฒˆ์งธ ๋ฐฐ์—ด์€ ์ด๋ฆ„์ด๋‹ค. -> wonuk
	    String name = parts[0];
	    // ๋‘๋ฒˆ์งธ ๋ฐฐ์—ด์€ ๊ฐœ์›”์ˆ˜์ด๋‹ค. -> 1200
	    int months = Integer.parseInt(parts[1]);
	    
			// ๊ฐœ์›”์ˆ˜ ๋‚˜๋ˆ„๊ธฐ 12๋กœ ๋‚˜์ด๋ฅผ ๊ตฌํ•˜๋Š” ๋กœ์ง (12๊ฐœ์›” ๋‹จ์œ„๋งŒ ๊ณ ๋ ค)
			int age = months / 12;
	
			return new Person(name, age);
		}
}

------------------

public class PersonToStringConverter implements Converter<Person, String> {
		
		@Override
		public String convert(Person source) {
				// ์ด๋ฆ„
				String name = source.getName();
				// ๊ฐœ์›”์ˆ˜
				int months = source.getAge * 12;
				// "wonuk:1200"
				return name + ":" + months;
		}
	
}
------------------

PersonToStringConverter converter = new PersonToStringConverter();
String source = "wonuk:1200";
converter.convert(source);

๐Ÿ“Œ Spring์ด ์ œ๊ณตํ•˜๋Š” Converter

  1. Converter

    • ๊ธฐ๋ณธ์ ์ธ ๋ณ€ํ™˜์„ ๋‹ด๋‹นํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค
    • ๋‹จ์ผ ํƒ€์ž…์—์„œ ๋‹จ์ผ ํƒ€์ž…์œผ๋กœ ๋ณ€ํ™˜ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.
      • Converter<Source, Type>
  2. ConverterFactory

  3. GenericConverter

  4. ConditionalGenericConverter

1๋ฒˆ๋งŒ ์™ธ์šฐ๊ณ , ๋‚˜๋จธ์ง€๋Š” ํ•„์š”ํ• ๋•Œ ๊ณต์‹๋ฌธ์„œ์—์„œ ์ฐพ๋˜์ง€~


๐Ÿ”„ ConversionService

์ปจ๋ฒ„ํ„ฐ๋ฅผ ๋ชจ์•„์„œ ํŽธ๋ฆฌํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๊ฒŒ ํ•ด์ค€๋‹ค

  • ์ธํ„ฐํŽ˜์ด์Šค์ธ๋ฐ ์ปจ๋ฒ„ํ„ฐ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€๋„ ํ™•์ธํ•ด์ฃผ๊ณ , ์‚ฌ์šฉ๋„ ํ•ด์ค€๋‹ค
  • ์šฐ์„  ์ˆœ์œ„ = ๋‚ด๊ฐ€ ์ปค์Šคํ…€ํ•ด ๊ตฌํ˜„ํ•œ ์ปจ๋ฒ„ํ„ฐ >>>> ๊ธฐ๋ณธ์ œ๊ณต ์ปจ๋ฒ„ํ„ฐ

๐Ÿ“Œ DefalutConversionService

ConversionService๋ฅผ ๊ตฌํ˜„ํ•œ ๊ตฌํ˜„์ฒด, ConvertRegistry์— ๋‹ค์–‘ํ•œ Converter๋ฅผ ๋“ฑ๋ก

๐Ÿ“Œ ConverterRegistry

Converter๋ฅผ ๋“ฑ๋กํ•˜๊ณ  ๊ด€๋ฆฌ

โœ”๏ธ ์˜ˆ์‹œ


import static org.assertj.core.api.Assertions.*;// โœ… Assertions static ์ž„ํฌํŠธ ํ•„์š”

public class ConversionServiceTest {

    @Test
    void defaultConversionService() {
        // given
        DefaultConversionService dcs = new DefaultConversionService();
        dcs.addConverter(new StringToPersonConverter());
        Person wonuk = new Person("wonuk", 100);
        
		// when
        Person stringToPerson = dcs.convert("wonuk:1200", Person.class);
        
		// then 
        assertThat(stringToPerson.getName()).isEqualTo(wonuk.getName());
        assertThat(stringToPerson.getAge()).isEqualTo(wonuk.getAge());
    }

}

์œ„ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด ์•Œ์ˆ˜์žˆ๋Š” ๊ฒƒ โ‡ข> ์ปจ๋ฒ„ํ„ฐ ๋“ฑ๋ก / ์‚ฌ์šฉ์€ ๋ถ„๋ฆฌ ! (ISP ์›์น™ ์ ์šฉ)

๐Ÿ“Œ ์‚ฌ์šฉํ•ด๋ณด๊ธฐ

  • ์ด๋ฏธ ๋‚ด๋ถ€์ ์œผ๋กœ ConversionService ๊ฐ€ ์žˆ์œผ๋‹ˆ๊นŒ

1๏ธโƒฃ WebMvcConfigurer๋ฅผ ๊ตฌํ˜„

2๏ธโƒฃ addFomatters()๋กœ ์ปจ๋ฒ„ํ„ฐ๋ฅผ ๋“ฑ๋ก๋งŒํ•˜๊ณ  ์‚ฌ์šฉ

โ–ถ๏ธ ๊ฒฐ๊ณผ



๐Ÿ“– Formatter

๊ฐ์ฒด๋ฅผ ํŠน์ •ํ•œ ํฌ๋งท์— ๋งž์ถฐ์„œ ๋ฌธ์ž๋กœ ์ถœ๋ ฅํ•˜๋Š”๋ฐ ํŠนํ™” (Converter๋ณด๋‹ค ๋” ์„ธ๋ถ€๊ธฐ๋Šฅ )

  • printer, parser ์ƒ์† ( ๊ฐ์ฒด๋ฅผ ๋ฌธ์ž๋กœ ๋ณ€ํ™˜ / ๋ฌธ์ž๋ฅผ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ )

Locale

  • ์ง€์—ญ / ์–ธ์–ด ์ •๋ณด๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ์ฒด
    ์–ธ์–ด์ฝ”๋“œ en, ko / ๊ตญ๊ฐ€์ฝ”๋“œ US, KR

๐Ÿ“Œ ์‚ฌ์šฉํ•˜๊ธฐ


1๏ธโƒฃ Formatter ๋งŒ๋“ค๊ธฐ

@Slf4j
public class PriceFormatter implements Formatter<Number> {
	
	@Override
  public Number parse(String text, Locale locale) throws ParseException {
    log.info("text = {}, locale={}", text, locale);
		
		// ๋ณ€ํ™˜ ๋กœ์ง
		// NumberFormat์ด ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ
		NumberFormat format = NumberFormat.getInstance(locale);
		// "10,000" -> 10000L
		return format.parse(text);
  }

  @Override
  public String print(Number object, Locale locale) {
			log.info("object = {}, locale = {}", object, locale);
			// 10000L -> "10,000"
      return NumberFormat.getInstance(locale).format(object);
  }
	
}

2๏ธโƒฃ ๋ณ€ํ™˜ํ•˜๊ธฐ

class PriceFormatterTest {

		PriceFormatter formatter = new PriceFormatter();

    @Test
    void parse() throws ParseException {
        // given, when
        Number result = formatter.parse("1,000", Locale.KOREA);

        // then
        // parse ๊ฒฐ๊ณผ๋Š” Long
        Assertions.assertThat(result).isEqualTo(1000L);
    }

    @Test
    void print() {
        // given, when
        String result = formatter.print(1000, Locale.KOREA);

        // then
        Assertions.assertThat(result).isEqualTo("1,000");
    }
}

๐Ÿ“Œ Spring์ด ์ œ๊ณตํ•˜๋Š” Formatter

โœ”๏ธ FormattingConversionService

ConversionService + Formatter ๊ฒฐํ•ฉ ๊ตฌํ˜„์ฒด

์–ด๋Œ‘ํ„ฐ ํŒจํ„ด์œผ๋กœ ํฌ๋งคํ„ฐ๊ฐ€ ์ปจ๋ฒ„ํ„ฐ์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜๋„๋ก ๋งŒ๋“ค์–ด์ค€๋‹ค

โœ”๏ธ DefaultFormatteingConversionService

FormattingConversionService + ํ†ตํ™”, ์ˆซ์ž๊ด€๋ จ Formatter๋ฅผ ์ถ”๊ฐ€ํ•œ ๊ฒƒ

public class FormattingConversionServiceTest {

    @Test
    void formattingConversionService() {

        // given
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();

        // Converter ๋“ฑ๋ก
        conversionService.addConverter(new StringToPersonConverter());
        conversionService.addConverter(new PersonToStringConverter());
        // Formatter ๋“ฑ๋ก
        conversionService.addFormatter(new PriceFormatter());

        // when
        String result = conversionService.convert(10000, String.class);

        // then
        Assertions.assertThat(result).isEqualTo("10,000");

    }

}

โœ”๏ธ Annotation (DTO)

@NumberFormat - ์ˆซ์ž๊ด€๋ จ ์ง€์ • Formatter ์‚ฌ์šฉ
@DateTimeFormat - ๋‚ ์งœ๊ด€๋ จ ์ง€์ • Formatter ์‚ฌ์šฉ ๋“ฑ๋“ฑ,,,

๐Ÿ’ก์™ธ์šธ ํ•„์š” ์—†์Œ ๊ณต์‹๋ฌธ์„œ์—์„œ ์ฐพ์œผ์…ˆ

1๊ฐœ์˜ ๋Œ“๊ธ€

comment-user-thumbnail
2025๋…„ 4์›” 17์ผ

๊ท€์—ฌ์šด ์ฝ”์ฝ”๋ณผ๋“ค..

๋‹ต๊ธ€ ๋‹ฌ๊ธฐ