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

환경: mysql 5.7

 

mysql 8버전부터 rank over 함수가 들어오면서 드디어! 편한 함수의 세상이 열리긴 한다만..

아직 5.7을 사용하고 있기에 그 이하 버전에 등수를 구하는 방법을 알아야 한다.

서버에 부하가 가지 않고 제일 좋은 방법은 집계할 때 등수를 같이 쌓아주어 그냥 select 해가면 되는 것이지만..

서버는 그닥 user-friendly 하지 않다. 그리고 기획자는 서버에 없는 것만 요청한다..

 

1. count 사용 서브쿼리

select * from (
	select 
    (select count(*) +1 from table for_rank where for_rank.start_date ='2023-02-06' and for_rank.prize_total > this.prize_total) as r,
	this.* 
    from table this 
	where this.start_date ='2023-02-06'
	order by prize_total  desc
) as with_rank
where id = 'tbot0275'

2. @사용자 정의 변수 사용

select * from (
	select (@rank := @rank +1) as r,
	this.* 
	from table this, (select @rank := 0) as ranking
	where this.start_date ='2023-02-06'
	order by prize_total desc
)as weekly_with_rank
where id = 'tbot0275'

 

허나 두 쿼리의 결과가 다를 수 있다. 왜냐면 동점자를 다루는 방식이 다르기 때문.

위 이미지에서처럼 1번의 경우 동점자를 같은 등수로 치게 되고 2번의 경우는 어쨌건 한 줄 서기라 조회에 따라 랜덤 하게 등수가 나올 수 있다.

 

728x90
반응형

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

DB isolation level  (0) 2024.05.22
[mysql] merge into..?  (0) 2024.05.17
[DB] 분산환경에서 데이터 저장소 선택과 활용  (0) 2023.07.24
[형상관리] flyway vs liquibase  (0) 2022.07.08
[mysql] jsonpath  (0) 2022.05.27
반응형

환경: java11

 

요구사항: size 5인 array에 데이터를 넣고 없으면 빈 값으로 입력

String motionAvatar = "1022,1,,,"
int motionAvatarCount = 5;
String[] motionAvatarDetails = Stream
                                .concat(this.motionAvatar.split(","), Stream.generate(() -> ""))
                                .limit(motionAvatarCount)
                                .toArray(String[]::new);
                                
    //["1022", "1", "", "", ""]
  1. Stream.generate(() -> "") creates an infinite stream of empty strings.
  2. Stream.concat(...) concatenates the split array with the stream of empty strings.
  3. limit(motionAvatarCount) limits the concatenated stream to motionAvatarCount elements.
  4. toArray(String[]::new) converts the limited stream into an array of strings.

for loop 안돌고 세련된 방법으로 해보고 싶었는데 stream.generate 는 처음 봤다..

728x90
반응형
반응형

환경: springboot2.7.6, java 17, springcloud 2021.0.8

이전글:

2024.01.29 - [개발/spring] - [cloud] 파일을 동적으로 관리하는 config server

2024.01.29 - [개발/spring] - [cloud] spring cloud bus

spring cloud config service에서 파일로 관리되는 설정파일을 공용으로 쓰도록 설정되어 있고(native방식)

해당 설정파일에는 디비 연결 정보가 들어있어 password와 같은 민감정보가 있다.

파일로 관리되기 때문에 파일이 노출되면 민감정보가 바로 드러나게 되는데, 이를 암호화해서 관리해보자.

 

비대칭키 사용

1. config 서버에 암복호화 추가

config server에 bootstrap.yml을 만들고 암호화에 사용할 대칭키를 입력한다.

encrypt:
  key: abcdefghijklmnopqrstuvwxyz0123456789

암호화 확인

복호화 확인

 

2. 공용으로 관리하는 설정 파일(bootstrap.yml에 명시된 name)에 아래와 같이 내용 추가

password는 plain으로 sa인데 위의 암호화 api로 암호화 한 값을 넣었

spring:
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:test
    username: sa
    password: '{cipher}07407f328f8c2ba016949743f42e0a9da120328d9e0f44f38bad351b9c2b7419'

 

참고로 설정 서버에 직접 파일을 확인하면 아래와 같이 복호화된 값이 나온다. 이 값이 cloud bus 로 연결된 사용처들에게 전달되는 값이다.

참고로 프로퍼티에 암호화된 값이 잘못되었을 경우 위의 password 부분에 n/a라고 표시된다.

 

대칭키 사용

RSA 키쌍 생성

keytool -genkeypair -alias apiEncryptionKey -keyalg RSA -dname "CN=Kenneth Lee, OU=API Development, O=joneconsulting.co.kr, L=Seoul, C=KR"  -keypass "1q2w3e4r" -keystore apiEncryptionKey.jks -storepass "1q2w3e4r"

keytool -genkeypair -alias apiEncryptionKey -keyalg RSA -dname "CN=Kenneth Lee, OU=API Development, O=joneconsulting.co.kr, L=Seoul, C=KR"  -keypass "1q2w3e4r" -keystore apiEncryptionKey.jks -storepass "1q2w3e4r"

private key 확인

 keytool -list -keystore apiEncryptionKey.jks -v

인증서 가져오기

 keytool -export -alias apiEncryptionKey -keystore apiEncryptionKey.jks -rfc -file trustServer.cer

공개키 가져오기

keytool -import -alias trustServer -file trustServer.cer -keystore publicKey.jks

확인

테스트: 위와 동일한 방법

위와 동일하게 파일에는 암호화 값이 있고 사용시에는 자동으로 복호화 해준다.

728x90
반응형
반응형

환경: springboot2.7.6, java 17, springcloud 2021.0.8

 

사용 이유:

분산 시스템의 노드(서비스들)를 경량 메시지 브로커(rabbit mq)와 연결하여 상태 및 구성에 대한 변경 사항을 연결된 노드에게 전달(broadcast)한다.

즉 중간에 메세지를 전달하는 미들웨어(AMQP; 메시지 지향 프로토)를 둠으로써 안정적으로 변경사항을 적용하도록 함

 

사용 법:

우선 기존에 spring cloud config 서버가 있어야 하고 거기서 설정파일을 가져오게끔 bootstrap도 추가되어 있어야 한다.

아래 글 참고.

2024.01.29 - [개발/spring] - [cloud] 파일을 동적으로 관리하는 config server

버스로 연결하려는 모든 프로젝트에 아래와 같이 디펜덴시 추가한다. 수정 시 확산을 위해 actuator도 필요하다.

<!-- 설정파일 외부  -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

<!-- actuator -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator </artifactId>
</dependency>
<!-- spring cloud bus -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp </artifactId>
</dependency>
management:
  endpoints:
    web:
      exposure:
        include: busrefresh

actuator 설정에 busrefresh 추가

연결된 서비스 중 아무데서나 위 api를 호출하면 204 성공이 떨어지고 spring cloud config 서버를 통해 받고 있던 데이터들이 전 서비스에 걸쳐 모두 한 번에 refresh 되는 것을 볼 수 있다. (이전에는 각각 /actuator/refresh를 호출했어야 했음)

728x90
반응형
반응형

환경: springboot2.7.6, java 17, springcloud 2021.0.8

 

application.properties/yml과 같은 설정파일을 수정하면 서버를 재시작해야 한다는 부담이 있다.

spring cloud에서 제공하는 config server를 이용하면 설정 파일을 수정해도 재시작하지 않고, 동적으로 값을 읽어 올 수 있다.

 

공용 프로퍼티 설정 서비스 만들기(이하 config service라고 명명)

1. 프로퍼티 파일을 외부로 빼준다. 보통 공통 값(디비, API url, 공통으로 사용하는 값 등)을 뺀 파일을 외부에 생성한다.

여기서는 documents아래에 임의의 폴더를 생성하여 만들었다.

참고로 윈도우로 작업하였다.

 

2. 이 파일을 추적할 서버를 새로 만든다. 아래와 같이 디펜덴시를 추가하고 어노테이션을 단다.

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-server</artifactId>
</dependency>
@EnableConfigServer
@SpringBootApplication

그리고 이제 추적할 파일에 대한 정보를 프로퍼티 파일에 작성한다.

server:
  port: 8888

spring:
  application:
    name: config-service
  cloud:
    config:
      server:
        git:
          uri: file:///Users/JIHYUN BANG/Documents/source/inflearn/git-local-repo
     	  default-label: main

파일을 바로 볼거라 file:// 로 시작하는데 깃으로 관리한다면 아래 처럼 해당 주소를 기입하면 될 것 같다.

        git:
          uri: https://github.com/haileyjhbang/inflearn-config.git
         # username: bbb //private repository 일 경우 필요
         # password: aaa //private repository 일 경우 필요
     #  uri: file:///Users/JIHYUN BANG/Documents/source/inflearn/git-local-repo

여기서 주의해야하는 것은 파일 경로를 알기 위해 아래와 같이 명령어를 입력할 경우 (윈도우의 경우) 아래와 같이 C드라이브 아래부터 나오는데, /c 를 제외하고 /Users 부터 입력해야 한다는 것.

$ pwd
/c/Users/JIHYUN BANG/Documents/source/inflearn/git-local-repo

 

2-1. 파일 시스템으로 프로퍼티들을 관리할 경우 profiles.active: native

spring:
  application:
    name: config-service
  profiles:
    active: native
  cloud:
    config:
      server:
        native:
          search-locations: file:///${user.home}/Documents/source/inflearn/native-repo

 

3. 서버를 시작하고 아래 주소로 확인해 보면 ecommerce.yml의 내용이 화면에 찍힌다.

http://localhost:8888/ecommerce/default

 

4. 허나 로그를 보면 아래와 같은 에러가 지나가는데

org.springframework.cloud.config.server.environment.NoSuchLabelException: No such label: main
	at org.springframework.cloud.config.server.environment.JGitEnvironmentRepository.refresh(JGitEnvironmentRepository.java:307) ~[spring-cloud-config-server-3.1.8.jar:3.1.8]
	at org.springframework.cloud.config.server.environment.JGitEnvironmentRepository.getLocations(JGitEnvironmentRepository.java:256) ~[spring-cloud-config-server-3.1.8.jar:3.1.8]
	at org.springframework.cloud.config.server.environment.MultipleJGitEnvironmentRepository.getLocations(MultipleJGitEnvironmentRepository.java:139) ~[spring-cloud-config-server-3.1.8.jar:3.1.8]
...

Caused by: org.eclipse.jgit.api.errors.RefNotFoundException: Ref main cannot be resolved
	at org.eclipse.jgit.api.CheckoutCommand.call(CheckoutCommand.java:223) ~[org.eclipse.jgit-5.13.1.202206130422-r.jar:5.13.1.202206130422-r]
	at org.springframework.cloud.config.server.environment.JGitEnvironmentRepository.checkout(JGitEnvironmentRepository.java:461) ~[spring-cloud-config-server-3.1.8.jar:3.1.8]
	at org.springframework.cloud.config.server.environment.JGitEnvironmentRepository.refresh(JGitEnvironmentRepository.java:300) ~[spring-cloud-config-server-3.1.8.jar:3.1.8]
	... 69 common frames omitted

2024-01-29 14:01:10.993  INFO 20748 --- [nio-8888-exec-1] .c.s.e.MultipleJGitEnvironmentRepository : Will try to refresh master label instead.
2024-01-29 14:01:11.218  WARN 20748 --- [nio-8888-exec-1] .c.s.e.MultipleJGitEnvironmentRepository : Could not merge remote for master remote: null

내용을 살펴보면 git-local-repo\ecommerce.yml 가 깃으로 관리되는데 main 브랜치가 없다는 것이다.

다시 보니 기본 브랜치가 master로 되어 있었고

설정파일을 아래처럼 수정하면 에러가 사라진다.

spring.cloud.config.server.git.default-label: master

 

5. 신기한 점은 git uri에  file://로 설정할 경우 해당 파일 경로가 remote 깃과 연결되지 않았을 때는 파일 저장만으로도 바로 반영이 되었는데, 

remote 깃과 연결한 후에는 파일을 수정하여 저장하더라도 자동으로 git에서 파일을 가져와서 override 하기 때문에 파일이 수정되지 않는다. 그 후에는 깃에 커밋하고 푸시해야만 반영됨.

즉, 파일시스템으로 프로퍼티를 관리하기위한 제대로된 방법은 native를 사용하는 것이다.

file://인 경우 ecommerce.yml이 반영되려면 깃에 커밋을 하지 않고도 저장만 하면 바로 반영이 된다. 즉, 저장을 하고 http://localhost:8888/ecommerce/default 를 호출하면 서버 재시작 없이도 변경된 내용을 확인할 수 있음. 물론 이는 지금 깃이 아닌 파일을 바라보도록 했기 때문.. 깃으로 수정했다면 remote push까지 해야한다. <<-깃에 연결 전

 

 

공용 설정 가져오는 서버 설정 추가(사용처; 이하 user service라고 명명)

 

1. 사용처에 아래 디펜덴시 추가

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

2. 공용으로 관리하기로 한 정보는 설정파일에서 없애고 resources/ 아래에 bootstrap.yml 추가(이름 꼭 확인)

해당 파일은 프로젝트에서 별도로 가지고 있는 application.yml 보다 먼저 로딩된다.

로딩이 그렇다는 것이고 적용 우선 순위는 아래와 같다.

 application.yml   application-`name`.yml   application-`name`-<profile>.yml

즉, bootstrap.yml로 로딩된 ecommerce.yml 은 위 순서에서 application-ecommerce.yml으로 인식되어 두번째 우선순위를 갖는다.

# application.yml에서 일부를 띠어서 공용으로 관리하기로 했기 때문에
# application.yml 이전에 불러올 정보를 가져올 프로퍼티임
spring:
  cloud:
    config:
      uri: http://127.0.0.1:8888
      name: ecommerce   #파일명.yml 일 때 파일명 작성

3. 설정 후 서버 시작 시 아래 로그 지나감 확인

INFO 24156 --- [  restartedMain] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://127.0.0.1:8888
INFO 24156 --- [  restartedMain] c.c.c.ConfigServicePropertySourceLocator : Located environment: name=ecommerce, profiles=[default], label=null, version=4c8198ea3c95ce84f573a78f41b637a4da81ad49, state=null
INFO 24156 --- [  restartedMain] b.c.PropertySourceBootstrapConfiguration : Located property source: [BootstrapPropertySource {name='bootstrapProperties-configClient'}, BootstrapPropertySource {name='bootstrapProperties-file:///Users/JIHYUN BANG/Documents/source/inflearn/git-local-repo/file:/C:/Users/JIHYUN BANG/Documents/source/inflearn/git-local-repo/ecommerce.yml'}]

 

4. 혹시 실행 시 아래 에러를 만나면, (그래도 실행은 잘 되지만) 프로퍼티 수정이 필요하다.

2024-01-29 15:29:08.527 ERROR 20628 --- [  restartedMain] o.a.catalina.session.StandardManager     : Exception loading sessions from persistent storage

java.io.EOFException: null
	at java.base/java.io.ObjectInputStream$PeekInputStream.readFully(ObjectInputStream.java:2926) ~[na:na]
...
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) ~[spring-boot-2.7.6.jar:2.7.6]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) ~[spring-boot-2.7.6.jar:2.7.6]
	at com.example.userservice.UserServiceApplication.main(UserServiceApplication.java:18) ~[classes/:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
	at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49) ~[spring-boot-devtools-2.7.6.jar:2.7.6]

해당 에러는 Tomcat의 SESSIONS.ser 파일이 없을 때 나는 에러로 추정되며, 해당 파일은 Tomcat이 애플리케이션 세션 상태를 저장하는 데 사용된다. (참고로 현 프로젝트는 spring-boot-starter-web 안의 tomcat을 사용 중)

세션 지속성(persistent session)은 웹 서버가 다운되거나 재시작될 때 사용자 세션 정보를 유지하는 기능인데 이를 통해 서버가 재시작된 후에도 사용자가 이전의 세션을 계속 사용할 수 있음.

세션 지속성을 '끈다'는 것은 이러한 세션 정보의 유지를 비활성화하는 것을 의미한다. 즉, 서버가 다운되거나 재시작되면 모든 사용자 세션 정보가 손실되는 것이다. 이 기능은 개발 과정에서 테스트나 디버깅을 쉽게 하기 위해 사용되곤 하는데, 서비스의 안정성이나 사용자 경험을 위해서는 일반적으로 세션 지속성이 활성화되어 있는 것이 좋다.

spring boot의 경우 내장 tomcat을 사용하기 때문에 직접적인 tomcat 설정 파일을 수정할 수 없음

따라서 에러를 없애기 위해서 application.yml에 아래 설정을 추가(기본적으로 true로 세팅됨)

server:
  servlet:
    session:
      persistent: false

 

5. 공용 설정파일인 ecommerce.yml 파일을 동적으로 가져오려면

위의 config service에서는 파일 커밋 시 바로 반영된 것을 확인할 수 있었으나, 사용처에서는 바로 확인이 안 된다는 것을 알 수 있다. 동적으로 가져오려면 아래와 같은 방법이 있다.

  1. 사용처를 재기동
  2. actuator refresh api 이용
  3. spring cloud bus 사용

1번 방법은.. 매우 비효율적

 

5-1. actuator refresh 사용

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator </artifactId>
</dependency>
management:
  endpoints:
    web:
      exposure:
        include: refresh, health, beans

재시작 후 ecommerce.yml을 수정하고 저장하면

config service에 반영된 것 확인되고 아래처럼 POST 요청하면 서버가 리로드 되면서 수정된 사항이 반영된다.

다만 actuator refrest를 사용할 경우 해당 프로퍼티를 바라보는 모든 서비스들에 대해 실행해줘야 하기 때문에.. 만약 관련 서비스들이 많다면 여간 번거로운 작업이 아닐 수 없다. 하여 보통은 spring cloud bus를 사용한다.

 

5-1-1. 여러 profile 사용

위와 같이 여러 환경 프로퍼티 파일이 있을 때 사용처에서 아래와 같이 설정하면 해당 환경으로 가져온다.

spring:
  cloud:
    config:
      uri: http://127.0.0.1:8888
      name: ecommerce
  profiles:
    active: prod

-> ecommerce-prod.yml

spring:
  cloud:
    config:
      uri: http://127.0.0.1:8888
      name: ecommerce
  profiles:
    active: dev

-> ecommerce-dev.yml

참고로 해당 값은 아래의 active profile에 dev로 적어도 같은 효과이다.

혹은 VM options에 아래와 같이 작성한다.

-Dspring.profiles.active=dev

 

환경별 프로퍼티 파일을 추가하고 config server를 다시 불러오면 아래와 같이 해당 환경의 프로퍼티 파일과 default 파일을 모두 불러오는 것을 볼 수 있다.

728x90
반응형
반응형

환경: java 17, springboot 2.7.6

 

아래 라이브러리를 사용하여 JWT토큰 파싱할 때 에러날 경우

소스:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
subject = Jwts.parser().setSigningKey(environment.getProperty("token.secret"))
        .parseClaimsJws(jwt).getBody()
        .getSubject();

에러:

java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter
	at io.jsonwebtoken.impl.Base64Codec.decode(Base64Codec.java:26) ~[jjwt-0.9.1.jar:0.9.1]
	at io.jsonwebtoken.impl.DefaultJwtParser.setSigningKey(DefaultJwtParser.java:151) ~[jjwt-0.9.1.jar:0.9.1]
	at com.example.gatewayservice.filter.AuthorizationFilter.isValidJwt(AuthorizationFilter.java:54) ~[classes/:na]

 

해결:

아래 디펜덴시를 추가하면 해결

    <dependency>
        <groupId>javax.xml.bind</groupId>
        <artifactId>jaxb-api</artifactId>
        <version>2.3.1</version>
    </dependency>

 

https://medium.com/@jiri.caga/java-lang-classnotfoundexception-javax-xml-bind-datatypeconverter-in-java-17-solved-499d0ea776d0

728x90
반응형
반응형

환경: springboot 2.7, java11

현 상황: 외부 api가 아래와 같은 json으로 전달될 때 서버에서 해당 값을 받아서 내려야 함

    "isBan": true,
    "isPaidUser": false,

 

1.  primitive + is~ + no jsonproperty

private boolean isPaidUser;

private boolean isBan;

////// getter/setter모양

 public boolean isPaidUser() {
      return isPaidUser;
    }

    public void setPaidUser(boolean paidUser) {
      isPaidUser = paidUser;
    }

    public boolean isBan() {
      return isBan;
    }

    public void setBan(boolean ban) {
      isBan = ban;
    }

결과

: 옳지 않은 값 반환

 

2. wrapper + no is + no jsonproperty

private Boolean isPaidUser;

private Boolean ban;

////// getter/setter모양

   public Boolean getPaidUser() {
      return isPaidUser;
    }

    public void setPaidUser(Boolean paidUser) {
      isPaidUser = paidUser;
    }

    public Boolean getBan() {
      return ban;
    }

    public void setBan(Boolean ban) {
      this.ban = ban;
    }

: null 반환

 

3. wrapper + is~ + no jsonproperty

  private Boolean isPaidUser;

  private Boolean isBan;
  
////// getter/setter모양

  public Boolean getPaidUser() {
    return isPaidUser;
  }

  public void setPaidUser(Boolean paidUser) {
    isPaidUser = paidUser;
  }

  public Boolean getBan() {
    return isBan;
  }

  public void setBan(Boolean ban) {
    isBan = ban;
  }
}

: wrapper class 사용 시 jsonproperty 없어도 알맞은 값 반환, 그 이유는 boolean의 경우 자동으로 is를 자동으로 삭제하여 판단하기 때문 getter/setter 생김새 참고

 

4. primitive + is ~ + with jsonproperty

@JsonProperty("isPaidUser")
private boolean isPaidUser;

@JsonProperty("isBan")
private boolean isBan;

////// getter/setter모양

//자동 생성
    public boolean isBan() {
      return isBan;
    }

    public void setBan(boolean ban) {
      isBan = ban;
    }
    
// jackson lib에서 사용하는 getter
    public boolean getIsBan(){
      return isBan;
    }

: JsonProperty를 사용하게 되면 설정한 IsBan의 함수를 찾아서 내리고

@Getter로 자동생성된 isBan에서 ban이 추가적으로 내려옴??? 왜 이놈만 두 번 내려올까..

=> 정확한 원리는 모르겠음

 

5. primitive + is~ + getter override

private boolean isPaidUser;
private boolean isBan;

////// getter/setter모양

    public boolean getIsBan() {
      return isBan;
    }

    public boolean getIsPaidUser() {
      return isPaidUser;
    }

: getter를 getIs~ 로 작성해주면 해결

 

6. primitive + no is + with jsonproperty

@JsonProperty("isPaidUser")
private boolean paidUser;

@JsonProperty("isBan")
private boolean ban;

: set할 때는 paidUser를 찾아 세팅하고 내릴 때는 자동 생성되는 getter사용

: 해결

 

정리

When using Jackson for JSON serialization and deserialization, it recognizes this convention and automatically maps it to the property name without the "is" prefix.

If you annotate a getter method with @JsonProperty, Jackson will use the annotated method as the getter for that property.

 

json의 isEnable 값을 기본적으로는 enable 값에 꽂음

enable -> getter 사용해서 내림..

아직 완벽하게 이해한게 아니라 좀 더 보완해야 한다..

 

결론

1. Wrapper class

  • is~ 로 필드명 주기

2. primitive type

  • is 없이 필드명 주고 JsonProperty 사용하기
  • is 로 필드명 주고 getIs~로 getter override
728x90
반응형
반응형

환경: springboot2.7.6, mysql, spring jpa

 

mybatis 사용 시 where절에 대한 동적 쿼리를 작성하는 일이 다분했는데,

jpa를 사용하면서 어떻게 하면 효율적 일지 고민하게 된다.

변수 한두개면 그냥 일반적인 jpa문을 여러 개 만들어두고 서비스단에서 분기 쳐서 서로 다른 repository의 함수(정적인 쿼리)를 가져오게 했는데,

옵션이 되는 변수가 많아지면 가독성에도, 관리에도 어려움이 있는 코드가 된다.

그리하여.. where절을 동적으로 변환할 수 있는 방법이 있나 찾아본다.

 

 

1. Specification & Criteria

Specification을 사용하면 자바 코드를 작성하 듯 쿼리를 작성할 수 있다.

허나 직관적으로 SQL문이라는 생각이 안 들고..

사용법도 복잡해서 개인적으로 선호하지 않는다..

https://docs.spring.io/spring-data/jpa/reference/jpa/specifications.html 

 

2. query dsl 활용

BooleanBuilder를 사용하여, 여러 조건을 빌더에 담을 수 있다.

 1번 방법보다는 가독성이 좋긴 하지만,, 이 역시 자바코드로 작성하기에 if문이 많아질 수 있다는 부담이 있다..

또한 query dsl 특성상 컴파일을 따로 하여 Q클래스를 만들고 이를 기반으로 작성되는데,,

컴파일 시 에러가 나면 이후에 모든 컴파일에서 에러가 난 것으로 보여 컴파일 에러 원인을 찾기가 어려웠던 경험이 있고

잡다하게 만들어야 하는 클래스들이 많아서 개인적으로 안 좋아한다.

https://samuel-mumo.medium.com/dynamic-queries-and-querydsl-jpa-support-in-spring-a1b4e233084b

 

3. JPQL에서 작성하기

개인적으로 좋아하는 쿼리 작성법이다.

아래와 같이 작성하면 dto의 값이 있는지 없는지에 따라서 다음 쿼리 적용 유무를 판단한다.

(:#{#request.arenaId} IS NULL OR t.arenaId = :#{#request.arenaId})

주의해야 할 점은 OR절이 있다 보니 관련 조건에 꼭 괄호처리를 하여 한다. 그렇지 않으면 전체가 조회될 수도..

허나 IN 절 작성 시.. 몇몇 에러를 만났는데. 아래에 기술한다.


참고로 request.sequences는 리스트 타입이다.

에러

 Antlr.NoViableAltException: unexpected AST node: {vector}

해결

is null 앞절에(List에) 괄호 추가

+ "AND ((:#{#request.sequences}) IS NULL OR t.seq IN :#{#request.sequences}) "

 

에러

Caused by: java.sql.SQLException: Operand should contain 1 column(s)

해결

coalesce 사용

https://www.w3schools.com/sql/func_mysql_coalesce.asp

+ "AND (COALESCE(:#{#request.sequences}) IS NULL OR t.seq IN :#{#request.sequences}) "

 

에러

h2를 이용한 repository 테스트를 진행할 경우, 아래와 같은 에러가 발생한다.

Caused by: org.h2.jdbc.JdbcSQLNonTransientException: Unknown data type: "?"; SQL statement:

coalesce의 쓰임새가 h2와 mysql이 다르다는 소리다.

실제로 h2에서는 coalesce가 사용되지 않은 쿼리도 문제없이 작동했다.

하지만 나는 h2와 mysql 모두를 만족시켜야 하므로.. 좀 더 연구해 보니 아래와 같은 쿼리가 나오게 된다..

+ "AND ((COALESCE(:#{#request.sequences}, t.seq) = t.seq) OR t.seq IN :#{#request.sequences}) "

그래서 최종 쿼리의 모양새는

 @Query(value =
      "SELECT new com.model.Response(t.seq, t.startDate, t.dismissedDate, t.seatCnt, t.prizeTotal) "
          + "FROM Table t " + "WHERE (:#{#request.arenaId} IS NULL OR t.arenaId = :#{#request.arenaId}) "
          + "AND ((COALESCE(:#{#request.sequences}, t.seq) = t.seq) OR t.seq IN :#{#request.sequences}) "
          + "AND t.startDate >= :#{#request.startDate} AND t.dismissedDate <= :#{#request.endDate} ")
  Page<Response> getRecords(Request request, Pageable pageable);

의미론적인 에러

위와 같이 작성하여 프로그래밍적인 에러는 피했는데, 막상 데이터를 조회해보니 잘못 조회되는 부분이 있었다.

in절에 대한 문제였는데, 위 쿼리를 보면서 설명한다.

  1. sequences는 List<Long>이고 화면에서 받아오는 값이 아닌, id의 유무에 따라 서버에서 받아서 채워 넣는 값이다. 최초에 null이다.
  2. id가 있으면 디비에서 조회해서 세팅을 해주고 없으면 emptyList가 들어간다.
  3. id가 있지만 참여를 하지 않아 디비에 데이터가 없으면 최종적으로는 getRecord함수가 emptyList로 반환되기를 원했다.
  4. 하지만 위 쿼리는 sequences 값의 emptyList와 null을 구분하지 않는다. 
    1. COALESCE(:#{#request.sequences}, t.seq) = t.seq 이 부분이 true가 되어서 다음 AND 절로 넘어간다.
  5. 그래서 null의 전체조회 의미와 emptyList의 참여하지 않음이 같은 결과를 내게 된다..
    1. 즉, 참여하지 않은 사람이 전체에 참여한 것처럼 나온다.

따라서 사용에 주의해야한다!!

728x90
반응형
반응형

환경: springboot2.7, java 17, maven

 

h2를 내장하는 테스트 프로젝트 생성

아래와 같이 설정했다고 가정

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.200</version>
    <scope>runtime</scope>  # <-- 기본이 test 로 되어 있고, 이러면 프로젝트에서 사용할 수 없음
</dependency>
spring:
  h2:
    console:
      enabled: true
      settings:
        web-allow-others: true
      path: /h2-console   // H2 콘솔 사용한다는 뜻
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:test  //db 연결 후 테이블 자동 생성

하지만 서버가 뜨고 db 연결 시 아래와 같은 에러 발생

testdb 데이터베이스를 springboot실행 시 자동 생성해주어야 하는데 그렇지 못함.

H2 1.4.198 이후 버전부터는 보안 문제로 자동으로 디비를 생성하지 않음.

h2 버전의 문제로 1.4 -> 1.3.176으로 낮추면 해결됨

((최신 버전으로 해도 그러는지 확인 필요))

 


 

h2를 메인으로 쓰면서, 서비스 시작 시 entity 테이블을 자동으로 생성하고자 하는 경우

application.yml에 아래처럼 작

spring:
  h2:
    console:
      enabled: true
      settings:
        web-allow-others: true
      path: /h2-console
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:test
  jpa:
    defer-datasource-initialization: true   ###
    hibernate:
      ddl-auto: create-drop
    show-sql: true
    generate-ddl: true

springboot 2.5 이상에서는 위처럼 defer-datasource-initialization 값을 추가해야 한다.

https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.5-Release-Notes#hibernate-and-datasql

 

Spring Boot 2.5 Release Notes

Spring Boot. Contribute to spring-projects/spring-boot development by creating an account on GitHub.

github.com

 

+ 초기 데이터까지 넣어야 한다면

/resources 경로에 data.sql 파일로 쿼리를 넣으면, entity 생성 후 자동 실행한다.

728x90
반응형
반응형

springboot 버전과 spring cloud 버전이 맞지 않은 경우

Caused by: java.lang.ClassNotFoundException: org.springframework.boot.logging.DeferredLogFactory

또한 terminal에서 mvn spring-boot:run 명령어로 실행할 경우 home으로 잡혀있는 자바/mvn으로 시도하기 때문에 호환이 안될 수 있는데, 이럴 때는 자바 경로를 명시적으로 지정하고(혹은 자바 홈을 바꾸고..) 실행해야한다.

 

 

springboot 2.3.8

  • spring cloud Hoxton.SR12 버전과 맞고
  • java 11로 실행해야 함
<properties>
    <java.version>11</java.version>
    <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>

자바 버전이 안 맞으면 아래 에러 발생

ASM ClassReader failed to parse class file - probably due to a new Java class file version that isn't supported yet

 

application routing 시 api gateway로 zuul을 사용했었는데

springboot2.4부터 fadeout 되어서 더 이상 사용 불가다. spring cloud gateway를 사용하라고 한다.

실 서비스로 zuul을 더이상 사용할 수는 없게 되었지만, 테스트 프로젝트 생성 시 2.3.8 정도로 사용해야 한다.

728x90
반응형

+ Recent posts