๐Ÿ“งย ์ด๋ฉ”์ผ ์ฃผ์ธ ๋ˆ„๊ตฌ ? - Two-Factor ํ™•์ธ์‚ฌ์‚ด ์—ฌ์ •๊ธฐ

์œ ๊ฑด์šฐยท2025๋…„ 1์›” 31์ผ

ํ”„๋กœ์ ํŠธ

๋ชฉ๋ก ๋ณด๊ธฐ
9/9

โ“๊ฐœ์š”

Spring Security ๋ฅผ ์ด์šฉํ•˜์—ฌ ์ธ์ฆ ๋ฐ ์ธ๊ฐ€๋ฅผ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ๋†“์น˜๊ณ  ์žˆ๋˜์ ์ด ์žˆ์—ˆ๋‹ค. ํšŒ์›๊ฐ€์ž…์„ ์ˆ˜ํ–‰ํ•œ ํ›„ ํšŒ์›์ด ์ž…๋ ฅํ•œ ์ •๋ณด๊ฐ€ ๋งž๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ๋กœ์ง์„ ๊ตฌํ˜„ํ•œ์ ์ด ์—†์—ˆ๋‹ค. ๋‹ค๋ฅธ ๋งŽ์€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ด์šฉํ•˜๋ฉด ํšŒ์›๊ฐ€์ž… ํ›„ ์ด๋ฉ”์ผ ์ธ์ฆ์„ ์ˆ˜ํ–‰ํ•˜๋ผ๊ณ  ํ•˜๊ฑฐ๋‚˜ ํŽ˜์ด์Šค ์•„์ด๋””๋ฅผ ํ†ตํ•ด ํ•œ๋ฒˆ ๋” ์ธ์ฆ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค. ์ด๊ฒƒ์„ 2FA(Two-Factor-Authentication) ์ด๋ผ๊ณ  ํ•œ๋‹ค. ์ด๋ถ€๋ถ„์„ ํšŒ์›๊ฐ€์ž…์‹œ ์ ์šฉํ•ด ๋ณด๋ ค๊ณ  ํ•œ๋‹ค.







๐Ÿ’ก 2FA ์— ๋Œ€ํ•ด

์ฝ”๋“œ๋กœ ๊ตฌํ˜„ํ•˜๊ธฐ ์ „์— 2FA ์— ๋Œ€ํ•ด ๊ฐœ๋… ๋จผ์ € ์งš๊ณ  ๋„˜์–ด๊ฐ€๊ฒ ๋‹ค. 2FA ๋Š” ์ธ์ฆ ์ˆ˜ํ–‰์‹œ 2๊ฐ€์ง€ ๋ฐฉ๋ฒ•์œผ๋กœ ์ธ์ฆ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์„ ๋งํ•œ๋‹ค. 2๊ฐ€์ง€ ๋ฐฉ์‹์œผ๋กœ ์ธ์ฆ์„ ์ˆ˜ํ–‰ํ•˜์—ฌ ๋ณด์•ˆ์„ ๊ฐ•ํ™”ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ MFA(๋‹ค์ค‘์ธ์ฆ๋ฐฉ์‹)์ด๋ผ๊ณ ๋„ ๋ถˆ๋ฆฐ๋‹ค.

๋Œ€ํ‘œ์ ์ธ ์ž‘๋™๋ฐฉ์‹

  1. ์‚ฌ์šฉ์ž๊ฐ€ ์•„์ด๋””์™€ ํŒจ์Šค์›Œ๋“œ ์ž…๋ ฅ
  2. ์ฒซ๋ฒˆ์งธ ์ธ์ฆ์ด ์„ฑ๊ณตํ•˜๋ฉด ๋‘๋ฒˆ์งธ ์ธ์ฆ ์ง„ํ–‰
  • ์ธ์ฆ ์•ฑ์„ ํ†ตํ•œ ์ผํšŒ์šฉ ์ฝ”๋“œ(TOTP)
  • SMS๋กœ ์ „์†ก๋œ ์ธ์ฆ ์ฝ”๋“œ
  • Email๋กœ ์ „์†ก๋œ ์ธ์ฆ ์ฝ”๋“œ
  • ๋ณด์•ˆํ‚ค๋ฅผ ํ†ตํ•œ ๋ฌผ๋ฆฌ ์ธ์ฆ
  • ์ƒ์ฒด ์ธ์‹

์ด๋Ÿฌํ•œ ๋‹ค์ค‘ ์ธ์ฆ ๋ฐฉ์‹์€ ๋‹จ์ผ ์ธ์ฆ๋ฐฉ์‹์— ๋น„ํ•ด ํ•ด์ปค๊ฐ€ ๋‘๊ฐ€์ง€ ์ธ์ฆ ์ •๋ณด๋ฅผ ํ›”์ณ์•ผ ํ•˜๊ณ , ๋‘๋ฒˆ์งธ ์š”์†Œ๋Š” ์ง€๋ฌธ์ด๋‚˜ ์‹œ๊ฐ„ ์ œํ•œ ์š”์†Œ๋•Œ๋ฌธ์— ํ•ดํ‚นํ•˜๊ธฐ์–ด๋ ต๊ธฐ์— ๋ณด์•ˆ์„ฑ์ด ๋” ๋›ฐ์–ด๋‚˜๊ณ  ๋ฌด์ฐจ๋ณ„ ๋Œ€์ž…๊ณต๊ฒฉ๊ณผ ํ”ผ์‹ฑ ๊ณต๊ฒฉ์— ๋Œ€ํ•œ ๋ณด์•ˆ์„ ๊ฐ•ํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด ๋‚ด๊ฐ€ ๊ตฌํ˜„์„ ํ•  ๋ถ€๋ถ„์€ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์€ ์ด๋ฉ”์ผ ๋ฐฉ์‹์œผ๋กœ 2FA ๋ฅผ ๊ตฌํ˜„ํ•ด๋ณผ ์ƒ๊ฐ์ด๋‹ค. ํšŒ์›๊ฐ€์ž… ์‹œ์— ์ ์šฉ์‹œํ‚ฌ ๊ฒƒ์ด๋‹ค. ์—ฌ๋Ÿฌ ๋‹ค๋ฅธ ์ธ์ฆ ๋ฐ ์ธ๊ฐ€๋ฅผ ๊ณ ๋ คํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ํ”„๋ก ํŠธ๊ฐ€์—†๊ณ  ํ…Œ์ŠคํŠธํ•ด๋ณผ๋งŒํ•œ ๋””๋ฐ”์ด์Šค๊ฐ€ ์—†๊ธฐ์— ํšŒ์›๊ฐ€์ž…์‹œ ์ด๋ฉ”์ผ ์ธ์ฆ์œผ๋กœ๋ผ๋„ ๊ตฌํ˜„ํ•˜๋ ค๊ณ ํ•œ๋‹ค.







๐Ÿง‘โ€๐Ÿ’ป ์ฝ”๋“œ๋กœ ๊ตฌํ˜„ํ•˜๊ธฐ

MailService

@Slf4j
@Service
@RequiredArgsConstructor
public class MailService {

    private final JavaMailSender javaMailSender;
    private final SpringTemplateEngine templateEngine;
    private final EncryptionUtil encryptionUtil;
    private static final String MAIL_SUBJECT = "์ธ์ฆ ์ฝ”๋“œ ์•ˆ๋‚ด";

    @Value("${spring.mail.username}")
    String host;

    public String sendMail(String to) throws MessagingException {
        MimeMessage message = javaMailSender.createMimeMessage();
        MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(message, true, StandardCharsets.UTF_8.name());

        Context context = new Context();
        String code = generateVerificationCode(); // ์ธ์ฆ ์ฝ”๋“œ ์ƒ์„ฑ 
        context.setVariable("code", code); // Email ์ธ์ฆ Form ์— ๋„ฃ์„ Code 
        context.setVariable("email", encryptionUtil.encrypt(to)); // Email ์ธ์ฆ ํผ์— ๋“ค์–ด๊ฐˆ ์•”ํ˜ธํ™”๋œ Email 

        String htmlContent = templateEngine.process("email/verification", context); // Email ์ธ์ฆํผ String ๋ณ€ํ™˜ 

        mimeMessageHelper.setFrom(host); 
        mimeMessageHelper.setTo(to);
        mimeMessageHelper.setSubject(MAIL_SUBJECT);
        mimeMessageHelper.setText(htmlContent, true);

        javaMailSender.send(message); 

        return code;
    }

    private String generateVerificationCode() {
        Random random = new Random();
        StringBuilder code = new StringBuilder();
        IntStream.range(0, 6).forEach(e -> code.append(random.nextInt(10)));
        return code.toString();
    }
  • ์ด๋ฉ”์ผ์„ ์ „๋‹ฌํ•˜๋Š” ์—ญํ• ์„ ๊ฐ€์ง„ ๊ฐ์ฒด์ด๋‹ค.
  • ์ด๋ฉ”์ผ ์ „์†ก ์‹œ ์ธ์ฆ ์ฝ”๋“œ์™€ Email ์„ ๋‹ด์•„๋ณด๋‚ธ๋‹ค.
  • Email ์€ hidden ํ•„๋“œ์— ๋ฐ”์ธ๋”ฉ ์‹œํ‚ฌ๊ฒƒ์ด๋‹ค.



UserService

@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final MailService mailService;

    @Transactional
    public UserCreateRespDto register(UserCreateRepDto dto) {
        String encodedPassword = passwordEncoder.encode(dto.getPassword());

        boolean isExistsNickname = userRepository.existsByNickname(dto.getNickname());
        if (isExistsNickname) { // ๋‹‰๋„ค์ž„ ์ค‘๋ณต ๊ฒ€์ฆ 
            throw new BusinessException(ErrorCode.EXISTS_ALREADY_USER);
        }

        boolean isExistsEmail = userRepository.existsByEmail(dto.getEmail());
        if (isExistsEmail) { // ์ด๋ฉ”์ผ ์ค‘๋ณต ๊ฒ€์ฆ 
            throw new BusinessException(ErrorCode.EXISTS_ALREADY_EMAIL);
        }

        User user = UserCreateRepDto.from(dto, encodedPassword);
        User savedUser = userRepository.save(user);

        try {
            mailService.sendMail(savedUser.getEmail()); // ์ด๋ฉ”์ผ ์ „์†ก 
        } catch (MessagingException e) {
            throw new BusinessException(ErrorCode.SERVER_ERROR);
        }

        return new UserCreateRespDto(savedUser);
    }
 }
  • Domain-Service ์ธ UserService ์—์„œ ํšŒ์›๊ฐ€์ž… ์‹œ ์ด๋ฉ”์ผ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋„๋ก ์ฒ˜๋ฆฌํ•œ๋‹ค.
  • ํ•˜์ง€๋งŒ ์ด๋ฉ”์ผ์„ ๋ณด๋‚ด๋„๋ก ํ•˜๋Š” ๋กœ์ง์ด ์—ฌ๊ธฐ์žˆ์–ด๋„ ๋˜๋Š”๊ฐ€ ์‚ด์ง ์˜์‹ฌ์Šค๋Ÿฝ๋‹ค.
  • ๊ฑฐ๊ธฐ๋‹ค try-catch ๋กœ ์˜ˆ์™ธ์ฒ˜๋ฆฌ๊นŒ์ง€ ํ•ด์•ผํ•˜๋‹ˆ ๋น„์ฆˆ๋‹ˆ์Šค๋กœ์ง์„ ์ฝ๋Š”๋ฐ ๋ฐฉํ•ด๋œ๋‹ค.
  • ์ถ”ํ›„ ๊ฐœ์„ ํ•ด ๋ณด๋„๋ก ํ•˜์ž!



Email ์ „์†ก ์—ฌ๋ถ€ ํ™•์ธํ•˜๊ธฐ

  • ์ด๋ฉ”์ผ์€ ์ •์ƒ์ ์œผ๋กœ ์ „์†ก๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•˜์˜€๋‹ค.







๐Ÿšจ ๋ฌธ์ œ์  ๋ฐœ์ƒ

  • ํšŒ์›๊ฐ€์ž… ์š”์ฒญ์„ ๋‚ ๋ ธ์„๋•Œ API ์†๋„๊ฐ€ 5์ดˆ ๊ฐ€๊นŒ์ด ๋‚˜์˜ค๋Š”๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.
  • ํšŒ์›๊ฐ€์ž…์‹œ ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•˜๋Š”๊ฒƒ๋„ ์•„๋‹ˆ์—ฌ์„œ Email ๋ฐœ์†ก ๋กœ์ง์ด ๋ฌธ์ œ๋ž€๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด ์–ด๋–ค์‹์œผ๋กœ ํ•ด๊ฒฐํ•ด์•ผํ•˜๋Š”์ง€ ํ๋ฆ„์„ ์ด์•ผ๊ธฐํ•ด๋ณด๋ฉฐ ์ •๋ฆฌํ•˜๊ณ  ๊ตฌํ˜„ํ•˜๋„๋ก ํ•ด๋ณด์ž

  1. ์ด๋ฉ”์ผ ๋ฐœ์†ก ๋กœ์ง์„ ํšŒ์›๊ฐ€์ž… ๋กœ์ง๊ณผ ๋ถ„๋ฆฌ์‹œ์ผœ์•ผ ํ•œ๋‹ค.
  2. ์ด๋ฉ”์ผ ๋ฐœ์†ก์€ ๋™๊ธฐ์ ์œผ๋กœ ๋™์ž‘ํ•˜์ง€ ์•Š์•„๋„ ๊ดœ์ฐฎ๋‹ค.
  3. ์ด๋ฉ”์ผ ๋ฐœ์†ก์€ ์ด๋ฒคํŠธ ํ˜•์‹์œผ๋กœ ์ฒ˜๋ฆฌํ•ด๋ณด์ž.
  4. ์ด๋ฉ”์ผ ๋ฐœ์†ก์€ ํšŒ์›๊ฐ€์ž…ํ•˜๋ ค๊ณ  ํ•˜๋Š” ์œ ์ € ๋ฐ์ดํ„ฐ๊ฐ€ DB์— ์ €์žฅ๋˜๊ณ  ๋ฐœ์†ก๋˜์–ด์•ผ ํ•œ๋‹ค.
  5. ์ด๋ฉ”์ผ ๋ฐœ์†ก์ด ์‹คํŒจํ•˜์˜€์„๋•Œ ์žฌ์‹œ๋„ ๋กœ์ง์„ ์ถ”๊ฐ€ํ•ด๋ณด๋Š”๊ฒƒ๋„ ๊ณ ๋ คํ•  ์ˆ˜ ์žˆ๋‹ค.



EventListener

@Slf4j
@Component
@RequiredArgsConstructor
public class UserRegisterEventListener {

    private final MailService mailService;
    private final RedisTemplate<String, String> redisTemplate;
    private static final long VERIFICATION_CODE_EXPIRATION = 10 * 60;

    @Async
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleUserRegistration(UserRegisterEvent event) {
        try {
            String code = mailService.sendMail(event.email());
            ValueOperations<String, String> ops = redisTemplate.opsForValue();
            ops.set(event.email(), code, Duration.ofSeconds(VERIFICATION_CODE_EXPIRATION));
        } catch (MessagingException e) {
            log.warn("Mail ์ „์†ก ์‹คํŒจ : {}", e.getMessage());
        }
    }
}
  • ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœํ–‰๋˜๋ฉด ํ•ด๋‹น ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋กœ์ง์„ ๊ตฌํ˜„ํ•œ๋‹ค.
  • ๋น„๋™๊ธฐ๋กœ ๋™์ž‘ํ•˜๋ฉฐ ํŠธ๋ž™์žญ์…˜์„ ๋ถ„๋ฆฌํ•œ๋‹ค.
  • ํšŒ์›๊ฐ€์ž… ๋กœ์ง์ด ์ปค๋ฐ‹๋œ๋‹ค๋ฉด ๋ฉ”์ผ ๋ฐœ์†ก ๋กœ์ง์ด ๋™์ž‘ํ•œ๋‹ค.
  • ์ถ”ํ›„ ์ธ์ฆ ์ฝ”๋“œ๋ฅผ ๊ฒ€์ฆํ•ด์•ผํ•˜๊ธฐ์— ๋ ˆ๋””์Šค์— ๊ฐ’์„ ์ €์žฅํ•ด๋‘”๋‹ค.



UserService

@Transactional
public UserCreateRespDto register(UserCreateRepDto dto) {
		String encodedPassword = passwordEncoder.encode(dto.getPassword());

		boolean isExistsNickname = userRepository.existsByNickname(dto.getNickname());
		if (isExistsNickname) {
				throw new BusinessException(ErrorCode.EXISTS_ALREADY_USER);
		}

		boolean isExistsEmail = userRepository.existsByEmail(dto.getEmail());
		if (isExistsEmail) {
				throw new BusinessException(ErrorCode.EXISTS_ALREADY_EMAIL);
		}

		User user = UserCreateRepDto.from(dto, encodedPassword);
		User savedUser = userRepository.save(user);
		
		// ์ด๋ฒคํŠธ ๋ฐœํ–‰ 
		applicationEventPublisher.publishEvent(new UserRegisterEvent(dto.getEmail()));

		return new UserCreateRespDto(savedUser);
}
  • ApplicationEventPublisher ๋ฅผ ํ†ตํ•ด ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•œ๋‹ค.



API ํ˜ธ์ถœ ๊ฒฐ๊ณผ

  • 200ms ๋Œ€๋กœ ํ™•์‹คํžˆ ์„ฑ๋Šฅ์ด ๊ฐœ์„ ๋˜์—ˆ๋‹ค.
  • ๋งŒ์•ฝ ํšŒ์›๊ฐ€์ž… ํŠธ๋ž˜ํ”ฝ์ด ๋Š˜์–ด๋‚˜๋ฉด ํ๋ฅผ ์ด์šฉํ•ด ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์•ˆ๋„ ์žˆ์„๊ฒƒ ๊ฐ™๋‹ค.







โœ… ์ด๋ฉ”์ผ ์œ ํšจ์„ฑ ๊ฒ€์ฆ

AuthService

@Transactional
public void verificationEmail(String email, String code) {
		ValueOperations<String, String> ops = redisTemplate.opsForValue();
		String decryptedEmail = encryptionUtil.decrypt(email);
		String storedCode = ops.get(decryptedEmail);
		if (!storedCode.equals(code)) {
				throw new BusinessException(ErrorCode.INVALID_EMAIL_CODE);
		}
		redisTemplate.delete(decryptedEmail);

		User findUser = userRepository.findByEmail(decryptedEmail)
                .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
		findUser.addAuthorizationRole();
		userRepository.save(findUser);
}
  • ์•”ํ˜ธํ™”ํ™”๋œ ์ด๋ฉ”์ผ๊ณผ ์ฝ”๋“œ๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ์ฝ”๋“œ์ด๋‹ค.
  • Redis ์— ์ €์žฅ๋˜์žˆ๋Š” ์ธ์ฆ์ฝ”๋“œ๋ฅผ ๊บผ๋‚ด์™€ ๊ฒ€์ฆํ•˜๊ณ  Redis ์— ๊ฐ’์„ ์ง€์šด๋‹ค.
  • ๋ณตํ˜ธํ™”ํ•œ ์ด๋ฉ”์ผ์„ ํ†ตํ•ด DB ์— ์œ ์ € ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ๊ณ  ๊ถŒํ•œ์„ ์ถ”๊ฐ€ํ•œ ํ›„ ์ €์žฅํ•œ๋‹ค.





Result

AuthService ์—์„œ ๊ถŒํ•œ์ถ”๊ฐ€ํ•˜์˜€๋‹ค๋ฉด ๊ถŒํ•œ์„ ๊ฒ€์ฆํ•˜๊ฒƒ์€ ํ•„ํ„ฐ์—๊ฒŒ ์œ„์ž„ํ•˜๋„๋กํ•œ๋‹ค. JWT ํ† ํฐ์—๋„ ๊ถŒํ•œ์„ ์ถ”๊ฐ€ํ•˜๋„๋ก ํ•ด์•ผ ํ•„ํ„ฐ๋‹จ์—์„œ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๋‹ค. JWT ํ† ํฐ ๊ฒ€์ฆ ๋ฌธ์ œ๋Š” ์ž‘์€ ๋ฌธ์ œ๊ธฐ์— ์—ฌ๊ธฐ์„œ ๋‹ค๋ฃจ์ง„ ์•Š๊ฒ ๋‹ค.












๐Ÿ“– ํ†บ์•„๋ณด๊ธฐ

ํšŒ์›๊ฐ€์ž… ์‹œ ์ด๋ฉ”์ผ ์œ ํšจ์„ฑ ๊ฒ€์ฆ์„ ์ˆ˜ํ–‰ํ•˜๋„๋ก ํ•˜๋Š” 2FA ๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด์•˜๋‹ค. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์žˆ์—ˆ๋‹ค๋ฉด ์ข€๋” ๋‹ค๋ฅธ ์ธ์ฆ ๋ฐฉ์‹์„ ๊ณ ๋ คํ•ด๋ณผ ์ˆ˜ ์žˆ๊ฒ ๋Š”๋ฐ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์—†๊ธฐ์— ๊ตฌํ˜„์กฐ๊ฑด์ด ๊นŒ๋‹ค๋กœ์šด์ ์ด ์•„์‰ฝ๋‹ค. ์ถ”ํ›„ ์ธ์ฆ ๋ฐ ์ธ๊ฐ€๋ฅผ ํ•™์Šต์„ ํ•˜์—ฌ SSO ๊ตฌ์ถ•๋„ ๋‹ค๋ค„๋ณผ ์˜ˆ์ •์ด๋‹ค. ์ด๋ฉ”์ผ ์ธ์ฆ์„ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ์—ฌ๋Ÿฌ ํ‚ค์›Œ๋“œ๋“ค์„ ์–ป๊ณ  ๊ฐ”์œผ๋ฏ€๋กœ ๊ฐ€์น˜์žˆ๋Š” ํ•™์Šต์ด์—ˆ๋‹ค. ์ด๋ฒคํŠธ ๋ฐœํ–‰์— ๋Œ€ํ•ด์„œ๋„ ์ข‹์€ ๊ณต๋ถ€๊ฐ€ ๋ ๊ฒƒ๊ฐ™์•„ ์ถ”ํ›„ ๊ณต๋ถ€ํ•ด๋ด์•ผ๊ฒ ๋‹ค.

profile
โœ…ย ์ ๋‹นํ•œ ์ถ”์ƒํ™”๋ฅผ ์ฐพ์•„๊ฐ€๋Š” ๊ฐœ๋ฐœ์ž์ž…๋‹ˆ๋‹ค.

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