<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>끄적끄적</title>
    <link>https://programmingnote.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 11 Jul 2026 08:42:37 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor></managingEditor>
    <image>
      <title>끄적끄적</title>
      <url>https://tistory1.daumcdn.net/tistory/2950981/attach/6a52de8786274ba1a19fcf6517fa7693</url>
      <link>https://programmingnote.tistory.com</link>
    </image>
    <item>
      <title>[Spring] Service 순환참조를 @EventListener로 풀어보자</title>
      <link>https://programmingnote.tistory.com/89</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Framework로 개발하다 보면 Service를 도메인 기준으로 나누는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다 보면 Aservice에서 BService로 참조를 하게 될 경우도 있고, 반대로 BService에서 AService로 참조할 수도 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우에 각 서비스 클래스를 빈으로 만들면서 서로를 참조하기 때문에 다음과 같은 오류를 만나게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;961&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uN0PK/btrp5ADrav8/PqsxrgoC9RIoMRRtIESkK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uN0PK/btrp5ADrav8/PqsxrgoC9RIoMRRtIESkK0/img.png&quot; data-alt=&quot;서비스간 순환참조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uN0PK/btrp5ADrav8/PqsxrgoC9RIoMRRtIESkK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuN0PK%2Fbtrp5ADrav8%2FPqsxrgoC9RIoMRRtIESkK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;961&quot; height=&quot;178&quot; data-origin-width=&quot;961&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;서비스간 순환참조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 매우 복잡한 비지니스 구조를 가지고 있기 때문에, 추후에 해당 문제를 해결하기 위한 방법이 애매하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 서비스의 빈생성을 @Lazy 등.. 과 같은 방법으로 지연 로딩하여 생성 순서를 정해줄 수 있지만, 여러가 서비스가 얽혀 있는 상황이라면 깔끔한 방법이 되지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 구조에서 가장 간단하게 개발할 수 있는 방법으로 @EventListener을 선택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@EventListner를 사용하면 두 서비스간의 의존성을 끊고, 이벤트 방식으로 필요한 처리를 각 서비스에 적절하게 위임할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@EventListner를 사용하는 방법은 다른 블로그에 자세하게 많이 나와있으니 참고하고, 간단하게만 말하자면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크게 Event 발생을 위한 Event객체와 Event발생 시 일어날 행위에 대한 EventHandler가 있고 이러한 이벤트를 publish 해줄 ApplicationEventPublisher로 이루어져 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Event로 사용한 단순 Event클래스&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Data
public class AEvent {

    private Long id;

    public AEvent(Long id) {
        this.id = id;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Event발생 시 일어날 행위에 대한 EventHandler&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class AEventHandler {

    private final AService aService;

    @EventListener
    public void test(AEvent aEvent) {
        aService.eventTest(aEvent.getId());
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Event를 puslish 하는 ApplicationEventPublisher&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class BService {

    private final ApplicationEventPublisher publisher;

    public void eventTest() {
        publisher.publishEvent(new AEvent(100L));
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApplicationEventPublisher에서 event에 따라서 handler를 실행해주고 Handler에 등록된 이벤트 파라미터 객체에 따라 리스너로 동작하여 실행되게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스와 서비스 사이에서 ApplicationEventPublisher가 이벤트에 따라 연결해주기 때문에 두 서비스 간의 순환 참조가 해결된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;* pulish한 결과의 리턴 값이 필요한 경우&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 publish 후에 리턴값이 필요하다면 AEvent에 리턴 값을 받아 참조할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Data
public class AEvent {

    private Long id;

    private String returnValue;

    public AEvent(Long id) {
        this.id = id;
    }

    public AEvent(Long id, String returnValue) {
        this.id = id;
        this.returnValue = returnValue;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class AEventHandler {

    private final AService aService;

    @EventListener
    public void test(AEvent aEvent) {
        String returnValue = aService.eventTest(aEvent.getReturnValue());
        aEvent.setReturnValue(returnValue);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class BService {

    private final ApplicationEventPublisher publisher;

    public void eventTest() {
        AEvent aEvent = new AEvent(100L, &quot;Hi&quot;);
        publisher.publishEvent(aEvent);
        System.out.println(&quot;Request AEvent after returnValue : &quot; + aEvent.getReturnValue());
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름을 요약해보면&amp;nbsp;BService -&amp;gt; publish -&amp;gt; AService -&amp;gt; AEvent returnValue 변경 -&amp;gt; BService에서 AEvent의 값 참조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@EventListener는 아래와 같이 부가적인 기능도 제공한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 기본적으로 동기 호출이지만 @EnableAsync, @Async로 비동기 호출도 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. handler안에 같은 이벤트 객체를 받는 여러 파라미터가 있다면, @Order로 호출 순서를 지정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;* ApplicationEventPublisher에서 어떻게 EventHandler의 @EventListener 함수를 호출할 수 있을까?&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApplicationEventPublisher의 publishEvent를 찾아가다 보면 아래 메서드를 만난다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;866&quot; data-origin-height=&quot;597&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2sJyt/btrp536s6Z9/iQikcMDSj0K5HKATQFahu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2sJyt/btrp536s6Z9/iQikcMDSj0K5HKATQFahu0/img.png&quot; data-alt=&quot;AbstractApplicationContext&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2sJyt/btrp536s6Z9/iQikcMDSj0K5HKATQFahu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2sJyt%2Fbtrp536s6Z9%2FiQikcMDSj0K5HKATQFahu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;866&quot; height=&quot;597&quot; data-origin-width=&quot;866&quot; data-origin-height=&quot;597&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AbstractApplicationContext&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;this.getApplicationEventMulticaster().multicastEvent((ApplicationEvent)applicationEvent, eventType);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 발생하는 ApplicationEvent인지(어플리케이션 생명주기에 따라 실행되는 이벤트)에 따라 구분되고, 위 함수로 실행되게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;multicastEvent 메소드를 따라가 보면, 아래가 나온다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;747&quot; data-origin-height=&quot;371&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0fqEX/btrp6QZ1R3r/z2cZoDm4JKYt7s99aBNGO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0fqEX/btrp6QZ1R3r/z2cZoDm4JKYt7s99aBNGO0/img.png&quot; data-alt=&quot;SimpleApplicationEventMulticaster&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0fqEX/btrp6QZ1R3r/z2cZoDm4JKYt7s99aBNGO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0fqEX%2Fbtrp6QZ1R3r%2Fz2cZoDm4JKYt7s99aBNGO0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;747&quot; height=&quot;371&quot; data-origin-width=&quot;747&quot; data-origin-height=&quot;371&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SimpleApplicationEventMulticaster&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 getApplicationListeners에서 이미 애플리케이션에 등록된 리스너를 찾아서 실행해 주고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getApplicationListeners는&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;SimpleApplicationEventMulticaster의 추상클래스인 AbstractApplicationEventMulticaster의 defaultRetriever에 정의된 리스너에 의해서 호출되고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public abstract class AbstractApplicationEventMulticaster implements ApplicationEventMulticaster, BeanClassLoaderAware, BeanFactoryAware {
    private final AbstractApplicationEventMulticaster.DefaultListenerRetriever defaultRetriever = new AbstractApplicationEventMulticaster.DefaultListenerRetriever();
    final Map&amp;lt;AbstractApplicationEventMulticaster.ListenerCacheKey, AbstractApplicationEventMulticaster.CachedListenerRetriever&amp;gt; retrieverCache = new ConcurrentHashMap(64);
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 여기서 한가지 의문이 드는 건 언제 AbstractApplicationEventMulticaster에 리스너가 등록되냐는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 에플리케이션이 실행하는 시점에 찾아볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main메서드에서 실행되는 SpringApplication.run() 메서드를 찾아보면 아래와 같이 따라갈 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@SpringBootApplication
public class BlogApplication {

    public static void main(String[] args) {
        SpringApplication.run(BlogApplication.class, args);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public ConfigurableApplicationContext run(String... args) {
    ...
        context.setApplicationStartup(this.applicationStartup);
        this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
        this.refreshContext(context);
        this.afterRefresh(context, applicationArguments);
        stopWatch.stop();
        if (this.logStartupInfo) {
            (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
        }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public void refresh() throws BeansException, IllegalStateException {
   ...
            this.initApplicationEventMulticaster();
            this.onRefresh();
            this.registerListeners();
            this.finishBeanFactoryInitialization(beanFactory);
            this.finishRefresh();
   ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
    ...

    beanFactory.setTempClassLoader((ClassLoader)null);
    beanFactory.freezeConfiguration();
    beanFactory.preInstantiateSingletons();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpringApplication.run() -&amp;gt; this.refresh(context) -&amp;gt; this.refreshContext(context) -&amp;gt; this.finishBeanFactoryInitialization(beanFactory) -&amp;gt; beanFactory.preInstantiateSingletons()&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 경로를 따라오다보면 아래 메서드가 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1521&quot; data-origin-height=&quot;743&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xlBSc/btrp6G3Zg5K/VVf5DRjng51fqVPOsS80j1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xlBSc/btrp6G3Zg5K/VVf5DRjng51fqVPOsS80j1/img.png&quot; data-alt=&quot;DefaultListableBeanFactory.preInstantiateSingletons&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xlBSc/btrp6G3Zg5K/VVf5DRjng51fqVPOsS80j1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxlBSc%2Fbtrp6G3Zg5K%2FVVf5DRjng51fqVPOsS80j1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1521&quot; height=&quot;743&quot; data-origin-width=&quot;1521&quot; data-origin-height=&quot;743&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;DefaultListableBeanFactory.preInstantiateSingletons&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;org.springframework.context.event.internalEventListenerProcessor bean 이름을 가진 EventListenerMethodProcessor 클래스를 만나면서 싱글톤 인스턴트로 만들어지고 난 후의 작업이 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(SmartInitializingSingleton을 상속하고 있는 클래스가 몇개 있는데, 해당 클래스를 상속하면 싱글톤 초기화 시 작업이 이루어지는 클래스가 몇 개 있는 것 같다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 해당 메소드를 따라가다 보면 AbstractApplicationContext 추상 클래스 변수 ApplicationEventMulticaster에 추가되고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하자면&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SimpleApplicationEventMulticaster -&amp;gt; AbstractApplicationEventMulticaster -&amp;gt; ApplicationEventMulticaster에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 시작 시점에서 DefaultListableBeanFactory가 AbstractApplicationContext의 AbstractApplicationEventMulticaster로 리스너 핸들러를 등록해주고, AbstractApplicationContext 상위 인터페이스의 ApplicationContext가 상속한 ApplicationEventPublisher를 통해서 publish 할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(내용이 맞는지 모르겠지만, 남이 짠 코드 해석도 힘든데 저걸 짠 사람은 얼마나 대단할까..-_-)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Web/Spring</category>
      <category>@EventListener</category>
      <category>Spring</category>
      <category>순환참조</category>
      <author>알 수 없는 사용자</author>
      <guid isPermaLink="true">https://programmingnote.tistory.com/89</guid>
      <comments>https://programmingnote.tistory.com/89#entry89comment</comments>
      <pubDate>Sat, 8 Jan 2022 22:54:55 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] 각각 다른 패키지에 클래스 이름이 같은 엔티티가 있을 경우 인식 문제</title>
      <link>https://programmingnote.tistory.com/88</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;실무를 하다 보면 종종 다른 데이터베이스에 같은 이름의 테이블이 존재하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, A와 B 데이터베이스에 Category라는 테이블이 있고, 하나의 프로젝트에서 두 개의 엔티티를 만들어 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;# 스키마가 다른 같은 이름의 테이블&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;create table schema_a.category
(
    category_seq int auto_increment primary key
);

create table schema_b.category
(
    category_seq int auto_increment primary key
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 이런 경우 테이블 명칭을 그대로 사용하여 엔티티 클래스 이름으로 사용하여 다음과 같이 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;983&quot; data-origin-height=&quot;334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pm4Wk/btrputrr1bq/JcrmV9lMZmePL7uuHgMm60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pm4Wk/btrputrr1bq/JcrmV9lMZmePL7uuHgMm60/img.png&quot; data-alt=&quot;schema_a에 있는 category 엔티티&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pm4Wk/btrputrr1bq/JcrmV9lMZmePL7uuHgMm60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpm4Wk%2Fbtrputrr1bq%2FJcrmV9lMZmePL7uuHgMm60%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;983&quot; height=&quot;334&quot; data-origin-width=&quot;983&quot; data-origin-height=&quot;334&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;schema_a에 있는 category 엔티티&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;959&quot; data-origin-height=&quot;286&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZEabc/btrpoW8fssN/nL89Z9gKgmmqIKmpWJYfC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZEabc/btrpoW8fssN/nL89Z9gKgmmqIKmpWJYfC0/img.png&quot; data-alt=&quot;schema_b에 있는 category 엔티티&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZEabc/btrpoW8fssN/nL89Z9gKgmmqIKmpWJYfC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZEabc%2FbtrpoW8fssN%2FnL89Z9gKgmmqIKmpWJYfC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;959&quot; height=&quot;286&quot; data-origin-width=&quot;959&quot; data-origin-height=&quot;286&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;schema_b에 있는 category 엔티티&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 막연하게 @Table에 스키마와 테이블 명칭을 지정해두었으니 해당 엔티티를 사용할 때 문제없을 줄 알았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;578&quot; data-origin-height=&quot;324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SgNoh/btrpz2lv5DP/0P1m2YMVqCkXBqL27Wn8r1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SgNoh/btrpz2lv5DP/0P1m2YMVqCkXBqL27Wn8r1/img.png&quot; data-alt=&quot;queryDSL 사용예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SgNoh/btrpz2lv5DP/0P1m2YMVqCkXBqL27Wn8r1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSgNoh%2Fbtrpz2lv5DP%2F0P1m2YMVqCkXBqL27Wn8r1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;578&quot; height=&quot;324&quot; data-origin-width=&quot;578&quot; data-origin-height=&quot;324&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;queryDSL 사용예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 위 사진과 같이 QClass를 통해 패키지 경로가 지정된 채로 사용되어 query가 generated될 줄 알았기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 위 코드를 실행시켜보면 보기좋게 에러가 발생한다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;65&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/phWyr/btrpzLEr8HT/oFKTs3ySh50QZEaWcc8SyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/phWyr/btrpzLEr8HT/oFKTs3ySh50QZEaWcc8SyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/phWyr/btrpzLEr8HT/oFKTs3ySh50QZEaWcc8SyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FphWyr%2FbtrpzLEr8HT%2FoFKTs3ySh50QZEaWcc8SyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1410&quot; height=&quot;65&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;65&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;select category
from com.example.blog.entity.schema_b.Category category
where category.categoryASeq = ?1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명 schema_a 패지키 경로에 있는 category엔티티를 사용하였지만, 실제 쿼리는 schema_b로 나가면서 필드를 찾을 수 없는 에러가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 QueryDSL이나 기타 다른 라이브러리 오류인 줄 알았지만...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태생부터 &amp;nbsp;entity명칭은 다른 패키지라 하더라도 같으면 안 된다는 규칙(?)이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/30590324/select-a-default-entity-when-two-or-more-entities-with-same-name-exist-spring&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://stackoverflow.com/questions/30590324/select-a-default-entity-when-two-or-more-entities-with-same-name-exist-spring&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1641126396366&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Select a default Entity when two or more Entities with same name exist - Spring Boot, Spring Data JPA,&quot; data-og-description=&quot;I'm using spring data jpa for persistence. Is there a way where one entity can be marked as default if more than one entity exists with the same name. Something like @Primary annotation used to res...&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/30590324/select-a-default-entity-when-two-or-more-entities-with-same-name-exist-spring&quot; data-og-url=&quot;https://stackoverflow.com/questions/30590324/select-a-default-entity-when-two-or-more-entities-with-same-name-exist-spring&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cjjqsj/hyMWpEB66b/ijsWDpUf5k5AfARZsCgpK1/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/30590324/select-a-default-entity-when-two-or-more-entities-with-same-name-exist-spring&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/30590324/select-a-default-entity-when-two-or-more-entities-with-same-name-exist-spring&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cjjqsj/hyMWpEB66b/ijsWDpUf5k5AfARZsCgpK1/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Select a default Entity when two or more Entities with same name exist - Spring Boot, Spring Data JPA,&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;I'm using spring data jpa for persistence. Is there a way where one entity can be marked as default if more than one entity exists with the same name. Something like @Primary annotation used to res...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.objectdb.com/java/jpa/entity/types#Entity_Classes_&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.objectdb.com/java/jpa/entity/types#Entity_Classes_&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1641126413008&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;JPA Persistable Types&quot; data-og-description=&quot;JPA Persistable Types The term persistable types refers to data types that can be used in storing data in the database. ObjectDB supports all the JPA persistable types, which are: User defined classes - Entity classes, Mapped superclasses, Embeddable class&quot; data-og-host=&quot;www.objectdb.com&quot; data-og-source-url=&quot;https://www.objectdb.com/java/jpa/entity/types#Entity_Classes_&quot; data-og-url=&quot;https://www.objectdb.com/java/jpa/entity/types#Entity_Classes_&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.objectdb.com/java/jpa/entity/types#Entity_Classes_&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.objectdb.com/java/jpa/entity/types#Entity_Classes_&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;JPA Persistable Types&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;JPA Persistable Types The term persistable types refers to data types that can be used in storing data in the database. ObjectDB supports all the JPA persistable types, which are: User defined classes - Entity classes, Mapped superclasses, Embeddable class&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.objectdb.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Entity classes are represented&amp;nbsp;in queries&amp;nbsp;by&amp;nbsp;entity names. By default, the entity name is the unqualified name of the entity class (i.e. the short class name excluding the package name). A different entity name can be set explicitly by using the&amp;nbsp;name&amp;nbsp;attribute of the&amp;nbsp;Entity&amp;nbsp;annotation:&lt;br /&gt;&lt;br /&gt;Entity names must be unique. When two entity classes in different packages share the same class name, explicit entity name setting is required to avoid collision.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술적으로 어떤 이유인진 모르겠지만, 유니크해야 한다고 한다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일 시에는 패키지 경로로 체크하기 때문에 오류 표시가 안 나지만, 실제 런타임 시 쿼리가 만들어질 때 오류가 발생하는 케이스이므로 주의해야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트하다가 발견한 내용으로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hibernate 5.3.9 버전에서 해당 코드를 돌리면 단순히 위와 같이 쿼리를 만들 때 오류가 나지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hibernate 5.4.x 버전부터는 아래와 같이 오류의 이유가 정확히 출력된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Caused by: org.hibernate.DuplicateMappingException: The [com.example.blog.entity.schema_a.Category] and [com.example.blog.entity.schema_b.Category] entities share the same JPA entity name: [Category] which is not allowed!&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 궁금해서 찾아보니 org.hibernate.boot.internal.InFlightMetadataCollectorImpl 클래스에 addEntityBinding이 실행되면서 엔티티가 바인딩될 때 엔티티 클래스 이름이 같은 놈이 있는지 체크하는 로직이 추가된 것으로 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5.3.9 버전에도 entityName을 체크하는 로직은 있었지만, jpaEntityName을 비교하는 로직도 추가된 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;646&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cm5iyc/btrpBCHcSOg/MKuEKin1A4kLUczWQtIklK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cm5iyc/btrpBCHcSOg/MKuEKin1A4kLUczWQtIklK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cm5iyc/btrpBCHcSOg/MKuEKin1A4kLUczWQtIklK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcm5iyc%2FbtrpBCHcSOg%2FMKuEKin1A4kLUczWQtIklK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;918&quot; height=&quot;646&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;646&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로, entityName은 패키지 경로를 포함한 엔티티 클래스의 이름 같고, jpaEntityName은 @Entity의 name속성에 정의된 이름 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;46&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cebe9s/btrptg0dsRs/DjS8RGKtpd1hP6Zoz2DUA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cebe9s/btrptg0dsRs/DjS8RGKtpd1hP6Zoz2DUA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cebe9s/btrptg0dsRs/DjS8RGKtpd1hP6Zoz2DUA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcebe9s%2Fbtrptg0dsRs%2FDjS8RGKtpd1hP6Zoz2DUA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;591&quot; height=&quot;46&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;46&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;277&quot; data-origin-height=&quot;84&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cU1Oeo/btrpzKyLqMg/2DvYhSfQDApmw3PjVYSxvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cU1Oeo/btrpzKyLqMg/2DvYhSfQDApmw3PjVYSxvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cU1Oeo/btrpzKyLqMg/2DvYhSfQDApmw3PjVYSxvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcU1Oeo%2FbtrpzKyLqMg%2F2DvYhSfQDApmw3PjVYSxvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;277&quot; height=&quot;84&quot; data-origin-width=&quot;277&quot; data-origin-height=&quot;84&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Entity 속성 name까지 각각 다 지정하여 분기를 피한다 하더라도 결국 exception을 만나 동작하지 않게 된다. &amp;nbsp;&amp;nbsp;&lt;/p&gt;</description>
      <category>Web/JPA &amp;amp; QueryDSL</category>
      <category>entity name</category>
      <category>JPA</category>
      <author>알 수 없는 사용자</author>
      <guid isPermaLink="true">https://programmingnote.tistory.com/88</guid>
      <comments>https://programmingnote.tistory.com/88#entry88comment</comments>
      <pubDate>Sun, 2 Jan 2022 22:10:25 +0900</pubDate>
    </item>
    <item>
      <title>[QueryDSL] IN 절에 여러개의 Parameter 사용하기</title>
      <link>https://programmingnote.tistory.com/87</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;실무를 하다 보면 일괄로 데이터를 업데이트하거나 인서트하는 작업을 자주 하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 작업을 기능으로 만들 경우에 입력된 값이 수정이나 입력될 수 있는 값인지 검증하는 작업이 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 생각해보면 클라이언트에서 입력된 값이 a, b라면 아래와 같은 쿼리로 풀 수 있을 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1635085182944&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select *
from Account
where Login_Id in ('a', 'b');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 복잡한 실무에서는 단일 값으로만 검증을 취할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 회원의 아이디 이메일, 아이디, 이름으로 검증을 취해야 할 경우 위와 같은 쿼리로는 불가능하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 회원의 고유 일련번호(account_seq)로 판단은 가능하겠지만, 클라이언트에서 회원의 고유 일련번호는 모르는 경우가 많을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 경우, 여러 방법이 있겠지만 IN 절에 칼럼을 여러 개 지정하여 여러 파라미터를 넘길 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1635085990309&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select *
from Account
where (Email, Login_Id, Name) in
(('aaa@naver.com', 'aaa', '홍길동'),('bbb@naver.com', 'bbb', '테스트'));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문법은 대표적인 대부분의 RDMS에서 지원하지만 MSSQL에서는 지원하지 않는 것으로 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여기서 한 단계 더 나아가, JPA를 사용할 경우 QueryDSL에서 어떻게 사용할 수 있는지 알아보자.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 IN 키워드를 기준으로 왼쪽 칼럼 부분은 QueryDSL의 Expressions.list로 표현할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1635086913758&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Expressions.list(account.Email, account.loginId, account.name).in(...)

=&amp;gt; (Email, Login_Id, Name) in (...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;IN 키워드의 우측 부분은 검색하고자 하는 파라미터 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값은 동적으로 몇 개의 값이 들어오는지 알 수 없기 때문에, 왼쪽 칼럼 부분과 달리 가공해주어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서부터 삽질이 시작되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;863&quot; data-origin-height=&quot;415&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ydO20/btriCFZHMZo/JgA1rhj7D3EonZtgPnXHJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ydO20/btriCFZHMZo/JgA1rhj7D3EonZtgPnXHJk/img.png&quot; data-alt=&quot;QueryDSL&amp;amp;amp;nbsp;SimpleExpression in메소드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ydO20/btriCFZHMZo/JgA1rhj7D3EonZtgPnXHJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FydO20%2FbtriCFZHMZo%2FJgA1rhj7D3EonZtgPnXHJk%2Fimg.png&quot; data-origin-width=&quot;863&quot; data-origin-height=&quot;415&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;QueryDSL&amp;nbsp;SimpleExpression in메소드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;in 메서드에 들어갈 수 있는 파리미터를 살펴보니 Expression을 가변 인자로 받고 있는 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 나는 처음에 검색할 List를 Expression 배열로 변환하여 사용하면 될 것이라 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 첫 번째 삽질&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1635088533322&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public List&amp;lt;Account&amp;gt; findAllCustom(List&amp;lt;SearchAccount&amp;gt; searchAccounts) {

	return
    	query
            .selectFrom(account)
            .where(
            	Expressions.list(account.Email, account.loginId, account.name).in((Expression&amp;lt;? extends Tuple&amp;gt;[])searchAccounts.stream()
                	.map(entry -&amp;gt; Expressions.list(Expressions.constant(entry.getEmail()), Expressions.constant(entry.getLoginId()), Expressions.constant(entry.getName())))
                    .toArray(Expression[]::new))
            )
            .fetch();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 칼럼에 검색 값으로 적용될 파라미터를 Expressions.list로 만들어주고, 이를 배열로 넘겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, IN절에서 stream으로 구한 값을 배열로 바꾸고, (Expression&amp;lt;?&amp;nbsp;extends&amp;nbsp;Tuple&amp;gt;[]) 로 명시적으로 변환해주지 않는다면, 아래와 같이 타입을 명확히 할 수 없는 빌드 오류가 발생하기 때문에, 주의하자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1495&quot; data-origin-height=&quot;37&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qKL9h/btriFsLRrvw/mNzubCqJy3bSIY8TRPsfpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qKL9h/btriFsLRrvw/mNzubCqJy3bSIY8TRPsfpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qKL9h/btriFsLRrvw/mNzubCqJy3bSIY8TRPsfpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqKL9h%2FbtriFsLRrvw%2FmNzubCqJy3bSIY8TRPsfpk%2Fimg.png&quot; data-origin-width=&quot;1495&quot; data-origin-height=&quot;37&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로는, 아쉽게 실패다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1881&quot; data-origin-height=&quot;266&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cK4b1i/btriO0AJTj4/28ipg1gZRKCHg6IfiJQECk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cK4b1i/btriO0AJTj4/28ipg1gZRKCHg6IfiJQECk/img.png&quot; data-alt=&quot;쿼리 실행 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cK4b1i/btriO0AJTj4/28ipg1gZRKCHg6IfiJQECk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcK4b1i%2FbtriO0AJTj4%2F28ipg1gZRKCHg6IfiJQECk%2Fimg.png&quot; data-origin-width=&quot;1881&quot; data-origin-height=&quot;266&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;쿼리 실행 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1635092294843&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select account0_.Account_Seq as account_1_0_,
       account0_.Email       as email2_0_,
       account0_.Login_Id
                             as login_id3_0_,
       account0_.Name        as name4_0_,
       account0_.Use_Yn      as use_yn5_0_
from Account account0_
where (account0_.Email, account0_.Login_Id, account0_.Name) in
      ('aaa@test.co.kr', 'aaa', '홍길동', ('bbb@test.co.kr', 'bbb', '테스트'))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼핏 성공한 것처럼 보이지만, IN 키워드 우측에 들어가는 검색 파라미터가 첫 번째 묶음 쪽에 괄호가 정상적으로 들어가지 않는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 여러 가지 시도를 해보았지만, 이 방법으로는 해결하지 못할 것 같아 다른 방법을 찾아 헤맸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 할 수 있을까 생각하다가 결국 QueryDSL gitbhub issue를 뒤지기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/querydsl/querydsl/issues/2910&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/querydsl/querydsl/issues/2910&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1635088729125&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Multiple Column In clause Parameter &amp;middot; Issue #2910 &amp;middot; querydsl/querydsl&quot; data-og-description=&quot;@Entity @Data class Image { private long id; private long imgId; private int index; } //In service, select all that its index and imgId in a map HashMap&amp;lt;Integer, Long&amp;gt; usedImageMap = ... List...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/querydsl/querydsl/issues/2910&quot; data-og-url=&quot;https://github.com/querydsl/querydsl/issues/2910&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/vYZvY/hyL4HfX1Ny/V7F1UC9zCW40i0xxPkQ9o1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/querydsl/querydsl/issues/2910&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/querydsl/querydsl/issues/2910&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/vYZvY/hyL4HfX1Ny/V7F1UC9zCW40i0xxPkQ9o1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Multiple Column In clause Parameter &amp;middot; Issue #2910 &amp;middot; querydsl/querydsl&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;@Entity @Data class Image { private long id; private long imgId; private int index; } //In service, select all that its index and imgId in a map HashMap&amp;lt;integer, long=&quot;&quot;&amp;gt; usedImageMap = ... List...&amp;lt;/integer,&amp;gt;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 글은 분명 나와 같은 질문에 답을 얻은 것으로 보이지만, 동작하지는 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 두 번째 삽질&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;github issue에서 다음과 같은 글을 찾았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/querydsl/querydsl/issues/2920&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/querydsl/querydsl/issues/2920&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1635092567138&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Queryng With In Clause And Static Values &amp;middot; Issue #2920 &amp;middot; querydsl/querydsl&quot; data-og-description=&quot;Observed vs. expected behavior Do the right query Steps to reproduce private List&amp;lt;Long&amp;gt; findRealizacaoAvaliacoes(final ResetAvaliacaoCommand cmd) { return baseRepository.findAll(qRealizacaoAv...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/querydsl/querydsl/issues/2920&quot; data-og-url=&quot;https://github.com/querydsl/querydsl/issues/2920&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c9cwHS/hyL4A81OQG/2OVXMdCCQiHHaV4dzz2SG0/img.png?width=1200&amp;amp;height=600&amp;amp;face=1003_112_1048_160&quot;&gt;&lt;a href=&quot;https://github.com/querydsl/querydsl/issues/2920&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/querydsl/querydsl/issues/2920&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c9cwHS/hyL4A81OQG/2OVXMdCCQiHHaV4dzz2SG0/img.png?width=1200&amp;amp;height=600&amp;amp;face=1003_112_1048_160');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Queryng With In Clause And Static Values &amp;middot; Issue #2920 &amp;middot; querydsl/querydsl&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Observed vs. expected behavior Do the right query Steps to reproduce private List findRealizacaoAvaliacoes(final ResetAvaliacaoCommand cmd) { return baseRepository.findAll(qRealizacaoAv...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요약하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QueryDSL에서 쿼리를 제너레이트 할 때, 우선순위에 따라 괄호가 매겨지는데 첫 번째 삽질과 같이 했을 경우에는 우선순위에 따라 첫 파라미터 묶음에 괄호가 생략되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해서는 아래와 같이 template을 이용하라는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1635092727909&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Override
    public List&amp;lt;Account&amp;gt; findAllCustom(List&amp;lt;SearchAccount&amp;gt; searchAccounts) {

        return query
                .selectFrom(account)
                .where(
                        Expressions.list(account.Email, account.loginId, account.name).in(searchAccountIn(searchAccounts))
                )
                .fetch();
    }

    private Expression[] searchAccountIn(List&amp;lt;SearchAccount&amp;gt; searchAccounts) {

        List&amp;lt;Expression&amp;gt; tuples = new ArrayList&amp;lt;&amp;gt;();

        for(SearchAccount searchAccount : searchAccounts) {
            tuples.add(Expressions.template(Object.class, &quot;({0}, {1}, {2})&quot;, searchAccount.getEmail(), searchAccount.getLoginId(), searchAccount.getName()));
        }

        return tuples.toArray(new Expression[0]);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;template을 이용하여 위와 같이 문자열로 지정해주면 정상적으로 쿼리가 나간다는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2208&quot; data-origin-height=&quot;61&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lIPAC/btriGkHfZSb/4WMUn8ZI2UBtHtO7Z0t5mK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lIPAC/btriGkHfZSb/4WMUn8ZI2UBtHtO7Z0t5mK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lIPAC/btriGkHfZSb/4WMUn8ZI2UBtHtO7Z0t5mK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlIPAC%2FbtriGkHfZSb%2F4WMUn8ZI2UBtHtO7Z0t5mK%2Fimg.png&quot; data-origin-width=&quot;2208&quot; data-origin-height=&quot;61&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 쿼리의 테스트 결과로 정상적으로 원하는 쿼리가 나가는 것을 확인할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 세 번째 삽질&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 뭔가 찝찝하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 삽질의 경우 단건 검색 결과의 경우 정상적으로 나가지만, 멀티 검색 결과의 경우 괄호가 잘못 들어가는 경우였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 두 번째 삽질에서의 코드를 단건으로 파라미터를 넘겨보면 어떻게 될까?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1853&quot; data-origin-height=&quot;191&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwWAz5/btriLRqxY6V/sjyMMklnpPVkjEIKgxtR1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwWAz5/btriLRqxY6V/sjyMMklnpPVkjEIKgxtR1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwWAz5/btriLRqxY6V/sjyMMklnpPVkjEIKgxtR1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwWAz5%2FbtriLRqxY6V%2FsjyMMklnpPVkjEIKgxtR1K%2Fimg.png&quot; data-origin-width=&quot;1853&quot; data-origin-height=&quot;191&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1635093242125&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select account0_.Account_Seq as account_1_0_,
       account0_.Email       as email2_0_,
       account0_.Login_Id
                             as login_id3_0_,
       account0_.Name        as name4_0_,
       account0_.Use_Yn      as use_yn5_0_
from Account account0_
where (account0_.Email, account0_.Login_Id, account0_.Name) in ('aaa@test.co.kr', 'aaa', '홍길동')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명 예상한 쿼리는 맞지만 쿼리 문법 오류가 나면서 실패한다. 왜일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지원하는 모든 RDMS를 살펴본 건 아니지만, 테스트한 MariaDB의 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단 건의 경우에도 무조건 부모 괄호가 하나 존재해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1635095285739&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select account0_.Account_Seq as account_1_0_,
       account0_.Email       as email2_0_,
       account0_.Login_Id
                             as login_id3_0_,
       account0_.Name        as name4_0_,
       account0_.Use_Yn      as use_yn5_0_
from Account account0_
where (account0_.Email, account0_.Login_Id, account0_.Name) in
      (('aaa@test.co.kr', 'aaa', '홍길동'))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 또 여기서 삽질을 하다가 어쨌든 쿼리를 template 문자열에 의해서 생성되는 것이니 템플릿에 괄호를 추가하면 되지 않을까 싶었다.&lt;/p&gt;
&lt;pre id=&quot;code_1635095398662&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private Expression[] searchAccountIn(List&amp;lt;SearchAccount&amp;gt; searchAccounts) {

        List&amp;lt;Expression&amp;gt; tuples = new ArrayList&amp;lt;&amp;gt;();

        for(SearchAccount searchAccount : searchAccounts) {
            tuples.add(Expressions.template(Object.class, &quot;(({0}, {1}, {2}))&quot;, searchAccount.getEmail(), searchAccount.getLoginId(), searchAccount.getName()));
        }

        return tuples.toArray(new Expression[0]);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별다른 것 없이, &lt;span style=&quot;color: #000000;&quot;&gt;&quot;({0}, {1}, {2})&quot; =&amp;gt; &lt;span style=&quot;color: #000000;&quot;&gt;&quot;(({0}, {1}, {2}))&quot;로 겉에 괄호를 하나 추가한 것으로 해결할 수 있었다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;더 효율적인 부분이 있을진 모르겠지만, 공식적으로 나와있는 사용법이나 글이 없는 것 같아 정리해보았다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Web/JPA &amp;amp; QueryDSL</category>
      <category>IN Clause</category>
      <category>Multi IN Clause</category>
      <category>QueryDSL</category>
      <category>RDMS</category>
      <author>알 수 없는 사용자</author>
      <guid isPermaLink="true">https://programmingnote.tistory.com/87</guid>
      <comments>https://programmingnote.tistory.com/87#entry87comment</comments>
      <pubDate>Mon, 25 Oct 2021 02:17:25 +0900</pubDate>
    </item>
    <item>
      <title>[QueryDSL &amp;amp; JPQL] Converter가 작동하지 않는 경우</title>
      <link>https://programmingnote.tistory.com/86</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;보통 JPA를 활용하여 개발할 때 데이터베이스에 Y, N 여부 값을 넣을 경우에는 boolean converter를 따로 만들어서 사용하곤 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 경우에 따라 버그(?)가 발생하는 경우가 있어 남긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CONFIG 테이블에 BooleanToYNConverter를 사용하는 칼럼이 있어, DB값이 Y라면 엔티티에는 true, 반대의 경우에는 false로 가져올 때, &amp;nbsp;해당 칼럼을 where 절로 쿼리를 select 할 때, case when 구문으로 true, false를 임의로 주면 파라미터 바인딩이 제대로 되지 않는 현상이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샘플 코드는 아래를 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1632134910148&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;CONFIG&quot;)
@Setter
public class Config {

    @Id
    @GeneratedValue
    @Column(name = &quot;Config_Seq&quot;)
    private Long configSeq;

    @Column(name = &quot;User_Seq&quot;)
    private Long userSeq;

    @Column(name = &quot;Apply_Date&quot;)
    private LocalDate applyDate;

    @Convert(converter = BooleanToYNConverter.class)
    @Column(name = &quot;Use_Yn&quot;)
    private Boolean useYn;

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Config엔티티는 위와 같이 구성되어있고, useYn 필드가 BooleanToYNConverter 컨버트를 사용하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1632134975252&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Converter
public class BooleanToYNConverter implements AttributeConverter&amp;lt;Boolean, String&amp;gt; {

    @Override
    public String convertToDatabaseColumn(Boolean attribute) {
        return attribute != null &amp;amp;&amp;amp; attribute ? &quot;Y&quot; : &quot;N&quot;;
    }

    @Override
    public Boolean convertToEntityAttribute(String s) {
        return &quot;Y&quot;.equalsIgnoreCase(s);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨버트는 무난하게 많이 사용하는 형태로 이상 없이 작성되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1632135837830&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;query
	.selectFrom(config)
	.where(config.useYn.eq(true))
	.fetch();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 위와 같은 쿼리를 querydsl로 날리면, 아래와 같이 Use_Yn = &amp;nbsp;'Y'로 컨버트 되어 쿼리가 실행되게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1012&quot; data-origin-height=&quot;296&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CEvgY/btrfpCyGFtQ/7DirhNShfqpe1UxRz7LwpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CEvgY/btrfpCyGFtQ/7DirhNShfqpe1UxRz7LwpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CEvgY/btrfpCyGFtQ/7DirhNShfqpe1UxRz7LwpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCEvgY%2FbtrfpCyGFtQ%2F7DirhNShfqpe1UxRz7LwpK%2Fimg.png&quot; data-origin-width=&quot;1012&quot; data-origin-height=&quot;296&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지는 정상적인 동작이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이제 &amp;nbsp;select절에 &amp;nbsp;case when을 써보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1027&quot; data-origin-height=&quot;900&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kXc3c/btrftosFPNc/E2HHkX1yrtgSs3AKFQLqhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kXc3c/btrftosFPNc/E2HHkX1yrtgSs3AKFQLqhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kXc3c/btrftosFPNc/E2HHkX1yrtgSs3AKFQLqhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkXc3c%2FbtrftosFPNc%2FE2HHkX1yrtgSs3AKFQLqhK%2Fimg.png&quot; data-origin-width=&quot;1027&quot; data-origin-height=&quot;900&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건문에 파라미터가 true로 들어가는 현상이 나타난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시라도 로깅에만 그렇게 찍히는 건지 useYn을 false로 바꿔보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;880&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beu9hN/btrfA2aSEHZ/ShYTe2Ew0mv3HRhCG6Asok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beu9hN/btrfA2aSEHZ/ShYTe2Ew0mv3HRhCG6Asok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beu9hN/btrfA2aSEHZ/ShYTe2Ew0mv3HRhCG6Asok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbeu9hN%2FbtrfA2aSEHZ%2FShYTe2Ew0mv3HRhCG6Asok%2Fimg.png&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;880&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건문을 바꿔 실행해보면 컨버터가 잘 실행된 상태로 N 값으로 쿼리가 나간다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 처음 이 문제를 보았을 때, 완전 갈피를 못 잡았었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;true일 때만 발생하는 현상으로, querydsl부터 실제 쿼리가 나갈 때 까지 디버깅을 태워본 결과 컨버터가 적용 안된다 라고만 알 수 있었고, 정확히 어떤 원인인지는 파악하지 못했다.(아직 부족한 게 많다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;암튼.. 어떻게든 이 문제를 해결하기 위해 별에 별짓을 다해본 결과, case builder에서 true로 결과를 가져올 때, 발생하는 현상이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1036&quot; data-origin-height=&quot;960&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwCtsU/btrfqD4YftB/DxBo3xMmq9MaLkU73RcEjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwCtsU/btrfqD4YftB/DxBo3xMmq9MaLkU73RcEjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwCtsU/btrfqD4YftB/DxBo3xMmq9MaLkU73RcEjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwCtsU%2FbtrfqD4YftB%2FDxBo3xMmq9MaLkU73RcEjk%2Fimg.png&quot; data-origin-width=&quot;1036&quot; data-origin-height=&quot;960&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무 설정 바꾸지 않은 채로, case builder에서 true만 false로 바꾸었더니 정상적으로 파라미터가 컨버터가 적용되어 Y값으로 바인딩되어 쿼리가 실행되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 관련하여 &amp;nbsp;querydsl에 이슈를 남겨보았으나 기존에도 있던 이슈로 답변이 왔다. JPQL의 문제다 라고 말하는 것처럼 보이는데, 확인하라고 알려준 이슈내용이 어떤 건지 모르겠어서 정확히 파악이 안 되었다 ㅠㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 이슈 : &lt;a href=&quot;https://github.com/querydsl/querydsl/issues/3017&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/querydsl/querydsl/issues/3017&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1632137527053&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Querydsl parameter binding problem with @Convert Field &amp;middot; Issue #3017 &amp;middot; querydsl/querydsl&quot; data-og-description=&quot;Observed vs. expected behavior When the field to which @convert is applied is used as a search condition, if true is returned to the casebuilder statement in select, the @convert is not applied to ...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/querydsl/querydsl/issues/3017&quot; data-og-url=&quot;https://github.com/querydsl/querydsl/issues/3017&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b5Tp52/hyLFEbzPXY/M8MiSJfhcUMZuzKGZyfVF1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/querydsl/querydsl/issues/3017&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/querydsl/querydsl/issues/3017&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b5Tp52/hyLFEbzPXY/M8MiSJfhcUMZuzKGZyfVF1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Querydsl parameter binding problem with @Convert Field &amp;middot; Issue #3017 &amp;middot; querydsl/querydsl&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Observed vs. expected behavior When the field to which @convert is applied is used as a search condition, if true is returned to the casebuilder statement in select, the @convert is not applied to ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;querydsl도 결국 JPQL을 기반으로 동작한다고 알고 있지만.. 이런 경우에도 JPQL의 영향인건지는 잘 이해가지 않는다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 테스트한 환경은 4.2.1버전이였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특이한 건 4.3.0~ 4.4.0 버전까지는 같은 코드로 아예 쿼리 자체가 실행되지 않는 오류가 뜨고, 가장 최신 버전은 5.0.0 버전에서는 컨버터가 정상적으로 적용되고 있는 것으로 확인된다.&lt;/p&gt;</description>
      <category>Web/JPA &amp;amp; QueryDSL</category>
      <category>CONVERTER</category>
      <category>QueryDSL</category>
      <author>알 수 없는 사용자</author>
      <guid isPermaLink="true">https://programmingnote.tistory.com/86</guid>
      <comments>https://programmingnote.tistory.com/86#entry86comment</comments>
      <pubDate>Mon, 20 Sep 2021 20:53:05 +0900</pubDate>
    </item>
    <item>
      <title>[QueryDSL] from, join 절에 서브쿼리 사용하기</title>
      <link>https://programmingnote.tistory.com/85</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://programmingnote.tistory.com/84&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 포스팅&lt;/a&gt;에서 한 테이블 내에서 특정 값을 그룹핑하여 최댓값을 가진 row를 가진 방법을 보았었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 Row_Number를 사용하여 추출한 데이터가 가장 빠른 방법으로 보였는데, 이는 서브 쿼리로 해야 한다는 점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPQL?hibernate?에 의해 where절과 select절에서만 서브 쿼리가 사용 가능하다고 한다..(내부적인 원리는 나중에 따로 공부해봐야겠다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 글 : &lt;a href=&quot;https://stackoverflow.com/questions/60258377/querydsl-4-x-subqueries-in-the-from-clause&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://stackoverflow.com/questions/60258377/querydsl-4-x-subqueries-in-the-from-clause&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1631344905735&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;QueryDSL 4.x subqueries in the FROM clause&quot; data-og-description=&quot;Are subqueries in the from clause supported in the latest QueryDSL version? I tried my best but could not find a solution for now.&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/60258377/querydsl-4-x-subqueries-in-the-from-clause&quot; data-og-url=&quot;https://stackoverflow.com/questions/60258377/querydsl-4-x-subqueries-in-the-from-clause&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/hKU7s/hyLyYoFodK/s2qoYxlIIl4tKjH0DJwUn1/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/60258377/querydsl-4-x-subqueries-in-the-from-clause&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/60258377/querydsl-4-x-subqueries-in-the-from-clause&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/hKU7s/hyLyYoFodK/s2qoYxlIIl4tKjH0DJwUn1/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;QueryDSL 4.x subqueries in the FROM clause&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Are subqueries in the from clause supported in the latest QueryDSL version? I tried my best but could not find a solution for now.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㅇㅏ무튼... 어떻게든 querydsl에서 서브 쿼리를 사용해서 위 케이스를 풀어낼 수 없을까 생각하다가.. 엔티티 레벨(?)에서 설정할 수 있는 법이 없을까 고민하다가 우연히 봤었던 @SubSelect 옵션이 생각났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 글 : &lt;a href=&quot;https://stackoverflow.com/questions/25226244/what-is-the-use-of-synchronize-in-hibernate&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://stackoverflow.com/questions/25226244/what-is-the-use-of-synchronize-in-hibernate&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1631345733737&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;What is the use of @Synchronize in Hibernate&quot; data-og-description=&quot;As per Hibernate documentaion, There is no difference between a view and a base table for a Hibernate mapping. This is transparent at the database level, although some DBMS do not support vi...&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/25226244/what-is-the-use-of-synchronize-in-hibernate&quot; data-og-url=&quot;https://stackoverflow.com/questions/25226244/what-is-the-use-of-synchronize-in-hibernate&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/z3dUA/hyLyR4cNGB/196hIIu0jdFjljbFh22D10/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/25226244/what-is-the-use-of-synchronize-in-hibernate&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/25226244/what-is-the-use-of-synchronize-in-hibernate&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/z3dUA/hyLyR4cNGB/196hIIu0jdFjljbFh22D10/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;What is the use of @Synchronize in Hibernate&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;As per Hibernate documentaion, There is no difference between a view and a base table for a Hibernate mapping. This is transparent at the database level, although some DBMS do not support vi...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마치 view처럼 사용할 수 있는 entity를 만드는 걸로 entity에 맵핑할 쿼리를 직접 작성하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1631348302878&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * Map an immutable and read-only entity to a given SQL select expression.
 *
 * @see Synchronize
 *
 * @author Sharath Reddy
 */
@Target(TYPE)
@Retention(RUNTIME)
public @interface Subselect {
	/**
	 * The query.
	 */
	String value();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주석의 내용과 같이 update될 수 없는 select용 엔티티이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1631346195530&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Subselect(
    &quot;select &quot; +
    &quot;       ROW_NUMBER() over (PARTITION BY c.User_Seq ORDER BY Apply_Date desc, Config_Seq desc) as Row_Num, &quot; +
    &quot;       c.Config_Seq, &quot; +
    &quot;       c.User_Seq, &quot; +
    &quot;       c.Apply_Date &quot; +
    &quot;from CONFIG c &quot;
)
@Immutable
@Synchronize(&quot;CONFIG&quot;)
public class ConfigSubSelect {

    @Column(name = &quot;Row_Num&quot;)
    private Long rowNum;

    @Id
    @GeneratedValue
    @Column(name = &quot;Config_Seq&quot;)
    private Long configSeq;

    @Column(name = &quot;User_Seq&quot;)
    private Long userSeq;

    @Column(name = &quot;Apply_Date&quot;)
    private LocalDate applyDate;

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 테이블 단위가 아닌 subselect에 작성된 query로 결과가 엔티티에 맵핑되게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 엔티티를 똑같이 querydsl에서 사용해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1631346401354&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;query
	.selectFrom(configSubSelect)
	.where(configSubSelect.rowNum.eq(1L))
	.fetch();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2213&quot; data-origin-height=&quot;46&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byzjMh/btreLKo3ApW/keUDvUHve7xODjixGVHgDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byzjMh/btreLKo3ApW/keUDvUHve7xODjixGVHgDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byzjMh/btreLKo3ApW/keUDvUHve7xODjixGVHgDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyzjMh%2FbtreLKo3ApW%2FkeUDvUHve7xODjixGVHgDk%2Fimg.png&quot; data-origin-width=&quot;2213&quot; data-origin-height=&quot;46&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1631346571700&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select configsubs0_.Config_Seq as config_s1_0_,
       configsubs0_.Apply_Date as apply_da2_0_,
       configsubs0_.Row_Num    as row_num3_0_,
       configsubs0_.User_Seq   as user_seq4_0_
from (select ROW_NUMBER() over (PARTITION BY c.User_Seq ORDER BY Apply_Date desc, Config_Seq desc) as Row_Num,
             c.Config_Seq,
             c.User_Seq,
             c.Apply_Date
      from CONFIG c) configsubs0_
where configsubs0_.Row_Num = ?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행결과의 query를 보면 서브 쿼리로 원하는 서브 쿼리가 나간 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;querydsl은 jpql기반이기 때문에, JPQL 마찬가지로 동작될 것으로 예상되고,&amp;nbsp;spring data jpa에서 제공하는 find~ 메서드 또한 동일하게 사용 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론, update나 insert는 불가능하고, 엔티티 파일을 하나 더 만들어야 하지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스적으로 서브 쿼리를 써야만 하는 테이블이라면 다른 곳에서도 똑같이 쓸 확률이 있기 때문에 만들어놓고 공통으로 사용해도 괜찮을 것 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Web/JPA &amp;amp; QueryDSL</category>
      <category>@subquery</category>
      <category>@subselect</category>
      <category>join subquery</category>
      <category>QueryDSL</category>
      <author>알 수 없는 사용자</author>
      <guid isPermaLink="true">https://programmingnote.tistory.com/85</guid>
      <comments>https://programmingnote.tistory.com/85#entry85comment</comments>
      <pubDate>Sat, 11 Sep 2021 17:22:24 +0900</pubDate>
    </item>
    <item>
      <title>테이블 내에서 group by의 최대값을 가진 row 검색하는 방법 + 속도체크</title>
      <link>https://programmingnote.tistory.com/84</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 동일 테이블에서 그룹핑하여 최대값을 가진 row 데이터를 리스트에 출력하는 업무를 맡았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 효율적인 쿼리가 무엇일까 고민하다가 생각해낸 두가지 방법과 실제로 성능이 어떤지 테스트해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항은 다음과 같다.(실제 업무와 비슷하게 예시로 들었다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 회원에게 가장 최근에 적용된 권한이 어떤 권한인지 검색한다. 그 권한은 시스템 관리자가 설정한 적용날짜에 따라 적용되고, 적용날짜는 하나의 테이블에 회원 일련번호와 적용날짜, 권한정보 하나의 로우로 로그데이터와 같이 insert만 해놓은 테이블(update 없음)에서 회원에게 설정된 가장 최근 적용날짜, 적용날짜가 같은 정보가 있다면 가장 최근에 등록된 데이터에서 권한정보를 가져와야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비유를 들려하니 내가 봐도 뭔소리인지 모르겠고... 애초에 이렇게 설계되지 않았으면 좋았겠지만, 이미 있는 구조에서 개발들어가야하는 상황으로 극단적 상황으로 보자. 여기서 비지니스는 중요한 것이 아니기에 예시 테이블을 봐보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1630839022631&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;create table CONFIG (
    Config_Seq INT AUTO_INCREMENT PRIMARY KEY, -- pk
    User_Seq INT, -- 회원 일련정보
    Apply_Date DATE -- 적용날짜
    -- 권한정보 칼럼 등..
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CONFIG 테이블에서 User_Seq를 그룹핑하여 Apply_Date가 가장 최근이고, 그 중에서 Config_Seq가 가장 최근에 등록된 ROW를 출력해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단건 쿼리로는 다음과 같이 될 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1630839223922&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select *
from CONFIG
where User_Seq = 10
order by Apply_Date desc, Config_Seq desc
limit 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만.. 얘를 리스트로 뽑아내려니 머리가 아파지기 시작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법이 안떠오르는 것은 아니였지만, 어떤게 효율이 좋을지 궁금해지기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫번째로는 무지성으로 테이블을 여러번 조인 시키는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1630839473080&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select c1.*
from CONFIG c1
left join CONFIG c2
on c1.User_Seq = c2.User_Seq and c1.Apply_Date &amp;lt; c2.Apply_Date
left join CONFIG c3
on c1.User_Seq = c3.User_Seq and c1.Apply_Date = c3.Apply_Date and c1.Config_Seq &amp;lt; c3.Config_Seq
where c2.Config_Seq is null
and c3.Config_Seq is null;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건에 따라 검색할 테이블(c1), 날짜조건 테이블(c2), 최근 등록된 데이터 조건 테이블(c3)를 두어 c2,c3에 없는 테이블을 검색한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;두번째 방법은 ROW_NUMBER를 이용한다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1630839571816&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select *
from
(
    select
        ROW_NUMBER() over (PARTITION BY c.User_Seq ORDER BY Apply_Date desc, Config_Seq desc) as Row_Num,
        c.*
    from CONFIG c
) as config
where config.Row_Num = 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브쿼리로 한번 감싸줘야하지만, 테이블을 한번만 보고 뽑아낼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 그냥 대충만 봐도 두번째 방법이 가독성도 좋고 쿼리 속도에서도 훨씬 빠를 것으로 예상되지만, 얼마나 차이날지 테스트해보았다.&lt;/p&gt;
&lt;pre id=&quot;code_1630839816249&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE PROCEDURE dorepeat(p1 INT, apply_date DATE)
BEGIN
    SET @x =0;

    REPEAT
    SET @x = @x+1;

    INSERT INTO CONFIG (User_Seq, Apply_Date)
    VALUES (@x, apply_date);
    UNTIL @x &amp;gt; p1

    END REPEAT;
END;

call dorepeat(999, '2021-08-01');
call dorepeat(999, '2021-09-01');
call dorepeat(999, '2021-09-01');
call dorepeat(999, '2021-07-01');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로시저로 간단하게 4000건의 데이터만 만들어서 테스트해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1630840141320&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;gt; select c1.*
      from CONFIG c1
      left join CONFIG c2
      on c1.User_Seq = c2.User_Seq and c1.Apply_Date &amp;lt; c2.Apply_Date
      left join CONFIG c3
      on c1.User_Seq = c3.User_Seq and c1.Apply_Date = c3.Apply_Date and c1.Config_Seq &amp;lt; c3.Config_Seq
      where c2.Config_Seq is null
      and c3.Config_Seq is null
[2021-09-05 20:08:17] 1,000 rows retrieved starting from 1 in 2 s 492 ms (execution: 2 s 334 ms, fetching: 158 ms)

&amp;gt; select *
      from
      (
          select
              ROW_NUMBER() over (PARTITION BY c.User_Seq ORDER BY Apply_Date desc, Config_Seq desc) as Row_Num,
              c.*
          from CONFIG c
      ) as config
      where config.Row_Num = 1
[2021-09-05 20:08:17] 1,000 rows retrieved starting from 1 in 28 ms (execution: 10 ms, fetching: 18 ms)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론.. 인덱스도 없는 완전 기본형 테이블이지만, 4천건의 데이터밖에 없는데 생각보다 더 차이가 많이 난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무지성 테이블 조인의 경우 요구사항 조건이 늘어날 수록 조건의 개수에 따라 테이블 조인이 더 많아지고, 테이블의 데이터 개수에 따라 속도가 훨씬 영향을 많이 받는다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 ROW_NUMBER의 경우 조건이 늘어나도 ORDER BY에 조건만 추가하면 되기에 무지성 테이블 조인은 방법이 있다는 것만 알아두는게 좋을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 뻔한 결과를 테스트까지 한 이유는.. &amp;nbsp;ORM에서 서브쿼리를 사용하는게 거부감이 들어(특히 QueryDSL) 어떻게든 서브쿼리를 사용하지 않고 해보려고 무지성 테이블과 비교까지 했는데 성능이슈가 무조건 날 것으로 보인다 ㅎ..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 다음에는 QueryDSL에서 어떻게 join절에 해당 쿼리를 녹여 적용했는지 포스팅하겠다.(&lt;a href=&quot;https://programmingnote.tistory.com/85&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://programmingnote.tistory.com/85&lt;/a&gt;)&lt;/p&gt;</description>
      <category>SQL</category>
      <category>group by</category>
      <category>row</category>
      <category>SQL</category>
      <category>최대값</category>
      <author>알 수 없는 사용자</author>
      <guid isPermaLink="true">https://programmingnote.tistory.com/84</guid>
      <comments>https://programmingnote.tistory.com/84#entry84comment</comments>
      <pubDate>Sun, 5 Sep 2021 20:18:40 +0900</pubDate>
    </item>
    <item>
      <title>클라이언트에서 html 태그 속성 조작</title>
      <link>https://programmingnote.tistory.com/83</link>
      <description>&lt;h2&gt;0. 시작&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt; 앞에서 살펴본 jwplayer 이슈 중에 video태그에 controls를 클라이언트가 적접 조작하여 커스텀된 컨트롤바가 아닌 기본 컨트롤바를 띄울 수 있다. 이럴 때 어떤 사이드 이슈가 발생할지 예상할 수 없기 떄문에, 어떻게 대처할지 생각해보았다.   &lt;/p&gt;
&lt;h2&gt;1. 분석&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;아래 사진은 넷플릭스 인터렉티브 비디오 &amp;quot;블랙미러 : 밴더너내치&amp;quot;에서 조작한 사진이다.&lt;br&gt;video태그를 찾아 controls 속성을 강제로 넣으면 빨간색 부분처럼 기본으로 제공되는 컨트롤바가 별개로 뜨면서 영상의 길이와 같은 정보가 노출된다. 평상시에는 중요하지 않은 이슈이기만 저렇게 인터렉티브 비디오일 경우 클라이언트에서 조작하여 원하는 영상 위치로 이동시킬 수 있지 않나싶다. &lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BkIFN/btrcXjTHMoE/GnUehk85tWZu7OsnWH3ack/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BkIFN/btrcXjTHMoE/GnUehk85tWZu7OsnWH3ack/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BkIFN/btrcXjTHMoE/GnUehk85tWZu7OsnWH3ack/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBkIFN%2FbtrcXjTHMoE%2FGnUehk85tWZu7OsnWH3ack%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
   &lt;/p&gt;
&lt;p&gt;저런 기본 컨트롤바를 꼭 막아야하는 경우 어떻게 해야할까의 나의 답은 controls 속성이 추가될 경우 특정 이벤트를 발생시켜 비디오를 팅기는게 어떨까 생각했다.   &lt;/p&gt;
&lt;h2&gt;2. 해결&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;특정 DOM 변경을 감시하고, 변경될 떄 이벤트처럼 callback 함수로 동작되는 javascript api이다.   &lt;/p&gt;
&lt;p&gt;사용법자체는 간단하기 떄문에 아래 샘플 코드를 보면 이해하기 쉽다.&lt;br&gt;아래 예제는 위에서 예로 들었던 video 태그에 controls속성이 추가되었을 때, alert을 띄우고 비디오 영상 주소를 지워 영상을 재생시킬 수 없게 하는 방식이다. &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;if(window.MutationObserver || window.WebKitMutationObserver) {
  videoMutationObserver();
}



function videoMutationObserver() {

  var nodeToObserve = document.querySelector(&amp;#39;video&amp;#39;);
  var callback = function(mutations, observer) {
    for (var index = 0; index &amp;lt; mutations.length; index) {
      var mutation = mutations[index];
      if (mutation.type === &amp;#39;attributes&amp;#39; &amp;amp;&amp;amp; mutation.attributeName == &amp;#39;controls&amp;#39; ) {
        alert(&amp;#39;비정상적인 동작입니다.&amp;#39;);
        mutation.target.setAttribute(&amp;#39;src&amp;#39;, &amp;#39;&amp;#39;);
        break;                          
      }
    }
    observer.disconnect();
  }
  const observer = new MutationObserver(callback);

  observer.observe(nodeToObserve, {
    attributes : true,
    attributeFilter : [&amp;#39;controls&amp;#39;],
    childList: false, // target node&amp;#39;s children
    subtree: false // target node&amp;#39;s descendants
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 샘플 예제를 실행했을 때 모습이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSBDlH/btrcUjTU6Bt/HlkhS4S75cLlQVZKrRXVUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSBDlH/btrcUjTU6Bt/HlkhS4S75cLlQVZKrRXVUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSBDlH/btrcUjTU6Bt/HlkhS4S75cLlQVZKrRXVUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSBDlH%2FbtrcUjTU6Bt%2FHlkhS4S75cLlQVZKrRXVUk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
   &lt;/p&gt;
&lt;p&gt;ie브라우저 낮은 버전에서만 잘 체크되면 관련된 이슈를 손쉽게 처리할 수 있을 것 같다.&lt;/p&gt;</description>
      <category>실무 이슈</category>
      <author>알 수 없는 사용자</author>
      <guid isPermaLink="true">https://programmingnote.tistory.com/83</guid>
      <comments>https://programmingnote.tistory.com/83#entry83comment</comments>
      <pubDate>Sun, 22 Aug 2021 20:48:28 +0900</pubDate>
    </item>
    <item>
      <title>HTMLMediaElement.playbackRate 조작이슈</title>
      <link>https://programmingnote.tistory.com/82</link>
      <description>&lt;h2&gt;0. 시작&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;이전까지는 jwplayer 버전에 따른 원인 분석과 해결방법을 알아보았다면,&lt;br&gt;이제는 jwplayer 버전은 유지하고, 추가 소스코드로만 해결할 수 있는 방법에 대한 생각을 정리할 것이다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;배속 이벤트 이슈&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;크롬 확장프로그램으로 비디오 조절기능을 사용시 배속 임의 조작&lt;/li&gt;
&lt;li&gt;HTMLMediaElement.playbackRate 클라이언트(브라우저 관리자 도구) 조작&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. 원인&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;구글링을 하던 도중 관리자도구에서 비디오의 속성값을 임의로 변경하여 클라이언트가 배속을 임의로 설정하는 경우를 발견하였다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;document.getElementById(&amp;quot;video&amp;quot;).querySelector(&amp;quot;video&amp;quot;).playbackRate = 10&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 외에도 크롬 확장프로그램에서 배속을 바꿀시 소스에 걸린 이벤트가 정상적으로 동작하지 않고 있었다.&lt;br&gt;이 부분은 jwplayer의 버전 문제인지는 모르겠지만, 위처럼 로우레벨(?)에서 클라이언트가 속성값 자체를 변경할 시 어떻게 대처해야할까에 대해 고민했다.&lt;/p&gt;
&lt;h2&gt;2. 해결&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;video태그에서 배속을 정하는 값은 아래 링크의 공식문서와 같다.&lt;br&gt;(&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/playbackRate&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/playbackRate&lt;/a&gt;)&lt;br&gt;아마 jwplayer이벤트에서도 결국엔 해당 값을 변경하는 것으로 배속을 제어할 것이라 생각했고, 이 값이 의도치않게 변경되는 것을 감지 및 방지하는 것에 대해 생각했다.&lt;/p&gt;
&lt;p&gt;내가 생각한 방법은 자바스크립트의 프로퍼티 타입 정의 함수이다.&lt;br&gt;(&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty&quot;&gt;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;본 글은 함수에 대한 내용을 다루기 위한 내용은 아니기 떄문에, 간력히 설명하면 속성에 값을 대입하거나 읽을 때, 특정 로직이 실행되게 하는 get,set를 정의할 수 있고, 프로퍼티 값을 변경유무 등을 설정할 수 있는 함수이다.&lt;/p&gt;
&lt;p&gt;프로퍼티 설정 중 set에 로직을 별도로 정의하여, 몇 배속까지 사용할 수 있는지를 설정하였다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 배속조절 막기

// 변경 불가능하게 const 처리
const maxPlayBackRate = 5;
document.addEventListener(&amp;quot;DOMContentLoaded&amp;quot;, function(){
  var originalDescriptor = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, &amp;#39;playbackRate&amp;#39;);

  Object.defineProperty(HTMLVideoElement.prototype,&amp;#39;playbackRate&amp;#39;,{
    set:function(value){
      if(value &amp;gt; maxPlayBackRate) {
        console.log( maxPlayBackRate + &amp;quot;보다 커서 적용안함&amp;quot;)
        return;
      } 
      originalDescriptor.set.call(this,value);
    },
    get:originalDescriptor.get
  });

})&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;샘플 코드로 maxPlayBackRate이상의 배속설정 값이 들어오면 설정되지 않도록 하는 코드이다.&lt;br&gt;테스트를 위해 클라이언트에서 값을 임의로 조정하면 아래와 같이 출력되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;943&quot; data-origin-height=&quot;390&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgZyLn/btrb9G37A4x/73eKwMfEbGalrjIUmZWIPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgZyLn/btrb9G37A4x/73eKwMfEbGalrjIUmZWIPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgZyLn/btrb9G37A4x/73eKwMfEbGalrjIUmZWIPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgZyLn%2Fbtrb9G37A4x%2F73eKwMfEbGalrjIUmZWIPK%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;943&quot; data-origin-height=&quot;390&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;보통 이렇게 처리를 하면, 클라이언트에서 임의로 바꿔도 시스템에서 허용가능한 범위까지만 배속을 설정할 수 있게 하는 것이기 때문에 상관없다.&lt;br&gt;하지만, 특수한 경우에 클라이언트 관리자모드에서는 동작하지 않고, 실제 소스동작에서만 작동해야하는 경우가 있다면 caller를 통해서 가능할 것 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const maxPlayBackRate = 5;
document.addEventListener(&amp;quot;DOMContentLoaded&amp;quot;, function(){
  var originalDescriptor = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, &amp;#39;playbackRate&amp;#39;);

  Object.defineProperty(HTMLVideoElement.prototype,&amp;#39;playbackRate&amp;#39;,{
    set:function(value){
      console.log(&amp;quot;caller name : &amp;quot; + arguments.callee.caller.name); // 호출 함수 체크
      if(value &amp;gt; maxPlayBackRate) {
        console.log( maxPlayBackRate + &amp;quot;보다 커서 적용안함&amp;quot;)
        return;
      } 
      originalDescriptor.set.call(this,value);
    },
    get:originalDescriptor.get
  });

})

function changePlayBackRate(speed) {
  document.querySelector(&amp;#39;video&amp;#39;).playbackRate = speed;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위 코드를 보면 알겠지만, 배속을 바꿀 수 있는 함수를 별도로 정의하고, playbackRate를 변경할 때 어디서 호출했는지 체크할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(&amp;quot;caller name : &amp;quot; + arguments.callee.caller.name); // 호출 함수 체크&lt;/code&gt;&lt;/pre&gt;&lt;ol&gt;
&lt;li&gt;&lt;p&gt;changePlayBackRate 함수 호출 시&lt;br&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cW1WqP/btrci9YTaC3/TjMch6qVk4O9YhbIHTBCA1/img.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;관리자도구에서 속성 값 변경 시&lt;br&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S2Tbj/btrb9Rrsf4N/ErVSlTYXARWxYpxKNTPtJK/img.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;jwplayer onReady 이벤트 시&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dDdDdX/btrclHOAHMS/KaIKIXZSq6omEZgmOukFKk/img.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;jwplayer(&amp;#39;video&amp;#39;).onReady(function() { document.querySelector(&amp;#39;video&amp;#39;).playbackRate = 3; })&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;물론 관리자도구에서 변경했다는 함수 이름이 없다는 것이기 때문에, 함수가 아닌 곳에서 진짜 호출했을 수도 있다.&lt;br&gt;이 부분만 잘 체크하여 사용한다면 혹시 모를 케이스에 대해서도 보장되는 안정적인 시스템이 구축될 것 같다.&lt;/p&gt;
&lt;p&gt;+) Object.defineProperty에 대해 찾아보면 알겠지만, 이 함수자체를 관리자도구에서 재정의하더라도 configurable 옵션떄문에 클라이언트가 재정의할 수 없다.&lt;/p&gt;</description>
      <category>실무 이슈</category>
      <category>defineProperty</category>
      <category>jwplayer</category>
      <category>playbackRate</category>
      <category>관리자 도구</category>
      <author>알 수 없는 사용자</author>
      <guid isPermaLink="true">https://programmingnote.tistory.com/82</guid>
      <comments>https://programmingnote.tistory.com/82#entry82comment</comments>
      <pubDate>Mon, 16 Aug 2021 17:06:06 +0900</pubDate>
    </item>
    <item>
      <title>jwplayer 버전 이슈(3) - onSeek, on('seek')</title>
      <link>https://programmingnote.tistory.com/81</link>
      <description>&lt;h2&gt;0. 시작&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;&lt;a href=&quot;https://programmingnote.tistory.com/79&quot;&gt;이슈리스트 1. pip 예시&lt;/a&gt;에서 본 것과 같이 여러 브라우저에서 jwplayer의 onSeek가 작동하지 않는 몇몇 케이스가 존재한다. 내가 발견한 케이스는 모두 5개로 아래 리스트와 같다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;seek 이벤트 이슈&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;firefox에서 기본으로 제공하는 pip기능에서 onSeek이벤트 미작동&lt;/li&gt;
&lt;li&gt;ios 모바일 safari web에서 video가 사파리 전용 플레이어로 실행됨과 동시에 onSeek이벤트 미작동&lt;/li&gt;
&lt;li&gt;클라이언트 브라우저 관리자모드에서 controls속성 추가시 onSeek이벤트 미작동&lt;/li&gt;
&lt;li&gt;맥북, safari에서 맥북에서 지원하는 터치바에서 동영상 이동시 onSeek이벤트 미작동&lt;/li&gt;
&lt;li&gt;크롬 확장프로그램으로 비디오 조절기능을 사용시 동영상 컨트롤바 제어 불가능&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. 원인파악&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;위 이슈들의 공통점으로 브라우저 상에서 발생하는 문제가 아닌 브라우저 이외의 프로세스나 하드웨어 자체에서 발생하는 이슈란 걸 알 수 있다.&lt;br&gt;그렇기 떄문에, 나는 처음에 웹 밖에서 일어나는 문제에서 발생하니깐 개발자가 어떻게 할 수 없는 영역의(?) 문제이지 않을까 생각했다.&lt;br&gt;이를 확인하기 위한 방법으로 jwplayer에서 제공하는 onSeek 이벤트가 아닌 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/seeking_event&quot;&gt;자바스크립트 자체 API - seeking&lt;/a&gt;, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/seeked_event&quot;&gt;자바스크립트 자체 API - seeked&lt;/a&gt;에서 제공하는 이벤트를 통해 테스트하였고, 이 떄는 정상적으로 동작함을 확인했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const video = document.querySelector(&amp;#39;video&amp;#39;);

video.addEventListener(&amp;#39;seeked&amp;#39;, (event) =&amp;gt; {
  console.log(&amp;#39;Video found the playback position it was looking for.&amp;#39;);
});&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이는 곧, jwplayer에서 제공하는 onSeek의 문제임을 확인할 수 있었고, 그럼 이 문제를 어떻게 해결할 수 있을지 찾기 시작했다.&lt;/p&gt;
&lt;h2&gt;2. 해결&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;jwplayer 공식문서를 뒤지기 시작했고, 여기서 관련된 내용을 볼 수 있었다.&lt;br&gt;(&lt;a href=&quot;https://support.jwplayer.com/articles/jw7-to-jw8-migration-reference&quot;&gt;https://support.jwplayer.com/articles/jw7-to-jw8-migration-reference&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;744&quot; data-origin-height=&quot;100&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7QhwX/btrbAAJCATO/GHsP8v3MXyfOnz12jKS5u0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7QhwX/btrbAAJCATO/GHsP8v3MXyfOnz12jKS5u0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7QhwX/btrbAAJCATO/GHsP8v3MXyfOnz12jKS5u0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7QhwX%2FbtrbAAJCATO%2FGHsP8v3MXyfOnz12jKS5u0%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;744&quot; data-origin-height=&quot;100&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;위 글에서 볼 수 있듯이 jwplayer 7부터는 onSeek 이벤트가 deprecated 되었고, on(&amp;#39;seek&amp;#39;) 형태의 이벤트로 바뀌었다는 것이다.&lt;br&gt;deprecated된 이유는 정확히 나와있지 않지만 onSeek 이벤트에 문제가 있고, 그렇기 떄문에 on(&amp;#39;seek&amp;#39;) 이벤트로 대체된 것이구나 생각했다.&lt;/p&gt;
&lt;p&gt;하여튼, 내가 사용했던 jwplayer는 6버전으로 7버전과 비교하여 테스트를 시작했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;html&amp;gt;
    &amp;lt;script type=&amp;quot;text/javascript&amp;quot; src=&amp;quot;jwplayer.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;body&amp;gt;
        &amp;lt;div id=&amp;quot;video&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;/body&amp;gt;
    &amp;lt;script&amp;gt;
        jwplayer(&amp;#39;video&amp;#39;).setup({
            file: &amp;quot;16_04.mp4&amp;quot;
        });

        // (1) jwplayer 6
        jwplayer(&amp;#39;video&amp;#39;).onSeek(function(){
            console.log(&amp;#39;onSeek&amp;#39;);
        })
        // (2) jwplayer 7 이상
        jwplayer(&amp;#39;video&amp;#39;).on(&amp;#39;seek&amp;#39;, function() {
            console.log(&amp;#39;onSeek&amp;#39;)
        })
    &amp;lt;/script&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;정말 간단하게 로컬에 영상을 넣어서 이벤트가 정상적으로 동작하는지 테스트하였고, jwplayer는 총 3가지 버전으로 테스트하였다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;6.12.4956 version&lt;br&gt;-&amp;gt; onSeek 이벤트만 존재하는 버전으로 위 이슈들이 나타난다.&lt;/li&gt;
&lt;li&gt;7.12.0 version&lt;br&gt;-&amp;gt; onSeek, on(&amp;#39;seek&amp;#39;) 이벤트가 둘 다 존재하나 두개의 이벤트에서 모두 같은 증상이 나타난다.&lt;/li&gt;
&lt;li&gt;7.12.13 version&lt;br&gt;-&amp;gt; onSeek, on(&amp;#39;seek&amp;#39;) 이벤트가 둘 다 존재하고, 두개 이벤트 모두 위 이슈사항들이 나타나지 않는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;예측이 절반만 맞은 테스트 결과이다. 7버전 이후부터는 on(&amp;#39;seek&amp;#39;) 이벤트가 별도로 존재했지만, 3번 버전에서만 이슈들이 해결되었다.&lt;br&gt;&lt;a href=&quot;https://releases.jwplayer.com/jw-player/&quot;&gt;jwplayer의 릴리즈 노트&lt;/a&gt;를 살펴보았지만, 내용도 너무 많고 대충 살펴보아도 관련한 내용이 보이지 않아 뚜렷한 원인은 찾지 못했다.&lt;/p&gt;
&lt;p&gt;아무튼 해결법은 찾았고, jwplayer 6 버전을 사용하는 시스템에서 위 문제를 해결하기 위에서는 jwplayer 7으로 변경하면 문제는 해결될 것으로 보인다.&lt;br&gt;다만, onSeek의 이벤트만 살펴보았기 떄문에, 소스의 수정없이 버전만 올려서 정상적으로 시스템이 동작하는지 전체적인 검수는 꼭 필요해 보인다.&lt;/p&gt;
&lt;h2&gt;3. 참고사항&lt;/h2&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;앞서 해결에서 살펴본 테스트에서의 jwplayer 파일은 공식홈페이지에서는 이미 예전 버전은 제공하지 않기 떄문에, 인터넷에서 떠도는 파일로 테스트한 것이다. 그렇기 떄문애, 버전별 테스트가 부정확 할 수도 있다는 점을 참고해야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://firefox-source-docs.mozilla.org/toolkit/components/pictureinpicture/pictureinpicture/index.html&quot;&gt;firefox pip 동작원리&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>실무 이슈</category>
      <category>jwplayer</category>
      <category>on('seek')</category>
      <category>onseek</category>
      <author>알 수 없는 사용자</author>
      <guid isPermaLink="true">https://programmingnote.tistory.com/81</guid>
      <comments>https://programmingnote.tistory.com/81#entry81comment</comments>
      <pubDate>Sun, 8 Aug 2021 17:37:25 +0900</pubDate>
    </item>
    <item>
      <title>jwplayer 버전 이슈(2) - playsinline</title>
      <link>https://programmingnote.tistory.com/80</link>
      <description>&lt;blockquote&gt;
&lt;p&gt;ios 모바일 웹 사파리에서 비디오 재생시 사파리 전용 플레이어로 재생됨.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;0. 시작&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;ios 모바일 웹 사파리에서 비디오를 재생시키면 사파리 전용플레이어로 재생되는 현상이 발생하였다.&lt;br&gt;사파리 전용 플레이어로 재생되면서 onSeek 이벤트가 작동하지 않는 문제도 있지만, 이 글에서는 모바일 웹에서 브라우저 전용 플레이어로 뜨는 조건에 대해서 적어보려한다.   &lt;/p&gt;
&lt;h2&gt;1. 원인파악&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;video 태그의 attribute를 찾아보면 ios 사파리의 플레이어의 사용여부(?) 설정은 playsinline으로 된다는 것을 알 수 있다.&lt;/p&gt;
&lt;p&gt;그럼 jwplayer를 사용했을 때, playsinline 속성이 어떻게 들어가는지 확인해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cS9Ouo/btraXW05htO/vTcz2W0XJAmZzo0HOakYsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cS9Ouo/btraXW05htO/vTcz2W0XJAmZzo0HOakYsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cS9Ouo/btraXW05htO/vTcz2W0XJAmZzo0HOakYsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcS9Ouo%2FbtraXW05htO%2FvTcz2W0XJAmZzo0HOakYsk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
  &lt;/p&gt;
&lt;p&gt;jwplayer 6버전으로 테스트한 결과이다. playsinline속성이 들어가긴 하는데, webkit- 접두사가 붙는 것이 보인다.   &lt;/p&gt;
&lt;p&gt;확인해보니 video 태그에서 동작하는 playsinline의 정책이 ios버전에 따라 바뀐 것을 알 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://webkit.org/blog/6784/new-video-policies-for-ios/&quot;&gt;https://webkit.org/blog/6784/new-video-policies-for-ios/&lt;/a&gt;  &lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 해결&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;결국 ios 특정버전 이상부터는 webkit- 접두사가 들어가지 않은 playsinline 속성을 사용해야 하는 것이고, jwplayer 6버전에서는 이를 지원하지 않는 것이다. 고로 2가지 해결방안을 생각해볼 수 있다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;playsinline 속성을 video 로드시 추가하기.&lt;/li&gt;
&lt;li&gt;jwplayer 버전올리기.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;h4&gt;1) playsinline 속성을 video 로드시 추가하기.&lt;/h4&gt;
&lt;p&gt;jwplayer가 영상을 재생할 준비가 되었을 떄 속성을 추가하여 동작하도록 처리한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;    jwplayer(&amp;#39;video&amp;#39;).onReady(function() {
        document.querySelector(&amp;#39;video&amp;#39;).setAttribute(&amp;#39;playsinline&amp;#39;, &amp;#39;&amp;#39;);
    })&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2) jwplayer 버전올리기.&lt;/h4&gt;
&lt;p&gt;jwplayer 7.12.13 버전으로 올려 테스트해보니 자동으로 playsinline 속성이 붙는 것이 확인되었다.&lt;br&gt;하지만, 몇버전부터 적용된건지 문서를 찾아봐도 모르겠다 -_-   &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://releases.jwplayer.com/jw-player/&quot;&gt;https://releases.jwplayer.com/jw-player/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>실무 이슈</category>
      <category>jwplayer</category>
      <category>playsinline</category>
      <author>알 수 없는 사용자</author>
      <guid isPermaLink="true">https://programmingnote.tistory.com/80</guid>
      <comments>https://programmingnote.tistory.com/80#entry80comment</comments>
      <pubDate>Sun, 1 Aug 2021 12:37:40 +0900</pubDate>
    </item>
  </channel>
</rss>