728x90
반응형
728x90
반응형
반응형

컴포넌트

  • 컴포넌트는 시스템의 구성 요소로 배포할 수 있는 가장 작은 단위이며, 자바의 경우 jar 파일이 컴포넌트임
  • 컴포넌트는 다양한 형태로 만들어질 수 있음
    • 여러 컴포넌트를 서로 링크하여 실행 가능한 단일 파일로 생성할 수 있음
    • 여러 컴포넌트를 묶어서 war 파일과 같은 단일 아카이브로 만들 수 있음
    • 컴포넌트 각각을 jar과 같이 동적으로 로드 할 수 있는 플러그인이나 실행 가능한 파일로 만들어 독립적으로 배포할 수 있음
  • 컴포넌트가 최종적으로 어떤 형태로 배포되든, 잘 설계된 컴포넌트라면 반드시 독립적으로 개발 가능해야 함
  • 참고. 앞으로 내용을 따라갈 때 거시적인 관점에서 하나의 큰 시스템을 생각하며 흐름을 따라가야 함.

컴포넌트 응집도

  • 어떤 클래스를 어느 컴포넌트에 포함시켜야 할지는 중요한 결정이므로, 제대로 된 소프트웨어 엔지니어링 원칙이 필요함

 

REP(reuse/release equivalance principle): 재사용/릴리스 등가 원칙

  • 정의: 재사용 단위는 릴리스 단위와 같다.
    • 이는 당연한데, 컴포넌트가 릴리스 절차를 통해 관리되지 않거나, 릴리스 번호가 없다면 재사용하고 싶지도, 할 수도 없음
    • 릴리스 번호가 없다면 컴포넌트들이 호환되는지 보증할 수 없음
    • 개발자는 새로운 버전이 언제 출시되고 무엇이 변했는지 알아야 함(새로운 버전으로의 통합 여부 및 시기 결정)
  • REP를 소프트웨어 설계와 아키텍처 관점에서 보면 다음과 같음
    • 단일 컴포넌트는 응집성이 높은 클래스와 모듈들로 구성되어야 함
    • 이를 다르게 보면 하나의 컴포넌트로 묶인 클래스와 모듈은 반드시 함께 릴리스 할 수 있어야 한다는 것
    • 하나의 컴포넌트로 묶인 클래스와 모듈은 버전이 같고, 동일한 릴리스로 관리되고, 동일한 릴리스 문서에 포함되어야 함
  • 이 원칙 만으로는 클래스와 모듈을 단일 컴포넌트로 묶는 방법을 제대로 설명하지 못하지만(약점), 이 원칙의 약점은 다음 두 원칙(CCP와 CRP)으로 보완할 수 있음

 

CCP(common closure principle): 공통 폐쇄 원칙

  • 정의: 동일한 이유로 동일한 시점에 변경되는 클래스는 같은 컴포넌트로 묶고, 다른 시점에 다른 이유로 변경되는 클래스는 분리하라
  • 대다수의 애플리케이션에서 유지보수성은 재사용성보다 훨씬 중요하며, 변경은 단일 컴포넌트에서 발생해야 함(독립적인 배포)
    • 수정이 필요한 경우 모든 컴포넌트를 조금씩 수정하기 보다는 하나의 컴포넌트만 수정하도록 하는게 낫다.
  • OCP(개방 폐쇄 원칙)는 class level이고, CCP는 컴포넌트 level.
    • OCP: 공통적인 변경에 대해 클래스가 닫혀 있도록 설계.
  • SRP(단일 책임 원칙) class level이고, CCP는 컴포넌트 level : 단일 컴포넌트는 변경의 이유가 여러 개 있어서는 안됨
    • SRP: 서로 다른 이유로 변경되는 메소드를 서로 다른 클래스로 분리하라
    • CCP: 서로 다른 이유로 변경되는 클래스를 서로 다른 컴포넌트로 분리하라.

 

CRP(common reuse principle): 공통 재사용 원칙

  • 정의: 컴포넌트 사용자들을 필요로 하지 않는 것에 의존하게 강요하지 말라.
  • CRP도 클래스와 모듈을 어느 컴포넌트에 위치시킬지 결정할 때 도움이 됨
  • 같이 재사용되는 경향이 있는 클래스와 모듈들은 같은 컴포넌트에 포함
    • CRP에서는 재사용한 클래스들의 연결고리가 동일한 컴포넌트에 포함되어 있어야
  • 컴포넌트를 의존하겠다고 결정한다는 것은 생각보다 많은 유지보수가 필요할 것을 암시하며 CRP에 의거하면 동일 컴포넌트로 묶어서는 안되는 것을 의의함
    • 컴포넌트의 단 하나의 클래스만을 사용한다고 해서 의존성이 약해지는게 아님
      • 의존성은 이분법적인 개념(Y/N) 이지 %가 아님
    • 이로 인해 사용되는 컴포넌트가 변경될 때마다 같이 변경(재배포 등)해야 할 가능성이 높음
    • 그러므로 의존하는 컴포넌트가 있다면 해당 컴포넌트의 모든 클래스에 대해 의존함을 확실히 인지해야 함
  • CRP는 강하게 결합되지 않은 클래스들을 동일한 컴포넌트에 위치시켜서는 안 된다고 말함
  • ISP(인터페이스 분리 원칙)는 class level, CRP는 컴포넌트 level : 필요하지 않은 것에 의존하지 말라
    • ISP: 사용하지 않는 메소드가 있는 클래스에 의존하지 말라
    • CRP: 사용하지 않는 클래스를 가진 컴포넌트에 의존하지 말라

 

컴포넌트 응집도에 대한 균형 다이어그램

  • 세 원칙은 서로 상충되는데, REP와 CCP는 포함 원칙(컴포넌트를 크게 만듦)이며, CRP는 배제 원칙(컴포넌트를 작게 만)임
  • 뛰어난 아키텍트는 3가지 원칙들이 균형을 이루는 방법을 찾아야 함.

응집도에 관한 세 원칙이 어떻게 상호작용하는가

 

  • 각 변은 반대쪽 꼭지점에 있는 원칙을 포기했을 때 감수해야 할 비용을 나타냄
    • CCP를 포기하면, 사소한 변경이 생겼을 때 너무 많은 컴포넌트에 영향이 미침
    • CRP를 포기하면 불필요한 릴리스가 너무 빈번해짐
    • REP를 포기하면 재사용이 어려워짐
  • 일반적으로 삼각형의 오른쪽에서 시작해서 왼쪽으로 이동해 감
    • 프로젝트 처음부터 재사용성이 필요하지는 않다가 성숙해지다보면 점점 재사용성(REP)이 중요해짐
  • 프로젝트의 컴포넌트 구조는 시간과 성숙도에 따라 변한다.

컴포넌트 결합

ADP(Acyclic dependency principle): 의존성 비순환 원칙

  • 정의: 컴포넌트 의존성 그래프에 순환이 있어서는 안된다.
  • 많은 개발자가 동일한 소스 파일을 수정하는 환경에서 코드가 동작하지 않게 될 수 있으며, 2가지 해결방법이 발전되어 옴
    • 주단위 빌드
    • 순환 의존성 제거하기

 

주 단위 빌드

  • 4일은 서로를 신경쓰지 않고, 금요일이 되면 코드를 통합하여 시스템을 빌드함
  • 프로젝트가 커지면서 통합에 드는 시간이 계속해서 늘어나게 됨
  • 결국 빌드 일정을 늘려야 하고, 통합과 테스트는 수행하기 점점 어려워지며, 빠른 피드백이 주는 장점을 잃게됨

 

순환 의존성 제거하기

  • 이 문제의 해결책은 개발 환경을 릴리스 가능한 컴포넌트 단위로 분리하는 것
  • 이를 통해 컴포넌트는 개별 관리자 또는 단일 개발팀이 책임질 수 있는 작업 단위가 됨
  • 개발자가 해당 컴포넌트가 동작하도록 만든 후, 릴리스하여 다른 개발자가 사용할 수 있도록 만듬
  • 이는 단순하며 합리적이라 널리 사용되지만 컴포넌트 사이의 의존성 구조를 반드시 관리해야 함
  • 의존성 구조에 순환이 있어서는 안되며, 컴포넌트 간의 의존성은 비순환 방향 그래프(DAG, Directed Acyclic Graph)여야 함

oneway

  • 어느 컴포넌트에서 시작하더라도 의존성 관계를 따라 최초의 컴포넌트로 돌아갈 수 없음
  • Presenters를 담당하는 팀에서 새로운 릴리스를 만들면 이 릴리스에 영향받는 팀을 쉽게 찾을 수 있음
  • Main은 새로 릴리스되더라도 시스템에서 영향받는 컴포넌트가 없음
  • 시스템 전체를 릴리스한다면 릴리스 절차는 상향식으로 진행됨(Entities부터 시작해 Main은 마지막에 처리)

이처럼 구성요소 간 의존성을 파악하고 있으면 시스템을 빌드하는 방법을 알 수 있음

 

순환이 컴포넌트 의존성 그래프에 미치는 영향

circular

  • 요구사항으로 Entities에 포함된 클래스 하나가 Authorizer의 클래스 하나를 사용할 수 밖에 없다면 순환 의존성이 발생함
  • Database 컴포넌트 개발자는 컴포넌트를 릴리스 하려면 Entities, Authorizer, Interactors 모두와 호환되어야 함
  • 세 컴포넌트는 하나의 거대 컴포넌트가 되며, 개발자 서로가 얽매여 모두 항상 정확하게 동일한 릴리스를 사용해야 함
  • Entities를 테스트하려면 Authorizer와 Interactors도 빌드하고 통합해야 하면서 어려워짐
  • 모듈의 개수가 많아짐에 따라 빌드 관련 이슈는 기하급수적으로 증가함
  • 컴포넌트를 어떤 순서로 빌드해야 올바른지 파악하기 힘들어지며, 올바른 순서라는 것 자체가 없을 수 있음

 

순환 끊기

  • 컴포넌트 사이의 순환을 끊고, DAG로 복구하는 것은 언제든 가능하며, 의존성 역전 원칙 또는 새로운 컴포넌트 생성으로 가능함
  • 의존성 역전 원칙(DIP)
    • User가 필요로 하는 메소드를 제공하는 인터페이스(permissions in Entity)를 제공함
    • 그리고 이 인터페이스는 Entities에, 구현체는 Authorizer에 위치시킴

DIP

 

  • 새로운 컴포넌트 생성
    • Entities와 Authorizer가 의존하는 새로운 컴포넌트를 만듬
    • 그리고 두 컴포넌트가 모두 의존하는 클래스들을 새로운 컴포넌트로 이동시킴

 

흐뜨러짐(Jitters)

  • 두 번째 해결책(새로운 컴포넌트 생성)이 시사하는 바는 요구사항이 변경되면 컴포넌트 구조도 변경될 수 있다는 사실
  • 실제로 애플리케이션이 성장하면서 컴포넌트 의존성 구조는 서서히 흐트러지며 또 성장함
  • 따라서 의존성 구조에 순환이 발생하는지를 항상 관찰해야 하며, 어떤 식으로든 끊어내야 함

 

하향식(top-down) 설계

  • 프로젝트 초기에는 컴포넌트 구조를 설계할 수 없음. 즉, 컴포넌트 구조는 하향식(top-down)으로 설계될 수 없음
    • 컴포넌트 의존성 다이어그램은 애플리케이션 기능과는 거의 관련이 없고, 빌드 가능성과 유지보수성의 지도와 같음
    • 컴포넌트는 시스템에서 가장 먼저 설계할 수 있는 대상이 아니며, 시스템이 성장하고 변경될 때 함께 진화함
  • 하지만 모듈들이 점차 쌓이기 시작하면 의존성 관리에 대한 요구가 점차 늘어남
    • 변경되는 범위가 시스템의 가능한 한 작은 일부로 한정되기를 원함
    • 함께 변경되는 클래스는 같은 위치에 배치시킴: 단일 책임 원칙(SRP), 공통 폐쇄 원칙(CRP)
    • 의존성 구조와 관련된 최우선 관심사는 변동성의 격리(자주 변경되는 컴포넌트로 부터 다른 컴포넌트를 보호함)
  • 애플리케이션이 계속 성장하면서 재사용 가능한 요소를 만드는 일에 관심을 기울이기 시작함: 공통 재사용 원칙(CRP)
    • 결국 순환이 발생하면 컴포넌트 의존성 그래프는 조금씩 흐트러지고 또 성장함: 의존성 비순한 원칙(ADP)

"아무런 클래스도 설계하지 않은 상태에서 컴포넌트 의존성 구조를 설계하려고 시도하면 큰 실패를 맛볼 수 있다. 공통 폐쇄 원칙에 대해 그다지 파악하지 못하고 있고, 재사용 가능한 요소도 알지 못하며, 컴포넌트를 생성할 때 거의 확실히 순환 의존성이 발생할 것이다. 따라서 컴포넌트 의존성 구조는 시스템의 논리적 설계에 발맞춰 성장하며 또 진화해야 한다."

 

SDP: 안정된 의존성 원칙

  • 정의: 안정성의 방향으로(더 안정된 쪽에) 의존하라.
  • 변경이 어려운 컴포넌트는 최대한 독립적으로
  • 변경이 어려운 컴포넌트에 한번 의존하게 되면 변동성이 큰 컴포넌트도 결국 변경이 어려워짐
  • 즉, 변경하기 쉽도록 모듈을 설계해도 이 모듈에 누군가가 의존성을 메달아 버리면 이 모듈도 변경하기 어려워짐

 

안정성(stability)

  • 안정성은 변경의 발생 빈도와는 직접적인 관련이 없고, 변경을 위해 필요한 작업과 관련됨
  • 안정적이라는 것은 변경을 위해 상상한 수고를 감수해야 한다는 것
    • 컴포넌트를 변경하기 어렵게 만드는 많은 요인(컴포넌트의 크기, 복잡도, 간결함 등)이 존재하는데, 이중 다른 컴포넌트가 해당 컴포넌트에 의존하게되면 변경이 특히 어려워짐
    • 왜냐하면 사소한 변경이라도 의존하는 모든 컴포넌트를 만족시키면서 변경하려면 상당한 노력이 들기 때문임

 

 

x is stable

  • X는 안정된 컴포넌트인데, 세 컴포넌트가 X에 의존하며 X는 변경하지 말아야 할 이유가 3가지나 됨
  • 이때 X는 세 컴포넌트를 책임진다고 말하며, 반대로 X는 어디에도 의존하지 않음
  • X가 변경되도록 만들 수 있는 외적인 영향이 전혀 없으므로, X는 독립적이라고 말함

 

t is unstable

  • 아래의 Y는 상당히 불안정한 컴포넌트임
  • 어떤 컴포넌트도 Y에 의존하지 않으므로 Y는 책임성이 없음
  • Y는 세 컴포넌트에 의존하므로 변경이 발생할 수 있는 외부 요인이 3가지이므로, Y는 의존적이라고 함

 

안정성 지표

  • 컴포넌트로 들어오고 나가는 의존성의 개수를 통해 컴포넌트의 불안정성(I)을 계산할 수 있음
    • fan-in: 안으로 들어오는 의존성으로 컴포넌트 내부의 클래스에 의존하는 컴포넌트 외부의 클래스 개수
    • fan-out: 바깥으로 나가는 의존성으로 컴포넌트 외부의 클래스에 의존하는 컴포넌트 내부의 클래스 개수
    • 불안정성(I)은 fan-out / (fan-in + fan-out)으로 계산 가능하며, [0, 1] 사이의 값을 가짐
  • 불안정성(I)가 0인 경우(X)
    • 해당 컴포넌트에 의존하는 다른 컴포넌트는 있지만, 해당 컴포넌트 자체는 다른 컴포넌트에 의존하지 않음
    • 이는 컴포넌트가 가질 수 있는 최고로 안정된 상태이며, 이러한 컴포넌트는 다른 컴포넌트를 책임지며 독립적임
    • X에게 의존하는 컴포넌트가 있으므로 변경이 어렵지만, X를 강제하는 의존성은 갖지 않음
  • 불안정성(I)가 1인 경우(Y)
    • 어떤 컴포넌트도 해당 컴포넌트에 의존하지 않지만, 해당 컴포넌트는 다른 컴포넌트에 의존함
    • 최고로 불안정한 상태이며 책임성이 없으므로 의존적임
    • 의존하는 컴포넌트가 없으므로 변경하지 말아야 할 이유가 없음
    • Y가 다른 컴포넌트에 의존한다는 뜻은 Y를 변경할 이유가 있다는 것임

 

모든 컴포넌트가 안정적이어야 하는 것은 아니다

  • 우리가 기대하는 것은 불안정한 컴포넌트와 안정된 컴포넌트가 모두 존재하는 상태

 

  • 위 다이어그램은 세 컴포넌트로 구성된 시스템이 갖는 이상적인 구조임
  • 상단에는 변경 가능한 컴포넌트들이 있고, 하단의 안정된 컴포넌트에 의존함
  • 위로 향하는 화살표가 있으면 안정된 의존성 원칙(SDP)에 위배되는 것인데, 존재하지 않음

 

  • Flexible은 변경하기 쉽도록 설계한 컴포넌트임
  • 우리는 Flexible이 불안정한 상태이기를 바라지만, Stable에서 Flexible에 의존성을 걸게 되면 SDP를 위배함
  • Flexible을 변경하려면 Stable과 Stable에 의존하는 나머지 컴포넌트에도 조치를 취해야 함

 

  • 이를 해결하려면 Flexible에 대한 Stable의 의존성을 끊어야 함
  • 예를 들어 Stable의 내부 클래스 U가 Flexible의 내부 클래스 C를 사용할 때, DIP를 도입하면 이 문제를 해결할 수 있음

 

 

DIP 적용

  • US라는 인터페이스를 생성하고 이를 UServer 컴포넌트에 넣은 후 C가 해당 인터페이스를 구현하도록 만듦
  • 이를 통해 Flexible에 대한 Stable의 의존성을 끊고, 두 컴포넌트는 모두 UServer에 의존하도록 강제함
  • UServer는 매우 안정되며(I=0) Flexible은 불안정성(I=1)을 유지할 수 있고, 모든 의존성은 I가 감소하는 방향으로 향함
    • 오로지 인터페이스만을 포함하는 컴포넌트(UServer)를 생성하는 방식이 이상하게 보일 수도 있음
    • 하지만 자바와 같은 정적 타입 언어에서는 이 방식이 흔히 사용되며 꼭 필요한 전략으로 알려져 있음
    • 이러한 추상 컴포넌트는 상당히 안정적이며, 따라서 덜 안정적인 컴포넌트가 의존할 수 있는 이상적인 대상임

 

SAP: 안정된 추상화 원칙

  • 정의: 컴포넌트는 안정된 정도만큼만 추상화되어야 한다.

고수준 정책(자주 변경해서는 안되는 소프트웨어)을 어디에 위치시켜야 하는가?

  • 고수준 정책을 캡슐화하는 소프트웨어는 안정된 컴포넌트에, 변동성이 큰 소프트웨어는 불안정한 컴포넌트에 포함시켜야 함
  • 하지만 고수준 정책을 안정된 곳에 위치시키면, 그 정책을 포함하는 소스 코드 수정이 어려워져 시스템 전체 아키텍처가 유연성을 잃음
  • 해결: 개방 폐쇄 원칙(OCP)
    • 이 원칙을 준수하는 클래스가 추상 클래스

안정된 추상화 원칙(SAP)

  • 안정된 추상화 원칙은 안정성과 추상화 정도 사이의 관계를 정의
    • 안정된 컴포넌트는 추상 컴포넌트여야 하며, 이를 통해 안정성이 컴포넌트를 확장하는 일을 방해해서는 안됨
    • 불안정한 컴포넌트는 반드시 구체 컴포넌트로써, 컴포넌트가 불안정하므로 내부의 구체적인 코드를 쉽게 변경할 수 있어야 함
  • 안정된 추상화 원칙(SAP)와 안정된 의존성 원칙(SDP)를 결합하면 컴포넌트에 대한 의존성 역전 원칙(DIP)

 

추상화 정도 측정하기

  • A = Na / Nc
    • Nc는 컴포넌트의 클래스 개수다.
    • Na는 컴포넌트의 추상 클래스와 인터페이스 개수다.
  • A = 0 : 컴포넌트에 추상 클래스가 하나도 없다.
  • A = 1 : 오로지 추상 컴포넌트만 있다.

 

주계열

  • 안정성(I)과 추상화 정도(A) 사이의 관계를 표현하면 다음과 같음
  • 최고로 안정적이며 추상화된 컴포넌트는 좌측 상단(0,1)
  • 최고로 불안정하며 구체화된 컴포넌트는 (1,0)에 위치함
  • 모든 컴포넌트가 이 두 지점에 위치하지는 않으며, 컴포넌트는 추상화와 안정화의 정도가 다양함
  • 컴포넌트가 위치할 수 있는 합리적인 궤적을 표현하면 다음과 같음

 

  • 고통의 영역(Zone of Pain)
    • (0, 0) 주변 구역에 위치한 컴포넌트들
    • 매우 안정적이며 구체적인데, 컴포넌트가 뻣뻣한 상태이므로 바람직하지 않음
    • 추상적이지 않으므로 확장이 어렵고, 안정적이므로 변경이 어려움
    • 제대로 설계된 컴포넌트라면 여기에 위치하지 않으며, 배제해야 하는 구역임
    • ex) 데이터베이스 스키마 or String 클래스(String은 변동성이 없으므로 해롭지는 않음) 등
  • 쓸모없는 구역(Zone of Uselessness)
    • (1, 1) 주변 구역에 위치한 컴포넌트들
    • 최고로 추상적이지만, 누구도 그 컴포넌트에 의존하지 않음(쓸모 없음)
    • 이는 누구도 구현하지 않은 채 남겨진 추상클래스인 경우가 많음
  • 주계열(The Main Sequence)
    • 변동성이 큰 컴포넌트들을 두 배제 구역으로부터 가능한 멀리 떨어뜨리는 선분
    • 쓸모없지 않으면서도 심각한 고통을 안겨주지도 않음
    • 가장 바람직한 지점은 주 계열의 종점이긴 하지만 일부 컴포넌트는 불가능할 수 있음
    • 주계열 바로 위에 또는 가깝게 위치할 때 가장 이상적

 

주계열과의 거리

  • 주계열로부터 얼마나 떨어져 있는지 측정하는 지표.
  • D=| A + I -1 |
  • D가 0에 가까울수록 이상적(주계열 선 위)
  • D가 0에서 가깝지 않다면 해당 컴포넌트는 재검토한 후 재구성하도록 계획 가능
    • D지표의 평균과 분산을 통해 다른 컴포넌트에 비해 예외적인 컴포넌트 추출 가능 -> 리팩

 

컴포넌트 산점도

 

  • 반대로 한 컴포넌트의 D를 시간 별(릴리즈 별)로 측정하여 주계열에서 멀리 떨어진 시점에 들어간 feature에 대해 리뷰 가능

 

  • 의존성 관리 지표는 설계의 의존성과 추상화 정도가 내가 “흘륭한” 패턴이라고 생각하는 수준에 얼마나 잘 부합하는지를 측정함
  • 지표는 임의로 결정된 표준을 기초로 한 측정값에 지나지 않기에, 다음 단계에 대한 힌트로 사용하면 충분

 

 

728x90
반응형
반응형

gshell은 bash의 상위호환 bash와 충돌나는 문법 없음

cshell은 구버전

 

Unix Shell
• Kernel을 둘러싸고 있는 껍데기
• 사용자가 제어할 수 있는 명령행 해석기

Bourne Shell
• 1977, Stephen Bourne
• /bin/sh

BASH (Bourne Again Shell)
• 1989, Brian Fox
• /bin/bash

 

ssh -> secure shell

-> 이걸로 게이트웨이까지 가고 그 이후론

rlogin -l 아이디 호스트명

이걸로 로그인

 

pwd: present working directory

id

uname: unix system  name -> os 정보 아키텍쳐 등

hostname

 

배쉬 내부 명령

command line argument(parameter)

echo는 기본적으로 new line이 나가기 때문에 그걸 무시하려면 -n옵션

 

ls

ls -1 파일 이름만 한줄에 하나씩

-a 숨겨진 것도

-F 파일

-l 롱포맷

 

ls 중 어떤 규칙만 필터 치고 싶다면..(패턴)

glob pattern

  • 임의의 길이(wildcard) *
  • 임의의 한 글자 ?

ls * -> 전체

ls .* -> 히든 전체

ls [a-f]*.txt -> a~f로 시작하는 텍스트 파일

ls *.tx? -> .txa 등의 확장자를 가진 파일

 

파일 시스템

  • File
    • OS에서 데이터를 디스크와 같은 저장소에 저장해 둘 때의 단위 형태
    • 넓은 의미에서는 일반 파일, 디렉토리와 각종 입출력 장치를 모두 파일로 간주
  • Directory
    • 파일을 담아두는 공간 (폴더)
    • 디렉토리 안에 디렉토리를 담을 수 있기 때문에 트리 구조로 형상화

 

cd : change directory

pwd

~ : 홈디렉토리

ls ~ 아이디 : 다른사람의 홈디렉토리

 

history

히스토리 중 다시 실행하고 싶다면

!12 : 히스토리 중 12번 재실행

!find : 히스토리 중 마지막 find로 실행했던 구문 재실행

!! : 바로 직전 명령 재실행

!$ : 바로 직전 명령의 마지막 argument

 

직전 명령 편집

echo "hello world"

^hello^java :hello -> java 변환(^ 캐럿이라고 부름)

 

touch

빈 파일 만들기, last modify date 바꿀 때

 

mkdir : make directory

rmdir : remove directory 빈 디렉토리만 삭제

 

cp 복사

mv 장소 이동, 이름 변경

rm 파일 삭제

rm -r : recursive하게 안에서부터 쫙 지우고 마지막으로 폴더 삭제

file /bin/gzip(파일) : 어떤 파일인데? 확인하는 용도

 

변수

사용자 변수
• 보통 소문자로 명명
• x=1
• x="hello world"
• x=hello world   (x)

주의사항

  • = 주위에 공백 없음

 

값을 꺼내쓰기(de-reference)

echo $x / ${x} / "$x"

 

single quote

  • literal, 문자열
  • 치환이 안됨

double quote(기본적으로 이거 사용)

  • 변수임

 

실행 결과를 변수로 저장

: backquote(`) 또는 $( )로 둘러싸기

date_str=`date +"%Y%m%d"`     
//            -> 날짜를 오른쪽 형식으로
date_str=$(date +"%Y%m%d")

 

명령 문자열을 실행하기

eval 명령문자열

  • cmd="ls –alF"
  • eval "$cmd

 

환경 변수

  • 환경(environment)
  • 프로세스를 둘러싼 주변 정보
  • env
  • 상속가능
  • export var1 = 2


exporting

  • sub-shell (쉘 안의 쉘)에서 상속받아 사용할 수 있도록 변수를 공개
  • 사용자 변수를 환경 변수로 전환
  • export나 declare –x로 지정

 

권한

u/g/o

chmod u+x test1.sh    
// -> owner에게 실행권한을 주기

실행 시 경로를 주거나 어떤걸로 실행할지 알려주면 됨

./test.sh
bash test.sh

 

소유자 변경

chown 아이디 파일

 

Boolean expression


&&, ||, !, –a, –o 

  • -a = &&이고 -o = ||

(( 0 && 1 )) && echo true || echo false   

  • -> then else
  • ((식)) && 참이면 실행 || 거짓이면 실행

(( 0 || 1)) && echo true || echo false

  • integer
    -eq -ne -gt -ge -lt –le

  • string
    < <= > >= = != -z –n
  • -z : zero length?
  • -n : null?

 

if

세미콜론, 공백과의 싸움

if [ 조건 ]; then
	실행문
fi

한 줄 쓰기

x=3; if [ $x –eq 3 ]; then echo "x is 3"; fi

 

주의사항

  • -eq 연산자는 정수 연산자
  • = 연산자는 문자열 연산자
  • 변수가 정의되지 않았거나 공백을 포함할 수 있으므로 quotation 필요
x="he llo"
if [ "$x" = "he llo" ]; then echo "$x"; fi

 

  • ==는 shell마다 약간 호환성 차이가 있음, 하나(=)를 써라

 

for/while

for loop은 foreach 형식만 지원

for(int i = 0; i <10; i++)의 형식은 while문으로 작성해야 함

while [ 조건 ]; do
	실행문
done
i=3; while [ "$i" -lt 10 ]; do echo "$i"; i=$((i+1)); done
3
4
5
6
7
8
9

무한루프 :

while :; do
	date
	sleep 5
done

or watch 명령어로 주기적으로 감시 가능

watch -n 5 "date"

for

for 변수명 in 리스트; do
	실행문
done

 

for dir in *; do
//* : 현재 경로의 파일/디렉토리를 dir에 바인딩
	[ -d "$dir" ] && (echo -n "$dir   "; cd "$dir"; ls | wc –l)
    //[ ] 가 참이면 ()를 실행
    // -d dir이 디렉토리냐?
    //에코쓰고 이동하고 안에 있는 파일의 갯수를 찍어(현재 디렉토리 하위 디렉토리 안의 파일 갯수 확인)
    //서브쉘이 끝나면 원래 디렉토리로 돌아옴; 돌아오는 코드 귀찮아서 서브쉘 
    //서브쉘 중간에 실패 시 자동으로 오니까..
done

swtich-case 문

case 변수 in 
케이스1)
	실행문;; 
케이스2) 
	실행문;;
*)
	실행문;;
esac

 

입출력 리다이렉트

 

1(생략) 이거 아니고 0(생략) 이거임! 0번이 표준입력

합칠 수도 있다!

 

필터

프로세서의 출력을 다른 프로세스의 입력으로 전달

  • | 파이프 기호를 이용
  • ls | head : ls의 출력을 head의 입력으로 받아

line by line으로 세서 출현 빈도 수로 정렬

cat test.txt | sort | uniq –c | sort -n
  • sort 알파벳순 정렬
  • uniq -c 중복을 없애면서 중복 갯수를 출력(카운트 옵션)
  • sort -nr  맨 앞을 숫자로 보겠다, 출현 빈도수가 높은걸 맨 앞으로 두고 나머지를 두라(r옵션이 desc)
find /etc 2>&1 | grep 허가
//찾아 여기서 에러랑 아닌거 같이 모아서 허가 찾아

 

문서 보기

cat은 페이징 없음

more/less 페이지씩 보여줌 엔터치면 더 보여줌(pager)

grep 문자열 파일

grep -r (모든 파일에 대해 recursive 하게 검색)

grep bash .bashrc | less -eMR

head 앞에 몇 줄만

tail 뒤에 몇 줄만 tail -f 최근 접속 실시간 출력

 

잘라내서 쓰기

cut –d 구분자 –f 필드번호 파일

구분자로 split해서 시작번호는 1번부터

1,3,4,7번째를 모아서 :로 붙여서 보여줌

grep irteam /etc/passwd | cut –d: –f1,3-4,7

 

shebang(#!)쉐벵

  • 어떤 인터프리터 프로그램을 사용할 것인가를 지정
  • #!/bin/bash
  • #!/usr/bin/env python
    • 권장하는 방식
    • 지정해주면 파이선으로 실행하지 않고 배쉬로 실행해도 배쉬가 첫 줄을 보고(인터프리터) 파이선인지 파악하여 파이선으로 실행 함
    • 리눅스 포직스(?)

 

commane line arguments

$0 자기자신 쉘 이름

$# 변수 몇 개?

$? 실행 결과 0:정상; 0이 아니면 에러

 

설정

Bourne shell

  • ~/.profile

Bash

  • login shell
  • bashrc
  • non-login shell
  • bash_profile -> 이거 하나 만 만들고 그 안에서 rc를 불러오도록 하는게 좋음

바로 적용(source)

source ~/.bashrc

 

find

find 디렉토리 목록 –name 이름패턴 –type 파일유형

find /usr/lib64
find /usr/lib64 –name "lib*.a" –ls
find /usr/lib64 –type d –name "py*"
find /usr/lib64 \( –name "python*" –o –name "pgsql*" \) –ls

 


쉘 문법 체크: shellcheck

컴퓨터에 깔아서 쓸 수도 있고, 인텔리제이 플러그인으로도 있다고 함

https://www.shellcheck.net/

 

ShellCheck – shell script analysis tool

ShellCheck finds bugs in your shell scripts

www.shellcheck.net

 

728x90
반응형

'개발 > shell' 카테고리의 다른 글

[shell] return & exit  (2) 2024.09.25
[shell] > 와 & 에 대한 고찰  (0) 2022.01.11
반응형
  • 인스턴스 여러개 띄운거 -> multi-process
  • 한 인스턴스 안에서 동시 요청 -> multi-thread
    • controller는 스프링에 의해 자동 multi thread(여러 요청이 동시에 들어올 경우)
    • 서비스 로직을 multi-thread로 구현한다는 것은 보통 비동기로 구현하는 것을 의미

 

multi-thread파악

 

11번 우르르쾅쾅 요청하기

curl -v http://localhost:5000/api/setting/enums & curl -v http://localhost:5000/api/setting/enums & curl -v http://localhost:5000/api/setting/enums & curl -v http://localhost:5000/api/setting/enums & curl -v http://localhost:5000/api/setting/enums & curl -v http://localhost:5000/api/setting/enums & curl -v http://localhost:5000/api/setting/enums & curl -v http://localhost:5000/api/setting/enums & curl -v http://localhost:5000/api/setting/enums & curl -v http://localhost:5000/api/setting/enums & curl -v http://localhost:5000/api/setting/enums

 

서버 설정

스래드 맥스를 5로 잡았다. 

springboot 2.7기준

server.tomcat.threads.max=5
server.tomcat.threads.min-spare=1

10초 딜레이를 넣은 api를 요청했다.

 

결과

5-5-1 순으로 쌓였다가 순차 실행한다.

2023-04-25 07:25:52 INFO  [c.n.d.common.filter.RequestLogFilter    .doFilter            :  36][http-nio-5000-exec-4] [REQUEST] [GET] /api/setting/enums
Headers : {host=localhost:5000, user-agent=curl/7.64.1, accept=*/*}
Request body :  - 

2023-04-25 07:25:52 INFO  [c.n.d.common.filter.RequestLogFilter    .doFilter            :  36][http-nio-5000-exec-2] [REQUEST] [GET] /api/setting/enums
Headers : {host=localhost:5000, user-agent=curl/7.64.1, accept=*/*}
Request body :  - 

2023-04-25 07:25:52 INFO  [c.n.d.common.filter.RequestLogFilter    .doFilter            :  36][http-nio-5000-exec-5] [REQUEST] [GET] /api/setting/enums
Headers : {host=localhost:5000, user-agent=curl/7.64.1, accept=*/*}
Request body :  - 

2023-04-25 07:25:52 INFO  [c.n.d.common.filter.RequestLogFilter    .doFilter            :  36][http-nio-5000-exec-1] [REQUEST] [GET] /api/setting/enums
Headers : {host=localhost:5000, user-agent=curl/7.64.1, accept=*/*}
Request body :  - 

2023-04-25 07:25:52 INFO  [c.n.d.common.filter.RequestLogFilter    .doFilter            :  36][http-nio-5000-exec-3] [REQUEST] [GET] /api/setting/enums
Headers : {host=localhost:5000, user-agent=curl/7.64.1, accept=*/*}
Request body :  - 

---> 응답 다 받고

2023-04-25 07:26:02 INFO  [c.n.d.common.filter.RequestLogFilter    .doFilter            :  36][http-nio-5000-exec-2] [REQUEST] [GET] /api/setting/enums
Headers : {host=localhost:5000, user-agent=curl/7.64.1, accept=*/*}
Request body :  - 

2023-04-25 07:26:02 INFO  [c.n.d.common.filter.RequestLogFilter    .doFilter            :  36][http-nio-5000-exec-4] [REQUEST] [GET] /api/setting/enums
Headers : {host=localhost:5000, user-agent=curl/7.64.1, accept=*/*}
Request body :  - 

2023-04-25 07:26:02 INFO  [c.n.d.common.filter.RequestLogFilter    .doFilter            :  36][http-nio-5000-exec-3] [REQUEST] [GET] /api/setting/enums
Headers : {host=localhost:5000, user-agent=curl/7.64.1, accept=*/*}
Request body :  - 

2023-04-25 07:26:02 INFO  [c.n.d.common.filter.RequestLogFilter    .doFilter            :  36][http-nio-5000-exec-5] [REQUEST] [GET] /api/setting/enums
Headers : {host=localhost:5000, user-agent=curl/7.64.1, accept=*/*}
Request body :  - 

2023-04-25 07:26:02 INFO  [c.n.d.common.filter.RequestLogFilter    .doFilter            :  36][http-nio-5000-exec-1] [REQUEST] [GET] /api/setting/enums
Headers : {host=localhost:5000, user-agent=curl/7.64.1, accept=*/*}
Request body :  - 

---> 응답 다 받고

2023-04-25 07:26:12 INFO  [c.n.d.common.filter.RequestLogFilter    .doFilter            :  36][http-nio-5000-exec-1] [REQUEST] [GET] /api/setting/enums
Headers : {host=localhost:5000, user-agent=curl/7.64.1, accept=*/*}
Request body :  -

 

다른 방법:

postman > runner > iteration 을 이용한 run도 응답을 다 받을 때 까지 기다림

https://stackoverflow.com/questions/36157105/postman-how-to-make-multiple-requests-at-the-same-time

 

Postman: How to make multiple requests at the same time

I want to POST data from Postman Google Chrome extension. I want to make 10 requests with different data and it should be at the same time. Is it possible to do such in Postman? If yes, can any...

stackoverflow.com


참고

브라우저로는 테스트가 힘들다..

탭으로 여러개 띄우면 안되고, 브라우저 자체를 여러개 해야하는데 동시 요청이 사실상 힘듦..

정 탭으로 해야하면 url 뒤에 fragment를 다르게 요청하면 된다고 한다.

  • In Chrome, the calls get queued when you call the same resource from two tabs, but executed in parallel when you make the calls from different windows.
  • In IE11, they always get executed in parallel
  • In Firefox, it doesn't matter whether the calls are from different tabs or windows, they always get queued.

All of them execute them in parallel when the URLs are slightly different, by adding a different fragment or parameter.

서버 설정이 1이면 당연히 순차겠지..

https://stackoverflow.com/questions/19774193/subsequent-rest-call-is-blocked-until-previous-one-is-finished

 

Subsequent REST call is blocked until previous one is finished

I Have REST service @Path("/rest") @Component public class MyRestService { @Inject private MyBean bean; @GET @Path("/do") public String start() { this.logger.info("

stackoverflow.com

 

728x90
반응형
반응형

1부 소개


1장 설계와 아키텍처란?

1. 설계(design)와 아키텍처(architecture) 차이: 없다.

  • 아키텍처: 저수준의 세부사항과는 분리된 고수준의 무언가를 가리킬 때
  • 설계: 저수준의 구조 또는결정사항 등을 의미

-> 저수준(세부사항) & 고수준(구조) 모두 소프트웨어 전체 설계의 구성요소; 개별로는 존재할 수 없고 경계도 뚜렷하지 않다.

2. 목표: 필요한 시스템을 만들고 유지보수하는데 투입되는 인력의 최소화

3. 함정: 지저분한 코드를 작성하면 단기간에는 더 빠르게 갈 수있고 생산성 낮아진다?

-> 빨리 가는 유일한 방법은 제대로 가는것이다.

결론

앞으로 클린 아키텍처가 무엇인지 공부해서 비용은 최소화, 생산성은 최대화 할 시스템을 만들자.


2장 두 가지 가치에 대한 이야기

1. 개발자가 높게 유지해야하는 두 가지 가치

  • 행위(behavior)
    • 요구사항 만족하도록 코드를 작성하는것
  • 구조(structure)/아키텍처
    • 변경하기 쉬워야
      • 변경사항을 적용하는데 드는 어려움은 변경되는 범위(scope)에 비례해야하며 변경사항의 형태(shape)와는 관련이 없어야 한다.
    • 아키텍처는 형태에 독립적이어야

-> 보통 행위에만 초점을 두지만, 둘 다 중요하다는 것.

2. 더 높은 가치란?

-> 구조

  • 완벽하게 동작하지만 수정이 아예 불가능한 프로그램 / 변경 비용이 창출비용을 앞서는 프로그램 -> 유지보수 불가능 -> 곧 쓰레기 (x)
  • 동작하지 않지만 변경 쉬운 프로그램 -> 동작하도록 수정 가능, 요구사항 변경시 유지보수 가능 (v)

결론

기능을 개발하기 쉽고, 간편하게 수정할 수 있으며, 확장하기 쉬운 아키텍처를 만들어야 한다.

이를 위해 투쟁하는 것이 곧 개발자의 책임

728x90
반응형
반응형

서로 다른 레이어에서 변수들을 전달할 때 서로 다른 dto를 사용하는데, 얼핏 비슷하면서도 한두 개 다른 변수들을 하나씩 매핑해 주는 게 매우 귀찮고 번거로웠다.

어휴..

그래서 이를 자동(?)으로 해주는 라이브러리 같은 게 있을까 싶어서 몇 개 찾아보았다.

구글링 해보면 여러 개가 나오는데 아래 3개를 먼저 확인해 보았다.

1. 스프링에 기본 내장되어 있는 BeanUtils.copyProperties

2. 사용하기 편해 보이는 ModelMapper

3. 요즘 제일 인기 있는 Mapstruct

 

실제로 사용하려는 목적이기에 현실적인 사용성을 중심으로 살펴보았다.

  dependency di주입 원리(setter 선언 필요?) 필드 커스텀 가능 여부
Mapstruct 추가 필요(4개) 빈 주입 필요
interface 작성
리플랙션X 
- 컴파일 시 작성된 getter/setter or 빌더 or 생성자 등 관련 코드를 이용하여 변환 class 만듦
- setter 없어도 됨
매핑으로 커스텀 설정 가능
ModelMapper 추가 필요(1개) 주입 필요 없음 리플렉션O getter/setter 기반
- 필드명/타입이 동일할 경우 필드 엑서스 레벨을 private으로 설정하면 setter가 필요없지만
- 커스텀 시 setter 선언 필요
커스텀을 위해 typeMap과 addMapping 을 이용하여 손수 매핑해줘야 함
BeanUtils.copyProperties 추가 필요 없음
spring 내장
주입 필요 없음
static method
void type
리플렉션O getter/setter 기반
- setter 선언 필요
- 커스텀 시 setter 수정 필요
더 복잡한 변환이 필요할 경우 사용 불가하며 spring BeanWrapper를 직접 구현해야 함

Mapstruct

lombok과 함께 사용가능(lombok compile 이후에 작동)

implementation 'org.mapstruct:mapstruct:1.5.3.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
testAnnotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
//If you are using Lombok 1.18.16 or newer you also need to add lombok-mapstruct-binding in order to make Lombok and MapStruct work together.
implementation 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
@Mapper(config = CommonMapper.class,
        nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE,
        nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)
public interface CharacterUpdateRequestMapper {

  @Mapping(target = "searchType", source = "findType")
  @Mapping(target = "searchValue", source = "value")
  @Mapping(target = "characterDetail", source = "detailCharacterData")
  CharacterUpdateRequest toRequest(ExternalCharacterUpdateRequest external);

  @Mapping(target = "position", source = "lastSavePosition")
  @Mapping(target = "openedDivisions", ignore = true)
  @Mapping(target = "divisionFlattened", ignore = true)
  CharacterDetail toDetail(ExternalCharacterDetail external);
}
@RequiredArgsConstructor
...
private final CharacterUpdateRequestMapper characterUpdateRequestMapper;

public CharacterUpdateRequest usingMapStruct() {
  return characterUpdateRequestMapper.toRequest(externalCharacterUpdateRequest);
}

필드명이 다를 경우만 mapping을 적어주면 된다(nested object도. 을 사용하여 매핑 가능).

class type이 다르면 converter를 별도로 구현해야 한다.

 

컴파일 시 구현체를 자동으로 생성해 주며 잘못된 매핑을 적을 경우 컴파일이 되지 않기 때문에 런타임 에러를 방지할 수 있다.

참고자료

https://mapstruct.org/documentation/stable/reference/html/#defining-mapper

https://madplay.github.io/post/mapstruct-in-springboot


ModelMapper

implementation 'org.modelmapper:modelmapper:3.1.1'
public class ModelMapperUtils {

  private static final ModelMapper MODEL_MAPPER;

  static {
    MODEL_MAPPER = new ModelMapper();
    MODEL_MAPPER.getConfiguration()
        .setMatchingStrategy(MatchingStrategies.STRICT)
        .setFieldAccessLevel(AccessLevel.PRIVATE)// 필드 전체 이름이 같아서 setter가 필요없다면
        .setFieldMatchingEnabled(true);
  }

  public static CharacterUpdateRequest fromExternalCharacterUpdateRequest(ExternalCharacterUpdateRequest externalCharacterUpdateRequest) {
    //클래스 매핑하고 필드 매핑해야함
    MODEL_MAPPER.createTypeMap(ExternalCharacterUpdateRequest.class, CharacterUpdateRequest.class)
        .addMapping(ExternalCharacterUpdateRequest::getDetailCharacterData, CharacterUpdateRequest::setCharacterDetail)
        .addMapping(ExternalCharacterUpdateRequest::getFindType, CharacterUpdateRequest::setSearchType);
    MODEL_MAPPER.createTypeMap(ExternalCharacterDetail.class, CharacterDetail.class)
        .addMapping(ExternalCharacterDetail::getLastSavePosition, CharacterDetail::setPosition);
    return MODEL_MAPPER.map(externalCharacterUpdateRequest, CharacterUpdateRequest.class);
  }
}

같은 형/타입일 경우 자동으로 매핑해 주고, 변형된 필드일 경우 손수 getter/setter를 사용하여 매핑해줘야 한다.

런타임 시점에 Reflection API를 사용하여 객체를 매핑하기 때문에 컴파일 시점에 성능 최적화를 하지 못하고 다른 방식보다 오버헤드가 많다는 특징이 있다.

참고자료

https://stackoverflow.com/questions/70274381/how-to-map-a-nested-list-using-modelmapper

https://www.baeldung.com/java-modelmapper


BeanUtils.copyProperties

예전에 다른 분께 memory leak 이슈가 있으니 사용에 조심해야 한다고 피드백받았던 적이 있어서 잠시 사용을 안 했었는데, 다시 찾아보니 관련 reference가 잘 나오지 않는다..

1. Circular References (순환 참조)

  • 객체 간에 순환 참조가 있는 경우, 객체가 서로를 참조하게 되어 가비지 컬렉션이 되지 않을 수 있습니다. 이로 인해 메모리 누수가 발생할 수 있습니다.

2. Large Object Graphs

  • 복사하는 객체가 큰 그래프를 가지거나 깊게 중첩된 경우, 복사 과정에서 많은 메모리를 소모할 수 있습니다. 특히, 여러 번 복사할 경우, 사용하지 않는 객체가 메모리에 남아 있을 수 있습니다.

3. Static Fields

  • 복사하려는 객체가 정적 필드를 포함하고 있다면, 이 필드는 여전히 메모리에 남아 있어 메모리 누수가 발생할 수 있습니다. 정적 필드는 클래스 로딩 시 한 번만 메모리에 할당됩니다.

 

dto 내부에서 생성자 및 세팅 함수 작성 시 유리하다.

public CharacterUpdateRequest(ExternalCharacterUpdateRequest externalCharacterUpdateRequest) {
  BeanUtils.copyProperties(externalCharacterUpdateRequest, this);
  super.setSearchValue("custom setting");
}

같은 형/타입일 경우 자동으로 매핑해 주고, 그 외에는 setter 이용하는 등 개발해줘야 한다.

 

변형폼:

//일부만 변환할 때
public static void copyProperties(Object source, Object target, Class<?> editable)

//일부만 제외할 때
public static void copyProperties(Object source, Object target, String... ignoreProperties)

시작은 mapStruct 말고 modelMapper를 쓰려고 시작한 서베이인데,
1. 리플렉션 방식은 필드 개수가 많아지면 성능이 구려짐
2. 결국 getter/setter를 통해 일대일 매핑
3. 가독성이 mapStruct가 훨씬 좋음
위의 이유로 인해 mapStruct 쓰는 게 나을 것 같다는 생각을 하였다.

mapStruct 사용후기

before: 아래와 같은 함수를 2쌍 만들어 주었어야 함

  public static OasisBannerResponse from(StaticOasisBanner entity) {
  return OasisBannerResponse.builder()
      .seq(entity.getSeq())
      .exposureType(entity.getExpsType())
      .regDate(entity.getRegDate())
      .startDate(entity.getStartDate())
      .endDate(entity.getEndDate())
      .linkType(CommonEnumUtil.fromCode(entity.getLinkType().toString(), LinkType.values()).getCode())
      .linkUrl(entity.getLinkUrl())
      .iconImageUrl(entity.getIconImgUrl())
      .backgroundColor(entity.getBackgroundColor())
      .koreanText(entity.getKoText())
      .englishText(entity.getEnText())
      .registerer(entity.getRegisterer())
      .updater(entity.getUpdater())
      .build();
}

after: 인터페이스에 변수 매핑하면 끝..

object -> entity 한방향 매핑만 잘 연결해 놓으면 역방향 매핑(entity -> object)은 @InheritInverseConfiguration만 달아주면 알아서 매핑해준다는게 너무 편리하였다.

@Mapper(config = CommonMapper.class,
        nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE,
        nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)
public interface OasisBannerMapper {

  @Mapping(target = "exposureType", source = "expsType")
  @Mapping(target = "iconImageUrl", source = "iconImgUrl")
  @Mapping(target = "koreanText", source = "koText")
  @Mapping(target = "englishText", source = "enText")
  @Mapping(target = "linkType", expression = "java(convertLinkType(entity.getLinkType()))")
  OasisBannerResponse toResponse(StaticOasisBanner entity);


  @Mapping(target = "extra", ignore = true)
  @Mapping(target = "lastDate", ignore = true)
  @InheritInverseConfiguration
  StaticOasisBanner toEntity(OasisBannerResponse request);

 

그러나...

단점 1. 커스텀이 (생각보다) 쉽지 않다(러닝 커브가 있음)

적절한 설정값을 확인해야 하고 다양한 매핑을 지원해야 하는지 확인해야 하는 등, 특이점이 많은 매핑에는 아직 시간이 걸린다(가끔은 그냥 빌더가 더 편하다).

예시로..

  @Mapping(target = "expsType", source = "request.exposureType")
  @Mapping(target = "iconImgUrl", source = "request.iconImageUrl")
  @Mapping(target = "koText", source = "request.koreanText")
  @Mapping(target = "enText", source = "request.englishText")
  @Mapping(target = "extra", ignore = true)
  @Mapping(target = "lastDate", ignore = true)
  @Mapping(target = "updater", source = "updater")
//  @InheritInverseConfiguration
  StaticOasisBanner toEntity(OasisBannerResponse request, String updater);

위처럼 argument가 여러 개일 경우 @InheritInverseConfiguration를 사용하지 못하고 하나씩 다 매핑해줘야 한다.(이걸로 못 찾고 시간을 허비함..)

@InheritInverseConfiguration를 사용하여 반대 매핑도 자동으로 되는 게 매력적이었는데, 다시 수동으로 하려니 메리트가 절감된 느낌

 

단점 2. 에러가 친절하지 못함

단점 1에 기술한대로 추가 arguments가 있을 때는 모두 매핑해야 한다는 사실을 모르고 처음에는 생각대로 해봤는데, 빌드조차 되지 않았다. 그런데 에러 내용이 갑자기 querydsl 관련 에러였다?

처음에는 빌드창을 조금만 띄워놓았어서 에러내용을 끝까지 못 보고 querydsl 설정이 꼬였나 싶어서 거기만 팠는데,, 에러창을 더 열어보니 결국은 mapstruct 빌드로 부터 시작한 것이었다.

파악해 보니, 빌드 순서가 lombok -> mapstruct -> querydsl 순이었고

mapstruct 빌드가 실패하면 querydsl도 빌드되지 않기 때문에 에러메시지는 querydsl이 실패한 것처럼 보이나,

실제로 에러의 시작점은 mapstruct이었던 것이다(처음에 이것을 모르고 querydsl 설정이 문제라고 생각하여 시간을 허비함).

 

단점 3. @MappingTarget사용 시 setter 사용

@InheritConfiguration
void setEntity(OasisBannerResponse request, @MappingTarget StaticOasisBanner entity);

위처럼 @MappingTarget을 이용하면 setter 기반으로 매핑을 만들어주기 때문에 target 객체에 setter를 선언해야 한다..

setter를 최소화하고 싶었는데.... 고민이 더 필요할 것 같다.

기존 <interface>

@Mapping(target = "exposureType", source = "expsType")
@Mapping(target = "iconImageUrl", source = "iconImgUrl")
@Mapping(target = "koreanText", source = "koText")
@Mapping(target = "englishText", source = "enText")
@Mapping(target = "linkType", source = "linkType", qualifiedByName = "convertToLinkTypeFrom")
OasisBannerResponse toResponse(StaticOasisBanner entity);

-> 구현체: Builder를 이용한 객체 생성(builder -> constructor -> getter/setter 순으로 찾아서 구현체 생성)

@Override
public OasisBannerResponse toResponse(StaticOasisBanner entity) {
    if ( entity == null ) {
        return null;
    }

    OasisBannerResponse.OasisBannerResponseBuilder oasisBannerResponse = OasisBannerResponse.builder();

    if ( entity.getExpsType() != null ) {
        oasisBannerResponse.exposureType( entity.getExpsType() );
    }
    ... 생략

 

<interface> MappingTarget 사용 시

 

@InheritConfiguration
void setEntity(OasisBannerRequest request, @MappingTarget StaticOasisBanner entity);

 

-> 구현체: setter를 이용한 매핑

@Override
public void setEntity(OasisBannerRequest request, StaticOasisBanner entity) {
    if ( request == null ) {
        return;
    }

    entity.setExpsType( request.getExposureType() );
    if ( request.getIconImageUrl() != null ) {
        entity.setIconImgUrl( request.getIconImageUrl() );
    }
    
    ... 생략

 

단점 4. lombok의 @Singular와 함께 사용 불가한 듯

<before> 기존에는 아래처럼 구현했었음

public class OasisBannerResponse {

    @JsonInclude(value = JsonInclude.Include.NON_DEFAULT)
    @JsonProperty("freeWayName")
    @Singular
    private List<CodeValueModel> freeWayNames;

... 변수 생략

    public static OasisBannerResponse from(StaticOasisBanner entity) {
      return OasisBannerResponse.builder()
          .startDate(entity.getStartDate())
          .endDate(entity.getEndDate())
          .linkType(entity.getLinkType())
          .linkUrl(entity.getLinkUrl())
          .iconImageUrl(entity.getIconImgUrl())
          .backgroundColor(entity.getBackgroundColor())
          .freeWayName(new CodeValueModel(LanguageType.KO.getCode(), entity.getKoText()))
          .freeWayName(new CodeValueModel(LanguageType.EN.getCode(), entity.getEnText()))
          .build();
	}
}

<after> mapStruct 사용

기댓값: singular처럼 매핑 -> but 컴파일 에러 남

  @Mapping(target = "iconImageUrl", source = "iconImgUrl")
  @Mapping(target = "freeWayName", source = "enText", qualifiedByName = "addEnglish")
  @Mapping(target = "freeWayName", source = "koText", qualifiedByName = "addKorean")
  OasisBannerResponse toResponse(StaticOasisBanner entity);

  @Named("addKorean")
  default CodeValueModel addKorean(String koText) {
    return toCodeValueModel(LanguageType.KO, koText);
  }

  @Named("addEnglish")
  default CodeValueModel addEnglish(String enText) {
    return toCodeValueModel(LanguageType.EN, enText);
  }
error: Target property "freeWayName" must not be mapped more than once.

error: Unmapped target property: "freeWayNames".

여러 개 있어서 싫고,,매핑을 안 해서 싫더냐?! 거참 까다로운 자식..

해결: 결국.. entity를 다 받아서 했다..ㅠ

@Mapping(target = "iconImageUrl", source = "iconImgUrl")
@Mapping(target = "freeWayNames", source = ".", qualifiedByName = "setFreeWayNames")
OasisBannerResponse toResponse(StaticOasisBanner entity);

@Named("setFreeWayNames")
default List<CodeValueModel> setFreeWayNames(StaticOasisBanner entity) {
  List<CodeValueModel> codes = new ArrayList<>();
  setLanguageText(LanguageType.KO, entity.getKoText(), codes);
  setLanguageText(LanguageType.EN, entity.getEnText(), codes);
  return codes;
}

그리고 

@Singular
private List<CodeValueModel> freeWayNames;

@Singular 선언이 있으면 위처럼 freeWayNames에 매핑을 했음에도 freeWayName도 매핑하라는 에러가 나고, 결국 @Singular를 지워야만 한다.

error: Unmapped target property: "freeWayName".

즉, 두개를 동시에 못쓴다는 이야기..


참고

1. 함수를 통해 변수를 변환하고 매핑해야 하는 경우

여러 가지 방법이 있겠지만 expression = "java()" 이 방법은 코드를 하드코딩하는 방법이라 잘못된 점을 빌드할 때까지 알 수 없음..

qualifiedByName = "" 방법을 사용하는 게 더 나은 것 같다.

enum을 더 효율적으로 매핑할 수 있는지 확인 필요

2. 설정을 함수별로 세팅할 수 있음

@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE,
             nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)
void update(AmcPackageRequest amcPackageRequest, @MappingTarget AmcPackage amcPackage);

3. MappingConfig 안에서 세부 설정을 할 수 있음

특히 collection mapping관련해서 설정값을 확인할 필요 있음


참고

성능: https://www.baeldung.com/java-performance-mapping-frameworks

사용방법: 

https://medium.com/naver-cloud-platform/%EA%B8%B0%EC%88%A0-%EC%BB%A8%ED%85%90%EC%B8%A0-%EB%AC%B8%EC%9E%90-%EC%95%8C%EB%A6%BC-%EB%B0%9C%EC%86%A1-%EC%84%9C%EB%B9%84%EC%8A%A4-sens%EC%9D%98-mapstruct-%EC%A0%81%EC%9A%A9%EA%B8%B0-8fd2bc2bc33b

https://jiwondev.tistory.com/250

728x90
반응형
반응형

환경: springboot 2.7.3, java11, gradle 7.5

테스트를 돌리는데

분명 이전에 성공했던 테스트인데

시간이 흐른 뒤 재실행했을 때 아래와 같은 에러가 났다.

Execution failed for task ':test'. > No tests found for given includes:

 

그 사이 코드가 바뀐 것도 없어 안될 리가 없을 터.

게다가 다른 클래스의 테스트 코드는 잘 실행되고 한 클래스의 테스트코드만 안돼서 이상했다.

 

구글링 해보면

junit vintage 버전 충돌 어쩌구라고 하는데 나에게는 해당되지 않는 듯하여 과감하게 버리고

https://www.inflearn.com/questions/15495/%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%8F%84%EC%A4%91-%EC%97%90%EB%9F%AC-%EB%B0%9C%EC%83%9D

 

테스트 도중 에러 발생 - 인프런 | 질문 & 답변

FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':test'. > No tests found for given includes: [jpabook.jpashop....

www.inflearn.com

여기 문제와 비슷한 것 같아서 설정을 바꿔보았는데도 동일하였다.

 

한 30분 씨름했는데,, 해결은 

invalid cache 날리고 혹시 몰라서 gradle 클린하고 테스트 실행하니까 잘되었다..ㅎㅎㅎㅎ 츠암나..

 

728x90
반응형
반응형

Entity Graph

  • Entity를 조회하는 시점에 연관 Entity들을 함께 조회할 수 있도록 해주는 기능

종류

  • 정적 선언 - @NamedEntityGraph
  • 동적 선언 - EntityManager.createEntityGraph()
@NamedEntityGraphs({
        @NamedEntityGraph(name = "orderWithCustomer", attributeNodes = { //같이 가져와
                @NamedAttributeNode("customer")
        }),
        @NamedEntityGraph(name = "orderWithOrderItems", attributeNodes = {
                @NamedAttributeNode("orderItems")
        }),
        @NamedEntityGraph(name = "orderWithCustomerAndOrderItems", attributeNodes = {
                @NamedAttributeNode("customer"),
                @NamedAttributeNode("orderItems")
        }),
        @NamedEntityGraph(name = "orderWithCustomerAndOrderItemsAndItem", attributeNodes = {
                @NamedAttributeNode("customer"),
                @NamedAttributeNode(value = "orderItems", subgraph = "orderItems")
        }, subgraphs = @NamedSubgraph(name = "orderItems", attributeNodes = { //orderItem 한 단계 더 들어갈 때
                @NamedAttributeNode("item")
        }))
})
@Getter
@Setter
@Entity
@Table(name = "Orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "order_id")
    private Long orderId;

    @ManyToOne(optional = false)
    @JoinColumn(name = "customer_id")
    private Customer customer;

    @Column(name = "order_dt")
    private LocalDateTime orderDate;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "order_id")
    private List<OrderItem> orderItems;

}
public interface OrderRepository extends JpaRepository<Order, Long> {
    //get, read, query, find -> select문
    //All -> 의미 없음; 뭘 가져올지는 return type으로
    //by -> 조건

    @EntityGraph("orderWithCustomer")
    //select * from order left join customer -> 커스토머 가져오고 
    //select * from orderItem left join item -> 추가로 발생
    List<Order> getAllBy();

    @EntityGraph("orderWithOrderItems")
    //select * from order left join orderItem -> 한방에
    List<Order> readAllBy();

    @EntityGraph("orderWithCustomerAndOrderItems")
    //select * from order left join customer left join orderItem -> 한방에
    List<Order> queryAllBy();

   @EntityGraph("orderWithCustomerAndOrderItemsAndItem")
    //select * from order left join customer left join orderItem left join item -> 한방에
    List<Order> findAllBy();

}

Pagination 쿼리에 Fetch Join

Pagination 쿼리에 Fetch Join을 적용하면 실제로는 모든 레코드를 가져오는 쿼리가 실행된다

: 다 가져와서 필요한 부분만 발라서 줌 

: DB 서버는 전체를 부르게 되니 부하 오짐

실제로는 에러가 아닌 warning에 아래와 같은 메세지가 나고 있었다..

디비에서는 다가져와서 메모리에서 할게 ㅎㅎ

HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
public Page<Order> getPagedOrderWithAssociations(Pageable pageable) {
    QOrder order = QOrder.order;
    QCustomer customer = QCustomer.customer;
    QOrderItem orderItem = QOrderItem.orderItem;
    QItem item = QItem.item;

    // TODO : pagination + fetch join ???
    //사라진 limit
    JPQLQuery<Order> query = from(order)
            .innerJoin(order.customer, customer).fetchJoin()
            .leftJoin(order.orderItems, orderItem).fetchJoin()
            .innerJoin(orderItem.item, item).fetchJoin();

    JPQLQuery<Order> pagedQuery = getQuerydsl().applyPagination(pageable, query);

    long totalCount = 0L;

    try {
        totalCount = pagedQuery.fetchCount();
    } catch (NoResultException e) {
        // ignore
    }

    List<Order> list = pagedQuery.fetch();

    return new PageImpl<>(list, pageable, totalCount);
}

 

해결 방법? 정해진 해결방법은 없음.

  • Pagination 쿼리와 Fetch Join을 분리
  • Pagination 쿼리에서는 entity가 아닌 ID만을 가져온다(where절을 만족하는 놈으로 미리 쳐버리는게 효율이 좋겠지)
  • Fetch Join에는 앞서 가져온 ID 조건을 추가

Q. 이럴 때 in절로 인한 디비 부하가 있을 수 있을텐데.. 괜찮나? 물론 전체를 가져오는 것 보다는 나을 것이며 PK니까 관련 인덱스도 있겠지만... DBA는 in절을 매우 싫어했는데ㅠ

public Page<Order> getPagedOrderWithAssociations(Pageable pageable) {
    QOrder order = QOrder.order;
    QCustomer customer = QCustomer.customer;
    QOrderItem orderItem = QOrderItem.orderItem;
    QItem item = QItem.item;

    // TODO #1 : pagination query
    //여기서 where절 미리 쳐버렸!
    JPQLQuery<Long> query = from(order).select(order.orderId);
    JPQLQuery<Long> pagedQuery = getQuerydsl().applyPagination(pageable, query);

    long totalCount = 0L;
    try {
        totalCount = pagedQuery.fetchCount();
    } catch (NoResultException ex) {
        // ignore
    }

    List<Long> ids = pagedQuery.fetch();

    // TODO #2 : fetch join
    List<Order> list = from(order)
            .innerJoin(order.customer, customer).fetchJoin()
            .leftJoin(order.orderItems, orderItem).fetchJoin()
            .innerJoin(orderItem.item, item).fetchJoin()
            .where(order.orderId.in(ids))
            .fetch();

    return new PageImpl<>(list, pageable, totalCount);
}

 

cf.) pagination query 에서 offset, limit 에 bind 된 parameter 값은 왜 log에 안 나오죠?

limit 뒤의 값을 알고싶어요..

select 
        order0_.order_id as order_id1_3_,
        order0_.customer_id as customer3_3_,
        order0_.order_dt as order_dt2_3_ 
from orders order0_
where
        order0_.order_id=?
limit ?
o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]

이유?

  • org.hibernate.type.descriptor.sql.BasicBinder
    • bind 된 parameter 값을 logging
  • offset, limit 는 DBMS 별로 지원이 될 수도 있고 안 될 수도 있는 쿼리
    • cf.) org.hibernate.dialect.pagination.LimitHandler
      • MySQLDialect vs Oracle8iDialect
  • offset, limit 는 BasicBinder 에서 처리가 되지 않음
  • dialect 에서도 logging을 해주지 않고 있음
package org.hibernate.dialect.pagination;

...

public interface LimitHandler {
  boolean supportsLimit(); //각 dbms에서 구현

  boolean supportsLimitOffset();
public class MySQLDialect extends Dialect {
  private static final Pattern ESCAPE_PATTERN = Pattern.compile("\\", 16);
  public static final String ESCAPE_PATTERN_REPLACEMENT = Matcher.quoteReplacement("\\\\");
  private final UniqueDelegate uniqueDelegate;
  private final MySQLStorageEngine storageEngine;
  private static final LimitHandler LIMIT_HANDLER = new AbstractLimitHandler() {
    public String processSql(String sql, RowSelection selection) {
      boolean hasOffset = LimitHelper.hasFirstRow(selection);
      return sql + (hasOffset ? " limit ?, ?" : " limit ?");
    }

    public boolean supportsLimit() {
      return true;
    }
  };

ㅋ 구현체에서 안 찍어줌 

해결방법

  • 굳이 offset, limit 값을 로깅하길 원한다면
    • log4jdbc와 같은 JDBC 레벨에서의 로깅이 가능한 라이브러리를 써야

둘 이상의 컬렉션을 Fetch Join하는 경우

  • Order Entity에 OrderAttribute Entity로의 일대다(1:N) 연관관계를 추가하는 경우
    • Order-OrderDetails (1:N)
    • Order-OrderAttributes (1:N)

둘 이상의 컬렉션을 Fetch Join하는 경우 MultipleBagFetchException 발생

@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "order_id")
private List<OrderItem> orderItems;

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "order_id")
private List<OrderAttribute> attributes;

//
  public List<Order> getOrdersWithAssociations() {
        QOrder order = QOrder.order;
        QCustomer customer = QCustomer.customer;
        QOrderItem orderItem = QOrderItem.orderItem;
        QItem item = QItem.item;
        QOrderAttribute attribute = QOrderAttribute.orderAttribute;

        return from(order)
                .innerJoin(order.customer, customer).fetchJoin()
                .leftJoin(order.orderItems, orderItem).fetchJoin()
                .leftJoin(order.attributes, attribute).fetchJoin()
                .innerJoin(orderItem.item, item).fetchJoin()
                .fetch();
    }
org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: 
[com.nhn.edu.jpa.entity.Order.orderItems, com.jpa.entity.Order.attributes]

트러블 슈팅 - 둘 이상의 컬렉션을 Fetch Join하는 경우

MultipleBagFetchException

  • Hibernate는 collection type으로 list, set, bag, map 등 다양한 타입을 지원
  • Java의 java.util.List 타입은 기본적으로 Hibernate의 Bag 타입으로 맵핑됨
  • Bag은 Hibernate에서 중복 요소를 허용하는 비순차(unordered) 컬렉션
  • 둘 이상의 컬렉션(Bag)을 Fetch Join하는 경우, 그 결과로 만들어지는 카테시안 곱(Cartesian Product)에서
    어느 행이 유효한 중복을 포함하고 있고 어느 행이 그렇지 않은 지 판단할 수 없어 Bag 컬렉션으로 변환될 수 없기 때문에 MultipleBagFetchException 예외 발생
    • 조인 할 때 row에 옆으로 쫙 늘어나면서 데이터가 n*n으로 나올거잖슴,, 반복되는 내용 때문인지 중복이 왜 나는지 구분이 안되니
    • 애초에 결과 값에 중복 허용 안하면 카테시안 곱에 의한 중복이 아니라는게 확실해짐

해결 방법

1. List를 Set으로 변경 : 중복 비허용

  • 도메인이나 비즈니스 로직에 따라 중복이 나오는게 맞는 것인지 고려해보고 적용
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "order_id")
private Set<OrderItem> orderItems;

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "order_id")
private Set<OrderAttribute> attributes;

//

    @Override
    public List<Order> getOrdersWithAssociations() {
        QOrder order = QOrder.order;
        QCustomer customer = QCustomer.customer;
        QOrderItem orderItem = QOrderItem.orderItem;
        QItem item = QItem.item;
        QOrderAttribute attribute = QOrderAttribute.orderAttribute;

        return from(order)
                .innerJoin(order.customer, customer).fetchJoin()
                .leftJoin(order.orderItems, orderItem).fetchJoin()
                .leftJoin(order.attributes, attribute).fetchJoin()
                .innerJoin(orderItem.item, item).fetchJoin()
                .fetch();
    }

2. @OrderColumn 적용 : 순서를 부여(ordered)

  • 디비 스키마를 변경할 수 있는지 고려해야함
WARN QueryTranslatorImpl   : HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "order_id")
@OrderColumn //순서를 부여함; 중복 허용
private List<OrderItem> orderItems;

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "order_id")
@OrderColumn
private List<OrderAttribute> attributes;
Hibernate: 
    
    create table order_attributes (
       order_attribute_id bigint generated by default as identity,
        attribute varchar(255),
        order_id bigint,
        attributes_order integer, ///순서를 저장하기 위한 콜롬이 추가로 생성된
        primary key (order_attribute_id)
    )
Hibernate: 
    
    create table order_items (
       order_line_id bigint generated by default as identity,
        quantity bigint,
        item_id bigint,
        order_id bigint,
        order_items_order integer, ///순서를 저장하기 위한 콜롬이 추가로 생성된
        primary key (order_line_id)
    )

1, 2번으로 해결이 안되면 Jpa를 쓰지말자! ㅋㅋ


Repository: spring이 제공하는 데이터에 접근하는 layer

  • Spring Data Repository
  • Repository는 JPA의 개념이 아니고, Spring Framework가 제공해주는 것임.
    • data access layer 구현을 위해 반복해서 작성했던, 유사한 코드를 줄일 수 있는 추상화 제공

 

이름 규칙으로 join 쿼리 가져오기(N+1 날 수 있음)

public interface MemberRepository extends JpaRepository<Member, Long> {
    // select m from Member m inner join MemberDetail md where md.type = ?
    // 연관관계가 있을때만 가능
    // _ 로 안으로 들어갈 수 있음
    List<Member> findByDetails_Pk_Type(String type);
}

 

DTO Projection

DTO Projection 이란: entity 바를 때

  • Repository 메서드가 Entity를 반환하는 것이 아니라 원하는 필드만 뽑아서 DTO(Data Transfer Object)로 반환하는 것
  • 메모리나 디비 성능에 도움

Dto Projection 방법

  • Interface 기반 Projection: 아래에 계속!
  • Class 기반 (DTO) Projection: 생성자
  • Dynamic Projection: runtime에 결정

 

트러블 슈팅 - Spring Data Repository 로는 Dto Projection을 할 수 없다?

Spring Data Repository를 이용한 Dto Projection

  • Repository 메서드의 반환 값으로 Entity가 아닌 Dto를 사용할 수 있다
  • interface / class
  • @Value + SpEL (target)
public interface OrderRepository extends OrderRepositoryCustom, JpaRepository<Order, Long> {
    List<OrderDto> findAllBy();
}

public interface OrderDto {
    Long getOrderId();
    CustomerDto getCustomer();
    List<OrderItemDto> getOrderItems();

    interface CustomerDto {
        String getCustomerName();
    }

    interface OrderItemDto {
        ItemDto getItem();
        Long getQuantity();
    }

    interface ItemDto {
        String getItemName();
        Long getItemPrice();
    }
}
Hibernate: 
    select
        order0_.order_id as order_id1_3_,
        order0_.customer_id as customer3_3_,
        order0_.order_dt as order_dt2_3_ 
    from
        orders order0_
Hibernate: 
    select
        customer0_.customer_id as customer1_0_0_,
        customer0_.customer_name as customer2_0_0_ 
    from
        customers customer0_ 
    where
        customer0_.customer_id=?

Hibernate: 
    select
        customer0_.customer_id as customer1_0_0_,
        customer0_.customer_name as customer2_0_0_ 
    from
        customers customer0_ 
    where
        customer0_.customer_id=?

Hibernate: 
    select
        orderitems0_.order_id as order_id4_2_0_,
        orderitems0_.order_line_id as order_li1_2_0_,
        orderitems0_.order_line_id as order_li1_2_1_,
        orderitems0_.item_id as item_id3_2_1_,
        orderitems0_.quantity as quantity2_2_1_,
        item1_.item_id as item_id1_1_2_,
        item1_.item_name as item_nam2_1_2_,
        item1_.item_price as item_pri3_1_2_ 
    from
        order_items orderitems0_ 
    left outer join
        items item1_ 
            on orderitems0_.item_id=item1_.item_id 
    where
        orderitems0_.order_id=?

Hibernate: 
    select
        orderitems0_.order_id as order_id4_2_0_,
        orderitems0_.order_line_id as order_li1_2_0_,
        orderitems0_.order_line_id as order_li1_2_1_,
        orderitems0_.item_id as item_id3_2_1_,
        orderitems0_.quantity as quantity2_2_1_,
        item1_.item_id as item_id1_1_2_,
        item1_.item_name as item_nam2_1_2_,
        item1_.item_price as item_pri3_1_2_ 
    from
        order_items orderitems0_ 
    left outer join
        items item1_ 
            on orderitems0_.item_id=item1_.item_id 
    where
        orderitems0_.order_id=?

Hibernate: 
    select
        orderitems0_.order_id as order_id4_2_0_,
        orderitems0_.order_line_id as order_li1_2_0_,
        orderitems0_.order_line_id as order_li1_2_1_,
        orderitems0_.item_id as item_id3_2_1_,
        orderitems0_.quantity as quantity2_2_1_,
        item1_.item_id as item_id1_1_2_,
        item1_.item_name as item_nam2_1_2_,
        item1_.item_price as item_pri3_1_2_ 
    from
        order_items orderitems0_ 
    left outer join
        items item1_ 
            on orderitems0_.item_id=item1_.item_id 
    where
        orderitems0_.order_id=?

하지만 N+1은 난다.

예제

  • interface / class
  • @Value + SpEL (target)
/*
{
    "name": "",
    "details": [{
        "type": "",
        "description": ""
    }]
}
 */
public interface MemberDto {
  String getName();
  List<MemberDetailDto> getDetails();

   interface MemberDetailDto{
    //target = memberDetailEntity
     @Value("#{target.pk.type}") ///!
    String getType();
    String getDescription();
  }
//아래처럼 하면 pk 안의 type으로 나옴
//  interface MemberDetailDto{
//    PkDto getPk();
//    String getDescription();
//
//    interface PkDto{
//      String getType();
//    }
//  }
}
728x90
반응형
반응형

JPA (Java Persistence API)

  • 자바 ORM 기술 표준
  • 표준 명세
    • JSR 338 - Java Persistence 2.2

JPA (Jakarta Persistence API)

  • Jakarta Persistence 3.1

JPA 주요 Spec 및 Hibernate version

Java Persistence 2.2 (Hibernate 5.3+)

  • Stream query results
  • @Repeatable annotations
  • Support Java 8 Date and Time API
  • Support CDI Injection in AttributeConverters

Jakarta Persistence 3.1 (Hibernate 6.1+)

  • UUID as Basic Java Type
  • JPQL / Criteria API의 확장 - 날짜, 숫자 함수 추가 등
  • ...

Hibernate 최신 버전

  • Hibernate 5.6
  • Hibernate 6.1

springboot 2.7.9 -> hibernate 5.6

JPA는 스펙; hibernate는 구현


SQL 설정

spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.SQL=debug

binding parameters :: request ?에 대한 바인딩 로그

logging.level.org.hibernate.type.descriptor.sql.BasicBinder=trace
  • cf.) org.hibernate.type.descriptor.sql.BasicExtractor :: result set 보여줌

기본 키 맵핑 전략

@GeneratedValue(strategy = GenerationType.IDENTITY)

자동 생성

  • TABLE 전략: 채번 테이블을 사용
  • SEQUENCE 전략: 데이터베이스 시퀀스를 사용해서 기본 키를 할당
    • ex.) Oracle
  • IDENTITY 전략: 기본 키 생성을 데이터베이스에 위임
    • ex.) MySQL
  • AUTO 전략: 선택한 데이터베이스 방언(dialect)에 따라 기본 키 맵핑 전략을 자동으로 선택

직접 할당

  • 애플리케이션에서 직접 식별자 값을 할당

복합 Key (Composite key)

  • @IdClass
  • @EmbeddedId / @Embeddable

복합 Key Class 제약조건

  • public class
  • public 기본(no-arg) 생성자
  • Serializable 인터페이스 구현
  • equals(), hashCode() 메서드 정의

영속성 전이 (cascade)

바뀔 때 같이 바뀔래? 삭제/수정같이?

  • Entity의 영속성 상태 변화를 연관된 Entity에도 함께 적용
  • 연관관계의 다중성 (Multiplicity) 지정 시 cascade 속성으로 설정
@OneToOne(cascade = CascadeType.PERSIST)
@OneToMany(cascade = CascadeType.ALL)
@ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.REMOVE })

cascade 종류

안 쓰면 각각 저장; 연관관계도 같이 저장

-> 멤버 저장하면 멤버 디테일도 저장

근데 두 엔티티의 lifecycle이 항상 같을 수 없어,, 진짜 필요할 때 고려해서 넣는 게 좋음

public enum CascadeType {
    ALL,        /* PERSIST, MERGE, REMOVE, REFRESH, DETACH */
    PERSIST,    // cf.) EntityManager.persist()
    MERGE,      // cf.) EntityManager.merge()
    REMOVE,     // cf.) EntityManager.remove()
    REFRESH,    // cf.) EntityManager.refresh()
    DETACH      // cf.) EntityManager.detach()
}

연관관계의 방향성

  • 단방향(unidirectional)
  • 양방향(bidirectional)

양방향 연관 관계

  • 관계의 주인(owner)
    • 양방향의 연관 관계의 주인은 외래 키(FK)가 있는 곳
      • 주인만 @JoinColumn 어노테이션 사용 가능
    • 연관 관계의 주인이 아닌 경우, mappedBy 속성으로 연관 관계의 주인을 지정

단방향 vs 양방향

단방향 맵핑만으로 연관관계 맵핑은 이미 완료

  • JPA 연관관계도 내부적으로 FK 참조를 기반으로 구현하므로 본질적으로 참조의 방향은 단방향

단방향에 비해 양방향은 복잡하고 양방향 연관관계를 맵핑하려면 객체에서 양쪽 방향을 모두 관리해야 함

  • 물리적으로 존재하지 않는 연관관계를 처리하기 위해 mappedBy 속성을 통해 관계의 주인을 정해야 함

단방향을 양방향으로 만들면 반대 방향으로의 객체 그래프 탐색 가능

  • 우선적으로는 단방향 맵핑을 사용하고 반대 방향으로의 객체 그래프 탐색 기능이 필요할 때 양방향을 사용

 

  • 일반적으로 단방향으로도 충분하지만 그게 아닌 경우가 있다.
    • 일대다 연관관계 시 '다'에 해당하는 양만큼 update문이 나갈 수 있음 -> 양방향 연관관계 필요
    • 복합키까지 쓰는 경우라면, 그리고 그 값이 FK에도 쓰는 경우라면 @MapsId로 지정해야 한다.
  • 예시: 단방향으로 설정 시 원치 않은 update문이 나갈 수 있음
class Member{
...
    @OneToMany(cascade = CascadeType.ALL) //member 바뀌면 아래도 알아서 바뀌라
    @JoinColumn(name = "member_id")
    private List<MemberDetail> details = new ArrayList<>();

}


//CASCADE 넣으면 이거 하나로 끝
  memberRepository.save(member);
Hibernate: insert into members (create_dt, name, member_id) values (?, ?, ?)
Hibernate: insert into member_details (description, type, member_detail_id) values (?, ?, ?)
Hibernate: insert into member_details (description, type, member_detail_id) values (?, ?, ?)
Hibernate: update member_details set member_id=? where member_detail_id=?
Hibernate: update member_details set member_id=? where member_detail_id=?

insert 할 때 한 번에 하면 되지 않나 왜 update를?

 

해결: 양방향 일대다(1:N)로 변경

class Member{
...
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "member")
    private List<MemberDetail> details = new ArrayList<>();
}
//

class MemberDetail{
...
    @ManyToOne
    @JoinColumn(name = "member_id")//column이름; 양방향일 때 관계 주인은 fk를 가지고 있는 여기!
    private Member member;
}

///
//양방향이기 때문에 양쪽으로 다 세팅해야 함
memberDetail1.setMember(member);

member.getDetails().add(memberDetail1);
 Repeated column in mapping for entity: com.jpa.entity.MemberDetail column: member_id (should be mapped with insert="false" update="false")

근데 에러가 남 두둥

@Entity
@Table(name = "MemberDetails")
public class MemberDetail {
    @EmbeddedId
    private Pk pk;

    private String description;

    @ManyToOne
    @JoinColumn(name = "member_id")///
    private Member member;

    @Getter
    @Setter
    @NoArgsConstructor
    @EqualsAndHashCode
    @Embeddable
    public static class Pk implements Serializable {
        @Column(name = "member_id") ////
        private Long memberId;

        private String type;
    }
}

@JoinColumn, @pk안의 @Column 모두 업데이트 가능하게 만들어서 에러

하나는 조회용이라고 밝혀줘야

@ManyToOne
@JoinColumn(name = "member_id", insertable = false, updatable = false)
private Member member;

그러면 이렇게 하면 될까? nope!
왜냐면 cascade 때문에. 그걸로 업데이트/인서트 하겠다고 한 건데 안 하겠다고 하면(updatable false) 안되지

그렇다면?

@ManyToOne
@MapsId("memberId")
private Member member;

pk에도 쓰이는 칼럼이 @JoinColumn에서도 써야 한다면, 같은 거를 쓴다고 알려줘야 함

MapsId 만 써도 되는데 PK가 복합키라서 그중에 뭐? 를 알려줘야 할 때

PK에서 사용되는 콜롬이 FK에도 쓰인다! @MapsId(변수명)

 

그러면 insert 세 개만 나간다!

정리: 양방향을 맺자

@Entity
@Table(name = "Orders")
public class Order {
...

    @OneToMany(mappedBy = "order", cascade = CascadeType.Merge, CasecadeType.PERSIST)//order 저장 시 detail도 저장하게 하려면 여기다가 cascade option 필요
    private List<OrderDetail> details = new ArrayList<>();
}

////

@Entity
@Table(name = "OrderDetails")
public class OrderDetail {
...
    @EmbeddedId
    private Pk pk = new Pk();

    @ManyToOne
//    @JoinColumn(name = "order_id") //원래대로라면 이렇게 하지만 PK에도 사용되니..
    @MapsId("orderId")
    private Order order;
    
    @Getter
    @Setter
    @NoArgsConstructor
    @EqualsAndHashCode
    @Embeddable
    public static class Pk implements Serializable {
        @Column(name = "order_id")
        private Long orderId;
        private String type;
    }
}

////

 @Transactional
public void doSomething() {
    Order order = new Order();
    order.setOrderDate(LocalDateTime.now());

    OrderDetail orderDetail1 = new OrderDetail("type1");
    orderDetail1.setDescription("order1-type1");
    orderDetail1.setOrder(order);

    OrderDetail orderDetail2 = new OrderDetail("type2");
    orderDetail2.setDescription("order1-type2");
    orderDetail2.setOrder(order);

    order.getDetails().add(orderDetail1);
    order.getDetails().add(orderDetail2);

    orderRepository.save(order); ///여기서 한번만 해도 detail이 들어가려면 Cascade가 필요한 것이다!
}

N + 1 문제

JPA에서 N+1 문제는 자식 엔티티가 얼마나 있는지에 관계없이 부모 엔티티 개수에 따라 추가적인 쿼리가 발생하기 때문에, 자식 엔티티의 개수와는 상관이 없습니다. 이 문제는 정확히 부모 엔티티의 개수와 관련이 있습니다. 좀 더 명확히 설명하자면 다음과 같습니다:

1. N+1 문제의 본질

N+1 문제란, 부모 엔티티를 조회하는 1번의 쿼리와, 각 부모 엔티티에 대해 자식 엔티티를 조회하기 위한 N번의 추가 쿼리가 발생하는 문제를 의미합니다. 여기서 N은 부모 엔티티의 개수를 의미합니다.

  • 1번의 쿼리: 부모 엔티티를 조회하는 쿼리입니다.
  • N번의 쿼리: 각 부모 엔티티마다 자식 엔티티를 조회하는 쿼리가 발생하는 것입니다.

이 문제는 자식 엔티티의 수가 아니라, 부모 엔티티의 수만큼 추가적인 쿼리가 발생하는 것이 문제의 핵심입니다.

2. 부모 1건, 자식 여러 건의 경우

부모 엔티티가 1건이라면 자식 엔티티가 아무리 많더라도, 부모 엔티티를 조회한 후 자식 엔티티를 조회하기 위한 쿼리가 단 1번 발생합니다.

@Entity
public class Parent {
    @Id
    private Long id;

    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
    private List<Child> children;
}

@Entity
public class Child {
    @Id
    private Long id;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

  • 쿼리 한 번으로 N 건의 레코드를 가져왔을 때, 연관관계 Entity를 가져오기 위해 쿼리를 N번 추가 수행하는 문제
@Entity
@Table(name = "Orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "order_id")
    private Long orderId;

    @ManyToOne(optional = false) //eager -> join
    @JoinColumn(name = "customer_id")
    private Customer customer;

    @Column(name = "order_dt")
    private LocalDateTime orderDate;

    @OneToMany(cascade = CascadeType.ALL) //lazy 아직..
    @JoinColumn(name = "order_id")
    private List<OrderItem> orderItems;
}

//
@Entity
@Table(name = "OrderItems")
public class OrderItem {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "order_line_id")
    private Long orderLineId;

    @ManyToOne
    @JoinColumn(name = "item_id")
    private Item item;

    private Long quantity;
}
public void getOne() {
    //단건은 연관관계 매핑을 고려해서 join사용
    orderRepository.findById(1L);
}

public void getMulti() {
    //findall은 명확하게 모르는 쿼리를 날리면 우선 실행하고 연관관계매핑을 적용
    orderRepository.findAll();
}

단 건을 실행하면 join으로 한 번에 가져오는데

findAll실행하면 우선 order 실행하고 eager인 customer 실행

Hibernate: 
    select
        order0_.order_id as order_id1_3_,
        order0_.customer_id as customer3_3_,
        order0_.order_dt as order_dt2_3_ 
    from
        orders order0_
//
    select
        customer0_.customer_id as customer1_0_0_,
        customer0_.customer_name as customer2_0_0_ 
    from
        customers customer0_ 
    where
        customer0_.customer_id=?
//
    select
        customer0_.customer_id as customer1_0_0_,
        customer0_.customer_name as customer2_0_0_ 
    from
        customers customer0_ 
    where
        customer0_.customer_id=?

 

feachType의 문제가 아니야!

해결 방법

  • Fetch Join
    • join을 사용하여 쿼리 하나로 실행하겠다.
    • JPQL join fetch
      • fetch 없으면, from절에 있는 메인 엔티티만 반환. 우선 진행시켜! 그 후에 from절 전에 있는 메인 엔티티를 하나씩 따져보면서 다시 N+1 실행
      • fetch를 써야 select절에 다른 것들도 받음
    • Querydsl fetchJoin()
  • 그룹화하여 쿼리 실행 횟수를 줄이겠다.
  • Entity Graph //선언적으로 어디까지 탐색할 수 있는지 지정가능
  • 그 외
    • Hibernate @BatchSize //나눠서
    • Hibernate @Fetch(FetchMode.SUBSELECT) //in절에 넣어서 실행
  • 주의. 그지 같은 join문으로 인해.. 성능이 더 나빠질 수 있음.
@Query("select o from Order o "
       + " inner join fetch o.customer as c "
       + " left join fetch o.orderItems as oi "
       + " inner join fetch oi.item as i")
List<Order> getOrdersWithAssociations();

Querydsl

복잡한 쿼리 작성 시 컴파일러의 도움을 받을 수 있음

  • JPA에서 제공하는 객체 지향 쿼리
    • JPQL: Entity 객체를 조회하는 객체 지향 쿼리 // text 기반이라 compiler의 도움을 못 받음
    • Criteria API: JPQL을 생성하는 빌더 클래스 //복잡함
  • third party library를 이용하는 방법
    • Querydsl
    • jOOQ //native query 기반

JPQL vs Criteria API

  • JPQL
    • SQL을 추상화해서 특정 DBMS에 의존적이지 않은 객체지향 쿼리
    • 문제 : 결국은 SQL이라는 점
      • 문자 기반 쿼리이다 보니 컴파일 타임에 오류를 발견할 수 없다
  • Criteria API
    • 프로그래밍 코드로 JPQL을 작성할 수 있고 동적 쿼리 작성이 쉽다
    • 컴파일 타임에 오류를 발견할 수 있고 IDE의 도움을 받을 수 있다
    • 문제 : 너무 복잡

Querydsl

  • Criteria API처럼 정적 타입을 이용해서 JPQL을 코드로 작성할 수 있도록 해 주는 오픈소스 프레임워크
  • Criteria API에 비해 복잡하지 않고 매우 편리하고 실용적

Spring Data JPA + Querydsl

  • QuerydslPredicateExecutor
  • QuerydslRepositorySupport //join이 많을 경우; 추상 interface -> impl

Custom Repository 구현

Custom Repository를 위한 interface 생성

@NoRepositoryBean
public interface MemberRepositoryCustom {
    List<Member> getMembersWithAssociation();
}

Custom Repository 기능 구현

  • QuerydslRepositorySupport 클래스 상속
    • 기본 생성자에서 Entity를 인자로 전달받는 상위 클래스의 생성자를 호출
  • Custom Repository interfade 구현
  • 구현 메서드에서 Querydsl의 Q-type class를 이용해서 쿼리 수행
public class MemberRepositoryImpl extends QuerydslRepositorySupport implements MemberRepositoryCustom {
    public MemberRepositoryImpl() {
        super(Member.class);
    }

    @Override
    public List<Member> getMembersWithAssociation() {  
        // ...
    }
}
<plugin>
 <groupId>com.mysema.maven</groupId>
 <artifactId>apt-maven-plugin</artifactId>
 <version>1.1.3</version>
 <configuration>
  <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
 </configuration>
 <executions>
  <execution>
   <phase>generate-sources</phase>
   <goals>
    <goal>process</goal>
   </goals>
   <configuration>
    <outputDirectory>target/generated-sources/annotations/java</outputDirectory> //해당 경로에 만든다
   </configuration>
  </execution>
 </executions>
</plugin>

project 설정 필요

기본 Repository interface 변경

  • 기본 Repository interface가 Custom Repository interface를 상속받도록
public interface MemberRepository extends MemberRepositoryCustom, JpaRepository<Member, Long> {
}

기본 Repository interface를 이용해서 Custom Repository interface에 선언된 확장 메서드 호출


트러블 슈팅 - Custom Repository 구현 시 흔히 하는 실수: naming rule

  • 기본 Repository interface
    • MemberRepository
  • Custom Repository interface
    • MemberRepositoryCustom
  • Custom Repository 구현 class
    • MemberRepositoryCustomImpl (X)
    • MemberRepositoryImpl (O)

참고) query dsl 사용 예시

  @Override
  public List<Student> getStudentsWithAssociations() {
    QStudent student = QStudent.student;//1개만 쓰면
    QStudent student1 = new QStudent("student1");//inner query 등으로 추가적으로 더 필요하면 인스턴스 더 만들어
    QEnrollment enrollment = QEnrollment.enrollment;
    QCourse course = QCourse.course;

    return from(student)
        .leftJoin(student.enrollments, enrollment).fetchJoin() //연관관계, q타입 걸치고
        .innerJoin(enrollment.course, course).fetchJoin()
//        .where(student.name.eq("gg"))
//        .select(student.studentId)
//        .fetchOne()
        .fetch() //list 반환 시 fetch; 하나만 가져올거면 fetchOne
        ;
  }

Repository 설정

@EnableJpaRepositories

public @interface EnableJpaRepositories {
	String[] basePackages() default {};

	Class<?>[] basePackageClasses() default {};

	Filter[] includeFilters() default {};

	Filter[] excludeFilters() default {};

	String repositoryImplementationPostfix() default "Impl";

    // ...
}

Spring Boot에서는

dependency에 spring-data-jpa를 추가하면 @EnableJpaRepositories 없어도 기본으로 세팅해 줌

  • Spring Data JPA Repository를 위한 auto configuration
    • org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration
      • @Import(JpaRepositoriesImportSelector)
        • JpaRepositoriesRegistrar
          • @EnableJpaRepositories

Spring Data Repository 메서드 탐색 순서

  • Spring Data Repository 추상화 덕분에 interface 선언만으로도 쿼리 생성 가능
  • interface에 선언된 메서드의 실제 구현체는 아래 순서로 탐색하게 된다
  1. 기본 구현체(JpaRepository의 구현체인 SimpleJpaRepository::saveAll etc.)
  2. 메서드 이름 규칙 적용 (cf. 메서드 이름으로 쿼리 생성 findByNameLikeAndPhone etc.)
  3. Custom Repository Implementation

  1. MemberRepositoryImpl -> MemberRepositoryCustomImpl 로 바꾸면? O
  2. OrderRepositoryImpl -> OrderRepositoryCustomImpl 로 바꾸면? O

Repository Fragment 를 이용한 Custom Repository 구현

: 최근에 바뀜; 하나의 커스텀 레파지토리에 다 넣을 필요 없이 여러개로 구현체를 나눠서 구현 가능(repository fragment)

: 나눠서 구현하고 주 repository에 상속하듯 사용해도 된다

Repository Fragment 를 이용한 Custom Repository

// no `@NoRepositoryBean`
public interface CustomizedMemberRepository {
    List<Member> getMembersWithAssociation();
}
// NOT MemberRepository + `Impl`
// BUT **CustomizedMemberRepository** + `Impl`
public class CustomizedMemberRepositoryImpl implements CustomizedMemberRepository {
    // ...
}

여러 개의 Custom Repository 구현 가능

  • 앞서 본 CustomizedMemberRepository interface와 CustomizedMemberRepositoryImpl class 와 같이
  • 예를 들면 GuestRepository interface, GuestRepositoryImpl class 같이 여러 개의 Custom Repository 구현 가능

Custom Repository 들로 구성된 Repository

public interface MemberRepository
    extends CustomizedMemberRepository, GuestRepository {
}

참고) @Repository  어노테이션이란(not jpa 과거에..)

  • 방식: streotype bean -> 해당 이름으로 된 빈들을 찾아서 자동으로 등록

그러나 jpa는.. 그 방식이 아니고

  • interface extends JpaRepository -> 다 뒤져서 jpa 후보군으로 등록
  • @NoRepositoryBean -> 그 후보군에서 빼줘

Qtype이 안 생겨서 -> no complie... -> 못 찾으면.. 세상 망함.. 온 세상이 빨개요..


cf.) 트러블 슈팅 - Querydsl Q-type class variable로 "member"나 "order"를 쓸 수 없다?!

  • Querydsl에서 Q-type class 인스턴스 생성 시 variable을 "member"로 주면 에러 발생

예제

QMember member = new QMember("member");
QOrder order = new QOrder("order");
unexpected token: member
unexpected token: order

이유

  • JPQL에 MEMBER OF 연산자가 있기 때문에 MEMBER가 예약어라 variable에 쓸 수 없음
@Query("SELECT m FROM Member m WHERE :detail MEMBER OF m.details")
Member getMemberContainingDetail(MemberDetail detail);
  • 마찬가지로 JPQL에 ORDER BY 연산자가 있기 때문에 ORDER가 예약어라 variable에 쓸 수 없음

해결방법

  • 아래 코드에서 각각의 Q-type class의 static 변수는 variable 값이 뭐라고 되어 있을까?
QMember member = QMember.member;
QOrder order = QOrder.order;

member, order :: 예약어; 다른걸로 쓰면 된다.

소스를 까보면 지들도 member를 회피하기 위해 member1로 쓰고있음

@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QMember extends EntityPathBase<Member> {

    private static final long serialVersionUID = 1029385075L;

    public static final QMember member = new QMember("member1"); /// member를 회피하기 위한
728x90
반응형
반응형

22년 초에 restTemplate을 사용하는 프로젝트를 작업하다가 위 문구를 보게 되었다.

음..? 잘 쓰고 있던 rest template이 deprecated 된다고? 

 

그래서 그 이후에 신규로 진행하는 프로젝트는 webClient를 사용하였다.

왜 webClient를 사용하였냐고 물으신다면, 위에서처럼 굳이 spring java doc에 대체하여 쓰라고 할 정도니, 스프링 진영에서 정식으로 밀고 있는 것이라 생각했기 때문이다(곧 대세가 될 것이라 생각했다).

참고로 webClient는 springframework 5에 추가된 것으로 기본적으로 reactive, non-blocking 요청을 지원한다(그래서 응답이 Mono, Flux 등으로 온다).

무튼 그렇게 webClient를 신규 프로젝트들에서 사용하게 되는데, 설정과 사용 시 상당한 라인의 코드가 필요한 것을 깨닫게 되었다.

공통 설정을 빈에 등록하는 코드, 그걸 가져와서 서비스마다 주입을 하고, 주입된 webClient로 get/post 등의 요청을 하는데도 상당한 코드가 필요하다.

get 사용 예시

물론 공통화하여 사용하고 있기는 하지만 외부 api가 새로 추가할 때마다 비슷한 양을 추가해야 한다.

사실 처음에는 webClient를 사용함으로써 webFlux에 친숙해지고, 궁극적으로는 non-blocking 요청에 대한 친근감(..)이 생기지 않을까 하는 마음이 컸다. 하지만 업무에서는 실질적으로 동기 요청이 훨씬 많았고, 이를 위해 억지로 mono.block()을 하고 있어 코드 양만 늘어난 샘이 되었다.. 결국 제대로 활용하지 못하고 있다는 생각이 들었다.


그렇게 시간이 지나고 22년 11월 springframework6이 정식(GA) 출시하면서 진짜 restTemplate에 @Deprecated가 달렸는지 궁금해졌다.

그런데 새로운 프래임워크를 열어보기도 전, 현재 사용하는 프로젝트(springboot2.7.3; springframework 5.3)에서 먼저 확인하니 안내 문구가 바뀌어져 있었다?

(확인해 보니 springframework3, 4에는 안내하는 javadoc 조차 없음)

NOTE: As of 5.0 this class is in maintenance mode, with only minor requests for changes and bugs to be accepted going forward. Please, consider using the org.springframework.web.reactive.client.WebClient which has a more modern API and supports sync, async, and streaming scenarios.

webClient를 추천하는 문구는 그대로인데.. deprecated 된다는 말은 쏙 빠지고, 유지보수모드(간단한 버그수정만 지원)로 전환한다는 말로 바뀌어져 있었다. 요 녀석들.. 고도의 밑장 빼기인가..


위에서 webClient를 사용하면서도 굳이 이걸 써야하는가? 다음 프로젝트에서도 또 webClient를 쓸 것인가? 대해 의문을 가지고 있었는데.. 마침 springframework6 문서에서 Http interface에 대한 글을 보게 된다.

https://docs.spring.io/spring-framework/docs/6.0.0/reference/html/integration.html#rest-http-interface

 

Integration

The Spring Framework provides abstractions for the asynchronous execution and scheduling of tasks with the TaskExecutor and TaskScheduler interfaces, respectively. Spring also features implementations of those interfaces that support thread pools or delega

docs.spring.io

얼핏 보니 생김새는 feign과 비슷하고 내부는 webClient로 되어 있는 듯하다.

쓱싹 만들어본다.

아래와 같이 세팅하고 받아준다.

http interface 는 webClient 기반이라 webflux dependency가 필요하다.

implementation 'org.springframework.boot:spring-boot-starter-webflux'

 

참고로 springboot3은 gradle7.6, java17 기반이니 11을 기본으로 사용하였다면 꼭 설정을 바꿔야지 아니면 아래와 같은 에러를 만난다.

 

1. api의 정보가 담긴 interface를 아래와 같이 만든다.

@HttpExchange(url = "/api/arena-ring")
public interface GiaArenaRingService {

  @GetExchange("/{id}")
  Map<String, Object> getArenaRingGame(@PathVariable BigInteger id);
}

2. 이 interface의 구현체는 스프링으로 부터 자동으로 생성되는데, 아래와 같이 빈을 등록해야 한다.

@Configuration
public class GiaHttpConfig {

  @Bean
  GiaArenaRingService getGiaArenaRingService(@Value("${gia-aapoker-dev}") String url) {
    WebClient client = WebClient.builder().baseUrl(url).build();
    HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(client)).build();
    GiaArenaRingService service = factory.createClient(GiaArenaRingService.class);
    return service;
  }
}

3. 사용하고자 하는 곳에서 이 빈을 주입한 후 해당 함수를 호출하면 된다.

@Service
@RequiredArgsConstructor
public class ExternalService {

  private final GiaArenaRingService giaArenaRingService;
  
    public Map<String, Object> getArenaRingGameHttpInterface(BigInteger id) {
        return giaArenaRingService.getArenaRingGame(id);
      }
  }

 

끝.

예시는 바로 객체를 꺼내 오도록 했으나 기존의 webClient처럼 Mono나 Flux로 반환하게끔 할 수도 있다(support both blocking and reactive return values).

 

사용법이 간단하고 api 스펙을 interface로 선언하기만 하면 되어 한눈에 볼 수 있다는 장점이 있는 것 같다.

webClient 기반이라 기존 webClient에서 지원하던 기능들은 설정방식만 조금 다를 뿐 다 지원할 듯하다.

가독성이 떨어지고 코드의 양이 많았던 webClient의 단점을 어느 정도 보완해 줄 수 있을 것 같아 기회가 되면 사용해 볼 생각.

추가 가이드: https://www.baeldung.com/spring-6-http-interface

 

++ 더불어

restTemplate, webClient 이 아직도 건재하다는 소식에 힘입어 세 방법 모두 사용해 본다.

스프링6 공식문서에 소개된 http clients

(restTemplate와 webClient를 공통 빈으로 등록하면 효율성과 가독성의 측면이 더 좋겠으나 샘플 프로젝트이므로 생략)

  //using WebClient
  public Mono<Map<String, Object>> getArenaRingGameWebClient(BigInteger id) {
    return WebClient.create(GIA_URL).get().uri(uriBuilder -> uriBuilder.path(STATIC_ARENA_RING_GAME_URI + "/" + id).build()).retrieve()
//        .onStatus(HttpStatus::isError, resp -> Mono.error(new RuntimeException("status is not 200")))
        .bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
        }).doOnNext(response -> Optional.ofNullable(response).orElseThrow(() -> new RuntimeException("body is null")));
  }

  //using RestTemplate
  public Map<String, Object> getArenaRingGameRestTemplate(BigInteger id) {
    String url = GIA_URL + STATIC_ARENA_RING_GAME_URI + "/" + id;
    RestTemplate restTemplate = new RestTemplate();
    return restTemplate.getForObject(url, Map.class);
  }

  //using httpClient
  public Map<String, Object> getArenaRingGameHttpInterface(BigInteger id) {
    return giaArenaRingService.getArenaRingGame(id);
  }
restTemplate: 2
webClient: 2
httpClient: 2

세 건 모두 잘 된다.


끝으로.

springboot2.x 를 사용해 본 유저라면 누구든 springboot3을 사용하고 싶어 할 것이다.

관련 migration guide가 있으니 springboot3을 사용하기 전 뭐가 달라졌는지 간단히 살펴보는 것이 좋겠다.

https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Migration-Guide


참고:

https://spring.io/blog/2022/11/16/spring-framework-6-0-goes-ga

 

 

Spring | Home

Cloud Your code, any cloud—we’ve got you covered. Connect and scale your services, whatever your platform.

spring.io

728x90
반응형
반응형

환경: java11, springboot 2.5.6

 

@SpringBootTest로 integration 테스트를 할 때 DB변경사항에 대해서 쿼리로 확인하고 싶을 때가 있다.

테스트가 끝나면 자동으로 메모리에서 사라지기 때문에 테스트 후에는 확인할 수가 없고

테스트 중간에 디버그 포인트를 걸어서 확인하는 방법을 설명한다.

 

1. properties 확인

아래 세가지 h2에 대한 설정 값이 들어있어야 한다. 특히 web-allow를 true로 주는 게 중요하다.

spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.h2.console.settings.web-allow-others=true

 

2. @SpringBootTest에 옵션 주기

webEnvironment의 기본 값은 MOCK이기 때문에 테스트 시 별도 포트를 사용하지 않는다.

우리는 테스트 서버를 띄우고 ui로 확인해야하기 때문에 물리적인 포트 할당이 필요하다.

아래와 같이 설정을 변경해준다.

@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)

 

3. 디버그 걸기

이렇게 까지 하고 디버그를 걸어도 h2 ui가 무한 로딩 중일 텐데, 디버그로 인해 모든 thread가 블로킹되었기 때문이다.

아래와 같이 디버그 옵션을 thread로 변경하여 해당 스래드만 멈추게 한다.

기본값이 All이라서 Thread로 바꿔주어야 한다.(매번 하기엔 좀 번거로울 수 있다.)

 

4. h2 console 확인

http://localhost:8080/h2-console

로 들어가면 아래와 같이 화면이 나온다. 

728x90
반응형

+ Recent posts