📌 사용 계기

데이터 검색 성능 개선을 해보는 프로젝트에서 검색 상태에 따라 다른 JPQL문을 생성할 필요가 있어 QueryDSL을 적용하게 되었다. 

 


📍QueryDSL이란?

JPQL문을 문자열이 아닌 자바 코드로 작성할 수 있도록 빌더 역할을 하는 오픈 소스

 


📍QueryDSL 특징

  • 쿼리를 코드로 작성해 컴파일 시 오류 발견이 가능하다.
  • 코드 기반에 단순하여 사용이 쉽고 가독성이 좋음
  • 자동 완성 등 IDE의 도움 가능
  • 동적 쿼리 구현 가능

 


📍적용 방법

  • build.gradle
buildscript {						// gradle로 task를 수행할 때에 사용되는 설정
	ext {							     // build.gradle에서 사용하는 전역 변수 설정
		queryDslVersion = "5.0.0"	// 변수 queryDslVersion에 5.0.0 대입
	}
}

plugins {							// gradle task의 집합
	//....//
	id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10'	// querydsl plugin 적용
}

dependencies {
	//....//

	// querydsl 의존성 추가
  // buildscript에서 적용한 변수 queryDslVersion이 여기에 적용
	implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"	
	implementation "com.querydsl:querydsl-apt:${queryDslVersion}"
}

// 변수 선언, 그 변수에 폴더 경로 저장
def querydslDir = "$buildDir/generated/querydsl"			

querydsl {															
	jpa = true														// JPA 사용여부 설정
	querydslSourcesDir = querydslDir		  // 사용할 경로 설정
}

sourceSets {														// build시 사용할 sourceSet 추가
	main.java.srcDir querydslDir
}

compileQuerydsl {												// queryDSL 컴파일시 사용할 옵션 설정
	options.annotationProcessorPath = configurations.querydsl
}

configurations {												// build 옵션
	compileOnly {
		extendsFrom annotationProcessor
	}
   // querydsl이 compileClasspath를 상속하도록 설정
	querydsl.extendsFrom compileClasspath	 
}
  • config
@Configuration
public class QuerydslConfig {

    @PersistenceContext
    // @PersistenceContext로 주입받은 Entity Manager은 Proxy로 감싸진다.
    // EntityManager 호출시 마다 Proxy를 통해 EntityManager을 생성하여 Thread-Safe를 보장
    private EntityManager entityManager;

    @Bean
    // JPAQuery를 생성해주는 factory클래스
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}
  • Q-Class

QueryDSL에서는 Entity로 설정된 클래스에 Q모델의 쿼리 타입 클래스를 미리 생성하고 메타데이터로 사용하여 쿼리를 메소드 기반으로 작성한다.

 

생성하는 방법은 Tool Window Bar (사이드에 보이는 메뉴명)에서 Gradle > other > compileJava 클릭

생성된 것을 확인

 

 


 

📍 QueryDSL 구현 방법 (총 3가지)

  • 방법 1 )  QuerydslRepositorySupport를 상속 받아 사용하는 방법
    • 구현코드
      • JpaRepository를 상속 받는 ProductRepository 인터페이스 하나
        import com.example.showmethemany.domain.Products;
        import org.springframework.data.jpa.repository.JpaRepository;
        
        public interface ProductRepository extends JpaRepository<Products, Long> {
        }
      • QuerydslRepositorySupport를 상속하고 Super생성자에 Entity를 지정해서 넣은 클래스 하나(ProductRepositorySupport)
        import com.example.showmethemany.domain.Products;
        import com.querydsl.jpa.impl.JPAQueryFactory;
        import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;
        import org.springframework.stereotype.Repository;
        
        import static com.example.showmethemany.domain.QProducts.products;
        
        @Repository
        public class ProductRepositorySupport extends QuerydslRepositorySupport {
        
            private final JPAQueryFactory queryFactory;
            public ProductRepositorySupport(JPAQueryFactory queryFactory) {
                super(Products.class);
                this.queryFactory = queryFactory;
            }
        
            public Products findByProductId (Long productId) {
                return queryFactory.select(products)
                        .from(products)
                        .where(products.id.eq(productId))
                        .fetchOne();
            }
        }
    • 테스트코드
      import com.example.showmethemany.domain.Products;
      import org.junit.jupiter.api.DisplayName;
      import org.junit.jupiter.api.Test;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.context.SpringBootTest;
      
      import static org.junit.jupiter.api.Assertions.*;
      
      @SpringBootTest
      class ProductRepositorySupportTest {
      
          @Autowired
          ProductRepository productRepository;
      
          @Autowired
          ProductRepositorySupport productRepositorySupport;
      
          @Test
          @DisplayName("QueryDSL Support 상속 구현 방식 테스트")
          void findByProductId() {
              //given
              Long productId = 1L;
      
              //when
              Products products = productRepositorySupport.findByProductId(productId);
      
              //then
              assertEquals(26910, products.getPrice());
          }
      }
    • 결과
      그러나 이 구현 방법의 문제점은 
      QuerydslRepositorySupport(코드상 ProductRepositorySupport)와 JpaRepository(코드상 ProductRepository)가 기능을 나눠 가져 항상 2개를 의존성으로 받아야 하는 문제가 있다
     
  • 방법 2 ) CustomRepository 상속과 구현이 필요한 방법
    • 구현코드
      • CustomRepository 인터페이스 하나
        import com.example.showmethemany.domain.Product
        
        public interface ProductRepositoryCustom {
            Products findByProductId (Long productId);
        }
      • CustomRepository 의 구현체 하나
        import com.example.showmethemany.domain.Products;
        import com.querydsl.jpa.impl.JPAQueryFactory;
        import javax.persistence.EntityManager;
        import static com.example.showmethemany.domain.QProducts.products;
        
        public class ProductRepositoryImpl implements ProductRepositoryCustom {
        
            private final JPAQueryFactory queryFactory;
        
            public ProductRepositoryImpl(EntityManager em) {
                this.queryFactory = new JPAQueryFactory(em);
            }
        
            @Override
            public Products findByProductId(Long productId) {
                return queryFactory.select(products)
                        .from(products)
                        .where(products.id.eq(productId))
                        .fetchOne();
            }
        }
      • JpaRepository 에 CustomRepository를 상속함으로써 CustomRepositoryImpl도 사용 가능
        import com.example.showmethemany.domain.Products;
        import org.springframework.data.jpa.repository.JpaRepository;
        
        public interface ProductRepository extends JpaRepository<Products, Long>, ProductRepositoryCustom {
        }
    • 테스트코드
      @SpringBootTest
      class ProductRepositoryTest {
      
          @Autowired
          ProductRepository productRepository;
      
          @Test
          @DisplayName("QueryDSL CustomRepository 상속 방식 테스트")
          void findByProductId() {
              //given
              Long productId = 1L;
      
              //when
              Products products = productRepository.findByProductId(productId);
      
              //then
              assertEquals(26910, products.getPrice());
          }
      }
    • 결과
      그러나 이 구현 방법의 문제점은 
      하나의 Repository당 interface하나와 Impl하나 총 2개를 더 만들어줘야 한다. (총 3개)
     
  • 방법 3 ) 상속과 구현 없이 가능한 방법
    • 구현코드
      • JPAQueryFactory를 주입 받은 Repository class 하나
        JPAQueryFactory만 주입 받으면 QueryDSL을 사용할 수 있고 특정 Entity 지정하지 않아도 여러 Entity를 대상으로 사용할 수 있다.
        import com.example.showmethemany.domain.Products;
        import com.querydsl.jpa.impl.JPAQueryFactory;
        import org.springframework.stereotype.Repository;
        import static com.example.showmethemany.domain.QProducts.products;
        
        @Repository
        public class ProductRepository {
        
            private final JPAQueryFactory queryFactory;
        
            public ProductRepository(JPAQueryFactory queryFactory) {
                this.queryFactory = queryFactory;
            }
        
            public Products findByProductId (Long productId) {
                return queryFactory.select(products)
                        .from(products)
                        .where(products.id.eq(productId))
                        .fetchOne();
            }
        }
    • 테스트코드
      @SpringBootTest
      class ProductRepositoryTest {
      
          @Autowired
          private ProductRepository productRepository;
      
          @Test
          @DisplayName("QueryDSL 상속/구현 없는 방식 테스트")
          void findById() {
              //given
              Long productId = 1L;
      
              //when
              Products products = productRepository.findByProductId(productId);
      
              //then
              assertEquals(26910, products.getPrice());
          }
      }
    • 결과
      이 구현 방법은 다른 경우보다 부담이 덜된다는 장점은 있지만 기본 JpaRepository와는 별개가 되기 때문에 기존 Repository의 메서드를 참조할 수 없게 된다. QueryDSL만 사용하게 되면 JPA로 간단히 사용했던 메소드를 구현해 줘야 한다는 단점이 있다. 그래서 프로젝트에는 QueryDSL과 Spring Data JPA를 같이 쓰는 방향으로 진행했다. 

📌 김영한 님의 "스프링 핵심 원리 - 기본편" 강의 듣고 정리

목차

     

    Section 1 . 객체 지향 설계와 스프링

    스프링이란? 

     

    ☘️ 스프링 생태계

    필수!

    • 스프링 프레임워크
    • 스프링 부트 : 스프링 기술들을 편하게 사용할 수 있게 도움을 주는 기술

    👆 선택

    • 스프링 데이터 : 데이터베이스를 사용하는 데 도움을 주는 기술 (가장 많이 쓰는게 Spring Data JPA)
    • 스프링 세션 : 세션 기능을 편리하게 사용할 수 있도록 도움을 주는 기술
    • 스프링 시큐리티 : 보안
    • 스프링 Rest Docs : API 문서와 테스트를 엮어 편리하게 도움을 주는 기술
    • 스프링 배치 : 배치 처리에 특화된 기술
    • 스프링 클라우드 : 클라우드에 특화된 기술
    • 등등 

     

    ☘️ 스프링 프레임 워크

    • 핵심 기술: 스프링 DI 컨테이너, AOP, 이벤트, 기타 (강의의 초점!!)
    • 웹 기술: 스프링 MVC, 스프링 WebFlux
    • 데이터 접근 기술: 트랜잭션, JDBC, ORM 기원, XML 지원
    • 기술 통합: 캐시, 이메일, 원격접근, 스케쥴링
    • 테스트: 스프링 기반 테스트 지원
    • 언어: 코틀린, 그루비
    • 최근에는 스프링 부트를 통해서 스프링 프레임원크의 기술들을 편리하게 사용

     

    ☘️ 스프링 부트

    • 스프링을 편리하게 사용할 수 있도록 지원, 최근에는 기본으로 사용
    • 단독으로 실행할 수 있는 스프링 애플리케이션을 쉽게 생성
    • Tomcat 같은 웹 서버를 내장해서 별도의 웹 서버를 설치하지 않아도 됨
    • 손쉬운 빌드 구성을 위한 starter 종속성 제공
    • 스프링과 3rd party(외부) 라이브러리 자동 구성, 의존성 버전 관리
    • 메트릭, 상태 확인, 외부 구성 같은 프로덕션 준비 기능 제공 (모니터링 제공)
    • 관례에 의한 간결한 설정
    • 스프링 부트는 스프링과 별도로 사용가능한 것이 아니다! 스프링 프레임워크 위에서 편리하게 기능을 사용할 수 있도록 돕는다!

     

    ☘️ 스프링 단어?

    스프링이라는 단어는 문맥에 따라 다르게 사용된다.

    • 스프링 DI 컨테이너 기술
    • 스프링 프레임워크
    • 스프링 부트, 스프링 프레임워크 등을 모두 포함한 스프링 생태계 

     

    ☘️ 스프링 왜 만들었나요?

    핵심 개념: 이 기술을 왜 만들었는가? 이  기술의 핵심 컨셉은? (로드존슨이랑 유겐 힐러가 왜 만들었을까!?!?!)

     

         ❓웹 애플리케이션 만들고, DB 접근 편리하게 해주는 기술?

         ❓전자정부 프레임워크?

         ❓웹 서버도 자동으로 띄워주고?

         ❓클라우드, 마이크로 서비스?

     

    인걸까?? ❌ 이건 다 결과물일 뿐이다

     

     * 스프링의 진짜 핵심

    • 스프링은 자바 언어 기반의 프레임워크
    • 자바 언어의 가장 큰 특징 - 객체 지향 언어
    • 스프링은 객체 지향 언어가 가진 강력한 특징을 살려내는 프레임워크
    • 스프링은 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크

    사람들이 스프링 등장에 열광했던 건 스프링 이전의 EJB는 객체 지향의 장점을 망치는 기술이었기 때문에 이걸 살릴 수 있었던 기술에 열광했던 거다!! 

     

     


    좋은 객체 지향 프로그래밍이란?

     

    ☘️ 객체 지향 특징

    • 추상화 
    • 캡슐화
    • 상속
    • 다형성

     

    ☘️ 객체 지향 프로그래밍

    • 객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러개의 독립된 단위,
      "객체"들의 모임으로 파악하고자 하는 것이다. 각자의 객체는 메시지를 주고받고, 데이터를 처리할 수 있다. (협력)
      • 혼자 있는 객체는 없다.
      • 객체끼리 뿐만 아니라 수많은 객체 클라이언트와 객체 서버는 서로 협력 관계를 가진다. (요청과 응답하면서)
      • 서버끼리도, 시스템끼리도! 
    • 객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용된다. 

    유연하고, 변경이 용이하다? 

    객체 단위로 개발하기 때문에

    • 레고 블럭 조립, 키보드 마우스 교체, 컴퓨터 부품 교체 처럼
    • 컴포넌트를 쉽고 유연하게 변경하면서 개발할 수 있는 방법

    이런 궁극의 유연함을 가질 수 있게 하는 객체 지향의 핵심! 다형성(Polymorphism) 이다. 

     

     

    ☘️ 다형성(Polymorphism) 

    자동차라는 역할이 존재하고 이 자동차 역할에 그 어떤 종류에 자동차가 구현(K3든, 아반떼든, 테슬라든) 되어도 운전자는 자동차를 운전할 수 있다. 운전자는 자동차 역할이라는 인터페이스에 의존하고 있기 때문에 구현이 어떤게 와도 상관없다.

    이는 운전자를 위한 것이다.

    자동차 내부 동작 방식이 바뀌어도 운전자에게는 영향이 없기 때문에 쓰던 방식 그대로 쓸 수 있다. 운전자는 새로 뭔가를 배울 필요가 없다. 또한 구현체를 바꿔버려도 운전이 그대로 가능하니까 자동차 역할만 지키면 새로운 구현체도 들어올 수 있어 확장성이 생긴다.

    이게 가능한 것이 역할과 구현을 나눠놨기 때문이다.  (연극에서 하나의 역할을 여러사람이 할 수 있는 것도)

     

     

    ☘️ 역할과 구현을 분리

    • 역할과 구현으로 구분하면 세상이 단순해지고, 유연해지며 변경도 편리
    • 장점
      • 클라이언트는 대상의 역할(인터페이스)만 의존
      • 클라이언트는 구현 내부 몰라도 된다. 변경도 몰라도 되며, 심지어 구현 대상 자체가 바뀌어도 영향 X

    프로그래밍 언어에서도 이러한 장점을 받아들이게 된다.

     

     

    ☘️ 자바 언어에서는 이걸 어떻게 받아들였을 까?

    자바 언어의 다형성을 활용한 방식은 객체 설계시 역할과 구현을 명확하게 분리했다.

    객체 설계시 역할(인터페이스)을 먼저 부여하고, 그 역할을 수행하는 구현 객체 만드는 방식으로 차용

    • 역할 = 인터페이스
    • 구현 = 인터페이스를 구현한 클래스,  구현 객체

    역할 > 구현. 역할이 더 중요하다

     

    오버라이딩 된 메서드가 실행될 때 다형성으로 인터페이스를 구현한 객체를 실행 시점에 유연하게 변경 할 수 있다.

    상속 관계에서도 다형성, 오버라이딩 적용 가능

     

     

    ☘️ 다형성의 본질

    • 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경 할 수 있다. 
    • 다형성의 본질을 이해하려면 협력이라는 객체사이의 관계에서 시작해야한다. 
    • 클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있다. 

     

    ☘️ 역할과 구현을 분리 : 정리

    • 역할과 구현이라는 컨셉을 다형성을 통해 객체화 할 수 있다.
    • 유연하고 변경이 용이
    • 확장 가능
    • 클라이언트에 영향을 주지 않는 변경이 가능

    따라서, 인터페이스를 안정적으로 잘 설계하는 것이 중요하다. 

     

     

    ☘️ 하지만 한계도 있다

    역할(인터페이스)이 변하면 클라이언트, 서버 모두 변경이 발생한다. 그래서 더더욱 안정적인 인터페이스가 필요하다.  

     

     

    ☘️ 스프링과 객체 지향

    객체지향의 꽃은 다형성

    스프링은 이런 다형성을 극대화해서 이용이 가능하게 한다.

    스프링에서 말하는 제어의 역전(IoC), 의존관계 주입(DI)은 다형성을 활용해서 역할과 구현을 편리하게 다룰 수 있도록 지원해주는 역할을 해준다!

    그래서 결론! 스프링을 사용하면 블럭 조립하든 구현을 편리하게 변경할 수 있다!

     

    다형성이 스프링에 굉장히 중요한데 여기서 한가지가 더 있어야 완전히 스프링을 설명할 수 있다.

    그게 바로 좋은 객체 지향 설계의 5가지 원칙, 소위 SOLID 라고 불리는 원칙이다.


    좋은 객체 지향 설계의 5가지 원칙 (SOLID)

     

    ☘️ SRP (단일 책임의 원칙 / Single reponsibility principle)

    한 클래스는 하나의 책임만 가져야 한다는 원칙. 하지만 하나의 책임이라는 게 모호하고 문맥과 상황따라 다를 수 있는 거라 중요한건 변경을 기준으로 한다. 변경의 이유를 하나만 가질 것 변경의 이유가 여러가지가 생겼다면 한가지 이상의 책임을 맡고 있는 것이다. 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것이다.

     

    ☘️ OCP (개방-폐쇄 원칙 / Open-Closed principle)

    소프트웨어 요소는 확장에는 열려있고, 변경에는 닫혀있어야 한다. 

    다형성을 활용해 이 원칙을 지킬수가 있다. 인터페이스를 구현한 새로운 클래스를 만들어 새로운 기능을 구현하면 확장에는 열려있다. 그리고 이 변화가 외부의 기존 코드에 변경을 주지 않는다. 변경에는 닫혀있는 것이다. 

     

    그런데, 개발 폐쇄 원칙은 문제점이 있다. 

    만약 MemberRepository m = new MemoryMemberRepository(); 를 

    MemberRepository m = new JdbcMemberRepository();로 구현 객체를 변경할 경우

    클라이언트가 코드에서 변경을 해야한다. 다형성을 사용했으나 OCP 원칙을 지킬 수 없는데 이 문제를 해결하기 위해 객체를 생성하고, 연관관계를 맺어주는 별도의 조립, 설정자가 필요하다. 

     

    이 역할을 스프링의 컨테이너가 해준다! 이 원칙을 지키기위해 IoC(제어의 역전)도 DI(의존성 주입)도 필요한 것!

     

    ☘️ LSP (리스코프 치환 원칙 / Liskov Substitution principle)

    프로그램 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다. 

    이것은 단순히 코드에 상위 타입이 하위타입의 인스턴스로 교체를 성공하여 컴파일이 성공하는 걸 의미하지 않는다.  

    상위 타입의 객체를 하위 타입의 객체로 치환했다고 하더라도 상위 타입으로 동작하던 기존 프로그램은 정상적으로 동작해야하는 원칙이다. 상위의 엑셀 기능이 하위 타입으로 교체했을 때 후진 기능이 되버리면 리스코프 치환 원칙에 위배된 것이다. 하위 클래스는 인터페이스 규약을 다 지켜야한다. 그래서 다형성을 지원하기 위해 필요한 원칙으로 인터페이스를 구현한 구현체를 믿고 사용하려면 이 원칙이 필요하다. 

     

    ☘️ ISP (인터페이스 분리 원칙 / Interface segregation principle)

    특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다. 

     

    자동차 인터페이스 > 운전 인터페이스, 정비 인터페이스 로 분리

    사용자 클라이언트 > 운전자 클라이언트, 정비사 클라이언트로 분리

    이런식으로 분리하면 정비 인터페이스를 변경해도 정비사 클라이언트만 영향을 주지 운전자 클라이언트는 영향을 주지않는다. 기능을 적당한 크기로 자르는 것이 중요. 인터페이스(역할)이 명확해지고, 하나의 큰 역할이 아닌 보다 작은 역할들로 잘라낸 것이라 대체 가능성이 높아진다.

     

    ☘️ DIP (의존관계 역전 원칙 / Dependency inversion principle)

    구체화에 의존하지 말고 추상화에 의존해라! = 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻

     

    SOLID에서 특히 중요한 원칙이 OCP와 DIP

    앞서 이야기한 역할에 의존해야하는 것과 같은 이야기! 클라이언트가 인터페이스를 의존해야 유연하게 구현체를 바꿀 수 있기 때문이다. 

     

    만약 MemberRepository m = new MemoryMemberRepository(); 를

    MemberRepository m = new JdbcMemberRepository();로 구현 객체를 변경할 경우

     

    이 예시를 보면 인터페이스를 의존하면서도 구현 클래스에도 결국 같이 의존하고 있어 결국 구현체 변경시 코드까지 변경해야 하는 것이다. (DIP 위반)

    그럼 어떻게 하라는 거지?

     

    ☘️ 정리

    객체 지향의 핵심은 다형성이지만 다형성만으로는 쉽게 부품을 갈아 끼우듯이 개발할 수 없다.
    결국 구현 객체 변경시 클라이언트 코드 또한 변경해야하기 때문에 OCP, DIP를 지킬수 없다.

    이걸 지키기 위해 무언가가 필요한데 그걸 지키기 위해 탄생한게 스프링이다!

     


    객체 지향 설계와 스프링

     

    ☘️ 스프링 이야기가 객체 지향에서 나오는 이유?

    스프링은 아래 기술로 다형성 + OCP, DIP를 가능하게 지원한다

    • DI(Dependency Injection) : 의존관계, 의존성 주입
    • DI 컨테이너 제공

    이 기술들로 클라이언트 코드 변경 없이 기능을 확장이 가능하게 하고 부품 교체하듯 대체도 가능하다.

     

    ☘️ 정리

    • 모든 설계에 역할구현을 분리 (예시였던 자동차와 연극의 역을 생각해!)
    • 애플리케이션 설계도 인터페이스(역할)을 만들어두고 언제든 유연하게 변경할 수 있도록 만드는 게 좋은 객체 지향 설계다.  (> 이걸 가능하게 하는 것이 스프링 컨테이너)

    이상적으로는 모든 설계에 인터페이스 부여 하지만 이걸 도입하려면 추상화라는 비용이 발생한다.

    따라서 기능을 확장할 가능성이 없다면 구체 클래스를 직접 사용하고, 후에 리팩토링을 통해 인터페이스를 도입하는 것도 방

    'Programming > Spring' 카테고리의 다른 글

    QueryDSL 적용 방법  (0) 2023.04.01
    WebRTC 시그널링 서버 (Spring Boot로 구현)  (0) 2023.01.12
    Query DSL  (0) 2022.12.15
    스프링 부트(Spring Boot) 다운 그레이드  (0) 2022.12.07
    ORM / JPA / Hibernate / Spring Data JPA  (0) 2022.12.05

    📌 이 기술을 공부한 계기!

    항해99 실전 프로젝트에서 게임 사이트를 제작하면서 화상 및 음성 채팅이 필요했고 우리는 WebRTC를 채택했다.

    그런데 구현하는 과정에서 백엔드 입장에서 구현해야할 부분이 있었는 데, 백엔드는 뭘 해야줘야 하는 지부터 몰라서 많이 헷갈렸다. 공부를 하면서 백엔드는 WebRTC에 필요한 시그널링 서버를 구현해야 한다는 걸 알게되었고 따로 정리를 해보게 되었다 (백엔드 입장에서만 정리)

     


    📍WebRTC Signaling 서버를 설명하려면.. 

    시그널링 서버를 소개하려면 WebRTC를 소개해야하고, WebRTC를 소개하려면 Websocket을 소개해야하고, Websocket을 소개하려면 HTTP를 알아야한다. 

     

    HTTP는 브라우저와 서버가 소통하는 프로토콜로 인터넷 데이터 교환의 가장 기본이 된다.
    그런데 Http는 요청 방식을 보면 단방향이다. 브라우저가 요청하면 서버가 응답하고 그리고 끝!

    상태를 유지하지도 않고 금방 우리가 뭐했는지도 잊어버리는 stateless 무상태로 돌아간다. 심지어 브라우저의 요청에만 반응하기 때문에 단반향이라 실시간에 적합하지 않아 뭐가 새로 업데이트를 했는지 보려면 계-속 뭐가 새로 바뀌었니 하면서 서버에 요청을 해야한다.

    그래서 실시간성에 HTTP가 적합하지 않자 Websocket이 등장했다.
    Websocket은 브라우저와 서버간의 연결이 유지한다. 서로 계속 연결할 뿐만 아니라 양방향으로 가능한 통신이라 브라우저는 물론이고 서버도 요청없이 변화가 생기면 바로바로 브라우저에게 메세지를 보낼 수 있게 되었고 그 연결은 누군가 끊기 전까지 계속 된다.

    하지만 웹소켓도 중개자인 서버가 필요하고 서버가 일일이 상대방을 포워딩해서 내용을 다른 사람에게 전달해야하다 보니 서버의 성능, 비용등의 많은 문제가 생긴다.

    그럼 중개자가 없어지고 브라우저들 끼리만 연결하면 어떨까?
    이게 바로 WebRTC가 하는 기능이다. 이름도 Web Real Time Communication의 약자인 이유가 서버 없이 서로 메세지, 음성, 영상등을 주고 받아 중개자인 서버가 없으니 속력이 빨라 실시간에 적합하기 때문이다.

     

    📍WebRTC (Web Real-Time Communication) 란?

    웹과 앱에서 별 다른 드라이버나 플러그인 없이 실시간으로 데이터를 교환할 수 있어 카메라와 마이크를 사용해 P2P(Peer to Peer) 실시간 커뮤니케이션이 가능을 하게 하는 기술

     

    WebRTC는 별개의 플러그인을 설치하는 등의 귀찮은 작업도 필요없이 브라우저만 있으면 되기 때문에 편리하다.
    게다가 서버는 계속 전달해줘야 하는 역할에서 빠지니 서버가 가져야하는 부담이 줄어 좋다. 

    그럼 잠깐 여기서 서버가 할일이 여기서 어딘데? 

    바로 WebRTC 시작지점에서 서버가 해줘야 할일이 있다.  WebRTC는 그냥 이뤄지는 게 아니라 처음으로 서로에 대해 소개를 하는 시간이 필요하고,  이 부분에 있어서는 서버가 중개자로서의 역할을 해줘야 하는데 이를 시그널링 서버라고 한다.

     

    📍WebRTC에 필요한 시그널링(Signalling) 이란?

    서로 다른 네트워크에 있는 미디어 포맷 등을 상호 연동하기 위해 협의 과정을 거치는 데 이를 시그널링 이라고 한다. 이 과정에서 네트워크 주소 변환 및 방화벽에 대응하게 된다. 이 협의과정에서 중간 단계 역할을 하는 시그널링 서버를 백엔드가 구현해줘야한다!

     

     

    📍WebRTC 시그널링 서버의 "대략적인" 흐름을 만화로 그려봄!

     

     

    📍 시그널링 다이어그램

    https://github.com/satanas/simple-signaling-server

     

     

    📍 용어 설명

     

    ✒ WebSocket

    브라우저에서 지원하는 API 종류로 서버와 클라이언트 간의 메세지 교환을 위한 통신 규약 (프로토콜). 단방향으로 한번 Request/Response를 보내면 사라지는 HTTP의 stateless 속성과 달리 연결지향 양방향 전이중 통신이 가능한 프로토콜로 연결을 지속하여 상태를 유지한다. 주로 데이터의 빠른 업데이트, 실시간을 위해 많이 사용된다. 

     

    ✒ Session

    브라우저(클라이언트)와 서버 간의 연결 상태로 브라우저가 웹서버에 접속한 시점부터 브라우저를 종료하여 연결을 끝낼 때까지 같은 사용자로 부터 오는 요청을 하나의 상태로 보면서, 그 상태를 일정하게 유지하는 기술.  웹 서비스에서 브라우저 당 하나의 Session을 갖는다.

     

    ✒ offer 와 answer

    WebRTC 메세지는 아니지만 SDP라는 포맷으로 세션 정보를 생성하여 먼저 offer(제안)를 보내면 이에 대한 대답으로 answer(응답)를 보내며 서로 SDP를 교환한다. 

     

    ✒ SDP (Session Description Protocol)

    Peer가 가진 세션의 정보들을 포함하고 있다. 

    포함된 정보
    SDP를 생성한 Peer의 식별자 (유저이름, 세션아이디, 세션 버젼, 네트워크 타입, 주소 타입, 유니캐스트 주소 등),
    미디어라인 (미디어 타입(음성인지 영상인지등), 포트 번호, 미디어 형식(코덱) 등),
    실시간 트래픽을 주고 받을 IP, RTCP에 사용될 IP, ICE 파라미터, DTLS 파라미터, BUNDLE행에 사용되는 식별자,
    밴드위드스, 미디어 방향, opus 코덱 등등

     

    ✒ ICE (Interactive Connectivity Establishment) candidate

    웹 브라우저 간의 직접적인 Peer to Peer를 접속할 수 있도록 해주는 프레임워크. SDP가 미디어에 대한 정보를 준다면 네트워크에 대한 정보를 교환하는 것을 ICE candidate라고 한다. 각각의 Peer는 가장 좋은 후보에서 나쁜 후보 순서로 제안하며 서로가 맞는 최적의 주소값을 찾는다.

     

     

    📍 구현 코드

     

    GitHub - littlezero48/Study-Storage: 📒 다양한 공부 흔적을 남기는 곳

    📒 다양한 공부 흔적을 남기는 곳. Contribute to littlezero48/Study-Storage development by creating an account on GitHub.

    github.com

     

     


    참고 자료:

     

    [WebRTC] Signaling Server ( 시그널링 서버 )

    WebRTC에 대해서 이야기를 해봤는데 WebRTC를 유기적으로 잘 사용하기 위해서는 아래와 같은 서버가 필요하다. Signaling - Always needed NAT Traversal - need for production Media - depends on the app 이번 포스트는 Sign

    withseungryu.tistory.com

     

    WebRTC 연결성 및 NAT 통과 기법

    WebRTC Connectivity and NAT Traversal

    lovejaco.github.io

     

     

    Query DSL

    QueryDsl은 정적 타입을 이용해서 SQL과 같은 쿼리를 생성할 수 있도록 해주는 프레임워크

     

     

    ❓ 왜 사용할까?

     

    Querydsl에서 java 코드를 사용하기 때문에 컴파일 시점에 타입 체크나 오타를 잡아주어 IDE의 도움을 받을 수 있다. 

    Querydsl에서 파라미터를 바인딩 할 때 바로 넣을 수 있어 간편하고 가독성이 좋다.


    🟥 build.gradle 설정

    아래 사항을 모두 추가

    //querydsl 추가
    buildscript {
       dependencies {
          classpath("gradle.plugin.com.ewerk.gradle.plugins:querydsl-plugin:1.0.10")
       }
    }
    
    //apply plugin: 'io.spring.dependency-management'
    apply plugin: "com.ewerk.gradle.plugins.querydsl"
    
    dependencies {
    	//...//
    	
        // QueryDSL 추가
       implementation 'com.querydsl:querydsl-jpa'
       implementation 'com.querydsl:querydsl-apt'
    }
    
    //querydsl 추가 시작 (버전 5.0.0 이후 추가된 세팅)
    //def querydslDir = 'src/main/generated'
    def querydslDir = "$buildDir/generated/querydsl"
    
    querydsl {
       library = "com.querydsl:querydsl-apt"
       jpa = true
       querydslSourcesDir = querydslDir
    }
    
    sourceSets {
       main {
          java {
             srcDirs = ['src/main/java', querydslDir]
          }
       }
    }
    
    compileQuerydsl{
       options.annotationProcessorPath = configurations.querydsl
    }
    
    configurations {
       querydsl.extendsFrom compileClasspath
    }
    //querydsl 추가 끝

     

     


    🟧 Query DSL 설정 파일

    import com.querydsl.jpa.impl.JPAQueryFactory;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.persistence.EntityManager;
    import javax.persistence.PersistenceContext;
    
    @Configuration
    public class QuerydslConfig {
        @PersistenceContext
        private EntityManager entityManager;
    
        @Bean
        public JPAQueryFactory jpaQueryFactory() {
            return new JPAQueryFactory(entityManager);
        }
    }

     

     

     


    🟨 Q클래스 생성

    컴파일러를 돌리면 내가 프로젝트 내의 모든 Entity에 대한 Q클래스를 생성한다

     

     

     

     


    🟩 구현

     

    사용자 정의 인터페이스를 만든다.

    public interface MemoRepositoryCustom {
        Long countLikeFromLikeMemo(Long Id);
    }

     

    구현체를 만드렁 Querydsl로 조회할 메서드를 구현한다. 

    여기서 원하는 SQL을 날릴수 있도록 커스텀한다.

    이때 메소드 안에 엔티티를 Q클래스를 생성해 그 안에서 데이터를 가져오는 것도 가능하다.

    @Repository
    public class MemoRepositoryImpl implements MemoRepositoryCustom {
    
        private final JPAQueryFactory jpaQueryFactory; // 의존성 주입
    
        public MemoRepositoryImpl(JPAQueryFactory jpaQueryFactory) {
            this.jpaQueryFactory = jpaQueryFactory;
        }
    
        @Override
        public Long countLikeFromLikeMemo(Long memoId){
            // Q클래스 사용
            QLikeMemo likeMemo = QLikeMemo.likeMemo;
            return jpaQueryFactory
                    .select(likeMemo.count())
                    .from(likeMemo)
                    .where(
                            likeMemo.memo.id.eq(memoId)
                    )
                    .fetchOne();                              // 하나의 값만 반환
        }
    }

     

    이후 기존 JpaRepository에서 커스텀 레포지토리를 추가 상속한다. 

    public interface MemoRepository extends JpaRepository<Memo, Long>, MemoRepositoryCustom {                                                           
        Long countLikeFromLikeMemo(Long Id);
    }

     

    그리고 기존 Repository에 선언했으므로 bean으로 등록되어 의존성 주입후 사용하면 된다.

     

     


    🟦 Query DSL 기본 쿼리 메소드 (아주 간단하게)

    • select() : 조회 시작 메소드. 조회하려는 필드를 선택하고자 할때 매개값으로 조회할 사항을 선택할 수 있다.
    • from() : 조회하려는 엔티티 이름
    • selectFrom() : select하는 엔티티와 from의 엔티티가 일치할 경우, 그리고 그 엔티티의 모든 필드를 조회할 경우
    • where() : 데이터 조회 조건을 설정하는 부분
    • update() : 데이터 수정 시작 메소드
    • set() : 데이터 수정할 필드 이름을 첫번째 매개값으로, 넣을 값을 두번재 매개값으로 가진다.
    • delete() : 데이터 삭제 시작 메소드
    • fetchOne() : 단건 조회, 결과 조회가 없으면 Null
    • fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
    • fetchFirst() : = limit(1).fetchOne() 가장 처음 조회되는 첫번째 한건을 조회
    • fetchCount() : count쿼리로 변환하여 조회되는 결과 수만 반환, long으로 반환
    • orderBy() : 정렬 방법
    • GroupBy() : 집합
    • join() : 기본적으로 Inner join에 leftJoin()은 left outer join, rightJoin()은 right outer join에 해당 된다.

     

    스프링부트 버전에 대해 설정을 직접 해주지 않아 자바 17에서의 최신버전인 3.0.0으로 프로젝트를 생성하는 일이 생겨서 처음부터 다시 세팅하는 일 없이 바로 다운그레이드 할 수 있도록 방법에 대해 정리

     

    build.Gradle에서 아래 사항들을 작성

    build.Gradle이란 프로젝트 수준의 그레이들 설정 파일을 말하며 프로젝트의 모든 모듈에 적용되는 빌드 구성을 정의하고 있다.

     

    • buildscript를 통해 변경해서 build할 요소를 작성

    - 모든 모듈에 공통되는 Gradle 저장소와 종속 항목을 정의

    - buildscript는 보통 외부 라이브러리를 가져올 때 사용

     

    • buildscript { repositories { 

    - dependency의 외부 저장소 설정. 기본은 google()

     

    ❓ 하나의 dependency를 추가한다고 해서 단 하나만 가져오는게 아니라 이에 dependency가 의존하고 있는 또다른 dependency들도 끌고 오기 때문에 이를 여러 저장소에서 호출하는 걸까

     

    • buildscript { dependencies {

    - gradle의 플러그인 버전 설정

    - 바꿀 버전 dependency를 적용

     

    ❓ 여기선 또 RELESE를 씀

     

    • plugins에 다운 그레이드 하고자 하는 스프링부트 버젼으로 수정

    - 내 경우는 2.7.6버전으로 내리는 거라 id 'org.springframework.boot' version '2.7.6' 로 수정

     

    ❓ 그런데 다른글에서 보면 plugins에서도 2.7.6.RELEASE로 적어야한다는 데 나같은 경우엔 오히려 RELEASE를 쓰면 안됬다. 이유가 뭘까?

     

     

    buildscript가 pulgins 보다 우선되야 빌드가 적용된다. 

    한번 buildscript를 해서 프로젝트의 dependency를 수정하고 나면 buildscript 내용은 삭제해도 된다.

     

    buildscript {
        repositories {
            mavenLocal()
            maven { url 'https://maven.aliyun.com/repository/google/' }
            maven { url 'https://maven.aliyun.com/repository/public/' }
            maven { url 'https://maven.aliyun.com/repository/spring/' }
            maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }
            maven { url 'https://maven.aliyun.com/repository/spring-plugin/' }
    
            mavenCentral()
        }
        dependencies {
            classpath("org.springframework.boot:spring-boot-gradle-plugin:2.7.6.RELEASE")
        }
    }
    
    plugins {
        id 'java'
        id 'org.springframework.boot' version '2.7.6'	
        id 'io.spring.dependency-management' version '1.1.0'
    }

     

     

    [SpringBoot] 스프링부트 버전 변경 / 버전 다운그레이드

    개인 / 사이드 프로젝트를 진행할 때는 별생각 없이 Spring initializr의 도움을 받곤 한다 Springboot, Gradle의 버전을 딱히 신경 쓰고 있지 않다는 뜻이다 https://start.spring.io 인프런에서 Spring Cloud 강의를

    ryumodrn.tistory.com

     

    [Android/안드로이드] Gradle Scripts에 대한 정리

    📌Gradle Scripts란? > _안드로이드 프로젝트를 생성하면 Gradle Scripts 안에 build.gradle, settings.gradle 등의 파일이 자동으로 만들어진다. 이 파일들을 통해 안드로이드 프로젝트의 빌드 정보, 라이브러리

    velog.io

     

    + Recent posts