현재 운영 중인 서비스가 어른들의 사정(?)으로 기존에 앱스토어에 올라와있던 어플의 팀을 이전해야 하는 경우가 생겼는데 이 때 팀 변경 후(정확히는 변경 후, Service Ids를 새로 생성한 후) 기존 애플 유저들의 계정이 로그인 되지 않고 가입 프로세스를 타게 되었는데, 이를 해결 하기 위해 이전 된 팀으로 기존 유저 정보를 마이그레이션 하는 방법을 정리 해둔다.
# ruby 스크립트와 키파일이 존재하는 디렉토리로 이동 cd /Users/user/Documents/apple_client
# ruby 스크립트 실행 ruby client_secret.rb
스크립트가 정상적으로 실행됐으면 client_secret이 생성됨
1 2 3
Ignoring ffi-1.15.0 because its extensions are not built. Try: gem pristine ffi --version 1.15.0 # 아래 eyJraWQiOiJHMzg2UlM3MlY3IiwiYWxnIjoiRVMyNTYifQ.eyJpc3MiXXXXXXXXXxxxTJSOVM1IiwiaWF0IjoxNjM1MzAwOTY0LCJleHAiOjE2NTA4NTI5NjQsImxxxxxxXXXBzOi8vYXBwbGVpZC5hcHBsZS5jb20iLCJzdWIiOiJjb20uampsZWUuV2VkUXVlZW4ifQ.Zw18dKVQxxxxxXXXXXXeSVMT8B_aTkVHULNPOal7W_n_KTba3WtYwE-fQh8Ru6GhmNdVbx2VD1TnPBjTZmKB6PaZ58w
2. access_token 생성
기존 provider_id를 transfer 하기 위해선 apple api로 통신해야하는데 이 때 필요한 access_token을 생성한다.
URL : https://appleid.apple.com/auth/token HTTP Method : POST Content-Type : x-www-form-urlencoded Body : { grant_type : client_credentials scope : user.migration client_id : 클라이언트 아이디 client_secret : 위에서 생성한 client_secret }
요청 후 결과 확인
정상적으로 응답이 온다면 아래처럼 1시간 짜리 access_token이 발급된다.
1 시간이 경과하면 재발급을 받아야 한다.
3. 기존 유저 provider Id transfer
변경된 팀의 provider Id로 변경하기 전 단계
여기서 응답받은 transfer_sub 으로 다시 변경 요청을 보내야 변경된 팀 provider id를 받을 수 있음
request 작성
1 2 3 4 5 6 7 8 9 10 11
URL : https://appleid.apple.com/auth/usermigrationinfo HTTP Method : POST Content-Type : x-www-form-urlencoded Authorization: Bearer {access_token} Body : { sub : 기존 유저 Provider Id target : 새로 만든 팀 ID client_id : 클라이언트 아이디 client_secret : 위에서 생성한 client_secret }
이 때 전송되는 client_secret은 새로 만든 팀의 키 파일로 생성된 client_secret이다.
이 때 전송되는 client_secret은 새로 만든 팀의 키 파일로 생성된 client_secret이다.
이 때 전송되는 client_secret은 새로 만든 팀의 키 파일로 생성된 client_secret이다.
client_secret을 만드는 방법은 1번과 같다.
bearer token은 기존에 만든 것을 사용하면 된다.
1 2 3 4 5 6 7 8 9 10
URL : https://appleid.apple.com/auth/usermigrationinfo HTTP Method : POST Content-Type : x-www-form-urlencoded Authorization: Bearer {access_token} Body : { transfer_sub : 이전 단계에서 transfer된 sub client_id : 클라이언트 아이디 client_secret : 새로 만든 팀의 client_secret }
요청 후 결과 확인
000000.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.0000
형식으로 생성되는데 이 때 맨 앞에 00000. 부분은 변경되지 않으므로
이 부분을 확인
5. DB에 저장된 유저 provider Id 변경
필자의 서비스는 provider ID로 유니크한 KEY를 만들어 사용하고 있었으므로 이 부분을 업데이트 해주었다.
Caused by: java.lang.IllegalArgumentException: Unable to locate Attribute with the the given name [attribute명] on this ManagedType [class명] at org.hibernate.metamodel.model.domain.internal.AbstractManagedType.checkNotNull(AbstractManagedType.java:148) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final] at org.hibernate.metamodel.model.domain.internal.AbstractManagedType.getAttribute(AbstractManagedType.java:119) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final] at org.hibernate.metamodel.model.domain.internal.AbstractManagedType.getAttribute(AbstractManagedType.java:44) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final] at org.springframework.data.jpa.repository.query.QueryUtils.requiresOuterJoin(QueryUtils.java:697) ~[spring-data-jpa-2.5.2.jar:2.5.2] at org.springframework.data.jpa.repository.query.QueryUtils.toExpressionRecursively(QueryUtils.java:638) ~[spring-data-jpa-2.5.2.jar:2.5.2] at org.springframework.data.jpa.repository.query.QueryUtils.toExpressionRecursively(QueryUtils.java:617) ~[spring-data-jpa-2.5.2.jar:2.5.2] at org.springframework.data.jpa.repository.query.QueryUtils.toExpressionRecursively(QueryUtils.java:613) ~[spring-data-jpa-2.5.2.jar:2.5.2] at org.springframework.data.jpa.repository.query.JpaQueryCreator$PredicateBuilder.getTypedPath(JpaQueryCreator.java:385) ~[spring-data-jpa-2.5.2.jar:2.5.2] at org.springframework.data.jpa.repository.query.JpaQueryCreator$PredicateBuilder.build(JpaQueryCreator.java:308) ~[spring-data-jpa-2.5.2.jar:2.5.2] at org.springframework.data.jpa.repository.query.JpaQueryCreator.toPredicate(JpaQueryCreator.java:211) ~[spring-data-jpa-2.5.2.jar:2.5.2] at org.springframework.data.jpa.repository.query.JpaQueryCreator.create(JpaQueryCreator.java:124) ~[spring-data-jpa-2.5.2.jar:2.5.2] at org.springframework.data.jpa.repository.query.JpaQueryCreator.create(JpaQueryCreator.java:59) ~[spring-data-jpa-2.5.2.jar:2.5.2] at org.springframework.data.repository.query.parser.AbstractQueryCreator.createCriteria(AbstractQueryCreator.java:119) ~[spring-data-commons-2.5.2.jar:2.5.2] at org.springframework.data.repository.query.parser.AbstractQueryCreator.createQuery(AbstractQueryCreator.java:95) ~[spring-data-commons-2.5.2.jar:2.5.2] at org.springframework.data.repository.query.parser.AbstractQueryCreator.createQuery(AbstractQueryCreator.java:81) ~[spring-data-commons-2.5.2.jar:2.5.2] at org.springframework.data.jpa.repository.query.PartTreeJpaQuery$QueryPreparer.<init>(PartTreeJpaQuery.java:217) ~[spring-data-jpa-2.5.2.jar:2.5.2] at org.springframework.data.jpa.repository.query.PartTreeJpaQuery$CountQueryPreparer.<init>(PartTreeJpaQuery.java:348) ~[spring-data-jpa-2.5.2.jar:2.5.2] at org.springframework.data.jpa.repository.query.PartTreeJpaQuery.<init>(PartTreeJpaQuery.java:91) ~[spring-data-jpa-2.5.2.jar:2.5.2] ... 117 common frames omitted
Process finished with exit code 0
왜 발생 했을까?
The inconsistency between the subject part and the OrderBy part is certainly a bug.
For the given case the correct behaviour seem to be the one from the OrderBy part which is adhering to the conventions laid out in the Java Bean Specification 1.0.1 Section 8.8:
…
There is no way to write the property name in the middle of a java method name if the property begins with a single lower case letter followed by an upper case letter.
Therefore renaming the property, or less invasive providing a @Query annotation are the ways to solve this problem on your side GitFlip
독자들의 빠른 문제 해결을 위해 결론부터 이야기 하자면 위의 글에서 보는 것 처럼 query method에서 OrderBy를 사용 할 때 “dDay” 같이 맨 앞 한 자리가 소문자 변수명을 사용 하는 경우 spring-data-jpa의 버그로 발생하는 에러인데 해결방법으로는 @Query annotation을 사용하거나 변수명을 변경하는 것을 권하고 있다.
결론
해결 방법
1 2
@Query("SELECT c FROM Entity명 c WHERE c.dDay =:dday ORDER BY dDayStartDate") List<Category> findByDDayOrderByDDayStartDateAsc(DDay dday);
위에서 알려준 해결책 중 필자는 @Query Annotation을 사용하는 방법으로 문제를 수정하였는데 다른 이유가 있는 것은 아니고 현재 프로젝트에서 변수명을 변경하는 것이 더 코스트가 많이 발생하기 때문에 @Query를 사용해서 처리했다.
이 문제 때문에 꽤 많은 시간을 소비 하였는데 필자와 같은 무고한 피해자(?)가 발생하지 않기를 바라며 글을 마친다.
spring으로 된 프로젝트를 spring boot으로 전환 중에 spring-security-taglibs의 버전을 3.x 에서 5.5.1로 업데이트 했는데 아래와 같은 에러가 발생하여 이에 관한 내용을 정리 해둔다.
발생한 에러
1 2 3 4 5 6 7 8 9 10 11 12 13 14
org.apache.jasper.JasperException: /WEB-INF/views/layout/layout.jsp (line: [126], column: [0]) Attribute [ifAnyGranted] invalid for tag [authorize] according to TLD at org.apache.jasper.compiler.DefaultErrorHandler.jspError(DefaultErrorHandler.java:41) ~[tomcat-embed-jasper-9.0.48.jar!/:na] at org.apache.jasper.compiler.ErrorDispatcher.dispatch(ErrorDispatcher.java:292) ~[tomcat-embed-jasper-9.0.48.jar!/:na] at org.apache.jasper.compiler.ErrorDispatcher.jspError(ErrorDispatcher.java:115) ~[tomcat-embed-jasper-9.0.48.jar!/:na] at org.apache.jasper.compiler.Validator$ValidateVisitor.checkXmlAttributes(Validator.java:1287) ~[tomcat-embed-jasper-9.0.48.jar!/:na] at org.apache.jasper.compiler.Validator$ValidateVisitor.visit(Validator.java:900) ~[tomcat-embed-jasper-9.0.48.jar!/:na] at org.apache.jasper.compiler.Node$CustomTag.accept(Node.java:1558) ~[tomcat-embed-jasper-9.0.48.jar!/:na] at org.apache.jasper.compiler.Node$Nodes.visit(Node.java:2385) ~[tomcat-embed-jasper-9.0.48.jar!/:na] at org.apache.jasper.compiler.Node$Visitor.visitBody(Node.java:2437) ~[tomcat-embed-jasper-9.0.48.jar!/:na] at org.apache.jasper.compiler.Node$Visitor.visit(Node.java:2443) ~[tomcat-embed-jasper-9.0.48.jar!/:na] at org.apache.jasper.compiler.Node$Root.accept(Node.java:471) ~[tomcat-embed-jasper-9.0.48.jar!/:na] at org.apache.jasper.compiler.Node$Nodes.visit(Node.java:2385) ~[tomcat-embed-jasper-9.0.48.jar!/:na]
...
왜 발생했을까?
This section describes all of the deprecated APIs within the spring-security-taglibs module. If you are not using the spring-security-taglibs module or have already completed this task, you can safely skip to spring-security-web.
Spring Security’s authorize JSP tag deprecated the properties ifAllGranted, ifAnyGranted, and ifNotGranted in favor of using expressions.
문서에 적혀 있는 것 처럼 spring-security-taglibs 4.x 버전 이상 부터는 ifAllGranted, ifAnyGranted, ifNotGranted 속성들을 지원하지 않기 때문에 3.x 버전에서 5 버전으로 업그레이드하면서 해당 속성들을 사용한 코드들이 있는 경우 발생하는 문제였다.
해결방법
1 2 3 4 5 6 7 8 9 10 11
<sec:authorize ifAllGranted="ROLE_ADMIN,ROLE_USER"> <p>Must have ROLE_ADMIN and ROLE_USER</p> </sec:authorize> <sec:authorize ifAnyGranted="ROLE_ADMIN,ROLE_USER"> <p>Must have ROLE_ADMIN or ROLE_USER</p> </sec:authorize> <sec:authorize ifNotGranted="ROLE_ADMIN,ROLE_USER"> <p>Must not have ROLE_ADMIN or ROLE_USER</p> </sec:authorize>
위 처럼 되어 있을 경우
1 2 3 4 5 6 7 8 9 10 11 12
<sec:authorize access="hasRole('ROLE_ADMIN') and hasRole('ROLE_USER')"> <p>Must have ROLE_ADMIN and ROLE_USER</p> </sec:authorize> <sec:authorize access="hasAnyRole('ROLE_ADMIN','ROLE_USER')"> <p>Must have ROLE_ADMIN or ROLE_USER</p> </sec:authorize> <sec:authorize access="!hasAnyRole('ROLE_ADMIN','ROLE_USER')"> <p>Must not have ROLE_ADMIN or ROLE_USER</p> </sec:authorize>
간단하게 정리하면 커맨드라인이나 파일을 통해 클래스 패스가 jvm에 전달되는 방식을 제어하는데 대부분의 os에선 최대 명령 줄 제한이 있어서 이 제한을 넘는 명령은 IDEA에서 실행 할 수 없기 때문에 명령이 32768 자 보다 길면 동적 클래스 패스로 전환 할 것을 제안하고 이 때 긴~ 클래스 패스는 파일에 기록된 다음, 응용 프로그램 관리자가 읽어서 시스템 클래스 로더를 통해 로드 된다는 것이다.
2021-05-0322:06:26.17018752-18752/com.jgji.memo E/AndroidRuntime: FATAL EXCEPTION: main Process: com.jgji.memo, PID: 18752 java.lang.RuntimeException: Unable to start activity ComponentInfo{com.jgji.memo/com.jgji.memo.MainActivity}: android.view.InflateException: Binary XML file line #33 in com.jgji.memo:layout/activity_main: Binary XML file line #33 in com.jgji.memo:layout/activity_main: Error inflating class Button at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3449) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601) at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85) at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loop(Looper.java:223) at android.app.ActivityThread.main(ActivityThread.java:7656) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947) Caused by: android.view.InflateException: Binary XML file line #33 in com.jgji.memo:layout/activity_main: Binary XML file line #33 in com.jgji.memo:layout/activity_main: Error inflating class Button Caused by: android.view.InflateException: Binary XML file line #33 in com.jgji.memo:layout/activity_main: Error inflating class Button Caused by: java.lang.IllegalArgumentException: The style on this component requires your app theme to be Theme.AppCompat (or a descendant). at com.google.android.material.internal.ThemeEnforcement.checkTheme(ThemeEnforcement.java:243) at com.google.android.material.internal.ThemeEnforcement.checkAppCompatTheme(ThemeEnforcement.java:213) at com.google.android.material.internal.ThemeEnforcement.checkCompatibleTheme(ThemeEnforcement.java:148) at com.google.android.material.internal.ThemeEnforcement.obtainStyledAttributes(ThemeEnforcement.java:76) at com.google.android.material.button.MaterialButton.<init>(MaterialButton.java:229) at com.google.android.material.button.MaterialButton.<init>(MaterialButton.java:220) at com.google.android.material.theme.MaterialComponentsViewInflater.createButton(MaterialComponentsViewInflater.java:43) at androidx.appcompat.app.AppCompatViewInflater.createView(AppCompatViewInflater.java:123) at androidx.appcompat.app.AppCompatDelegateImpl.createView(AppCompatDelegateImpl.java:1551) at androidx.appcompat.app.AppCompatDelegateImpl.onCreateView(AppCompatDelegateImpl.java:1602) at android.view.LayoutInflater.tryCreateView(LayoutInflater.java:1059) at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:995) at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:959) at android.view.LayoutInflater.rInflate(LayoutInflater.java:1121) at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:1082) at android.view.LayoutInflater.inflate(LayoutInflater.java:680) at android.view.LayoutInflater.inflate(LayoutInflater.java:532) at android.view.LayoutInflater.inflate(LayoutInflater.java:479) at androidx.appcompat.app.AppCompatDelegateImpl.setContentView(AppCompatDelegateImpl.java:696) at androidx.appcompat.app.AppCompatActivity.setContentView(AppCompatActivity.java:170) at com.jgji.memo.MainActivity.onCreate(MainActivity.java:27) at android.app.Activity.performCreate(Activity.java:8000) at android.app.Activity.performCreate(Activity.java:7984) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1309) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3422) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601) at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
평소와 다름없이 유튜브를 배회하던 중 재난급 서버 장애내고 개발자 인생 끝날뻔 한 썰이란 영상을 보게 되었는데… ‘나는 지금 어떻게 하고 있지..?’ 라는 생각이 문득 들어서 얼른 나도 백업을 수시로 해둬야겠다는 생각에 적용한 mysql backup 방법을 남겨둔다.
왜 mysqldump로 했어요??
mysqldump는 스토리지 엔진에 관계없이 논리 백업을 수행 할 수 있는 도구다. 무엇을 이용해서 backup을 해야할까 고민 하던 중 장애와 관련된 XtraBackup 적용기를 보게 되었는데 데이터 사이즈가 크지 않다면 mysqldump를 사용하는 것이 간단하며 복원 시에도 신경 써야 할 것이 적다는 내용을 보고 mysqldump로 backup을 진행하기로 결정했다.
–single-transaction : lock 을 걸지 않고도 dump 파일의 정합성 보장하는데 InnoDB 테이블이 아닌 MyISAM or MEMORY 테이블인 경우에는 여전히 상태가 변경 될 수 있다. MySQL에선 큰 테이블을 덤프하려면 –quick 옵션과 결합하기를 권장한다.
–databases : dump 할 db명을 지정한다. 여러 개를 한번에 지정하는 것도 가능하다.
–tables : dump 할 table명을 지정한다. 마찬가지로 여러 개를 한번에 지정 할 수 있다.
그리고 필자는 테이블 별로 데이터를 gzip 형식으로 압축하기 위해 gzip 명령어를 사용했다.
그 결과
명령어를 실행하면 이 처럼 해당 경로에 .gz로 압축된 파일이 생성되고 이 파일의 압축을 해제하면 sql 파일이 된다.
예외 상황
mysqldump는 mysql client 유틸리티 이므로 당연히 mysql client가 있어야 실행 가능한데 아래 메시지는 그 mysql client가 설치되지 않았으니 설치 한 후 사용하라는 메시지이다.
1 2 3 4 5 6
The program 'mysqldump' can be found in the following packages: * mysql-client-5.5 * mariadb-client-5.5 * mysql-client-5.6 * percona-xtradb-cluster-client-5.5 Try: sudo apt-get install <selected package>
이 처럼 메시지가 출력 될 경우 위 리스트 중 하나를 설치하면 정상적으로 mysqldump가 이용 가능해 질 것이다.
shell script로 작성하기
이제 이 명령어를 스케줄러에 등록하기 위해 우선 쉘 스크립트 파일로 작성해야 하는데 필자 같은 경우는 아래와 같이 작성해서 사용하고 있다.
user명@ip-127.0.0.1:~$ crontab -l # Edit this file to introduce tasks to be run by cron. # # Each task to run has to be defined through a single line # indicating with different fields when the task will be run # and what command to run for the task # # To define the time you can provide concrete values for # minute (m), hour (h), day of month (dom), month (mon), # and day of week (dow) or use '*'in these fields (for'any').# # Notice that tasks will be started based on the cron's system # daemon's notion of time and timezones. # # Output of the crontab jobs (including errors) is sent through # email to the user the crontab file belongs to (unless redirected). # # For example, you can run a backup of all your user accounts # at 5 a.m every week with: # 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/ # # For more information see the manual pages of crontab(5) and cron(8) # # m h dom mon dow command 10 04 * * * bash 백업 실행 파일.sh
위 처럼 cat 명령어를 실행 한 것 처럼 콘솔에 crontab의 내용이 보여지게 된다. 이렇게 저장된 스케줄러들을 확인난 후에
1
crontab -e
명령어를 통해 스케줄러를 등록하면 되는데 이 때 crontab -e 명령어를 실행시키면 crontab을 열 텍스트 에디터를 선택하라는 메시지가 출력되거나, 디폴트로 지정된 텍스트 에디터로 crontab이 열리게 된다.
위 형식에 맞춰 언제마다 실행 시킬 지 정한 후 우리가 만든 backup용 sh 파일을 실행하게 하면 된다.
필자는 매일 04시 10분에 실행 시키기 위해 아래와 같이 작성하여 등록했다.
1
10 04 * * * bash 백업 실행 파일.sh
이렇게 작성하고 테스트를 하기 위해 date 명령어로 서버 시간을 확인한 후 실행 시간을 현재 시간에서 2분 뒤로 변경하여 2분 뒤에 실행하게 하였는데…
왜 백업이 안되지…?
이미지에서 보이는 것 처럼 파일 사이즈가 20 바이트 밖에 안되길래 확인해보니 백업이 정상적으로 이루어지지 않은 것 이었다.
왜 이렇게 됐을까를 고민하던 중… 스케줄러에 등록하기 전 sh파일을 단독으로 실행 했을 때 table 갯수만큼 비밀번호를 입력했던 것이 떠올라서 그 부분이 문제일 것이라고 판단되어 찾아보니 공식 문서에서 아래와 같은 글을 확인 할 수 있었다.
Use the –password or -p option on the command line with no password value specified. In this case, the client program solicits the password interactively:
shell> mysql -u francis -p db_name Enter password: **** The * characters indicate where you enter your password. The password is not displayed as you enter it.
It is more secure to enter your password this way than to specify it on the command line because it is not visible to other users. However, this method of entering a password is suitable only for programs that you run interactively. If you want to invoke a client from a script that runs noninteractively, there is no opportunity to enter the password from the keyboard. On some systems, you may even find that the first line of your script is read and interpreted (incorrectly) as your password.
정리하면 —password 옵션은 클라이언트에 대화식으로 암호를 요청하고, 이 경우 비대화형으로 실행되는 스크립트에서 클라이언트를 호출 할 경우 암호를 입력 할 기회가 없으므로 일부 시스템에서 잘못 해석 될 수도 있다는 내용이었다.
그래서 어떻게 해야되요…
공식 문서를 보면 .my.cnf파일을 만들어서 거기에
1 2
[client] password=password
아래와 같은 형식으로 지정하고 안전을 위해 파일의 엑서스 모드를 400 또는, 600으로 설정하라고 한다.
그 후 mysqldump에서 지정한 설정 파일을 읽어들이게 하기 위해
1
shell> mysql --defaults-file=/home/francis/mysql-opts
—password 옵션을 제외하고 password가 my.cnf 설정파일에 들어갔으므로 최종적인 sh 파일 내용은 위와 같이 된다.
결과
모든 난관을 극복하고 다시 crontab -e에 등록해서 스케줄을 실행한 결과 위와 같이 정상적으로 백업을 완료하여 파일 사이즈가 아까와 다르게 20 byte가 아닌 모습을 볼 수 있다.
P.S : 저는 그래도 안되는데요…?
필자의 경우에도 .my.cnf 파일을 생성하고 등록해줬음에도 불구하고 백업이 정상적으로 진행되지 않았는데 결국 찾은 해결 방법은 .my.cnf 파일을 따로 생성하지 않고, 아래와 같이 /etc/mysql/my.cnf 파일에 password를 추가하는 방법으로 해결 했다.
# One can use all long options that the program supports. # Run program with --help to get a list of available options and with # --print-defaults to see which it would actually understand and use. # # For explanations see # http://dev.mysql.com/doc/mysql/en/server-system-variables.html
# This will be passed to all mysql clients # It has been reported that passwords should be enclosed with ticks/quotes # escpecially if they contain "#" chars... # Remember to edit /etc/mysql/debian.cnf when changing the socket location. [client] port = 3306 socket = /var/run/mysqld/mysqld.sock password = password # !!! 이 부분 !!! # Here is entries for some specific programs # The following values assume you have at least 32M ram
# This was formally known as [safe_mysqld]. Both versions are currently parsed. [mysqld_safe] socket = /var/run/mysqld/mysqld.sock nice = 0
기존에 jar파일을 추가해서 Junit4로 테스트하던 프로젝트를 마찬가지로 jar파일을 Junit5로 버전 업해서 사용하고 있었는데 ‘굳이 이럴 필요없이 maven 프로젝트로 변경하면 되지 않나?’라는 생각에 maven 프로젝트로 변경 후 발생했던 에러를 해결한 방법을 작성해둔다…
상황
위에서 적은 것 처럼 기존에 Junit jar를 다운받아서 직접 경로를 지정하는 방식으로 junit4, 5를 둘 다 사용하고 있었는데 프로젝트를 maven으로 변경 후 의존성 충돌이 발생해 아래와 같은 에러가 발생하며 테스트가 진행되지 않았다.
발생한 에러
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
Internal Error occurred. org.junit.platform.commons.JUnitException: TestEngine with ID 'junit-vintage' failed to discover tests at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discoverEngineRoot(EngineDiscoveryOrchestrator.java:111) at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discover(EngineDiscoveryOrchestrator.java:85) at org.junit.platform.launcher.core.DefaultLauncher.discover(DefaultLauncher.java:92) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:75) at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71) at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33) at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:220) at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53) Caused by: org.junit.platform.commons.JUnitException: Unsupported version of junit:junit: 3.8.1. Please upgrade to version 4.12 or later. at org.junit.vintage.engine.JUnit4VersionCheck.checkSupported(JUnit4VersionCheck.java:49) at org.junit.vintage.engine.JUnit4VersionCheck.checkSupported(JUnit4VersionCheck.java:35) at org.junit.vintage.engine.VintageTestEngine.discover(VintageTestEngine.java:62) at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discoverEngineRoot(EngineDiscoveryOrchestrator.java:103) ... 7 more
Process finished with exit code -2
위 에러 로그처럼 ‘junit-vintage’를 아이디로 하는 테스트 엔진을 찾지 못했다는 에러가 발생해서 junit-vintage 의존성을 추가해주었는데..
라고 떡하니 써 적혀있어서 위에 3 개의 의존성을 모두 추가한 후 리빌드 후 다시 테스트를 실행해봤다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
<!-- Only needed to run tests in a version of IntelliJ IDEA that bundles older versions --> <dependency> <groupId>org.junit.platform</groupId> <artifactId>junit-platform-launcher</artifactId> <version>1.7.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>5.7.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> <version>5.7.1</version> <scope>test</scope> </dependency>
하지만 결과는 마찬가지였고… .m2폴더에서 Junit 의존성을 다 삭제하고, Intellij project setting에서 Modules, Libraies에서 모두 삭제하고 이것저것 다 해봤지만 모두 실패로 돌아가서 결국 새 maven 프로젝트를 만들어서 하나씩 비교해보기로 했다.
publicstaticvoidmain(String[] args)throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
List<Integer> differences = new ArrayList<>(); List<Integer> list = new ArrayList<>();
int preValue = 0;
int n = Integer.parseInt(br.readLine()); while (n-- > 0) { int input = Integer.parseInt(br.readLine()); list.add(input);
if (preValue != 0) { differences.add(Math.abs(preValue - input)); }
preValue = input; }
Collections.sort(list);
int gcd = Integer.MAX_VALUE; for (int i = 1; i < differences.size(); i++) { int temp = gcd(differences.get(i-1), differences.get(i)); gcd = temp > gcd ? gcd : temp; }
int answer = 0; int pre = list.get(0); for (int i = 1; i < list.size(); i++) { if (list.get(i) - pre != gcd) { answer += ((list.get(i) - pre) / gcd) - 1; } pre = list.get(i); }
System.out.println(answer); }
privatestaticintgcd(int a, int b){ return b!=0 ? gcd(b, a%b) : a; }
흐름
입력 받은 값들을 뺀 값의 차를 구한 값으로 약수를 구하기 위한 differences, 입력 받은 값을 그대로 저장하는 list 변수 생성.
두 리스트 모두 n 만큼 돌면서 값을 저장하는데
differences의 경우 첫 번쨰 루프에서 값을 저장 할 수 없으므로(2 번쨰 값부터 첫 번째 값과 뺄 수 있으므로) 이전 값이 있는 경우에만 리스트에 저장한다.
가로수들 사이에 끼워넣어야 되는 가로수의 개수를 구해야 하므로 입력 받은 가로수의 위치를 정렬한다.
가로수 간의 최소 간격을 구하기 위해 아까 저장한 가로수 간의 차이를 구한 리스트 differences를 돌면서 유클리드 호제법을 이용해 최대공약수를 구하고, 구한 최대 공약수 중 가장 작은 값을 저장한다.
가로수 간의 차이값에서 최대 공약수를 구하는 이유는,
문제가 가로수를 일정한 간격으로 최대한 적게 심기게 하기 위한 문제 이므로 두 수에서 공통되는 값들 중 가장 큰 수인 최대 공약수를 구하고,
그 최대 공약수들 중 가장 작은 값이어야 모두 일정한 간격으로 심을 수 있으므로 최대 공약수 중 가장 작을 값을 구한다.
이렇게 잘 구한 값으로 이제 몇 개의 가로수를 더 심어야 하는지 알아내야 하므로
아까 정렬시킨 가로수 리스트를 반복 하면서 이전 가로수 와의 간격이 최대 공약수 보다 큰 녀석들이 있으면
publicstaticvoidmain(String[] args)throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); int n = Integer.parseInt(br.readLine());
Map<String, Boolean> isDuplicate = new HashMap<>(); Map<String, Integer> domJudge = new HashMap<>();
for (int i = 0; i < n; i++) { String input = br.readLine();
if (!isDuplicate.containsKey(input)) { isDuplicate.put(input, false); }
publicstaticvoidmain(String[] args)throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); int n = Integer.parseInt(br.readLine());
LinkedList<Integer> queue = new LinkedList<>(); int dasom = Integer.parseInt(br.readLine()); for (int i = 0; i < n-1; i++) { int input = Integer.parseInt(br.readLine());
if (dasom <= input) { queue.offer(input); } }
Collections.sort(queue);
int answer = 0; for (; !queue.isEmpty() && queue.peekLast() >= dasom; answer++) { if (queue.peek() < dasom) { queue.poll(); }
queue.offer((queue.pollLast()-1)); dasom++;
Collections.sort(queue); }
System.out.println(answer); }
흐름
다솜이는 기호 1번이므로 첫 번째 입력을 다솜이의 표로 저장한다.
우선순위 큐로 사용하기 위해 list에 입력들을 저장하는데 다솜이 보다 작을 경우엔 문제의 답과 상관이 없으므로 다솜이의 표보다 적은 표는 큐에 저장하지 않는다.
우선순위 큐로서 동작하게 하기 위해 반복 전에 큐를 한번 정렬시켜준다.
그 후엔 큐가 비거나, 큐의 가장 큰 값이 다솜이가 될 때까지 반복한다.
반복하면서 큐의 맨 앞이 다솜이보다 작다면, 이제 그 사람은 다솜이가 재낀 것 이므로 큐에서 빼주고,
제일 표가 많은 놈한테서 표 하나를 뻇어서 다솜이가 가져가면 되므로 큐의 마지막 값에서 -1 빼서 다시 큐에 넣어준다.
그리고 제일 표가 많은 놈에게 뺏은 표를 다솜이에게 줬으므로 다솜이의 값을 1 증가시키고
우선순위 큐 처럼 동작 할 수 있게 큐를 다시 정렬 시킨다.
반복된 횟수를 출력하면 끝.
결과
여담
반복문에서 소팅을 하기 때문에 시간 초과가 날 것으로 예상했는데 수가 적은 문제라 그런지 무사히 통과되어 참 다행이다…