개요

회사에서 JPQL을 통해 테이블 조인 및 검색 조건을 추가하는 쿼리를 작성하고 있었는데, 쿼리 성능 개선을 위하여 inlineView(from 절 subquery)를 작성해야 할 필요가 있었다.


많은 게시글에서 JPA에서 inlineView를 사용하지 못한다는 글을 봐서, 사용할 수 있는 다양한 샘플과 함께 회사 코드에서는 사용이 어려웠던 이유, Spring Data JPA 레포지토리에 이슈를 생성한 것 까지 모두 작성해본다.

JPA에서 inlineView, 사용가능한가?

사용가능하다. 이전에 작성한 글에서 이미 말했듯이, Hibernate 6.1 버전부터 inlineView가 사용가능 하다.

다만, 다양한 케이스를 테스트 하면서 알게 된 점은 데이터를 Entity, Interface Projection, Class Projection, List 형식 모두 상관없이 쿼리를 작성할 수 있지만,
PageRequest와 함께 inlineView 쿼리를 작성하게 되면 JPA가 제공하는 count 쿼리만으로는 오류가 발생한다.

 

이 부분에 대해서는 Spring-Data-JPA 레포지토리에 이슈를 작성하여 문제가 맞는것인지, 해결 가능한 문제인지를 천천히 살펴 본 후 공유할 생각이다.

 

라이브러리를 유지보수 하시는 개발자께서는 이를 countQuery를 직접 작성하는 방법으로 문제를 해결하시길 권유하셨고, 이슈는 closed 상태가 되었다. 아마도 이 부분에 대해서 따로 지원하실 계획은 없는것으로 보인다.

아래에 해당 이슈를 함께 첨부한다.

https://github.com/spring-projects/spring-data-jpa/issues/3662

 

Support of Inline View with Pageable · Issue #3662 · spring-projects/spring-data-jpa

I found a bug that did not produce a normal count query when using Page with InlineView. It would be good to test it by referring to the example repository. I'm attaching a simple example first. Co...

github.com

 

 

JPA에서 inlineView 사용 쿼리

먼저 작성한 테스트 케이스는 다음과 같다.

[Spring JPA][Interface Projection] inline subquery with join - 가능

[Spring JPA][Class Projection] inline subquery with join - 가능

[Spring JPA][Interface Projection] multiple Join inline subquery with join - 가능

[Spring JPA][Interface Projection] multiple Join inline subquery with join and param - 가능

[Spring JPA][Interface Projection] page request without subquery - 가능

[Spring JPA][Interface Projection] page request without join - countQuery 추가 작성 필요

샘플을 직접 확인하고 싶은 경우 아래의 레포지토리에서 샘플을 테스트 할 수 있다.

https://github.com/gogoadl/spring-jpa

 

GitHub - gogoadl/spring-jpa: spring jpa course

spring jpa course. Contribute to gogoadl/spring-jpa development by creating an account on GitHub.

github.com

 

 

Page Request와 함께 InlineView 요청

샘플 쿼리를 디버깅하면서 page타입으로 데이터를 수신할 때에만 Spring-Data-JPA의 AbstractJpaQuery 클래스의 createCountQuery에서 호출이 실패하고 오류 로그가 발생하는 것을 알 수 있었다.

아래는 호출에 실패한 count Query와 오류 로그이다.

SELECT count(T) FROM (SELECT T2.id as id, T2.name as name, T2.address as address FROM Member T2 ) T
org.hibernate.query.SemanticException: The derived SqmFrom[id, name, address] can not be used in a context where the expression needs to be expanded to identifying parts, because a derived model part does not have identifying parts. Replace uses of the root with paths instead e.g. `derivedRoot.get("alias1")` or `derivedRoot.alias1`

 

생성한 카운트 쿼리를 실제로 실행해봐도 정상적으로 동작하지 않는것을 알 수 있었는데, 위의 카운트 쿼리에서 InlineView 쿼리까지는 정상적으로 동작하지만 결과 테이블을 T라는 alias로 접근 가능하도록 했고 이를 바깥 쪽 SELECT 할 때 alias 테이블 이름만 사용했기 때문에 문제가 발생하는 것으로 보인다. 이를 T.id 처럼 alias와 함께 필드 이름을 적용하면 정상적으로 카운트 쿼리가 동작한다.

마무리

Spring-Data-JPA에 기여하는 것은 실패했다. 내 샘플 코드와 함께 문제를 찾고, 이를 통해 비슷한 문제를 겪었던 사람들에게 도움이 되길 바랬는데.. (Spring 기여자가 되고 싶은 마음이 제일 크긴하다) 아쉽지만 다음 기회를 노려야겠다 ㅎㅎ

결국에는 countQuery를 직접 입력하여 inlineView 쿼리문을 작성할 수 있었고 이를 통해 기존 쿼리 응답의 경우 822.2ms, 수정된 쿼리의 경우 489.9ms 로 67.83%의 성능 개선을 얻을 수 있었다

성능 개선율은 아래의 위키 페이지를 참고했다.

https://zetawiki.com/wiki/%EC%84%B1%EB%8A%A5_%EA%B0%9C%EC%84%A0%EC%9C%A8

개요

회사에서 쿼리작성도중 From절 Subquery가 필요한 상황이 있었는데, 서비스가 Spring Data JPA를 사용하고 있었는데, 아직 자료가 많이 없는 상황이라 도움이 되기를 바라며 쿼리를 작성한 경험을 공유하고자 글을 쓴다.

JPA에서의 From절 Subquery 사용가능 여부 확인

JPA에서 From절 Subquery로 많은 검색을 해봤지만, 쓸만한 내용이 없었고 검색 결과에 가장 많이 보이는 내용은
JPA의 한계라는 내용이었다. 많은 글에서 JPA는 From절 Subquery를 지원하지 않는다는 내용을 볼 수 있었고, 그 중 Hibernate 6.1 버전에서 Subquery에 관한 지원이 추가되었다는 사실을 알게 되었다.

이 사실을 통해 hibernate 문서를 뒤졌고, from절에서 subquery를 사용하는 예제를 찾을 수 있었다.

하지만, 현재 Spring Data JPA 버전에서 6.1버전 이상의 Hibernate를 사용했는지 여부를 확인해야 했기에, 프로젝트 내부에서 버전 정보를 확인했다.

보시다시피 Hibernate6.4.1 버전을 사용하고 있었기에, 충분히 JPA로 쿼리를 생성할 수 있겠다는 결론을 내렸고, From절 Subquery를 만들어 보기로 했다.

From 절 Subquery 작성

테스트를 위해 기존에 JPA 학습을 위해 생성한 레포지토리 내에서 몇가지를 수정하여 테스트를 시작했다.
파일 구성은 아래에 소개한다.

테스트 파일을 작성 할 수도 있겠지만, 있었던 controller를 수정하는 것이 더 빠를것 같아서 Controller를 아래처럼 수정했다.

package com.hyeonwoo.spring.jpa.springjpa;

import com.hyeonwoo.spring.jpa.springjpa.domain.Address;
import com.hyeonwoo.spring.jpa.springjpa.domain.Member;
import com.hyeonwoo.spring.jpa.springjpa.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.ArrayList;

@Controller
@RequiredArgsConstructor
public class HelloController {
    private final MemberService memberService;

    @GetMapping("test")
    public void test(Model model) {
        memberService.testSpringMember();
    }

    @GetMapping("insert")
    public void insert() {
        Member a = new Member(1L,"박현우", new Address("Seoul", "Main Street", "12345"));
        Member b = new Member(2L,"차은우", new Address("Seoul", "Main Street", "12345"));
        Member c = new Member(3L,"장원영", new Address("Seoul", "Main Street", "12345"));
        Member d = new Member(4L,"카리나", new Address("Seoul", "Main Street", "12345"));
        Member e = new Member(5L,"홍윤기", new Address("Seoul", "Main Street", "12345"));

        memberService.insertTestDataSet(a);
        memberService.insertTestDataSet(b);
        memberService.insertTestDataSet(c);
        memberService.insertTestDataSet(d);
        memberService.insertTestDataSet(e);
    }
}

아래는 Service 클래스이다.

Service 클래스에서는 Spring Data JPA를 통해 쿼리를 생성하는 방법과 Entity Manager를 직접 다루는 방법 두가지를 시도해보았다.

package com.hyeonwoo.spring.jpa.springjpa.service;


import com.hyeonwoo.spring.jpa.springjpa.domain.Address;
import com.hyeonwoo.spring.jpa.springjpa.domain.Member;
import com.hyeonwoo.spring.jpa.springjpa.repository.MemberRepository;
import com.hyeonwoo.spring.jpa.springjpa.repository.SpringMemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;
    private final SpringMemberRepository springMemberRepository;

    public void testSpringMember() {
        List<Member> test = springMemberRepository.findByInlineViewTest("박현우");
        log.info(test.toString());
        // EXCEPTION
    }

    public void testMember() {
        List<Member> test = memberRepository.findByInlineViewTest("박현우");
        log.info(test.toString());
        // EXCEPTION
    }

    @Transactional
    public void insertTestDataSet(Member a) {
        springMemberRepository.save(a);
    }
}

아래는 Spring Data JPA를 통해 서브쿼리를 사용해보는 Repository 예제 코드이다.

Select 조건에 new Member라는 생성자를 통해 데이터를 삽입했는데, 생성자를 사용하지 않을 시 오류가 발생하여 아래의 StackOverflow 포스팅을 참고했다.

https://stackoverflow.com/questions/67500203/failed-to-convert-from-type-java-lang-object-to-type-org-springframework-d

 

Failed to convert from type [java.lang.Object[]] to type [@org.springframework.data.jpa.repository.Query com.data.models.Users]

I am trying to limit my query to only select specific columns from a table which am going to use, but when I write a simple select query I end up with an error Resolved [org.springframework.core.c...

stackoverflow.com

package com.hyeonwoo.spring.jpa.springjpa.repository;

import com.hyeonwoo.spring.jpa.springjpa.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface SpringMemberRepository extends JpaRepository<Member, Long> {

    @Query("SELECT new Member(T.id as id,T.name as name,T.address as address) FROM (SELECT T2.id as id,T2.name as name,T2.address as address FROM Member T2 WHERE T2.id = 1) T")
    List<Member> findByInlineViewTest(@Param("name") String name);
}

EntityManager를 사용하여 쿼리를 생성한 예제이다.

package com.hyeonwoo.spring.jpa.springjpa.repository;

import com.hyeonwoo.spring.jpa.springjpa.domain.Member;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
@RequiredArgsConstructor
public class MemberRepository {

    private final EntityManager em;

    public List<Member> findByInlineViewTest(String name) {
        return em.createQuery("SELECT T.id, T.name, T.address FROM ( SELECT T2.id as id,T2.name as name,T2.address as address FROM Member T2 WHERE T2.id = 1) T WHERE T.name = :name", Member.class)
                .setParameter("name", name)
                .getResultList();
    }
}

마지막으로 Entity 클래스이다. 특별한 내용은 없다.

package com.hyeonwoo.spring.jpa.springjpa.domain;

import jakarta.persistence.*;
import lombok.*;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter @Setter
@RequiredArgsConstructor
@AllArgsConstructor
public class Member {

    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    private String name;

    @Embedded
    private Address address;

}

package com.hyeonwoo.spring.jpa.springjpa.domain;

import jakarta.persistence.Embeddable;
import lombok.Getter;

@Embeddable
@Getter
public class Address { // 값 타입은 변경 불가능하게 설계해야 한다.
    private String city;
    private String street;
    private String zipCode;
    protected Address(){} // 임베디드 타입은 자바 기본 생성자를 public 또는 protected로 설정해야 한다.


    // Setter를 사용하지 않고 생성자에서 값을 모두 초기화 해서 변경 불가능한 클래스로 제공
    public Address(String city, String street, String zipCode) {
        this.city = city;
        this.street = street;
        this.zipCode = zipCode;
    }
}

위처럼 파일 세팅 후 H2 데이터베이스에 데이터를 임시로 삽입하는 API를 호출했고, 이후 서브쿼리를 통해 데이터를 조회하는 부분에서 디버깅을 통해 데이터가 잘 들어오는지 확인할 수 있었다.

알게된 점

  • hibernate 6.1버전 이상에서는 JPA를 통해 From 절 Subquery SQL을 작성 할 수 있다.
  • Subquery 작성 시 alias를 작성하지 않으면 이름을 찾지 못하는 오류가 있는 것 같다. 예제에서도 alias가 필수적으로 붙어있는 이유로 보인다.
  • Entity 타입으로 데이터를 조회할 수 있었지만, class Projection을 통해서는 데이터를 수신하는데 실패하고 있다. 이 부분은 좀 더 확인해보고, 공유할 만한 내용이 있다면 공유할 생각이다.

 

https://in.relation.to/2022/06/14/orm-61-final/

 

Hibernate 6.1 Final - In Relation To

Hibernate ORM 6.1 was released last week with a few new features[1] and bugfixes. The new features include:

in.relation.to

 

https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html

 

Hibernate ORM 6.1.7.Final User Guide

Fetching, essentially, is the process of grabbing data from the database and making it available to the application. Tuning how an application does fetching is one of the biggest factors in determining how an application will perform. Fetching too much dat

docs.jboss.org

 

개요

이 튜토리얼에서는 Spring Boot 애플리케이션의 로깅 수준을 런타임에 변경하는 방법을 살펴보겠습니다. 많은 것들과 마찬가지로 Spring Boot에는 우리를 위해 구성하는 기본 제공 로깅 기능이 있습니다 . 실행 중인 애플리케이션의 로깅 수준을 조정하는 방법을 살펴보겠습니다.

이를 위해 세 가지 방법을 살펴보겠습니다. Spring Boot Actuator 로거 엔드포인트를 사용하는 방법, Logback 의 자동 스캔 기능을 사용하는 방법 , 마지막으로 Spring Boot Admin 도구를 사용하는 방법입니다.

이 번역본에서는 Spring Boot Actuator를 통해 로깅 레벨을 변경하는 방법만 안내합니다.

Spring boot Actuator

/ loggers Actuator 엔드포인트를 사용하여 로깅 레벨을 표시하고 변경하는 것으로 시작하겠습니다 . / loggers 엔드포인트는 actuator/loggers 에서 사용할 수 있으며 경로의 일부로 이름을 추가하여 특정 로거에 액세스할 수 있습니다.

예를 들어, URL http://localhost:8080/actuator/loggers/root 를 사용하여 루트 로거에 접근할 수 있습니다.

설정

Spring Boot Actuator를 사용하여 애플리케이션을 설정하는 것부터 시작해 보겠습니다.

먼저, pom.xml 파일 에 Spring Boot Actuator Maven 종속성을 추가해야 합니다 .

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    <version>3.1.5</version>
</dependency>

gradle을 통해 종속성 추가

implementation 'org.springframework.boot:spring-boot-starter-actuator:3.1.5'

대부분의 엔드포인트는 기본적으로 비활성화되어 있으므로 application.properties 파일 에서 / loggers 엔드포인트 도 활성화해야 합니다 .

management.endpoints.web.exposure.include=loggers
management.endpoint.loggers.enabled=true

마지막으로, 실험의 효과를 볼 수 있도록 일련의 로깅 명령문을 포함하는 컨트롤러를 만들어 보겠습니다.

@RestController
@RequestMapping("/log")
public class LoggingController {
    private Log log = LogFactory.getLog(LoggingController.class);

    @GetMapping
    public String log() {
        log.trace("This is a TRACE level message");
        log.debug("This is a DEBUG level message");
        log.info("This is an INFO level message");
        log.warn("This is a WARN level message");
        log.error("This is an ERROR level message");
        return "See the log for details";
    }
}

/ loggersEndpoint 사용

애플리케이션을 시작하고 로그 API에 접근해 보겠습니다.

curl http://localhost:8080/log

그럼, 로그를 확인해 볼까요? 로그에는 세 개의 로깅 문장이 있습니다.

2019-09-02 09:51:53.498  INFO 12208 --- [nio-8080-exec-1] c.b.s.b.m.logging.LoggingController      : This is an INFO level message
2019-09-02 09:51:53.498  WARN 12208 --- [nio-8080-exec-1] c.b.s.b.m.logging.LoggingController      : This is a WARN level message
2019-09-02 09:51:53.498 ERROR 12208 --- [nio-8080-exec-1] c.b.s.b.m.logging.LoggingController      : This is an ERROR level message

이제 / loggers Actuator 엔드포인트를 호출하여 com.baeldung.spring.boot.management.logging 패키지 의 로깅 수준을 확인해 보겠습니다.

curl http://localhost:8080/actuator/loggers/com.baeldung.spring.boot.management.logging
  {"configuredLevel":null,"effectiveLevel":"INFO"}

로깅 수준을 변경하려면 / loggers 엔드포인트 에 POST 요청을 발행할 수 있습니다.

curl -i -X POST -H 'Content-Type: application/json' -d '{"configuredLevel": "TRACE"}'
  http://localhost:8080/actuator/loggers/com.baeldung.spring.boot.management.logging
  HTTP/1.1 204
  Date: Mon, 02 Sep 2019 13:56:52 GMT

로깅 수준을 다시 확인하면 TRACE 로 설정되어 있어야 합니다.

curl http://localhost:8080/actuator/loggers/com.baeldung.spring.boot.management.logging
  {"configuredLevel":"TRACE","effectiveLevel":"TRACE"}

마지막으로, 로그 API를 다시 실행하여 변경 사항이 실제로 어떻게 적용되는지 확인할 수 있습니다.

curl http://localhost:8080/log

이제 로그를 다시 확인해 보겠습니다.

2019-09-02 09:59:20.283 TRACE 12208 --- [io-8080-exec-10] c.b.s.b.m.logging.LoggingController      : This is a TRACE level message
2019-09-02 09:59:20.283 DEBUG 12208 --- [io-8080-exec-10] c.b.s.b.m.logging.LoggingController      : This is a DEBUG level message
2019-09-02 09:59:20.283  INFO 12208 --- [io-8080-exec-10] c.b.s.b.m.logging.LoggingController      : This is an INFO level message
2019-09-02 09:59:20.283  WARN 12208 --- [io-8080-exec-10] c.b.s.b.m.logging.LoggingController      : This is a WARN level message
2019-09-02 09:59:20.283 ERROR 12208 --- [io-8080-exec-10] c.b.s.b.m.logging.LoggingController      : This is an ERROR level message

Reference

https://www.baeldung.com/spring-boot-changing-log-level-at-runtime

개요

스프링 부트 프로젝트 생성 후 main 실행 시 아래의 로그와 함께 실행에 실패했다.

아래의 로그에서 알 수 있듯이, 8080 포트가 이미 사용중이므로 실행에 실패한것을 알 수 있다.

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::               (v2.7.16)

2023-10-03 22:35:24.447  INFO 24480 --- [           main] c.h.springboot.SpringbootApplication     : Starting SpringbootApplication using Java 11.0.17 on DESKTOP-5TJ7OBF with PID 24480 (D:\GitRepository_HW\springboot-2023\out\production\classes started by HyeonWoo in D:\GitRepository_HW\springboot-2023)
2023-10-03 22:35:24.451  INFO 24480 --- [           main] c.h.springboot.SpringbootApplication     : No active profile set, falling back to 1 default profile: "default"
2023-10-03 22:35:25.689  INFO 24480 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2023-10-03 22:35:25.701  INFO 24480 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2023-10-03 22:35:25.702  INFO 24480 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.80]
2023-10-03 22:35:25.836  INFO 24480 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2023-10-03 22:35:25.836  INFO 24480 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1303 ms
2023-10-03 22:35:26.071  INFO 24480 --- [           main] o.s.b.a.w.s.WelcomePageHandlerMapping    : Adding welcome page: class path resource [static/index.html]
2023-10-03 22:35:26.178  WARN 24480 --- [           main] ion$DefaultTemplateResolverConfiguration : Cannot find template location: classpath:/templates/ (please add some templates, check your Thymeleaf configuration, or set spring.thymeleaf.check-template-location=false)
2023-10-03 22:35:26.220  WARN 24480 --- [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop'; nested exception is org.springframework.boot.web.server.PortInUseException: Port 8080 is already in use
2023-10-03 22:35:26.223  INFO 24480 --- [           main] o.apache.catalina.core.StandardService   : Stopping service [Tomcat]
2023-10-03 22:35:26.235  INFO 24480 --- [           main] ConditionEvaluationReportLoggingListener : 

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2023-10-03 22:35:26.251 ERROR 24480 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

Web server failed to start. Port 8080 was already in use.

Action:

Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.


Process finished with exit code 1

또한 localhost 접속 시 서버를 실행하지 않았음에도 아래와 같이 It works! 라는 문구를 볼 수 있었다.

 

 

원인파악

예상했던대로, 8080포트를 사용할 것으로 예상되는 프로그램이 실행중인것을 작업관리자에서 확인할 수 있었다.

이 프로그램 httpd는 아파치 하이퍼텍스트 전송 프로토콜 (HTTP) 서버 프로그램이다.

아파치 설치시에 서비스 또는 시작 프로그램에 등록된 것으로 보인다.

여러가지 방법을 통해 이를 해결할 수 있겠지만, 나는 두가지의 경우만 확인해봤다.

1. SpringBoot의 기본 서버 포트를 변경한다.

2. 아파치 httpd를 서비스 또는 시작 프로그램에서 제거하기

 

오류 해결

1. SpringBoot의 기본 서버 포트를 변경한다.

이 경우 application.properties 파일에 새로운 옵션을 추가한다. server.port = [원하는 포트] 를 입력하여 쉽게 해결할 수 있다.

 

 

2. 아파치 httpd를 서비스 또는 시작 프로그램에서 제거하기.

역시나 service 목록에 아파치가 등록되어 있는것을 확인했다. 

Windows + R 버튼을  눌러 실행 창을 띄운 후 services.msc를 입력하여 서비스 창을 띄운 후 Apache2.4를 중지하면 된다.

컴퓨터를 새로 킬 때마다 실행되는 것을 막으려면 오른쪽 마우스로 Apache2.4를 클릭 후 속성에서 시작유형을 사용 안 함으로 변경하자.

 

Reference

https://zetawiki.com/wiki/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8_%EC%84%9C%EB%B2%84_%ED%8F%AC%ED%8A%B8_%EB%B3%80%EA%B2%BD

+ Recent posts