서론

현재 운영 중인 서비스가 어른들의 사정(?)으로 기존에 앱스토어에 올라와있던 어플의 팀을 이전해야 하는 경우가 생겼는데 이 때 팀 변경 후(정확히는 변경 후, Service Ids를 새로 생성한 후) 기존 애플 유저들의 계정이 로그인 되지 않고 가입 프로세스를 타게 되었는데, 이를 해결 하기 위해 이전 된 팀으로 기존 유저 정보를 마이그레이션 하는 방법을 정리 해둔다.


본론

1. 기존 팀 Key file로 client_secret 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
require 'jwt'

key_file = '기존 키파일명'
team_id = '기존 팀 아이디'
client_id = '클라이언트 아이디'
key_id = '기존 키 아이디'

ecdsa_key = OpenSSL::PKey::EC.new IO.read key_file

headers = {
'kid' => key_id
}

claims = {
'iss' => team_id,
'iat' => Time.now.to_i,
'exp' => Time.now.to_i + 86400*180,
'aud' => 'https://appleid.apple.com',
'sub' => client_id,
}

token = JWT.encode claims, ecdsa_key, 'ES256', headers

puts token
  1. 위 코드를 복사해서 ruby script 파일(.rb)로 만든다.
  2. 키 파일과 작성한 script 파일을 같은 디렉토리 내에 위치 시킨다.
  3. 터미널에서 .rb 스크립트를 실행 시킨다.
1
2
3
4
5
# ruby 스크립트와 키파일이 존재하는 디렉토리로 이동
cd /Users/user/Documents/apple_client

# ruby 스크립트 실행
ruby client_secret.rb
  1. 스크립트가 정상적으로 실행됐으면 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 생성

  1. request 작성
1
2
3
4
5
6
7
8
9
10
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. 요청 후 결과 확인
  • 정상적으로 응답이 온다면 아래처럼 1시간 짜리 access_token이 발급된다.
  • 1 시간이 경과하면 재발급을 받아야 한다.

3. 기존 유저 provider Id transfer

  • 변경된 팀의 provider Id로 변경하기 전 단계
  • 여기서 응답받은 transfer_sub 으로 다시 변경 요청을 보내야 변경된 팀 provider id를 받을 수 있음
  1. request 작성
access_token 설정 token_type이 bearer였으므로, bearer token으로 설정
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
}
  1. 요청 후 결과 확인

4. 변경된 팀 Provider Id로 transfer

  1. requset 생성
    • 이 때 전송되는 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
}
  1. 요청 후 결과 확인
  • 000000.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.0000
  • 형식으로 생성되는데 이 때 맨 앞에 00000. 부분은 변경되지 않으므로
  • 이 부분을 확인

5. DB에 저장된 유저 provider Id 변경

  • 필자의 서비스는 provider ID로 유니크한 KEY를 만들어 사용하고 있었으므로 이 부분을 업데이트 해주었다.

참고 사이트

댓글 공유

서론

spring으로 된 프로젝트를 spring boot으로 전환 중… query method로 작성되어 있는 부분에서 발생한 문제에 대해 알아보고 이에 대한 해결 방법을 정리 해둔다.


본론

에러가 발생한 code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

public class XXXX {
...

private DDay dDay;

...
}

public interface Repository {
...

List<Category> findByDDayOrderByDDayStartDateAsc(DDay dday);

...
}

발생한 에러 전문

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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>

아래 처럼 치환해주면 정상적으로 작동하는 것을 확인 할 수 있을 것이다.


참고 사이트

댓글 공유

상황

평소와 같이 테스트 코드를 작성하고 실행을 했는데… 아래와 같이 에러가 발생하며 테스트가 진행되지 않았다.

발생한 에러


해결

위 이미지처럼 프로젝트 루트에 존재하는 .idea 폴더에 workspace.xml 파일을 열어서

1
2
3
<component name="PropertiesComponent">
<property name="dynamic.classpath" value="true" />
</component>

를 추가해주면 된다.

보통 이미 PropertiesComponent 태그는 존재 할 테니 그 아래 있는

1
<property name="dynamic.classpath" value="true" />

옵션만 정상적으로 넣어주면 된다.


보너스

dynamic.classpath 옵션이 뭔데 넣으면 되는 거에요?

이 질문에 대한 답은 역시 없는게 없는 스택오버 플로우에 이미 존재한다. What does the dynamic.classpath flag do? (IntelliJ project settings)

간단하게 정리하면 커맨드라인이나 파일을 통해 클래스 패스가 jvm에 전달되는 방식을 제어하는데 대부분의 os에선 최대 명령 줄 제한이 있어서 이 제한을 넘는 명령은 IDEA에서 실행 할 수 없기 때문에 명령이 32768 자 보다 길면 동적 클래스 패스로 전환 할 것을 제안하고 이 때 긴~ 클래스 패스는 파일에 기록된 다음, 응용 프로그램 관리자가 읽어서 시스템 클래스 로더를 통해 로드 된다는 것이다.

끝!


참고 사이트

댓글 공유

서론

어쩌다보니 학부생 시절 가장 싫어해 마지않던 안드로이드를 다시 공부하게 되었는데, 간단한 화면 하나 만들고도 실행이 안되서 삽질한 내용을 정리한다.


발생한 에러

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
2021-05-03 22:06:26.170 18752-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)

해결

결론부터 바로 말하면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

...

import androidx.appcompat.app.AppCompatActivity;

...

@SuppressLint(value = "StaticFieldLeak")
public class MainActivity extends AppCompatActivity {

private MemoDatabase db;
private List<MemoEntity> memoList;
private RecyclerView recyclerView;
private MemoDAO memoDAO;

...

되어 있던 코드를 AppCompatActivity 말고 Activity를 상속 받도록 변경해서 해결 했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...

import android.app.Activity;

...

@SuppressLint(value = "StaticFieldLeak")
public class MainActivity extends Activity {

private MemoDatabase db;
private List<MemoEntity> memoList;
private RecyclerView recyclerView;
private MemoDAO memoDAO;

...

여담

둘의 정확한 차이는 모르겠지만, AppCompatActivity가 이전 안드로이드버전까지 커버해주는 클래스이고, Activity는 최신버전만 지원한다고 하는데… 이 문제랑 무슨 상관이 있는지 필자의 좁은 식견으로는 알 수가 없었기에 문제 해결에 도움을 받은 스택오버플로우의 글을 링크하며 글을 마무리 한다. You need to use a Theme.AppCompat theme (or descendant) with this activity


참고 사이트

댓글 공유

서론

평소와 다름없이 유튜브를 배회하던 중 재난급 서버 장애내고 개발자 인생 끝날뻔 한 썰이란 영상을 보게 되었는데… ‘나는 지금 어떻게 하고 있지..?’ 라는 생각이 문득 들어서 얼른 나도 백업을 수시로 해둬야겠다는 생각에 적용한 mysql backup 방법을 남겨둔다.


왜 mysqldump로 했어요??

mysqldump는 스토리지 엔진에 관계없이 논리 백업을 수행 할 수 있는 도구다.
무엇을 이용해서 backup을 해야할까 고민 하던 중 장애와 관련된 XtraBackup 적용기를 보게 되었는데
데이터 사이즈가 크지 않다면 mysqldump를 사용하는 것이 간단하며 복원 시에도 신경 써야 할 것이 적다는 내용을 보고 mysqldump로 backup을 진행하기로 결정했다.

그럼 어떻게 해요??

mysqldump엔 여러가지 옵션들이 있지만 그 중 필자는 아래 처럼 사용 했다.

1
2
mysqldump --single-transaction --databases [db명] --tables [테이블명] -h [db주소] -u [username] -p | gzip > [파일명].gz

mysqldump option

  • –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로 작성하기

이제 이 명령어를 스케줄러에 등록하기 위해 우선 쉘 스크립트 파일로 작성해야 하는데 필자 같은 경우는 아래와 같이 작성해서 사용하고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash
HOST=host명
USER=user명
PASSWORD=password
DATABASE=db명
TABLES=(
table1
table2
table3
...
)

TABLES_STRING=''
for TABLE in "${TABLES[@]}"
do :
sudo mysqldump --single-transaction --databases ${DATABASE} --tables ${TABLE} --host ${HOST} --user ${USER} --password ${PASSWORD} | gzip > /home/backup/${TABLE}.sql.gz
done

이렇게 작성한 sh 파일을 bash 명령어로 실행하면 테이블 갯수만큼 비밀번호를 입력하면 백업 작업을 정상적으로 완료하게 된다… (정말?)


crontab을 자동 backup

이제 백업을 자동으로 실행하게 하기 위해서 리눅스에 존재하는 crontab을 이용하기로 한다.

crontab 등록 방법

리눅스 서버에 접속해서

1
crontab -l

명령어를 실행하면 아래 처럼 해당 유저로 서버에 등록된 스케줄들이 보이게 되는데 해당 유저로 등록된 서비스가 없다면 아래 처럼

1
2
user명@ip-127.0.0.1:~$ crontab -l
no crontab for user명

메시지가 출력되고 해당 유저로 등록된 서버스가 존재 할 경우엔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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이 열리게 된다.

어떤 식이든 crontab이 열리면

1
2
3
4
5
6
7
8
9
* * * * *  수행할 명령어
┬ ┬ ┬ ┬ ┬
│ │ │ │ │
│ │ │ │ │
│ │ │ │ └───────── 요일 (0 - 6) (0:일요일, 1:월요일, 2:화요일, …, 6:토요일)
│ │ │ └───────── 월 (1 - 12)
│ │ └───────── 일 (1 - 31)
│ └───────── 시 (0 - 23)
└───────── 분 (0 - 59)

위 형식에 맞춰 언제마다 실행 시킬 지 정한 후 우리가 만든 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

처럼 파일의 전체 경로를 지정하라고 설명하고 있다.

바뀐 sh파일 내용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
HOST=host명
USER=user명
DATABASE=db명
TABLES=(
table1
table2
table3
...
)

TABLES_STRING=''
for TABLE in "${TABLES[@]}"
do :
sudo mysqldump --single-transaction --databases ${DATABASE} --tables ${TABLE} --host ${HOST} --user ${USER} | gzip > /home/backup/${TABLE}.sql.gz
done

—password 옵션을 제외하고 password가 my.cnf 설정파일에 들어갔으므로 최종적인 sh 파일 내용은 위와 같이 된다.

결과

모든 난관을 극복하고 다시 crontab -e에 등록해서 스케줄을 실행한 결과 위와 같이 정상적으로 백업을 완료하여 파일 사이즈가 아까와 다르게 20 byte가 아닌 모습을 볼 수 있다.


P.S : 저는 그래도 안되는데요…?

필자의 경우에도 .my.cnf 파일을 생성하고 등록해줬음에도 불구하고 백업이 정상적으로 진행되지 않았는데 결국 찾은 해결 방법은 .my.cnf 파일을 따로 생성하지 않고, 아래와 같이 /etc/mysql/my.cnf 파일에 password를 추가하는 방법으로 해결 했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
...

# 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 의존성을 추가해주었는데..


그 후 삽질

1
2
3
4
5
6
7
<!-- https://mvnrepository.com/artifact/org.junit.vintage/junit-vintage-engine -->
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.7.1</version>
<scope>test</scope>
</dependency>

추가 해줘도 똑같은 에러만 반복 할 뿐이었다.

그래서 공식 문서를 살펴보니..

JUnit 5 = JUnit Platform + JUnit Jupiter + 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 프로젝트를 만들어서 하나씩 비교해보기로 했다.


해결

그 결과… .iml 파일에 아래 처럼 의존성들이 남아있는 것을 확인했고…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<orderEntry type="module-library" scope="TEST">
<library name="JUnit5.4">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter/5.4.2/junit-jupiter-5.4.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter-api/5.4.2/junit-jupiter-api-5.4.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/apiguardian/apiguardian-api/1.0.0/apiguardian-api-1.0.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/opentest4j/opentest4j/1.1.1/opentest4j-1.1.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/junit/platform/junit-platform-commons/1.4.2/junit-platform-commons-1.4.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter-params/5.4.2/junit-jupiter-params-5.4.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter-engine/5.4.2/junit-jupiter-engine-5.4.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/junit/platform/junit-platform-engine/1.4.2/junit-platform-engine-1.4.2.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter-api:5.7.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.apiguardian:apiguardian-api:1.1.0" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.opentest4j:opentest4j:1.2.0" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.junit.platform:junit-platform-commons:1.7.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter-engine:5.7.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.junit.platform:junit-platform-engine:1.7.1" level="project" />

모든 의존성을 삭제 후 다시 Junit5만 의존성 추가하여 테스트를 실행하니…

드디어 성공적인 테스트를 완료 할 수 있었다.

여담

project setting에서 분명 모듈 관련된 설정을 모두 삭제 했는데 왜 .iml파일에 설정이 삭제되지 않고 남아있는진 모르겠지만, 필자와 같은 상황이 발생하는 다른 사람들에게 도움이 되길 바라며 글을 마무리한다.

댓글 공유

문제

https://www.acmicpc.net/problem/2485


코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public static void main(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);
}

private static int gcd(int a, int b) {
return b!=0 ? gcd(b, a%b) : a;
}

흐름

  1. 입력 받은 값들을 뺀 값의 차를 구한 값으로 약수를 구하기 위한 differences,
    입력 받은 값을 그대로 저장하는 list 변수 생성.
  2. 두 리스트 모두 n 만큼 돌면서 값을 저장하는데
  3. differences의 경우 첫 번쨰 루프에서 값을 저장 할 수 없으므로(2 번쨰 값부터 첫 번째 값과 뺄 수 있으므로) 이전 값이 있는 경우에만 리스트에 저장한다.
  4. 가로수들 사이에 끼워넣어야 되는 가로수의 개수를 구해야 하므로 입력 받은 가로수의 위치를 정렬한다.
  5. 가로수 간의 최소 간격을 구하기 위해 아까 저장한 가로수 간의 차이를 구한 리스트 differences를 돌면서 유클리드 호제법을 이용해 최대공약수를 구하고, 구한 최대 공약수 중 가장 작은 값을 저장한다.
  6. 가로수 간의 차이값에서 최대 공약수를 구하는 이유는,
  7. 문제가 가로수를 일정한 간격으로 최대한 적게 심기게 하기 위한 문제 이므로 두 수에서 공통되는 값들 중 가장 큰 수인 최대 공약수를 구하고,
  8. 그 최대 공약수들 중 가장 작은 값이어야 모두 일정한 간격으로 심을 수 있으므로 최대 공약수 중 가장 작을 값을 구한다.
  9. 이렇게 잘 구한 값으로 이제 몇 개의 가로수를 더 심어야 하는지 알아내야 하므로
  10. 아까 정렬시킨 가로수 리스트를 반복 하면서 이전 가로수 와의 간격이 최대 공약수 보다 큰 녀석들이 있으면
  11. 그 녀석들 사이에 가로수를 심어야 되므로,
  12. 구한 간격을 최대 공약수로 나눈 값에서 -1 한 값을 더한다.
  13. 이 때 -1을 하는 이유는 두 수의 간격이므로 하나는 이미 심어져 있으니 -1 을 한다.
  14. 그렇게 추가로 심어야 하는 가로수의 개수를 모두 구하면
  15. 끝.

결과


댓글 공유

문제

https://www.acmicpc.net/problem/10527


코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public static void main(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);
}

domJudge.put(input, domJudge.getOrDefault(input, 0) + 1);
}

Map<String, Integer> kattis = new HashMap<>();

for (int i = 0; i < n; i++) {
String input = br.readLine();

if (isDuplicate.containsKey(input)) {
isDuplicate.put(input, true);
}

kattis.put(input, kattis.getOrDefault(input, 0) + 1);
}

List<Map.Entry<String, Integer>> entries1 = new ArrayList<>(domJudge.entrySet());
List<Map.Entry<String, Integer>> entries2 = new ArrayList<>(kattis.entrySet());

entries1.addAll(entries2);
Collections.sort(entries1, (e1, e2) -> {
if (e1.getKey().compareTo(e2.getKey()) > 0) {
return 1;
} else if (e1.getKey().compareTo(e2.getKey()) < 0) {
return -1;
} else {
return Integer.compare(e1.getValue(), e2.getValue());
}
});

int answer = 0;
Set<String> set = new HashSet<>();
for (int i = 0; i < entries1.size(); i++) {
Map.Entry<String, Integer> entry = entries1.get(i);

if (isDuplicate.getOrDefault(entry.getKey(), false) && set.add(entry.getKey())) {
answer += entry.getValue();
}
}

System.out.println(answer);
}

흐름

  • 우선 이 문제는 domJudge와 kattis가 순서대로 n만큼씩 입력 받는 문제다.

예제를 보며 얘기하면

1
2
3
4
5
6
7
8
9
10
11
5
correct
wronganswer
correct
correct
timelimit
wronganswer
correct
timelimit
correct
timelimit

이렇게 되어 있을 때 위에 다섯 줄은 domJudge에서 채점한 결과고, 그 아래 다섯 줄은 kattis 채점한 결과다.

이걸 생각하고 프로그램을 작성하면

  1. 우선 양 쪽에서 채점된 결과들 중에 작은 녀석의 값을 골라야 하므로 채점 결과를 Key로 하는 Map을 만들고
  2. n 만큼 돌면서 domJudge의 채점 결과를 저장하는데 이 때 처음나온 채점 결과는 Map에 false로 저장하고, 이전에 이미 나온 결과는 +1 해준다.
  3. 그 후 다시 n만큼 돌면서 kattis의 채점결과를 저장하는데
  4. 이 때는 전에 domJudge에서 나온 채점 결과 인지 확인해야 하므로 중복을 체크하기 위해 만든 Map에 채점 결과가 존재하면 그 key의 값을 true로 변경시켜서 양 쪽에서 모두 나온 결과 임을 저장한다.
  5. 그 후 양 쪽에서 모두 나온 채점 결과들 중 작은 값들을 더하기 위해
  6. 두 Map을 리스트로 변환 후 합쳐서 key와 value로 sorting 한다.
  7. 그 후 합친 리스트를 반복하면서..
  8. 채점 결과가 양 쪽 모두에서 나온 녀석이면서 이전에 더한 key가 아니라면 쭉 더하고
  9. 출력하고 끝낸다.

결과


다른 분의 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line = br.readLine().trim();
int n = Integer.parseInt(line);
Map<String, Integer> map = new HashMap<>();
for(int i = 0; i < n; i++){
String dom = br.readLine();
map.put(dom, map.getOrDefault(dom, 0) + 1);
}
int cnt = 0;
for(int i = 0; i < n; i++){
String kattis = br.readLine();
if(map.containsKey(kattis) && map.get(kattis) > 0){
cnt++;
map.put(kattis, map.get(kattis) - 1);
}
}
System.out.println(cnt);
}
  • 나 처럼 바보 같이 Map을 여러 개 쓰지 않고 하나의 맵에 +1, -1로 해서 겹치는 결과 인지 아닌지 판단했다.
  • 나는 왜 이렇게 생각하지 못했을까? 다른 곳에서도 흔히 쓰이는 방식인데
  • 오늘도 역시 반성해본다…

댓글 공유

문제

https://www.acmicpc.net/problem/1417


코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static void main(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. 다솜이는 기호 1번이므로 첫 번째 입력을 다솜이의 표로 저장한다.
  2. 우선순위 큐로 사용하기 위해 list에 입력들을 저장하는데 다솜이 보다 작을 경우엔 문제의 답과 상관이 없으므로 다솜이의 표보다 적은 표는 큐에 저장하지 않는다.
  3. 우선순위 큐로서 동작하게 하기 위해 반복 전에 큐를 한번 정렬시켜준다.
  4. 그 후엔 큐가 비거나, 큐의 가장 큰 값이 다솜이가 될 때까지 반복한다.
  5. 반복하면서 큐의 맨 앞이 다솜이보다 작다면, 이제 그 사람은 다솜이가 재낀 것 이므로 큐에서 빼주고,
  6. 제일 표가 많은 놈한테서 표 하나를 뻇어서 다솜이가 가져가면 되므로 큐의 마지막 값에서 -1 빼서 다시 큐에 넣어준다.
  7. 그리고 제일 표가 많은 놈에게 뺏은 표를 다솜이에게 줬으므로 다솜이의 값을 1 증가시키고
  8. 우선순위 큐 처럼 동작 할 수 있게 큐를 다시 정렬 시킨다.
  9. 반복된 횟수를 출력하면 끝.

결과


여담

반복문에서 소팅을 하기 때문에 시간 초과가 날 것으로 예상했는데 수가 적은 문제라 그런지 무사히 통과되어 참 다행이다…

댓글 공유

Junggu Ji

author.bio


author.job