๊ด€๋ฆฌ ๋ฉ”๋‰ด

Coding Planet

AOP(Aspect-Oriented Programming)๋ž€? ์˜ˆ์‹œ ํฌํ•จ ๋ณธ๋ฌธ

๐ŸŒฑSPRING

AOP(Aspect-Oriented Programming)๋ž€? ์˜ˆ์‹œ ํฌํ•จ

jhj.sharon 2023. 5. 18. 20:13
๋ฐ˜์‘ํ˜•

 

1. AOP(Aspect-Oriented Programming)๋ž€?

 

  AOP(Aspect-Oriented Programming)๋Š” ํ”„๋กœ๊ทธ๋ž˜๋ฐ ํŒจ๋Ÿฌ๋‹ค์ž„ ์ค‘ ํ•˜๋‚˜๋กœ, ๊ด€์‹ฌ์‚ฌ์˜ ๋ถ„๋ฆฌ(Concern Separation)๋ฅผ ์œ„ํ•ด ์‚ฌ์šฉ๋˜๋Š” ๊ธฐ์ˆ ์ด๋‹ค. AOP๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ๊ด€๋ จ ์—†๋Š” ๋ถ€๊ฐ€์ ์ธ ๊ธฐ๋Šฅ๋“ค์„ ๋ชจ๋“ˆํ™”ํ•˜์—ฌ ์ฝ”๋“œ์˜ ์ค‘๋ณต์„ ์ค„์ด๊ณ  ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ค๋Š” ๋ฐ์— ์ฃผ๋กœ ํ™œ์šฉ๋œ๋‹ค. AOP์—์„œ๋Š” ๋‹ค์–‘ํ•œ ๊ด€์ (Aspect)์„ ์ •์˜ํ•˜๊ณ , ์ด๋Ÿฌํ•œ ๊ด€์ ๋“ค์„ ํ•ต์‹ฌ ๋กœ์ง์— ์ ์šฉํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค.

 

   Spring์—์„œ๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ํด๋ž˜์Šค(Service, Dao ๋“ฑ)์—์„œ ์ค‘๋ณต๋˜๋Š” ๊ณตํ†ต ์ฝ”๋“œ ๋ถ€๋ถ„(commit, rollback, log์ฒ˜๋ฆฌ)์„ ๋ณ„๋„์˜ ์˜์—ญ์œผ๋กœ ๋ถ„๋ฆฌํ•ด ๋‚ด๊ณ , ์ฝ”๋“œ๊ฐ€ ์‹œํ–‰ ๋˜๊ธฐ ์ „์ด๋‚˜ ์ด ํ›„์˜ ์‹œ์ ์— ํ•ด๋‹น ์ฝ”๋“œ๋ฅผ ๋ถ™์—ฌ ๋„ฃ์Œ์œผ๋กœ์จ ์†Œ์Šค ์ฝ”๋“œ์˜ ์ค‘๋ณต์„ ์ค„์ด๊ณ , ํ•„์š”ํ•  ๋•Œ ๋งˆ๋งˆ๋‹ค ๊ฐ€์ ธ๋‹ค ์“ธ ์ˆ˜ ์žˆ๊ฒŒ ๊ฐ์ฒดํ™” ํ•˜๋Š” ๊ธฐ์ˆ ์„ ๋งํ•œ๋‹ค.

 

 

 

 

 

 

2. Spring AOP์˜ ํ•ต์‹ฌ ์šฉ์–ด

 

 

* Aspect๋ž€? : Advice + Pointcut = Aspect

์‹ค์ œ๋กœ ๋™์ž‘ ์ฝ”๋“œ๋ฅผ ์˜๋ฏธํ•˜๋Š” Advice์™€
์ž‘์„ฑํ•œ Advice๊ฐ€ ์‹ค์ œ๋กœ ์ ์šฉ๋œ ๋ฉ”์†Œ๋“œ์ธ Pointcut์„ ํ•ฉ์นœ ๊ฐœ๋…์œผ๋กœ
๋ถ€๊ฐ€๊ธฐ๋Šฅ(๋กœ๊น…, ๋ณด์•ˆ, ํŠธ๋žœ์žญ์…˜ ๋“ฑ)์„ ๋‚˜ํƒ€๋‚ด๋Š” ๊ณตํ†ต ๊ด€์‹ฌ์‚ฌ์— ๋Œ€ํ•œ ์ถ”์ƒ์ ์ธ ๋ช…์นญ.
(์—ฌ๋Ÿฌ ๊ฐ์ฒด์— ๊ณตํ†ต์œผ๋กœ ์ ์šฉ๋˜๋Š” ๋ถ€๊ฐ€๊ธฐ๋Šฅ์„ ์ž‘์„ฑํ•œ ํด๋ž˜์Šค ๋‚˜ํƒ€๋ƒ„)

AOP ๊ฐœ๋…์„ ์ ์šฉํ•˜๋ฉด ํ•ต์‹ฌ๊ธฐ๋Šฅ ์ฝ”๋“œ ์‚ฌ์ด์— ๋ผ์–ด์žˆ๋Š” ๋ถ€๊ฐ€๊ธฐ๋Šฅ์„ ๋…๋ฆฝ์ ์ธ ์š”์†Œ๋กœ ๊ตฌ๋ถ„ํ•ด ๋‚ผ ์ˆ˜ ์žˆ๊ณ ,
์ด๋ ‡๊ฒŒ ๊ตฌ๋ถ„๋œ ๋ถ€๊ฐ€๊ธฐ๋Šฅ Aspect๋Š” ๋Ÿฐํƒ€์ž„ ์‹œ์— ํ•„์š”ํ•œ ์œ„์น˜์— ๋™์ ์œผ๋กœ ์ฐธ์—ฌํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 

 

 

3.Spring AOP์˜ ๊ตฌ์กฐ

 

 

 

 

 

 

4. Spring AOP์˜ ๊ตฌํ˜„ ๋ฐฉ์‹

1) XML ๊ธฐ๋ฐ˜์˜ AOP ๋„ค์ž„์ŠคํŽ˜์ด์Šค๋ฅผ ํ†ตํ•œ AOP ๊ตฌํ˜„

  • ๋ถ€๊ฐ€๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” Advice ํด๋ž˜์Šค๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.
  • XML ์„ค์ • ํŒŒ์ผ์— <aop:config>๋ฅผ ์ด์šฉํ•ด์„œ Aspect๋ฅผ ์„ค์ •ํ•œ๋‹ค.

 

2) @Aspect๋ฅผ ์ •์˜ํ•˜๋Š” ํƒœ๊ทธ

  • @Aspect ์–ด๋…ธํ…Œ์ด์…˜์„ ์ด์šฉํ•ด์„œ ๋ถ€๊ฐ€๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” Aspect ํด๋ž˜์Šค๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.
  • ์ด ๋•Œ, Aspect ํด๋ž˜์Šค๋Š” ์–ด๋“œ๋ฐ”์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฉ”์†Œ๋“œ์™€ ํฌ์ธํŠธ์ปท์„ ํฌํ•จํ•œ๋‹ค.
  • dispachter-servlet์œผ๋กœ ์ง€์ •๋œ XML ์„ค์ • ํŒŒ์ผ์— <aop:aspect-autoproxy/>๋ฅผ ์„ค์ •ํ•œ๋‹ค.

servlet-context.xml์— ์„ค์ •๋œ ๋ชจ์Šต

 

 

5. Spring AOP์˜ Advice์™€ annotation

 

 

 

 

6. ์‹ค์ œ ์ ์šฉ ์˜ˆ์‹œ

 

** ์ „์ฒด ๊ตฌ์กฐ

 

1) ํฌ์ธํŠธ์ปท ์„ค์ •ํ•˜๊ธฐ

 

 

 

 

 

2) Before Aspect ์˜ˆ์‹œ 

  • ์•„๋ž˜ ์˜ˆ์‹œ๋Š” JoinPoint ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์›น ํŽ˜์ด์ง€์— ์ ‘์†ํ•œ ์‚ฌ๋žŒ์˜ ip์ฃผ์†Œ๋ฅผ ์ถœ๋ ฅํ•ด์ฃผ๋Š” ๋ฉ”์„œ๋“œ๋ฅผ Before๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ•œ ๊ฒƒ์ด๋‹ค.
  • JointPoint ์ธํ„ฐํŽ˜์ด์Šค๋Š” advice๊ฐ€ ์ ์šฉ๋˜๋Š” ๋™์•ˆ Target Object์˜ ์ •๋ณด, ์ „๋‹ฌ๋˜๋Š” ๋งค๊ฐœ๋ณ€์ˆ˜, ๋ฉ”์„œ๋“œ, ๋ฐ˜ํ™˜๊ฐ’, ์˜ˆ์™ธ ๋“ฑ์„ ์–ป์„ ์ˆ˜ ์žˆ๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•œ๋‹ค.
  • @Before("CommonPointcut.implPointcut()") : ๋ชจ๋“  ServiceImple ํด๋ž˜์Šค์— ์ ‘๊ทผํ•˜๊ธฐ ์ „์— ์‚ฌ์šฉ์ž์˜ log๋กœ ๋‚จ๊ธฐ๋Š” ๋ฉ”์„œ๋“œ ์‹คํ–‰

 

import java.util.Arrays;

import javax.servlet.http.HttpServletRequest;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import edu.kh.comm.member.model.vo.Member;

@Component
@Aspect
public class BeforeAspect {
	
	private Logger logger = LoggerFactory.getLogger(BeforeAspect.class);
	

	@Before("CommonPointcut.implPointcut()")
	public void serviceStart(JoinPoint jp) { //๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜
		
		String str = "----------------------------------------------\n";
		
		
		String className = jp.getTarget().getClass().getSimpleName();
		String methodName = jp.getSignature().getName();
		
		String param = Arrays.toString( jp.getArgs() );
		
		str += "Start : " + className + " - " + methodName + "\n";
		// Start : MemberServiceImpl - login
		
		str += "Parameter : " + param + "\n";
		
		try {
			//HttpServletRequest๋ฅผ ์ด์šฉํ•ด์„œ ip ์–ป์–ด์˜ค๊ธฐ

			
			HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
			
			Member loginMember = (Member) req.getSession().getAttribute("loginMember");
			
			//ip.xxx.xxx.xxx.xxx(email : test01@naver.com)
			str += "ip : " + getRemoteAddr(req);
			
			if(loginMember != null) {// ๋กœ๊ทธ์ธ ์ƒํƒœ์ธ ๊ฒฝ์šฐ
				str += "(email : " + loginMember.getMemberEmail() + ")";
				
			}
			
		}catch(Exception e) {
			str+="[์Šค์ผ€์ค„๋Ÿฌ ๋™์ž‘]";
		}
		logger.info(str);
		
		
	}
	
	//pc  / ๋ธŒ๋ผ์šฐ์ € / os๊ฐ€ ๊ฐ์ž ๋‹ฌ๋ผ๋„
	//ip ์ฃผ์†Œ๋ฅผ ์–ป์–ด์˜ค๋Š” ๋ฉ”์„œ๋“œ
	
	public static String getRemoteAddr(HttpServletRequest request) {

        String ip = null;

        ip = request.getHeader("X-Forwarded-For");

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 
            ip = request.getHeader("Proxy-Client-IP"); 
        } 

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 
            ip = request.getHeader("WL-Proxy-Client-IP"); 
        } 

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 
            ip = request.getHeader("HTTP_CLIENT_IP"); 
        } 

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 
            ip = request.getHeader("HTTP_X_FORWARDED_FOR"); 
        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 
            ip = request.getHeader("X-Real-IP"); 
        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 
            ip = request.getHeader("X-RealIP"); 
        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 
            ip = request.getHeader("REMOTE_ADDR");
        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 
            ip = request.getRemoteAddr(); 
        }

		return ip;
	}

}

 

์ฝ˜์†”์— ์ฐํžŒ ๊ฒฐ๊ณผ

 

 

 

 

 

 

 

3)After Aspect ์˜ˆ์‹œ 

  • @After("CommonPointcut.implPointcut()") : ServeImple์ด ๋๋‚œ๋’ค  ์‹คํ–‰๋œ๋‹ค.
import java.util.Arrays;

@Component
@Aspect
@Order(1)
public class AfterAspect {

	private Logger logger = LoggerFactory.getLogger(AfterAspect.class);
	

	@After("CommonPointcut.implPointcut()")
	public void serviceEnd(JoinPoint jp) {
		
	
		String className = jp.getTarget().getClass().getSimpleName();
	
		String methodName = jp.getSignature().getName();
	
		String str = "End : " + className + " - " + methodName + "\n";
		// Start : MemberServiceImpl - login
		
		
		 str += "----------------------------------------------\n";
		 
		 logger.info(str);
		
	}
}

 

 

 

4) Around Aspect

  • @Arount : ์ „์ฒ˜๋ฆฌ(@Before)์™€ ํ›„์ฒ˜๋ฆฌ(@After)๋ฅผ ํ•œ ๋ฒˆ์— ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋Š” advice์ด๋‹ค.
  • ์•„๋ž˜์˜ ๋ฉ”์„œ๋“œ๋Š” ๋ฉ”์„œ๋“œ ์ฒ˜๋ฆฌ ์†Œ์š” ์‹œ๊ฐ„์„ ์•Œ๋ ค์ฃผ๋Š” ๋ฉ”์„œ๋“œ์ด๋‹ค.
  • ProceedingJointPoint ์ธํ„ฐํŽ˜์ด์Šค : ์ฃผ๋กœ Around ์–ด๋“œ๋ฐ”์ด์Šค์—์„œ ํ™œ์šฉ๋˜๋ฉฐ ํ•ต์‹ฌ๋กœ์ง์˜ ํ˜ธ์ถœ์„ ์ œ์–ดํ•˜๊ณ  ์ถ”๊ฐ€์ ์ธ ์ฒ˜๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š”๋ฐ ์‚ฌ์šฉ๋œ๋‹ค.
  • proceed() :  Around ์–ด๋“œ๋ฐ”์ด์Šค์—์„œ ํ•ต์‹ฌ ๋กœ์ง์˜ ํ˜ธ์ถœ์„ ์ง„ํ–‰ํ•˜๋Š” ๋ฉ”์„œ๋“œ. ์ด ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๋กœ์ง์„ ์‹คํ–‰ํ•˜๊ณ  Around ์–ด๋“œ๋ฐ”์ด์Šค ๋‚ด์—์„œ ์›ํ•˜๋Š” ํƒ€์ด๋ฐ์— ํ˜ธ์ถœ์„ ์ค‘๋‹จํ•˜๊ฑฐ๋‚˜ ์ถ”๊ฐ€์ ์ธ ์ฒ˜๋ฆฌ๋ฅผ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋”ฐ๋ผ์„œ ํ•ต์‹ฌ ๋กœ์ง์„ ์‹คํ–‰ํ•˜๋Š” proceed()์ด์ „์— ์ „์ฒ˜๋ฆฌ ๋‚ด์šฉ์„ ์“ฐ๊ณ  ์ดํ›„์— ํ›„์ฒ˜๋ฆฌ ๋‚ด์šฉ์„ ์“ฐ๋ฉด ๋œ๋‹ค.
  • ์•„๋ž˜์—์„œ๋Š” 1970/01/01 ์˜ค์ „ 9์‹œ(ํ•œ๊ตญ OS ๊ธฐ์ค€) ๋ถ€ํ„ฐ  ์ง€๊ธˆ๊นŒ์ง€ ์ง€๋‚œ ์‹œ๊ฐ„์„ ms๋‹จ์œ„๋กœ ๋‚˜ํƒ€๋‚ธ ๊ฐ’์„ ์ถœ๋ ฅํ•˜๋Š” System.currentTimeMillis() ์„ ์ด์šฉํ•˜์—ฌ ์„œ๋น„์Šค ์‹œ์ž‘์ „(@Before)์™€ ์„œ๋น„์Šค๊ฐ€ ๋๋‚œํ›„ (@After)์˜ ์‹œ๊ฐ„ ์ฐจ์ด๋ฅผ ์–ป์–ด๋‚ด ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์‹œ๊ฐ„์„ ์–ป์–ด๋‚ด๋Š” Aspect๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ๋‹ค.
@Component
@Aspect
@Order(3)
public class AroundAspect {

	private Logger logger = LoggerFactory.getLogger(AroundAspect.class);
	
	// @Around : ์ „์ฒ˜๋ฆฌ(@Before)์™€ ํ›„์ฒ˜๋ฆฌ(@After)๋ฅผ ํ•œ ๋ฒˆ์— ์ž‘์„ฑ ๊ฐ€๋Šฅํ•œ advice
	@Around("CommonPointcut.implPointcut()")
	public Object runningTime(ProceedingJoinPoint jp) throws Throwable {
		//๋ฉ”์„œ๋“œ ์†Œ์š” ์‹œ๊ฐ„์„ ์•Œ๋ ค์ฃผ๋Š” ๋ฉ”์„œ๋“œ
		//ProceedingJoinPoint ์ธํ„ฐํŽ˜์ด์„œ : ์ „/ํ›„ ์ฒ˜๋ฆฌ ๊ธฐ๋Šฅ, ๊ฐ’์„ ์–ป์–ด์˜ฌ ์ˆ˜ ์žˆ๋Š” ๋ฉ”์„œ๋“œ ์ œ๊ณต
		
		// proceed() ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ ์ „  : @Before advice ์ž‘์„ฑ
		// proceed() ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ ํ›„  : @After advice ์ž‘์„ฑ
		// ๋ฉ”์†Œ๋“œ ๋งˆ์ง€๋ง‰์— proceed()์˜ ๋ฐ˜ํ™˜๊ฐ’์„ ๋ฆฌํ„ดํ•ด์•ผํ•จ.
		
		// System.currentTimeMillis() : 
		//	1970/01/01 ์˜ค์ „ 9์‹œ(ํ•œ๊ตญ OS ๊ธฐ์ค€) ๋ถ€ํ„ฐ 
		//  ์ง€๊ธˆ๊นŒ์ง€ ์ง€๋‚œ ์‹œ๊ฐ„์„ ms๋‹จ์œ„๋กœ ๋‚˜ํƒ€๋‚ธ ๊ฐ’
		
		long startMs = System.currentTimeMillis();
		
		Object obj = jp.proceed();// ์ „ํ›„ ์ฒ˜๋ฆฌ๋ฅผ ๋‚˜๋ˆ„๋Š” ๊ธฐ์ค€. ์ด๋ณด๋‹ค ์œ„๊ฐ€ befor, ๋’ค๊ฐ€ after
		
		long endMs = System.currentTimeMillis();
		
		logger.info("Running Time : " + (endMs-startMs) + "ms");
		
		return obj;
		
		
		
	}
}

 

 

 

 

 

5)AfterReturning Aspect ์˜ˆ์‹œ 

  • @After๋Š” ๋ฐ˜ํ™˜๊ฐ’์„ ์–ป์–ด์˜ฌ ์ˆ˜ ์—†๋‹ค.
  • @AfterReturning๋Š” ๊ธฐ์กด @After์— ๋ฐ˜ํ™˜๊ฐ’ ์–ป์–ด์˜ค๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.
  •  returning="returnObj" : ๋ฐ˜ํ™˜ ๊ฐ’์„ ์ €์žฅํ•  ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์ง€์ •
@Component
@Aspect
@Order(5) // ํฐ ์ˆซ์ž ์ˆœ์œผ๋กœ ๋จผ์ € ์‹คํ–‰
public class AfterReturningAspect {
	
	private Logger logger = LoggerFactory.getLogger(AfterReturningAspect.class);
	
	@AfterReturning(pointcut = "CommonPointcut.implPointcut()", returning="returnObj")
	public void serviceReutrnValue(Object returnObj ) {
		
		
		logger.info("return Value: " + returnObj);
	}
}

AfterReturning์˜ Advice๊ฐ€ ์‹คํ–‰๋˜์–ด @After์˜ "End: ..." ์œ„์— return Value๊ฐ€ ์ถœ๋ ฅ๋˜์—ˆ๋‹ค.

 

 

 

 

 

6)AfterThrowing Aspect ์˜ˆ์‹œ 

 

  • @AfterThrowing : ๊ธฐ์กด @After + Throwing๋œ ์˜ˆ์™ธ ์–ป์–ด์˜ค๊ธฐ
  • throwing="exceptionObj" : ๋˜์ €์ง„ ์˜ˆ์™ธ๋ฅผ ์ €์žฅํ•  ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์ง€์ •

 

@Component
@Aspect
public class AfterThrowingAspect {
	
	private Logger logger = LoggerFactory.getLogger(AfterThrowingAspect.class);
	

	@AfterThrowing(pointcut = "CommonPointcut.implPointcut()", throwing="exceptionObj")
	public void serviceReutrnValue(Exception exceptionObj ) {
		
		String str = "<<exception>> : " + exceptionObj.getStackTrace()[0];
		
		str += " - " + exceptionObj.getMessage();
		
		logger.error(str);
		
	}
}

 

๊ฒŒ์‹œ๊ธ€์„ ์ƒ์„ธ์กฐํšŒํ•˜๋Š” sql์— ์„ธ๋ฏธ์ฝœ๋ก (;)์„ ์ถ”๊ฐ€ํ•ด์„œ ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œ์ผฐ๋‹ค.
์ฝ˜์†”์— ์ฐํžŒ ์˜ˆ์™ธ

 

๋ฐ˜์‘ํ˜•
Comments